# pyright: reportRedeclaration=false
"""Step definitions for v3 Actor YAML schema support (core scenarios).

Extended scenarios (tool actors, update mode, LSP dict, context
propagation) are in ``actor_v3_schema_extended_steps.py``.
"""

from __future__ import annotations

from typing import Any
from unittest.mock import MagicMock, patch

import yaml
from behave import given, then, when

from cleveragents.actor.compiler import CompilationMetadata, CompiledActor
from cleveragents.actor.config import ActorConfiguration
from cleveragents.actor.registry import ActorRegistry
from cleveragents.core.exceptions import NotFoundError, ValidationError
from cleveragents.domain.models.core.actor import Actor
from cleveragents.reactive.config_parser import ReactiveConfigParser


def _make_actor(
    *,
    name: str = "local/test-actor",
    provider: str = "custom",
    model: str = "gpt-4",
    config_blob: dict[str, Any] | None = None,
    yaml_text: str | None = None,
    compiled_metadata: dict[str, Any] | None = None,
    graph_descriptor: dict[str, Any] | None = None,
) -> Actor:
    blob = config_blob or {}
    return Actor(
        id=1,
        name=name,
        provider=provider,
        model=model,
        config_blob=blob,
        config_hash=Actor.compute_hash(blob),
        graph_descriptor=graph_descriptor,
        unsafe=False,
        is_built_in=False,
        is_default=False,
        yaml_text=yaml_text,
        schema_version="1.0",
        compiled_metadata=compiled_metadata,
    )


# ── Given steps ──────────────────────────────────────────────────────────


@given('a v3 LLM actor blob with model "{model}"')
def step_v3_llm_blob(context: Any, model: str) -> None:
    context.v3_blob = {
        "name": "local/test-llm",
        "type": "llm",
        "description": "A test LLM actor",
        "model": model,
    }


@given("a v3 graph actor blob with a route block")
def step_v3_graph_blob(context: Any) -> None:
    context.v3_blob = {
        "name": "local/test-graph",
        "type": "graph",
        "description": "A test graph actor",
        "model": "gpt-4",
        "route": {
            "nodes": [
                {
                    "id": "planner",
                    "type": "agent",
                    "name": "Planner",
                    "description": "Plans tasks",
                    "config": {"model": "gpt-4"},
                },
                {
                    "id": "executor",
                    "type": "tool",
                    "name": "Executor",
                    "description": "Executes tasks",
                    "config": {"tool_name": "exec/run"},
                },
            ],
            "edges": [{"from_node": "planner", "to_node": "executor"}],
            "entry_node": "planner",
            "exit_nodes": ["executor"],
        },
    }


@given("a mock actor service for v3 testing")
def step_mock_actor_service(context: Any) -> None:
    mock_actor_service = MagicMock()

    def upsert_side_effect(**kwargs: Any) -> Actor:
        return _make_actor(
            name=kwargs.get("name", "local/test"),
            provider=kwargs.get("provider", "custom"),
            model=kwargs.get("model", "gpt-4"),
            config_blob=kwargs.get("config_blob"),
            yaml_text=kwargs.get("yaml_text"),
            compiled_metadata=kwargs.get("compiled_metadata"),
            graph_descriptor=kwargs.get("graph_descriptor"),
        )

    mock_actor_service.upsert_actor.side_effect = upsert_side_effect
    # C4: use NotFoundError instead of generic Exception.
    mock_actor_service.get_actor.side_effect = NotFoundError(
        "Actor not found", resource_type="actor", resource_id="unknown"
    )

    mock_provider_registry = MagicMock()
    mock_provider_registry.get_configured_providers.return_value = []

    mock_settings = MagicMock()

    context.mock_actor_service = mock_actor_service
    context.registry = ActorRegistry(
        actor_service=mock_actor_service,
        provider_registry=mock_provider_registry,
        settings=mock_settings,
    )


@given("a v3 LLM actor YAML text")
def step_v3_llm_yaml_text(context: Any) -> None:
    context.v3_yaml_text = yaml.safe_dump(
        {
            "name": "local/test-llm",
            "type": "llm",
            "description": "A test LLM actor for v3 schema",
            "model": "gpt-4",
            "skills": ["local/file-ops", "local/code-search"],
            "lsp": ["local/pyright"],
        }
    )


@given("a v3 actor YAML text missing description")
def step_v3_yaml_missing_desc(context: Any) -> None:
    context.v3_yaml_invalid = yaml.safe_dump(
        {
            "name": "local/test-no-desc",
            "type": "llm",
            "model": "gpt-4",
        }
    )


