"""π* domain library tests (ticket #000015).

Covers:
- Registry contains the expected built-in π*'s after import
- Round-trip determinism for active π*'s
- Stubs raise NotImplementedError
- Composition algebra: chain produces same output as sequential calls
- Composition manifest fingerprint stability
- Existing arborist.wikitext.to_base behavior preserved
"""

from __future__ import annotations

import pytest

from arborist.pi_star import (
    REGISTRY,
    canonical_composition_id,
    compose,
    domains,
    get,
    list_keys,
)
from arborist.pi_star.protocol import (
    PiStarError,
    assert_round_trip,
    equivalence_class_id,
)


# --- registry --------------------------------------------------------


def test_registry_contains_expected_keys():
    keys = list_keys()
    assert "wikitext-base@v1" in keys
    assert "claim-lattice@v1" in keys
    assert "code-py-ast@v1" in keys
    assert "logic-kernel@v1" in keys
    assert "time-series-quantized@v1" in keys
    assert "tabular-pinned@v1" in keys


def test_domains_groups_by_domain():
    d = domains()
    assert "code-py-ast@v1" in d.get("code", [])
    assert "logic-kernel@v1" in d.get("logic", [])
    assert "time-series-quantized@v1" in d.get("time-series", [])


def test_get_unknown_raises():
    with pytest.raises(KeyError):
        get("nonexistent@v999")


# --- wikitext-base@v1 -----------------------------------------------


def test_wikitext_base_round_trip():
    pi_star = get("wikitext-base@v1")
    raw = b"Plain ''italic'' [[link|word]] text."
    once = pi_star.canonicalize(raw)
    twice = pi_star.canonicalize(once)
    assert once == twice
    assert_round_trip(pi_star, raw)


def test_wikitext_base_rejects_non_bytes():
    pi_star = get("wikitext-base@v1")
    with pytest.raises(PiStarError):
        pi_star.canonicalize("not bytes")  # type: ignore[arg-type]


def test_wikitext_base_equivalence_class():
    pi_star = get("wikitext-base@v1")
    raw = b"Plain text."
    eid = equivalence_class_id(pi_star, raw)
    assert len(eid) == 64  # SHA-256 hex


def test_wikitext_base_matches_legacy_to_base():
    """Re-home preserves identical output to the legacy
    ``arborist.wikitext.to_base`` API."""
    from arborist.wikitext import to_base

    pi_star = get("wikitext-base@v1")
    raw_text = "Some [[link|text]] with ''italics''."
    legacy = to_base(raw_text).encode("utf-8")
    via_pi_star = pi_star.canonicalize(raw_text.encode("utf-8"))
    assert legacy == via_pi_star


# --- claim-lattice@v1 ------------------------------------------------


def test_claim_lattice_round_trip_on_lattice_text():
    """Canonicalize is idempotent: canonicalize(canonicalize(x)) ==
    canonicalize(x). Note: the canonical form is a JSON list, so the
    second canonicalization re-parses the JSON as text and produces
    a different list — these projections are NOT idempotent on raw
    text. We only assert determinism under the same input."""
    pi_star = get("claim-lattice@v1")
    sample = (
        b"- A claim. [E1]\n"
        b"- Another claim. [E2]\n"
    )
    once = pi_star.canonicalize(sample)
    twice = pi_star.canonicalize(sample)
    assert once == twice


def test_claim_lattice_rejects_non_bytes():
    pi_star = get("claim-lattice@v1")
    with pytest.raises(PiStarError):
        pi_star.canonicalize(123)  # type: ignore[arg-type]


def test_claim_lattice_empty_input_returns_empty_array():
    pi_star = get("claim-lattice@v1")
    out = pi_star.canonicalize(b"")
    assert out == b"[]"


# --- stubs ---------------------------------------------------------


