"""Tests for Caliper events pagination via the generic Paginator.

Tests pagination logic in isolation using a mock transport that implements
``request_paginated()``, exercising the same code path as ``EventsResource.stream()``.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any

import pytest

from timeback_caliper.lib.pagination import Paginator
from timeback_caliper.types.api import StoredEvent


@dataclass
class _FakePage:
    data: list[Any]
    has_more: bool
    total: int | None = None


class MockPaginatedTransport:
    """Mock transport returning pre-defined paginated responses."""

    def __init__(self, pages: list[_FakePage]) -> None:
        self._pages = pages
        self._call_count = 0
        self.requests: list[dict[str, Any]] = []

    async def request_paginated(
        self, path: str, *, params: dict[str, Any] | None = None
    ) -> _FakePage:
        self.requests.append({"path": path, "params": params or {}})
        if self._call_count >= len(self._pages):
            return _FakePage(data=[], has_more=False, total=0)
        page = self._pages[self._call_count]
        self._call_count += 1
        return page

    @property
    def call_count(self) -> int:
        return self._call_count


def make_event_dict(id: int) -> dict[str, Any]:
    return {
        "id": id,
        "externalId": f"urn:uuid:event-{id}",
        "sensor": "https://example.edu/sensors/1",
        "type": "ActivityEvent",
        "action": "Completed",
        "eventTime": "2024-01-15T10:00:00Z",
        "sendTime": "2024-01-15T10:00:01Z",
        "created_at": "2024-01-15T10:00:02Z",
    }


def _to_stored_event(raw: Any) -> StoredEvent:
    return StoredEvent(**raw)


class TestPaginatorOffsetIncrement:
    @pytest.mark.asyncio
    async def test_offset_increments_by_returned_items(self) -> None:
        transport = MockPaginatedTransport(
            [
                _FakePage(
                    data=[make_event_dict(1), make_event_dict(2), make_event_dict(3)],
                    has_more=True,
                    total=5,
                ),
                _FakePage(data=[make_event_dict(4), make_event_dict(5)], has_more=False, total=5),
            ]
        )
        paginator: Paginator[StoredEvent] = Paginator(
            transport, "/events", limit=10, transform=_to_stored_event
        )
        events = await paginator.to_list()

        assert len(events) == 5
        assert transport.call_count == 2
        assert transport.requests[0]["params"]["offset"] == 0
        assert transport.requests[1]["params"]["offset"] == 3


class TestPaginatorExhaustion:
    @pytest.mark.asyncio
    async def test_exhausts_when_has_more_false(self) -> None:
        transport = MockPaginatedTransport(
            [_FakePage(data=[make_event_dict(1), make_event_dict(2)], has_more=False, total=2)]
        )
        paginator: Paginator[StoredEvent] = Paginator(
            transport, "/events", limit=10, transform=_to_stored_event
        )
        events = await paginator.to_list()

        assert len(events) == 2
        assert transport.call_count == 1

    @pytest.mark.asyncio
    async def test_exhausts_on_empty_response(self) -> None:
        transport = MockPaginatedTransport(
            [
                _FakePage(data=[make_event_dict(1)], has_more=True, total=10),
                _FakePage(data=[], has_more=False, total=10),
            ]
        )
        paginator: Paginator[StoredEvent] = Paginator(
            transport, "/events", limit=10, transform=_to_stored_event
        )
        events = await paginator.to_list()

        assert len(events) == 1
        assert transport.call_count == 2


class TestPaginatorMaxItems:
    @pytest.mark.asyncio
    async def test_respects_max_items_limit(self) -> None:
        transport = MockPaginatedTransport(
            [_FakePage(data=[make_event_dict(i) for i in range(1, 6)], has_more=True, total=100)]
        )
        paginator: Paginator[StoredEvent] = Paginator(
            transport, "/events", limit=10, max_items=3, transform=_to_stored_event
        )
        events = await paginator.to_list()
        assert len(events) == 3

    @pytest.mark.asyncio
    async def test_max_items_across_pages(self) -> None:
        transport = MockPaginatedTransport(
            [
                _FakePage(data=[make_event_dict(1), make_event_dict(2)], has_more=True, total=10),
                _FakePage(data=[make_event_dict(3), make_event_dict(4)], has_more=True, total=10),
                _FakePage(data=[make_event_dict(5), make_event_dict(6)], has_more=True, total=10),
            ]
        )
        paginator: Paginator[StoredEvent] = Paginator(
            transport, "/events", limit=2, max_items=5, transform=_to_stored_event
        )
        events = await paginator.to_list()
        assert len(events) == 5
        assert transport.call_count == 3


class TestPaginatorParams:
    @pytest.mark.asyncio
    async def test_passes_filter_params(self) -> None:
        transport = MockPaginatedTransport([_FakePage(data=[], has_more=False, total=0)])
        params = {"startDate": "2024-01-01", "sensor": "https://example.edu/sensors/1"}
        paginator: Paginator[StoredEvent] = Paginator(
            transport, "/events", params=params, limit=50, transform=_to_stored_event
        )
        await paginator.to_list()

        request_params = transport.requests[0]["params"]
        assert request_params["startDate"] == "2024-01-01"
        assert request_params["sensor"] == "https://example.edu/sensors/1"
        assert request_params["limit"] == 50
        assert request_params["offset"] == 0


class TestStoredEventParsing:
    @pytest.mark.asyncio
    async def test_parses_stored_events(self) -> None:
        transport = MockPaginatedTransport(
            [_FakePage(data=[make_event_dict(1)], has_more=False, total=1)]
        )
        paginator: Paginator[StoredEvent] = Paginator(
            transport, "/events", limit=10, transform=_to_stored_event
        )
        events = await paginator.to_list()

        assert len(events) == 1
        assert isinstance(events[0], StoredEvent)
        assert events[0].id == 1
        assert events[0].external_id == "urn:uuid:event-1"


class TestPaginatorToArray:
    @pytest.mark.asyncio
    async def test_to_array_raises_when_exceeding_max_items(self) -> None:
        transport = MockPaginatedTransport(
            [
                _FakePage(data=[make_event_dict(i) for i in range(1, 6)], has_more=True, total=10),
                _FakePage(
                    data=[make_event_dict(i) for i in range(6, 11)], has_more=False, total=10
                ),
            ]
        )
        paginator: Paginator[StoredEvent] = Paginator(
            transport, "/events", limit=5, transform=_to_stored_event
        )
        with pytest.raises(RuntimeError, match=r"to_array.*exceeded"):
            await paginator.to_array(max_items=3)

    @pytest.mark.asyncio
    async def test_to_array_succeeds_within_limit(self) -> None:
        transport = MockPaginatedTransport(
            [_FakePage(data=[make_event_dict(1), make_event_dict(2)], has_more=False, total=2)]
        )
        paginator: Paginator[StoredEvent] = Paginator(
            transport, "/events", limit=10, transform=_to_stored_event
        )
        events = await paginator.to_array(max_items=10)
        assert len(events) == 2


class TestPaginatorFirst:
    @pytest.mark.asyncio
    async def test_first_returns_single_event(self) -> None:
        transport = MockPaginatedTransport(
            [_FakePage(data=[make_event_dict(42)], has_more=True, total=100)]
        )
        paginator: Paginator[StoredEvent] = Paginator(
            transport, "/events", limit=100, transform=_to_stored_event
        )
        event = await paginator.first()
        assert event is not None
        assert event.id == 42
        assert transport.requests[0]["params"]["limit"] == 1

    @pytest.mark.asyncio
    async def test_first_returns_none_when_empty(self) -> None:
        transport = MockPaginatedTransport([_FakePage(data=[], has_more=False, total=0)])
        paginator: Paginator[StoredEvent] = Paginator(
            transport, "/events", limit=100, transform=_to_stored_event
        )
        event = await paginator.first()
        assert event is None


class TestPaginatorFirstPage:
    @pytest.mark.asyncio
    async def test_first_page_returns_page_result(self) -> None:
        transport = MockPaginatedTransport(
            [_FakePage(data=[make_event_dict(1), make_event_dict(2)], has_more=True, total=5)]
        )
        paginator: Paginator[StoredEvent] = Paginator(
            transport, "/events", limit=2, transform=_to_stored_event
        )
        page = await paginator.first_page()
        assert len(page.data) == 2
        assert page.has_more is True
        assert page.total == 5
        assert page.next_offset == 2

    @pytest.mark.asyncio
    async def test_first_page_no_more_when_all_fit(self) -> None:
        transport = MockPaginatedTransport(
            [_FakePage(data=[make_event_dict(1)], has_more=False, total=1)]
        )
        paginator: Paginator[StoredEvent] = Paginator(
            transport, "/events", limit=10, transform=_to_stored_event
        )
        page = await paginator.first_page()
        assert page.has_more is False
        assert page.next_offset is None
