"""Behave steps for reactive application coverage."""

from __future__ import annotations

import asyncio
import logging
import tempfile
from pathlib import Path
from typing import Any

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

from cleveragents.core.exceptions import CleverAgentsException, UnsafeConfigurationError
from cleveragents.reactive.application import ReactiveCleverAgentsApp
from cleveragents.reactive.config_parser import AgentConfig, ReactiveConfig
from cleveragents.reactive.context_manager import ContextManager
from cleveragents.reactive.route import BridgeConfig, RouteConfig, RouteType
from cleveragents.reactive.stream_router import SimpleLLMAgent, SimpleToolAgent


def _write_config_file(data: dict[str, Any]) -> Path:
    temp_dir = Path(tempfile.mkdtemp())
    config_path = temp_dir / "config.yml"
    config_path.write_text(yaml.safe_dump(data), encoding="utf-8")
    return config_path


def _run_async(coro):
    try:
        loop = asyncio.get_event_loop()
        if loop.is_closed():
            raise RuntimeError
        if loop.is_running():
            return asyncio.run(coro)
    except Exception:  # pylint: disable=broad-except
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
    return loop.run_until_complete(coro)


class RotatingOperators:
    def __init__(self, sequences: list[list[dict[str, Any]]]):
        self.sequences = sequences
        self.index = 0

    def __iter__(self):
        sequence = self.sequences[self.index]
        self.index = (self.index + 1) % len(self.sequences)
        return iter(sequence)


class ProcessAgent:
    def process(self, message):
        return f"{message}_process"


class SkipOnceContainsDict(dict):
    def __init__(self, skip_keys: set[str]):
        super().__init__()
        self._skip_keys = {key: True for key in skip_keys}

    def __contains__(self, key):
        if self._skip_keys.get(key):
            self._skip_keys[key] = False
            return True
        return super().__contains__(key)


class EndSentinel:
    def __eq__(self, other):
        return other == "end"


class ErrorEmittingAgent:
    def __init__(self, router):
        self.router = router

    def process_message_sync(self, message, metadata=None):
        self.router.streams["__error__"].on_next("boom")
        return "ignored"


class EmptyAgent:
    def __call__(self, message):
        return ""


@given("a temporary reactive config with a stream route")
def step_temp_stream_config(context: Context) -> None:
    data = {
        "agents": {"echo_actor": {"type": "custom", "config": {}}},
        "routes": {
            "echo_stream": {
                "type": "stream",
                "agents": ["echo_actor"],
                "operators": [{"type": "map", "params": {"agent": "echo_actor"}}],
            }
        },
        "global_context": {},
    }
    context.config_path = _write_config_file(data)


@given("a temporary reactive config that requires unsafe mode")
def step_temp_unsafe_config(context: Context) -> None:
    data = {
        "agents": {"echo_actor": {"type": "custom", "config": {}}},
        "routes": {
            "echo_stream": {
                "type": "stream",
                "agents": ["echo_actor"],
                "operators": [{"type": "map", "params": {"agent": "echo_actor"}}],
            }
        },
        "global_context": {"unsafe": True},
    }
    context.config_path = _write_config_file(data)


@when(
    'I initialize the reactive app with verbose "{verbose}" and temperature override "{temperature}"'
)
def step_init_app_with_override(
    context: Context, verbose: str, temperature: str
) -> None:
    context.original_log_level = logging.getLogger().level
    context.init_error = None
    context.temperature_override = float(temperature)
    try:
        context.app = ReactiveCleverAgentsApp(
            [context.config_path],
            verbose=int(verbose),
            unsafe=True,
            temperature_override=context.temperature_override,
        )
    except Exception as exc:  # pylint: disable=broad-except
        context.init_error = exc


@when("I initialize the reactive app without the unsafe flag")
def step_init_app_without_unsafe(context: Context) -> None:
    context.init_error = None
    try:
        context.app = ReactiveCleverAgentsApp(
            [context.config_path],
            verbose=0,
            unsafe=False,
        )
    except Exception as exc:  # pylint: disable=broad-except
        context.init_error = exc


@then("the reactive app should load the configuration")
def step_assert_loaded(context: Context) -> None:
    assert context.init_error is None
    assert context.app.config is not None


@then("the root logger level should be INFO")
def step_root_logger_info(context: Context) -> None:
    assert logging.getLogger().level == logging.INFO
    if hasattr(context, "original_log_level"):
        logging.getLogger().setLevel(context.original_log_level)


@then('the config should include the temperature override "{temperature}"')
def step_temp_override_set(context: Context, temperature: str) -> None:
    assert context.app.config is not None
    assert context.app.config.global_context.get("_temperature_override") == float(
        temperature
    )


@then("the stream route should be registered")
def step_stream_route_registered(context: Context) -> None:
    assert "echo_stream" in context.app.stream_router.streams


