"""Steps for resource_inheritance_coverage.feature — diff-coverage boost."""

from __future__ import annotations

import os
import tempfile
import types
from typing import Any
from unittest.mock import patch

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

from cleveragents.application.services.resource_registry_service import (
    ResourceRegistryService,
)
from cleveragents.core.exceptions import NotFoundError, ValidationError
from cleveragents.infrastructure.database.models import Base
from cleveragents.resource.handlers.resolver import (
    HandlerResolutionError,
    clear_handler_cache,
    resolve_handler,
    resolve_handler_polymorphic,
)
from cleveragents.resource.inheritance import (
    ResourceTypeParentRemovalError,
    resolve_fields,
)


def _make_service() -> ResourceRegistryService:
    """Create a ResourceRegistryService backed by an in-memory SQLite DB."""
    engine = create_engine("sqlite:///:memory:")
    Base.metadata.create_all(engine)
    factory = sessionmaker(bind=engine)
    return ResourceRegistryService(session_factory=factory)


def _write_yaml(data: dict[str, Any]) -> str:
    """Write a YAML config to a temp file and return the path."""
    fd, path = tempfile.mkstemp(suffix=".yaml")
    with os.fdopen(fd, "w") as f:
        yaml.dump(data, f)
    return path


@given("a fresh in-memory registry service for cov")
def step_fresh_service(context: Context) -> None:
    context.cov_svc = _make_service()
    context.cov_error = None
    context.cov_result = None  # type: ignore[assignment]


@given('a YAML config for "acme/child" that inherits "nonexistent-parent" for cov')
def step_yaml_nonexistent_parent(context: Context) -> None:
    context.cov_yaml_path = _write_yaml(
        {
            "name": "acme/child",
            "description": "Child type",
            "resource_kind": "physical",
            "sandbox_strategy": "copy_on_write",
            "inherits": "nonexistent-parent",
            "user_addable": True,
        }
    )


@given(
    'a YAML config for "acme/child" that inherits a type causing chain error for cov'
)
def step_yaml_chain_error(context: Context) -> None:
    # Register a parent that itself has an invalid inherits pointing to
    # a missing grandparent — so chain validation will fail when we
    # register the child referencing this parent via the service.
    # Instead, we inject a mock that makes validate_chain raise.
    context.cov_yaml_path = _write_yaml(
        {
            "name": "acme/chain-child",
            "description": "Chain child",
            "resource_kind": "physical",
            "sandbox_strategy": "copy_on_write",
            "inherits": "git-checkout",
        }
    )
    context.cov_chain_error_mock = True


@given('a YAML config for a built-in type name "git-checkout" for cov')
def step_yaml_duplicate(context: Context) -> None:
    context.cov_yaml_path = _write_yaml(
        {
            "name": "git-checkout",
            "description": "Duplicate",
            "resource_kind": "physical",
            "sandbox_strategy": "copy_on_write",
            "user_addable": True,
            "built_in": True,
            "cli_args": [],
            "parent_types": [],
            "child_types": [],
            "handler": "cleveragents.resource.handlers.git_checkout:GitCheckoutHandler",
        }
    )


@when("I call register_type with that config for cov")
def step_register_type(context: Context) -> None:
    svc: ResourceRegistryService = context.cov_svc
    try:
        if getattr(context, "cov_chain_error_mock", False):
            from cleveragents.resource.inheritance import (
                ResourceTypeParentNotFoundError,
            )

            with patch(
                "cleveragents.application.services.resource_registry_service"
                ".validate_chain",
                side_effect=ResourceTypeParentNotFoundError("Mocked chain error"),
            ):
                svc.register_type(context.cov_yaml_path)
        else:
            svc.register_type(context.cov_yaml_path)
    except (ValidationError, NotFoundError) as exc:
        context.cov_error = exc


@then('a ValidationError should be raised mentioning "{text}" for cov')
def step_validation_error_mentioning(context: Context, text: str) -> None:
    assert context.cov_error is not None, "Expected an error but none was raised"
    assert isinstance(context.cov_error, ValidationError), (
        f"Expected ValidationError, got {type(context.cov_error).__name__}"
    )
    assert text.lower() in str(context.cov_error).lower(), (
        f"Expected '{text}' in error message, got: {context.cov_error}"
    )


