"""Route bridge coverage steps (ported and adapted)."""

from __future__ import annotations

import asyncio
from pathlib import Path

from behave import given, then, when
from rx.scheduler.eventloop import AsyncIOScheduler  # type: ignore[attr-defined]

from cleveragents.core.exceptions import ConfigurationError
from cleveragents.langgraph.graph import GraphConfig
from cleveragents.langgraph.nodes import Edge, NodeConfig, NodeType
from cleveragents.langgraph.state import GraphState
from cleveragents.reactive.route import BridgeConfig, RouteConfig, RouteType
from cleveragents.reactive.route_bridge import RouteBridge
from cleveragents.reactive.stream_router import (
    ReactiveStreamRouter,
    StreamMessage,
)

_DEF_CHECKPOINT_DIR = Path("/tmp/langgraph_checkpoints")

# Reuse a single event loop for async helpers in tests
_TEST_LOOP = asyncio.new_event_loop()
asyncio.set_event_loop(_TEST_LOOP)


def _standard_topological_levels(nodes: dict[str, object]) -> dict[int, set[str]]:
    middle_nodes = {name for name in nodes if name not in {"start", "end"}}
    return {0: {"start"}, 1: middle_nodes, 2: {"end"}}


def _build_standard_langgraph_stub(corrupt_state_manager: bool = False):
    start_cfg = NodeConfig(name="start", type=NodeType.START)
    agent_node_cfg = NodeConfig(
        name="agent_node", type=NodeType.AGENT, agent="dummy", parallel=False
    )
    function_node_cfg = NodeConfig(
        name="function_node",
        type=NodeType.FUNCTION,
        function="stub_function",
        parallel=False,
    )
    end_cfg = NodeConfig(name="end", type=NodeType.END)

    def _stub_node(cfg):
        return type("StubNode", (), {"config": cfg})()

    nodes = {
        "start": _stub_node(start_cfg),
        "agent_node": _stub_node(agent_node_cfg),
        "function_node": _stub_node(function_node_cfg),
        "end": _stub_node(end_cfg),
    }

    edges = [
        Edge(source="start", target="agent_node"),
        Edge(source="agent_node", target="function_node"),
        Edge(source="function_node", target="end"),
    ]

    graph_config = GraphConfig(
        name="stub_graph",
        nodes={name: node.config for name, node in nodes.items()},
        edges=edges,
        entry_point="start",
        checkpointing=True,
        checkpoint_dir=_DEF_CHECKPOINT_DIR,
        parallel_execution=False,
    )

    class StubStateManager:
        def __init__(self, checkpoint_dir: Path):
            self.checkpoint_dir = checkpoint_dir
            self.update_count = 0
            self.updated_state = None
            self.update_state_called = False
            self.checkpoint_saved = False

        def _save_checkpoint(self):  # pylint: disable=protected-access
            self.checkpoint_saved = True
            return None

        def update_state(self, state=None):
            self.update_state_called = True
            self.updated_state = state or {}
            self.update_count += 1

        def get_state(self):
            return self.updated_state or {}

    langgraph = type("StubLangGraph", (), {})()
    langgraph.nodes = nodes
    langgraph.config = graph_config
    langgraph.state_manager = (
        None if corrupt_state_manager else StubStateManager(_DEF_CHECKPOINT_DIR)
    )
    langgraph._topological_levels = lambda: _standard_topological_levels(nodes)  # type: ignore[attr-defined]
    return langgraph


def _build_stub_langgraph_from_config(
    graph_config: GraphConfig, checkpoint_dir: Path | None = None
):
    class StubStateManager:
        def __init__(self, ckp: Path | None):
            self.checkpoint_dir = ckp
            self.update_count = 0
            self.update_state_called = False
            self.updated_state = None

        def update_state(self, state=None):
            self.update_state_called = True
            self.updated_state = state or {}
            self.update_count += 1

        def get_state(self):
            return self.updated_state or {}

        def _save_checkpoint(self):  # pylint: disable=protected-access
            return None

    nodes = {
        name: type("StubNode", (), {"config": cfg})()
        for name, cfg in graph_config.nodes.items()
    }
    if "start" not in nodes:
        nodes["start"] = type(
            "StubNode", (), {"config": NodeConfig(name="start", type=NodeType.START)}
        )()
    if "end" not in nodes:
        nodes["end"] = type(
            "StubNode", (), {"config": NodeConfig(name="end", type=NodeType.END)}
        )()

    langgraph = type("StubLangGraph", (), {})()
    langgraph.config = graph_config
    langgraph.nodes = nodes
    langgraph.state_manager = StubStateManager(
        checkpoint_dir or graph_config.checkpoint_dir or _DEF_CHECKPOINT_DIR
    )
    langgraph._topological_levels = lambda: _standard_topological_levels(nodes)  # type: ignore[attr-defined]
    return langgraph


