"""
Integration tests for _run_standalone() with mocked Runner.run_streamed().

These tests verify the event mapping, budget tracking, and error handling
in standalone mode without requiring a real OpenAI API key.
"""

import io
from dataclasses import dataclass
from typing import Any, AsyncIterator, List
from unittest.mock import patch, MagicMock

import pytest

from .runner import KivotRunner, _try_parse_json, _extract_messages_from_stream
from .budget import BudgetConfig


# ---- Mock helpers ----


@dataclass
class MockUsage:
    input_tokens: int = 100
    output_tokens: int = 50
    total_tokens: int = 150
    requests: int = 1


@dataclass
class MockContextWrapper:
    usage: MockUsage


class MockRawItem:
    """Mock for raw OpenAI item attributes."""

    def __init__(self, **kwargs: Any) -> None:
        for k, v in kwargs.items():
            setattr(self, k, v)


class MockItem:
    """Mock for RunItem (ToolCallItem, ToolCallOutputItem, MessageOutputItem)."""

    def __init__(self, type: str, raw_item: Any = None, output: Any = None) -> None:
        self.type = type
        self.raw_item = raw_item or MockRawItem()
        self.output = output


class MockRawResponsesStreamEvent:
    """Mock for RawResponsesStreamEvent."""

    def __init__(self, data_type: str, **data_attrs: Any) -> None:
        self.data = MockRawItem(type=data_type, **data_attrs)


class MockRunItemStreamEvent:
    """Mock for RunItemStreamEvent."""

    def __init__(self, name: str, item: MockItem) -> None:
        self.name = name
        self.item = item


class MockAgentUpdatedStreamEvent:
    """Mock for AgentUpdatedStreamEvent."""

    def __init__(self, agent_name: str) -> None:
        self.new_agent = MockRawItem(name=agent_name)


class MockRunResultStreaming:
    """Mock for RunResultStreaming returned by Runner.run_streamed()."""

    def __init__(
        self,
        events: List[Any],
        final_output: str = "Hello!",
        usage: MockUsage | None = None,
        new_items: List[Any] | None = None,
    ) -> None:
        self._events = events
        self.final_output = final_output
        self.context_wrapper = MockContextWrapper(usage=usage or MockUsage())
        self.new_items = new_items or []

    async def stream_events(self) -> AsyncIterator[Any]:
        for event in self._events:
            yield event


class MockAgent:
    """Mock agent for testing."""

    def __init__(
        self,
        name: str = "test-agent",
        model: str = "gpt-4o-mini",
        instructions: str = "You are helpful.",
    ) -> None:
        self.name = name
        self.model = model
        self.instructions = instructions


# ---- Tests ----