@then("a ValidationError should be raised mentioning chain error for cov")
def step_validation_error_chain(context: Context) -> None:
    assert context.cov_error is not None, "Expected an error but none was raised"
    assert isinstance(context.cov_error, ValidationError), (
        f"Expected ValidationError, got {type(context.cov_error).__name__}"
    )


@when('I call remove_type for "{name}" for cov')
def step_remove_type(context: Context, name: str) -> None:
    svc: ResourceRegistryService = context.cov_svc
    try:
        svc.remove_type(name)
    except (
        ValidationError,
        NotFoundError,
        ResourceTypeParentRemovalError,
    ) as exc:
        context.cov_error = exc


@given('a custom type "{name}" is registered for cov')
def step_register_custom_type(context: Context, name: str) -> None:
    path = _write_yaml(
        {
            "name": name,
            "description": f"Custom type {name}",
            "resource_kind": "physical",
            "sandbox_strategy": "copy_on_write",
            "user_addable": True,
        }
    )
    context.cov_svc.register_type(path)


@given('a resource of type "{type_name}" exists for cov')
def step_register_resource(context: Context, type_name: str) -> None:
    context.cov_svc.register_resource(type_name, name=f"test/{type_name}-res")


@given('a child type "{child}" inheriting "{parent}" is registered for cov')
def step_register_child_type(context: Context, child: str, parent: str) -> None:
    path = _write_yaml(
        {
            "name": child,
            "description": f"Child type {child}",
            "resource_kind": "physical",
            "sandbox_strategy": "copy_on_write",
            "inherits": parent,
        }
    )
    context.cov_svc.register_type(path)


@then("a ResourceTypeParentRemovalError should be raised for cov")
def step_parent_removal_error(context: Context) -> None:
    assert context.cov_error is not None, "Expected error"
    assert isinstance(context.cov_error, ResourceTypeParentRemovalError)


@then("no error should be raised for cov")
def step_no_error(context: Context) -> None:
    assert context.cov_error is None, f"Expected no error, got: {context.cov_error}"


@when('I call resolve_type_inheritance_chain for "{name}" for cov')
def step_resolve_chain(context: Context, name: str) -> None:
    svc: ResourceRegistryService = context.cov_svc
    try:
        context.cov_result = svc.resolve_type_inheritance_chain(name)
    except (NotFoundError, ValidationError) as exc:
        context.cov_error = exc


@then('the chain should contain "{a}" and "{b}"')
def step_chain_contains(context: Context, a: str, b: str) -> None:
    chain = context.cov_result
    assert isinstance(chain, list), f"Expected list, got {type(chain)}"
    assert a in chain, f"Expected '{a}' in chain {chain}"
    assert b in chain, f"Expected '{b}' in chain {chain}"


@then("a NotFoundError should be raised for cov")
def step_not_found_error(context: Context) -> None:
    assert context.cov_error is not None, "Expected error"
    assert isinstance(context.cov_error, NotFoundError)


@when('I check is_subtype_of "{child}" of "{ancestor}" for cov')
def step_is_subtype_of(context: Context, child: str, ancestor: str) -> None:
    svc: ResourceRegistryService = context.cov_svc
    context.cov_result = svc.is_subtype_of(child, ancestor)


@then("the is_subtype_of result should be True for cov")
def step_result_true(context: Context) -> None:
    assert context.cov_result is True


@then("the is_subtype_of result should be False for cov")
def step_result_false(context: Context) -> None:
    assert context.cov_result is False


@given('a resource of type "container-instance" exists for cov ops')
def step_register_container_resource(context: Context) -> None:
    svc: ResourceRegistryService = context.cov_svc
    svc.register_resource("container-instance", name="test/my-container")


@when('I call list_resources with type_name "{tn}" and exact=True for cov')
def step_list_exact(context: Context, tn: str) -> None:
    svc: ResourceRegistryService = context.cov_svc
    context.cov_result = svc.list_resources(type_name=tn, exact=True)