def _ensure_topological_levels(graph):
    if not hasattr(graph, "_topological_levels"):
        graph._topological_levels = lambda: _standard_topological_levels(  # type: ignore[attr-defined]
            getattr(graph, "nodes", {})
        )


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)


@given("I have a clean test environment for route bridge")
def step_clean_env(context):
    context.stream_router = ReactiveStreamRouter()
    context.agents = {"agentA": DummyAgent("agentA")}
    context.route_bridge = None
    context.stream_message = None
    context.langgraph = None
    context.graph_result = None
    context.stream_config_result = None
    context.graph_config_result = None
    context.route_bridge_error = None
    context.state_flattener_called = False


class DummyAgent:
    def __init__(self, name: str):
        self.name = name

    def process_message_sync(self, message, metadata=None):
        return f"processed:{message}"

    def get_capabilities(self):
        return []


@given("I have a route bridge stream router and agents")
def step_stream_router_agents(context):
    context.stream_router = ReactiveStreamRouter()
    context.agents = {"agentA": DummyAgent("agentA"), "dummy": DummyAgent("dummy")}
    for name, agent in context.agents.items():
        context.stream_router.register_agent(name, agent)


@given("I have an AsyncIO scheduler")
def step_asyncio_scheduler(context):
    try:
        loop = asyncio.get_running_loop()
    except RuntimeError:
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
    context.scheduler = AsyncIOScheduler(loop)


@when("I create the route bridge instance")
def step_create_route_bridge(context):
    context.route_bridge = RouteBridge(
        context.stream_router,
        context.agents,
        scheduler=getattr(context, "scheduler", None),
    )


@when("I create a RouteBridge instance with scheduler")
def step_create_route_bridge_scheduler(context):
    step_create_route_bridge(context)


@then("the route bridge should be initialized correctly")
def step_rb_initialized(context):
    assert context.route_bridge is not None
    assert hasattr(context.route_bridge, "stream_router")


@then("the route bridge logger should be set up")
def step_logger_set(context):
    assert context.route_bridge.logger is not None


@then("route bridge active conversions should be empty")
def step_active_conversions_empty(context):
    assert context.route_bridge._active_conversions == {}


@then("the route bridge should be initialized with scheduler")
def step_scheduler_stored(context):
    assert context.route_bridge.scheduler is not None


@then("the route bridge scheduler should be stored correctly")
def step_scheduler_correct(context):
    assert context.route_bridge.scheduler == getattr(context, "scheduler", None)


@given("I have a RouteBridge instance")
def step_have_route_bridge(context):
    if not getattr(context, "route_bridge", None):
        step_create_route_bridge(context)
    # Reset results for clarity
    context.upgrade_result = False
    context.downgrade_result = False
    context.upgrade_error = None
    context.downgrade_error = None
    context.route_bridge._active_conversions.clear()


@given("I have a route config without bridge configuration")
def step_route_no_bridge(context):
    context.route_config = RouteConfig(
        name="r1",
        type=RouteType.GRAPH,
        nodes={
            "agent_node": {"type": "agent", "agent": "dummy", "parallel": False},
            "function_node": {
                "type": "function",
                "function": "stub_function",
                "parallel": False,
            },
        },
        edges=[
            {"source": "start", "target": "agent_node"},
            {"source": "agent_node", "target": "function_node"},
            {"source": "function_node", "target": "end"},
        ],
        entry_point="start",
        checkpointing=True,
        checkpoint_dir=str(_DEF_CHECKPOINT_DIR),
    )
    context.downgrade_result = False
    context.upgrade_result = False


