"""Step definitions for m2_actor_tool_smoke.feature.

All step names are prefixed with 'M2' to avoid AmbiguousStep conflicts
with existing actor/skill/tool step files.
"""

from __future__ import annotations

import tempfile
from pathlib import Path
from typing import Any

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

from cleveragents.actor.loader import ActorLoader
from cleveragents.actor.schema import ActorConfigSchema
from cleveragents.domain.models.core.skill import (
    Skill,
    SkillInlineTool,
)
from cleveragents.domain.models.core.tool import (
    Tool,
    ToolCapability,
    ToolSource,
    ToolType,
)
from cleveragents.infrastructure.events.reactive import ReactiveEventBus
from cleveragents.skills.context import SkillContext
from cleveragents.skills.protocol import SkillDefinition, SkillMetadata
from cleveragents.skills.registry import SkillRegistry
from cleveragents.skills.schema import SkillConfigSchema
from cleveragents.tool.context import ToolExecutionContext
from cleveragents.tool.lifecycle import (
    ToolDescriptor,
    ToolResult,
    ToolRuntime,
)
from cleveragents.tool.registry import ToolRegistry
from cleveragents.tool.runtime import ToolSpec

# ── Fixture paths ──────────────────────────────────────────────────

_FIXTURES_DIR = Path(__file__).resolve().parent.parent / "fixtures" / "m2"
_ACTORS_DIR = _FIXTURES_DIR / "actors"
_HIERARCHICAL_ACTOR = _ACTORS_DIR / "m2_hierarchical_actor.yaml"
_SKILL_PACK = _FIXTURES_DIR / "m2_skill_pack.yaml"


# ── Mock ToolInstance for lifecycle tests ──────────────────────────


class _M2MockToolInstance:
    """Minimal ToolInstance for M2 lifecycle testing."""

    def __init__(self, name: str) -> None:
        self._name = name
        self._activated = False
        self._deactivated = False
        self._execute_count = 0

    def discover(self) -> ToolDescriptor:
        return ToolDescriptor(
            name=self._name,
            description=f"M2 mock tool {self._name}",
            source="custom",
        )

    def activate(self, ctx: ToolExecutionContext) -> None:
        self._activated = True

    def execute(self, params: dict[str, Any], ctx: ToolExecutionContext) -> ToolResult:
        self._execute_count += 1
        return ToolResult(success=True, data={"echo": params.get("text", "")})

    def deactivate(self, ctx: ToolExecutionContext) -> None:
        self._deactivated = True

    @property
    def is_activated(self) -> bool:
        return self._activated

    @property
    def is_deactivated(self) -> bool:
        return self._deactivated


# ════════════════════════════════════════════════════════════════════
# Actor YAML loading
# ════════════════════════════════════════════════════════════════════


@given("I load the M2 hierarchical actor fixture")
def step_m2_load_hierarchical_actor(context: Context) -> None:
    context._m2_actor = ActorConfigSchema.from_yaml_file(_HIERARCHICAL_ACTOR)


@then('the M2 actor name should be "{name}"')
def step_m2_actor_name(context: Context, name: str) -> None:
    actor: ActorConfigSchema = context._m2_actor
    assert actor.name == name, f"Expected '{name}', got '{actor.name}'"


@then('the M2 actor type should be "{actor_type}"')
def step_m2_actor_type(context: Context, actor_type: str) -> None:
    actor: ActorConfigSchema = context._m2_actor
    assert actor.type.value == actor_type, (
        f"Expected type '{actor_type}', got '{actor.type.value}'"
    )


@then("the M2 actor should have {count:d} graph nodes")
def step_m2_actor_node_count(context: Context, count: int) -> None:
    actor: ActorConfigSchema = context._m2_actor
    assert actor.route is not None, "Expected a route definition"
    assert len(actor.route.nodes) == count, (
        f"Expected {count} nodes, got {len(actor.route.nodes)}"
    )


@then('the M2 actor entry node should be "{entry}"')
def step_m2_actor_entry_node(context: Context, entry: str) -> None:
    actor: ActorConfigSchema = context._m2_actor
    assert actor.route is not None
    assert actor.route.entry_node == entry, (
        f"Expected entry '{entry}', got '{actor.route.entry_node}'"
    )


@then('the M2 actor exit nodes should include "{exit_node}"')
def step_m2_actor_exit_nodes(context: Context, exit_node: str) -> None:
    actor: ActorConfigSchema = context._m2_actor
    assert actor.route is not None
    assert exit_node in actor.route.exit_nodes, (
        f"Expected '{exit_node}' in exit nodes: {actor.route.exit_nodes}"
    )