@then("an unsafe configuration error should be raised")
def step_unsafe_error(context: Context) -> None:
    assert isinstance(context.init_error, UnsafeConfigurationError)


@given("a reactive app instance")
def step_reactive_app_instance(context: Context) -> None:
    context.original_log_level = logging.getLogger().level
    context.app = ReactiveCleverAgentsApp()


@when("I configure logging for all verbosity levels")
def step_configure_logging_levels(context: Context) -> None:
    context.logged_levels = {}
    for verbose in (0, 1, 2, 3, 4):
        context.app._configure_logging(verbose)  # pylint: disable=protected-access
        context.logged_levels[verbose] = logging.getLogger().level


@then("each verbosity should map to the expected logging level")
def step_logging_level_mappings(context: Context) -> None:
    expected = {
        0: logging.CRITICAL,
        1: logging.ERROR,
        2: logging.WARNING,
        3: logging.INFO,
        4: logging.DEBUG,
    }
    assert context.logged_levels == expected
    logging.getLogger().setLevel(context.original_log_level)


@given("a reactive app with stream operators and varied agent types")
def step_app_with_varied_agents(context: Context) -> None:
    context.app = ReactiveCleverAgentsApp()
    agents = {
        "tool_actor": AgentConfig(
            name="tool_actor",
            type="custom",
            config={"tools": [{"operation": "uppercase"}]},
        ),
        "llm_agent": AgentConfig(name="llm_agent", type="llm", config={}),
        "plain_actor": AgentConfig(name="plain_actor", type="custom", config={}),
        "loop_actor1": AgentConfig(name="loop_actor1", type="custom", config={}),
        "loop_actor2": AgentConfig(name="loop_actor2", type="custom", config={}),
        "loop_actor3": AgentConfig(name="loop_actor3", type="custom", config={}),
    }
    route = RouteConfig(
        name="agent_route",
        type=RouteType.STREAM,
        agents=["tool_actor", "plain_actor"],
        operators=[{"type": "map", "params": {"agent": "tool_actor"}}],
    )
    all_operators = [
        {"params": {"actor": "loop_actor1"}},
        {"params": {"actor": "loop_actor2"}},
        {"params": {"actor": "loop_actor3"}},
    ]
    object.__setattr__(route, "operators", all_operators)
    context.app.config = ReactiveConfig(agents=agents, routes={"agent_route": route})


@when("I register agents from the reactive config")
def step_register_agents(context: Context) -> None:
    context.app._register_agents_from_config()  # pylint: disable=protected-access


@then("the stream router should include tool and llm agent aliases")
def step_agent_aliases_present(context: Context) -> None:
    agents = context.app.stream_router.agents
    assert "tool_actor" in agents
    assert "tool_agent" in agents
    assert "llm_agent" in agents
    assert "llm_actor" in agents


@then("the operator referenced actors should be registered")
def step_operator_actors_registered(context: Context) -> None:
    agents = context.app.stream_router.agents
    assert "loop_actor1" in agents
    assert "loop_actor2" in agents
    assert "loop_actor3" in agents


@then("the tool actor should be registered as a SimpleToolAgent")
def step_tool_agent_type(context: Context) -> None:
    assert isinstance(context.app.stream_router.agents["tool_actor"], SimpleToolAgent)


@then("the llm agent should be registered as a SimpleLLMAgent")
def step_llm_agent_type(context: Context) -> None:
    assert isinstance(context.app.stream_router.agents["llm_agent"], SimpleLLMAgent)


@given("a reactive app without configuration")
def step_app_without_config(context: Context) -> None:
    context.app = ReactiveCleverAgentsApp()


@when("I load configuration from a missing file")
def step_load_missing_config(context: Context) -> None:
    context.load_error = None
    missing_path = Path(tempfile.mkdtemp()) / "missing_reactive_config.yml"
    try:
        context.app.load_configuration([missing_path])
    except Exception as exc:  # pylint: disable=broad-except
        context.load_error = exc


@then("a configuration load error should be raised")
def step_assert_load_error(context: Context) -> None:
    assert isinstance(context.load_error, CleverAgentsException)
    assert "Failed to load configuration" in str(context.load_error)


@given("a reactive app with a stream route that has a bridge config")
def step_app_stream_bridge(context: Context) -> None:
    context.app = ReactiveCleverAgentsApp()
    route = RouteConfig(
        name="bad_stream",
        type=RouteType.STREAM,
        operators=[{"type": "map", "params": {"agent": "dummy"}}],
        bridge=BridgeConfig(),
    )
    context.app.config = ReactiveConfig(routes={"bad_stream": route})


@when("I validate the reactive routes")
def step_validate_routes(context: Context) -> None:
    context.validation_error = None
    try:
        context.app._validate_routes()  # pylint: disable=protected-access
    except Exception as exc:  # pylint: disable=broad-except
        context.validation_error = exc