class TestRunStandalone:
    """Tests for KivotRunner._run_standalone() with mocked streaming."""

    @pytest.mark.asyncio
    async def test_standalone_emits_basic_events(self) -> None:
        """Basic flow: run.started, model.start, model.delta, model.end, run.completed."""
        events = [
            MockRawResponsesStreamEvent("response.created"),
            MockRawResponsesStreamEvent("response.output_text.delta", delta="Hello"),
            MockRawResponsesStreamEvent("response.output_text.delta", delta=" world"),
            MockRawResponsesStreamEvent("response.completed"),
        ]
        mock_result = MockRunResultStreaming(
            events=events,
            final_output="Hello world",
            usage=MockUsage(input_tokens=50, output_tokens=20, total_tokens=70),
        )

        buf = io.StringIO()

        with patch("kivot_agents.runner._HAS_TEMPORAL", False), \
             patch("kivot_agents.runner.KivotRunner._run_standalone") as mock_standalone:
            # We call _run_standalone directly to avoid routing logic
            pass

        # Call _run_standalone directly with mocked imports
        with patch.dict("sys.modules", {
            "agents": MagicMock(),
            "agents.stream_events": MagicMock(),
        }):
            # Mock the actual Runner.run_streamed
            import sys
            mock_agents = sys.modules["agents"]
            mock_agents.Runner.run_streamed.return_value = mock_result

            mock_stream_events = sys.modules["agents.stream_events"]
            mock_stream_events.RawResponsesStreamEvent = MockRawResponsesStreamEvent
            mock_stream_events.RunItemStreamEvent = MockRunItemStreamEvent
            mock_stream_events.AgentUpdatedStreamEvent = MockAgentUpdatedStreamEvent

            # Patch stderr to capture events
            with patch("kivot_agents.standalone.sys.stderr", buf):
                agent = MockAgent()
                result = await KivotRunner._run_standalone(agent, input="Hi")

        output = buf.getvalue()
        assert "[run.started]" in output
        assert "[model.start]" in output
        assert "[model.delta]" in output
        assert "[model.end]" in output
        assert "[run.completed]" in output
        assert result.final_output == "Hello world"
        assert result.usage is not None
        assert result.usage["total_tokens"] == 70

    @pytest.mark.asyncio
    async def test_standalone_tool_call_events(self) -> None:
        """tool_called + tool_output mapped to tool.call.started/completed."""
        tool_call_item = MockItem(
            type="tool_call_item",
            raw_item=MockRawItem(name="greet", arguments='{"name":"Alice"}', call_id="c1"),
        )
        tool_output_item = MockItem(
            type="tool_call_output_item",
            raw_item=MockRawItem(call_id="c1"),
            output="Hello, Alice!",
        )

        events = [
            MockRawResponsesStreamEvent("response.created"),
            MockRunItemStreamEvent("tool_called", tool_call_item),
            MockRunItemStreamEvent("tool_output", tool_output_item),
            MockRawResponsesStreamEvent("response.completed"),
        ]

        mock_result = MockRunResultStreaming(
            events=events,
            final_output="Greeted Alice",
            new_items=[tool_call_item, tool_output_item],
        )

        buf = io.StringIO()

        with patch.dict("sys.modules", {
            "agents": MagicMock(),
            "agents.stream_events": MagicMock(),
        }):
            import sys
            mock_agents = sys.modules["agents"]
            mock_agents.Runner.run_streamed.return_value = mock_result

            mock_stream_events = sys.modules["agents.stream_events"]
            mock_stream_events.RawResponsesStreamEvent = MockRawResponsesStreamEvent
            mock_stream_events.RunItemStreamEvent = MockRunItemStreamEvent
            mock_stream_events.AgentUpdatedStreamEvent = MockAgentUpdatedStreamEvent

            with patch("kivot_agents.standalone.sys.stderr", buf):
                agent = MockAgent()
                result = await KivotRunner._run_standalone(agent, input="Greet Alice")

        output = buf.getvalue()
        assert "[tool.call.started]" in output
        assert "tool=greet" in output
        assert "[tool.call.completed]" in output
        assert "Hello, Alice!" in output

    @pytest.mark.asyncio
    async def test_standalone_quiet_mode(self) -> None:
        """quiet=True suppresses event output."""
        events = [
            MockRawResponsesStreamEvent("response.created"),
            MockRawResponsesStreamEvent("response.completed"),
        ]
        mock_result = MockRunResultStreaming(events=events, final_output="Quiet")

        buf = io.StringIO()

        with patch.dict("sys.modules", {
            "agents": MagicMock(),
            "agents.stream_events": MagicMock(),
        }):
            import sys
            mock_agents = sys.modules["agents"]
            mock_agents.Runner.run_streamed.return_value = mock_result

            mock_stream_events = sys.modules["agents.stream_events"]
            mock_stream_events.RawResponsesStreamEvent = MockRawResponsesStreamEvent
            mock_stream_events.RunItemStreamEvent = MockRunItemStreamEvent
            mock_stream_events.AgentUpdatedStreamEvent = MockAgentUpdatedStreamEvent

            with patch("kivot_agents.standalone.sys.stderr", buf):
                agent = MockAgent()
                result = await KivotRunner._run_standalone(
                    agent, input="Hi", quiet=True
                )

        # No events should be printed
        assert buf.getvalue() == ""
        assert result.final_output == "Quiet"
        assert result.usage is not None

    @pytest.mark.asyncio
    async def test_standalone_budget_violation_logged(self) -> None:
        """Budget exceeded -> run.failed event in log."""
        events = [
            MockRawResponsesStreamEvent("response.created"),
            MockRawResponsesStreamEvent("response.completed"),
        ]
        mock_result = MockRunResultStreaming(
            events=events,
            final_output="Done",
            usage=MockUsage(input_tokens=200, output_tokens=100, total_tokens=300),
        )

        buf = io.StringIO()

        with patch.dict("sys.modules", {
            "agents": MagicMock(),
            "agents.stream_events": MagicMock(),
        }):
            import sys
            mock_agents = sys.modules["agents"]
            mock_agents.Runner.run_streamed.return_value = mock_result

            mock_stream_events = sys.modules["agents.stream_events"]
            mock_stream_events.RawResponsesStreamEvent = MockRawResponsesStreamEvent
            mock_stream_events.RunItemStreamEvent = MockRunItemStreamEvent
            mock_stream_events.AgentUpdatedStreamEvent = MockAgentUpdatedStreamEvent

            with patch("kivot_agents.standalone.sys.stderr", buf):
                agent = MockAgent()
                result = await KivotRunner._run_standalone(
                    agent,
                    input="Hi",
                    budgets=BudgetConfig(max_tokens=100),
                )

        output = buf.getvalue()
        assert "[run.failed]" in output
        assert "budget_exceeded" in output
        assert "token limit" in output
        # run.completed should still be emitted after the budget warning
        assert "[run.completed]" in output

    @pytest.mark.asyncio
    async def test_standalone_error_handling(self) -> None:
        """Exception during streaming -> run.failed + re-raise."""

        class FailingResult:
            final_output = ""
            context_wrapper = MockContextWrapper(usage=MockUsage())
            new_items = []

            async def stream_events(self):
                raise ValueError("API connection failed")
                yield  # make it a generator  # noqa: E501

        buf = io.StringIO()

        with patch.dict("sys.modules", {
            "agents": MagicMock(),
            "agents.stream_events": MagicMock(),
        }):
            import sys
            mock_agents = sys.modules["agents"]
            mock_agents.Runner.run_streamed.return_value = FailingResult()

            mock_stream_events = sys.modules["agents.stream_events"]
            mock_stream_events.RawResponsesStreamEvent = MockRawResponsesStreamEvent
            mock_stream_events.RunItemStreamEvent = MockRunItemStreamEvent
            mock_stream_events.AgentUpdatedStreamEvent = MockAgentUpdatedStreamEvent

            with patch("kivot_agents.standalone.sys.stderr", buf):
                agent = MockAgent()
                with pytest.raises(ValueError, match="API connection failed"):
                    await KivotRunner._run_standalone(agent, input="Hi")

        output = buf.getvalue()
        assert "[run.started]" in output
        assert "[run.failed]" in output
        assert "API connection failed" in output


