"""Step definitions for resource_registry_service_coverage_boost.feature.

Covers the remaining uncovered lines in resource_registry_service.py:
- Lines 579, 581-582, 584-586: link_child type incompatibility ValidationError
- Lines 646-648: link_child generic exception rollback
- Lines 686-688: unlink_child generic exception rollback
- Line 785: _build_tree_node type_filter skips non-matching children
"""

from __future__ import annotations

import json
from datetime import UTC, datetime
from typing import Any
from unittest.mock import MagicMock

from behave import given, then, when  # type: ignore[import-untyped]
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker

from cleveragents.application.services.resource_registry_service import (
    ResourceRegistryService,
)
from cleveragents.core.exceptions import ValidationError
from cleveragents.domain.models.core.resource import PhysVirt, Resource
from cleveragents.infrastructure.database.models import (
    Base,
    ResourceLinkModel,
    ResourceModel,
    ResourceTypeModel,
)

# ---------------------------------------------------------------------------
# Valid ULID constants (Crockford base32: [0-9A-HJKMNP-TV-Z]{26})
# ---------------------------------------------------------------------------

# Scenario 1: type incompatibility
_PARENT_RES_1 = "01JA00000000000000000000AA"
_CHILD_RES_1 = "01JB00000000000000000000BB"

# Scenario 2: link_child generic error
_PARENT_RES_2 = "01JC00000000000000000000CC"
_CHILD_RES_2 = "01JE00000000000000000000EE"

# Scenario 3: unlink_child generic error
_PARENT_RES_3 = "01JG00000000000000000000GG"
_CHILD_RES_3 = "01JH00000000000000000000HH"

# Scenario 4: tree type filter
_TREE_PARENT = "01JK00000000000000000000KK"
_TREE_CHILD1 = "01JM00000000000000000000MM"
_TREE_CHILD2 = "01JN00000000000000000000NN"

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_inmem_session_factory() -> tuple[Any, Any]:
    """Create an in-memory SQLite engine + unclosable session factory.

    Returns (engine, session_factory) where session_factory returns a
    wrapper whose close() is a no-op so the service can call
    session.close() without losing the shared in-memory database.
    """
    engine = create_engine(
        "sqlite:///:memory:",
        echo=False,
        connect_args={"check_same_thread": False},
    )
    Base.metadata.create_all(engine)
    real_session = sessionmaker(
        bind=engine,
        expire_on_commit=False,
        autoflush=True,
        autocommit=False,
    )()

    class _Unclosable:
        """Wraps a real session but makes close() a no-op."""

        def __init__(self, s: Session) -> None:
            object.__setattr__(self, "_s", s)

        def close(self) -> None:
            pass

        def __getattr__(self, name: str) -> Any:
            return getattr(object.__getattribute__(self, "_s"), name)

        def __setattr__(self, name: str, value: Any) -> None:
            setattr(object.__getattribute__(self, "_s"), name, value)

    wrapper = _Unclosable(real_session)
    return engine, lambda: wrapper


def _seed_resource_type(
    session: Any,
    name: str,
    *,
    resource_kind: str = "physical",
    sandbox_strategy: str = "none",
    allowed_child_types: list[str] | None = None,
) -> None:
    """Insert a ResourceTypeModel row into the session."""
    now = datetime.now(tz=UTC).isoformat()
    child_json = json.dumps(allowed_child_types) if allowed_child_types else None
    row = ResourceTypeModel(
        name=name,
        namespace="builtin" if "/" not in name else name.split("/", 1)[0],
        description=f"Test type {name}",
        resource_kind=resource_kind,
        sandbox_strategy=sandbox_strategy,
        user_addable=True,
        handler_ref=None,
        args_schema_json=None,
        allowed_parent_types_json=None,
        allowed_child_types_json=child_json,
        auto_discover_json=None,
        capabilities_json=json.dumps(
            {"read": True, "write": True, "sandbox": False, "checkpoint": False}
        ),
        equivalence_json=None,
        source="test",
        created_at=now,
        updated_at=now,
    )
    session.add(row)
    session.commit()


