"""
Deep behavioral tests for pagination patterns.
Tests offset pagination, cursor pagination, page calculation,
and result windowing logic.
"""

import base64
import json


# --- Offset pagination patterns ---


class TestOffsetPagination:
    """Tests for offset-based pagination."""

    def _calculate_offset(self, page, page_size):
        """Calculate offset for page."""
        return (max(1, page) - 1) * page_size

    def _calculate_page_from_offset(self, offset, page_size):
        """Calculate page number from offset."""
        if page_size <= 0:
            return 1
        return (offset // page_size) + 1

    def test_first_page_offset(self):
        assert self._calculate_offset(1, 10) == 0

    def test_second_page_offset(self):
        assert self._calculate_offset(2, 10) == 10

    def test_zero_page_treated_as_first(self):
        assert self._calculate_offset(0, 10) == 0

    def test_negative_page_treated_as_first(self):
        assert self._calculate_offset(-1, 10) == 0

    def test_page_from_offset(self):
        assert self._calculate_page_from_offset(0, 10) == 1
        assert self._calculate_page_from_offset(10, 10) == 2
        assert self._calculate_page_from_offset(25, 10) == 3


class TestPageSizeValidation:
    """Tests for page size validation."""

    def _validate_page_size(
        self, page_size, min_size=1, max_size=100, default=20
    ):
        """Validate and normalize page size."""
        if page_size is None:
            return default
        if page_size < min_size:
            return min_size
        if page_size > max_size:
            return max_size
        return page_size

    def test_valid_size(self):
        assert self._validate_page_size(50) == 50

    def test_none_uses_default(self):
        assert self._validate_page_size(None) == 20

    def test_too_small_uses_min(self):
        assert self._validate_page_size(0) == 1

    def test_too_large_uses_max(self):
        assert self._validate_page_size(500) == 100


# --- Total pages calculation patterns ---


class TestTotalPagesCalculation:
    """Tests for total pages calculation."""

    def _calculate_total_pages(self, total_items, page_size):
        """Calculate total number of pages."""
        if page_size <= 0:
            return 0
        return (total_items + page_size - 1) // page_size

    def _has_next_page(self, current_page, total_pages):
        """Check if there is a next page."""
        return current_page < total_pages

    def _has_previous_page(self, current_page):
        """Check if there is a previous page."""
        return current_page > 1

    def test_exact_division(self):
        assert self._calculate_total_pages(100, 10) == 10

    def test_with_remainder(self):
        assert self._calculate_total_pages(101, 10) == 11

    def test_zero_items(self):
        assert self._calculate_total_pages(0, 10) == 0

    def test_has_next_page(self):
        assert self._has_next_page(1, 5) is True
        assert self._has_next_page(5, 5) is False

    def test_has_previous_page(self):
        assert self._has_previous_page(1) is False
        assert self._has_previous_page(2) is True


# --- Page range calculation patterns ---


class TestPageRangeCalculation:
    """Tests for calculating visible page ranges."""

    def _calculate_page_range(self, current_page, total_pages, window_size=5):
        """Calculate range of pages to display."""
        half_window = window_size // 2
        start = max(1, current_page - half_window)
        end = min(total_pages, start + window_size - 1)
        # Adjust start if end is at max
        start = max(1, end - window_size + 1)
        return list(range(start, end + 1))

    def test_middle_page(self):
        pages = self._calculate_page_range(5, 10, window_size=5)
        assert pages == [3, 4, 5, 6, 7]

    def test_first_page(self):
        pages = self._calculate_page_range(1, 10, window_size=5)
        assert pages == [1, 2, 3, 4, 5]

    def test_last_page(self):
        pages = self._calculate_page_range(10, 10, window_size=5)
        assert pages == [6, 7, 8, 9, 10]

    def test_few_pages(self):
        pages = self._calculate_page_range(2, 3, window_size=5)
        assert pages == [1, 2, 3]


# --- Pagination result building patterns ---


class TestPaginationResultBuilding:
    """Tests for building pagination result objects."""

    def _build_pagination_info(self, items, page, page_size, total_count):
        """Build pagination info object."""
        total_pages = (
            (total_count + page_size - 1) // page_size if page_size > 0 else 0
        )
        return {
            "items": items,
            "pagination": {
                "page": page,
                "page_size": page_size,
                "total_items": total_count,
                "total_pages": total_pages,
                "has_next": page < total_pages,
                "has_previous": page > 1,
            },
        }

    def test_includes_items(self):
        result = self._build_pagination_info([1, 2, 3], 1, 10, 100)
        assert result["items"] == [1, 2, 3]

    def test_includes_page_info(self):
        result = self._build_pagination_info([], 2, 10, 100)
        assert result["pagination"]["page"] == 2
        assert result["pagination"]["page_size"] == 10

    def test_includes_total_info(self):
        result = self._build_pagination_info([], 1, 10, 100)
        assert result["pagination"]["total_items"] == 100
        assert result["pagination"]["total_pages"] == 10

    def test_includes_navigation_flags(self):
        result = self._build_pagination_info([], 5, 10, 100)
        assert result["pagination"]["has_next"] is True
        assert result["pagination"]["has_previous"] is True


# --- Cursor pagination patterns ---


class TestCursorPagination:
    """Tests for cursor-based pagination."""

    def _encode_cursor(self, data):
        """Encode data into cursor string."""
        json_str = json.dumps(data, default=str)
        return base64.b64encode(json_str.encode()).decode()

    def _decode_cursor(self, cursor):
        """Decode cursor string into data."""
        try:
            json_str = base64.b64decode(cursor.encode()).decode()
            return json.loads(json_str)
        except Exception:
            return None

    def _build_cursor_response(self, items, cursor_field, has_more=True):
        """Build cursor-based pagination response."""
        if not items:
            return {"items": [], "next_cursor": None, "has_more": False}
        last_item = items[-1]
        cursor_value = last_item.get(cursor_field)
        cursor = self._encode_cursor(
            {"v": cursor_value, "id": last_item.get("id")}
        )
        return {
            "items": items,
            "next_cursor": cursor if has_more else None,
            "has_more": has_more,
        }

    def test_encode_decode_roundtrip(self):
        original = {"v": "2025-01-01", "id": 123}
        cursor = self._encode_cursor(original)
        decoded = self._decode_cursor(cursor)
        assert decoded == original

    def test_cursor_response_with_items(self):
        items = [
            {"id": 1, "created_at": "2025-01-01"},
            {"id": 2, "created_at": "2025-01-02"},
        ]
        response = self._build_cursor_response(items, "created_at")
        assert response["next_cursor"] is not None
        assert response["has_more"] is True

    def test_cursor_response_empty(self):
        response = self._build_cursor_response([], "created_at")
        assert response["next_cursor"] is None
        assert response["has_more"] is False


class TestCursorComparison:
    """Tests for cursor comparison logic."""

    def _compare_by_cursor(
        self, item, cursor_data, sort_field, direction="asc"
    ):
        """Check if item comes after cursor position."""
        item_value = item.get(sort_field)
        cursor_value = cursor_data.get("v")
        if direction == "asc":
            return item_value > cursor_value
        return item_value < cursor_value

    def test_after_cursor_asc(self):
        item = {"created_at": "2025-01-02"}
        cursor = {"v": "2025-01-01"}
        assert (
            self._compare_by_cursor(item, cursor, "created_at", "asc") is True
        )

    def test_before_cursor_asc(self):
        item = {"created_at": "2025-01-01"}
        cursor = {"v": "2025-01-02"}
        assert (
            self._compare_by_cursor(item, cursor, "created_at", "asc") is False
        )

    def test_after_cursor_desc(self):
        item = {"created_at": "2025-01-01"}
        cursor = {"v": "2025-01-02"}
        assert (
            self._compare_by_cursor(item, cursor, "created_at", "desc") is True
        )


# --- Keyset pagination patterns ---


class TestKeysetPagination:
    """Tests for keyset pagination patterns."""

    def _build_keyset_cursor(self, sort_values, item_id):
        """Build keyset cursor from multiple sort values."""
        return self._encode_keyset({"sorts": sort_values, "id": item_id})

    def _encode_keyset(self, data):
        """Encode keyset data."""
        json_str = json.dumps(data, default=str)
        return base64.b64encode(json_str.encode()).decode()

    def _decode_keyset(self, cursor):
        """Decode keyset cursor."""
        try:
            json_str = base64.b64decode(cursor.encode()).decode()
            return json.loads(json_str)
        except Exception:
            return None

    def test_keyset_with_multiple_sorts(self):
        cursor = self._build_keyset_cursor(
            {"created_at": "2025-01-01", "score": 95}, "id123"
        )
        decoded = self._decode_keyset(cursor)
        assert decoded["sorts"]["created_at"] == "2025-01-01"
        assert decoded["sorts"]["score"] == 95
        assert decoded["id"] == "id123"


# --- Infinite scroll patterns ---


class TestInfiniteScrollPagination:
    """Tests for infinite scroll pagination patterns."""

    def _build_infinite_scroll_response(
        self, items, page_size, total_remaining=None
    ):
        """Build infinite scroll response."""
        return {
            "items": items,
            "load_more": len(items) >= page_size,
            "remaining": total_remaining,
        }

    def _should_preload_next(
        self, scroll_position, content_height, threshold=0.8
    ):
        """Check if next page should be preloaded."""
        if content_height <= 0:
            return False
        scroll_ratio = scroll_position / content_height
        return scroll_ratio >= threshold

    def test_full_page_has_load_more(self):
        response = self._build_infinite_scroll_response([1, 2, 3], page_size=3)
        assert response["load_more"] is True

    def test_partial_page_no_load_more(self):
        response = self._build_infinite_scroll_response([1, 2], page_size=3)
        assert response["load_more"] is False

    def test_preload_threshold_reached(self):
        assert self._should_preload_next(800, 1000, threshold=0.8) is True

    def test_preload_threshold_not_reached(self):
        assert self._should_preload_next(500, 1000, threshold=0.8) is False


# --- Page link building patterns ---


class TestPageLinkBuilding:
    """Tests for building pagination links."""

    def _build_page_link(self, base_url, page, page_size, **extra_params):
        """Build URL for a specific page."""
        params = {"page": page, "page_size": page_size}
        params.update(extra_params)
        param_str = "&".join(f"{k}={v}" for k, v in params.items())
        return f"{base_url}?{param_str}"

    def _build_pagination_links(
        self, base_url, current_page, total_pages, page_size
    ):
        """Build all pagination links."""
        links = {
            "self": self._build_page_link(base_url, current_page, page_size)
        }
        if current_page > 1:
            links["first"] = self._build_page_link(base_url, 1, page_size)
            links["prev"] = self._build_page_link(
                base_url, current_page - 1, page_size
            )
        if current_page < total_pages:
            links["next"] = self._build_page_link(
                base_url, current_page + 1, page_size
            )
            links["last"] = self._build_page_link(
                base_url, total_pages, page_size
            )
        return links

    def test_page_link_format(self):
        link = self._build_page_link("/api/items", 2, 10)
        assert "page=2" in link
        assert "page_size=10" in link

    def test_pagination_links_middle_page(self):
        links = self._build_pagination_links("/api/items", 5, 10, 10)
        assert "self" in links
        assert "first" in links
        assert "prev" in links
        assert "next" in links
        assert "last" in links

    def test_pagination_links_first_page(self):
        links = self._build_pagination_links("/api/items", 1, 10, 10)
        assert "prev" not in links
        assert "first" not in links
        assert "next" in links


# --- Result windowing patterns ---


class TestResultWindowing:
    """Tests for result windowing patterns."""

    def _window_results(self, results, start, count):
        """Get a window of results."""
        return results[start : start + count]

    def _sliding_window(self, results, window_size, overlap=0):
        """Generate sliding windows of results."""
        windows = []
        step = window_size - overlap
        for i in range(0, len(results), step):
            window = results[i : i + window_size]
            if window:
                windows.append(window)
        return windows

    def test_window_from_start(self):
        results = [1, 2, 3, 4, 5]
        window = self._window_results(results, 0, 3)
        assert window == [1, 2, 3]

    def test_window_from_middle(self):
        results = [1, 2, 3, 4, 5]
        window = self._window_results(results, 2, 2)
        assert window == [3, 4]

    def test_sliding_windows_no_overlap(self):
        results = list(range(10))
        windows = self._sliding_window(results, 3, overlap=0)
        assert len(windows) == 4  # [0,1,2], [3,4,5], [6,7,8], [9]

    def test_sliding_windows_with_overlap(self):
        results = list(range(6))
        windows = self._sliding_window(results, 3, overlap=1)
        # [0,1,2], [2,3,4], [4,5]
        assert len(windows) == 3
        assert windows[0] == [0, 1, 2]
        assert windows[1] == [2, 3, 4]


# --- Empty result handling patterns ---


class TestEmptyResultHandling:
    """Tests for handling empty paginated results."""

    def _build_empty_response(self, page, page_size, total=0):
        """Build response for empty results."""
        return {
            "items": [],
            "pagination": {
                "page": page,
                "page_size": page_size,
                "total_items": total,
                "total_pages": 0,
                "has_next": False,
                "has_previous": page > 1,
            },
        }

    def _is_beyond_range(self, page, total_pages):
        """Check if page is beyond available range."""
        return page > max(1, total_pages)

    def test_empty_response_structure(self):
        response = self._build_empty_response(1, 10)
        assert response["items"] == []
        assert response["pagination"]["total_pages"] == 0

    def test_beyond_range_check(self):
        assert self._is_beyond_range(5, 3) is True
        assert self._is_beyond_range(2, 3) is False
        assert (
            self._is_beyond_range(1, 0) is False
        )  # Page 1 is valid even with 0 pages


# --- Batch pagination patterns ---


class TestBatchPagination:
    """Tests for batch-based pagination patterns."""

    def _paginate_batch(
        self, batch, batch_number, items_per_batch, items_per_page
    ):
        """Get a specific page from a batch."""
        batch_start = batch_number * items_per_batch
        pages_per_batch = items_per_batch // items_per_page
        return {
            "batch": batch_number,
            "pages_in_batch": pages_per_batch,
            "first_item_index": batch_start,
        }

    def _calculate_batch_for_page(self, page, items_per_page, items_per_batch):
        """Calculate which batch contains a page."""
        item_index = (page - 1) * items_per_page
        return item_index // items_per_batch

    def test_batch_info(self):
        info = self._paginate_batch(None, 0, 100, 10)
        assert info["batch"] == 0
        assert info["pages_in_batch"] == 10
        assert info["first_item_index"] == 0

    def test_batch_for_page(self):
        batch = self._calculate_batch_for_page(1, 10, 100)
        assert batch == 0
        batch = self._calculate_batch_for_page(11, 10, 100)
        assert batch == 1


# --- Seek pagination patterns ---


class TestSeekPagination:
    """Tests for seek-based pagination patterns."""

    def _build_seek_condition(self, last_id, direction="forward"):
        """Build seek condition based on last seen ID."""
        if direction == "forward":
            return f"id > {last_id}"
        return f"id < {last_id}"

    def _extract_seek_id(self, items, position="last"):
        """Extract ID for seeking from items."""
        if not items:
            return None
        if position == "last":
            return items[-1].get("id")
        return items[0].get("id")

    def test_forward_seek(self):
        condition = self._build_seek_condition(100, "forward")
        assert condition == "id > 100"

    def test_backward_seek(self):
        condition = self._build_seek_condition(100, "backward")
        assert condition == "id < 100"

    def test_extract_last_id(self):
        items = [{"id": 1}, {"id": 2}, {"id": 3}]
        last_id = self._extract_seek_id(items, "last")
        assert last_id == 3

    def test_extract_first_id(self):
        items = [{"id": 1}, {"id": 2}, {"id": 3}]
        first_id = self._extract_seek_id(items, "first")
        assert first_id == 1
