#!/usr/bin/env python3
"""Teaching fixture: explicit completeness before inventory-driven planning.
Run: python3 mcp-inventory-completeness.py
No network calls. This is not an MCP protocol implementation.
"""
from dataclasses import dataclass
import unittest


class IncompleteRead(Exception):
    pass


@dataclass(frozen=True)
class Inventory:
    ids: tuple[str, ...]
    pages: int
    snapshot: str


def read_inventory(fetch, *, max_pages=10):
    """fetch(cursor) returns adapter-normalized pages for ONE inventory scope.

    Adapter must attest complete=True only on a complete terminal page.
    snapshot must identify a stable enumeration, not just a request ID.
    Exceptions never return partial inventory as a successful result.
    """
    cursor = None
    seen_cursors = set()
    ids = []
    seen_ids = set()
    snapshot = None
    for page_number in range(1, max_pages + 1):
        try:
            page = fetch(cursor)
        except Exception as exc:
            raise IncompleteRead("page fetch failed") from exc
        if not isinstance(page, dict):
            raise IncompleteRead("invalid page")
        token = page.get("snapshot")
        if not isinstance(token, str) or not token:
            raise IncompleteRead("missing stable snapshot")
        if snapshot is None:
            snapshot = token
        if token != snapshot:
            raise IncompleteRead("snapshot changed")
        rows = page.get("ids")
        if not isinstance(rows, list):
            raise IncompleteRead("invalid ids")
        for item in rows:
            if not isinstance(item, str) or not item or item in seen_ids:
                raise IncompleteRead("invalid or duplicate id")
            seen_ids.add(item)
            ids.append(item)
        if "next" not in page or type(page.get("complete")) is not bool:
            raise IncompleteRead("missing pagination evidence")
        next_cursor = page["next"]
        if next_cursor is None:
            if page["complete"] is not True:
                raise IncompleteRead("terminal page is not complete")
            return Inventory(tuple(ids), page_number, snapshot)
        if (not isinstance(next_cursor, str) or not next_cursor
                or next_cursor in seen_cursors or page["complete"]):
            raise IncompleteRead("invalid pagination transition")
        seen_cursors.add(next_cursor)
        cursor = next_cursor
    raise IncompleteRead("page budget exhausted")


def page(ids, next_cursor=None, complete=True, snapshot="snapshot-a"):
    return dict(ids=ids, next=next_cursor, complete=complete, snapshot=snapshot)


class CompletenessTests(unittest.TestCase):
    def test_two_pages(self):
        pages = {None: page(["a"], "p2", False), "p2": page(["b"])}
        self.assertEqual(read_inventory(pages.__getitem__), Inventory(("a", "b"), 2, "snapshot-a"))

    def test_empty_inventory(self):
        self.assertEqual(read_inventory(lambda _: page([])).ids, ())

    def test_truncated_terminal_page(self):
        with self.assertRaises(IncompleteRead):
            read_inventory(lambda _: page(["a"], complete=False))

    def test_cursor_loop(self):
        with self.assertRaises(IncompleteRead):
            read_inventory(lambda _: page([], "same", False))

    def test_budget(self):
        with self.assertRaisesRegex(IncompleteRead, "budget"):
            read_inventory(lambda _: page(["a"], "p2", False), max_pages=1)

    def test_page_failure(self):
        def fetch(cursor):
            if cursor is None:
                return page(["a"], "p2", False)
            raise TimeoutError("fixture timeout")
        with self.assertRaisesRegex(IncompleteRead, "fetch failed"):
            read_inventory(fetch)

    def test_snapshot_changes(self):
        pages = {None: page(["a"], "p2", False), "p2": page(["b"], snapshot="snapshot-b")}
        with self.assertRaisesRegex(IncompleteRead, "snapshot changed"):
            read_inventory(pages.__getitem__)

    def test_duplicate_id(self):
        pages = {None: page(["a"], "p2", False), "p2": page(["a"])}
        with self.assertRaises(IncompleteRead):
            read_inventory(pages.__getitem__)

    def test_missing_completion_evidence(self):
        with self.assertRaises(IncompleteRead):
            read_inventory(lambda _: {"ids": [], "snapshot": "a", "next": None})

    def test_contradictory_completion(self):
        with self.assertRaises(IncompleteRead):
            read_inventory(lambda _: page([], "p2", True))


if __name__ == "__main__":
    unittest.main(verbosity=2)
