"""AgentLoop shared-path behavior tests."""

from __future__ import annotations

from typing import Any

import pytest

from supply_agent.agent.loop import AgentLoop
from supply_agent.tools.base import tool
from supply_agent.tools.registry import ToolRegistry
from supply_agent.types import (
    AgentEventType,
    Message,
    Role,
    ToolCall,
)


class _FakeLLM:
    """Deterministic LLM that returns scripted assistant messages."""

    def __init__(self, responses: list[Message]) -> None:
        self._responses = list(responses)
        self.calls: list[dict[str, Any]] = []

    def chat(
        self,
        messages: list[Message],
        tools: list[Any] | None = None,
        temperature: float | None = None,
        *,
        iteration: int = 0,
    ) -> Message:
        self.calls.append(
            {
                "messages": list(messages),
                "tools": tools,
                "iteration": iteration,
            }
        )
        if not self._responses:
            raise AssertionError("FakeLLM: no more scripted responses")
        return self._responses.pop(0)

    async def achat(
        self,
        messages: list[Message],
        tools: list[Any] | None = None,
        temperature: float | None = None,
        *,
        iteration: int = 0,
    ) -> Message:
        return self.chat(
            messages,
            tools=tools,
            temperature=temperature,
            iteration=iteration,
        )


def _system() -> Message:
    return Message(role=Role.SYSTEM, content="base system")


def _assistant_text(content: str) -> Message:
    return Message(role=Role.ASSISTANT, content=content)


def _assistant_tools(*calls: ToolCall) -> Message:
    return Message(role=Role.ASSISTANT, content=None, tool_calls=list(calls))


@pytest.fixture
def skill_tools() -> ToolRegistry:
    registry = ToolRegistry()

    @tool(name="load_skill")
    def load_skill(name: str) -> str:
        return f"skill-body:{name}"

    registry.register(load_skill, name="load_skill")
    return registry


def test_stream_loads_skill_into_system_message(skill_tools: ToolRegistry) -> None:
    active_skills: list[str] = []
    system_parts = {"value": "base system"}

    def builder() -> Message:
        parts = [system_parts["value"]]
        for name in active_skills:
            parts.append(f"LOADED:{name}")
        return Message(role=Role.SYSTEM, content="\n\n".join(parts))

    llm = _FakeLLM(
        [
            _assistant_tools(
                ToolCall(
                    id="c1",
                    name="load_skill",
                    arguments='{"name": "demo"}',
                )
            ),
            _assistant_text("done"),
        ]
    )
    loop = AgentLoop(
        llm=llm,  # type: ignore[arg-type]
        tools=skill_tools,
        system_message=builder(),
        system_message_builder=builder,
        messages=[Message(role=Role.USER, content="hi")],
        max_iterations=5,
        active_skills=active_skills,
    )

    events = list(loop.stream())
    assert any(e.type == AgentEventType.DONE for e in events)
    assert "demo" in active_skills
    # Second LLM call must see refreshed system prompt with skill content.
    assert "LOADED:demo" in (llm.calls[1]["messages"][0].content or "")


def test_stream_nudges_best_answer_on_max_iterations() -> None:
    registry = ToolRegistry()

    @tool(name="noop")
    def noop() -> str:
        return '{"ok": true}'

    registry.register(noop, name="noop")

    llm = _FakeLLM(
        [
            _assistant_tools(ToolCall(id="c1", name="noop", arguments="{}")),
            _assistant_text("final after nudge"),
        ]
    )
    loop = AgentLoop(
        llm=llm,  # type: ignore[arg-type]
        tools=registry,
        system_message=_system(),
        messages=[Message(role=Role.USER, content="hi")],
        max_iterations=1,
    )

    events = list(loop.stream())
    done = next(e for e in events if e.type == AgentEventType.DONE)
    assert done.data["content"] == "final after nudge"
    assert len(llm.calls) == 2
    assert "best answer" in (llm.calls[1]["messages"][-1].content or "").lower()


def test_run_and_stream_share_max_iteration_nudge() -> None:
    registry = ToolRegistry()

    @tool(name="noop")
    def noop() -> str:
        return '{"ok": true}'

    registry.register(noop, name="noop")

    def make_loop(responses: list[Message]) -> AgentLoop:
        return AgentLoop(
            llm=_FakeLLM(responses),  # type: ignore[arg-type]
            tools=registry,
            system_message=_system(),
            messages=[Message(role=Role.USER, content="hi")],
            max_iterations=1,
        )

    run_result = make_loop(
        [
            _assistant_tools(ToolCall(id="c1", name="noop", arguments="{}")),
            _assistant_text("from-run"),
        ]
    ).run()
    stream_events = list(
        make_loop(
            [
                _assistant_tools(ToolCall(id="c1", name="noop", arguments="{}")),
                _assistant_text("from-stream"),
            ]
        ).stream()
    )
    stream_done = next(e for e in stream_events if e.type == AgentEventType.DONE)

    assert run_result.content == "from-run"
    assert stream_done.data["content"] == "from-stream"
    assert run_result.iterations == stream_done.data["iterations"] == 1


@pytest.mark.asyncio
async def test_astream_loads_skill(skill_tools: ToolRegistry) -> None:
    active_skills: list[str] = []

    def builder() -> Message:
        parts = ["base"]
        for name in active_skills:
            parts.append(f"LOADED:{name}")
        return Message(role=Role.SYSTEM, content="\n\n".join(parts))

    llm = _FakeLLM(
        [
            _assistant_tools(
                ToolCall(
                    id="c1",
                    name="load_skill",
                    arguments='{"name": "async-demo"}',
                )
            ),
            _assistant_text("ok"),
        ]
    )
    loop = AgentLoop(
        llm=llm,  # type: ignore[arg-type]
        tools=skill_tools,
        system_message=builder(),
        system_message_builder=builder,
        messages=[Message(role=Role.USER, content="hi")],
        max_iterations=5,
        active_skills=active_skills,
    )

    events = [event async for event in loop.astream()]
    assert any(e.type == AgentEventType.DONE for e in events)
    assert "async-demo" in active_skills
    assert "LOADED:async-demo" in (llm.calls[1]["messages"][0].content or "")
