"""Tests for the cross-layer audit.

The properties under test are the ones that make this different from a pile of
checks — and both encode a specific incident:

* **A check that could not run is a FAILURE, not a pass.** A dead webhook looked
  exactly like a healthy repo for three weeks because "unverified" and "verified"
  shared an exit code.
* **A skipped check still names its layer boundary.** A skip that cannot say what
  it would have compared is as opaque as no check at all.

Plus the precision the port had to gain: promoting a *census* to a *gate* raises
the bar, because a noisy row in a report is tolerable and a noisy row in a gate is
how the real findings get ignored.
"""

from __future__ import annotations

import json
from pathlib import Path

import pytest

from mgf.fed import _audit
from mgf.fed._audit import (
    CHECKS,
    COMMITTED,
    DECLARED,
    CheckResult,
    Finding,
    check_feedback_status,
    check_fleet,
    run_audit,
    run_checks,
)

# --------------------------------------------------------------------------- #
# The two structural properties.
# --------------------------------------------------------------------------- #


def test_every_check_declares_the_layer_boundary_it_crosses() -> None:
    valid = {_audit.DECLARED, _audit.COMMITTED, _audit.PUBLISHED, _audit.INSTALLED, _audit.LIVE}
    for name, (_fn, _net, layers) in CHECKS.items():
        assert len(layers) == 2, name
        assert set(layers) <= valid, f"{name} names a layer that does not exist: {layers}"


def test_an_unrequested_skip_is_a_failure_not_a_pass(tmp_path: Path, monkeypatch) -> None:
    """THE property. A check that cannot run must not share an exit code with a
    check that ran clean — that equivalence is what let a dead webhook pass."""
    monkeypatch.setattr(
        _audit,
        "check_fleet",
        lambda root, **kw: CheckResult("fleet", (DECLARED, COMMITTED), skipped="no token"),
    )
    monkeypatch.setitem(CHECKS, "fleet", (_audit.check_fleet, False, (DECLARED, COMMITTED)))
    assert run_audit(["fleet"], root=tmp_path) == 1


def test_a_skip_requested_by_offline_does_not_fail_the_run(tmp_path: Path) -> None:
    # --offline is an explicit opt-in to a subset: the operator asked for it, so it
    # is not a failure. It is still NAMED in the output (asserted below).
    assert run_audit(["pypi-namespace"], offline=True, root=tmp_path) == 0


def test_offline_skips_are_named_never_silent(tmp_path: Path, capsys) -> None:
    run_audit(["pypi-namespace", "governance"], offline=True, root=tmp_path)
    out = capsys.readouterr().out
    assert "did NOT run" in out
    assert "pypi-namespace" in out and "governance" in out


def test_a_skipped_check_still_reports_its_layers(tmp_path: Path) -> None:
    # A skip that cannot say what it WOULD have compared is as opaque as no check.
    [res] = run_checks(["ci-freshness"], offline=True, root=tmp_path)
    assert res.skipped == "--offline"
    assert res.layers == (_audit.LIVE, _audit.COMMITTED)


def test_clean_run_exits_zero(tmp_path: Path, monkeypatch) -> None:
    monkeypatch.setitem(
        CHECKS,
        "fleet",
        (
            lambda root, **kw: CheckResult("fleet", (DECLARED, COMMITTED), examined=3),
            False,
            (DECLARED, COMMITTED),
        ),
    )
    assert run_audit(["fleet"], root=tmp_path) == 0


def test_findings_exit_one(tmp_path: Path, monkeypatch) -> None:
    monkeypatch.setitem(
        CHECKS,
        "fleet",
        (
            lambda root, **kw: CheckResult(
                "fleet", (DECLARED, COMMITTED), examined=1, findings=[Finding("x", "gap")]
            ),
            False,
            (DECLARED, COMMITTED),
        ),
    )
    assert run_audit(["fleet"], root=tmp_path) == 1


def test_json_output_carries_layers_and_skips(tmp_path: Path, capsys) -> None:
    run_audit(["pypi-namespace"], offline=True, json_only=True, root=tmp_path)
    data = json.loads(capsys.readouterr().out)
    assert data["offline"] is True
    assert data["skipped"] == ["pypi-namespace"]
    assert data["checks"][0]["layers"] == [_audit.PUBLISHED, _audit.DECLARED]