@given("a v3 graph actor YAML text with route")
def step_v3_graph_yaml_text(context: Any) -> None:
    context.v3_graph_yaml_text = yaml.safe_dump(
        {
            "name": "local/test-graph",
            "type": "graph",
            "description": "A test graph actor",
            "model": "gpt-4",
            "skills": ["local/file-ops"],
            "route": {
                "nodes": [
                    {
                        "id": "planner",
                        "type": "agent",
                        "name": "Planner",
                        "description": "Plans tasks",
                        "config": {"model": "gpt-4"},
                    },
                    {
                        "id": "executor",
                        "type": "tool",
                        "name": "Executor",
                        "description": "Executes tasks",
                        "config": {"tool_name": "exec/run"},
                    },
                ],
                "edges": [{"from_node": "planner", "to_node": "executor"}],
                "entry_node": "planner",
                "exit_nodes": ["executor"],
            },
        }
    )


@given("a v3 LLM actor config dict")
def step_v3_llm_config_dict(context: Any) -> None:
    context.v3_config = {
        "name": "local/test-llm",
        "type": "llm",
        "description": "A test LLM actor",
        "model": "gpt-4",
        "system_prompt": "You are helpful",
        "skills": ["local/file-ops"],
        "lsp": ["local/pyright"],
        "tools": ["local/search"],
    }


@given("a v3 graph actor config dict with route")
def step_v3_graph_config_dict(context: Any) -> None:
    context.v3_config = {
        "name": "local/test-graph",
        "type": "graph",
        "description": "A test graph actor",
        "model": "gpt-4",
        "route": {
            "nodes": [
                {
                    "id": "planner",
                    "type": "agent",
                    "name": "Planner",
                    "description": "Plans",
                    "config": {},
                },
                {
                    "id": "executor",
                    "type": "tool",
                    "name": "Executor",
                    "description": "Executes",
                    "config": {},
                },
            ],
            "edges": [{"from_node": "planner", "to_node": "executor"}],
            "entry_node": "planner",
            "exit_nodes": ["executor"],
        },
    }


@given("an existing actor in the mock service")
def step_existing_actor(context: Any) -> None:
    """Set up mock to return an existing actor for duplicate checks."""
    context.mock_actor_service.get_actor.side_effect = None
    context.mock_actor_service.get_actor.return_value = _make_actor(
        name="local/test-llm",
    )


# ── When steps ───────────────────────────────────────────────────────────


@when("I call ActorConfiguration.from_blob with the v3 blob")
def step_call_from_blob_v3(context: Any) -> None:
    context.actor_config = ActorConfiguration.from_blob(
        blob=context.v3_blob,
        name="local/test",
    )


@when("I call ActorRegistry.add with the v3 YAML")
def step_call_registry_add_v3(context: Any) -> None:
    context.result_actor = context.registry.add(context.v3_yaml_text)
    context.upsert_kwargs = context.mock_actor_service.upsert_actor.call_args


@when("I call ActorRegistry.add with the invalid v3 YAML")
def step_call_registry_add_invalid_v3(context: Any) -> None:
    try:
        context.registry.add(context.v3_yaml_invalid)
        context.add_error = None
    except ValidationError as exc:
        context.add_error = exc


@when("I call ActorRegistry.add with the v3 graph YAML")
def step_call_registry_add_v3_graph(context: Any) -> None:
    # M16: mock compile_actor to return controlled metadata.
    mock_metadata = CompilationMetadata(
        node_ids=["planner", "executor"],
        tool_nodes=["executor"],
        lsp_bindings=[],
        subgraph_refs={},
        entry_node="planner",
        exit_nodes=["executor"],
    )
    mock_compiled = CompiledActor(
        name="local/test-graph",
        nodes={},
        edges=[],
        entry_point="planner",
        metadata=mock_metadata,
    )
    with patch(
        "cleveragents.actor.compiler.compile_actor",
        return_value=mock_compiled,
    ):
        context.result_actor = context.registry.add(context.v3_graph_yaml_text)
        context.upsert_kwargs = context.mock_actor_service.upsert_actor.call_args


@when("I parse the v3 config through ReactiveConfigParser")
def step_parse_v3_config(context: Any) -> None:
    parser = ReactiveConfigParser()
    context.reactive_config = parser._build(context.v3_config)


# ── Then steps ───────────────────────────────────────────────────────────


@then('the configuration should have provider "{provider}"')
def step_config_has_provider(context: Any, provider: str) -> None:
    assert context.actor_config.provider == provider, (
        f"Expected provider '{provider}', got '{context.actor_config.provider}'"
    )


@then('the configuration should have model "{model}"')
def step_config_has_model(context: Any, model: str) -> None:
    assert context.actor_config.model == model, (
        f"Expected model '{model}', got '{context.actor_config.model}'"
    )


@then('the configuration should have a graph_descriptor with type "{gtype}"')
def step_config_has_graph_descriptor(context: Any, gtype: str) -> None:
    gd = context.actor_config.graph_descriptor
    assert gd is not None, "Expected graph_descriptor, got None"
    assert gd.get("type") == gtype, (
        f"Expected graph_descriptor type '{gtype}', got '{gd.get('type')}'"
    )
    # Verify key graph descriptor fields are present.
    route = gd.get("route")
    assert isinstance(route, dict), (
        f"Expected graph_descriptor to contain 'route' dict, got {type(route)}"
    )
    assert "nodes" in route, f"Expected 'nodes' in route: {route}"
    assert "edges" in route, f"Expected 'edges' in route: {route}"
    assert "entry_node" in route, f"Expected 'entry_node' in route: {route}"


