# pyright: reportRedeclaration=false
"""Step definitions for v3 actor route synthesis (issue #10807).

Tests that ReactiveConfigParser synthesises execution routes for
type:llm and type:tool actors in both the flat v3 format and the
nested ``actors:`` map format.
"""

from __future__ import annotations

import asyncio
from typing import Any
from unittest.mock import MagicMock

from behave import given, then, when

from cleveragents.reactive.application import ReactiveCleverAgentsApp
from cleveragents.reactive.config_parser import ReactiveConfig, ReactiveConfigParser
from cleveragents.reactive.route import RouteType
from cleveragents.reactive.stream_router import SimpleLLMAgent
from cleveragents.reactive.stream_router import SimpleLLMAgent


def _run_async(coro: Any) -> Any:
    """Run an async coroutine synchronously for BDD steps."""
    loop = asyncio.new_event_loop()
    try:
        return loop.run_until_complete(coro)
    finally:
        loop.close()


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


@given("a flat v3 LLM actor config for route synthesis")
def step_flat_v3_llm_config(context: Any) -> None:
    context.v3_route_config = {
        "name": "local/test-llm-route",
        "type": "llm",
        "description": "A test LLM actor for route synthesis",
        "model": "openai/gpt-4",
        "system_prompt": "You are helpful",
    }


@given("a flat v3 tool actor config for route synthesis")
def step_flat_v3_tool_config(context: Any) -> None:
    context.v3_route_config = {
        "name": "local/test-tool-route",
        "type": "tool",
        "description": "A test tool actor for route synthesis",
        "model": "",
        "tools": ["local/search"],
    }


@given("a nested actors map config with cleveragents version 3")
def step_nested_actors_map_config(context: Any) -> None:
    context.nested_actors_config = {
        "name": "local/rune-strategist",
        "cleveragents": {
            "version": "3.0",
            "default_actor": "strategist",
        },
        "actors": {
            "strategist": {
                "type": "llm",
                "config": {
                    "actor": "anthropic/claude-sonnet-4-5",
                    "temperature": 0.3,
                },
            },
        },
    }


@given("a nested actors map config with default_actor set")
def step_nested_actors_default_actor(context: Any) -> None:
    context.nested_actors_config = {
        "name": "local/multi-actor",
        "cleveragents": {
            "version": "3.0",
            "default_actor": "secondary",
        },
        "actors": {
            "primary": {
                "type": "llm",
                "config": {"actor": "openai/gpt-4"},
            },
            "secondary": {
                "type": "llm",
                "config": {"actor": "anthropic/claude-sonnet-4-5"},
            },
        },
    }
    context.expected_default_actor = "secondary"


@given("a nested actors map config without default_actor")
def step_nested_actors_no_default(context: Any) -> None:
    context.nested_actors_config = {
        "name": "local/single-actor",
        "actors": {
            "only_agent": {
                "type": "llm",
                "config": {"actor": "openai/gpt-4"},
            },
        },
    }
    context.expected_first_agent = "only_agent"


@given("a config with agents and explicit routes")
def step_config_with_explicit_routes(context: Any) -> None:
    context.explicit_routes_config = {
        "agents": {
            "main": {
                "type": "llm",
                "config": {"provider": "openai", "model": "gpt-4"},
            },
        },
        "routes": {
            "my_graph": {
                "type": "graph",
                "nodes": {
                    "main": {"type": "agent", "agent": "main"},
                },
                "edges": [],
                "entry_point": "main",
            },
        },
    }


@given("a ReactiveCleverAgentsApp configured from a flat v3 LLM config")
def step_app_from_flat_v3(context: Any) -> None:
    config_data = {
        "name": "local/test-e2e-llm",
        "type": "llm",
        "description": "E2E test LLM actor",
        "model": "openai/gpt-4",
        "system_prompt": "You are helpful",
    }
    parser = ReactiveConfigParser()
    rc = parser._build(config_data)

    # Build a minimal app with a FakeListLLM-backed SimpleLLMAgent so that
    # run_single_shot() exercises the actual GraphExecutor → agent path.
    from langchain_community.llms import FakeListLLM

    app = ReactiveCleverAgentsApp.__new__(ReactiveCleverAgentsApp)
    app.config = rc
    app.stream_router = MagicMock()
    fake_llm = FakeListLLM(responses=["Test LLM response"])
    agent = SimpleLLMAgent(
        name="local/test-e2e-llm", config=rc.agents["local/test-e2e-llm"].config
    )
    agent._llm = fake_llm  # bypass provider registry
    app.stream_router.agents = {"local/test-e2e-llm": agent}
    context.e2e_app = app