@then("route validation should fail for bridge config")
def step_validation_failed(context: Context) -> None:
    assert isinstance(context.validation_error, CleverAgentsException)


@given("a reactive app with stream and graph routes and a merge")
def step_app_routes_with_merge(context: Context) -> None:
    context.app = ReactiveCleverAgentsApp()
    agents = {
        "echo_actor": AgentConfig(name="echo_actor", type="custom", config={}),
    }
    stream_route = RouteConfig(
        name="echo_stream",
        type=RouteType.STREAM,
        agents=["echo_actor"],
        operators=[{"type": "map", "params": {"agent": "echo_actor"}}],
    )
    graph_route = RouteConfig(
        name="graph_route",
        type=RouteType.GRAPH,
        nodes={"node1": {"type": "actor", "actor": "echo_actor"}},
        edges=[
            {"source": "start", "target": "node1"},
            {"source": "node1", "target": "end"},
        ],
    )
    context.app.config = ReactiveConfig(
        agents=agents,
        routes={"echo_stream": stream_route, "graph_route": graph_route},
        merges=[{"sources": ["merge_source"], "target": "merge_target"}],
    )
    context.app.stream_router.create_stream("merge_source")
    context.app.stream_router.create_stream("merge_target")


@when("I build the reactive routes")
def step_build_routes(context: Context) -> None:
    context.app._build_routes()  # pylint: disable=protected-access


@then("the stream route should be created")
def step_stream_route_created(context: Context) -> None:
    assert "echo_stream" in context.app.stream_router.streams


@then("a route bridge should be initialized for graph routes")
def step_route_bridge_initialized(context: Context) -> None:
    assert context.app.route_bridge is not None


@then("merge subscriptions should forward messages")
def step_merge_forwarding(context: Context) -> None:
    captured: list[Any] = []
    context.app.stream_router.streams["merge_target"].subscribe(
        lambda msg: captured.append(getattr(msg, "content", msg))
    )
    context.app.stream_router.streams["merge_source"].on_next("merged")
    assert captured == ["merged"]


@then("rxpy stream presence should be detected")
def step_detect_rxpy_stream(context: Context) -> None:
    assert context.app._is_rxpy_stream_present() is True  # pylint: disable=protected-access


@then("the first graph route should be returned")
def step_first_graph_route(context: Context) -> None:
    route = context.app._get_graph_route()  # pylint: disable=protected-access
    assert route is not None
    assert route.name == "graph_route"


@given("a reactive app with a workflow controller graph route")
def step_workflow_controller_route(context: Context) -> None:
    context.app = ReactiveCleverAgentsApp()
    context.app.stream_router.register_agent("workflow_controller", lambda msg: msg)
    route = RouteConfig(
        name="workflow_graph",
        type=RouteType.GRAPH,
        nodes={
            "router": {
                "type": "message_router",
                "rules": [
                    {
                        "match_type": "prefix",
                        "pattern": "",
                        "target": "workflow_controller",
                    }
                ],
            },
            "workflow_controller": {"type": "actor", "actor": "workflow_controller"},
        },
        edges=[{"source": "workflow_controller", "target": "router"}],
    )
    context.app.config = ReactiveConfig(routes={"workflow_graph": route})
    context.route = route


@when('I execute the workflow controller route with prompt "{prompt}"')
def step_execute_workflow_route(context: Context, prompt: str) -> None:
    context.workflow_result = context.app._execute_graph_route(  # pylint: disable=protected-access
        prompt, context.route
    )


@then("the workflow route should return the extracted message")
def step_workflow_return(context: Context) -> None:
    assert context.workflow_result == "ROUTE_ASK_TOPIC:Hello"


@then("the graph context should include default writing metadata")
def step_graph_context_defaults(context: Context) -> None:
    ctx = context.app.config.global_context
    assert "stage_order" in ctx
    assert "writing_stage" in ctx
    assert "paper_details" in ctx
    assert "latex_generation" in ctx["stage_order"]


