"""Step definitions targeting uncovered lines in cleveragents.langgraph.nodes."""

import asyncio

from behave import given, then, when

from cleveragents.langgraph.nodes import Edge, Node, NodeConfig, NodeType, ToolAgent
from cleveragents.langgraph.state import GraphState

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_state(messages=None, metadata=None):
    return GraphState(messages=messages or [], metadata=metadata or {})


def _run(coro):
    """Run a coroutine in a fresh event loop."""
    loop = asyncio.new_event_loop()
    try:
        return loop.run_until_complete(coro)
    finally:
        loop.close()


class _RecordingToolAgent(ToolAgent):
    """ToolAgent subclass that records inputs."""

    def __init__(self, name: str, response: str):
        super().__init__(name)
        self.response = response
        self.received_inputs: list = []

    async def process_message(self, message, context=None):
        self.received_inputs.append(message)
        return self.response

    def get_capabilities(self):
        return []


# ---------------------------------------------------------------------------
# Line 130 - Unknown node type -> result = {}
# ---------------------------------------------------------------------------


@given("a node whose type is patched to an unrecognised value")
def step_patch_node_type(context):
    config = NodeConfig(name="unknown", type=NodeType.END)
    node = Node(config)
    # Patch the type to something that won't match any branch
    node.type = "UNRECOGNISED"
    context.node = node
    context.state = _make_state()


@when("I execute the unknown-type node")
def step_execute_unknown_type(context):
    context.result = _run(context.node.execute(context.state))


@then("the result should contain current_node and nothing else")
def step_assert_unknown_type_result(context):
    assert context.result == {"current_node": "unknown"}


# ---------------------------------------------------------------------------
# Line 176 - ToolAgent with empty current_message -> fallback to last message
# ---------------------------------------------------------------------------


@given("a ToolAgent node with an empty current_message and messages in state")
def step_toolagent_empty_current_msg(context):
    tool_agent = _RecordingToolAgent("tool_fb", response="tooled")
    config = NodeConfig(name="tool_fb_node", type=NodeType.AGENT, agent="tool_fb")
    context.node = Node(config, agents={"tool_fb": tool_agent})
    context.state = _make_state(
        messages=[
            {"role": "user", "content": "hello from user"},
            {"role": "assistant", "content": "last_content"},
        ],
        metadata={"current_message": ""},  # falsy string
    )


@when("I execute the ToolAgent fallback node")
def step_execute_toolagent_fallback(context):
    context.result = _run(context.node.execute(context.state))


@then("the ToolAgent should receive the last message content as input")
def step_assert_toolagent_fallback_input(context):
    agent = context.node.agents["tool_fb"]
    # current_message is "" (falsy), so it should fall back to last message content
    assert agent.received_inputs[-1] == "last_content"


# ---------------------------------------------------------------------------
# Line 270 - _execute_function with non-callable object
# ---------------------------------------------------------------------------


@given("a function node whose registered object is not callable")
def step_non_callable_function(context):
    config = NodeConfig(name="fn_nc", type=NodeType.FUNCTION, function="not_a_fn")
    # Register a non-callable object under the function name
    context.node = Node(config, agents={"not_a_fn": 42})
    context.state = _make_state()


@when("I execute the non-callable function node")
def step_execute_non_callable_fn(context):
    context.result = _run(context.node.execute(context.state))


@then("the result should report a not-callable error")
def step_assert_non_callable_error(context):
    assert context.result.get("failed_node") == "fn_nc"
    assert "not callable" in context.result.get("error", "")


# ---------------------------------------------------------------------------
# Line 319 - message router rule with target=None -> continue
# ---------------------------------------------------------------------------


@given("a message router with a rule that has no target")
def step_router_null_target(context):
    rules = [
        {"condition": {"equals": "anything"}, "target": None},
    ]
    config = NodeConfig(
        name="router_null", type=NodeType.MESSAGE_ROUTER, metadata={"rules": rules}
    )
    context.node = Node(config)
    context.state = _make_state(metadata={"current_message": "anything"})


@when("I execute the null-target router")
def step_execute_null_target_router(context):
    context.result = _run(context.node.execute(context.state))


@then("the router should return routed_to as None")
def step_assert_null_target_router(context):
    metadata = context.result.get("metadata", {})
    assert metadata.get("routed_to") is None


# ---------------------------------------------------------------------------
# Lines 321-322 - message router rule with condition=None -> unconditional match
# ---------------------------------------------------------------------------


@given('a message router with an unconditional rule targeting "{target}"')
def step_router_unconditional(context, target):
    rules = [
        {"condition": None, "target": target},
    ]
    config = NodeConfig(
        name="router_uncond", type=NodeType.MESSAGE_ROUTER, metadata={"rules": rules}
    )
    context.node = Node(config)
    context.state = _make_state(metadata={"current_message": "irrelevant"})


@when("I execute the unconditional router")
def step_execute_unconditional_router(context):
    context.result = _run(context.node.execute(context.state))


@then('the router should route to "{target}"')
def step_assert_router_target(context, target):
    metadata = context.result.get("metadata", {})
    assert metadata.get("routed_to") == target


# ---------------------------------------------------------------------------
# Lines 319 + 321-322 combined - null-target skipped, then unconditional hit
# ---------------------------------------------------------------------------


@given(
    'a message router with a null-target rule followed by an unconditional rule to "{target}"'
)
def step_router_mixed_rules(context, target):
    rules = [
        {"condition": {"equals": "x"}, "target": None},  # line 319: continue
        {"condition": None, "target": target},  # lines 321-322: match
    ]
    config = NodeConfig(
        name="router_mixed", type=NodeType.MESSAGE_ROUTER, metadata={"rules": rules}
    )
    context.node = Node(config)
    context.state = _make_state(metadata={"current_message": "x"})


@when("I execute the mixed-rules router")
def step_execute_mixed_router(context):
    context.result = _run(context.node.execute(context.state))


# ---------------------------------------------------------------------------
# Line 337 - evaluate_edge_condition returns True when no condition
# ---------------------------------------------------------------------------


@given("an edge with no condition")
def step_edge_no_condition(context):
    context.edge = Edge(source="a", target="b", condition=None)
    context.node = Node(NodeConfig(name="edge_node", type=NodeType.END))
    context.state = _make_state(messages=[{"role": "user", "content": "test"}])


@when("I evaluate the unconditioned edge")
def step_evaluate_unconditioned_edge(context):
    context.edge_result = context.node.evaluate_edge_condition(
        context.edge, context.state
    )


@then("the edge evaluation should return true")
def step_assert_unconditioned_edge(context):
    assert context.edge_result is True