@given("a ReactiveCleverAgentsApp configured from a nested actors map config")
def step_app_from_nested_actors(context: Any) -> None:
    config_data = {
        "name": "local/test-e2e-nested",
        "cleveragents": {
            "version": "3.0",
            "default_actor": "strategist",
        },
        "actors": {
            "strategist": {
                "type": "llm",
                "config": {
                    "actor": "anthropic/claude-sonnet-4-5",
                    "temperature": 0.3,
                },
            },
        },
    }
    parser = ReactiveConfigParser()
    rc = parser._build(config_data)

    from langchain_community.llms import FakeListLLM

    app = ReactiveCleverAgentsApp.__new__(ReactiveCleverAgentsApp)
    app.config = rc
    app.stream_router = MagicMock()
    fake_llm = FakeListLLM(responses=["Nested actors response"])
    agent = SimpleLLMAgent(name="strategist", config=rc.agents["strategist"].config)
    agent._llm = fake_llm  # bypass provider registry
    app.stream_router.agents = {"strategist": agent}
    context.e2e_app = app


@given("a v3 graph actor config dict with route for regression check")
def step_v3_graph_regression(context: Any) -> None:
    context.v3_route_config = {
        "name": "local/test-graph-regression",
        "type": "graph",
        "description": "Regression check for graph route synthesis",
        "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"],
        },
    }


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


@when("I build the flat v3 config through ReactiveConfigParser")
def step_build_flat_v3(context: Any) -> None:
    parser = ReactiveConfigParser()
    context.reactive_config = parser._build(context.v3_route_config)


@when("I build the nested actors config through ReactiveConfigParser")
def step_build_nested_actors(context: Any) -> None:
    parser = ReactiveConfigParser()
    context.reactive_config = parser._build(context.nested_actors_config)


@when("I build the explicit routes config through ReactiveConfigParser")
def step_build_explicit_routes(context: Any) -> None:
    parser = ReactiveConfigParser()
    context.reactive_config = parser._build(context.explicit_routes_config)


@when("I build the v3 graph config through ReactiveConfigParser")
def step_build_v3_graph(context: Any) -> None:
    parser = ReactiveConfigParser()
    context.reactive_config = parser._build(context.v3_route_config)


@when("I call run_single_shot with a test prompt")
def step_call_run_single_shot(context: Any) -> None:
    context.run_result = _run_async(
        context.e2e_app.run_single_shot("Describe a concise strategy")
    )


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


@then("the reactive config routes should not be empty")
def step_routes_not_empty(context: Any) -> None:
    rc: ReactiveConfig = context.reactive_config
    assert rc.routes, (
        f"Expected non-empty routes, got empty. Agents: {list(rc.agents.keys())}"
    )


@then("the reactive config should have exactly one route")
def step_exactly_one_route(context: Any) -> None:
    rc: ReactiveConfig = context.reactive_config
    assert len(rc.routes) == 1, (
        f"Expected exactly 1 route, got {len(rc.routes)}: {list(rc.routes.keys())}"
    )


@then("the synthesised route should be a graph route")
def step_route_is_graph(context: Any) -> None:
    rc: ReactiveConfig = context.reactive_config
    route = next(iter(rc.routes.values()))
    assert route.type == RouteType.GRAPH, f"Expected GRAPH route, got {route.type}"


@then("the synthesised route entry point should be the router")
def step_route_entry_is_router(context: Any) -> None:
    rc: ReactiveConfig = context.reactive_config
    route = next(iter(rc.routes.values()))
    assert route.entry_point == "__router__", (
        f"Expected entry_point '__router__', got '{route.entry_point}'"
    )


@then("the synthesised route should have exactly two nodes")
def step_route_two_nodes(context: Any) -> None:
    rc: ReactiveConfig = context.reactive_config
    route = next(iter(rc.routes.values()))
    assert len(route.nodes) == 2, (
        f"Expected 2 nodes (router + actor), got {len(route.nodes)}: "
        f"{list(route.nodes.keys())}"
    )


@then("the synthesised route should have a message router node")
def step_route_has_router(context: Any) -> None:
    rc: ReactiveConfig = context.reactive_config
    route = next(iter(rc.routes.values()))
    assert "__router__" in route.nodes, (
        f"Expected '__router__' node in route nodes: {list(route.nodes.keys())}"
    )
    router_data = route.nodes["__router__"]
    assert router_data.get("type") == "message_router", (
        f"Expected message_router type, got '{router_data.get('type')}'"
    )
    rules = router_data.get("rules", [])
    assert len(rules) >= 1, "Expected at least one routing rule"


@then("the synthesised route node should reference the actor name")
def step_route_node_references_actor(context: Any) -> None:
    rc: ReactiveConfig = context.reactive_config
    route = next(iter(rc.routes.values()))
    actor_name = next(iter(rc.agents.keys()))
    assert actor_name in route.nodes, (
        f"Expected node '{actor_name}' in route nodes: {list(route.nodes.keys())}"
    )
    node_data = route.nodes[actor_name]
    assert node_data.get("agent") == actor_name, (
        f"Expected node agent '{actor_name}', got '{node_data.get('agent')}'"
    )


@then("the synthesised route should have an edge to end")
def step_route_edge_to_end(context: Any) -> None:
    rc: ReactiveConfig = context.reactive_config
    route = next(iter(rc.routes.values()))
    assert len(route.edges) >= 1, f"Expected at least 1 edge, got {len(route.edges)}"
    targets = [e.get("target") for e in route.edges if isinstance(e, dict)]
    assert "end" in targets, f"Expected an edge to 'end', got targets: {targets}"