@given("a reactive app with a chained graph route")
def step_chained_graph_route(context: Context) -> None:
    context.app = ReactiveCleverAgentsApp()
    # SEC1: Use named operation instead of legacy code block.
    SimpleToolAgent.register_operation(
        "append_tool", lambda content, meta, ctx: str(content) + "_tool"
    )
    context.app.stream_router.register_agent(
        "tool_actor",
        SimpleToolAgent([{"operation": "append_tool"}]),
    )
    context.app.stream_router.register_agent(
        "callable_actor", lambda msg: f"{msg}_callable"
    )
    context.app.stream_router.register_agent("process_actor", ProcessAgent())

    class EdgeObj:
        def __init__(self, source: str, target: str, condition: Any = None):
            self.source = source
            self.target = target
            self.condition = condition

    route = RouteConfig(
        name="chain_graph",
        type=RouteType.GRAPH,
        nodes={
            "router": {
                "type": "message_router",
                "rules": [
                    {"match_type": "prefix", "pattern": "", "target": None},
                    {
                        "match_type": "prefix",
                        "pattern": "TOOL:",
                        "target": "tool_node",
                        "extract_message": True,
                        "separator": ":",
                    },
                ],
            },
            "tool_node": {"type": "actor", "actor": "tool_actor"},
            "callable_node": {"type": "actor", "actor": "callable_actor"},
            "process_node": {"type": "actor", "actor": "process_actor"},
        },
        edges=[{"source": "tool_node", "target": "callable_node"}],
    )
    edges: list[Any] = [
        {
            "source": "tool_node",
            "target": "router",
            "condition": {"type": "context_value"},
        },
        {"source": "tool_node", "target": "callable_node"},
        EdgeObj(
            "callable_node",
            "process_node",
            {"type": "context_value", "key": "route_key", "value": "go"},
        ),
        {"source": "process_node", "target": "end"},
    ]
    object.__setattr__(route, "edges", edges)
    context.app.config = ReactiveConfig(
        routes={"chain_graph": route}, global_context={"route_key": "go"}
    )
    context.route = route


@when('I execute the chained graph route with prompt "{prompt}"')
def step_execute_chain_route(context: Context, prompt: str) -> None:
    context.chain_result = context.app._execute_graph_route(  # pylint: disable=protected-access
        prompt, context.route
    )


@then('the chained graph route should return "{expected}"')
def step_chain_result(context: Context, expected: str) -> None:
    assert context.chain_result == expected


@given("a reactive app with a graph route without router rules")
def step_graph_route_without_rules(context: Context) -> None:
    context.app = ReactiveCleverAgentsApp()
    route = RouteConfig(
        name="empty_rules",
        type=RouteType.GRAPH,
        nodes={"actor": {"type": "actor", "actor": "missing"}},
        edges=[],
    )
    context.app.config = ReactiveConfig(routes={"empty_rules": route})
    context.route = route


@when('I execute the graph route with prompt "{prompt}"')
def step_execute_graph_route(context: Context, prompt: str) -> None:
    context.graph_prompt = prompt
    context.graph_result = context.app._execute_graph_route(  # pylint: disable=protected-access
        prompt, context.route
    )


@then("the graph route should return an empty string")
def step_graph_empty_result(context: Context) -> None:
    assert context.graph_result == ""


@given("a reactive app with a graph route targeting a missing agent")
def step_missing_agent_route(context: Context) -> None:
    context.app = ReactiveCleverAgentsApp()
    route = RouteConfig(
        name="missing_agent",
        type=RouteType.GRAPH,
        nodes={
            "router": {
                "type": "message_router",
                "rules": [
                    {
                        "match_type": "contains",
                        "pattern": "MISSING",
                        "target": "missing_node",
                    }
                ],
            }
        },
        edges=[],
    )
    context.app.config = ReactiveConfig(routes={"missing_agent": route})
    context.route = route


@when('I execute the missing agent route with prompt "{prompt}"')
def step_execute_missing_agent(context: Context, prompt: str) -> None:
    context.missing_agent_prompt = prompt
    context.missing_agent_result = context.app._execute_graph_route(  # pylint: disable=protected-access
        prompt, context.route
    )


@then("the graph route should return the original prompt")
def step_missing_agent_result(context: Context) -> None:
    assert context.missing_agent_result == context.missing_agent_prompt


@given("a reactive app with a suffix matching graph route")
def step_suffix_route(context: Context) -> None:
    context.app = ReactiveCleverAgentsApp()
    route = RouteConfig(
        name="suffix_graph",
        type=RouteType.GRAPH,
        nodes={
            "router": {
                "type": "message_router",
                "rules": [
                    {
                        "match_type": "suffix",
                        "pattern": "!",
                        "target": "end",
                    }
                ],
            }
        },
        edges=[],
    )
    context.app.config = ReactiveConfig(routes={"suffix_graph": route})
    context.route = route


@when('I execute the suffix route with prompt "{prompt}"')
def step_execute_suffix_route(context: Context, prompt: str) -> None:
    context.suffix_prompt = prompt
    context.suffix_result = context.app._execute_graph_route(  # pylint: disable=protected-access
        prompt, context.route
    )


@then("the suffix route should return the original prompt")
def step_suffix_result(context: Context) -> None:
    assert context.suffix_result == context.suffix_prompt


@given("a reactive app with a stream route configured")
def step_app_stream_route(context: Context) -> None:
    class PrefixAgent:
        def process_message_sync(self, message, metadata=None):
            return "DISCOVERY_RESPONSE:Hello\nROUTE_ASK_TOPIC:Prompt"

    context.app = ReactiveCleverAgentsApp()
    context.app.stream_router.register_agent("echo_actor", PrefixAgent())
    route = RouteConfig(
        name="echo_stream",
        type=RouteType.STREAM,
        agents=["echo_actor"],
        operators=[{"type": "map", "params": {"agent": "echo_actor"}}],
    )
    context.app.config = ReactiveConfig(routes={"echo_stream": route})
    context.app._build_routes()  # pylint: disable=protected-access


