"""Behave step implementations for UKO loader coverage-boost scenarios.

Targets uncovered lines in src/cleveragents/application/services/uko_loader.py:
  87-90, 101-102, 181, 249-259, 310, 361, 363, 370, 443, 451, 457, 469, 472,
  517, 522, 531.
"""

from __future__ import annotations

import tempfile
from pathlib import Path

from behave import given, then, when
from behave.runner import Context

from cleveragents.application.services.uko_loader import (
    UKOLoader,
    UKOValidationError,
    _detect_layer,
    _extract_iri,
)
from cleveragents.domain.models.core.uko import (
    UKONode,
    UKOOntology,
    UKOPrefix,
    UKOVersion,
)

# ── helpers ────────────────────────────────────────────────────────────────


def _make_version(ver: str = "0.1.0") -> UKOVersion:
    return UKOVersion(
        version_iri=f"https://cleveragents.ai/ontology/uko/{ver}",
        version=ver,
        label="Test",
        comment="Test",
    )


def _make_prefix(prefix: str = "uko") -> UKOPrefix:
    return UKOPrefix(
        prefix=prefix,
        uri="https://cleveragents.ai/ontology/uko#",
    )


# ── Given ──────────────────────────────────────────────────────────────────


@given('a full IRI "{uri}"')
def step_given_full_iri(context: Context, uri: str) -> None:
    context.uri = uri


@given('a value string "{value}"')
def step_given_value_string(context: Context, value: str) -> None:
    context.value_string = value


@given("a TTL string with a property whose value starts with http:")
@given("a TTL string with a property whose value starts with http::")
def step_given_ttl_http_property(context: Context) -> None:
    context.ttl_text = context.text


@given("a UKO ontology with a prefixed parent referencing an undefined prefix")
def step_given_undefined_prefix_parent(context: Context) -> None:
    context.ontology = UKOOntology(
        version=_make_version(),
        prefixes=(_make_prefix("uko"),),
        nodes=(
            UKONode(
                uri="https://cleveragents.ai/ontology/uko#Child",
                rdf_type="owl:Class",
                label="Child",
                parent_uris=("nope:DoesNotExist",),
                layer=0,
            ),
        ),
    )
    context.loader = UKOLoader()


@given("a UKO ontology with a prefixed parent referencing a non-existent node")
def step_given_nonexistent_prefixed_parent(context: Context) -> None:
    context.ontology = UKOOntology(
        version=_make_version(),
        prefixes=(_make_prefix("uko"),),
        nodes=(
            UKONode(
                uri="https://cleveragents.ai/ontology/uko#Child",
                rdf_type="owl:Class",
                label="Child",
                parent_uris=("uko:Ghost",),
                layer=0,
            ),
        ),
    )
    context.loader = UKOLoader()


@given("a UKO ontology where a node has an external parent URI")
def step_given_external_parent(context: Context) -> None:
    context.ontology = UKOOntology(
        version=_make_version(),
        prefixes=(_make_prefix("uko"),),
        nodes=(
            UKONode(
                uri="https://cleveragents.ai/ontology/uko#Leaf",
                rdf_type="owl:Class",
                label="Leaf",
                # Parent points to a URI that does NOT exist in nodes
                parent_uris=("https://example.org/external#Root",),
                layer=0,
            ),
        ),
    )
    context.loader = UKOLoader()


@given("a UKO ontology with diamond inheritance shape")
def step_given_diamond(context: Context) -> None:
    """Diamond: D -> B, D -> C, B -> A, C -> A."""
    ns = "https://cleveragents.ai/ontology/uko#"
    context.ontology = UKOOntology(
        version=_make_version(),
        prefixes=(_make_prefix("uko"),),
        nodes=(
            UKONode(
                uri=f"{ns}A",
                rdf_type="owl:Class",
                label="A",
                layer=0,
            ),
            UKONode(
                uri=f"{ns}B",
                rdf_type="owl:Class",
                label="B",
                parent_uris=(f"{ns}A",),
                layer=0,
            ),
            UKONode(
                uri=f"{ns}C",
                rdf_type="owl:Class",
                label="C",
                parent_uris=(f"{ns}A",),
                layer=0,
            ),
            UKONode(
                uri=f"{ns}D",
                rdf_type="owl:Class",
                label="D",
                parent_uris=(f"{ns}B", f"{ns}C"),
                layer=0,
            ),
        ),
    )
    context.loader = UKOLoader()


@given("a UKO ontology where a node is its own parent")
def step_given_self_cycle(context: Context) -> None:
    ns = "https://cleveragents.ai/ontology/uko#"
    context.ontology = UKOOntology(
        version=_make_version(),
        prefixes=(_make_prefix("uko"),),
        nodes=(
            UKONode(
                uri=f"{ns}SelfRef",
                rdf_type="owl:Class",
                label="SelfRef",
                parent_uris=(f"{ns}SelfRef",),
                layer=0,
            ),
        ),
    )
    context.loader = UKOLoader()


