"""Step definitions for binding_resolution.feature."""

from __future__ import annotations

from unittest.mock import MagicMock

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

from cleveragents.application.services.binding_resolution_service import (
    BindingResolutionService,
)
from cleveragents.core.exceptions import NotFoundError, ValidationError
from cleveragents.domain.models.core.project import (
    LinkedResource,
    NamespacedProject,
)
from cleveragents.domain.models.core.resource import (
    PhysVirt,
    Resource,
)
from cleveragents.domain.models.core.resource_slot import BindingResult
from cleveragents.domain.models.core.resource_type import (
    ResourceKind,
    ResourceTypeSpec,
    SandboxStrategy,
)
from cleveragents.domain.models.core.tool import (
    BindingMode,
    ResourceAccessMode,
    ResourceSlot,
    Tool,
    ToolSource,
    ToolType,
)

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


def _make_resource(
    resource_id: str,
    type_name: str,
    name: str | None = None,
) -> Resource:
    return Resource(
        resource_id=resource_id,
        name=name,
        resource_type_name=type_name,
        classification=PhysVirt.PHYSICAL,
        description=f"Test resource {resource_id}",
    )


def _make_type_spec(
    name: str,
    parent_types: list[str] | None = None,
) -> ResourceTypeSpec:
    is_builtin = "/" not in name
    return ResourceTypeSpec(
        name=name,
        description=f"Type {name}",
        resource_kind=ResourceKind.PHYSICAL,
        sandbox_strategy=SandboxStrategy.NONE,
        parent_types=parent_types or [],
        built_in=is_builtin,
    )


# ------------------------------------------------------------------
# Background
# ------------------------------------------------------------------


@given("a mock resource registry")
def step_mock_registry(context: Context) -> None:
    from cleveragents.resource.inheritance import is_subtype_of as _is_subtype_of

    registry = MagicMock()
    registry._resources: dict[str, Resource] = {}
    registry._types: dict[str, ResourceTypeSpec] = {
        "git-checkout": _make_type_spec("git-checkout", parent_types=[]),
        "fs-directory": _make_type_spec(
            "fs-directory",
            parent_types=["git-checkout", "fs-directory"],
        ),
        "fs-file": _make_type_spec("fs-file", parent_types=[]),
    }
    # Inheritance map used by is_subtype_of: maps type name -> inherits value.
    # Populated by step helpers when multi-level inheritance is needed.
    registry._inherits: dict[str, str | None] = {
        "git-checkout": None,
        "fs-directory": None,
        "fs-file": None,
    }

    def show_resource(name_or_id: str) -> Resource:
        res = registry._resources.get(name_or_id)
        if res is None:
            raise NotFoundError(
                resource_type="resource",
                resource_id=name_or_id,
            )
        return res

    def show_type(name: str) -> ResourceTypeSpec:
        spec = registry._types.get(name)
        if spec is None:
            raise NotFoundError(
                resource_type="resource_type",
                resource_id=name,
            )
        return spec

    def is_subtype_of_mock(type_name: str, ancestor_name: str) -> bool:
        """Walk the full inheritance chain using the _inherits map."""
        type_registry = {
            t: {"inherits": registry._inherits.get(t)}
            for t in set(list(registry._types.keys()) + list(registry._inherits.keys()))
        }
        return _is_subtype_of(type_name, ancestor_name, type_registry)

    registry.show_resource = show_resource
    registry.show_type = show_type
    registry.is_subtype_of = is_subtype_of_mock
    context.mock_registry = registry


@given("a binding resolution service using the registry")
def step_service(context: Context) -> None:
    context.binding_service = BindingResolutionService(
        resource_registry=context.mock_registry,
    )


# ------------------------------------------------------------------
# Given: projects
# ------------------------------------------------------------------


@given('a project "{name}" with a linked resource:')
def step_project_one_resource(context: Context, name: str) -> None:
    parts = name.split("/", 1)
    namespace = parts[0] if len(parts) == 2 else "local"
    proj_name = parts[1] if len(parts) == 2 else parts[0]

    links: list[LinkedResource] = []
    for row in context.table:
        rid = row["resource_id"]
        tname = row["type_name"]
        alias = row["alias"] if row["alias"] else None

        resource = _make_resource(rid, tname, name=None)
        context.mock_registry._resources[rid] = resource

        links.append(
            LinkedResource(
                resource_id=rid,
                alias=alias,
            )
        )

    context.project = NamespacedProject(
        name=proj_name,
        namespace=namespace,
        linked_resources=links,
    )