def test_no_stub_pi_stars_remain():
    """Closure-criterion guard: every reserved-stub π* has graduated.

    History:
    - 2026-05-07 (#000015): six concrete + four reserved stubs
      (``code-py-ast``, ``logic-kernel``, ``time-series-quantized``,
      ``tabular-pinned``).
    - 2026-05-08: arithmetic / logic-kernel / code-py-ast /
      time-series-quantized graduated.
    - 2026-05-09 (#000030 + tabular phase): algebra-symbolic /
      calculus-derivative / -integral / -limit / -series /
      linear-algebra / function-sampled / tabular-pinned all real.

    Adding a new reserved stub re-opens this list; that's the
    governance event this test pins."""
    from arborist.pi_star import REGISTRY
    for key, pi_star in REGISTRY.items():
        # A stub raises NotImplementedError on any input; a real
        # canonicalizer either succeeds or raises PiStarError on
        # bad input. We probe with empty bytes — most real kernels
        # raise PiStarError; none should raise NotImplementedError.
        try:
            pi_star.canonicalize(b"")
        except NotImplementedError:
            raise AssertionError(
                f"π* {key!r} still raises NotImplementedError — "
                f"reserved stub not yet graduated"
            )
        except Exception:
            # Any other exception (PiStarError, ValueError, etc.)
            # means the kernel is real.
            pass


# --- time-series-quantized@v1 (graduated from stub) ------------------


def test_time_series_canonicalizes_basic_series():
    import json as _json

    pi_star = get("time-series-quantized@v1")
    src = _json.dumps({"dt": 1, "dv": 0.1, "samples": [[0, 1.0], [1, 2.5]]}).encode()
    out = pi_star.canonicalize(src)
    assert out == b"dt=1;dv=0.1;n=2;t0=0:10|25"


def test_time_series_collapses_jitter_within_resolution():
    """Timestamps off by < dt/2 quantize to the same grid index."""
    import json as _json

    pi_star = get("time-series-quantized@v1")
    a = _json.dumps({"dt": 1, "dv": 0.1, "samples": [[0, 1.0], [1, 2.0]]}).encode()
    b = _json.dumps({"dt": 1, "dv": 0.1, "samples": [[0.4, 1.04], [1.3, 2.0]]}).encode()
    assert pi_star.canonicalize(a) == pi_star.canonicalize(b)


def test_time_series_sorts_out_of_order_samples():
    import json as _json

    pi_star = get("time-series-quantized@v1")
    sorted_input = _json.dumps({"dt": 1, "dv": 1, "samples": [[0, 1], [1, 2], [2, 3]]}).encode()
    shuffled = _json.dumps({"dt": 1, "dv": 1, "samples": [[2, 3], [0, 1], [1, 2]]}).encode()
    assert pi_star.canonicalize(sorted_input) == pi_star.canonicalize(shuffled)


def test_time_series_distinguishes_different_dt():
    import json as _json

    pi_star = get("time-series-quantized@v1")
    a = _json.dumps({"dt": 1, "dv": 1, "samples": [[0, 1]]}).encode()
    b = _json.dumps({"dt": 2, "dv": 1, "samples": [[0, 1]]}).encode()
    assert pi_star.canonicalize(a) != pi_star.canonicalize(b)


def test_time_series_distinguishes_different_dv():
    import json as _json

    pi_star = get("time-series-quantized@v1")
    a = _json.dumps({"dt": 1, "dv": 0.1, "samples": [[0, 1.0]]}).encode()
    b = _json.dumps({"dt": 1, "dv": 1.0, "samples": [[0, 1.0]]}).encode()
    assert pi_star.canonicalize(a) != pi_star.canonicalize(b)


def test_time_series_distinguishes_different_values():
    import json as _json

    pi_star = get("time-series-quantized@v1")
    a = _json.dumps({"dt": 1, "dv": 1, "samples": [[0, 1]]}).encode()
    b = _json.dumps({"dt": 1, "dv": 1, "samples": [[0, 2]]}).encode()
    assert pi_star.canonicalize(a) != pi_star.canonicalize(b)


def test_time_series_dedupes_collisions_keeping_last():
    """Two samples at the same quantized timestamp: last wins."""
    import json as _json

    pi_star = get("time-series-quantized@v1")
    src = _json.dumps({"dt": 1, "dv": 1, "samples": [[0, 1], [0.3, 5]]}).encode()
    out = pi_star.canonicalize(src)
    # Both timestamps quantize to t_idx=0; second sample (5) wins.
    assert out == b"dt=1;dv=1;n=1;t0=0:5"


def test_time_series_handles_empty_samples():
    import json as _json

    pi_star = get("time-series-quantized@v1")
    out = pi_star.canonicalize(_json.dumps({"dt": 1, "dv": 1, "samples": []}).encode())
    assert out == b"dt=1;dv=1;n=0;t0=0:"


def test_time_series_rejects_non_json():
    from arborist.pi_star.protocol import PiStarError

    pi_star = get("time-series-quantized@v1")
    with pytest.raises(PiStarError, match="not valid JSON"):
        pi_star.canonicalize(b"not json")