@given("I have a graph route config with bridge configuration")
def step_graph_route_with_bridge(context):
    context.route_config = RouteConfig(
        name="g1",
        type=RouteType.GRAPH,
        nodes={
            "agent_node": {"type": "agent", "agent": "dummy", "parallel": False},
            "function_node": {
                "type": "function",
                "function": "stub_function",
                "parallel": False,
            },
        },
        edges=[
            {"source": "start", "target": "agent_node"},
            {"source": "agent_node", "target": "function_node"},
            {"source": "function_node", "target": "end"},
        ],
        entry_point="start",
        bridge=BridgeConfig(),
        checkpointing=True,
        checkpoint_dir=str(_DEF_CHECKPOINT_DIR),
        parallel_execution=False,
    )


@given("I have a stream route config with needs_state upgrade condition")
def step_stream_route_needs_state(context):
    context.route_config = RouteConfig(
        name="needs_state",
        type=RouteType.STREAM,
        operators=[{"type": "map", "params": {"agent": "dummy"}}],
        bridge=BridgeConfig(upgrade_conditions={"needs_state": True}),
    )


@given("I have a stream route config with message_count upgrade condition")
def step_stream_route_message_count(context):
    context.route_config = RouteConfig(
        name="message_count",
        type=RouteType.STREAM,
        operators=[{"type": "map", "params": {"agent": "dummy"}}],
        bridge=BridgeConfig(upgrade_conditions={"message_count": 5}),
    )


@given("I have a stream route config with complexity_threshold upgrade condition")
def step_stream_route_complexity_threshold(context):
    context.route_config = RouteConfig(
        name="complexity_threshold",
        type=RouteType.STREAM,
        operators=[
            {"type": "map", "params": {"agent": "dummy"}},
            {"type": "map", "params": {"function": "f"}},
        ],
        bridge=BridgeConfig(upgrade_conditions={"complexity_threshold": 6}),
    )


@given("I have a stream route config with custom_predicate upgrade condition")
def step_stream_route_custom_predicate(context):
    context.route_config = RouteConfig(
        name="custom_predicate",
        type=RouteType.STREAM,
        operators=[{"type": "map", "params": {"agent": "dummy"}}],
        bridge=BridgeConfig(upgrade_conditions={"custom_predicate": None}),
    )


@given("I have a stream route config with non-callable custom_predicate")
def step_stream_route_non_callable_predicate(context):
    context.route_config = RouteConfig(
        name="non_callable_predicate",
        type=RouteType.STREAM,
        operators=[{"type": "map", "params": {"agent": "dummy"}}],
        bridge=BridgeConfig(upgrade_conditions={"custom_predicate": "not_callable"}),
    )


@given("I have a stream message")
def step_stream_message(context):
    context.stream_message = StreamMessage(content="data", metadata={})


@given("I have a stream message with requires_state metadata")
def step_stream_message_requires_state(context):
    context.stream_message = StreamMessage(
        content="data", metadata={"requires_state": True}
    )


@given("I have a graph route config for downgrade")
def step_graph_route_for_downgrade(context):
    context.route_config = RouteConfig(
        name="graph_to_stream",
        type=RouteType.GRAPH,
        nodes={
            "agent_node": {"type": "agent", "agent": "dummy", "parallel": False},
            "function_node": {
                "type": "function",
                "function": "stub_function",
                "parallel": False,
            },
        },
        edges=[
            {"source": "start", "target": "agent_node"},
            {"source": "agent_node", "target": "function_node"},
            {"source": "function_node", "target": "end"},
        ],
        entry_point="start",
        checkpointing=True,
        checkpoint_dir=str(_DEF_CHECKPOINT_DIR),
        parallel_execution=False,
        bridge=BridgeConfig(),
    )


@given("I have a LangGraph instance")
def step_have_langgraph_instance(context):
    context.langgraph = _build_standard_langgraph_stub()
    _ensure_topological_levels(context.langgraph)
    context.route_bridge._active_conversions[context.route_config.name] = {
        "type": "graph",
        "instance": context.langgraph,
        "original_config": context.route_config,
    }


@given("I have a graph state")
def step_graph_state(context):
    context.graph_state = GraphState()
    context.graph_state.metadata = {}
    context.graph_state.messages = []


@given("I have a graph route config with idle_time downgrade condition")
def step_graph_route_idle_time(context):
    bridge_config = BridgeConfig(downgrade_conditions={"idle_time": 5.0})
    context.route_config = RouteConfig(
        name="graph_idle",
        type=RouteType.GRAPH,
        nodes={"n1": {"type": "agent", "agent": "dummy"}},
        edges=[{"source": "start", "target": "n1"}],
        bridge=bridge_config,
    )


