"""CI pin: PUBLIC_API.json must match each module's live `__all__`,
and PUBLIC_API.md must not silently fall behind either.

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.

DOC-04 (2026-07-29, filed by hrb-app) — the `.md` had drifted too, and
worse: 18 names and 8 whole `Client` verbs were missing, hrb-app's
promotion gate greps the `.md`, and the gate reported "not promoted"
for a shipped surface from 2026-05-26 to 2026-07-29. The failure mode
was structural, not careless — "the `.json` check passes because the
`.json` is generated, and the `.md` has no check at all". The last two
tests in this file are that missing check. They are deliberately a
PRESENCE test, not a generated-block diff: the `.md` is prose plus
hand-written tables and should stay writable by a human, so the gate
asserts that nothing is *absent*, and leaves how it is described to
the author.

The test does FOUR 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.

3. **For every name in a documented module's `__all__`**, the string
   MUST appear somewhere in `PUBLIC_API.md`. Catches: an export that
   ships and is never described, which is what DOC-04 measured.

4. **For every public `Client` method**, the name MUST appear in
   `PUBLIC_API.md`. `__all__` cannot see methods at all, so for the
   verb surface the `.md` is the ONLY contract — and it is the half
   that had an eight-verb hole.

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

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,
        )


@pytest.fixture(scope="module")
def public_api_md() -> str:
    """Load PUBLIC_API.md. Fail-loud if missing — for the verb surface
    it is the ONLY contract artifact (DOC-04)."""
    path = _REPO / "PUBLIC_API.md"
    if not path.exists():
        pytest.fail(f"{path} missing. It is the human-readable half of the DOC-08 contract.")
    return path.read_text(encoding="utf-8")


def test_public_api_md_describes_every_exported_name(
    public_api_json: dict[str, object],
    public_api_md: str,
) -> None:
    """DOC-04 gate, half one. Every `__all__` export of every
    documented module must be MENTIONED in PUBLIC_API.md.

    Presence, not shape: the `.md` is prose around hand-written tables
    and stays a human artifact. What must never happen again is a name
    shipping with no entry at all, because a consumer's promotion gate
    greps this file and reads absence as "not public"."""
    modules_block: dict[str, dict[str, list[str]]] = public_api_json["modules"]  # type: ignore[assignment]
    missing: dict[str, list[str]] = {}
    for module_name in modules_block:
        try:
            mod = importlib.import_module(module_name)
        except ImportError:  # already reported by the JSON test above
            continue
        absent = [n for n in sorted(getattr(mod, "__all__", [])) if n not in public_api_md]
        if absent:
            missing[module_name] = absent

    if missing:
        detail = "\n".join(f"  {mod}: {names}" for mod, names in sorted(missing.items()))
        pytest.fail(
            "names exported from __all__ but absent from PUBLIC_API.md "
            f"(DOC-04 — the drift a consumer's gate reads as 'not public'):\n{detail}",
            pytrace=False,
        )


def test_public_api_md_describes_every_client_verb(public_api_md: str) -> None:
    """DOC-04 gate, half two — the one the JSON structurally cannot
    cover. `__all__` enumerates TYPES; an SDK's contract is its VERBS,
    and `Client`'s methods appear in no generated artifact. Eight of
    them (the whole Part-13 read surface) were undocumented for two
    months precisely because nothing looked."""
    from mgf.hrb._client import Client

    public = sorted(
        name
        for name, member in vars(Client).items()
        if not name.startswith("_")
        and (callable(member) or isinstance(member, (property, classmethod)))
    )
    absent = [name for name in public if name not in public_api_md]
    if absent:
        pytest.fail(
            f"public Client methods absent from PUBLIC_API.md: {absent}. "
            "For the verb surface the .md is the only contract artifact — "
            "PUBLIC_API.json is generated from __all__ and cannot see methods.",
            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
