"""Tests for the agent loop."""

from unittest.mock import AsyncMock, MagicMock

import pytest

from fliiq.runtime.agent.config import AgentConfig
from fliiq.runtime.agent.loop import AgentResult, agent_loop
from fliiq.runtime.agent.tools import ToolRegistry
from fliiq.runtime.llm.providers import LLMProvider, LLMResponse, ToolCall


def _make_text_response(text: str) -> LLMResponse:
    return LLMResponse(
        content=text,
        model="test-model",
        provider=LLMProvider.ANTHROPIC,
        stop_reason="end_turn",
        raw_content=[{"type": "text", "text": text}],
    )


def _make_tool_response(text: str, tool_calls: list[ToolCall]) -> LLMResponse:
    raw = []
    if text:
        raw.append({"type": "text", "text": text})
    for tc in tool_calls:
        raw.append({"type": "tool_use", "id": tc.id, "name": tc.name, "input": tc.input})
    return LLMResponse(
        content=text,
        model="test-model",
        provider=LLMProvider.ANTHROPIC,
        tool_calls=tool_calls,
        stop_reason="tool_use",
        raw_content=raw,
    )


async def test_text_only_response():
    """Agent returns immediately when model gives text-only response."""
    llm = AsyncMock()
    llm.generate = AsyncMock(return_value=_make_text_response("Hello!"))

    tools = ToolRegistry()
    messages = [{"role": "user", "content": "hi"}]

    result = await agent_loop(llm, messages, tools)

    assert result.final_text == "Hello!"
    assert result.iterations == 1
    assert result.stop_reason == "end_turn"


async def test_tool_call_then_text():
    """Agent calls a tool, gets result, then model responds with text."""
    tc = ToolCall(id="tc_1", name="get_time", input={"tz": "UTC"})

    llm = AsyncMock()
    llm.generate = AsyncMock(side_effect=[
        _make_tool_response("Let me check.", [tc]),
        _make_text_response("The time is 12:00 UTC."),
    ])

    tools = ToolRegistry()
    tools.register(
        "get_time",
        "Get current time",
        {"type": "object", "properties": {"tz": {"type": "string"}}},
        lambda params: "12:00 UTC",
    )

    messages = [{"role": "user", "content": "what time is it?"}]
    result = await agent_loop(llm, messages, tools)

    assert result.final_text == "The time is 12:00 UTC."
    assert result.iterations == 2
    assert result.stop_reason == "end_turn"
    # Messages should have: user, assistant (tool call), user (tool result), assistant (final)
    assert len(result.messages) == 4


async def test_max_iterations():
    """Agent stops at max_iterations."""
    tc = ToolCall(id="tc_1", name="noop", input={})

    llm = AsyncMock()
    llm.generate = AsyncMock(return_value=_make_tool_response("working...", [tc]))

    tools = ToolRegistry()
    tools.register("noop", "Does nothing", {"type": "object"}, lambda p: "ok")

    config = AgentConfig(max_iterations=3)
    messages = [{"role": "user", "content": "loop forever"}]

    result = await agent_loop(llm, messages, tools, config=config)

    assert result.iterations == 3
    assert result.stop_reason == "max_iterations"


async def test_tool_execution_error():
    """Tool errors are returned as error strings, not exceptions."""
    tc = ToolCall(id="tc_1", name="fail_tool", input={})

    llm = AsyncMock()
    llm.generate = AsyncMock(side_effect=[
        _make_tool_response("", [tc]),
        _make_text_response("Tool failed, sorry."),
    ])

    tools = ToolRegistry()

    def _fail(params):
        raise ValueError("Something broke")

    tools.register("fail_tool", "Always fails", {"type": "object"}, _fail)

    messages = [{"role": "user", "content": "do the thing"}]
    result = await agent_loop(llm, messages, tools)

    # The error was caught and returned as a string, loop continued
    assert result.iterations == 2
    assert result.stop_reason == "end_turn"


async def test_unknown_tool():
    """Unknown tool names return error string."""
    tc = ToolCall(id="tc_1", name="nonexistent", input={})

    llm = AsyncMock()
    llm.generate = AsyncMock(side_effect=[
        _make_tool_response("", [tc]),
        _make_text_response("I couldn't find that tool."),
    ])

    tools = ToolRegistry()
    messages = [{"role": "user", "content": "use a tool"}]

    result = await agent_loop(llm, messages, tools)
    assert result.iterations == 2


async def test_on_tool_call_callback():
    """on_tool_call callback is invoked for each tool call."""
    tc = ToolCall(id="tc_1", name="echo", input={"msg": "hello"})
    callback_calls = []

    llm = AsyncMock()
    llm.generate = AsyncMock(side_effect=[
        _make_tool_response("", [tc]),
        _make_text_response("done"),
    ])

    tools = ToolRegistry()
    tools.register("echo", "Echo", {"type": "object"}, lambda p: p.get("msg", ""))

    messages = [{"role": "user", "content": "echo hello"}]
    result = await agent_loop(
        llm, messages, tools,
        on_tool_call=lambda name, inp, res: callback_calls.append((name, inp, res)),
    )

    assert len(callback_calls) == 1
    assert callback_calls[0][0] == "echo"
    assert callback_calls[0][2] == "hello"


async def test_no_tools_registered():
    """Agent works fine with empty tool registry (text-only mode)."""
    llm = AsyncMock()
    llm.generate = AsyncMock(return_value=_make_text_response("Just text."))

    tools = ToolRegistry()
    messages = [{"role": "user", "content": "hi"}]

    result = await agent_loop(llm, messages, tools)
    assert result.final_text == "Just text."
    assert result.iterations == 1


async def test_ask_user_tool_in_loop():
    """Agent calls ask_user tool, gets answer, continues."""
    tc = ToolCall(id="tc_1", name="ask_user", input={"question": "SQLite or Postgres?"})

    llm = AsyncMock()
    llm.generate = AsyncMock(side_effect=[
        _make_tool_response("I need to know your preference.", [tc]),
        _make_text_response("Got it, using SQLite."),
    ])

    tools = ToolRegistry()
    tools.register_builtins(ask_user_handler=lambda q: "SQLite", mode="supervised")

    messages = [{"role": "user", "content": "set up a database"}]
    result = await agent_loop(llm, messages, tools)

    assert result.final_text == "Got it, using SQLite."
    assert result.iterations == 2
    # Verify the tool result message contains the user's answer
    tool_result_msg = result.messages[2]
    assert tool_result_msg["role"] == "user"
    assert tool_result_msg["content"][0]["content"] == "SQLite"