# ── Actor loader discovery from fixture dir ───────────────────────


@given("I create an M2 actor loader from the fixture directory")
def step_m2_create_loader(context: Context) -> None:
    context._m2_loader = ActorLoader(search_roots=[_ACTORS_DIR])


@when("I run M2 actor discovery")
def step_m2_run_discovery(context: Context) -> None:
    loader: ActorLoader = context._m2_loader
    context._m2_discovered = loader.discover()


@then("the M2 loader should find {count:d} actors")
def step_m2_loader_count(context: Context, count: int) -> None:
    loader: ActorLoader = context._m2_loader
    actors = loader.list_actors()
    assert len(actors) == count, (
        f"Expected {count} actors, got {len(actors)}: {[a.name for a in actors]}"
    )


@then('the M2 loader should contain actor "{name}"')
def step_m2_loader_contains(context: Context, name: str) -> None:
    loader: ActorLoader = context._m2_loader
    config = loader.get(name)
    assert config is not None, f"Actor '{name}' not found in M2 loader"


# ════════════════════════════════════════════════════════════════════
# Skill pack loading and registry
# ════════════════════════════════════════════════════════════════════


@given("I load the M2 skill pack fixture")
def step_m2_load_skill_pack(context: Context) -> None:
    context._m2_skill_config = SkillConfigSchema.from_yaml_file(_SKILL_PACK)


@then('the M2 skill name should be "{name}"')
def step_m2_skill_name(context: Context, name: str) -> None:
    cfg: SkillConfigSchema = context._m2_skill_config
    assert cfg.name == name, f"Expected '{name}', got '{cfg.name}'"


@then("the M2 skill should have {count:d} tool references")
def step_m2_skill_tool_refs(context: Context, count: int) -> None:
    cfg: SkillConfigSchema = context._m2_skill_config
    assert len(cfg.tools) == count, f"Expected {count} tool refs, got {len(cfg.tools)}"


@then("the M2 skill should have {count:d} inline tools")
def step_m2_skill_inline_tools(context: Context, count: int) -> None:
    cfg: SkillConfigSchema = context._m2_skill_config
    assert len(cfg.inline_tools) == count, (
        f"Expected {count} inline tools, got {len(cfg.inline_tools)}"
    )


def _build_m2_skill_definition() -> SkillDefinition:
    """Build a SkillDefinition from the M2 fixture for registry tests."""
    skill = Skill(
        name="m2test/file-ops-pack",
        description="M2 smoke test skill pack with file operation tools",
        tool_refs=["builtin/read_file", "builtin/list_files"],
        anonymous_tools=[
            SkillInlineTool(
                description="Echo input back for testing",
                source=ToolSource.CUSTOM,
                code="def m2_echo(text: str) -> str:\n    return f'echo: {text}'",
                timeout=300,
            ),
        ],
    )
    metadata = SkillMetadata.from_skill(skill)
    return SkillDefinition(
        skill=skill,
        metadata=metadata,
    )


@given("I register the M2 skill pack in a skill registry")
def step_m2_register_skill_pack(context: Context) -> None:
    registry = SkillRegistry()
    defn = _build_m2_skill_definition()
    registry.register(defn)
    context._m2_skill_registry = registry


@when('I resolve tools for M2 skill "{name}"')
def step_m2_resolve_tools(context: Context, name: str) -> None:
    registry: SkillRegistry = context._m2_skill_registry
    context._m2_resolved_tools = registry.resolve_tools(name)


@then("the M2 resolved tools should include a tool_ref entry")
def step_m2_resolved_has_tool_ref(context: Context) -> None:
    entries = context._m2_resolved_tools
    has_ref = any(not e.is_inline for e in entries)
    assert has_ref, "Expected at least one tool_ref entry in resolved tools"


@then("the M2 resolved tools should include an inline entry")
def step_m2_resolved_has_inline(context: Context) -> None:
    entries = context._m2_resolved_tools
    has_inline = any(e.is_inline for e in entries)
    assert has_inline, "Expected at least one inline entry in resolved tools"


@when("I list all M2 skills in the registry")
def step_m2_list_skills(context: Context) -> None:
    registry: SkillRegistry = context._m2_skill_registry
    context._m2_skill_list = registry.list_all()


@then('the M2 skill list should contain "{name}"')
def step_m2_skill_list_contains(context: Context, name: str) -> None:
    skill_list = context._m2_skill_list
    names = [m.name for m in skill_list]
    assert name in names, f"Expected '{name}' in {names}"


@then("the M2 skill list should have {count:d} entries")
def step_m2_skill_list_count(context: Context, count: int) -> None:
    skill_list = context._m2_skill_list
    assert len(skill_list) == count, f"Expected {count} entries, got {len(skill_list)}"


