"""E2E tests for gap detection infrastructure (Issue #257).

Tests SSE gap_detected event format, gap event broadcast structure,
and gap detect→fill pipeline integration.
"""

import json
from unittest.mock import MagicMock

import pytest


class TestSSEGapDetectedFormat:
    """Verify SSE gap_detected event format matches FlowSurface consumer expectations."""

    def test_sse_gap_detected_event_structure(self):
        """SSE message should have event: gap_detected, id:, and data: lines."""
        from opendeviationbar.sidecar import _broadcast_gap_event

        # Mock SSE client queue
        client_queue = MagicMock()
        client_queue.put_nowait = MagicMock()

        from opendeviationbar.sidecar import _sse_clients, _sse_clients_lock

        with _sse_clients_lock:
            _sse_clients.append(client_queue)

        try:
            event = {
                "symbol": "BTCUSDT",
                "expected_tid": 1001,
                "actual_tid": 1010,
                "gap_size": 9,
                "detected_at_ms": 1700000000000,
                "filled_inline": False,
            }
            _broadcast_gap_event(event)

            # Verify put_nowait was called with correct SSE format
            assert client_queue.put_nowait.called
            message = client_queue.put_nowait.call_args[0][0]

            # Parse SSE format
            lines = message.strip().split("\n")
            assert lines[0] == "event: gap_detected"
            assert lines[1].startswith("id: gap:BTCUSDT:")
            assert lines[2].startswith("data: ")

            # Verify data is valid JSON
            data = json.loads(lines[2][len("data: ") :])
            assert data["symbol"] == "BTCUSDT"
            assert data["expected_tid"] == 1001
            assert data["actual_tid"] == 1010
            assert data["gap_size"] == 9
            assert data["filled_inline"] is False
        finally:
            with _sse_clients_lock:
                _sse_clients.remove(client_queue)

    def test_sse_gap_detected_event_id_uniqueness(self):
        """Event IDs should be unique per gap event (symbol + timestamp)."""
        from opendeviationbar.sidecar import _broadcast_gap_event

        client_queue = MagicMock()
        from opendeviationbar.sidecar import _sse_clients, _sse_clients_lock

        with _sse_clients_lock:
            _sse_clients.append(client_queue)

        try:
            event1 = {
                "symbol": "BTCUSDT",
                "detected_at_ms": 1700000000000,
                "gap_size": 5,
            }
            event2 = {
                "symbol": "ETHUSDT",
                "detected_at_ms": 1700000000000,
                "gap_size": 3,
            }
            _broadcast_gap_event(event1)
            _broadcast_gap_event(event2)

            calls = client_queue.put_nowait.call_args_list
            msg1 = calls[0][0][0]
            msg2 = calls[1][0][0]

            id1 = next(line for line in msg1.split("\n") if line.startswith("id: "))
            id2 = next(line for line in msg2.split("\n") if line.startswith("id: "))

            # IDs should differ (different symbols)
            assert id1 != id2
            assert "BTCUSDT" in id1
            assert "ETHUSDT" in id2
        finally:
            with _sse_clients_lock:
                _sse_clients.remove(client_queue)


class TestGapDetectFillPipeline:
    """Test gap detection → event emission → fill pipeline using PyO3 bindings."""

    def test_gap_event_watcher_extraction(self):
        """GapEventWatcher can be extracted from StreamManager before start()."""
        from opendeviationbar._core import StreamManager

        sm = StreamManager(["BTCUSDT"], [250])
        watcher = sm.take_gap_event_watcher()

        assert watcher is not None

        # Second call returns None (already taken)
        watcher2 = sm.take_gap_event_watcher()
        assert watcher2 is None

    def test_gap_event_watcher_no_event_without_gap(self):
        """next_event(0) returns None when no gap has occurred."""
        from opendeviationbar._core import StreamManager

        sm = StreamManager(["BTCUSDT"], [250])
        watcher = sm.take_gap_event_watcher()

        # Non-blocking poll should return None immediately
        event = watcher.next_event(0)
        assert event is None

    def test_fill_gap_before_start_returns_error(self):
        """fill_gap() before start() should raise RuntimeError."""
        from opendeviationbar._core import StreamManager

        sm = StreamManager(["BTCUSDT"], [250])

        with pytest.raises(RuntimeError, match="[Nn]ot started|[Mm]anager"):
            sm.fill_gap("BTCUSDT", 100, 200)