# --------------------------------------------------------------------------- #
# check: fleet — declared ↔ committed/disk
# --------------------------------------------------------------------------- #


def _fleet_env(tmp_path: Path, *, registry: list[str], fleet_md: list[str]) -> Path:
    (tmp_path / "mgf-standard").mkdir(parents=True)
    (tmp_path / "mgf-standard" / "MGF_LIBRARIES.json").write_text(
        json.dumps({"libraries": [{"repo": r} for r in registry]}), encoding="utf-8"
    )
    (tmp_path / "mgf").mkdir(parents=True)
    (tmp_path / "mgf" / "FLEET.md").write_text("\n".join(f"- {n}" for n in fleet_md), "utf-8")
    for r in registry:
        (tmp_path / r).mkdir(exist_ok=True)
    return tmp_path


def test_fleet_flags_a_registry_member_absent_from_fleet_md(tmp_path: Path) -> None:
    root = _fleet_env(tmp_path, registry=["mgf-a", "mgf-b"], fleet_md=["mgf-a"])
    res = check_fleet(root)
    assert [f.target for f in res.findings] == ["mgf-b"]
    assert "not named in FLEET.md" in res.findings[0].detail


def test_fleet_flags_a_registry_member_missing_from_disk(tmp_path: Path) -> None:
    root = _fleet_env(tmp_path, registry=["mgf-a"], fleet_md=["mgf-a"])
    (root / "mgf-a").rmdir()
    assert any("not on disk" in f.detail for f in check_fleet(root).findings)


def test_fleet_is_clean_when_registry_disk_and_fleet_md_agree(tmp_path: Path) -> None:
    root = _fleet_env(tmp_path, registry=["mgf-a", "mgf-b"], fleet_md=["mgf-a", "mgf-b"])
    assert check_fleet(root).ok


def test_fleet_skips_rather_than_fails_without_the_court_workspace(tmp_path: Path) -> None:
    # FLEET.md lives in the private workspace, which is not a federation member. Its
    # absence is not a defect OF THE FLEET, so it must skip — and the skip then fails
    # the run, which is the honest outcome: nothing was verified.
    (tmp_path / "mgf-standard").mkdir(parents=True)
    res = check_fleet(tmp_path)
    assert res.skipped and not res.findings


# --------------------------------------------------------------------------- #
# check: feedback-status — the precision a gate needs that a census did not
# --------------------------------------------------------------------------- #


def _feedback(tmp_path: Path, body: str) -> Path:
    (tmp_path / "mgf-x").mkdir(parents=True)
    (tmp_path / "mgf-x" / "FEEDBACK.md").write_text(body, encoding="utf-8")
    return tmp_path


def test_a_paper_without_a_status_is_flagged(tmp_path: Path) -> None:
    root = _feedback(tmp_path, "## PAPER-12 — a thing\n\nsome prose, no status\n")
    assert [f.target for f in check_feedback_status(root).findings] == ["mgf-x:PAPER-12"]


def test_a_paper_with_a_status_bullet_is_clean(tmp_path: Path) -> None:
    root = _feedback(tmp_path, "## PAPER-12 — a thing\n\n**Status:** SHIPPED\n")
    assert check_feedback_status(root).ok


def test_an_emoji_prefixed_status_still_counts(tmp_path: Path) -> None:
    # Anchoring straight to a letter reported every `✅ SHIPPED` as unstated — i.e. it
    # called resolved papers unstated, the same over-reporting class as the release gate.
    root = _feedback(tmp_path, "## PAPER-12 — a thing\n\n**Status:** ✅ SHIPPED\n")
    assert check_feedback_status(root).ok


def test_a_cohort_heading_naming_a_RANGE_is_not_a_paper(tmp_path: Path) -> None:
    """The false positive this check produced on its very first run against the real
    fleet: `(PAPER-100..106)` groups seven papers that each carry their own heading."""
    root = _feedback(
        tmp_path,
        "## Plasma cohort — filed 2026-06-11 (PAPER-100..106)\n\nAll seven resolved.\n"
        "### PAPER-100 — a real one\n\n**Status:** SHIPPED\n",
    )
    assert check_feedback_status(root).ok


