"""Step definitions for features/builtin_adapter.feature."""

from __future__ import annotations

from typing import Any

from behave import given, then, when

from cleveragents.domain.models.core.tool import ResourceSlot
from cleveragents.mcp.adapter import MCPToolAdapter
from cleveragents.tool.builtins import (
    ALL_FILE_TOOLS,
    ALL_GIT_TOOLS,
    ALL_SUBPLAN_TOOLS,
    BuiltinAdapter,
)
from cleveragents.tool.runtime import ToolSpec

__all__: list[str] = []


# ---------------------------------------------------------------------------
# Givens
# ---------------------------------------------------------------------------


@given("a builtin adapter")
def step_given_builtin_adapter(context: Any) -> None:
    context.builtin_adapter = BuiltinAdapter()


@given('an MCP tool input schema with a "{param_name}" property')
def step_given_mcp_schema(context: Any, param_name: str) -> None:
    context.mcp_tool_name = f"test-tool-{param_name}"
    context.mcp_input_schema = {
        "type": "object",
        "properties": {
            param_name: {"type": "string", "description": f"The {param_name}"},
        },
        "required": [param_name],
    }


@given("an MCP tool with an empty input schema")
def step_given_empty_schema(context: Any) -> None:
    context.mcp_tool_name = "test-tool-empty"
    context.mcp_input_schema = {}


@given("an MCP tool with an input schema missing the properties key")
def step_given_no_properties_schema(context: Any) -> None:
    context.mcp_tool_name = "test-tool-no-props"
    context.mcp_input_schema = {"type": "object"}


@given('an MCP tool with both "file_path" and "filepath" properties')
def step_given_duplicate_file_params(context: Any) -> None:
    context.mcp_tool_name = "test-tool-dup-files"
    context.mcp_input_schema = {
        "type": "object",
        "properties": {
            "file_path": {"type": "string"},
            "filepath": {"type": "string"},
        },
    }


# ---------------------------------------------------------------------------
# Whens
# ---------------------------------------------------------------------------


@when("I call discover on the builtin adapter")
def step_when_discover(context: Any) -> None:
    context.discovered_tools = context.builtin_adapter.discover()


@when("I call register on the builtin adapter")
def step_when_register(context: Any) -> None:
    context.registered_names = context.builtin_adapter.register(context.registry)


@when("I call activate on the builtin adapter")
def step_when_activate(context: Any) -> None:
    context.activate_exception = None
    try:
        context.builtin_adapter.activate()
    except Exception as exc:
        context.activate_exception = exc


@when("I call deactivate on the builtin adapter")
def step_when_deactivate(context: Any) -> None:
    context.deactivate_exception = None
    try:
        context.builtin_adapter.deactivate()
    except Exception as exc:
        context.deactivate_exception = exc


@when("I infer resource slots for the schema")
def step_when_infer_slots(context: Any) -> None:
    context.inferred_slots = MCPToolAdapter.infer_resource_slots(
        context.mcp_tool_name,
        context.mcp_input_schema,
    )


# ---------------------------------------------------------------------------
# Thens
# ---------------------------------------------------------------------------


@then("the builtin adapter should not be discovered yet")
def step_then_not_discovered(context: Any) -> None:
    assert not context.builtin_adapter.is_discovered, (
        "Expected is_discovered to be False before calling discover()"
    )


@then("the builtin adapter should be discovered")
def step_then_is_discovered(context: Any) -> None:
    assert context.builtin_adapter.is_discovered, (
        "Expected is_discovered to be True after calling discover()"
    )


@then("the discovered tools should include all file tools")
def step_then_includes_file_tools(context: Any) -> None:
    discovered_names = {t.name for t in context.discovered_tools}
    for spec in ALL_FILE_TOOLS:
        assert spec.name in discovered_names, (
            f"File tool '{spec.name}' not in discovered tools"
        )


@then("the discovered tools should include all git tools")
def step_then_includes_git_tools(context: Any) -> None:
    discovered_names = {t.name for t in context.discovered_tools}
    for spec in ALL_GIT_TOOLS:
        assert spec.name in discovered_names, (
            f"Git tool '{spec.name}' not in discovered tools"
        )


@then("the discovered tools should include all subplan tools")
def step_then_includes_subplan_tools(context: Any) -> None:
    discovered_names = {t.name for t in context.discovered_tools}
    for spec in ALL_SUBPLAN_TOOLS:
        assert spec.name in discovered_names, (
            f"Subplan tool '{spec.name}' not in discovered tools"
        )


@then("the total discovered tool count should be {count:d}")
def step_then_discovered_count(context: Any, count: int) -> None:
    actual = len(context.discovered_tools)
    assert actual == count, f"Expected {count} discovered tools, got {actual}"


@then("the builtin adapter lifecycle calls should succeed without error")
def step_then_lifecycle_no_exception(context: Any) -> None:
    assert context.activate_exception is None, (
        f"activate raised: {context.activate_exception}"
    )
    assert context.deactivate_exception is None, (
        f"deactivate raised: {context.deactivate_exception}"
    )


@then('all registered tools should have source "{source}"')
def step_then_all_source(context: Any, source: str) -> None:
    tools: list[ToolSpec] = context.registry.list_tools()
    for spec in tools:
        assert spec.source == source, (
            f"Tool '{spec.name}' has source='{spec.source}', expected '{source}'"
        )


@then("the inferred slots should be empty")
def step_then_slots_empty(context: Any) -> None:
    assert len(context.inferred_slots) == 0, (
        f"Expected no inferred slots, got {len(context.inferred_slots)}: "
        f"{[s.name for s in context.inferred_slots]}"
    )


@then("the inferred slot count should be {count:d}")
def step_then_slot_count(context: Any, count: int) -> None:
    actual = len(context.inferred_slots)
    assert actual == count, (
        f"Expected {count} inferred slot(s), got {actual}: "
        f"{[s.name for s in context.inferred_slots]}"
    )


@then('the inferred slots should contain a "{slot_name}" slot')
def step_then_slot_exists(context: Any, slot_name: str) -> None:
    names = [s.name for s in context.inferred_slots]
    assert slot_name in names, (
        f"Slot '{slot_name}' not found in inferred slots: {names}"
    )


@then('the "{slot_name}" slot should have resource_type "{expected}"')
def step_then_slot_resource_type(context: Any, slot_name: str, expected: str) -> None:
    slot = _find_slot(context.inferred_slots, slot_name)
    assert slot.resource_type == expected, (
        f"Slot '{slot_name}' resource_type='{slot.resource_type}', "
        f"expected '{expected}'"
    )


@then('the "{slot_name}" slot should have access "{expected}"')
def step_then_slot_access(context: Any, slot_name: str, expected: str) -> None:
    slot = _find_slot(context.inferred_slots, slot_name)
    assert slot.access.value == expected, (
        f"Slot '{slot_name}' access='{slot.access.value}', expected '{expected}'"
    )


@then('the "{slot_name}" slot should have binding "{expected}"')
def step_then_slot_binding(context: Any, slot_name: str, expected: str) -> None:
    slot = _find_slot(context.inferred_slots, slot_name)
    assert slot.binding.value == expected, (
        f"Slot '{slot_name}' binding='{slot.binding.value}', expected '{expected}'"
    )


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


def _find_slot(slots: list[ResourceSlot], name: str) -> ResourceSlot:
    """Find a ResourceSlot by name or raise AssertionError."""
    for slot in slots:
        if slot.name == name:
            return slot
    raise AssertionError(f"Slot '{name}' not found in {[s.name for s in slots]}")