class TestHelperFunctions:
    """Tests for standalone helper functions."""

    def test_try_parse_json_valid(self) -> None:
        result = _try_parse_json('{"name": "Alice"}')
        assert result == {"name": "Alice"}

    def test_try_parse_json_invalid(self) -> None:
        result = _try_parse_json("not json")
        assert result == "not json"

    def test_try_parse_json_none(self) -> None:
        result = _try_parse_json(None)
        assert result is None

    def test_extract_messages_empty(self) -> None:
        mock_result = MockRunResultStreaming(events=[], new_items=[])
        messages = _extract_messages_from_stream(mock_result)
        assert messages == []

    def test_extract_messages_with_items(self) -> None:
        content_part = MockRawItem(text="Hello!")
        msg_item = MockItem(
            type="message_output_item",
            raw_item=MockRawItem(content=[content_part]),
        )
        tool_call_item = MockItem(
            type="tool_call_item",
            raw_item=MockRawItem(call_id="c1", name="greet", arguments='{"x":1}'),
        )
        tool_output_item = MockItem(
            type="tool_call_output_item",
            raw_item=MockRawItem(call_id="c1"),
            output="result",
        )

        mock_result = MockRunResultStreaming(
            events=[],
            new_items=[tool_call_item, tool_output_item, msg_item],
        )
        messages = _extract_messages_from_stream(mock_result)

        assert len(messages) == 3
        assert messages[0]["role"] == "assistant"
        assert messages[0]["tool_calls"][0]["name"] == "greet"
        assert messages[1]["role"] == "tool"
        assert messages[1]["content"] == "result"
        assert messages[2]["role"] == "assistant"
        assert messages[2]["content"] == "Hello!"