@given('a project "{name}" with linked resources:')
def step_project_multi_resources(context: Context, name: str) -> None:
    parts = name.split("/", 1)
    namespace = parts[0] if len(parts) == 2 else "local"
    proj_name = parts[1] if len(parts) == 2 else parts[0]

    links: list[LinkedResource] = []
    for row in context.table:
        rid = row["resource_id"]
        tname = row["type_name"]
        alias = row["alias"] if row["alias"] else None

        resource = _make_resource(rid, tname, name=None)
        context.mock_registry._resources[rid] = resource

        links.append(
            LinkedResource(
                resource_id=rid,
                alias=alias,
            )
        )

    context.project = NamespacedProject(
        name=proj_name,
        namespace=namespace,
        linked_resources=links,
    )


@given('a project "{name}" with no linked resources')
def step_project_empty(context: Context, name: str) -> None:
    parts = name.split("/", 1)
    namespace = parts[0] if len(parts) == 2 else "local"
    proj_name = parts[1] if len(parts) == 2 else parts[0]

    context.project = NamespacedProject(
        name=proj_name,
        namespace=namespace,
        linked_resources=[],
    )


# ------------------------------------------------------------------
# Given: resources in registry
# ------------------------------------------------------------------


@given('a resource "{res_name}" of type "{type_name}" in the registry')
def step_resource_in_registry(
    context: Context,
    res_name: str,
    type_name: str,
) -> None:
    rid = "01HGZ6FE0AQDYTR4BX00000020"
    if res_name == "local/wrong-type":
        rid = "01HGZ6FE0AQDYTR4BX00000050"
    resource = _make_resource(rid, type_name, name=res_name)
    context.mock_registry._resources[res_name] = resource
    context.mock_registry._resources[rid] = resource


# ------------------------------------------------------------------
# Given: resource type overrides
# ------------------------------------------------------------------


@given('the resource type "{tname}" has parent_types including "{parent}"')
def step_type_parent(context: Context, tname: str, parent: str) -> None:
    existing = context.mock_registry._types.get(tname)
    parents = list(existing.parent_types) if existing else []
    if parent not in parents:
        parents.append(parent)
    context.mock_registry._types[tname] = _make_type_spec(tname, parent_types=parents)
    # Also update the _inherits map so is_subtype_of() walks the chain correctly.
    # Use the first parent as the direct ancestor (single-inheritance model).
    if parents:
        context.mock_registry._inherits[tname] = parents[0]


@given('the resource type "{tname}" has no parent_types')
def step_type_no_parents(context: Context, tname: str) -> None:
    context.mock_registry._types[tname] = _make_type_spec(tname, parent_types=[])


# ------------------------------------------------------------------
# Given: tools
# ------------------------------------------------------------------


@given('a tool "{tool_name}" with a contextual slot "{slot}" of type "{rtype}"')
def step_tool_contextual(
    context: Context,
    tool_name: str,
    slot: str,
    rtype: str,
) -> None:
    context.tool = Tool(
        name=tool_name,
        description=f"Tool {tool_name}",
        source=ToolSource.BUILTIN,
        tool_type=ToolType.TOOL,
        resource_slots=[
            ResourceSlot(
                name=slot,
                resource_type=rtype,
                access=ResourceAccessMode.READ_WRITE,
                binding=BindingMode.CONTEXTUAL,
            ),
        ],
    )


@given(
    'a tool "{tool_name}" with a static slot "{slot}" bound to'
    ' "{res}" of type "{rtype}"'
)
def step_tool_static(
    context: Context,
    tool_name: str,
    slot: str,
    res: str,
    rtype: str,
) -> None:
    context.tool = Tool(
        name=tool_name,
        description=f"Tool {tool_name}",
        source=ToolSource.BUILTIN,
        tool_type=ToolType.TOOL,
        resource_slots=[
            ResourceSlot(
                name=slot,
                resource_type=rtype,
                access=ResourceAccessMode.READ_ONLY,
                binding=BindingMode.STATIC,
                static_resource=res,
            ),
        ],
    )


@given('a tool "{tool_name}" with a parameter slot "{slot}" of type "{rtype}"')
def step_tool_parameter(
    context: Context,
    tool_name: str,
    slot: str,
    rtype: str,
) -> None:
    context.tool = Tool(
        name=tool_name,
        description=f"Tool {tool_name}",
        source=ToolSource.BUILTIN,
        tool_type=ToolType.TOOL,
        resource_slots=[
            ResourceSlot(
                name=slot,
                resource_type=rtype,
                access=ResourceAccessMode.READ_WRITE,
                binding=BindingMode.PARAMETER,
            ),
        ],
    )