def test_a_real_paper_that_merely_MENTIONS_a_range_is_still_judged(tmp_path: Path) -> None:
    """The control for the fix above. `### PAPER-100 — … + REL-10..13` is a real paper;
    a looser "contains a range" rule would silently drop it — trading a false positive
    for a false negative, which in a gate is the worse of the two."""
    root = _feedback(tmp_path, "### PAPER-100 — Index of MUSTs omits REL-10..13\n\nno status\n")
    assert [f.target for f in check_feedback_status(root).findings] == ["mgf-x:PAPER-100"]


def test_template_headings_are_not_papers(tmp_path: Path) -> None:
    root = _feedback(tmp_path, "## PAPER-NN — <one-line title>\n\nthe submission template\n")
    assert check_feedback_status(root).ok


# --------------------------------------------------------------------------- #
# Ported behaviour that must not regress.
# --------------------------------------------------------------------------- #


def test_release_ignored_covers_the_unshipped_paths(tmp_path: Path) -> None:
    """The tuned list (PAPER-130): without it the gate called 28 distributions stale
    and 26 were noise. A gate that cries wolf 26 times is how the two real findings
    get ignored — over-reporting is a correctness bug in a gate, not a safe default."""
    for unshipped in (
        "docs/x.md",
        "tests/test_x.py",
        "uv.lock",
        "PUBLIC_API.json",
        "fed-resources/a.toml",
        "CHANGELOG.md",
        ".woodpecker.yml",
        "ops/deploy.sh",
    ):
        assert _audit.RELEASE_IGNORED.match(unshipped), unshipped
    for shipped in ("src/mgf/x/__init__.py", "src/mgf/x/core.py"):
        assert not _audit.RELEASE_IGNORED.match(shipped), shipped


def test_least_privilege_secrets_are_never_widened() -> None:
    # If a repo asks for a publish credential outside its allowed events, the fix is
    # the REPO. This set exists so the gate can never recommend loosening the secret.
    assert "devpi_password" in _audit.LEAST_PRIVILEGE
    assert "codeberg_package_token" in _audit.LEAST_PRIVILEGE


def test_held_deploy_path_arming_is_a_finding() -> None:
    """The inversion: while a path is HELD, the secrets being unavailable is the
    intended state and ARMING them is the failure. Registering these publishes the
    website, which is under the founder's explicit hold."""
    doc = {
        "when": {"event": "deployment"},
        "steps": [{"name": "deploy", "environment": {"TOK": {"from_secret": "resend_api_key"}}}],
    }
    findings = _audit._scope_findings("mgf-website", doc, {"resend_api_key": {"deployment"}})
    assert any("ARMED" in f.detail for f in findings)


def test_held_deploy_path_unregistered_is_the_intended_state() -> None:
    doc = {
        "when": {"event": "deployment"},
        "steps": [{"name": "deploy", "environment": {"TOK": {"from_secret": "resend_api_key"}}}],
    }
    assert _audit._scope_findings("mgf-website", doc, {}) == []


def test_an_unregistered_secret_outside_a_held_path_is_a_finding() -> None:
    doc = {"steps": [{"name": "s", "environment": {"T": {"from_secret": "nope"}}}]}
    findings = _audit._scope_findings("mgf-other", doc, {})
    assert findings and "dies at COMPILE" in findings[0].detail


def test_clone_and_services_demand_every_pipeline_event() -> None:
    # They are never pruned by a step-level `when:` — a scope gap there kills the run.
    doc = {"when": {"event": ["push", "tag"]}, "clone": {"x": {"from_secret": "k"}}}
    assert _audit._secret_demands(doc)["k"] == {"push", "tag"}


def test_a_step_when_narrows_the_events_it_demands() -> None:
    doc = {
        "when": {"event": ["push", "tag"]},
        "steps": [
            {
                "name": "publish",
                "when": {"event": "tag"},
                "environment": {"P": {"from_secret": "devpi_password"}},
            }
        ],
    }
    assert _audit._secret_demands(doc)["devpi_password"] == {"tag"}


def test_unknown_check_names_are_rejected_not_silently_ignored() -> None:
    assert "not-a-check" not in CHECKS


