"""Tests for the typed ReplayGap signal on Session.stream_events (CLI-15)."""

from __future__ import annotations

from collections.abc import AsyncIterator

import pytest

from zb_mom_ww_mxgateway import ReplayGap, Session
from zb_mom_ww_mxgateway.generated import mxaccess_gateway_pb2 as pb


class _FakeClient:
    """Minimal client stub exposing only what Session.stream_events needs."""

    def __init__(self, events: list[pb.MxEvent]) -> None:
        self._events = events
        self.last_request: pb.StreamEventsRequest | None = None

    def stream_events_raw(
        self,
        request: pb.StreamEventsRequest,
    ) -> AsyncIterator[pb.MxEvent]:
        self.last_request = request

        async def _gen() -> AsyncIterator[pb.MxEvent]:
            for event in self._events:
                yield event

        return _gen()


def _gap_sentinel(*, requested: int, oldest: int) -> pb.MxEvent:
    return pb.MxEvent(
        session_id="session-1",
        family=pb.MX_EVENT_FAMILY_UNSPECIFIED,
        replay_gap=pb.ReplayGap(
            requested_after_sequence=requested,
            oldest_available_sequence=oldest,
        ),
    )


def _normal_event(worker_sequence: int) -> pb.MxEvent:
    return pb.MxEvent(
        session_id="session-1",
        worker_sequence=worker_sequence,
        family=pb.MX_EVENT_FAMILY_ON_DATA_CHANGE,
    )


def _session(events: list[pb.MxEvent]) -> tuple[Session, _FakeClient]:
    client = _FakeClient(events)
    session = Session(client=client, session_id="session-1")  # type: ignore[arg-type]
    return session, client


@pytest.mark.asyncio
async def test_replay_gap_sentinel_surfaces_as_typed_signal() -> None:
    session, _client = _session(
        [_gap_sentinel(requested=5, oldest=10), _normal_event(10)],
    )

    items = [item async for item in session.stream_events(after_worker_sequence=5)]

    assert isinstance(items[0], ReplayGap)
    assert items[0].requested_after_sequence == 5
    assert items[0].oldest_available_sequence == 10
    # Resume cursor is oldest_available_sequence - 1 so the next replayed event
    # is the oldest the gateway still retains.
    assert items[0].resume_after_worker_sequence == 9

    # Normal events after the sentinel pass through unchanged as MxEvent.
    assert isinstance(items[1], pb.MxEvent)
    assert not items[1].HasField("replay_gap")
    assert items[1].worker_sequence == 10


@pytest.mark.asyncio
async def test_normal_events_are_unaffected() -> None:
    session, _client = _session([_normal_event(1), _normal_event(2)])

    items = [item async for item in session.stream_events()]

    assert all(isinstance(item, pb.MxEvent) for item in items)
    assert [item.worker_sequence for item in items] == [1, 2]
    assert not any(isinstance(item, ReplayGap) for item in items)


@pytest.mark.asyncio
async def test_stream_events_forwards_resume_cursor() -> None:
    session, client = _session([])

    async for _ in session.stream_events(after_worker_sequence=42):
        pass

    assert client.last_request is not None
    assert client.last_request.after_worker_sequence == 42
    assert client.last_request.session_id == "session-1"


def test_replay_gap_resume_cursor_never_negative() -> None:
    gap = ReplayGap.from_proto(
        pb.ReplayGap(requested_after_sequence=0, oldest_available_sequence=0),
    )
    assert gap.resume_after_worker_sequence == 0
