"""Step definitions for the UKO Analyzer Plugin Framework feature."""

from __future__ import annotations

from typing import Any

from behave import given, then, when

from cleveragents.domain.models.acms.analyzers import (
    AnalyzerProtocol,
    AnalyzerRegistry,
    UKOTriple,
)
from cleveragents.domain.models.acms.markdown_analyzer import MarkdownAnalyzer
from cleveragents.domain.models.acms.python_analyzer import PythonAnalyzer

__all__: list[str] = []


# ---------------------------------------------------------------------------
# UKOTriple steps
# ---------------------------------------------------------------------------


@given('a UKOTriple with subject "{subj}" predicate "{pred}" and object_uri "{obj}"')
def step_given_triple_with_obj_uri(
    context: Any, subj: str, pred: str, obj: str
) -> None:
    context.triple = UKOTriple(subject_uri=subj, predicate=pred, object_uri=obj)


@given('a UKOTriple with subject "{subj}" predicate "{pred}" and object_value "{val}"')
def step_given_triple_with_obj_value(
    context: Any, subj: str, pred: str, val: str
) -> None:
    context.triple = UKOTriple(subject_uri=subj, predicate=pred, object_value=val)


@then('the triple subject_uri should be "{expected}"')
def step_then_triple_subject(context: Any, expected: str) -> None:
    assert context.triple.subject_uri == expected


@then('the triple predicate should be "{expected}"')
def step_then_triple_predicate(context: Any, expected: str) -> None:
    assert context.triple.predicate == expected


@then('the triple object_uri should be "{expected}"')
def step_then_triple_object_uri(context: Any, expected: str) -> None:
    assert context.triple.object_uri == expected


@then('the triple object_value should be "{expected}"')
def step_then_triple_object_value(context: Any, expected: str) -> None:
    assert context.triple.object_value == expected


@then("the triple confidence should be {expected:g}")
def step_then_triple_confidence(context: Any, expected: float) -> None:
    assert context.triple.confidence == expected


@then("creating a UKOTriple with empty subject_uri should raise ValueError")
def step_then_triple_empty_subject(context: Any) -> None:
    try:
        UKOTriple(subject_uri="", predicate="rdf:type", object_uri="uko:X")
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


@then("creating a UKOTriple with empty predicate should raise ValueError")
def step_then_triple_empty_predicate(context: Any) -> None:
    try:
        UKOTriple(subject_uri="uko://a", predicate="", object_uri="uko:X")
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


@then("creating a UKOTriple with confidence {score:g} should raise ValueError")
def step_then_triple_bad_confidence(context: Any, score: float) -> None:
    try:
        UKOTriple(
            subject_uri="uko://a",
            predicate="rdf:type",
            object_uri="uko:X",
            confidence=score,
        )
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


@then("modifying the triple confidence should raise an error")
def step_then_triple_immutable(context: Any) -> None:
    try:
        context.triple.confidence = 0.5  # type: ignore[misc]
        raise AssertionError("Expected frozen error")
    except (TypeError, ValueError, AttributeError):
        pass


# ---------------------------------------------------------------------------
# AnalyzerRegistry steps
# ---------------------------------------------------------------------------


@given("an AnalyzerRegistry with a PythonAnalyzer registered")
def step_given_registry_with_python(context: Any) -> None:
    context.registry = AnalyzerRegistry()
    context.registry.register(PythonAnalyzer())


@given("an empty AnalyzerRegistry")
def step_given_empty_registry(context: Any) -> None:
    context.registry = AnalyzerRegistry()


@given("an AnalyzerRegistry with both Python and Markdown analyzers")
def step_given_registry_with_both(context: Any) -> None:
    context.registry = AnalyzerRegistry()
    context.registry.register(PythonAnalyzer())
    context.registry.register(MarkdownAnalyzer())


@when('I look up the analyzer for extension "{ext}"')
def step_when_lookup_extension(context: Any, ext: str) -> None:
    context.lookup_result = context.registry.get_for_extension(ext)


@when('I register another analyzer for "{ext}"')
def step_when_register_another(context: Any, ext: str) -> None:
    """Register a second MarkdownAnalyzer that also claims .py."""

    class _DummyAnalyzer:
        @property
        def supported_extensions(self) -> frozenset[str]:
            return frozenset({ext})

        @property
        def domain(self) -> str:
            return "dummy"

        def analyze(self, content: str, resource_uri: str) -> list[UKOTriple]:
            return []

    context.registry.register(_DummyAnalyzer())


