"""AP-14 closed-box guard: ``mgf.fed`` reaches ``mgf.common`` via its PUBLIC API.

``mgf.common`` is an *external* package to ``mgf.fed`` (separately released), so
import-linter can't enforce the closed-box rule on its submodules — grimp treats
external packages as opaque. We enforce it here with the same AST walk the
``mgf-fed catch-up`` closed-box check runs on consumer repos, pointed at self.

One class of exception remains, documented:

* the entries in ``_DEFERRED`` — KNOWN leaks awaiting a public mgf.common API
  (a no-side-effect identity setter, a public doctor runtime-snapshot env var,
  a lifecycle reset/quiet-logger seam). Each must be removed once that API
  ships; ``test_deferred_leaks_are_all_real`` flags any that no longer exist.

``atomic_write`` was formerly reached via the blessed ``mgf.common._io`` seam;
mgf-common 0.48 promoted it to the public ``mgf.common.io.atomic_write`` (AP-14),
so the blessing was dropped and every reach now goes through the public API.
"""

from __future__ import annotations

import ast
import re
from pathlib import Path

_SRC = Path(__file__).resolve().parent.parent / "src" / "mgf" / "fed"

# Blessed private modules — allowed anywhere in mgf.fed. Empty since mgf-common
# 0.48 promoted atomic_write to the public mgf.common.io (was the lone _io bless).
_BLESSED: set[str] = set()

# Known, DEFERRED private reaches — NOW EMPTY (0.49). mgf-common 0.49 promoted the
# last three seams to public — `set_identity` (was `_identity._set_identity` /
# `_identity._APP_NAME` via `app_name()`+`current_context()`), `reset_context`
# (was `_lifecycle._GLOBAL_CONTEXT`), and `RUNTIME_SNAPSHOT_ENV_VAR` (was
# `_doctor_snapshot`). mgf.fed now reaches mgf.common ONLY through its public API
# — fully closed-box (AP-14 / DP-13). Keep this empty; a new entry is a regression.
_DEFERRED: dict[str, set[str]] = {}

_DUNDER = re.compile(r"^__.*__$")


def _private_reaches(tree: ast.AST) -> set[str]:
    """Private `mgf.common._x` names imported in *tree* (excludes dunders)."""
    found: set[str] = set()
    for node in ast.walk(tree):
        if isinstance(node, ast.ImportFrom) and node.module:
            if node.module == "mgf.common":
                # `from mgf.common import _x`
                found |= {a.name for a in node.names if a.name.startswith("_")}
            elif node.module.startswith("mgf.common."):
                # `from mgf.common._x import Y` → the _x segment
                seg = node.module.split(".")[2]
                if seg.startswith("_"):
                    found.add(seg)
        elif isinstance(node, ast.Import):
            for a in node.names:
                # `import mgf.common._x [as ...]`
                if a.name.startswith("mgf.common."):
                    seg = a.name.split(".")[2]
                    if seg.startswith("_"):
                        found.add(seg)
    return {n for n in found if not _DUNDER.match(n)}


def _reaches_by_file() -> dict[str, set[str]]:
    out: dict[str, set[str]] = {}
    for path in _SRC.rglob("*.py"):
        if "__pycache__" in path.parts:
            continue
        names = _private_reaches(ast.parse(path.read_text(encoding="utf-8")))
        names -= _BLESSED
        if names:
            out[path.relative_to(_SRC).as_posix()] = names
    return out


def test_no_undocumented_private_mgf_common_reaches() -> None:
    offenders: list[str] = []
    for rel, names in _reaches_by_file().items():
        extra = names - _DEFERRED.get(rel, set())
        for name in sorted(extra):
            offenders.append(f"{rel}: mgf.common.{name}")
    assert not offenders, (
        "AP-14: mgf.fed reaches mgf.common private modules — route through the "
        "public API, or (if no public API exists yet) add to "
        "tests/test_closed_box.py::_DEFERRED with a reason:\n  "
        + "\n  ".join(offenders)
    )


def test_deferred_leaks_are_all_real() -> None:
    """A `_DEFERRED` entry that no longer matches an actual reach is stale —
    remove it (the public mgf.common API presumably landed)."""
    actual = _reaches_by_file()
    stale: list[str] = []
    for rel, names in _DEFERRED.items():
        gone = names - actual.get(rel, set())
        for name in sorted(gone):
            stale.append(f"{rel}: mgf.common.{name}")
    assert not stale, (
        "AP-14: stale _DEFERRED entries (no longer imported) — remove them:\n  "
        + "\n  ".join(stale)
    )