def _seed_resource(
    session: Any,
    resource_id: str,
    type_name: str,
    *,
    namespaced_name: str | None = None,
) -> None:
    """Insert a ResourceModel row into the session."""
    now = datetime.now(tz=UTC).isoformat()
    row = ResourceModel(
        resource_id=resource_id,
        namespaced_name=namespaced_name,
        namespace=None,
        type_name=type_name,
        resource_kind="physical",
        location="/tmp/test",
        description=None,
        read_only=False,
        auto_discovered=False,
        sandbox_strategy=None,
        content_hash=None,
        properties_json=None,
        metadata_json=None,
        created_at=now,
        updated_at=now,
    )
    session.add(row)
    session.commit()


# ---------------------------------------------------------------------------
# Scenario: link_child rejects child type not in allowed_child_types
# (targets lines 579, 581-582, 584-586)
# ---------------------------------------------------------------------------


@given("an in-memory resource registry with two resources of incompatible types")
def step_setup_incompatible_resources(context: Any) -> None:
    engine, factory = _make_inmem_session_factory()
    context.boost_engine = engine
    context.boost_factory = factory

    session = factory()

    # Parent type only allows "fs-directory" children
    _seed_resource_type(
        session,
        "git-checkout",
        resource_kind="physical",
        sandbox_strategy="git_worktree",
        allowed_child_types=["fs-directory"],
    )

    # Child type that is NOT in the allowed list
    _seed_resource_type(
        session,
        "container-instance",
        resource_kind="physical",
        sandbox_strategy="snapshot",
    )

    # Seed parent resource of type git-checkout
    _seed_resource(
        session,
        _PARENT_RES_1,
        "git-checkout",
        namespaced_name="test/parent-repo",
    )

    # Seed child resource of type container-instance (not allowed)
    _seed_resource(
        session,
        _CHILD_RES_1,
        "container-instance",
        namespaced_name="test/bad-child",
    )

    # Build service (bootstrap will add other built-in types)
    context.boost_svc = ResourceRegistryService(session_factory=factory)


@when("I attempt to link the child to the parent with incompatible types")
def step_link_incompatible_child(context: Any) -> None:
    context.boost_link_error = None
    try:
        context.boost_svc.link_child(
            "test/parent-repo",
            "test/bad-child",
        )
    except ValidationError as exc:
        context.boost_link_error = exc


@then("a ValidationError should be raised indicating the child type is not allowed")
def step_verify_type_incompatibility_error(context: Any) -> None:
    assert context.boost_link_error is not None, (
        "Expected ValidationError for type incompatibility"
    )
    assert "not allowed" in context.boost_link_error.message


@then("the error details should contain the parent and child type names")
def step_verify_error_details_types(context: Any) -> None:
    details = context.boost_link_error.details
    assert details["parent_type"] == "git-checkout"
    assert details["child_type"] == "container-instance"


# ---------------------------------------------------------------------------
# Scenario: link_child generic exception rollback
# (targets lines 646-648)
# ---------------------------------------------------------------------------