@when("I run a single shot without allowing rxpy")
def step_run_single_shot_blocked(context: Context) -> None:
    context.single_shot_error = None
    try:
        _run_async(context.app.run_single_shot("hello"))
    except Exception as exc:  # pylint: disable=broad-except
        context.single_shot_error = exc


@then("the single shot should fail with a reactive stream error")
def step_single_shot_error(context: Context) -> None:
    assert isinstance(context.single_shot_error, CleverAgentsException)


@when("I run a single shot allowing rxpy")
def step_run_single_shot_allowed(context: Context) -> None:
    context.single_shot_result = _run_async(
        context.app.run_single_shot("hello", allow_rxpy_in_run_mode=True)
    )


@then("the stream output should have routing prefixes removed")
def step_stream_prefix_stripped(context: Context) -> None:
    assert context.single_shot_result == "Hello\nPrompt"


@given("a reactive app with a graph route configured")
def step_app_graph_route(context: Context) -> None:
    context.app = ReactiveCleverAgentsApp()
    context.app.stream_router.register_agent(
        "prefix_actor", lambda _msg: "ROUTE_ASK_TOPIC:Graph"
    )
    route = RouteConfig(
        name="graph_route",
        type=RouteType.GRAPH,
        nodes={
            "router": {
                "type": "message_router",
                "rules": [
                    {
                        "match_type": "prefix",
                        "pattern": "CMD:",
                        "target": "actor_node",
                        "extract_message": True,
                        "separator": ":",
                    }
                ],
            },
            "actor_node": {"type": "actor", "actor": "prefix_actor"},
        },
        edges=[{"source": "actor_node", "target": "end"}],
    )
    context.app.config = ReactiveConfig(routes={"graph_route": route})


@when('I run a graph single shot with prompt "{prompt}"')
def step_run_graph_single_shot(context: Context, prompt: str) -> None:
    context.graph_single_shot_result = _run_async(context.app.run_single_shot(prompt))


@then("the graph output should have routing prefixes removed")
def step_graph_prefix_stripped(context: Context) -> None:
    assert context.graph_single_shot_result == "Graph"


@given("a context manager for run with context")
def step_context_manager_for_run(context: Context) -> None:
    temp_dir = Path(tempfile.mkdtemp())
    context.context_manager = ContextManager("reactive_test", context_dir=temp_dir)


@when('I run the reactive app with context and prompt "{prompt}"')
def step_run_with_context(context: Context, prompt: str) -> None:
    context.run_with_context_result = _run_async(
        context.app.run_with_context(prompt, context_manager=context.context_manager)
    )


@then("the context manager should have stored user and assistant messages")
def step_context_messages(context: Context) -> None:
    messages = context.context_manager.get_conversation_history()
    assert len(messages) == 2
    assert messages[0]["role"] == "user"
    assert messages[1]["role"] == "assistant"


@then("the context manager should persist the global context")
def step_context_global_context(context: Context) -> None:
    ctx = context.context_manager.get_global_context()
    assert "writing_stage" in ctx


@when("I call the reactive helper methods without configuration")
def step_call_helpers_without_config(context: Context) -> None:
    context.initial_agents = dict(context.app.stream_router.agents)
    context.helper_error = None
    try:
        context.app._register_agents_from_config()  # pylint: disable=protected-access
        context.app._validate_routes()  # pylint: disable=protected-access
        context.app._build_routes()  # pylint: disable=protected-access
    except Exception as exc:  # pylint: disable=broad-except
        context.helper_error = exc
    context.rxpy_present = context.app._is_rxpy_stream_present()  # pylint: disable=protected-access
    context.graph_route = context.app._get_graph_route()  # pylint: disable=protected-access


@then("the reactive helper methods should return defaults")
def step_helpers_return_defaults(context: Context) -> None:
    assert context.helper_error is None
    assert context.rxpy_present is False
    assert context.graph_route is None
    assert context.app.stream_router.agents == context.initial_agents


@given("a reactive app with deferred route and operator agents")
def step_deferred_route_agents(context: Context) -> None:
    context.app = ReactiveCleverAgentsApp()
    deferred = ["route_actor", "op1_actor", "op2_actor", "op3_actor"]
    context.deferred_agents = deferred
    agents = {
        name: AgentConfig(name=name, type="custom", config={}) for name in deferred
    }
    route = RouteConfig(
        name="deferred_route",
        type=RouteType.STREAM,
        agents=["route_actor"],
        operators=[
            {"type": "map", "params": {"actor": "op1_actor"}},
            {"type": "map", "params": {"agent": "op2_actor"}},
            {"type": "map", "params": {"actor": "op3_actor"}},
        ],
    )
    context.app.config = ReactiveConfig(agents=agents, routes={"deferred_route": route})