@given("I have a graph state that has been idle too long")
def step_graph_state_idle(context):
    context.graph_state = GraphState()
    current_time = asyncio.get_event_loop().time()
    context.graph_state.metadata = {"last_updated": current_time - 10.0}
    context.graph_state.messages = []


@given("I have a graph route config with state_size downgrade condition")
def step_graph_route_state_size(context):
    bridge_config = BridgeConfig(downgrade_conditions={"state_size": 1})
    context.route_config = RouteConfig(
        name="graph_state_size",
        type=RouteType.GRAPH,
        nodes={"n1": {"type": "agent", "agent": "dummy"}},
        edges=[{"source": "start", "target": "n1"}],
        bridge=bridge_config,
    )


@given("I have a graph state with minimal messages")
def step_graph_state_minimal(context):
    context.graph_state = GraphState()
    context.graph_state.metadata = {}
    context.graph_state.messages = [{"content": "only_one"}]


@given("I have a graph route config with no_conditionals_used downgrade condition")
def step_graph_route_no_conditionals(context):
    bridge_config = BridgeConfig(downgrade_conditions={"no_conditionals_used": True})
    context.route_config = RouteConfig(
        name="graph_no_conditionals",
        type=RouteType.GRAPH,
        nodes={"n1": {"type": "agent", "agent": "dummy"}},
        edges=[{"source": "start", "target": "n1"}],
        bridge=bridge_config,
    )


@given("I have a graph state with no conditional edges used")
def step_graph_state_no_conditionals(context):
    context.graph_state = GraphState()
    context.graph_state.metadata = {"conditional_edges_used": False}
    context.graph_state.messages = []


@given("I have a stream route config with bridge configuration")
def step_stream_route_with_bridge_config(context):
    bridge_config = BridgeConfig(
        upgrade_conditions={"needs_state": True},
        downgrade_conditions={"idle_time": 30.0},
    )
    context.route_config = RouteConfig(
        name="stream_with_bridge",
        type=RouteType.STREAM,
        operators=[{"type": "map", "params": {"agent": "dummy"}}],
        bridge=bridge_config,
    )


@when("I check downgrade conditions")
def step_check_downgrade(context):
    if not getattr(context, "graph_state", None):
        step_graph_state(context)
    try:
        context.downgrade_result = _run_async(
            context.route_bridge.check_downgrade_conditions(
                context.route_config, context.graph_state
            )
        )
    except Exception as exc:  # pylint: disable=broad-except
        context.downgrade_result = False
        context.downgrade_error = exc


@then("the downgrade result should be True")
def step_downgrade_result_true(context):
    assert context.downgrade_result is True


@then("the downgrade result should be False")
def step_downgrade_result_false(context):
    assert context.downgrade_result is False


@given("I have processed enough messages to trigger upgrade")
def step_processed_enough_messages(context):
    dummy_instance = type("DummyConversion", (), {"update_count": 5})()
    context.route_bridge._active_conversions[context.route_config.name] = {
        "type": "stream",
        "instance": dummy_instance,
        "original_config": context.route_config,
    }


@given("I have a complex route configuration")
def step_complex_route_configuration(context):
    # Ensure the route has enough operators to exceed the threshold
    if (
        not getattr(context, "route_config", None)
        or context.route_config.type != RouteType.STREAM
    ):
        context.route_config = RouteConfig(
            name="complex_route",
            type=RouteType.STREAM,
            operators=[
                {"type": "map", "params": {"agent": "dummy"}},
                {"type": "map", "params": {"function": "f1"}},
                {"type": "map", "params": {"function": "f2"}},
                {"type": "map", "params": {"function": "f3"}},
                {"type": "map", "params": {"function": "f4"}},
            ],
            bridge=BridgeConfig(upgrade_conditions={"complexity_threshold": 6}),
        )
    else:
        # Enrich existing operators to make the route more complex
        context.route_config.operators.extend(
            [
                {"type": "map", "params": {"function": "extra_f1"}},
                {"type": "map", "params": {"function": "extra_f2"}},
                {"type": "map", "params": {"function": "extra_f3"}},
            ]
        )


