import asyncio

from behave import given, then, when

from cleveragents.agents.base import Agent
from cleveragents.langgraph.nodes import Edge, Node, NodeConfig, NodeType
from cleveragents.langgraph.state import GraphState


class DummyAgent(Agent):
    def __init__(self, name: str, response: str):
        super().__init__(name)
        self._response = response
        self.received_inputs = []

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

    def get_capabilities(self):
        return []


class DummyToolAgent(Agent):
    def __init__(self, name: str, response: str):
        super().__init__(name)
        self._response = response
        self.received_inputs = []

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

    def get_capabilities(self):
        return []


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


@given("a node with history limit 2 messages")
def step_node_with_history_limit(context):
    config = NodeConfig(
        name="history", type=NodeType.AGENT, metadata={"max_history_messages": 2}
    )
    context.node = Node(config)


@given("a conversation history of 3 short messages")
def step_history_three_messages(context):
    context.messages = [
        {"role": "user", "content": "hi"},
        {"role": "assistant", "content": "there"},
        {"role": "user", "content": "friend"},
    ]


@when("I prepare the conversation history")
def step_prepare_history(context):
    trimmed, truncated = context.node._prepare_conversation_history(context.messages)
    context.trimmed_history = trimmed
    context.truncated = truncated


@then("it should return 2 messages and indicate truncation")
def step_assert_history(context):
    assert len(context.trimmed_history) == 2
    assert context.truncated is True
    assert context.trimmed_history[0]["content"] == "there"
    assert context.trimmed_history[1]["content"] == "friend"


@given("an agent node referencing a missing agent")
def step_agent_missing(context):
    config = NodeConfig(name="agent", type=NodeType.AGENT, agent="missing")
    context.node = Node(config, agents={})
    context.state = _make_state(messages=[{"role": "user", "content": "hi"}])


@when("I execute the agent node")
def step_execute_agent(context):
    loop = asyncio.new_event_loop()
    try:
        context.result = loop.run_until_complete(context.node.execute(context.state))
    finally:
        loop.close()


@then("it should return a not found assistant message")
def step_assert_missing_agent(context):
    messages = context.result.get("messages", [])
    assert messages
    assert messages[0]["content"] == "Agent missing not found"
    assert messages[0]["agent"] == "missing"


@given('a tool agent node with a current_message "{text}" and long history')
def step_tool_agent_with_current(context, text):
    tool_agent = DummyToolAgent("tool", response="ok")
    config = NodeConfig(
        name="tool_node",
        type=NodeType.AGENT,
        agent="tool",
        metadata={"max_history_messages": 1},
    )
    context.node = Node(config, agents={"tool": tool_agent})
    long_history = [
        {"role": "user", "content": "first"},
        {"role": "assistant", "content": "second"},
    ]
    context.state = _make_state(
        messages=long_history, metadata={"current_message": text}
    )


@when("I execute the tool agent node")
def step_execute_tool_agent(context):
    loop = asyncio.new_event_loop()
    try:
        context.result = loop.run_until_complete(context.node.execute(context.state))
    finally:
        loop.close()


@then('the tool agent should receive input "{text}"')
def step_assert_tool_input(context, text):
    agent = context.node.agents["tool"]
    assert agent.received_inputs[-1] == text


@then("the response metadata should include last_agent_node")
def step_assert_last_agent_metadata(context):
    metadata = context.result.get("metadata", {})
    assert metadata.get("last_agent_node") == "tool_node"


@given("a function node with an async function")
def step_async_function_node(context):
    async def async_fn(state):
        return "async-result"

    config = NodeConfig(name="fn", type=NodeType.FUNCTION, function="async_fn")
    context.node = Node(config, agents={"async_fn": async_fn})
    context.state = _make_state()


@given("a function node with a sync function that returns a coroutine")
def step_sync_function_coroutine(context):
    async def inner():
        return "sync-inner"

    def sync_fn(state):
        return inner()

    config = NodeConfig(name="fn_sync", type=NodeType.FUNCTION, function="sync_fn")
    context.node = Node(config, agents={"sync_fn": sync_fn})
    context.state = _make_state()


