"""Behave step implementations for UKO ontology feature."""

from __future__ import annotations

from pathlib import Path

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

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

_TTL_FILE = Path(__file__).resolve().parents[2] / "docs" / "ontology" / "uko.ttl"


# ---------------------------------------------------------------------------
# Given
# ---------------------------------------------------------------------------


@given("a TTL string")
@given("a TTL string:")
def step_given_ttl_string(context: Context) -> None:
    context.ttl_text = context.text


@given("the UKO ontology is loaded from the TTL file")
def step_given_uko_loaded(context: Context) -> None:
    loader = UKOLoader()
    context.ontology = loader.load(_TTL_FILE)
    context.loader = loader


@given("a TTL string with a node missing rdf_type")
@given("a TTL string with a node missing rdf_type:")
def step_given_ttl_missing_rdf_type(context: Context) -> None:
    context.ttl_text = context.text


@given("a UKO ontology with an undefined prefix node")
def step_given_undefined_prefix(context: Context) -> None:
    context.ontology = UKOOntology(
        version=UKOVersion(
            version_iri="https://cleveragents.ai/ontology/uko/0.1.0",
            version="0.1.0",
            label="Test",
            comment="Test",
        ),
        prefixes=(
            UKOPrefix(
                prefix="uko",
                uri="https://cleveragents.ai/ontology/uko#",
            ),
        ),
        nodes=(
            UKONode(
                uri="badprefix:Thing",
                rdf_type="owl:Class",
                label="Thing",
                layer=0,
            ),
        ),
    )
    context.loader = UKOLoader()


@given("a UKO ontology with a cycle in inheritance")
def step_given_cycle(context: Context) -> None:
    context.ontology = UKOOntology(
        version=UKOVersion(
            version_iri="https://cleveragents.ai/ontology/uko/0.1.0",
            version="0.1.0",
            label="Test",
            comment="Test",
        ),
        prefixes=(
            UKOPrefix(
                prefix="uko",
                uri="https://cleveragents.ai/ontology/uko#",
            ),
        ),
        nodes=(
            UKONode(
                uri="https://cleveragents.ai/ontology/uko#A",
                rdf_type="owl:Class",
                label="A",
                parent_uris=("https://cleveragents.ai/ontology/uko#B",),
                layer=0,
            ),
            UKONode(
                uri="https://cleveragents.ai/ontology/uko#B",
                rdf_type="owl:Class",
                label="B",
                parent_uris=("https://cleveragents.ai/ontology/uko#A",),
                layer=0,
            ),
        ),
    )
    context.loader = UKOLoader()


@given("a UKO ontology with a node referencing a non-existent parent")
def step_given_nonexistent_parent(context: Context) -> None:
    context.ontology = UKOOntology(
        version=UKOVersion(
            version_iri="https://cleveragents.ai/ontology/uko/0.1.0",
            version="0.1.0",
            label="Test",
            comment="Test",
        ),
        prefixes=(
            UKOPrefix(
                prefix="uko",
                uri="https://cleveragents.ai/ontology/uko#",
            ),
        ),
        nodes=(
            UKONode(
                uri="https://cleveragents.ai/ontology/uko#Orphan",
                rdf_type="owl:Class",
                label="Orphan",
                parent_uris=("https://cleveragents.ai/ontology/uko#DoesNotExist",),
                layer=0,
            ),
        ),
    )
    context.loader = UKOLoader()


# ---------------------------------------------------------------------------
# When
# ---------------------------------------------------------------------------


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


@when("I get nodes at layer {layer:d}")
def step_when_get_layer(context: Context, layer: int) -> None:
    loader: UKOLoader = getattr(context, "loader", UKOLoader())
    context.layer_nodes = loader.get_layer_nodes(context.ontology, layer)


@when("I validate the ontology")
def step_when_validate(context: Context) -> None:
    loader: UKOLoader = getattr(context, "loader", UKOLoader())
    context.validation_errors = loader.validate(context.ontology)


@when('I resolve inheritance for "{uri}"')
def step_when_resolve_inheritance(context: Context, uri: str) -> None:
    loader: UKOLoader = getattr(context, "loader", UKOLoader())
    context.inheritance_chain = loader.resolve_inheritance(context.ontology, uri)