@given("I seed a message count for the route")
def step_seed_message_count(context):
    if not getattr(context, "route_config", None):
        context.route_config = RouteConfig(
            name="message_count_route",
            type=RouteType.STREAM,
            operators=[{"type": "map", "params": {"agent": "dummy"}}],
            bridge=BridgeConfig(upgrade_conditions={"message_count": 5}),
        )
    state_manager = type("DummyStateManager", (), {"update_count": 7})()
    dummy_graph = type("DummyGraph", (), {"state_manager": state_manager})()
    context.route_bridge._active_conversions[context.route_config.name] = {
        "type": "graph",
        "instance": dummy_graph,
        "original_config": context.route_config,
    }


@when("I get message count for a route")
def step_get_message_count(context):
    if not getattr(context, "route_config", None):
        step_seed_message_count(context)
    context.message_count = context.route_bridge._get_message_count(
        context.route_config.name
    )


@when("I check upgrade conditions")
def step_check_upgrade(context):
    if not getattr(context, "stream_message", None):
        context.stream_message = StreamMessage(content="data", metadata={})
    try:
        context.upgrade_result = _run_async(
            context.route_bridge.check_upgrade_conditions(
                context.route_config, context.stream_message
            )
        )
    except Exception as exc:  # pylint: disable=broad-except
        context.upgrade_result = False
        context.upgrade_error = exc


@when("I check upgrade conditions synchronously")
def step_check_upgrade_sync(context):
    if not getattr(context, "stream_message", None):
        context.stream_message = StreamMessage(content="data", metadata={})
    coro = context.route_bridge.check_upgrade_conditions(
        context.route_config, context.stream_message
    )
    context.upgrade_result = _run_async(coro)


@then("the route bridge upgrade result should be True")
def step_upgrade_result_true(context):
    assert context.upgrade_result is True


@then("the route bridge upgrade result should be False")
def step_upgrade_result_false(context):
    assert context.upgrade_result is False


@then("the route bridge message count should be at least 5")
def step_message_count_at_least(context):
    assert context.route_bridge._get_message_count(context.route_config.name) >= 5


@then("the route bridge message count should be returned")
def step_message_count_returned(context):
    assert context.message_count is not None


@then("the route bridge message count result should be a valid integer")
def step_message_count_valid_integer(context):
    assert isinstance(context.message_count, int)


@then("route bridge error handling should occur")
def step_error_handling(context):
    assert context.upgrade_error is None or isinstance(context.upgrade_error, Exception)


@then("the route bridge system should remain stable")
def step_system_stable(context):
    assert context.route_bridge is not None


@given("I have an invalid graph route config")
def step_invalid_graph_route_config(context):
    # Build an invalid placeholder but catch validation so test can proceed
    try:
        context.route_config = RouteConfig(
            name="bad_graph",
            type=RouteType.GRAPH,
            nodes={},
            edges=[],
        )
    except ConfigurationError as exc:
        context.route_config = None
        context.downgrade_error = exc


@given("I have a corrupted LangGraph instance")
def step_corrupted_langgraph(context):
    context.langgraph = _build_standard_langgraph_stub(corrupt_state_manager=True)


@when("I attempt to downgrade graph to stream")
def step_attempt_downgrade(context):
    if context.route_config is None:
        context.downgrade_result = False
        context.downgrade_error = context.downgrade_error or ConfigurationError(
            "Invalid graph route"
        )
        return
    try:
        context.downgrade_result = asyncio.get_event_loop().run_until_complete(
            context.route_bridge.downgrade_graph_to_stream(
                context.route_config, context.langgraph
            )
        )
        context.downgrade_error = None
    except ConfigurationError as exc:
        context.downgrade_result = False
        context.downgrade_error = exc
    except Exception as exc:  # pylint: disable=broad-except
        context.downgrade_error = exc


@then("And the system should remain stable")
def step_system_stable_downgrade(context):
    assert context.route_bridge is not None


@given("I have a route config that supports both upgrade and downgrade")
def step_route_upgrade_downgrade(context):
    context.route_config = RouteConfig(
        name="cycle",
        type=RouteType.STREAM,
        operators=[{"type": "map", "params": {"agent": "dummy"}}],
        bridge=BridgeConfig(
            upgrade_conditions={"needs_state": True},
            downgrade_conditions={"state_size": 0},
        ),
    )
    context.stream_message = StreamMessage(
        content="x", metadata={"requires_state": True}
    )