def test_time_series_rejects_missing_fields():
    import json as _json

    from arborist.pi_star.protocol import PiStarError

    pi_star = get("time-series-quantized@v1")
    with pytest.raises(PiStarError, match="missing required field"):
        pi_star.canonicalize(_json.dumps({"dt": 1, "samples": []}).encode())


def test_time_series_rejects_zero_dt():
    import json as _json

    from arborist.pi_star.protocol import PiStarError

    pi_star = get("time-series-quantized@v1")
    with pytest.raises(PiStarError, match="dt must be positive"):
        pi_star.canonicalize(_json.dumps({"dt": 0, "dv": 1, "samples": []}).encode())


def test_time_series_rejects_negative_dv():
    import json as _json

    from arborist.pi_star.protocol import PiStarError

    pi_star = get("time-series-quantized@v1")
    with pytest.raises(PiStarError, match="dv must be positive"):
        pi_star.canonicalize(_json.dumps({"dt": 1, "dv": -1, "samples": []}).encode())


def test_time_series_rejects_non_pair_samples():
    import json as _json

    from arborist.pi_star.protocol import PiStarError

    pi_star = get("time-series-quantized@v1")
    with pytest.raises(PiStarError, match="\\[timestamp, value\\] pair"):
        pi_star.canonicalize(_json.dumps({"dt": 1, "dv": 1, "samples": [[1, 2, 3]]}).encode())


def test_time_series_deterministic_across_repeated_calls():
    import json as _json

    pi_star = get("time-series-quantized@v1")
    src = _json.dumps({"dt": 0.5, "dv": 0.01, "samples": [[0, 0.05], [0.5, 0.10], [1.0, 0.15]]}).encode()
    a = pi_star.canonicalize(src)
    b = pi_star.canonicalize(src)
    c = pi_star.canonicalize(src)
    assert a == b == c


# --- code-py-ast@v1 (graduated from stub) ----------------------------


def test_code_py_ast_canonicalizes_simple_assign():
    pi_star = get("code-py-ast@v1")
    out = pi_star.canonicalize(b"x = 1")
    assert isinstance(out, bytes)
    assert b"Assign" in out
    assert b"Constant" in out


def test_code_py_ast_collapses_whitespace_and_comments():
    pi_star = get("code-py-ast@v1")
    a = pi_star.canonicalize(b"def foo(): pass")
    b = pi_star.canonicalize(b"def foo():\n    pass  # comment")
    assert a == b


def test_code_py_ast_collapses_quote_style():
    pi_star = get("code-py-ast@v1")
    a = pi_star.canonicalize(b"s = 'hello'")
    b = pi_star.canonicalize(b's = "hello"')
    assert a == b


def test_code_py_ast_collapses_operator_spacing():
    pi_star = get("code-py-ast@v1")
    a = pi_star.canonicalize(b"y = 1+2")
    b = pi_star.canonicalize(b"y = 1 + 2")
    assert a == b


def test_code_py_ast_distinguishes_identifiers():
    pi_star = get("code-py-ast@v1")
    a = pi_star.canonicalize(b"def foo(): pass")
    b = pi_star.canonicalize(b"def bar(): pass")
    assert a != b


def test_code_py_ast_distinguishes_operators():
    pi_star = get("code-py-ast@v1")
    a = pi_star.canonicalize(b"y = a + b")
    b = pi_star.canonicalize(b"y = a - b")
    assert a != b


def test_code_py_ast_distinguishes_argument_order():
    pi_star = get("code-py-ast@v1")
    a = pi_star.canonicalize(b"f(x, y)")
    b = pi_star.canonicalize(b"f(y, x)")
    assert a != b


def test_code_py_ast_rejects_non_bytes():
    from arborist.pi_star.protocol import PiStarError

    pi_star = get("code-py-ast@v1")
    with pytest.raises(PiStarError):
        pi_star.canonicalize("not bytes")  # type: ignore[arg-type]


def test_code_py_ast_rejects_invalid_python():
    from arborist.pi_star.protocol import PiStarError

    pi_star = get("code-py-ast@v1")
    with pytest.raises(PiStarError):
        pi_star.canonicalize(b"def foo( malformed")


