import asyncio
from copy import deepcopy

from behave import given, then, when

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


class FailingAgent(Agent):
    def __init__(self, name: str, exc: Exception):
        super().__init__(name)
        self.exc = exc
        self.received_inputs = []

    async def process_message(self, message, context=None):
        self.received_inputs.append(message)
        raise self.exc

    def get_capabilities(self):
        return []


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

    async def process_message(self, message, context=None):
        self.received_inputs.append(message)
        self.received_contexts.append(deepcopy(context))
        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 default history limits")
def step_default_history(context):
    config = NodeConfig(name="history_default", type=NodeType.AGENT)
    context.node = Node(config)


@when("I prepare the conversation history for empty input")
def step_prepare_empty_history(context):
    trimmed, truncated = context.node._prepare_conversation_history([])
    context.trimmed_history = trimmed
    context.truncated = truncated


@then("it should return empty history without truncation")
def step_assert_empty_history(context):
    assert context.trimmed_history == []
    assert context.truncated is False


@given("a node with invalid history metadata and two messages")
def step_invalid_history_metadata(context):
    config = NodeConfig(
        name="history_invalid",
        type=NodeType.AGENT,
        metadata={"max_history_messages": "oops", "max_history_chars": "nope"},
    )
    context.node = Node(config)
    context.messages = [
        {"role": "user", "content": "first"},
        {"role": "assistant", "content": "second"},
    ]


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


@then("it should return full history without truncation")
def step_assert_invalid_history(context):
    assert context.trimmed_history == context.messages
    assert context.truncated is False


@given("an agent node without an agent configured")
def step_agent_missing_config(context):
    config = NodeConfig(name="agent_missing", type=NodeType.AGENT)
    context.node = Node(config)
    context.state = _make_state(messages=[{"role": "user", "content": "hi"}])


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


@then("the result should include failed_node and error text")
def step_assert_missing_agent_error(context):
    assert context.result.get("failed_node") == "agent_missing"
    assert "no agent specified" in context.result.get("error", "")


@given("an agent node with no prior messages")
def step_agent_no_messages(context):
    agent = RecordingAgent("rec", response="ok")
    config = NodeConfig(name="agent_empty", type=NodeType.AGENT, agent="rec")
    context.node = Node(config, agents={"rec": agent})
    context.state = _make_state(messages=[])


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


@then("the agent should receive empty input and track node metadata")
def step_assert_empty_agent_input(context):
    agent = context.node.agents["rec"]
    assert agent.received_inputs[-1] == ""
    metadata = context.result.get("metadata", {})
    assert metadata.get("last_agent_node") == "agent_empty"


@given("an agent node with nested metadata context and long history")
def step_agent_nested_context(context):
    agent = RecordingAgent("rec", response="nested-ok")
    config = NodeConfig(
        name="agent_nested",
        type=NodeType.AGENT,
        agent="rec",
        metadata={"max_history_messages": 1},
    )
    context.node = Node(config, agents={"rec": agent})
    history = [
        {"role": "user", "content": "short"},
        {"role": "assistant", "content": "long message that exceeds limit"},
    ]
    nested_metadata = {"current_message": "now", "context": {"foo": "bar"}}
    context.state = _make_state(messages=history, metadata=nested_metadata)


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


@then("the agent context should merge nested values and mark truncation")
def step_assert_agent_context_merge(context):
    agent = context.node.agents["rec"]
    received_context = agent.received_contexts[-1]
    assert received_context["foo"] == "bar"
    assert received_context["_history_truncated"] is True
    assert received_context["_history_original_length"] == 2


@given("an agent node whose handler raises an exception")
def step_agent_raises(context):
    failing_agent = FailingAgent("fail", RuntimeError("boom"))
    config = NodeConfig(name="agent_fail", type=NodeType.AGENT, agent="fail")
    context.node = Node(config, agents={"fail": failing_agent})
    context.state = _make_state(messages=[{"role": "user", "content": "hello"}])


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


@then("the agent response should contain the error string")
def step_assert_agent_error_path(context):
    messages = context.result.get("messages", [])
    assert messages
    assert "Error processing message: boom" in messages[0]["content"]


@given("a function node missing a target with retry policy")
def step_function_missing_with_retry(context):
    config = NodeConfig(
        name="fn_missing",
        type=NodeType.FUNCTION,
        function="missing_fn",
        retry_policy={"max_retries": 1, "delay": 0},
    )
    context.node = Node(config, agents={})
    context.state = _make_state()


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


@then("it should report failure after retries")
def step_assert_function_missing(context):
    assert context.result.get("failed_node") == "fn_missing"
    assert "not found" in context.result.get("error", "")