# ── Skill context invocation ──────────────────────────────────────


@given("I create an M2 skill context with sandbox")
def step_m2_create_skill_context(context: Context) -> None:
    tmp = Path(tempfile.mkdtemp(prefix="m2_skill_ctx_"))
    context._m2_skill_ctx = SkillContext(
        plan_id="m2-plan-001",
        project_id="m2-project-001",
        sandbox_path=tmp,
    )


@when('I register a tool invocation for "{tool_name}" in the M2 context')
def step_m2_register_invocation(context: Context, tool_name: str) -> None:
    ctx: SkillContext = context._m2_skill_ctx
    ctx.register_tool_invocation(
        tool_name=tool_name,
        input_data={"path": "test.txt"},
        output_data={"content": "hello"},
        duration_ms=1.5,
    )


@then("the M2 skill context should have {count:d} invocation records")
def step_m2_skill_ctx_invocations(context: Context, count: int) -> None:
    ctx: SkillContext = context._m2_skill_ctx
    assert len(ctx.change_tracker) == count, (
        f"Expected {count} records, got {len(ctx.change_tracker)}"
    )


# ════════════════════════════════════════════════════════════════════
# Tool lifecycle
# ════════════════════════════════════════════════════════════════════


@given('I create an M2 tool runtime with a mock tool "{name}"')
def step_m2_create_tool_runtime(context: Context, name: str) -> None:
    tool = Tool(
        name=name,
        description=f"M2 mock tool {name}",
        source=ToolSource.BUILTIN,
        tool_type=ToolType.TOOL,
        capability=ToolCapability(writes=False, read_only=True, checkpointable=False),
        timeout=300,
    )
    instance = _M2MockToolInstance(name)
    runtime = ToolRuntime(event_bus=ReactiveEventBus())
    runtime.register_tool(tool, instance)
    context._m2_runtime = runtime
    context._m2_mock_instance = instance


@given('I create an M2 execution context with plan "{plan_id}"')
def step_m2_create_exec_ctx(context: Context, plan_id: str) -> None:
    context._m2_exec_ctx = ToolExecutionContext(plan_id=plan_id)


@when('I discover M2 tool "{name}"')
def step_m2_discover_tool(context: Context, name: str) -> None:
    runtime: ToolRuntime = context._m2_runtime
    context._m2_descriptor = runtime.discover(name)


@then('the M2 discovered descriptor name should be "{name}"')
def step_m2_descriptor_name(context: Context, name: str) -> None:
    desc: ToolDescriptor = context._m2_descriptor
    assert desc.name == name, f"Expected '{name}', got '{desc.name}'"


@when('I activate M2 tool "{name}"')
def step_m2_activate_tool(context: Context, name: str) -> None:
    runtime: ToolRuntime = context._m2_runtime
    ctx: ToolExecutionContext = context._m2_exec_ctx
    runtime.activate(name, ctx)


@when('I execute M2 tool "{name}" with params text "{text}"')
def step_m2_execute_tool(context: Context, name: str, text: str) -> None:
    runtime: ToolRuntime = context._m2_runtime
    ctx: ToolExecutionContext = context._m2_exec_ctx
    context._m2_exec_result = runtime.execute(name, {"text": text}, ctx)


@then("the M2 execution result should be successful")
def step_m2_exec_result_success(context: Context) -> None:
    result: ToolResult = context._m2_exec_result
    assert result.success, f"Expected success, got error: {result.error}"


@when('I deactivate M2 tool "{name}"')
def step_m2_deactivate_tool(context: Context, name: str) -> None:
    runtime: ToolRuntime = context._m2_runtime
    ctx: ToolExecutionContext = context._m2_exec_ctx
    runtime.deactivate(name, ctx)


@then("the M2 tool should be deactivated")
def step_m2_tool_deactivated(context: Context) -> None:
    instance: _M2MockToolInstance = context._m2_mock_instance
    assert instance.is_deactivated, "Expected mock tool to be deactivated"


# ── Tool registry ─────────────────────────────────────────────────


@given("I create an M2 tool registry")
def step_m2_create_tool_registry(context: Context) -> None:
    context._m2_tool_registry = ToolRegistry()


def _m2_noop_handler(**kwargs: Any) -> dict[str, bool]:
    """Typed no-op handler for M2 tool registry test specs."""
    return {"ok": True}


@when('I register an M2 tool spec "{name}"')
def step_m2_register_tool_spec(context: Context, name: str) -> None:
    registry: ToolRegistry = context._m2_tool_registry
    spec = ToolSpec(
        name=name,
        description=f"M2 test tool {name}",
        handler=_m2_noop_handler,
    )
    registry.register(spec)


