"""Tests for the ReAct reasoning loop."""

from __future__ import annotations

from typing import Any

import pytest

from agent_scout.events import Event, EventBus, EventType
from agent_scout.react_loop import (
    Action,
    ActionType,
    ReActConfig,
    ReActLoop,
    Step,
)

# --- Mock implementations ---


class MockLLM:
    """Mock LLM that returns scripted thoughts and actions."""

    def __init__(self, script: list[tuple[str, Action]]) -> None:
        self._script = list(script)
        self._call_count = 0

    async def think(
        self,
        task: str,
        steps: list[Step],
        available_tools: list[dict[str, Any]],
    ) -> tuple[str, Action]:
        if self._call_count < len(self._script):
            thought, action = self._script[self._call_count]
            self._call_count += 1
            return thought, action
        # Default: finish
        return "Done.", Action(action_type=ActionType.FINISH, message="Complete")

    async def reflect(
        self,
        task: str,
        steps: list[Step],
        latest_observation: str,
    ) -> str:
        return f"Reflected on: {latest_observation[:50]}"


class MockTools:
    """Mock tool executor."""

    def __init__(self, results: dict[str, str] | None = None) -> None:
        self._results = results or {}
        self.calls: list[tuple[str, dict[str, Any]]] = []

    async def execute(self, tool_name: str, tool_input: dict[str, Any]) -> str:
        self.calls.append((tool_name, tool_input))
        if tool_name in self._results:
            return self._results[tool_name]
        return f"Result from {tool_name}"

    def get_available_tools(self) -> list[dict[str, Any]]:
        return [
            {"name": "web_search", "description": "Search the web"},
            {"name": "browse", "description": "Browse a URL"},
        ]


class MockUser:
    """Mock user interaction."""

    def __init__(self, responses: list[str] | None = None) -> None:
        self._responses = list(responses or ["yes"])
        self._idx = 0

    async def ask(self, message: str) -> str:
        if self._idx < len(self._responses):
            resp = self._responses[self._idx]
            self._idx += 1
            return resp
        return "ok"


@pytest.fixture
def bus():
    return EventBus()


# --- Tests ---


async def test_simple_finish(bus: EventBus):
    """Agent immediately finishes."""
    llm = MockLLM(
        [
            ("Task is trivial.", Action(action_type=ActionType.FINISH, message="Done")),
        ]
    )
    tools = MockTools()
    loop = ReActLoop(llm, tools, bus)

    steps = await loop.run("Say hello")
    assert len(steps) == 1
    assert steps[0].action.action_type == ActionType.FINISH
    assert steps[0].thought == "Task is trivial."


async def test_tool_call_then_finish(bus: EventBus):
    """Agent calls a tool, then finishes."""
    llm = MockLLM(
        [
            (
                "I need to search for flights.",
                Action(
                    action_type=ActionType.TOOL_CALL,
                    tool_name="web_search",
                    tool_input={"query": "flights NYC London"},
                ),
            ),
            (
                "Found results. Task complete.",
                Action(
                    action_type=ActionType.FINISH,
                    message="Best flight: $450 on BA",
                    result={"price": 450},
                ),
            ),
        ]
    )
    tools = MockTools(results={"web_search": "BA: $450, UA: $520, AA: $480"})
    loop = ReActLoop(llm, tools, bus)

    steps = await loop.run("Find flights NYC to London")
    assert len(steps) == 2
    assert steps[0].action.action_type == ActionType.TOOL_CALL
    assert steps[0].observation == "BA: $450, UA: $520, AA: $480"
    assert steps[1].action.action_type == ActionType.FINISH
    assert tools.calls == [("web_search", {"query": "flights NYC London"})]


async def test_ask_user(bus: EventBus):
    """Agent asks the user a question."""
    llm = MockLLM(
        [
            (
                "Need to confirm budget.",
                Action(
                    action_type=ActionType.ASK_USER,
                    message="What is your max budget?",
                ),
            ),
            (
                "User said $500. Finishing.",
                Action(action_type=ActionType.FINISH, message="Done"),
            ),
        ]
    )
    tools = MockTools()
    user = MockUser(responses=["$500"])
    loop = ReActLoop(llm, tools, bus, user=user)

    steps = await loop.run("Find flights")
    assert len(steps) == 2
    assert steps[0].action.action_type == ActionType.ASK_USER
    assert steps[0].observation == "$500"


async def test_ask_user_without_user_handler(bus: EventBus):
    """Agent asks user but no user handler is available."""
    llm = MockLLM(
        [
            (
                "Need input.",
                Action(action_type=ActionType.ASK_USER, message="Preference?"),
            ),
            ("Proceeding without input.", Action(action_type=ActionType.FINISH)),
        ]
    )
    tools = MockTools()
    loop = ReActLoop(llm, tools, bus, user=None)

    steps = await loop.run("Task")
    assert steps[0].observation == "No user interaction available."