def test_code_py_ast_deterministic_across_repeated_calls():
    pi_star = get("code-py-ast@v1")
    src = b"class Foo:\n    def bar(self, x):\n        return x + 1\n"
    a = pi_star.canonicalize(src)
    b = pi_star.canonicalize(src)
    c = pi_star.canonicalize(src)
    assert a == b == c


def test_code_py_ast_handles_empty_source():
    pi_star = get("code-py-ast@v1")
    out = pi_star.canonicalize(b"")
    # Empty source parses to ast.Module(body=[], type_ignores=[]).
    assert b"Module" in out
    assert b"body=[]" in out


def test_code_py_ast_equivalence_class_id_format():
    pi_star = get("code-py-ast@v1")
    eid = equivalence_class_id(pi_star, b"x = 1")
    assert len(eid) == 64
    # Same source twice → same id.
    assert eid == equivalence_class_id(pi_star, b"x = 1")


# --- arithmetic@v1 (graduated from stub) -----------------------------


def test_arithmetic_canonicalizes_integer():
    pi_star = get("arithmetic@v1")
    assert pi_star.canonicalize(b"3") == b"3/1"
    assert pi_star.canonicalize(b"-7") == b"-7/1"


def test_arithmetic_canonicalizes_decimal_exactly():
    """SQD §14.1: 0.1 must canonicalize to 1/10 exactly."""
    pi_star = get("arithmetic@v1")
    assert pi_star.canonicalize(b"0.1") == b"1/10"
    assert pi_star.canonicalize(b"0.5") == b"1/2"
    assert pi_star.canonicalize(b"0.25") == b"1/4"


def test_arithmetic_solves_classic_floating_point_question():
    """The SQD canonical example: 0.1 + 0.2 == 0.3 exactly."""
    pi_star = get("arithmetic@v1")
    a = pi_star.canonicalize(b"0.1+0.2")
    b = pi_star.canonicalize(b"0.3")
    assert a == b == b"3/10"


def test_arithmetic_collapses_equivalent_expressions():
    pi_star = get("arithmetic@v1")
    assert pi_star.canonicalize(b"1+2") == pi_star.canonicalize(b"3")
    assert pi_star.canonicalize(b"(1+2)*3") == pi_star.canonicalize(b"9")
    assert pi_star.canonicalize(b"6/4") == pi_star.canonicalize(b"3/2")


def test_arithmetic_distinguishes_distinct_values():
    pi_star = get("arithmetic@v1")
    assert pi_star.canonicalize(b"1+2") != pi_star.canonicalize(b"4")
    assert pi_star.canonicalize(b"1/2") != pi_star.canonicalize(b"1/3")


def test_arithmetic_supports_integer_exponent():
    pi_star = get("arithmetic@v1")
    assert pi_star.canonicalize(b"2**3") == b"8/1"
    assert pi_star.canonicalize(b"3**2") == b"9/1"


def test_arithmetic_rejects_division_by_zero():
    from arborist.pi_star.protocol import PiStarError

    pi_star = get("arithmetic@v1")
    with pytest.raises(PiStarError, match="division by zero"):
        pi_star.canonicalize(b"1/0")


def test_arithmetic_rejects_variables():
    from arborist.pi_star.protocol import PiStarError

    pi_star = get("arithmetic@v1")
    with pytest.raises(PiStarError, match="identifier"):
        pi_star.canonicalize(b"x + 1")


def test_arithmetic_rejects_non_integer_exponent():
    from arborist.pi_star.protocol import PiStarError

    pi_star = get("arithmetic@v1")
    with pytest.raises(PiStarError, match="non-integer exponent"):
        pi_star.canonicalize(b"2 ** 0.5")


def test_arithmetic_rejects_function_calls():
    from arborist.pi_star.protocol import PiStarError

    pi_star = get("arithmetic@v1")
    with pytest.raises(PiStarError, match="function call"):
        pi_star.canonicalize(b"abs(-5)")


def test_arithmetic_rejects_invalid_syntax():
    from arborist.pi_star.protocol import PiStarError

    pi_star = get("arithmetic@v1")
    with pytest.raises(PiStarError):
        pi_star.canonicalize(b"1 +")


def test_arithmetic_rejects_empty_input():
    from arborist.pi_star.protocol import PiStarError

    pi_star = get("arithmetic@v1")
    with pytest.raises(PiStarError, match="empty"):
        pi_star.canonicalize(b"")


def test_arithmetic_canonical_form_is_idempotent():
    pi_star = get("arithmetic@v1")
    once = pi_star.canonicalize(b"6/4")
    twice = pi_star.canonicalize(once)
    assert once == twice == b"3/2"