@when("I parse the TTL string expecting no nodes")
def step_when_parse_expecting_no_nodes(context: Context) -> None:
    loader = UKOLoader()
    # This TTL has a node without rdf:type — the parser skips it
    context.ontology = loader.load_from_string(context.ttl_text)
    context.loader = loader


@when("I inject a node without rdf_type into the ontology")
def step_inject_typeless_node(context: Context) -> None:
    typeless = UKONode(
        uri="https://cleveragents.ai/ontology/uko#Broken",
        rdf_type="",
        label="Broken",
    )
    existing = list(context.ontology.nodes)
    existing.append(typeless)
    context.ontology = context.ontology.model_copy(
        update={"nodes": tuple(existing)},
    )


@when("I validate the modified ontology")
def step_validate_modified(context: Context) -> None:
    loader = UKOLoader()
    context.validation_errors = loader.validate(context.ontology)


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


@when("I attempt to parse the TTL string expecting a validation error")
def step_when_parse_expecting_error(context: Context) -> None:
    loader = UKOLoader()
    try:
        loader.load_from_string(context.ttl_text)
        context.validation_error_msg = None
    except UKOValidationError as exc:
        context.validation_error_msg = str(exc)


# ---------------------------------------------------------------------------
# Then
# ---------------------------------------------------------------------------


@then('the prefix "{name}" should map to "{uri}"')
def step_then_prefix_maps(context: Context, name: str, uri: str) -> None:
    prefix_map = {p.prefix: p.uri for p in context.ontology.prefixes}
    assert name in prefix_map, f"Prefix '{name}' not found; have {list(prefix_map)}"
    assert prefix_map[name] == uri, f"Expected {uri}, got {prefix_map[name]}"


@then('I should find a node with label "{label}"')
def step_then_find_node(context: Context, label: str) -> None:
    labels = [n.label for n in context.layer_nodes]
    assert label in labels, f"Label '{label}' not found in {labels}"


@then('the ontology version should be "{version}"')
def step_then_version(context: Context, version: str) -> None:
    assert context.ontology.version.version == version


@then('the ontology label should be "{label}"')
def step_then_label(context: Context, label: str) -> None:
    assert context.ontology.version.label == label


@then('the ontology comment should be "{comment}"')
def step_then_comment(context: Context, comment: str) -> None:
    assert context.ontology.version.comment == comment


@then('the version IRI should be "{iri}"')
def step_then_version_iri(context: Context, iri: str) -> None:
    assert context.ontology.version.version_iri == iri


@then("no nodes should be present in the ontology")
def step_then_no_nodes(context: Context) -> None:
    # The "Broken" node has no rdf:type, so the parser skips it
    broken_nodes = [n for n in context.ontology.nodes if n.label == "Broken"]
    assert len(broken_nodes) == 0, f"Expected no 'Broken' nodes, found {broken_nodes}"


# REMOVED: Duplicate @then step - already defined in subplan_service_coverage_boost_steps.py


@then("there should be no validation errors")
def step_then_no_errors(context: Context) -> None:
    assert context.validation_errors == [], (
        f"Expected no errors, got: {context.validation_errors}"
    )


@then("the inheritance chain should have at least {count:d} nodes")
def step_then_chain_length(context: Context, count: int) -> None:
    assert len(context.inheritance_chain) >= count, (
        f"Expected >= {count} nodes, got {len(context.inheritance_chain)}"
    )


@then('the first node in the chain should have label "{label}"')
def step_then_first_chain_label(context: Context, label: str) -> None:
    assert context.inheritance_chain[0].label == label, (
        f"Expected '{label}', got '{context.inheritance_chain[0].label}'"
    )


@then('the last node in the chain should have label "{label}"')
def step_then_last_chain_label(context: Context, label: str) -> None:
    assert context.inheritance_chain[-1].label == label, (
        f"Expected '{label}', got '{context.inheritance_chain[-1].label}'"
    )


@then("there should be at least {count:d} nodes")
def step_then_min_nodes(context: Context, count: int) -> None:
    assert len(context.layer_nodes) >= count, (
        f"Expected >= {count}, got {len(context.layer_nodes)}"
    )