@given("a resource registry service prepared for link_child with a faulty session")
def step_setup_faulty_link_child(context: Any) -> None:
    # We need show_resource to return real Resource objects, but the
    # session used inside link_child's main body should fail on flush.
    parent_resource = Resource(
        resource_id=_PARENT_RES_2,
        name="test/link-parent",
        resource_type_name="git-checkout",
        classification=PhysVirt.PHYSICAL,
        description=None,
        properties={},
        location="/tmp/p",
        content_hash=None,
        sandbox_strategy=None,
    )
    child_resource = Resource(
        resource_id=_CHILD_RES_2,
        name="test/link-child",
        resource_type_name="fs-directory",
        classification=PhysVirt.PHYSICAL,
        description=None,
        properties={},
        location="/tmp/c",
        content_hash=None,
        sandbox_strategy=None,
    )

    # Mock session that blows up on flush()
    mock_session = MagicMock()
    parent_type_row = MagicMock()
    parent_type_row.allowed_child_types_json = None

    # query().filter_by().first() calls in sequence:
    # 1st: parent_type_row (type lookup)
    # 2nd: None (no existing link)
    mock_session.query.return_value.filter_by.return_value.first.side_effect = [
        parent_type_row,
        None,
    ]
    mock_session.query.return_value.filter_by.return_value.all.return_value = []
    mock_session.flush.side_effect = RuntimeError("disk I/O error in link_child")

    context.boost_link_mock_session = mock_session

    svc = ResourceRegistryService.__new__(ResourceRegistryService)
    svc._session_factory = lambda: mock_session

    call_count = {"n": 0}

    def fake_show(name_or_id: str) -> Resource:
        call_count["n"] += 1
        if call_count["n"] == 1:
            return parent_resource
        return child_resource

    svc.show_resource = fake_show  # type: ignore[assignment]
    context.boost_link_svc = svc


@when("I attempt link_child and a generic exception occurs")
def step_attempt_link_child_generic_error(context: Any) -> None:
    context.boost_link_generic_error = None
    try:
        context.boost_link_svc.link_child("test/link-parent", "test/link-child")
    except RuntimeError as exc:
        context.boost_link_generic_error = exc


@then("the link_child session should have been rolled back")
def step_verify_link_child_rollback(context: Any) -> None:
    context.boost_link_mock_session.rollback.assert_called()


@then("the generic exception from link_child should propagate")
def step_verify_link_child_error_propagates(context: Any) -> None:
    assert context.boost_link_generic_error is not None
    assert "disk I/O error in link_child" in str(context.boost_link_generic_error)


# ---------------------------------------------------------------------------
# Scenario: unlink_child generic exception rollback
# (targets lines 686-688)
# ---------------------------------------------------------------------------


@given("a resource registry service prepared for unlink_child with a faulty session")
def step_setup_faulty_unlink_child(context: Any) -> None:
    parent_resource = Resource(
        resource_id=_PARENT_RES_3,
        name="test/unlink-parent",
        resource_type_name="git-checkout",
        classification=PhysVirt.PHYSICAL,
        description=None,
        properties={},
        location="/tmp/up",
        content_hash=None,
        sandbox_strategy=None,
    )
    child_resource = Resource(
        resource_id=_CHILD_RES_3,
        name="test/unlink-child",
        resource_type_name="fs-directory",
        classification=PhysVirt.PHYSICAL,
        description=None,
        properties={},
        location="/tmp/uc",
        content_hash=None,
        sandbox_strategy=None,
    )

    mock_session = MagicMock()
    # The link query returns a mock link (not None), so we pass the
    # NotFoundError check.  Then delete() explodes.
    mock_link = MagicMock()
    mock_session.query.return_value.filter_by.return_value.first.return_value = (
        mock_link
    )
    mock_session.delete.side_effect = RuntimeError("disk I/O error in unlink_child")

    context.boost_unlink_mock_session = mock_session

    svc = ResourceRegistryService.__new__(ResourceRegistryService)
    svc._session_factory = lambda: mock_session

    call_count = {"n": 0}

    def fake_show(name_or_id: str) -> Resource:
        call_count["n"] += 1
        if call_count["n"] == 1:
            return parent_resource
        return child_resource

    svc.show_resource = fake_show  # type: ignore[assignment]
    context.boost_unlink_svc = svc


@when("I attempt unlink_child and a generic exception occurs")
def step_attempt_unlink_child_generic_error(context: Any) -> None:
    context.boost_unlink_generic_error = None
    try:
        context.boost_unlink_svc.unlink_child("test/unlink-parent", "test/unlink-child")
    except RuntimeError as exc:
        context.boost_unlink_generic_error = exc