@then('the result list should contain only "{tn}" typed resources for cov')
def step_list_only_type(context: Context, tn: str) -> None:
    for r in context.cov_result:
        assert r.resource_type_name == tn, (
            f"Expected type '{tn}', got '{r.resource_type_name}'"
        )


@given("two resources of incompatible types for cov ops")
def step_incompatible_resources(context: Context) -> None:
    svc: ResourceRegistryService = context.cov_svc
    # devcontainer-file allows parent_types=["devcontainer-instance"]
    # and child_types=[]  — so it cannot be a parent of anything.
    # git-checkout has child_types that do NOT include "container-instance".
    svc.register_resource("git-checkout", name="test/parent-gc", location="/tmp/gc")
    svc.register_resource("container-instance", name="test/child-ci")
    context.cov_parent_name = "test/parent-gc"
    context.cov_child_name = "test/child-ci"


@when("I call link_child with incompatible types for cov ops")
def step_link_incompatible(context: Context) -> None:
    svc: ResourceRegistryService = context.cov_svc
    try:
        svc.link_child(context.cov_parent_name, context.cov_child_name)
    except (ValidationError, NotFoundError) as exc:
        context.cov_error = exc


@given("a parent resource with children of mixed types for cov ops")
def step_mixed_children(context: Context) -> None:
    svc: ResourceRegistryService = context.cov_svc
    svc.register_resource("git-checkout", name="test/tree-root", location="/tmp/tree")
    svc.register_resource("fs-directory", name="test/child-fs", location="/tmp/fs")
    svc.register_resource(
        "devcontainer-instance", name="test/child-dc", location="/tmp/dc"
    )
    svc.link_child("test/tree-root", "test/child-fs")
    svc.link_child("test/tree-root", "test/child-dc")


@when("I call get_resource_tree with type_filter for cov ops")
def step_tree_filter(context: Context) -> None:
    svc: ResourceRegistryService = context.cov_svc
    context.cov_result = svc.get_resource_tree(
        "test/tree-root", type_filter="fs-directory"
    )


@then("the tree should only contain children matching the filter for cov")
def step_tree_filter_check(context: Context) -> None:
    tree = context.cov_result
    assert len(tree) == 1
    root_node = tree[0]
    children = root_node["children"]
    for child_node in children:
        r = child_node["resource"]
        assert r.resource_type_name == "fs-directory", (
            f"Expected fs-directory, got {r.resource_type_name}"
        )


@then('a HandlerResolutionError should be raised mentioning "{text}" for cov')
def step_handler_error_mentioning(context: Context, text: str) -> None:
    assert context.cov_error is not None, "Expected error"
    assert isinstance(context.cov_error, HandlerResolutionError), (
        f"Expected HandlerResolutionError, got {type(context.cov_error).__name__}"
    )
    assert text.lower() in str(context.cov_error).lower(), (
        f"Expected '{text}' in '{context.cov_error}'"
    )


@when("I call resolve_handler with a class that fails to instantiate for cov")
def step_resolve_handler_instantiate_error(context: Context) -> None:
    clear_handler_cache()
    # Create a temp module with a class whose __init__ raises
    mod = types.ModuleType("_cov_test_bad_init")
    exec(
        "class BadHandler:\n    def __init__(self):\n"
        "        raise RuntimeError('boom')",
        mod.__dict__,
    )
    import sys

    sys.modules["_cov_test_bad_init"] = mod
    try:
        resolve_handler("_cov_test_bad_init:BadHandler")
    except HandlerResolutionError as exc:
        context.cov_error = exc
    finally:
        sys.modules.pop("_cov_test_bad_init", None)


@when("I call resolve_handler with a module that has an import error for cov")
def step_resolve_handler_import_error(context: Context) -> None:
    clear_handler_cache()
    # Patch importlib.import_module to raise RuntimeError (not
    # ModuleNotFoundError) to hit the generic except at resolver.py:91-94.
    with patch(
        "importlib.import_module",
        side_effect=RuntimeError("synthetic import error"),
    ):
        try:
            resolve_handler("some.module:SomeClass")
        except HandlerResolutionError as exc:
            context.cov_error = exc


