"""Boundary tests for the verify-laws Python parity gate.

Per Phase 7 of the console-v2-fix build (updated in Build A Phase 5): the gate at
`recoil/console-v2/scripts/verify_laws/check_fallback_registration_python.py`
walks recoil/api/**, recoil/pipeline/**, recoil/execution/** for
emit_fallback("…") and fire_sanctioned_fallback("…") call sites and asserts
every literal name is registered in the canonical
`recoil/pipeline/_lib/sanctioned_fallbacks.py` (FallbackRecord shape).

The gate is invoked from `pnpm verify-laws` (Cluster 4 CI gate). These
tests run it as a subprocess — the same form CI uses — so we exercise
the actual `__main__` exit-code surface, not an importable shim.

Coverage:
  1. The gate exits 0 against the real recoil/api/ tree (no drift today).
  2. The gate exits 1 when an unregistered emit_fallback("…") call exists
     in a fake module placed under a tmp scan root.
"""
from __future__ import annotations

import subprocess
import sys
from pathlib import Path

# Repo root resolved from this test file: tests/ -> api/ -> recoil/ -> CLAUDE_PROJECTS
REPO_ROOT = Path(__file__).resolve().parents[3]
GATE_PATH = (
    REPO_ROOT
    / "recoil"
    / "console-v2"
    / "scripts"
    / "verify_laws"
    / "check_fallback_registration_python.py"
)


def test_gate_script_exists() -> None:
    """The Phase 7 gate file is present at the canonical path."""
    assert GATE_PATH.is_file(), f"gate not found at {GATE_PATH}"


def test_gate_exits_zero_against_current_tree() -> None:
    """No drift today — every emit_fallback() call in recoil/api/** uses a
    name that exists in REGISTRY."""
    result = subprocess.run(
        [sys.executable, str(GATE_PATH)],
        capture_output=True,
        text=True,
        cwd=str(REPO_ROOT),
    )
    assert result.returncode == 0, (
        f"gate exit={result.returncode}\n"
        f"stdout:\n{result.stdout}\n"
        f"stderr:\n{result.stderr}"
    )
    # Sanity: success line includes the registry size.
    assert "match registry" in result.stdout


def test_gate_rejects_unregistered_name(tmp_path: Path) -> None:
    """Drop a fake module in a tmp tree, monkeypatch SCAN_ROOT to it, and
    assert exit code 1 with the offending file:line printed."""
    # Build a fake module containing a bogus emit_fallback() call site.
    fake = tmp_path / "fake_drift.py"
    fake.write_text(
        'emit_fallback("bogus_name_unregistered", scope="x")\n',
        encoding="utf-8",
    )

    # Python -c shim: import the gate module, override SCAN_ROOTS to the tmp
    # tree so the test's fake module is the ONLY location scanned (no real
    # codebase names leak in), then call main().
    shim = (
        "import importlib.util, sys\n"
        f"spec = importlib.util.spec_from_file_location('gate', r'{GATE_PATH}')\n"
        "mod = importlib.util.module_from_spec(spec)\n"
        "spec.loader.exec_module(mod)\n"
        "from pathlib import Path\n"
        f"mod.SCAN_ROOTS = [Path(r'{tmp_path}')]\n"
        "sys.exit(mod.main())\n"
    )
    result = subprocess.run(
        [sys.executable, "-c", shim],
        capture_output=True,
        text=True,
        cwd=str(REPO_ROOT),
    )
    assert result.returncode == 1, (
        f"expected exit 1 (drift), got {result.returncode}\n"
        f"stdout:\n{result.stdout}\n"
        f"stderr:\n{result.stderr}"
    )
    # The offending name must appear in the human-readable output.
    assert "bogus_name_unregistered" in result.stdout
    # And the file path must be cited.
    assert "fake_drift.py" in result.stdout


def test_gate_parses_fallbackrecord_registry() -> None:
    """Build A Phase 5: the canonical registry uses FallbackRecord(...) constructors
    with register_sanctioned_fallback(). Verify the gate's regex captures every
    name defined in the canonical registry file, and that the parsed set is a
    subset of the live canonical registry (which also includes dynamic registrations
    from consumer modules like run_shot.py)."""
    # Import the gate as a module and call parse_registry() directly.
    import importlib.util

    spec = importlib.util.spec_from_file_location("gate_mod", str(GATE_PATH))
    assert spec is not None and spec.loader is not None
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)

    registry_text = mod.REGISTRY_PATH.read_text(encoding="utf-8")
    parsed = mod.parse_registry(registry_text)

    # Cross-check against the live canonical import — the gate's parsed names
    # must be a subset of the canonical registry (canonical may have more entries
    # from pipeline/_lib/ that the regex doesn't scan for in FallbackRecord form).
    # Build A Phase 5: canonical registry replaces the old api-side REGISTRY.
    from recoil.pipeline._lib.sanctioned_fallbacks import list_sanctioned_fallbacks

    canonical_names = {r.name for r in list_sanctioned_fallbacks()}
    assert parsed.issubset(canonical_names), (
        f"gate regex parsed names not in canonical registry: "
        f"{sorted(parsed - canonical_names)}. "
        f"Canonical registry has: {sorted(canonical_names)}"
    )