@then("the unlink_child session should have been rolled back")
def step_verify_unlink_child_rollback(context: Any) -> None:
    context.boost_unlink_mock_session.rollback.assert_called()


@then("the generic exception from unlink_child should propagate")
def step_verify_unlink_child_error_propagates(context: Any) -> None:
    assert context.boost_unlink_generic_error is not None
    assert "disk I/O error in unlink_child" in str(context.boost_unlink_generic_error)


# ---------------------------------------------------------------------------
# Scenario: get_resource_tree with type_filter excludes non-matching children
# (targets line 785)
# ---------------------------------------------------------------------------


@given(
    "an in-memory resource registry with a parent and two children of different types"
)
def step_setup_tree_with_mixed_types(context: Any) -> None:
    engine, factory = _make_inmem_session_factory()
    context.boost_tree_engine = engine
    context.boost_tree_factory = factory

    session = factory()

    # Seed resource types
    _seed_resource_type(
        session,
        "git-checkout",
        resource_kind="physical",
        sandbox_strategy="git_worktree",
        allowed_child_types=["fs-directory", "container-instance"],
    )
    _seed_resource_type(
        session,
        "fs-directory",
        resource_kind="physical",
        sandbox_strategy="copy_on_write",
    )
    _seed_resource_type(
        session,
        "container-instance",
        resource_kind="physical",
        sandbox_strategy="snapshot",
    )

    # Seed parent resource
    _seed_resource(
        session,
        _TREE_PARENT,
        "git-checkout",
        namespaced_name="test/tree-parent",
    )

    # Seed child 1: fs-directory
    _seed_resource(
        session,
        _TREE_CHILD1,
        "fs-directory",
        namespaced_name="test/child-dir",
    )

    # Seed child 2: container-instance
    _seed_resource(
        session,
        _TREE_CHILD2,
        "container-instance",
        namespaced_name="test/child-container",
    )

    # Create links: parent -> child1, parent -> child2
    now = datetime.now(tz=UTC).isoformat()
    link1 = ResourceLinkModel(
        parent_id=_TREE_PARENT,
        child_id=_TREE_CHILD1,
        created_at=now,
    )
    link2 = ResourceLinkModel(
        parent_id=_TREE_PARENT,
        child_id=_TREE_CHILD2,
        created_at=now,
    )
    session.add(link1)
    session.add(link2)
    session.commit()

    context.boost_tree_svc = ResourceRegistryService(session_factory=factory)


@when("I call get_resource_tree with a type_filter for only one child type")
def step_call_tree_with_type_filter(context: Any) -> None:
    context.boost_tree_result = context.boost_tree_svc.get_resource_tree(
        "test/tree-parent",
        depth=-1,
        type_filter="fs-directory",
    )


@then("the tree should contain only the child matching the type filter")
def step_verify_tree_has_matching_child(context: Any) -> None:
    tree = context.boost_tree_result
    assert len(tree) == 1, "Tree should have exactly one root node"
    root_node = tree[0]
    children = root_node["children"]

    # Only the fs-directory child should be present
    child_type_names = [node["resource"].resource_type_name for node in children]
    assert "fs-directory" in child_type_names, (
        f"Expected fs-directory child in tree, got: {child_type_names}"
    )


@then("the non-matching child should be excluded from the tree")
def step_verify_tree_excludes_non_matching(context: Any) -> None:
    tree = context.boost_tree_result
    root_node = tree[0]
    children = root_node["children"]

    child_type_names = [node["resource"].resource_type_name for node in children]
    assert "container-instance" not in child_type_names, (
        f"container-instance should be filtered out, got: {child_type_names}"
    )
    assert len(children) == 1, (
        f"Expected exactly 1 child after filtering, got {len(children)}"
    )