@given("a UKO ontology with some nodes")
def step_given_some_nodes(context: Context) -> None:
    ns = "https://cleveragents.ai/ontology/uko#"
    context.ontology = UKOOntology(
        version=_make_version(),
        prefixes=(_make_prefix("uko"),),
        nodes=(
            UKONode(
                uri=f"{ns}Existing",
                rdf_type="owl:Class",
                label="Existing",
                layer=0,
            ),
        ),
    )
    context.loader = UKOLoader()


@given("a TTL string with dots in bare subject names:")
def step_given_ttl_dots_in_bare_names(context: Context) -> None:
    context.ttl_text = context.text


@given("a TTL string without trailing period:")
def step_given_ttl_no_trailing_period(context: Context) -> None:
    context.ttl_text = context.text


@given("a TTL string with a stray single token:")
def step_given_ttl_stray_token(context: Context) -> None:
    context.ttl_text = context.text


@given("a TTL string with trailing semicolons and bare keywords:")
def step_given_ttl_trailing_semicolons(context: Context) -> None:
    context.ttl_text = context.text


@given("a UKO loader instance")
def step_given_loader_instance(context: Context) -> None:
    context.loader = UKOLoader()


@given("a temporary TTL file with valid content")
def step_given_temp_ttl_file(context: Context) -> None:
    ttl_content = (
        "@prefix uko: <https://cleveragents.ai/ontology/uko#> .\n"
        "@prefix owl: <http://www.w3.org/2002/07/owl#> .\n"
        "@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n"
        "\n"
        "uko: a owl:Ontology ;\n"
        "    owl:versionIRI <https://cleveragents.ai/ontology/uko/0.1.0> ;\n"
        '    rdfs:label "Temp" ;\n'
        '    rdfs:comment "Temp test" .\n'
        "\n"
        "uko:Widget a owl:Class ;\n"
        '    rdfs:label "Widget" .\n'
    )
    with tempfile.NamedTemporaryFile(
        mode="w",
        suffix=".ttl",
        delete=False,
        encoding="utf-8",
    ) as tmp:
        tmp.write(ttl_content)
    context.ttl_tmp_path = Path(tmp.name)
    context.loader = UKOLoader()


@given("a TTL string using rdf:type for ontology declaration:")
def step_given_rdf_type_ontology(context: Context) -> None:
    context.ttl_text = context.text


# ── When ───────────────────────────────────────────────────────────────────


@when("I detect the layer for that URI")
def step_when_detect_layer(context: Context) -> None:
    context.detected_layer = _detect_layer(context.uri)


@when("I extract the IRI from that value")
def step_when_extract_iri(context: Context) -> None:
    context.extracted_iri = _extract_iri(context.value_string)


@when("I parse the TTL coverage string")
def step_when_parse_ttl_coverage(context: Context) -> None:
    loader = UKOLoader()
    context.parsed_ontology = loader.load_from_string(context.ttl_text)
    context.loader = loader


@when("I run validation on the ontology")
def step_when_run_validation(context: Context) -> None:
    loader: UKOLoader = context.loader
    context.validation_errors = loader.validate(context.ontology)


@when("I resolve the inheritance chain for the node with external parent")
def step_when_resolve_external_parent(context: Context) -> None:
    loader: UKOLoader = context.loader
    context.inheritance_chain = loader.resolve_inheritance(
        context.ontology,
        "https://cleveragents.ai/ontology/uko#Leaf",
    )


@when("I resolve the inheritance chain for the diamond leaf")
def step_when_resolve_diamond_leaf(context: Context) -> None:
    loader: UKOLoader = context.loader
    context.inheritance_chain = loader.resolve_inheritance(
        context.ontology,
        "https://cleveragents.ai/ontology/uko#D",
    )


@when("I attempt to resolve inheritance for the self-referencing node")
def step_when_resolve_self_cycle(context: Context) -> None:
    loader: UKOLoader = context.loader
    try:
        loader.resolve_inheritance(
            context.ontology,
            "https://cleveragents.ai/ontology/uko#SelfRef",
        )
        context.cycle_error = None
    except UKOValidationError as exc:
        context.cycle_error = str(exc)


@when("I resolve the inheritance chain for a URI not in the ontology")
def step_when_resolve_missing_uri(context: Context) -> None:
    loader: UKOLoader = context.loader
    context.inheritance_chain = loader.resolve_inheritance(
        context.ontology,
        "https://cleveragents.ai/ontology/uko#NoSuchNode",
    )


@when('I resolve the prefixed URI "{uri}" with an empty prefix map')
def step_when_resolve_prefixed_uri(context: Context, uri: str) -> None:
    context.resolved_uri = context.loader._resolve_uri(uri, {})