@when("I perform an upgrade and then downgrade cycle")
def step_cycle_upgrade_downgrade(context):
    context.langgraph = asyncio.get_event_loop().run_until_complete(
        context.route_bridge.upgrade_stream_to_graph(
            context.route_config, context.stream_message
        )
    )
    context.route_bridge._active_conversions[context.route_config.name] = {
        "type": "graph",
        "instance": context.langgraph,
        "original_config": context.route_config,
    }
    _ensure_topological_levels(context.langgraph)
    # Convert Node objects to NodeConfig dict
    node_configs = {}
    for name, node in context.langgraph.nodes.items():
        if hasattr(node, "config") and node.config:
            node_configs[name] = node.config
        else:
            # Create a minimal NodeConfig for nodes without config
            node_configs[name] = NodeConfig(
                name=name, type=getattr(node, "type", NodeType.FUNCTION)
            )

    context.stream_config = asyncio.get_event_loop().run_until_complete(
        context.route_bridge.downgrade_graph_to_stream(
            RouteConfig.from_graph_config(
                GraphConfig(
                    name="cycle_g",
                    nodes=node_configs,
                    edges=context.langgraph.config.edges,
                    entry_point="start",
                )
            ),
            context.langgraph,
        )
    )


@then("the cycle should complete successfully")
def step_cycle_success(context):
    assert context.stream_config is not None


@then("the final configuration should be consistent")
def step_final_config_consistent(context):
    assert context.stream_config.name


@then("active conversions should be tracked correctly")
def step_active_conversions_tracked(context):
    assert context.route_bridge._active_conversions


# Additional coverage steps


class _FailingRegistry:
    def list_actors(self):
        raise RuntimeError("registry failure")


@given("I have a failing agent registry for route bridge")
def step_failing_registry(context):
    context.stream_router = ReactiveStreamRouter()
    context.agents = _FailingRegistry()


@then("the route bridge agents should be empty")
def step_agents_empty(context):
    assert context.route_bridge.agents == {}


@given("I have a pending message count below threshold")
def step_message_count_below_threshold(context):
    dummy_instance = type("DummyConversion", (), {"update_count": 2})()
    context.route_bridge._active_conversions[context.route_config.name] = {
        "type": "stream",
        "instance": dummy_instance,
        "original_config": context.route_config,
    }


@given("I have a simple stream route config with high complexity threshold")
def step_simple_route_high_complexity(context):
    context.route_config = RouteConfig(
        name="simple_high_threshold",
        type=RouteType.STREAM,
        operators=[{"type": "map", "params": {"agent": "dummy"}}],
        bridge=BridgeConfig(upgrade_conditions={"complexity_threshold": 999}),
    )


@given("I have a stream route config with callable custom_predicate")
def step_route_callable_predicate(context):
    def _predicate(message, _cfg):
        return True

    bridge_config = BridgeConfig(upgrade_conditions={"custom_predicate": None})
    object.__setattr__(
        bridge_config, "upgrade_conditions", {"custom_predicate": _predicate}
    )
    context.route_config = RouteConfig(
        name="callable_predicate",
        type=RouteType.STREAM,
        operators=[{"type": "map", "params": {"agent": "dummy"}}],
        bridge=bridge_config,
    )


@given("I have a stream route config with state extractor and subscriptions")
def step_route_with_state_extractor(context):
    def _extractor(_msg):
        return {"metadata": {"extracted": True}}

    bridge_config = BridgeConfig(preserve_subscriptions=True)
    object.__setattr__(bridge_config, "state_extractor", _extractor)

    context.route_config = RouteConfig(
        name="extractor_route",
        type=RouteType.STREAM,
        operators=[{"type": "map", "params": {"agent": "dummy"}}],
        subscriptions=["a", "b"],
        bridge=bridge_config,
    )


@when("I create a graph config from a stream without agents")
def step_create_graph_config_without_agents(context):
    context.route_config = RouteConfig(
        name="function_only_stream",
        type=RouteType.STREAM,
        operators=[{"type": "map", "params": {"function": "noop"}}],
    )
    context.graph_config_result = context.route_bridge._create_graph_from_stream(
        context.route_config
    )


@given("I seed a fallback update_count conversion")
def step_seed_fallback_update_count(context):
    context.route_config = RouteConfig(
        name="update_count_route",
        type=RouteType.STREAM,
        operators=[{"type": "map", "params": {"agent": "dummy"}}],
        bridge=BridgeConfig(upgrade_conditions={"message_count": 5}),
    )
    instance = type("DummyConversion", (), {"update_count": 3})()
    context.route_bridge._active_conversions[context.route_config.name] = {
        "type": "graph",
        "instance": instance,
        "original_config": context.route_config,
    }