async def test_max_iterations(bus: EventBus):
    """Loop stops at max iterations."""
    # LLM never finishes — always calls tool
    infinite_script = [
        (
            f"Step {i}",
            Action(
                action_type=ActionType.TOOL_CALL,
                tool_name="web_search",
                tool_input={"q": f"query {i}"},
            ),
        )
        for i in range(100)
    ]
    llm = MockLLM(infinite_script)
    tools = MockTools()
    config = ReActConfig(max_iterations=5)
    loop = ReActLoop(llm, tools, bus, config=config)

    steps = await loop.run("Infinite task")
    assert len(steps) == 5


async def test_stop_mid_execution(bus: EventBus):
    """Loop can be stopped externally."""
    call_count = 0

    class StoppingLLM:
        def __init__(self, loop_ref: list):
            self._loop_ref = loop_ref

        async def think(self, task, steps, tools):
            nonlocal call_count
            call_count += 1
            if call_count == 2 and self._loop_ref:
                self._loop_ref[0].stop()
            return f"Think {call_count}", Action(
                action_type=ActionType.TOOL_CALL,
                tool_name="web_search",
                tool_input={},
            )

        async def reflect(self, task, steps, obs):
            return "ok"

    loop_ref: list = []
    llm = StoppingLLM(loop_ref)
    tools = MockTools()
    config = ReActConfig(max_iterations=10)
    loop = ReActLoop(llm, tools, bus, config=config)
    loop_ref.append(loop)

    steps = await loop.run("Task")
    # Should stop after step 2 (where stop was called)
    assert len(steps) == 2


async def test_events_emitted(bus: EventBus):
    """Verify all expected events are emitted during a step."""
    received: list[Event] = []

    async def handler(event: Event):
        received.append(event)

    bus.subscribe(handler, event_type=None)

    llm = MockLLM(
        [
            (
                "Searching.",
                Action(
                    action_type=ActionType.TOOL_CALL,
                    tool_name="web_search",
                    tool_input={"q": "test"},
                ),
            ),
            ("Done.", Action(action_type=ActionType.FINISH)),
        ]
    )
    tools = MockTools()
    loop = ReActLoop(llm, tools, bus)

    await loop.run("Test task", task_id="t-1")

    event_types = [e.event_type for e in received]
    # Step 1: thought, action, tool_call_start, tool_call_end, observation, reflection
    # Step 2: thought, action, observation (finish — no reflection)
    assert EventType.THOUGHT in event_types
    assert EventType.ACTION in event_types
    assert EventType.TOOL_CALL_START in event_types
    assert EventType.TOOL_CALL_END in event_types
    assert EventType.OBSERVATION in event_types
    assert EventType.REFLECTION in event_types

    # All events should have the task_id
    for e in received:
        assert e.task_id == "t-1"


async def test_tool_error_handled(bus: EventBus):
    """Tool errors are caught and returned as observations."""

    class ErrorTools:
        async def execute(self, tool_name, tool_input):
            raise ConnectionError("Network down")

        def get_available_tools(self):
            return [{"name": "web_search", "description": "Search"}]

    llm = MockLLM(
        [
            (
                "Searching.",
                Action(
                    action_type=ActionType.TOOL_CALL,
                    tool_name="web_search",
                    tool_input={},
                ),
            ),
            ("Tool failed. Finishing.", Action(action_type=ActionType.FINISH)),
        ]
    )
    tools = ErrorTools()
    loop = ReActLoop(llm, tools, bus)

    steps = await loop.run("Test")
    assert "Tool error" in steps[0].observation
    assert "Network down" in steps[0].observation


async def test_reflection_disabled(bus: EventBus):
    """Reflection can be disabled in config."""
    received: list[Event] = []

    async def handler(event: Event):
        received.append(event)

    bus.subscribe(handler, EventType.REFLECTION)

    llm = MockLLM(
        [
            (
                "Search.",
                Action(
                    action_type=ActionType.TOOL_CALL,
                    tool_name="web_search",
                    tool_input={},
                ),
            ),
            ("Done.", Action(action_type=ActionType.FINISH)),
        ]
    )
    tools = MockTools()
    config = ReActConfig(enable_reflection=False)
    loop = ReActLoop(llm, tools, bus, config=config)

    steps = await loop.run("Test")
    assert all(s.reflection == "" for s in steps)
    assert len(received) == 0  # No reflection events emitted


async def test_steps_property(bus: EventBus):
    """Steps property returns a copy."""
    llm = MockLLM([("Done.", Action(action_type=ActionType.FINISH))])
    tools = MockTools()
    loop = ReActLoop(llm, tools, bus)

    await loop.run("Test")
    steps = loop.steps
    assert len(steps) == 1
    # Modifying the returned list shouldn't affect internal state
    steps.clear()
    assert len(loop.steps) == 1