@pytest.mark.parametrize("name", sorted(CHECKS))
def test_every_check_is_runnable_offline_without_raising(name: str, tmp_path: Path) -> None:
    # An empty root is the hostile case: no registry, no repos, no workspace. A check
    # must skip or report, never crash the sweep for the checks behind it.
    [res] = run_checks([name], offline=True, root=tmp_path)
    assert res.name == name


# --------------------------------------------------------------------------- #
# The committed artifact + its CI-side gate.
#
# The expensive half needs the whole federation on disk plus credentials, so it
# cannot run in a single-repo CI clone. The cheap half asks the artifact four
# questions about ITSELF — and each is a way a checker has actually died here.
# --------------------------------------------------------------------------- #


def _artifact(tmp_path: Path, **over) -> Path:
    body = {
        "generated": _audit._today(),
        "checks": [],
        "findings": 0,
        "skipped": [],
        "ran": ["fleet", "governance"],
    }
    body.update(over)
    p = tmp_path / "AUDIT.json"
    p.write_text(json.dumps(body), encoding="utf-8")
    return p


def test_a_missing_artifact_fails(tmp_path: Path) -> None:
    assert _audit.check_artifact(tmp_path / "nope.json") == 1


def test_a_fresh_complete_clean_artifact_passes(tmp_path: Path) -> None:
    assert _audit.check_artifact(_artifact(tmp_path)) == 0


def test_an_unstamped_artifact_fails(tmp_path: Path, capsys) -> None:
    # An artifact with no date can never be called stale — so it would read as
    # current forever.
    assert _audit.check_artifact(_artifact(tmp_path, generated="")) == 1
    assert "UNSTAMPED" in capsys.readouterr().out


def test_a_stale_artifact_fails(tmp_path: Path, capsys) -> None:
    # The 2026-07-29 conformance tracker asserted a result from 2026-07-04 for
    # twenty-five days, because nothing was ever going to look.
    assert _audit.check_artifact(_artifact(tmp_path, generated="2020-01-01")) == 1
    assert "STALE" in capsys.readouterr().out


def test_an_INCOMPLETE_artifact_fails_even_with_zero_findings(tmp_path: Path, capsys) -> None:
    """THE property. An artifact from an --offline run carries real numbers and covers
    half the boundaries. Zero findings across the checks that RAN is not a clean audit,
    and must never be allowed to read as one."""
    art = _artifact(tmp_path, findings=0, skipped=["ci-freshness", "governance"])
    assert _audit.check_artifact(art) == 1
    out = capsys.readouterr().out
    assert "INCOMPLETE" in out and "ci-freshness" in out


def test_an_artifact_with_findings_fails(tmp_path: Path) -> None:
    assert _audit.check_artifact(_artifact(tmp_path, findings=3)) == 1


def test_an_unreadable_artifact_fails(tmp_path: Path) -> None:
    p = tmp_path / "AUDIT.json"
    p.write_text("{ not json", encoding="utf-8")
    assert _audit.check_artifact(p) == 1


def test_the_artifact_records_what_did_NOT_run(tmp_path: Path) -> None:
    run_audit(
        ["pypi-namespace", "fleet"],
        offline=True,
        root=tmp_path,
        write=tmp_path / "AUDIT.json",
        when="2026-07-30",
    )
    data = json.loads((tmp_path / "AUDIT.json").read_text())
    assert "pypi-namespace" in data["skipped"]
    assert data["generated"] == "2026-07-30"


def test_an_offline_artifact_does_not_satisfy_the_gate(tmp_path: Path) -> None:
    # End-to-end of the property above: writing an offline artifact and then gating
    # it must FAIL, however clean the checks that ran happened to be.
    run_audit(
        ["pypi-namespace", "fleet"],
        offline=True,
        root=tmp_path,
        write=tmp_path / "AUDIT.json",
        when=_audit._today(),
    )
    assert _audit.check_artifact(tmp_path / "AUDIT.json") == 1


def test_the_audits_own_artifact_is_not_shipped_source(tmp_path: Path) -> None:
    """`AUDIT.json` is this module's committed artifact — a repo-root governance file
    that no wheel ships. Leaving it out made the audit report the very repo that stores
    its results, one hour after the same gate taught us the same lesson about
    `AGENTS.md`. Two entries in one session, from opposite directions: an exclusion
    list decays, and it decays fastest right after you add something to the repo."""
    assert _audit.RELEASE_IGNORED.match("AUDIT.json")