@then("the ontology should have prefixes defined")
def step_then_has_prefixes(context: Context) -> None:
    assert len(context.ontology.prefixes) > 0


@then("the ontology should have nodes defined")
def step_then_has_nodes(context: Context) -> None:
    assert len(context.ontology.nodes) > 0


@then('"{version}" should be in the supported versions')
def step_then_supported_version(context: Context, version: str) -> None:
    assert version in context.ontology.supported_versions


@then("the node count should be {count:d}")
def step_node_count(context: Context, count: int) -> None:
    assert len(context.layer_nodes) == count, (
        f"Expected {count} nodes but got {len(context.layer_nodes)}"
    )


@then('validation errors should include "{text}"')
def step_validation_errors_include(context: Context, text: str) -> None:
    assert any(text in e for e in context.validation_errors), (
        f"Expected '{text}' in validation errors: {context.validation_errors}"
    )


@then('the node with label "{label}" should have parent "{parent_uri}"')
def step_then_node_has_parent(
    context: Context,
    label: str,
    parent_uri: str,
) -> None:
    matching = [n for n in context.layer_nodes if n.label == label]
    assert matching, f"No node with label '{label}' found in layer nodes"
    node = matching[0]
    assert parent_uri in node.parent_uris, (
        f"Expected parent '{parent_uri}' for node '{label}'; got: {node.parent_uris}"
    )


@then(
    'the node with label "{label}" should have property "{prop}" resolved to a full URI'
)
def step_then_property_resolved(context: Context, label: str, prop: str) -> None:
    matching = [n for n in context.layer_nodes if n.label == label]
    assert matching, f"No node with label '{label}' found in layer nodes"
    node = matching[0]
    prop_dict = dict(node.properties)
    assert prop in prop_dict, (
        f"Property '{prop}' not found on node '{label}'; have: {list(prop_dict)}"
    )
    value = prop_dict[prop]
    assert value.startswith("http://") or value.startswith("https://"), (
        f"Expected full URI for '{prop}' on '{label}', got: '{value}'"
    )


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


@then('the node with label "{label}" should not have property "{prop}"')
def step_then_property_absent(context: Context, label: str, prop: str) -> None:
    matching = [n for n in context.layer_nodes if n.label == label]
    assert matching, f"No node with label '{label}' found in layer nodes"
    node = matching[0]
    prop_dict = dict(node.properties)
    assert prop not in prop_dict, (
        f"Expected no '{prop}' on '{label}', but found: '{prop_dict[prop]}'"
    )


@then("a cycle detection error should be raised")
def step_then_cycle_error(context: Context) -> None:
    assert context.cycle_error is not None, "Expected cycle error but none raised"
    assert "Cycle" in context.cycle_error or "cycle" in context.cycle_error


@then('a validation error should mention "{text}"')
def step_then_validation_error_mention(context: Context, text: str) -> None:
    assert context.validation_error_msg is not None, (
        "Expected a UKOValidationError but none was raised"
    )
    assert text in context.validation_error_msg, (
        f"Expected '{text}' in error message: {context.validation_error_msg}"
    )


# ---------------------------------------------------------------------------
# Duplicate-key handling in properties tuple
# ---------------------------------------------------------------------------


@given("a UKO node with duplicate property keys")
def step_given_duplicate_props(context: Context) -> None:
    context.dup_node = UKONode(
        uri="https://cleveragents.ai/ontology/uko#TestDup",
        rdf_type="owl:DatatypeProperty",
        label="TestDup",
        properties=(
            ("rdfs:domain", "https://cleveragents.ai/ontology/uko#First"),
            ("rdfs:domain", "https://cleveragents.ai/ontology/uko#Second"),
        ),
    )


@when("I convert the node properties to a dict")
def step_when_convert_props_dict(context: Context) -> None:
    context.prop_dict = dict(context.dup_node.properties)


@then("the dict should contain only the last value for the duplicated key")
def step_then_last_value(context: Context) -> None:
    assert context.prop_dict["rdfs:domain"] == (
        "https://cleveragents.ai/ontology/uko#Second"
    ), f"Expected last value 'Second', got: {context.prop_dict['rdfs:domain']}"
