"""Tests for the generic Paginator class in lib/pagination.py.

Exercises to_list, to_array, first, first_page, __anext__, max_items,
transform, and reset -- all paths uncovered by the EventsPaginator tests.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any

import pytest

from timeback_caliper.lib.pagination import PageResult, Paginator


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


class MockPaginatedTransport:
    def __init__(self, pages: list[FakePaginatedResponse]) -> None:
        self._pages = pages
        self._call_idx = 0
        self.requests: list[dict[str, Any]] = []

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


class TestPaginatorToList:
    @pytest.mark.asyncio
    async def test_single_page(self) -> None:
        transport = MockPaginatedTransport(
            [FakePaginatedResponse(data=[1, 2, 3], has_more=False, total=3)]
        )
        p = Paginator(transport, "/items", limit=10)
        result = await p.to_list()
        assert result == [1, 2, 3]

    @pytest.mark.asyncio
    async def test_multi_page(self) -> None:
        transport = MockPaginatedTransport(
            [
                FakePaginatedResponse(data=[1, 2], has_more=True, total=4),
                FakePaginatedResponse(data=[3, 4], has_more=False, total=4),
            ]
        )
        p = Paginator(transport, "/items", limit=2)
        result = await p.to_list()
        assert result == [1, 2, 3, 4]


class TestPaginatorToArray:
    @pytest.mark.asyncio
    async def test_raises_on_exceeded_max(self) -> None:
        transport = MockPaginatedTransport(
            [FakePaginatedResponse(data=list(range(20)), has_more=False, total=20)]
        )
        p = Paginator(transport, "/items", limit=100)
        with pytest.raises(RuntimeError, match=r"to_array.*exceeded"):
            await p.to_array(max_items=5)

    @pytest.mark.asyncio
    async def test_succeeds_within_max(self) -> None:
        transport = MockPaginatedTransport(
            [FakePaginatedResponse(data=[1, 2], has_more=False, total=2)]
        )
        p = Paginator(transport, "/items", limit=100)
        result = await p.to_array(max_items=10)
        assert result == [1, 2]


class TestPaginatorFirst:
    @pytest.mark.asyncio
    async def test_returns_first_item(self) -> None:
        transport = MockPaginatedTransport(
            [FakePaginatedResponse(data=["a", "b"], has_more=True, total=10)]
        )
        p = Paginator(transport, "/items", limit=10)
        item = await p.first()
        assert item == "a"
        assert transport.requests[0]["params"]["limit"] == 1

    @pytest.mark.asyncio
    async def test_returns_none_when_empty(self) -> None:
        transport = MockPaginatedTransport(
            [FakePaginatedResponse(data=[], has_more=False, total=0)]
        )
        p = Paginator(transport, "/items", limit=10)
        item = await p.first()
        assert item is None


class TestPaginatorFirstPage:
    @pytest.mark.asyncio
    async def test_returns_page_result_with_metadata(self) -> None:
        transport = MockPaginatedTransport(
            [FakePaginatedResponse(data=["x", "y"], has_more=True, total=5)]
        )
        p = Paginator(transport, "/items", limit=2)
        page = await p.first_page()
        assert isinstance(page, PageResult)
        assert page.data == ["x", "y"]
        assert page.has_more is True
        assert page.total == 5
        assert page.next_offset == 2

    @pytest.mark.asyncio
    async def test_no_more_when_exhausted(self) -> None:
        transport = MockPaginatedTransport(
            [FakePaginatedResponse(data=["z"], has_more=False, total=1)]
        )
        p = Paginator(transport, "/items", limit=10)
        page = await p.first_page()
        assert page.has_more is False
        assert page.next_offset is None


class TestPaginatorMaxItems:
    @pytest.mark.asyncio
    async def test_stops_at_max_items(self) -> None:
        transport = MockPaginatedTransport(
            [
                FakePaginatedResponse(data=[1, 2, 3], has_more=True, total=10),
                FakePaginatedResponse(data=[4, 5, 6], has_more=True, total=10),
            ]
        )
        p = Paginator(transport, "/items", limit=3, max_items=4)
        result = await p.to_list()
        assert len(result) == 4


class TestPaginatorTransform:
    @pytest.mark.asyncio
    async def test_applies_transform(self) -> None:
        transport = MockPaginatedTransport(
            [FakePaginatedResponse(data=[1, 2, 3], has_more=False, total=3)]
        )
        p: Paginator[str] = Paginator(
            transport, "/items", limit=10, transform=lambda x: f"item-{x}"
        )
        result = await p.to_list()
        assert result == ["item-1", "item-2", "item-3"]

    @pytest.mark.asyncio
    async def test_transform_applied_to_first(self) -> None:
        transport = MockPaginatedTransport(
            [FakePaginatedResponse(data=[42], has_more=False, total=1)]
        )
        p: Paginator[str] = Paginator(transport, "/items", limit=10, transform=lambda x: f"v{x}")
        item = await p.first()
        assert item == "v42"


class TestPaginatorReset:
    @pytest.mark.asyncio
    async def test_reset_allows_re_iteration(self) -> None:
        transport = MockPaginatedTransport(
            [
                FakePaginatedResponse(data=[1, 2], has_more=False, total=2),
                FakePaginatedResponse(data=[1, 2], has_more=False, total=2),
            ]
        )
        p = Paginator(transport, "/items", limit=10)
        first_pass = await p.to_list()
        assert first_pass == [1, 2]

        p.reset()
        second_pass = await p.to_list()
        assert second_pass == [1, 2]
        assert len(transport.requests) == 2


class TestPaginatorTotal:
    @pytest.mark.asyncio
    async def test_total_property_populated_after_fetch(self) -> None:
        transport = MockPaginatedTransport(
            [FakePaginatedResponse(data=[1], has_more=False, total=42)]
        )
        p = Paginator(transport, "/items", limit=10)
        assert p.total is None
        await p.to_list()
        assert p.total == 42