@then('the analyzer domain should be "{expected}"')
def step_then_analyzer_domain(context: Any, expected: str) -> None:
    assert context.lookup_result is not None
    assert context.lookup_result.domain == expected


@then("the lookup result should be None")
def step_then_lookup_none(context: Any) -> None:
    assert context.lookup_result is None


@then("registering a plain object should raise TypeError")
def step_then_register_plain_object(context: Any) -> None:
    try:
        context.registry.register("not an analyzer")  # type: ignore[arg-type]
        raise AssertionError("Expected TypeError")
    except TypeError:
        pass


@then("registering an analyzer with no extensions should raise ValueError")
def step_then_register_no_extensions(context: Any) -> None:
    class _Empty:
        @property
        def supported_extensions(self) -> frozenset[str]:
            return frozenset()

        @property
        def domain(self) -> str:
            return "empty"

        def analyze(self, content: str, resource_uri: str) -> list[UKOTriple]:
            return []

    try:
        context.registry.register(_Empty())
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


@then('the registry should list extensions including "{ext1}" and "{ext2}"')
def step_then_list_extensions(context: Any, ext1: str, ext2: str) -> None:
    exts = context.registry.list_extensions()
    assert ext1 in exts, f"{ext1} not in {exts}"
    assert ext2 in exts, f"{ext2} not in {exts}"


@then("the registry length should be {expected:d}")
def step_then_registry_length(context: Any, expected: int) -> None:
    assert len(context.registry) == expected


# ---------------------------------------------------------------------------
# PythonAnalyzer steps
# ---------------------------------------------------------------------------


@given("a PythonAnalyzer instance")
def step_given_python_analyzer(context: Any) -> None:
    context.analyzer = PythonAnalyzer()


@then("it should satisfy the AnalyzerProtocol")
def step_then_satisfies_protocol(context: Any) -> None:
    assert isinstance(context.analyzer, AnalyzerProtocol)


@then('its supported extensions should include "{ext1}" and "{ext2}"')
def step_then_supported_extensions(context: Any, ext1: str, ext2: str) -> None:
    exts = context.analyzer.supported_extensions
    assert ext1 in exts, f"{ext1} not in {exts}"
    assert ext2 in exts, f"{ext2} not in {exts}"


@when('I analyze Python content "{content}" with resource URI "{uri}"')
def step_when_analyze_python(context: Any, content: str, uri: str) -> None:
    context.triples = context.analyzer.analyze(content.replace("\\n", "\n"), uri)


@when('I analyze Python content with a class "{cls_name}"')
def step_when_analyze_python_class(context: Any, cls_name: str) -> None:
    code = f"class {cls_name}:\n    pass\n"
    context.triples = context.analyzer.analyze(code, "src/test.py")


@when('I analyze Python content with a function "{func_name}"')
def step_when_analyze_python_function(context: Any, func_name: str) -> None:
    code = f"def {func_name}():\n    pass\n"
    context.triples = context.analyzer.analyze(code, "src/test.py")


@when('I analyze Python content with class "{cls_name}" and method "{method_name}"')
def step_when_analyze_python_method(
    context: Any, cls_name: str, method_name: str
) -> None:
    code = f"class {cls_name}:\n    def {method_name}(self):\n        pass\n"
    context.triples = context.analyzer.analyze(code, "src/test.py")


@when('I analyze Python content with import "{mod_name}"')
def step_when_analyze_python_import(context: Any, mod_name: str) -> None:
    code = f"import {mod_name}\n"
    context.triples = context.analyzer.analyze(code, "src/test.py")


@when('I analyze Python content with from-import "{mod_name}"')
def step_when_analyze_python_from_import(context: Any, mod_name: str) -> None:
    code = f"from {mod_name} import Path\n"
    context.triples = context.analyzer.analyze(code, "src/test.py")


@when('I analyze Python content with docstring "{docstring}"')
def step_when_analyze_python_docstring(context: Any, docstring: str) -> None:
    code = f'"""{docstring}"""\nx = 1\n'
    context.triples = context.analyzer.analyze(code, "src/test.py")


@when("I analyze Python content with class docstring")
def step_when_analyze_python_class_docstring(context: Any) -> None:
    code = 'class Foo:\n    """Class doc."""\n    pass\n'
    context.triples = context.analyzer.analyze(code, "src/test.py")


@when("I analyze malformed Python content")
def step_when_analyze_malformed_python(context: Any) -> None:
    context.triples = context.analyzer.analyze("def (\n    broken", "src/bad.py")