@when("I register deferred route agents from the reactive config")
def step_register_deferred_agents(context: Context) -> None:
    context.app._register_agents_from_config()  # pylint: disable=protected-access


@then("deferred route agents and operator aliases should be registered")
def step_deferred_agents_registered(context: Context) -> None:
    agents = context.app.stream_router.agents
    for name in context.deferred_agents:
        assert name in agents
        alias = f"{name[:-6]}_agent"
        assert alias in agents


@given(
    "a reactive app with a stream route referencing an unknown actor and a missing merge target"
)
def step_stream_unknown_actor_merge(context: Context) -> None:
    context.app = ReactiveCleverAgentsApp()
    route = RouteConfig(
        name="fallback_stream",
        type=RouteType.STREAM,
        agents=[],
        operators=[{"type": "map", "params": {"actor": "fallback_actor"}}],
    )
    context.app.config = ReactiveConfig(
        routes={"fallback_stream": route},
        merges=[{"sources": ["missing_source"], "target": "missing_target"}],
    )


@then("a fallback stream agent should be registered")
def step_fallback_agent_registered(context: Context) -> None:
    assert "fallback_actor" in context.app.stream_router.agents


@then("the missing merge target should not be created")
def step_missing_merge_target(context: Context) -> None:
    assert "missing_target" not in context.app.stream_router.streams


@given("a reactive app with a graph route using a non-dict edge condition")
def step_graph_non_dict_condition(context: Context) -> None:
    context.app = ReactiveCleverAgentsApp()
    context.app.stream_router.register_agent("echo_actor", lambda msg: msg)
    route = RouteConfig(
        name="cond_graph",
        type=RouteType.GRAPH,
        nodes={
            "router": {
                "type": "message_router",
                "rules": [
                    {"match_type": "prefix", "pattern": "", "target": "echo_node"}
                ],
            },
            "echo_node": {"type": "actor", "actor": "echo_actor"},
        },
        edges=[{"source": "echo_node", "target": "end", "condition": "always"}],
    )
    context.app.config = ReactiveConfig(routes={"cond_graph": route})
    context.route = route


@given("a reactive app with a graph route using an empty suffix rule")
def step_graph_empty_suffix(context: Context) -> None:
    context.app = ReactiveCleverAgentsApp()
    route = RouteConfig(
        name="suffix_empty",
        type=RouteType.GRAPH,
        nodes={
            "router": {
                "type": "message_router",
                "rules": [{"match_type": "suffix", "pattern": "", "target": "end"}],
            }
        },
        edges=[],
    )
    context.app.config = ReactiveConfig(routes={"suffix_empty": route})
    context.route = route


@given("a reactive app with a graph route extracting without a separator")
def step_graph_extract_no_separator(context: Context) -> None:
    context.app = ReactiveCleverAgentsApp()
    route = RouteConfig(
        name="extract_graph",
        type=RouteType.GRAPH,
        nodes={
            "router": {
                "type": "message_router",
                "rules": [
                    {
                        "match_type": "prefix",
                        "pattern": "PREFIX",
                        "target": "end",
                        "extract_message": True,
                        "separator": ":",
                    }
                ],
            }
        },
        edges=[],
    )
    context.app.config = ReactiveConfig(routes={"extract_graph": route})
    context.route = route


@given("a reactive app with a graph route using a process agent")
def step_graph_process_agent(context: Context) -> None:
    context.app = ReactiveCleverAgentsApp()
    context.app.stream_router.register_agent("proc_actor", ProcessAgent())
    route = RouteConfig(
        name="process_graph",
        type=RouteType.GRAPH,
        nodes={
            "router": {
                "type": "message_router",
                "rules": [
                    {
                        "match_type": "prefix",
                        "pattern": "PROC:",
                        "target": "proc_node",
                        "extract_message": True,
                        "separator": ":",
                    }
                ],
            },
            "proc_node": {"type": "actor", "actor": "proc_actor"},
        },
        edges=[{"source": "proc_node", "target": "end"}],
    )
    context.app.config = ReactiveConfig(routes={"process_graph": route})
    context.route = route


@given("a reactive app with a graph route using a non-callable agent")
def step_graph_non_callable_agent(context: Context) -> None:
    context.app = ReactiveCleverAgentsApp()
    context.app.stream_router.register_agent("noop_actor", object())
    route = RouteConfig(
        name="noop_graph",
        type=RouteType.GRAPH,
        nodes={
            "router": {
                "type": "message_router",
                "rules": [
                    {"match_type": "prefix", "pattern": "", "target": "noop_node"}
                ],
            },
            "noop_node": {"type": "actor", "actor": "noop_actor"},
        },
        edges=[{"source": "noop_node", "target": "end"}],
    )
    context.app.config = ReactiveConfig(routes={"noop_graph": route})
    context.route = route