@when(
    "I call resolve_handler with a class that does not satisfy ResourceHandler for cov"
)
def step_resolve_handler_not_protocol(context: Context) -> None:
    clear_handler_cache()
    mod = types.ModuleType("_cov_test_no_protocol")
    exec("class NotAHandler:\n    pass", mod.__dict__)
    import sys

    sys.modules["_cov_test_no_protocol"] = mod
    try:
        resolve_handler("_cov_test_no_protocol:NotAHandler")
    except HandlerResolutionError as exc:
        context.cov_error = exc
    finally:
        sys.modules.pop("_cov_test_no_protocol", None)


@when("I call resolve_handler_polymorphic with a broken chain for cov")
def step_resolve_handler_poly_broken(context: Context) -> None:
    clear_handler_cache()
    # Registry with a cycle
    registry: dict[str, Any] = {
        "typeA": {"inherits": "typeB", "handler": None},
        "typeB": {"inherits": "typeA", "handler": None},
    }
    try:
        resolve_handler_polymorphic("typeA", registry)
    except HandlerResolutionError as exc:
        context.cov_error = exc


@given("a type registry with child having no handler but ancestor has one for cov")
def step_registry_ancestor_handler(context: Context) -> None:
    context.cov_poly_registry = {
        "base-type": {
            "inherits": None,
            "handler": (
                "cleveragents.resource.handlers.git_checkout:GitCheckoutHandler"
            ),
        },
        "mid-type": {"inherits": "base-type", "handler": None},
        "leaf-type": {"inherits": "mid-type", "handler": None},
    }


@when("I call resolve_handler_polymorphic for the child type for cov")
def step_resolve_poly_child(context: Context) -> None:
    clear_handler_cache()
    try:
        context.cov_result = resolve_handler_polymorphic(
            "leaf-type", context.cov_poly_registry
        )
    except HandlerResolutionError as exc:
        context.cov_error = exc


@then("a valid handler should be returned for cov")
def step_valid_handler(context: Context) -> None:
    from cleveragents.resource.handlers.protocol import ResourceHandler

    assert context.cov_result is not None
    assert isinstance(context.cov_result, ResourceHandler)


@given("a registry where parent has properties and child overrides one for cov")
def step_registry_properties(context: Context) -> None:
    context.cov_prop_registry = {
        "base": {
            "inherits": None,
            "properties": {"color": "red", "size": "large"},
            "capabilities": {"read": True},
        },
        "child": {
            "inherits": "base",
            "properties": {"color": "blue"},
            "capabilities": {"write": True},
        },
    }


@when("I call resolve_fields for the child for cov")
def step_resolve_fields_child(context: Context) -> None:
    context.cov_result = resolve_fields("child", context.cov_prop_registry)


@then("the child properties should replace parent properties for cov")
def step_merged_properties(context: Context) -> None:
    result = context.cov_result
    props = result.get("properties", {})
    # After P2-2, properties is a scalar override: child replaces parent entirely.
    assert props.get("color") == "blue", f"Expected 'blue', got {props.get('color')}"
    assert props.get("size") is None, (
        f"Expected None (not inherited), got {props.get('size')}"
    )


@given("a registry with a gap in the chain for cov")
def step_registry_gap(context: Context) -> None:
    context.cov_gap_registry = {
        "child": {"inherits": "base", "capabilities": {"read": True}},
        "base": {"inherits": None, "capabilities": {"write": True}},
    }
    context.cov_gap_type = "child"


@when("I call resolve_fields for the type with a gap for cov")
def step_resolve_fields_gap(context: Context) -> None:
    from unittest.mock import patch as _patch

    with _patch(
        "cleveragents.resource.inheritance.resolve_inheritance_chain",
        return_value=["child", "nonexistent", "base"],
    ):
        context.cov_result = resolve_fields(
            context.cov_gap_type, context.cov_gap_registry
        )


@then("the result should still contain the available fields for cov")
def step_result_has_fields(context: Context) -> None:
    result = context.cov_result
    assert isinstance(result, dict)
    # Should have merged capabilities from child and base, skipping gap
    caps = result.get("capabilities", {})
    assert caps.get("read") is True or caps.get("write") is True, (
        f"Expected some capabilities, got {caps}"
    )