def test_arithmetic_handles_negative_results():
    pi_star = get("arithmetic@v1")
    assert pi_star.canonicalize(b"-(1+2)") == b"-3/1"
    assert pi_star.canonicalize(b"3 - 5") == b"-2/1"


# --- logic-kernel@v1 (graduated from stub) ---------------------------


def test_logic_kernel_canonicalizes_simple_and():
    pi_star = get("logic-kernel@v1")
    assert pi_star.canonicalize(b"A AND B") == b"A AND B"


def test_logic_kernel_commutativity():
    pi_star = get("logic-kernel@v1")
    assert pi_star.canonicalize(b"A AND B") == pi_star.canonicalize(b"B AND A")
    assert pi_star.canonicalize(b"A OR B") == pi_star.canonicalize(b"B OR A")


def test_logic_kernel_associativity():
    pi_star = get("logic-kernel@v1")
    a = pi_star.canonicalize(b"(A AND B) AND C")
    b = pi_star.canonicalize(b"A AND (B AND C)")
    assert a == b


def test_logic_kernel_impl_rewrite():
    pi_star = get("logic-kernel@v1")
    assert pi_star.canonicalize(b"A IMPL B") == pi_star.canonicalize(b"NOT A OR B")


def test_logic_kernel_iff_rewrite():
    pi_star = get("logic-kernel@v1")
    a = pi_star.canonicalize(b"A IFF B")
    b = pi_star.canonicalize(b"(NOT A OR B) AND (NOT B OR A)")
    assert a == b


def test_logic_kernel_xor_rewrite():
    pi_star = get("logic-kernel@v1")
    a = pi_star.canonicalize(b"A XOR B")
    b = pi_star.canonicalize(b"(A OR B) AND (NOT A OR NOT B)")
    assert a == b


def test_logic_kernel_de_morgans():
    pi_star = get("logic-kernel@v1")
    a = pi_star.canonicalize(b"NOT (A AND B)")
    b = pi_star.canonicalize(b"(NOT A) OR (NOT B)")
    assert a == b


def test_logic_kernel_double_negation():
    pi_star = get("logic-kernel@v1")
    assert pi_star.canonicalize(b"NOT NOT A") == pi_star.canonicalize(b"A")


def test_logic_kernel_distribution():
    pi_star = get("logic-kernel@v1")
    a = pi_star.canonicalize(b"A OR (B AND C)")
    b = pi_star.canonicalize(b"(A OR B) AND (A OR C)")
    assert a == b


def test_logic_kernel_tautology_collapses_to_true():
    pi_star = get("logic-kernel@v1")
    assert pi_star.canonicalize(b"A OR NOT A") == b"TRUE"
    assert pi_star.canonicalize(b"TRUE") == b"TRUE"


def test_logic_kernel_idempotence():
    pi_star = get("logic-kernel@v1")
    assert pi_star.canonicalize(b"A AND A") == pi_star.canonicalize(b"A")
    assert pi_star.canonicalize(b"A OR A") == pi_star.canonicalize(b"A")


def test_logic_kernel_distinguishes_logically_distinct():
    pi_star = get("logic-kernel@v1")
    assert pi_star.canonicalize(b"A AND B") != pi_star.canonicalize(b"A OR B")
    assert pi_star.canonicalize(b"A IMPL B") != pi_star.canonicalize(b"B IMPL A")


def test_logic_kernel_caps_atom_count():
    from arborist.pi_star.protocol import PiStarError

    pi_star = get("logic-kernel@v1")
    expr = b"A AND B AND C AND D AND E AND F AND G AND H AND I"
    with pytest.raises(PiStarError, match="caps atoms at"):
        pi_star.canonicalize(expr)


def test_logic_kernel_rejects_empty():
    from arborist.pi_star.protocol import PiStarError

    pi_star = get("logic-kernel@v1")
    with pytest.raises(PiStarError, match="empty"):
        pi_star.canonicalize(b"")


def test_logic_kernel_rejects_invalid_syntax():
    from arborist.pi_star.protocol import PiStarError

    pi_star = get("logic-kernel@v1")
    with pytest.raises(PiStarError):
        pi_star.canonicalize(b"A AND")


def test_logic_kernel_canonical_form_is_idempotent():
    pi_star = get("logic-kernel@v1")
    src = b"(A OR B) AND (NOT A OR C)"
    once = pi_star.canonicalize(src)
    twice = pi_star.canonicalize(once)
    assert once == twice