@then("the synthesised route should target the default actor")
def step_route_targets_default(context: Any) -> None:
    rc: ReactiveConfig = context.reactive_config
    route = next(iter(rc.routes.values()))
    expected = context.expected_default_actor
    # The default actor should appear as an actor node in the route.
    assert expected in route.nodes, (
        f"Expected '{expected}' as a node in route, "
        f"got nodes: {list(route.nodes.keys())}"
    )
    node_data = route.nodes[expected]
    assert node_data.get("type") == "actor", (
        f"Expected node type 'actor', got '{node_data.get('type')}'"
    )


@then("the synthesised route should target the first agent")
def step_route_targets_first(context: Any) -> None:
    rc: ReactiveConfig = context.reactive_config
    route = next(iter(rc.routes.values()))
    expected = context.expected_first_agent
    assert expected in route.nodes, (
        f"Expected '{expected}' as a node in route, "
        f"got nodes: {list(route.nodes.keys())}"
    )
    node_data = route.nodes[expected]
    assert node_data.get("type") == "actor", (
        f"Expected node type 'actor', got '{node_data.get('type')}'"
    )


@then("the route should be the explicitly defined one")
def step_route_is_explicit(context: Any) -> None:
    rc: ReactiveConfig = context.reactive_config
    assert "my_graph" in rc.routes, (
        f"Expected 'my_graph' route, got: {list(rc.routes.keys())}"
    )


@then("the result should be non-empty")
def step_result_non_empty(context: Any) -> None:
    assert context.run_result, f"Expected non-empty result, got: '{context.run_result}'"


# ── Fix C: actor key → provider/model translation ──────────────────────


@given('a nested actors map config with actor key "{actor_ref}"')
def step_nested_actors_with_actor_key(context: Any, actor_ref: str) -> None:
    context.nested_actors_config = {
        "actors": {
            "test_agent": {
                "type": "llm",
                "config": {
                    "actor": actor_ref,
                    "temperature": 0.3,
                },
            },
        },
    }


@given(
    'a nested actors map config with explicit provider "{provider}" and model "{model}"'
)
def step_nested_actors_explicit_provider_model(
    context: Any, provider: str, model: str
) -> None:
    context.nested_actors_config = {
        "actors": {
            "test_agent": {
                "type": "llm",
                "config": {
                    "provider": provider,
                    "model": model,
                    "temperature": 0.5,
                },
            },
        },
    }


@then('the agent config should have provider "{expected_provider}"')
def step_agent_config_has_provider(context: Any, expected_provider: str) -> None:
    rc: ReactiveConfig = context.reactive_config
    agent = next(iter(rc.agents.values()))
    actual = agent.config.get("provider")
    assert actual == expected_provider, (
        f"Expected provider '{expected_provider}', got '{actual}'"
    )


@then('the agent config should have model "{expected_model}"')
def step_agent_config_has_model(context: Any, expected_model: str) -> None:
    rc: ReactiveConfig = context.reactive_config
    agent = next(iter(rc.agents.values()))
    actual = agent.config.get("model")
    assert actual == expected_model, (
        f"Expected model '{expected_model}', got '{actual}'"
    )


@then("the agent config should not have provider set")
def step_agent_config_no_provider(context: Any) -> None:
    rc: ReactiveConfig = context.reactive_config
    agent = next(iter(rc.agents.values()))
    assert "provider" not in agent.config, (
        f"Expected no provider key in config, but found: '{agent.config.get('provider')}'"
    )


# ── Fix: explicit provider field (issue #10926) ──────────────────────────


@given("a flat v3 LLM actor config with explicit provider and model without slash")
def step_flat_v3_explicit_provider_no_slash(context: Any) -> None:
    """v3 LLM actor with explicit ``provider`` and a model that lacks a ``/``."""
    context.v3_route_config = {
        "name": "local/test-anthropic-bare",
        "type": "llm",
        "description": "Anthropic actor with bare model identifier",
        "provider": "anthropic",
        "model": "claude-sonnet-4-20250514",
        "system_prompt": "You are helpful",
    }


@given(
    "a flat v3 LLM actor config with model containing slash and no explicit provider"
)
def step_flat_v3_model_with_slash_no_provider(context: Any) -> None:
    """v3 LLM actor with no ``provider`` field; provider inferred from model slash."""
    context.v3_route_config = {
        "name": "local/test-openrouter",
        "type": "llm",
        "description": "OpenRouter actor inferred from model slash",
        "model": "openrouter/anthropic-claude-sonnet",
        "system_prompt": "You are helpful",
    }


@given("a flat v3 LLM actor config with model without slash and no explicit provider")
def step_flat_v3_model_no_slash_no_provider(context: Any) -> None:
    """v3 LLM actor with no ``provider`` and model has no ``/``; should get custom."""
    context.v3_route_config = {
        "name": "local/test-custom",
        "type": "llm",
        "description": "Custom actor with bare model and no explicit provider",
        "model": "gpt-4-turbo",
        "system_prompt": "You are helpful",
    }