@then('getting M2 tool "{name}" should return a spec')
def step_m2_get_tool_spec(context: Context, name: str) -> None:
    registry: ToolRegistry = context._m2_tool_registry
    spec = registry.get(name)
    assert spec is not None, f"Expected spec for '{name}', got None"


@then('listing M2 tools should include "{name}"')
def step_m2_list_tools_include(context: Context, name: str) -> None:
    registry: ToolRegistry = context._m2_tool_registry
    specs = registry.list_tools()
    names = [s.name for s in specs]
    assert name in names, f"Expected '{name}' in {names}"


# ════════════════════════════════════════════════════════════════════
# MCP stub tool discovery and invocation
# ════════════════════════════════════════════════════════════════════


def _get_mcp_stub_class() -> type:
    """Lazily import McpStubServer to avoid top-level sys.path mutation."""
    import sys as _sys

    _mocks_dir = str(Path(__file__).resolve().parent.parent / "mocks")
    if _mocks_dir not in _sys.path:
        _sys.path.insert(0, _mocks_dir)

    from mcp_stub_server import McpStubServer

    return McpStubServer


@given("I start the M2 MCP stub server")
def step_m2_start_mcp_stub(context: Context) -> None:
    cls = _get_mcp_stub_class()
    server = cls()
    server.start()
    context._m2_mcp_stub = server


@when("I discover tools from the M2 MCP stub")
def step_m2_discover_mcp_tools(context: Context) -> None:
    server = context._m2_mcp_stub
    context._m2_mcp_tools = server.discover()


@then("the M2 stub should expose {count:d} tools")
def step_m2_stub_tool_count(context: Context, count: int) -> None:
    tools = context._m2_mcp_tools
    assert len(tools) == count, f"Expected {count} tools, got {len(tools)}"


@then('the M2 stub tools should include "{name}"')
def step_m2_stub_tools_include(context: Context, name: str) -> None:
    tools = context._m2_mcp_tools
    names = [t.name for t in tools]
    assert name in names, f"Expected '{name}' in {names}"


@when('I invoke M2 MCP stub tool "{name}" with query "{query}"')
def step_m2_invoke_mcp_search(context: Context, name: str, query: str) -> None:
    server = context._m2_mcp_stub
    context._m2_mcp_result = server.invoke(name, {"query": query})


@then("the M2 MCP stub result should contain results")
def step_m2_mcp_result_has_results(context: Context) -> None:
    result = context._m2_mcp_result
    assert "results" in result, f"Expected 'results' key in {result}"
    assert len(result["results"]) > 0, "Expected non-empty results"


@then("I stop the M2 MCP stub server")
def step_m2_stop_mcp_stub(context: Context) -> None:
    server = context._m2_mcp_stub
    server.stop()
    assert not server.is_running, "Expected MCP stub to be stopped"


# ── MCP stub lifecycle guards ─────────────────────────────────────


@given("I have an M2 MCP stub server that is stopped")
def step_m2_mcp_stub_stopped(context: Context) -> None:
    cls = _get_mcp_stub_class()
    context._m2_mcp_stub = cls()


@when("I try to discover tools from the stopped M2 stub")
def step_m2_discover_stopped(context: Context) -> None:
    server = context._m2_mcp_stub
    try:
        server.discover()
        context._m2_mcp_error = None
    except RuntimeError as exc:
        context._m2_mcp_error = exc


@then("an M2 RuntimeError should be raised")
def step_m2_runtime_error_raised(context: Context) -> None:
    err = context._m2_mcp_error
    assert err is not None, "Expected RuntimeError but none was raised"
    assert isinstance(err, RuntimeError)


@when('I start and invoke M2 MCP stub tool "{name}" with url "{url}"')
def step_m2_start_and_invoke_fetch(context: Context, name: str, url: str) -> None:
    server = context._m2_mcp_stub
    server.start()
    context._m2_mcp_result = server.invoke(name, {"url": url})


@then("the M2 MCP stub fetch result should have status {status:d}")
def step_m2_mcp_fetch_status(context: Context, status: int) -> None:
    result = context._m2_mcp_result
    assert result.get("status") == status, (
        f"Expected status {status}, got {result.get('status')}"
    )


@then("the M2 MCP stub invocation log should have {count:d} entries")
def step_m2_mcp_invocation_log(context: Context, count: int) -> None:
    server = context._m2_mcp_stub
    log = server.invocation_log
    assert len(log) == count, f"Expected {count} entries, got {len(log)}"