@given("a reactive app with a graph route returning an empty response")
def step_graph_empty_response(context: Context) -> None:
    context.app = ReactiveCleverAgentsApp()
    context.app.stream_router.register_agent("empty_actor", EmptyAgent())
    route = RouteConfig(
        name="empty_graph",
        type=RouteType.GRAPH,
        nodes={
            "router": {
                "type": "message_router",
                "rules": [
                    {"match_type": "prefix", "pattern": "", "target": "empty_node"}
                ],
            },
            "empty_node": {"type": "actor", "actor": "empty_actor"},
        },
        edges=[{"source": "empty_node", "target": "end"}],
    )
    context.app.config = ReactiveConfig(routes={"empty_graph": route})
    context.route = route


@then("the graph route should return the prompt unchanged")
def step_graph_prompt_unchanged(context: Context) -> None:
    assert context.graph_result == context.graph_prompt


@then('the graph route should return "{expected}"')
def step_graph_route_expected(context: Context, expected: str) -> None:
    assert context.graph_result == expected


@given("a reactive app with a chained graph route that breaks to the router")
def step_chained_break_route(context: Context) -> None:
    context.app = ReactiveCleverAgentsApp()
    context.app.stream_router.register_agent("starter_actor", lambda _msg: "DONE")
    context.app.stream_router.register_agent("tool_actor", SimpleToolAgent())
    context.app.stream_router.register_agent("weird_actor", object())
    route = RouteConfig(
        name="chain_break",
        type=RouteType.GRAPH,
        nodes={
            "router": {
                "type": "message_router",
                "rules": [
                    {"match_type": "prefix", "pattern": "START", "target": "start"},
                    {"match_type": "prefix", "pattern": "DONE", "target": "end"},
                ],
            },
            "start": {"type": "actor", "actor": "starter_actor"},
            "tool_node": {"type": "actor", "actor": "tool_actor"},
            "weird_node": {"type": "actor", "actor": "weird_actor"},
        },
        edges=[
            {"source": "start", "target": "tool_node"},
            {"source": "tool_node", "target": "weird_node"},
            {"source": "weird_node", "target": "router"},
        ],
    )
    context.app.config = ReactiveConfig(routes={"chain_break": route})
    context.route = route


@when('I execute the chained break route with prompt "{prompt}"')
def step_execute_chained_break(context: Context, prompt: str) -> None:
    context.chained_break_result = context.app._execute_graph_route(  # pylint: disable=protected-access
        prompt, context.route
    )


@then('the chained break route should return "{expected}"')
def step_chained_break_result(context: Context, expected: str) -> None:
    assert context.chained_break_result == expected


@given("a reactive app with a chained graph route that has no chained targets")
def step_chained_empty_route(context: Context) -> None:
    context.app = ReactiveCleverAgentsApp()
    context.app.stream_router.register_agent("starter_actor", lambda _msg: "DONE")
    context.app.stream_router.register_agent("tool_actor", SimpleToolAgent())
    route = RouteConfig(
        name="chain_empty",
        type=RouteType.GRAPH,
        nodes={
            "router": {
                "type": "message_router",
                "rules": [
                    {"match_type": "prefix", "pattern": "BEGIN", "target": "start"},
                    {"match_type": "prefix", "pattern": "DONE", "target": "end"},
                ],
            },
            "start": {"type": "actor", "actor": "starter_actor"},
            "tool_node": {"type": "actor", "actor": "tool_actor"},
        },
        edges=[{"source": "start", "target": "tool_node"}],
    )
    context.app.config = ReactiveConfig(routes={"chain_empty": route})
    context.route = route


@when('I execute the chained empty route with prompt "{prompt}"')
def step_execute_chained_empty(context: Context, prompt: str) -> None:
    context.chained_empty_result = context.app._execute_graph_route(  # pylint: disable=protected-access
        prompt, context.route
    )


@then('the chained empty route should return "{expected}"')
def step_chained_empty_result(context: Context, expected: str) -> None:
    assert context.chained_empty_result == expected


@given("a reactive app with a graph route lacking edges")
def step_looping_graph_route(context: Context) -> None:
    context.app = ReactiveCleverAgentsApp()
    context.app.stream_router.register_agent("loop_actor", lambda _msg: "LOOP")
    route = RouteConfig(
        name="loop_graph",
        type=RouteType.GRAPH,
        nodes={
            "router": {
                "type": "message_router",
                "rules": [{"match_type": "prefix", "pattern": "", "target": "loop"}],
            },
            "loop": {"type": "actor", "actor": "loop_actor"},
        },
        edges=[],
    )
    context.app.config = ReactiveConfig(routes={"loop_graph": route})
    context.route = route


