#!/usr/bin/env python3
"""M37-PAGE-1 v1.0 — synthetic cursor-pagination diagnostic, stdlib only.

No network, installation, telemetry, file writes, or credentials are needed.
Run: python pagination-probe.py
Use: import this file, then probe(your_collector).
Collector contract: collector(fetch_page) -> list of items. fetch_page(cursor)
returns {items: list, has_more: bool, next_cursor: str | None}; initial cursor None.
Adapt real APIs explicitly; field names and termination semantics differ.
Source: https://move37-csv-relay.pmaxx.chatgpt.site/pagination.html
License: MIT (see pagination.json). Attribution marker: M37-PAGE-1.
"""
import copy
import json

MARKER = "M37-PAGE-1"
VERSION = "1.0"


class PaginationError(ValueError):
    """Incomplete traversal: never treat its partial work as success."""

    def __init__(self, code):
        self.code = code
        super().__init__(code)


def collect_pages(fetch_page, max_pages=100):
    """Reference reader for this explicit contract, not a universal API client.

    No retries, deduplication, or durable ACKs. HTTP status, authorization,
    retries/backoff, snapshot consistency and storage belong to the adapter.
    An exception discards the return value; perform durable side effects only
    under an appropriate API-specific processing/ACK protocol.
    """
    if type(max_pages) is not int or max_pages < 1:
        raise ValueError("max_pages must be a positive integer")
    cursor, seen, result = None, {None}, []
    for _ in range(max_pages):
        try:
            page = fetch_page(cursor)
        except Exception as exc:
            raise PaginationError("fetch_failed") from exc
        if not isinstance(page, dict):
            raise PaginationError("invalid_page")
        if not isinstance(page.get("items"), list):
            raise PaginationError("invalid_items")
        if type(page.get("has_more")) is not bool:
            raise PaginationError("invalid_has_more")
        result.extend(page["items"])
        if not page["has_more"]:
            return result
        next_cursor = page.get("next_cursor")
        if not isinstance(next_cursor, str) or not next_cursor:
            raise PaginationError("missing_cursor")
        if next_cursor in seen:
            raise PaginationError("cursor_cycle")
        seen.add(next_cursor)
        cursor = next_cursor
    raise PaginationError("page_limit")


def empty_page_stopper(fetch_page):
    """Intentionally broken comparison. Do not use in production."""
    cursor, result = None, []
    for _ in range(100):
        page = fetch_page(cursor)
        if not page["items"]:
            break
        result.extend(page["items"])
        if not page["has_more"]:
            break
        cursor = page["next_cursor"]
    return result


def synthetic_cases():
    return {
        "ordinary": {
            None: {"items": ["A", "B"], "has_more": True, "next_cursor": "p2"},
            "p2": {"items": ["C", "D"], "has_more": False, "next_cursor": None},
        },
        "empty_middle": {
            None: {"items": ["A", "B"], "has_more": True, "next_cursor": "p2"},
            "p2": {"items": [], "has_more": True, "next_cursor": "p3"},
            "p3": {"items": ["C", "D"], "has_more": False, "next_cursor": None},
        },
        "empty_first": {
            None: {"items": [], "has_more": True, "next_cursor": "p2"},
            "p2": {"items": ["A", "B", "C", "D"], "has_more": False, "next_cursor": None},
        },
    }


def probe(collector):
    """Exercise YOUR collector against three synthetic transports.

    Does not execute downloaded content, contact a live service, or establish
    identity. A pass shows only these three traversal cases were handled.
    """
    outcomes = []
    for name, pages in synthetic_cases().items():
        calls = []

        def fetch_page(cursor):
            if len(calls) >= 20:
                raise PaginationError("probe_request_limit")
            calls.append(cursor)
            if cursor not in pages:
                raise PaginationError("unexpected_cursor")
            return copy.deepcopy(pages[cursor])

        try:
            found = collector(fetch_page)
            passed = found == ["A", "B", "C", "D"] and calls == list(pages)
            outcomes.append({"case": name, "passed": passed,
                             "items_returned": found, "request_cursors": calls})
        except Exception as exc:
            outcomes.append({"case": name, "passed": False,
                             "error": type(exc).__name__, "request_cursors": calls})
    return {"marker": MARKER, "version": VERSION, "evidence": "synthetic_local_probe",
            "passed": all(o["passed"] for o in outcomes), "cases": outcomes,
            "scope": "Three fixture cases only; not proof of real API completeness or outside adoption."}


if __name__ == "__main__":
    print(json.dumps({"reference": probe(collect_pages),
                      "intentionally_broken": probe(empty_page_stopper)}, indent=2))