@given('a tool "{tool_name}" with mixed slots:')
def step_tool_mixed(context: Context, tool_name: str) -> None:
    slots: list[ResourceSlot] = []
    for row in context.table:
        binding = BindingMode(row["binding"])
        static_res: str | None = (
            row["static_resource"] if row["static_resource"] else None
        )
        slots.append(
            ResourceSlot(
                name=row["name"],
                resource_type=row["resource_type"],
                access=ResourceAccessMode.READ_WRITE,
                binding=binding,
                static_resource=static_res,
            )
        )
    context.tool = Tool(
        name=tool_name,
        description=f"Tool {tool_name}",
        source=ToolSource.BUILTIN,
        tool_type=ToolType.TOOL,
        resource_slots=slots,
    )


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


@when("the bindings are resolved")
def step_resolve(context: Context) -> None:
    context.binding_results = context.binding_service.resolve(
        context.tool,
        context.project,
    )


@when("the binding resolution is attempted")
def step_resolve_attempt(context: Context) -> None:
    try:
        context.binding_results = context.binding_service.resolve(
            context.tool,
            context.project,
        )
        context.binding_error = None
    except ValidationError as exc:
        context.binding_error = exc
        context.binding_results = None


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


@then('the binding for slot "{slot}" should have mode "{mode}"')
def step_check_mode(context: Context, slot: str, mode: str) -> None:
    result = _find_result(context, slot)
    assert result.binding_mode == mode, (
        f"Expected mode '{mode}', got '{result.binding_mode}'"
    )


@then('the binding for slot "{slot}" should have resource_id "{rid}"')
def step_check_rid(context: Context, slot: str, rid: str) -> None:
    result = _find_result(context, slot)
    assert result.resource_id == rid, (
        f"Expected resource_id '{rid}', got '{result.resource_id}'"
    )


@then('the binding for slot "{slot}" should not be deferred')
def step_not_deferred(context: Context, slot: str) -> None:
    result = _find_result(context, slot)
    assert not result.deferred, f"Slot '{slot}' should not be deferred"


@then('the binding for slot "{slot}" should be deferred')
def step_is_deferred(context: Context, slot: str) -> None:
    result = _find_result(context, slot)
    assert result.deferred, f"Slot '{slot}' should be deferred"


@then('the binding for slot "{slot}" should have no resource_id')
def step_no_rid(context: Context, slot: str) -> None:
    result = _find_result(context, slot)
    assert result.resource_id is None, (
        f"Expected None resource_id, got '{result.resource_id}'"
    )


@then('a binding ValidationError should be raised mentioning "{text}"')
def step_binding_error(context: Context, text: str) -> None:
    assert context.binding_error is not None, (
        "Expected ValidationError but none was raised"
    )
    assert isinstance(context.binding_error, ValidationError), (
        f"Expected ValidationError, got {type(context.binding_error).__name__}"
    )
    assert text in str(context.binding_error), (
        f"Expected '{text}' in error, got '{context.binding_error}'"
    )


@then("there should be {count:d} binding results")
def step_count_results(context: Context, count: int) -> None:
    assert len(context.binding_results) == count, (
        f"Expected {count} results, got {len(context.binding_results)}"
    )


# ------------------------------------------------------------------
# Internal helper
# ------------------------------------------------------------------


def _find_result(context: Context, slot_name: str) -> BindingResult:
    for result in context.binding_results:
        if result.slot_name == slot_name:
            return result
    raise AssertionError(f"No binding result for slot '{slot_name}'")


# ------------------------------------------------------------------
# Given: inheritance chain setup for multi-level tests
# ------------------------------------------------------------------


@given('the resource type "{tname}" inherits from nothing')
def step_type_inherits_nothing(context: Context, tname: str) -> None:
    """Register a root type with no parent in the inheritance chain."""
    if tname not in context.mock_registry._types:
        context.mock_registry._types[tname] = _make_type_spec(tname, parent_types=[])
    context.mock_registry._inherits[tname] = None


@given('the resource type "{tname}" inherits from "{parent}"')
def step_type_inherits_from(context: Context, tname: str, parent: str) -> None:
    """Register a type that directly inherits from *parent*."""
    if tname not in context.mock_registry._types:
        context.mock_registry._types[tname] = _make_type_spec(
            tname, parent_types=[parent]
        )
    # Ensure parent is also in the inherits map (as a root if not already set).
    if parent not in context.mock_registry._inherits:
        context.mock_registry._inherits[parent] = None
    context.mock_registry._inherits[tname] = parent
