"""Tests for SSE stream consumer: no duplicate events on line-by-line arrival.

Regression test for the bug where:
1. Original code accumulated lines into buffer, parsed on blank line,
   but cleared buffer after processing -- correct behavior.
2. An attempted fix added `processed_event_count` to skip already-seen
   events, but since the buffer WAS already cleared, the skip counter
   caused events to be DROPPED (not duplicated).

The root cause: `processed_event_count` persisted across buffer clears,
so after the first batch (count=1), a fresh buffer producing 1 event
would yield `events[1:]` = empty, silently dropping every subsequent event.

Fix: remove `processed_event_count` entirely. Buffer clear on line 505
(now 505) already prevents re-processing.
"""

from __future__ import annotations

import json

from anima.shell.cli_commands.talk_streaming import parse_sse_events, SSEEvent


def _make_chunk_lines(turn: int, seq: int, text: str) -> list[str]:
    """Build SSE lines for a single chunk event."""
    return [
        "event: chunk",
        f'data: {{"turn": {turn}, "seq": {seq}, "chunk": {json.dumps(text)}}}',
        "",
    ]


def _make_done_lines(turn: int, seq: int) -> list[str]:
    """Build SSE lines for a done event."""
    return [
        "event: done",
        f'data: {{"turn": {turn}, "seq": {seq}, "stop_reason": "complete"}}',
        "",
    ]


def _make_tool_call_lines(turn: int, tool_name: str) -> list[str]:
    """Build SSE lines for a tool_call event."""
    return [
        "event: tool_call",
        f'data: {{"turn": {turn}, "tool_name": {json.dumps(tool_name)}, "tool_arg_preview": "..."}}',
        "",
    ]


def _make_error_lines(turn: int, error: str) -> list[str]:
    """Build SSE lines for an error event."""
    return [
        "event: error",
        f'data: {{"turn": {turn}, "error": {json.dumps(error)}, "code": "INTERNAL"}}',
        "",
    ]


def _simulate_incremental_sse_consumption(all_lines: list[str]) -> list[SSEEvent]:
    """Simulate the fixed consume loop: buffer, parse on blank, clear.

    This replicates the exact logic from _consume_response_lines after the fix:
    accumulate lines, parse on blank line, clear buffer after dispatch.

    Args:
        all_lines: All SSE lines arriving one-by-one (including blank delimiters).

    Returns:
        All events dispatched, in order.
    """
    dispatched: list[SSEEvent] = []
    buffer: list[str] = []

    for line in all_lines:
        buffer.append(line)
        if not line:
            events = parse_sse_events(buffer)
            for event in events:
                dispatched.append(event)
            buffer = []

    return dispatched