@when("I extract version string from an empty IRI")
def step_when_extract_empty_version(context: Context) -> None:
    context.extracted_version = context.loader._extract_version_string("")


@when('I extract version string from "{iri}"')
def step_when_extract_version(context: Context, iri: str) -> None:
    context.extracted_version = context.loader._extract_version_string(iri)


@when("I load the ontology from the temporary file")
def step_when_load_from_file(context: Context) -> None:
    context.loaded_ontology = context.loader.load(context.ttl_tmp_path)


# ── Then ───────────────────────────────────────────────────────────────────


@then("the detected layer should be {layer:d}")
def step_then_detected_layer(context: Context, layer: int) -> None:
    assert context.detected_layer == layer, (
        f"Expected layer {layer}, got {context.detected_layer}"
    )


@then('the extracted IRI should be "{expected}"')
def step_then_extracted_iri(context: Context, expected: str) -> None:
    assert context.extracted_iri == expected, (
        f"Expected '{expected}', got '{context.extracted_iri}'"
    )


@then('the parsed ontology should contain a node labeled "{label}"')
def step_then_parsed_has_node(context: Context, label: str) -> None:
    labels = [n.label for n in context.parsed_ontology.nodes]
    assert label in labels, f"Label '{label}' not found in {labels}"


@then('node "{label}" property "{prop}" should equal "{expected}"')
def step_then_node_prop_equals(
    context: Context,
    label: str,
    prop: str,
    expected: str,
) -> None:
    matching = [n for n in context.parsed_ontology.nodes if n.label == label]
    assert matching, f"No node labeled '{label}'"
    prop_dict = dict(matching[0].properties)
    assert prop in prop_dict, (
        f"Property '{prop}' not on '{label}'; have {list(prop_dict)}"
    )
    assert prop_dict[prop] == expected, (
        f"Expected '{expected}', got '{prop_dict[prop]}'"
    )


@then('the validation result should mention "{text}"')
def step_then_validation_mentions(context: Context, text: str) -> None:
    combined = " ".join(context.validation_errors)
    assert text in combined, f"Expected '{text}' in errors: {context.validation_errors}"


@then("the chain should contain the starting node only")
def step_then_chain_single(context: Context) -> None:
    assert len(context.inheritance_chain) == 1, (
        f"Expected 1 node, got {len(context.inheritance_chain)}"
    )
    assert context.inheritance_chain[0].label == "Leaf"


@then("the chain should include each ancestor at most once")
def step_then_chain_no_duplicates(context: Context) -> None:
    uris = [n.uri for n in context.inheritance_chain]
    assert len(uris) == len(set(uris)), f"Duplicate nodes found: {uris}"
    # Diamond D->B,C->A  => chain should be D, B, C, A (4 unique)
    assert len(uris) == 4, f"Expected 4 nodes in diamond, got {len(uris)}: {uris}"


@then("a cycle error should be raised for that node")
def step_then_self_cycle_error(context: Context) -> None:
    assert context.cycle_error is not None, "Expected cycle error but got none"
    assert "Cycle" in context.cycle_error or "cycle" in context.cycle_error, (
        f"Error message does not mention cycle: {context.cycle_error}"
    )


@then("the chain should be empty")
def step_then_chain_empty(context: Context) -> None:
    assert len(context.inheritance_chain) == 0, (
        f"Expected empty chain, got {len(context.inheritance_chain)}"
    )


@then('the parsed ontology version should be "{version}"')
def step_then_parsed_version(context: Context, version: str) -> None:
    assert context.parsed_ontology.version.version == version, (
        f"Expected '{version}', got '{context.parsed_ontology.version.version}'"
    )


@then('the resolved URI should be "{expected}"')
def step_then_resolved_uri(context: Context, expected: str) -> None:
    assert context.resolved_uri == expected, (
        f"Expected '{expected}', got '{context.resolved_uri}'"
    )


@then('the extracted version should be "{expected}"')
def step_then_extracted_version(context: Context, expected: str) -> None:
    assert context.extracted_version == expected, (
        f"Expected '{expected}', got '{context.extracted_version}'"
    )


@then('the extracted version should be ""')
def step_then_extracted_version_empty(context: Context) -> None:
    assert context.extracted_version == "", (
        f"Expected empty string, got '{context.extracted_version}'"
    )


@then('the loaded ontology should have version "{version}"')
def step_then_loaded_version(context: Context, version: str) -> None:
    assert context.loaded_ontology.version.version == version, (
        f"Expected '{version}', got '{context.loaded_ontology.version.version}'"
    )


@then('the parsed ontology version label should be "{label}"')
def step_then_parsed_version_label(context: Context, label: str) -> None:
    assert context.parsed_ontology.version.label == label, (
        f"Expected '{label}', got '{context.parsed_ontology.version.label}'"
    )