@when('I analyze Python content with a class inheriting from "{base}"')
def step_when_analyze_python_inheritance(context: Any, base: str) -> None:
    code = f"class Child({base}):\n    pass\n"
    context.triples = context.analyzer.analyze(code, "src/test.py")


@then(
    'the triples should contain a triple with predicate "{pred}" and object_uri "{obj}"'
)
def step_then_triples_contain_pred_obj(context: Any, pred: str, obj: str) -> None:
    found = any(t.predicate == pred and t.object_uri == obj for t in context.triples)
    assert found, (
        f"No triple with predicate={pred!r} object_uri={obj!r} in {context.triples}"
    )


@then(
    'the triples should contain a triple with predicate "{pred}" and object_value "{val}"'
)
def step_then_triples_contain_pred_val(context: Any, pred: str, val: str) -> None:
    found = any(t.predicate == pred and t.object_value == val for t in context.triples)
    assert found, (
        f"No triple with predicate={pred!r} object_value={val!r} in {context.triples}"
    )


@then(
    "the triples should contain a triple with predicate"
    ' "uko:contains" linking module to class'
)
def step_then_triples_contain_module_contains_class(
    context: Any,
) -> None:
    found = any(
        t.predicate == "uko:contains"
        and "module" in t.subject_uri
        and "class" in t.object_uri
        for t in context.triples
    )
    assert found, f"No module-contains-class triple in {context.triples}"


@then(
    'the triples should contain a method triple with predicate "{pred}" and object_value "{val}"'
)
def step_then_triples_contain_method(context: Any, pred: str, val: str) -> None:
    found = any(t.predicate == pred and t.object_value == val for t in context.triples)
    assert found, (
        f"No method triple with predicate={pred!r} val={val!r} in {context.triples}"
    )


@then(
    'the triples should contain a triple with predicate "uko:references" referencing "{mod_name}"'
)
def step_then_triples_contain_import_ref(context: Any, mod_name: str) -> None:
    found = any(
        t.predicate == "uko:references" and mod_name in t.object_uri
        for t in context.triples
    )
    assert found, f"No import reference to {mod_name!r} in {context.triples}"


@then("the triples should contain a docstring triple for the class")
def step_then_triples_contain_class_docstring(context: Any) -> None:
    found = any(
        t.predicate == "uko-doc:hasDocstring" and "class" in t.subject_uri.lower()
        for t in context.triples
    )
    assert found, f"No class docstring triple in {context.triples}"


@then("the analyzer result should be an empty triple list")
def step_then_result_empty(context: Any) -> None:
    assert context.triples == []


@then("analyzing empty content should raise ValueError")
def step_then_analyze_empty_content(context: Any) -> None:
    try:
        context.analyzer.analyze("", "src/test.py")
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


@then("analyzing with empty resource_uri should raise ValueError")
def step_then_analyze_empty_uri(context: Any) -> None:
    try:
        context.analyzer.analyze("x = 1\n", "")
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


# ---------------------------------------------------------------------------
# MarkdownAnalyzer steps
# ---------------------------------------------------------------------------


@given("a MarkdownAnalyzer instance")
def step_given_markdown_analyzer(context: Any) -> None:
    context.analyzer = MarkdownAnalyzer()


@when('I analyze Markdown content "{content}" with resource URI "{uri}"')
def step_when_analyze_markdown(context: Any, content: str, uri: str) -> None:
    context.triples = context.analyzer.analyze(content.replace("\\n", "\n"), uri)


@when('I analyze Markdown content with heading "{heading}"')
def step_when_analyze_markdown_heading(context: Any, heading: str) -> None:
    content = f"# {heading}\n\nSome text here.\n"
    context.triples = context.analyzer.analyze(content, "docs/test.md")


@when("I analyze Markdown with nested headings")
def step_when_analyze_markdown_nested(context: Any) -> None:
    content = "# Top\n\n## Sub\n\nBody text.\n"
    context.triples = context.analyzer.analyze(content, "docs/test.md")


@when("I analyze Markdown content with a Python code block")
def step_when_analyze_markdown_codeblock(context: Any) -> None:
    content = "# Code\n\n```python\nprint('hello')\n```\n"
    context.triples = context.analyzer.analyze(content, "docs/test.md")


@when('I analyze Markdown content with a link to "{url}"')
def step_when_analyze_markdown_link(context: Any, url: str) -> None:
    content = f"See [example]({url}) for details.\n"
    context.triples = context.analyzer.analyze(content, "docs/test.md")


