from collections import deque
from typing import Any

from behave import given, then, when

from cleveragents.langgraph.nodes import Edge, NodeConfig, NodeType
from cleveragents.langgraph.pure_graph import PureGraph


def _build_graph(name: str, node_defs: list[tuple[str, str | None]]):
    nodes: dict[str, NodeConfig] = {
        "start": NodeConfig(name="start", type=NodeType.START),
        "end": NodeConfig(name="end", type=NodeType.END),
    }
    edges: list[Edge] = []
    previous = "start"
    for node_name, function_name in node_defs:
        nodes[node_name] = NodeConfig(
            name=node_name, type=NodeType.FUNCTION, function=function_name
        )
        edges.append(Edge(source=previous, target=node_name))
        previous = node_name
    edges.append(Edge(source=previous, target="end"))
    return PureGraph(name=name, nodes=nodes, edges=edges)


@given('a pure graph with nodes "{first}" and "{second}"')
def step_graph_with_two_nodes(context, first: str, second: str):
    context.graph = _build_graph("pure-two", [(first, None), (second, None)])


@when("I list its topological order")
def step_list_topological_order(context):
    context.topo_order = context.graph.topological_order()


@then("the order should be start {first} {second} end")
def step_assert_topological_order(context, first: str, second: str):
    assert context.topo_order == ["start", first, second, "end"]


@given('a pure graph with function nodes "double" and "increment"')
def step_graph_with_functions(context):
    context.call_sequence = deque()

    def double(x: Any) -> Any:
        context.call_sequence.append("double")
        return x * 2

    def increment(x: Any) -> Any:
        context.call_sequence.append("increment")
        return x + 1

    context.fn_registry = {
        "double": double,
        "increment": increment,
    }
    context.graph = _build_graph(
        "pure-fns", [("double", "double"), ("increment", "increment")]
    )


@given("registered functions that double then increment")
def step_registered_functions(_context):
    # Functions already registered in previous step
    return


@when("I execute the pure graph starting with {initial:d}")
def step_execute_graph(context, initial: int):
    context.exec_result = context.graph.execute(context.fn_registry, initial=initial)


@then("the final result should be {expected:d}")
def step_assert_final_result(context, expected: int):
    assert context.exec_result == expected


@then("the functions should run in declaration order")
def step_assert_call_order(context):
    assert list(context.call_sequence) == ["double", "increment"]


@given("a pure graph with a missing function node")
def step_graph_with_missing_function(context):
    context.graph = _build_graph("pure-missing", [("skipper", "missing_fn")])
    context.fn_registry = {}


@then("the result should remain {expected:d}")
def step_assert_result_unchanged(context, expected: int):
    assert context.exec_result == expected


@given('a pure graph with non-functional nodes "{first}" and "{second}"')
def step_graph_with_non_functional_nodes(context, first: str, second: str):
    # Build graph without assigning functions and keep registry empty
    context.graph = _build_graph("pure-inert", [(first, None), (second, None)])
    context.fn_registry = {}
