"""Tests for ReAct engine token streaming via execute_stream()."""

import pytest
from unittest.mock import AsyncMock, MagicMock

from agentkit.core.react import ReActEngine, ReActEvent
from agentkit.llm.gateway import LLMGateway
from agentkit.llm.protocol import LLMResponse, StreamChunk, TokenUsage


def _make_stream_chunks(
    content_parts: list[str],
    model: str = "test-model",
    usage: TokenUsage | None = None,
) -> list[StreamChunk]:
    """Build a list of StreamChunk objects simulating a streaming response."""
    chunks = []
    for part in content_parts:
        chunks.append(StreamChunk(content=part, model=model))
    # Final chunk with usage
    chunks.append(StreamChunk(
        content="",
        model=model,
        usage=usage or TokenUsage(prompt_tokens=10, completion_tokens=20),
        is_final=True,
    ))
    return chunks


def _make_stream_gateway(chunks_list: list[list[StreamChunk]]) -> MagicMock:
    """Create a mock LLMGateway whose chat_stream yields the given chunks."""
    gateway = MagicMock(spec=LLMGateway)

    async def _stream(**kwargs):
        for chunks in chunks_list:
            for chunk in chunks:
                yield chunk
        # Remove after use
        chunks_list.pop(0)

    gateway.chat_stream = _stream
    return gateway


class TestReActTokenStreaming:
    """Test that execute_stream() yields token events from chat_stream()."""

    @pytest.mark.asyncio
    async def test_yields_token_events(self):
        """execute_stream should yield token events for each stream chunk."""
        chunks = _make_stream_chunks(["Hello", " world", "!"])
        gateway = _make_stream_gateway([chunks])
        engine = ReActEngine(llm_gateway=gateway)

        events = []
        async for event in engine.execute_stream(
            messages=[{"role": "user", "content": "Hi"}],
        ):
            events.append(event)

        token_events = [e for e in events if e.event_type == "token"]
        assert len(token_events) == 3
        assert token_events[0].data["content"] == "Hello"
        assert token_events[1].data["content"] == " world"
        assert token_events[2].data["content"] == "!"

    @pytest.mark.asyncio
    async def test_final_answer_after_streaming(self):
        """After streaming tokens, a final_answer event should be yielded."""
        chunks = _make_stream_chunks(["The answer is 42"])
        gateway = _make_stream_gateway([chunks])
        engine = ReActEngine(llm_gateway=gateway)

        events = []
        async for event in engine.execute_stream(
            messages=[{"role": "user", "content": "What?"}],
        ):
            events.append(event)

        final_events = [e for e in events if e.event_type == "final_answer"]
        assert len(final_events) == 1
        assert "42" in final_events[0].data.get("output", "")

    @pytest.mark.asyncio
    async def test_token_events_have_correct_step(self):
        """Token events should carry the current step number."""
        chunks = _make_stream_chunks(["Hi"])
        gateway = _make_stream_gateway([chunks])
        engine = ReActEngine(llm_gateway=gateway)

        events = []
        async for event in engine.execute_stream(
            messages=[{"role": "user", "content": "Hi"}],
        ):
            events.append(event)

        token_events = [e for e in events if e.event_type == "token"]
        for te in token_events:
            assert te.step >= 1

    @pytest.mark.asyncio
    async def test_build_response_from_stream(self):
        """_build_response_from_stream should construct a valid LLMResponse."""
        usage = TokenUsage(prompt_tokens=5, completion_tokens=10)
        response = ReActEngine._build_response_from_stream(
            content="Hello world",
            tool_calls=[],
            usage=usage,
            model="test-model",
        )
        assert response.content == "Hello world"
        assert response.model == "test-model"
        assert response.usage.prompt_tokens == 5
        assert response.usage.completion_tokens == 10
        assert not response.has_tool_calls

    @pytest.mark.asyncio
    async def test_build_response_from_stream_no_usage(self):
        """_build_response_from_stream should handle None usage gracefully."""
        response = ReActEngine._build_response_from_stream(
            content="test",
            tool_calls=[],
            usage=None,
            model="test-model",
        )
        assert response.content == "test"
        assert response.usage.prompt_tokens == 0