@when("I execute the function node")
def step_execute_function_node(context):
    loop = asyncio.new_event_loop()
    try:
        context.result = loop.run_until_complete(context.node.execute(context.state))
    finally:
        loop.close()


@then('the function result message should be "{expected}"')
def step_assert_function_result(context, expected):
    messages = context.result.get("messages", [])
    assert messages
    assert messages[0]["content"] == expected


@given('a conditional node looking for field "{field}" equals "{value}"')
def step_conditional_node(context, field, value):
    config = NodeConfig(
        name="cond",
        type=NodeType.CONDITIONAL,
        condition={"field": field, "equals": value},
    )
    context.node = Node(config)
    context.state = _make_state(
        messages=[{"role": "assistant", field: value, "content": "reply"}]
    )


@when("I execute the conditional node with matching assistant message")
def step_execute_conditional(context):
    loop = asyncio.new_event_loop()
    try:
        context.result = loop.run_until_complete(context.node.execute(context.state))
    finally:
        loop.close()


@then("the condition_result should be true and metadata flagged")
def step_assert_conditional(context):
    assert context.result.get("condition_result") is True
    metadata = context.result.get("metadata", {})
    assert metadata.get("conditional_edges_used") is True


@given('a message router node with a rule matching field "{field}" value "{value}"')
def step_message_router_node(context, field, value):
    rules = [{"condition": {"field": field, "value": value}, "target": "urgent_node"}]
    config = NodeConfig(
        name="router", type=NodeType.MESSAGE_ROUTER, metadata={"rules": rules}
    )
    context.node = Node(config)
    context.state = _make_state(metadata={"current_message": {field: value}})


@when('I execute the message router with current_message containing topic "urgent"')
def step_execute_message_router(context):
    loop = asyncio.new_event_loop()
    try:
        context.result = loop.run_until_complete(context.node.execute(context.state))
    finally:
        loop.close()


@then('it should route to "urgent_node"')
def step_assert_router(context):
    metadata = context.result.get("metadata", {})
    assert metadata.get("routed_to") == "urgent_node"


@given('an edge with equals condition on field "{field}" value "{value}"')
def step_edge_condition(context, field, value):
    context.edge = Edge(
        source="a", target="b", condition={"field": field, "equals": value}
    )
    context.node = Node(NodeConfig(name="edge", type=NodeType.END))


@when("I evaluate the edge condition with matching last message")
def step_eval_edge(context):
    state = _make_state(messages=[{"role": "assistant", "content": "done"}])
    context.edge_result = context.node.evaluate_edge_condition(context.edge, state)


@then("the edge condition should be true")
def step_assert_edge_true(context):
    assert context.edge_result is True


@then("evaluating with no messages should also default to true")
def step_assert_edge_default_true(context):
    empty_state = _make_state()
    assert context.node.evaluate_edge_condition(context.edge, empty_state) is True


@given('I evaluate equals condition on message "{text}"')
def step_eval_equals(context, text):
    context.node = Node(NodeConfig(name="eval", type=NodeType.END))
    context.eval_equals = context.node._eval_condition(text, {"equals": text})


@then("the equals evaluation should return true")
def step_assert_eval_true(context):
    assert context.eval_equals is True


@when('I evaluate field condition on message dict with foo value "bar"')
def step_eval_field_true(context):
    context.eval_field_true = context.node._eval_condition(
        {"foo": "bar"}, {"field": "foo", "value": "bar"}
    )


@then("the field evaluation should return true")
def step_assert_field_true(context):
    assert context.eval_field_true is True


@when("I evaluate field condition on non-dict message")
def step_eval_field_false(context):
    context.eval_field_false = context.node._eval_condition(
        "not a dict", {"field": "foo", "value": "bar"}
    )


@then("it should return false")
def step_assert_field_false(context):
    assert context.eval_field_false is False


@given("a node configured for parallel execution")
def step_node_parallel(context):
    config = NodeConfig(name="parallel", type=NodeType.END, parallel=True)
    context.node = Node(config)


@then("can_execute_parallel should return true")
def step_assert_parallel(context):
    assert context.node.can_execute_parallel() is True