class TestSSEConsumeNoDuplicates:
    """Verify the fixed SSE consumer produces exactly the right events."""

    def test_single_chunk_then_done(self) -> None:
        """Single chunk + done: exactly 2 events, no duplicates."""
        lines = _make_chunk_lines(1, 1, "Hello") + _make_done_lines(1, 2)
        events = _simulate_incremental_sse_consumption(lines)

        assert len(events) == 2
        assert events[0].event == "chunk"
        assert events[0].data["chunk"] == "Hello"
        assert events[1].event == "done"

    def test_multiple_chunks_no_duplicates(self) -> None:
        """Five chunks + done: exactly 6 events, each chunk seen once."""
        chunks = ["Hello", " ", "world", "!", " Done."]
        lines: list[str] = []
        for i, text in enumerate(chunks):
            lines.extend(_make_chunk_lines(1, i + 1, text))
        lines.extend(_make_done_lines(1, len(chunks) + 1))

        events = _simulate_incremental_sse_consumption(lines)

        assert len(events) == 6
        chunk_texts = [
            str(e.data["chunk"]) for e in events if e.event == "chunk"
        ]
        assert chunk_texts == chunks
        assert events[-1].event == "done"

    def test_tool_call_interleaved_with_chunks(self) -> None:
        """tool_call events interleaved with chunks are not duplicated."""
        lines: list[str] = []
        lines.extend(_make_chunk_lines(1, 1, "Let me check..."))
        lines.extend(_make_tool_call_lines(1, "bash"))
        lines.extend(_make_chunk_lines(1, 2, "Here are the results."))
        lines.extend(_make_done_lines(1, 3))

        events = _simulate_incremental_sse_consumption(lines)

        assert len(events) == 4
        assert events[0].event == "chunk"
        assert events[0].data["chunk"] == "Let me check..."
        assert events[1].event == "tool_call"
        assert events[1].data["tool_name"] == "bash"
        assert events[2].event == "chunk"
        assert events[2].data["chunk"] == "Here are the results."
        assert events[3].event == "done"

    def test_error_event_not_duplicated(self) -> None:
        """Error event after chunks is dispatched exactly once."""
        lines: list[str] = []
        lines.extend(_make_chunk_lines(1, 1, "Starting..."))
        lines.extend(_make_error_lines(1, "Model overloaded"))

        events = _simulate_incremental_sse_consumption(lines)

        assert len(events) == 2
        assert events[0].event == "chunk"
        assert events[1].event == "error"
        assert events[1].data["error"] == "Model overloaded"

    def test_large_stream_no_duplicates(self) -> None:
        """100 chunks: each dispatched exactly once, correct order."""
        lines: list[str] = []
        expected_texts: list[str] = []
        for i in range(100):
            text = f"chunk_{i}"
            expected_texts.append(text)
            lines.extend(_make_chunk_lines(1, i + 1, text))
        lines.extend(_make_done_lines(1, 101))

        events = _simulate_incremental_sse_consumption(lines)

        assert len(events) == 101  # 100 chunks + 1 done
        actual_texts = [
            str(e.data["chunk"]) for e in events if e.event == "chunk"
        ]
        assert actual_texts == expected_texts

    def test_buggy_skip_counter_would_drop_events(self) -> None:
        """Demonstrate the exact bug: skip counter + clear = dropped events.

        This test simulates the BROKEN behavior to prove the bug existed,
        then verifies the fixed behavior produces correct output.
        """

        def _simulate_buggy_consumption(all_lines: list[str]) -> list[SSEEvent]:
            """Simulate the BROKEN code with processed_event_count."""
            dispatched: list[SSEEvent] = []
            buffer: list[str] = []
            processed_event_count = 0

            for line in all_lines:
                buffer.append(line)
                if not line:
                    events = parse_sse_events(buffer)
                    new_events = events[processed_event_count:]
                    processed_event_count = len(events)
                    for event in new_events:
                        dispatched.append(event)
                    buffer = []

            return dispatched

        lines = (
            _make_chunk_lines(1, 1, "Hello")
            + _make_chunk_lines(1, 2, " world")
            + _make_done_lines(1, 3)
        )

        # Buggy version: drops events after the first batch
        buggy_events = _simulate_buggy_consumption(lines)
        # The buggy code only sees the first event because:
        # After batch 1: processed_event_count=1, buffer cleared
        # Batch 2: 1 event parsed, events[1:] = [], skipped!
        # Batch 3: 1 event parsed, events[1:] = [], skipped!
        assert len(buggy_events) == 1  # Only first chunk survives
        assert buggy_events[0].data["chunk"] == "Hello"

        # Fixed version: all events dispatched
        fixed_events = _simulate_incremental_sse_consumption(lines)
        assert len(fixed_events) == 3
        assert fixed_events[0].data["chunk"] == "Hello"
        assert fixed_events[1].data["chunk"] == " world"
        assert fixed_events[2].event == "done"

    def test_empty_lines_between_events_handled(self) -> None:
        """Extra blank lines between events don't create phantom events."""
        lines = [
            "event: chunk",
            'data: {"turn": 1, "seq": 1, "chunk": "A"}',
            "",
            "",  # extra blank line (would be treated as a second parse trigger)
            "event: chunk",
            'data: {"turn": 1, "seq": 2, "chunk": "B"}',
            "",
        ]

        events = _simulate_incremental_sse_consumption(lines)

        # Should get exactly 2 chunk events, not 3 or more
        chunk_events = [e for e in events if e.event == "chunk"]
        assert len(chunk_events) == 2
        assert chunk_events[0].data["chunk"] == "A"
        assert chunk_events[1].data["chunk"] == "B"

    def test_no_trailing_blank_line_still_parses(self) -> None:
        """parse_sse_events handles events without trailing blank line.

        This covers the case where the stream ends abruptly without a
        final blank line delimiter. The parse function has a fallback
        for this (lines 117-123 in talk_streaming.py).
        """
        # Simulate: last event has no trailing blank line
        # In the consume loop, this means the event stays in buffer
        # and is never dispatched. This is expected SSE behavior --
        # incomplete events are not dispatched.
        lines = [
            "event: chunk",
            'data: {"turn": 1, "seq": 1, "chunk": "complete"}',
            "",
            "event: chunk",
            'data: {"turn": 1, "seq": 2, "chunk": "incomplete"}',
            # No trailing blank line
        ]

        events = _simulate_incremental_sse_consumption(lines)

        # Only the first event (with blank line delimiter) is dispatched
        assert len(events) == 1
        assert events[0].data["chunk"] == "complete"