@when("I analyze Markdown content with a bare code fence")
def step_when_analyze_markdown_bare_fence(context: Any) -> None:
    content = "# Title\n\n```\nsome code\n```\n"
    context.triples = context.analyzer.analyze(content, "docs/test.md")


@then("the triples should contain section containment for nested sections")
def step_then_nested_containment(context: Any) -> None:
    # Document should contain Top section, Top section should contain Sub
    doc_contains = [
        t
        for t in context.triples
        if t.predicate == "uko:contains"
        and "document" in t.subject_uri
        and "section" in t.object_uri
    ]
    section_contains = [
        t
        for t in context.triples
        if t.predicate == "uko:contains"
        and "section" in t.subject_uri
        and "section" in t.object_uri
    ]
    assert len(doc_contains) >= 1, f"Expected doc-contains-section, got {doc_contains}"
    assert len(section_contains) >= 1, (
        f"Expected section-contains-section, got {section_contains}"
    )


@then("the triples should contain a code block triple")
def step_then_contains_codeblock(context: Any) -> None:
    found = any(
        t.predicate == "rdf:type" and t.object_uri == "uko-code:CodeBlock"
        for t in context.triples
    )
    assert found, f"No code block triple in {context.triples}"


@then("analyzing empty markdown content should raise ValueError")
def step_then_md_analyze_empty_content(context: Any) -> None:
    try:
        context.analyzer.analyze("", "docs/test.md")
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


@then("analyzing markdown with empty resource_uri should raise ValueError")
def step_then_md_analyze_empty_uri(context: Any) -> None:
    try:
        context.analyzer.analyze("# Title\n", "")
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


# ---------------------------------------------------------------------------
# Coverage-gap steps
# ---------------------------------------------------------------------------


@when("I look up the analyzer for an empty extension")
def step_when_lookup_empty_extension(context: Any) -> None:
    context.lookup_result = context.registry.get_for_extension("")


@then("the registry list_analyzers should return {expected:d} analyzers")
def step_then_list_analyzers(context: Any, expected: int) -> None:
    result = context.registry.list_analyzers()
    assert len(result) == expected, f"Expected {expected} analyzers, got {len(result)}"


@when("I analyze Python content with a function with docstring")
def step_when_analyze_python_func_docstring(context: Any) -> None:
    code = 'def greet():\n    """Say hello."""\n    pass\n'
    context.triples = context.analyzer.analyze(code, "src/test.py")


@then(
    'the triples should contain a triple with predicate "uko-doc:hasDocstring" for a function'
)
def step_then_triples_func_docstring(context: Any) -> None:
    found = any(
        t.predicate == "uko-doc:hasDocstring" and "function" in t.subject_uri
        for t in context.triples
    )
    assert found, f"No function docstring triple in {context.triples}"


@when("I analyze Python content with class and method with docstring")
def step_when_analyze_python_method_docstring(context: Any) -> None:
    code = (
        "class Svc:\n"
        "    def run(self):\n"
        '        """Execute the service."""\n'
        "        pass\n"
    )
    context.triples = context.analyzer.analyze(code, "src/test.py")


@then(
    'the triples should contain a triple with predicate "uko-doc:hasDocstring" for a method'
)
def step_then_triples_method_docstring(context: Any) -> None:
    found = any(
        t.predicate == "uko-doc:hasDocstring"
        and "function" in t.subject_uri
        and "Svc" in t.subject_uri
        for t in context.triples
    )
    assert found, f"No method docstring triple in {context.triples}"


@then('the analyzer domain property should be "{expected}"')
def step_then_analyzer_domain_prop(context: Any, expected: str) -> None:
    assert context.analyzer.domain == expected


@when("I analyze Markdown with sibling headings at the same level")
def step_when_analyze_md_sibling_headings(context: Any) -> None:
    content = "# A\n\n## B\n\n## C\n\nText\n"
    context.triples = context.analyzer.analyze(content, "docs/test.md")


@then("each sibling section should be contained by the document")
def step_then_sibling_sections_contained_by_doc(context: Any) -> None:
    # Sections B and C are both at level 2 under A; the second ## pops the
    # first ## off the stack.  Both should be contained by the # A section
    # (which is contained by the document).
    section_contains = [
        t
        for t in context.triples
        if t.predicate == "uko:contains" and "section" in t.object_uri
    ]
    assert len(section_contains) >= 3, (
        f"Expected at least 3 contains-section triples, got {section_contains}"
    )
