"""CI pin: PUBLIC_API.json must match each module's live `__all__`.

O2 (review wave 3) — the JSON file had silently drifted by 22 names
from the actual `__all__` of `mgf.hrb`. Drift in either direction is
a defect (CFM-03). This test makes drift fail-fast in CI.

The test does TWO things:

1. **For every module the JSON enumerates**, the JSON's `all` list
   MUST exactly equal the module's runtime `__all__` (sorted).
   This catches: stale JSON (a name removed from `__all__` but
   still listed in JSON), stale `__all__` (a name added to JSON
   but never to `__all__`).

2. **For every public submodule in `mgf.hrb.*`**, the JSON MUST
   carry an entry. This catches: a new submodule added with
   `__all__` exports but the JSON forgot to enumerate it.

A failure here tells the contributor exactly which file to edit:
either fix the source `__all__` (if a name was added/removed
intentionally) or regenerate PUBLIC_API.json with the script
embedded in the failure message.
"""

from __future__ import annotations

import importlib
import json
import pkgutil
from pathlib import Path

import pytest

# The repo root (..../mgf-hrb/) — three parents up from this file.
_REPO = Path(__file__).resolve().parents[2]
_PUBLIC_API_JSON = _REPO / "PUBLIC_API.json"

# Single source of truth for the regeneration command — kept in lock-
# step with the assertion-failure messages so a contributor hitting
# this test sees exactly what to run.
_REGEN_HINT = (
    "Regenerate via: "
    "tools/generate_public_api_json.py > PUBLIC_API.json  "
    "(or inline: see the script's docstring)"
)


@pytest.fixture(scope="module")
def public_api_json() -> dict[str, object]:
    """Load PUBLIC_API.json. Fail-loud if missing or malformed —
    the file IS the contract."""
    if not _PUBLIC_API_JSON.exists():
        pytest.fail(
            f"{_PUBLIC_API_JSON} missing. The file is part of the contract "
            f"(DOC-08 / AP-10). {_REGEN_HINT}"
        )
    return json.loads(_PUBLIC_API_JSON.read_text())


def test_every_listed_module_matches_runtime_all(
    public_api_json: dict[str, object],
) -> None:
    """For each module enumerated in PUBLIC_API.json, the JSON's
    `all` list must equal the module's runtime `__all__` (both
    sorted). Catches drift in either direction."""
    modules_block: dict[str, dict[str, list[str]]] = public_api_json["modules"]  # type: ignore[assignment]
    failures: list[str] = []

    for module_name, entry in modules_block.items():
        try:
            mod = importlib.import_module(module_name)
        except ImportError as exc:
            failures.append(
                f"module {module_name!r} listed in PUBLIC_API.json but failed to import: {exc}"
            )
            continue

        runtime_all = sorted(getattr(mod, "__all__", []))
        documented_all = sorted(entry.get("all", []))

        if runtime_all != documented_all:
            only_in_runtime = sorted(set(runtime_all) - set(documented_all))
            only_in_doc = sorted(set(documented_all) - set(runtime_all))
            msg_parts = [f"DRIFT in {module_name}:"]
            if only_in_runtime:
                msg_parts.append(
                    f"  in __all__ but missing from PUBLIC_API.json: {only_in_runtime}"
                )
            if only_in_doc:
                msg_parts.append(f"  in PUBLIC_API.json but missing from __all__: {only_in_doc}")
            failures.append("\n".join(msg_parts))

    if failures:
        pytest.fail(
            "\n\n".join(failures) + "\n\n" + _REGEN_HINT,
            pytrace=False,
        )


def test_every_public_submodule_is_documented(
    public_api_json: dict[str, object],
) -> None:
    """For every importable submodule under `mgf.hrb.*` (excluding
    private `_*` and test/cli internals), the JSON must carry an
    entry. Catches a new public module that was added without
    updating PUBLIC_API.json.

    Excludes: `mgf.hrb._client` (private — leading underscore),
    `mgf.hrb.cli._*` (CLI internals), submodules under cli/
    (those are CLI implementations, not API surface)."""
    import mgf.hrb

    documented_modules: set[str] = set(
        public_api_json["modules"].keys()  # type: ignore[union-attr]
    )
    missing: list[str] = []

    pkg = mgf.hrb
    for info in pkgutil.iter_modules(pkg.__path__, prefix=f"{pkg.__name__}."):
        name = info.name
        # Skip private modules and the cli subpackage's internals.
        if any(part.startswith("_") for part in name.split(".")):
            continue
        # Skip the cli package itself's sub-modules — the package as
        # a whole is documented (`mgf.hrb.cli`), but each subcommand
        # implementation isn't.
        if name.startswith("mgf.hrb.cli."):
            continue
        if name not in documented_modules:
            missing.append(name)

    if missing:
        pytest.fail(
            f"public submodules NOT enumerated in PUBLIC_API.json: "
            f"{sorted(missing)}. Add each to the 'modules' block. "
            f"{_REGEN_HINT}",
            pytrace=False,
        )


def test_public_api_json_version_mirrors_package() -> None:
    """AP-13 — PUBLIC_API.json version MUST mirror mgf.hrb.__version__ (and pyproject)."""
    import json
    import tomllib
    from pathlib import Path

    import mgf.hrb

    root = Path(__file__).resolve()
    while not (root / "pyproject.toml").exists():
        root = root.parent
    data = json.loads((root / "PUBLIC_API.json").read_text(encoding="utf-8"))
    pj = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8"))["project"]["version"]
    assert data["version"] == mgf.hrb.__version__ == pj