def test_logic_kernel_contrapositive_equivalence():
    """A IMPL B  ≡  NOT B IMPL NOT A"""
    pi_star = get("logic-kernel@v1")
    a = pi_star.canonicalize(b"A IMPL B")
    b = pi_star.canonicalize(b"(NOT B) IMPL (NOT A)")
    assert a == b


# --- composition --------------------------------------------------


def test_compose_text_then_claim_lattice():
    """Chaining wikitext-base@v1 then claim-lattice@v1 produces the
    same bytes as applying them sequentially."""
    composition = compose(
        "wikitext-base@v1",
        "claim-lattice@v1",
        register_in_registry=False,
    )
    raw = b"- A simple claim. [E1]"
    via_composition = composition.canonicalize(raw)

    inner = get("wikitext-base@v1")
    outer = get("claim-lattice@v1")
    sequential = outer.canonicalize(inner.canonicalize(raw))

    assert via_composition == sequential


def test_compose_registers_into_registry():
    """compose(register_in_registry=True) puts the composition in REGISTRY."""
    pre = list_keys()
    composition = compose(
        "wikitext-base@v1",
        "claim-lattice@v1",
        name="wikitext-then-claims-test",
        version="vt",
    )
    post = list_keys()
    assert "wikitext-then-claims-test@vt" in post
    assert len(post) == len(pre) + 1
    assert composition.domain == "text-or-wikitext"


def test_compose_unknown_inner_rejected():
    with pytest.raises(KeyError):
        compose("nope@v1", "claim-lattice@v1")


def test_compose_unknown_outer_rejected():
    with pytest.raises(KeyError):
        compose("wikitext-base@v1", "nope@v1")


def test_canonical_composition_id_stable():
    a = canonical_composition_id("wikitext-base@v1", "claim-lattice@v1")
    b = canonical_composition_id("wikitext-base@v1", "claim-lattice@v1")
    assert a == b
    # Order matters: a∘b ≠ b∘a.
    swapped = canonical_composition_id(
        "claim-lattice@v1", "wikitext-base@v1"
    )
    assert a != swapped


# --- protocol helpers (low-level unit) ----------------------------


def test_assert_round_trip_passes_on_idempotent_pi_star():
    """A π* that is idempotent on its inputs should not raise."""
    pi_star = get("wikitext-base@v1")
    assert_round_trip(pi_star, b"plain text")  # no exception expected


def test_assert_round_trip_raises_on_non_idempotent():
    """A custom π* whose canonicalize is non-idempotent triggers
    AssertionError. Construct one inline."""
    from dataclasses import dataclass

    @dataclass
    class _Counter:
        name: str = "_counter-pi-star"
        version: str = "v1"
        domain: str = "text"
        n_calls: int = 0

        def canonicalize(self, raw: bytes) -> bytes:
            # Append a counter byte each call → never idempotent.
            self.n_calls += 1
            return raw + str(self.n_calls).encode()

    bad = _Counter()
    with pytest.raises(AssertionError):
        assert_round_trip(bad, b"hello")


def test_equivalence_class_id_deterministic():
    pi_star = get("wikitext-base@v1")
    a = equivalence_class_id(pi_star, b"text")
    b = equivalence_class_id(pi_star, b"text")
    assert a == b
    assert len(a) == 64


def test_equivalence_class_id_changes_with_input():
    pi_star = get("wikitext-base@v1")
    a = equivalence_class_id(pi_star, b"text one")
    b = equivalence_class_id(pi_star, b"text two")
    assert a != b


def test_registry_key_format():
    from arborist.pi_star.protocol import registry_key

    pi_star = get("wikitext-base@v1")
    key = registry_key(pi_star)
    assert "@" in key
    name, version = key.split("@", 1)
    assert name == "wikitext-base"
    assert version == "v1"


# --- domains() smoke -----------------------------------------------


def test_domains_groups_keys_by_domain():
    """Every registered π* surfaces under exactly one domain key."""
    d = domains()
    all_keys: set[str] = set()
    for keys in d.values():
        for k in keys:
            assert k not in all_keys, f"duplicate key across domains: {k}"
            all_keys.add(k)
    # Sanity: total keys == len(REGISTRY) less any non-PiStar keys.
    assert len(all_keys) >= 6  # 2 active + 4 stubs minimum