@then("the actor should be persisted with v3 fields in config_blob")
def step_actor_persisted_v3(context: Any) -> None:
    call_kwargs = context.upsert_kwargs
    assert call_kwargs is not None, "upsert_actor was not called"
    _, kwargs = call_kwargs
    blob = kwargs.get("config_blob", {})
    assert blob.get("type") == "llm", (
        f"Expected type 'llm' in config_blob, got '{blob.get('type')}'"
    )
    assert blob.get("description") == "A test LLM actor for v3 schema", (
        f"Missing or wrong description: {blob.get('description')}"
    )


@then("the persisted config_blob should contain skills")
def step_blob_has_skills(context: Any) -> None:
    _, kwargs = context.upsert_kwargs
    blob = kwargs.get("config_blob", {})
    skills = blob.get("skills", [])
    assert len(skills) == 2, f"Expected 2 skills, got {len(skills)}"
    assert "local/file-ops" in skills


@then("the persisted config_blob should contain lsp bindings")
def step_blob_has_lsp(context: Any) -> None:
    _, kwargs = context.upsert_kwargs
    blob = kwargs.get("config_blob", {})
    lsp = blob.get("lsp")
    assert lsp is not None, "Expected lsp in config_blob"
    assert "local/pyright" in lsp


@then("a ValidationError should be raised mentioning description")
def step_validation_error_description(context: Any) -> None:
    assert context.add_error is not None, "Expected a ValidationError to be raised"
    error_msg = str(context.add_error).lower()
    assert "description" in error_msg, (
        f"Error message should mention 'description': {context.add_error}"
    )


@then("the actor should be persisted with compiled metadata")
def step_actor_has_compiled_metadata(context: Any) -> None:
    _, kwargs = context.upsert_kwargs
    compiled = kwargs.get("compiled_metadata")
    assert compiled is not None, "Expected compiled_metadata, got None"
    assert "node_ids" in compiled, (
        f"compiled_metadata should contain node_ids: {compiled}"
    )


@then("the graph_descriptor should contain route information")
def step_graph_descriptor_route(context: Any) -> None:
    _, kwargs = context.upsert_kwargs
    gd = kwargs.get("graph_descriptor")
    assert gd is not None, "Expected graph_descriptor, got None"
    assert "route" in gd, f"graph_descriptor should contain 'route': {gd}"


@then("the reactive config should have one agent")
def step_reactive_has_one_agent(context: Any) -> None:
    agents = context.reactive_config.agents
    assert len(agents) == 1, f"Expected 1 agent, got {len(agents)}"


@then("the agent config should contain the model")
def step_agent_config_has_model(context: Any) -> None:
    agents = context.reactive_config.agents
    agent = next(iter(agents.values()))
    assert agent.config.get("model") == "gpt-4", (
        f"Agent model mismatch: {agent.config.get('model')}"
    )


@then("the agent config should contain skills")
def step_agent_config_has_skills(context: Any) -> None:
    agents = context.reactive_config.agents
    agent = next(iter(agents.values()))
    skills = agent.config.get("skills", [])
    assert "local/file-ops" in skills, f"Expected 'local/file-ops' in skills: {skills}"


@then("the reactive config should have agents for each node")
def step_reactive_has_node_agents(context: Any) -> None:
    agents = context.reactive_config.agents
    assert "planner" in agents, f"Missing 'planner' agent: {list(agents.keys())}"
    assert "executor" in agents, f"Missing 'executor' agent: {list(agents.keys())}"


@then("the reactive config should have a graph route")
def step_reactive_has_graph_route(context: Any) -> None:
    routes = context.reactive_config.routes
    assert len(routes) >= 1, f"Expected at least 1 route, got {len(routes)}"
    route = next(iter(routes.values()))
    assert route.type.value == "graph", f"Expected graph route, got {route.type.value}"
    # Nit: also check entry_point and edges.
    assert route.entry_point == "planner", (
        f"Expected entry_point 'planner', got '{route.entry_point}'"
    )
    assert len(route.edges) >= 1, f"Expected at least 1 edge, got {len(route.edges)}"


@then("the graph route edges should use source and target keys")
def step_graph_edges_use_source_target(context: Any) -> None:
    routes = context.reactive_config.routes
    route = next(iter(routes.values()))
    for edge in route.edges:
        assert "source" in edge, f"Edge missing 'source' key: {edge}"
        assert "target" in edge, f"Edge missing 'target' key: {edge}"
        assert "from" not in edge, f"Edge should not have 'from' key: {edge}"
        assert "to" not in edge, f"Edge should not have 'to' key: {edge}"