@when("I upgrade the stream route to graph")
def step_upgrade_stream_with_extractor(context):
    if not getattr(context, "stream_message", None):
        context.stream_message = StreamMessage(content="data", metadata={})
    context.graph_result = _run_async(
        context.route_bridge.upgrade_stream_to_graph(
            context.route_config, context.stream_message
        )
    )


@given("I have a graph route config with flattener and checkpointing")
def step_graph_route_with_flattener(context):
    def _flattener(state):
        context.state_flattener_called = True
        return state

    bridge_config = BridgeConfig(preserve_checkpointing=True)
    object.__setattr__(bridge_config, "state_flattener", _flattener)

    context.route_config = RouteConfig(
        name="graph_flat",
        type=RouteType.GRAPH,
        nodes={"n1": {"type": "agent", "agent": "dummy"}},
        edges=[{"source": "start", "target": "n1"}],
        bridge=bridge_config,
        checkpointing=True,
        checkpoint_dir=str(_DEF_CHECKPOINT_DIR),
        parallel_execution=False,
    )


@given("I have a LangGraph instance with checkpointing")
def step_langgraph_with_checkpointing(context):
    context.langgraph = _build_standard_langgraph_stub()
    # ensure checkpoint_dir persists for test assertions
    if hasattr(context.langgraph, "state_manager"):
        context.langgraph.state_manager.checkpoint_dir = _DEF_CHECKPOINT_DIR


@when("I downgrade the graph with flattener")
def step_downgrade_with_flattener(context):
    context.stream_config_result = _run_async(
        context.route_bridge.downgrade_graph_to_stream(
            context.route_config, context.langgraph
        )
    )


@then("the state flattener should run")
def step_state_flattener_run(context):
    assert context.state_flattener_called is True


@then("the checkpoint should be saved")
def step_checkpoint_saved_flag(context):
    assert getattr(context.langgraph.state_manager, "checkpoint_saved", False) is True


@then("the graph initial state should include extracted data")
def step_graph_state_includes_extracted(context):
    state_mgr = getattr(context.graph_result, "state_manager", None)
    assert state_mgr is not None
    state = state_mgr.get_state()
    assert isinstance(state, GraphState)
    assert state.metadata.get("extracted") is True


@given("I have a graph state with multiple messages")
def step_graph_state_multiple_messages(context):
    context.graph_state = GraphState()
    context.graph_state.metadata = {}
    context.graph_state.messages = [
        {"content": "a"},
        {"content": "b"},
        {"content": "c"},
    ]


@given("I have a graph route config with idle_time not exceeded")
def step_graph_route_idle_time_not_exceeded(context):
    bridge_config = BridgeConfig(downgrade_conditions={"idle_time": 1000.0})
    context.route_config = RouteConfig(
        name="graph_idle_safe",
        type=RouteType.GRAPH,
        nodes={"n1": {"type": "agent", "agent": "dummy"}},
        edges=[{"source": "start", "target": "n1"}],
        bridge=bridge_config,
    )
    context.graph_state = GraphState()
    context.graph_state.metadata = {"last_updated": _TEST_LOOP.time()}
    context.graph_state.messages = []


@then("the route bridge fallback message count should be returned")
def step_fallback_message_count(context):
    assert context.route_bridge._get_message_count(context.route_config.name) == 3


@given("I have a graph route config with large state size")
def step_graph_route_large_state(context):
    bridge_config = BridgeConfig(downgrade_conditions={"state_size": 1})
    context.route_config = RouteConfig(
        name="graph_state_large",
        type=RouteType.GRAPH,
        nodes={"n1": {"type": "agent", "agent": "dummy"}},
        edges=[{"source": "start", "target": "n1"}],
        bridge=bridge_config,
    )
    context.graph_state = GraphState()
    context.graph_state.metadata = {}
    context.graph_state.messages = [
        {"content": "a"},
        {"content": "b"},
        {"content": "c"},
    ]


@then("the resulting graph should use function nodes")
def step_result_graph_function_nodes(context):
    node_cfg = context.graph_config_result.nodes.get("op_0_map")
    assert node_cfg is not None
    assert node_cfg.type == NodeType.FUNCTION