@when('I execute the looping graph route with prompt "{prompt}"')
def step_execute_looping_graph(context: Context, prompt: str) -> None:
    context.looping_result = context.app._execute_graph_route(  # pylint: disable=protected-access
        prompt, context.route
    )


@then('the looping graph route should return "{expected}"')
def step_looping_graph_result(context: Context, expected: str) -> None:
    assert context.looping_result == expected


@given("a reactive app with a graph route using an end-like sentinel target")
def step_sentinel_graph_route(context: Context) -> None:
    context.app = ReactiveCleverAgentsApp()
    context.app.stream_router.register_agent("value_actor", lambda _msg: "value")
    route = RouteConfig(
        name="sentinel_graph",
        type=RouteType.GRAPH,
        nodes={
            "router": {
                "type": "message_router",
                "rules": [
                    {
                        "match_type": "prefix",
                        "pattern": "SENTINEL",
                        "target": "value_node",
                    }
                ],
            },
            "value_node": {"type": "actor", "actor": "value_actor"},
        },
        edges=[{"source": "value_node", "target": EndSentinel()}],
    )
    context.app.config = ReactiveConfig(routes={"sentinel_graph": route})
    context.route = route


@when('I execute the sentinel graph route with prompt "{prompt}"')
def step_execute_sentinel_graph(context: Context, prompt: str) -> None:
    context.sentinel_result = context.app._execute_graph_route(  # pylint: disable=protected-access
        prompt, context.route
    )


@then('the sentinel graph route should return "{expected}"')
def step_sentinel_graph_result(context: Context, expected: str) -> None:
    assert context.sentinel_result == expected


@given("a reactive app with a graph route that emits a discovery prefix")
def step_graph_route_with_prefix(context: Context) -> None:
    context.app = ReactiveCleverAgentsApp()
    context.app.stream_router.register_agent(
        "prefix_actor", lambda _msg: "DISCOVERY_RESPONSE:Hello"
    )
    route = RouteConfig(
        name="prefix_graph",
        type=RouteType.GRAPH,
        nodes={
            "router": {
                "type": "message_router",
                "rules": [
                    {
                        "match_type": "prefix",
                        "pattern": "CMD:",
                        "target": "prefix_node",
                        "extract_message": True,
                        "separator": ":",
                    }
                ],
            },
            "prefix_node": {"type": "actor", "actor": "prefix_actor"},
        },
        edges=[{"source": "prefix_node", "target": "end"}],
    )
    context.app.config = ReactiveConfig(routes={"prefix_graph": route})


@given("a context manager with global context")
def step_context_manager_with_global(context: Context) -> None:
    temp_dir = Path(tempfile.mkdtemp())
    context.context_manager = ContextManager("reactive_global", context_dir=temp_dir)
    context.context_manager.global_context = {"session": "demo"}


@when('I run a graph single shot with context and prompt "{prompt}"')
def step_run_graph_single_shot_context(context: Context, prompt: str) -> None:
    context.graph_context_result = _run_async(
        context.app.run_single_shot(prompt, context_manager=context.context_manager)
    )


@then('the graph output should be "{expected}"')
def step_graph_output_expected(context: Context, expected: str) -> None:
    assert context.graph_context_result == expected


@then("the reactive config should include the context global values")
def step_config_includes_globals(context: Context) -> None:
    assert context.app.config is not None
    for key, value in context.context_manager.global_context.items():
        assert context.app.config.global_context.get(key) == value


@given("a reactive app with a stream route that emits an error")
def step_stream_route_emits_error(context: Context) -> None:
    context.app = ReactiveCleverAgentsApp()
    context.app.stream_router.register_agent(
        "error_actor", ErrorEmittingAgent(context.app.stream_router)
    )
    route = RouteConfig(
        name="error_stream",
        type=RouteType.STREAM,
        agents=["error_actor"],
        operators=[{"type": "map", "params": {"agent": "error_actor"}}],
    )
    context.app.config = ReactiveConfig(routes={"error_stream": route})
    context.app._build_routes()  # pylint: disable=protected-access


@when("I run a single shot that triggers a stream error")
def step_run_single_shot_error(context: Context) -> None:
    context.single_shot_error = None
    try:
        _run_async(context.app.run_single_shot("boom", allow_rxpy_in_run_mode=True))
    except Exception as exc:  # pylint: disable=broad-except
        context.single_shot_error = exc


@then("the single shot should raise a reactive stream error")
def step_single_shot_stream_error(context: Context) -> None:
    assert isinstance(context.single_shot_error, CleverAgentsException)


@when("I run a single shot with no routes")
def step_run_single_shot_no_routes(context: Context) -> None:
    context.single_shot_result = _run_async(context.app.run_single_shot("ping"))


@then("the single shot should return an empty string")
def step_single_shot_empty_result(context: Context) -> None:
    assert context.single_shot_result == ""
