"""Tests for the deterministic ``.woodpecker.yml`` renderer + its drift wiring.

Mirrors ``test_drift.py`` / ``test_scaffold.py``: a self-contained ``tmp_path``
federation root (registry + a copy of the real ``mgf-standard/templates/``) holding
synthetic members for each pipeline SHAPE — plain library, cornerstone (mgf-common,
which is also a CLI + supervisor consumer), a vm-core-shaped library that declares
``[tool.mgf-fed]`` carve-outs, and a Zig member. The renderer is pure-text + reads the
real templates, so the markers asserted here are the same ones the fleet ships.
"""

from __future__ import annotations

import json
import shutil
from pathlib import Path

import pytest

from mgf.fed._drift import check
from mgf.fed._roster import Member, members
from mgf.fed._template import load_policy
from mgf.fed._woodpecker import (
    _KNOWN_CI_KEYS,
    derive_params,
    render_woodpecker,
    unknown_ci_key_detail,
    unknown_ci_keys,
)

# The real templates live next to this checkout's sibling mgf-standard. Copy them
# into each test root so the renderer reads faithful base.yml + frag-*.yml.
_REAL_TEMPLATES = Path(__file__).resolve().parents[3] / "mgf-standard" / "templates"

# These are federation-INTEGRATION tests: the renderer reads the REAL
# mgf-standard/templates, which exist only when the sibling repo is checked out
# alongside (the federation-root layout). A single-repo CI clone (the self-hosted
# Woodpecker) has no sibling mgf-standard, so skip there — the render is still
# validated by `mgf-fed drift-check` (supervisor) and by federation-root/local runs.
pytestmark = pytest.mark.skipif(
    not _REAL_TEMPLATES.exists(),
    reason="needs sibling mgf-standard/templates (federation-root); validated by drift-check",
)


def _root(tmp_path: Path) -> Path:
    """A federation root with one member per pipeline shape."""
    reg = {
        "release_waves": [["mgf-common", "mgf-lib", "mgf-vm-core", "mgf-zig"]],
        "libraries": [
            # cornerstone + CLI + supervisor consumer (mgf-common is hard-coded as
            # the cornerstone in derive_params, so the fixture MUST use that name).
            {
                "repo": "mgf-common",
                "lang": "python",
                "version": "1.0.0",
                "extras": ["dev", "observability", "standards", "vault"],
                "deps": {"mgf-test-supervisor": "*"},
            },
            # plain library: no CLI, no supervisor, not the cornerstone.
            {
                "repo": "mgf-lib",
                "lang": "python",
                "version": "1.0.0",
                "extras": ["dev", "standards"],
                "deps": {"mgf-common": "*"},
            },
            # vm-core-shaped: declares [tool.mgf-fed] vuln_ignore + extra_index.
            {
                "repo": "mgf-vm-core",
                "lang": "python",
                "version": "1.0.0",
                "extras": ["dev", "standards"],
                "deps": {"mgf-common": "*"},
            },
            {"repo": "mgf-zig", "lang": "zig", "version": "1.0.0"},
        ],
    }
    std = tmp_path / "mgf-standard"
    (std).mkdir(parents=True)
    (std / "MGF_LIBRARIES.json").write_text(json.dumps(reg), encoding="utf-8")
    shutil.copytree(_REAL_TEMPLATES, std / "templates")

    # --- mgf-common: cornerstone + CLI + supervisor consumer ---------------------
    common = tmp_path / "mgf-common"
    (common / "src" / "mgf" / "common").mkdir(parents=True)
    (common / "src" / "mgf" / "common" / "__init__.py").write_text("__version__ = '1.0.0'\n")
    (common / "pyproject.toml").write_text(
        '[project]\nname = "mgf-common"\nversion = "1.0.0"\n'
        "[project.optional-dependencies]\n"
        'dev = ["ruff"]\nobservability = []\nstandards = []\nvault = []\n'
        "[project.scripts]\n"
        'mgf-common = "mgf.common.__main__:main"\n',
        encoding="utf-8",
    )

    # --- mgf-lib: plain library --------------------------------------------------
    lib = tmp_path / "mgf-lib"
    (lib / "src" / "mgf" / "lib").mkdir(parents=True)
    (lib / "src" / "mgf" / "lib" / "__init__.py").write_text("__version__ = '1.0.0'\n")
    (lib / "pyproject.toml").write_text(
        '[project]\nname = "mgf-lib"\nversion = "1.0.0"\n'
        "[project.optional-dependencies]\ndev = []\nstandards = []\n",
        encoding="utf-8",
    )

    # --- mgf-vm-core: [tool.mgf-fed] carve-outs + a versioned mgf-common pin (AP-16) -
    vmc = tmp_path / "mgf-vm-core"
    (vmc / "src" / "mgf" / "vm" / "core").mkdir(parents=True)
    (vmc / "src" / "mgf" / "vm" / "core" / "__init__.py").write_text("__version__ = '1.0.0'\n")
    (vmc / "pyproject.toml").write_text(
        '[project]\nname = "mgf-vm-core"\nversion = "1.0.0"\n'
        'dependencies = ["mgf-common[standards]>=0.46,<0.50"]\n'
        "[project.optional-dependencies]\ndev = []\nstandards = []\n"
        "[tool.mgf-fed]\n"
        'vuln_ignore = ["GHSA-xxxx-yyyy-zzzz"]\n'
        "extra_index = true\n"
        'apt_packages = ["gcc", "libvirt-dev"]\n',
        encoding="utf-8",
    )

    # --- mgf-zig: Zig member -----------------------------------------------------
    (tmp_path / "mgf-zig").mkdir()
    (tmp_path / "mgf-zig" / "build.zig").write_text("// zig\n")

    return tmp_path


def _member(root: Path, repo: str) -> Member:
    return next(m for m in members(root) if m.repo == repo)


# --------------------------------------------------------------------------------
# render shapes
# --------------------------------------------------------------------------------


def test_render_is_valid_pipeline_with_top_level_tag_and_gitleaks(tmp_path: Path) -> None:
    """Every python render has the standard skeleton + the two drift-fixes."""
    root = _root(tmp_path)
    txt = render_woodpecker(_member(root, "mgf-lib"), root)
    # well-formed-enough pipeline: a top-level when: block and a steps: block.
    assert "when:" in txt and "steps:" in txt
    # drift-fix 1 — top-level when: fires on tag pushes (the publish flow).
    assert "- event: tag" in txt
    assert "refs/tags/v*" in txt
    # drift-fix 2 — secret-scan reads the canonical allowlist.
    assert "--config .gitleaks.toml" in txt
    # the canonical step-set is present.
    for step in ("lint", "typecheck", "test-3.12", "vuln-scan", "secret-scan", "publish"):
        assert f"name: {step}" in txt


def test_render_uses_full_non_partial_clone(tmp_path: Path) -> None:
    """A top-level `clone:` overrides Woodpecker's default treeless partial clone

    (`--filter=tree:0`), whose lazy promisor tree-fetch intermittently 504s
    against Codeberg and fails the `clone` step before any real step runs. Both
    the python and zig bases ship the pinned full-clone override (WF-09).
    """
    root = _root(tmp_path)
    for repo in ("mgf-lib", "mgf-zig"):
        txt = render_woodpecker(_member(root, repo), root)
        assert "clone:" in txt, repo
        assert "image: woodpeckerci/plugin-git:2.9.2" in txt, repo
        assert "partial: false" in txt, repo


def test_render_library_has_no_supervisor_or_cornerstone_steps(tmp_path: Path) -> None:
    """A plain library opts out of the supervisor + cornerstone + CLI fragments."""
    root = _root(tmp_path)
    txt = render_woodpecker(_member(root, "mgf-lib"), root)
    assert "supervisor-tier-0" not in txt
    assert "supervisor-self-check" not in txt
    assert "compat-matrix-example-app" not in txt
    assert "perf-nightly" not in txt
    assert "--version failed" not in txt  # no console-script smoke
    # The fleet-default pytest-CVE ignore is always present; a plain library adds no
    # OTHER (per-repo [tool.mgf-fed].vuln_ignore) advisory beyond it.
    assert "pip-audit --skip-editable --desc --ignore-vuln CVE-2025-71176" in txt
    assert "GHSA-" not in txt  # no per-repo carve-out


def test_lint_imports_gated_on_an_importlinter_contract(tmp_path: Path) -> None:
    """The DP-01 ``lint-imports`` line renders ONLY when the repo declares a contract
    (a ``.importlinter`` file or ``[tool.importlinter]``) — a fresh repo without one
    omits it instead of fast-failing the lint step on "Could not read any configuration"."""
    root = _root(tmp_path)
    # mgf-lib declares no contract → no lint-imports line.
    assert "- lint-imports" not in render_woodpecker(_member(root, "mgf-lib"), root)
    # a standalone .importlinter file (mgf-common's shape) → the line appears.
    (root / "mgf-lib" / ".importlinter").write_text(
        "[importlinter]\nroot_package = mgf.lib\n", encoding="utf-8"
    )
    assert "- lint-imports" in render_woodpecker(_member(root, "mgf-lib"), root)


def test_lint_imports_also_detects_pyproject_tool_table(tmp_path: Path) -> None:
    """``[tool.importlinter]`` in pyproject (mgf-cloud-app's shape) gates the line too."""
    root = _root(tmp_path)
    assert "- lint-imports" not in render_woodpecker(_member(root, "mgf-vm-core"), root)
    pp = root / "mgf-vm-core" / "pyproject.toml"
    pp.write_text(
        pp.read_text() + '\n[tool.importlinter]\nroot_package = "mgf.vm.core"\n', encoding="utf-8"
    )
    assert "- lint-imports" in render_woodpecker(_member(root, "mgf-vm-core"), root)


def test_browser_install_gated_on_the_browser_carve_out(tmp_path: Path) -> None:
    """``playwright install`` renders ONLY for a repo declaring ``[tool.mgf-fed] browser``.

    The default must stay off: the other members run on ``python:*-slim`` and must
    not pay a browser download for a suite that never opens one.
    """
    root = _root(tmp_path)
    assert "playwright install" not in render_woodpecker(_member(root, "mgf-lib"), root)
    pp = root / "mgf-lib" / "pyproject.toml"
    pp.write_text(pp.read_text() + "\n[tool.mgf-fed]\nbrowser = true\n", encoding="utf-8")
    txt = render_woodpecker(_member(root, "mgf-lib"), root)
    assert "python -m playwright install --with-deps chromium" in txt
    # Every test job needs it, not just one — the suite drives a browser throughout.
    assert txt.count("python -m playwright install --with-deps chromium") == 3


def test_browser_carve_out_is_a_known_ci_key(tmp_path: Path) -> None:
    """``browser`` must be recognised, not rejected as unknown ``[tool.mgf-fed]`` config."""
    root = _root(tmp_path)
    pp = root / "mgf-lib" / "pyproject.toml"
    pp.write_text(pp.read_text() + "\n[tool.mgf-fed]\nbrowser = true\n", encoding="utf-8")
    params = derive_params(_member(root, "mgf-lib"), root)
    assert params.browser is True


def _add_dev_dep(root: Path, repo: str, req: str) -> None:
    """Put ``req`` into ``repo``'s (empty) ``dev`` extra — the fixture's shape."""
    pp = root / repo / "pyproject.toml"
    txt = pp.read_text()
    assert "dev = []" in txt, "fixture shape changed: expected an empty dev extra"
    pp.write_text(txt.replace("dev = []", f'dev = ["{req}"]', 1), encoding="utf-8")


def test_ui_gate_gated_on_the_uitest_web_dependency(tmp_path: Path) -> None:
    """The WUI-07 ``ui-gate`` renders ONLY for a repo depending on ``mgf-uitest-web``.

    Detected from the dependency rather than declared: the specs cannot exist without
    the harness (their conftest imports ``mgf.uitest_web``), so a console cannot add
    the tier and forget to turn its gate on.
    """
    root = _root(tmp_path)
    assert "- name: ui-gate" not in render_woodpecker(_member(root, "mgf-lib"), root)
    _add_dev_dep(root, "mgf-lib", "mgf-uitest-web>=0.1,<0.2")
    txt = render_woodpecker(_member(root, "mgf-lib"), root)
    assert "- name: ui-gate" in txt
    # The gate must SELECT the tier explicitly: a console's addopts deselect `e2e`
    # by default, so a plain `pytest -q` would silently run none of these specs.
    assert "pytest tests/e2e -m e2e" in txt
    # It brings its own browser (the test jobs stay browser-free for a console).
    assert "python -m playwright install --with-deps chromium" in txt


def test_ui_gate_does_not_match_a_longer_distribution_name(tmp_path: Path) -> None:
    """A name that merely STARTS WITH the harness's must not switch the gate on."""
    root = _root(tmp_path)
    _add_dev_dep(root, "mgf-lib", "mgf-uitest-web-extras>=1.0")
    assert derive_params(_member(root, "mgf-lib"), root).has_ui_tests is False


def test_ui_gate_does_not_turn_on_the_test_job_browser_install(tmp_path: Path) -> None:
    """The harness dep gates the ui-gate step ONLY — it must not imply ``browser``.

    A console's normal test jobs never open a browser (its tier-3 specs are
    deselected there), so they must not pay the install; only the ui-gate does.
    """
    root = _root(tmp_path)
    _add_dev_dep(root, "mgf-lib", "mgf-uitest-web>=0.1,<0.2")
    params = derive_params(_member(root, "mgf-lib"), root)
    assert params.has_ui_tests is True
    assert params.browser is False
    txt = render_woodpecker(_member(root, "mgf-lib"), root)
    # exactly one install — the ui-gate's, not the three test jobs'
    assert txt.count("python -m playwright install --with-deps chromium") == 1


def test_fedres_check_gated_on_fed_resources(tmp_path: Path) -> None:
    """The standalone ``fedres check`` step renders ONLY when the repo carries
    ``fed-resources/source/project.toml`` (the fedres convention) — a repo without
    it omits the step instead of failing on a missing fed-resources/ tree."""
    root = _root(tmp_path)
    # mgf-lib has no fed-resources/ → no fedres-check step.
    assert "name: fedres-check" not in render_woodpecker(_member(root, "mgf-lib"), root)
    # add the convention → the step (and its `fedres check` gate) appears.
    src = root / "mgf-lib" / "fed-resources" / "source"
    src.mkdir(parents=True)
    (src / "project.toml").write_text(
        '[project]\nid = "mgf-lib"\nname = "mgf-lib"\nkind = "library"\n\n'
        "[projectors.architecture]\n[projectors.federation]\n",
        encoding="utf-8",
    )
    txt = render_woodpecker(_member(root, "mgf-lib"), root)
    assert "name: fedres-check" in txt
    assert "- fedres check" in txt


def test_mgf_common_matrix_gated_on_versioned_dep(tmp_path: Path) -> None:
    """The AP-16 pin-window compat-matrix renders ONLY for a repo that carries a
    versioned mgf-common dependency — it re-pins mgf-common DOWN to the declared floor
    (the MAJOR.MINOR of the ``>=`` bound) and re-runs the suite. The cornerstone
    itself, a repo with no versioned mgf-common pin, and a Zig repo all omit it."""
    root = _root(tmp_path)
    # mgf-vm-core pins `mgf-common[standards]>=0.46,<0.50` -> the step + floor 0.46.
    vmc = render_woodpecker(_member(root, "mgf-vm-core"), root)
    assert "name: mgf-common-matrix" in vmc
    assert 'pip install "mgf-common==0.46.*"' in vmc
    # the floor re-pin uses the steered retry, never a bare `|| pip install`; non-quiet
    # so pip logs the resolved version (the --quiet was dropped with the diagnostic).
    assert '|| (sleep 15 && pip install "mgf-common==0.46.*"' in vmc
    # the cornerstone is excluded (it doesn't pin itself; the gate excludes it too).
    assert "name: mgf-common-matrix" not in render_woodpecker(_member(root, "mgf-common"), root)
    # a repo whose pyproject declares no versioned mgf-common dep -> no floor -> off.
    assert "name: mgf-common-matrix" not in render_woodpecker(_member(root, "mgf-lib"), root)
    # a Zig repo (base-zig.yml, no python install) -> off.
    assert "name: mgf-common-matrix" not in render_woodpecker(_member(root, "mgf-zig"), root)


def test_mgf_common_matrix_floor_tracks_the_pin(tmp_path: Path) -> None:
    """The re-pin floor is derived from the repo's OWN pin, not hard-coded: change the
    pyproject floor and the rendered ``mgf-common==<floor>.*`` follows. A patch-level
    floor (``>=0.48.2``) collapses to the MINOR series ``0.48`` (what ``==X.Y.*`` can
    express)."""
    root = _root(tmp_path)
    pp = root / "mgf-vm-core" / "pyproject.toml"
    pp.write_text(pp.read_text().replace(">=0.46,<0.50", ">=0.48.2,<0.50"), encoding="utf-8")
    txt = render_woodpecker(_member(root, "mgf-vm-core"), root)
    assert 'pip install "mgf-common==0.48.*"' in txt


def test_mgf_common_matrix_floor_clamped_to_devpi_min(tmp_path: Path) -> None:
    """A declared floor BELOW devpi's oldest mgf-common (0.44) is clamped up — the
    matrix can only re-pin to a version devpi actually serves (0.41-0.43 were never
    backfilled). The repo's pin is left untouched; only the matrix-tested floor moves."""
    root = _root(tmp_path)
    pp = root / "mgf-vm-core" / "pyproject.toml"
    pp.write_text(pp.read_text().replace(">=0.46,<0.50", ">=0.41,<0.50"), encoding="utf-8")
    txt = render_woodpecker(_member(root, "mgf-vm-core"), root)
    assert 'pip install "mgf-common==0.44.*"' in txt  # 0.41 clamped up to devpi's 0.44
    assert '"mgf-common==0.41.*"' not in txt  # never the un-installable floor


def test_mgf_common_matrix_from_runtime_extra_pin(tmp_path: Path) -> None:
    """AP-16: a repo whose ONLY mgf-common pin lives in a *runtime* extra still gets the
    compat-matrix. ``mgf-fed`` keeps its runtime mgf-common in ``[cli]`` (its core is
    zero-dependency), so a core-deps-only floor scan would silently exclude the cockpit
    from the guard it ships to everyone else. The floor is read from core deps first,
    then the runtime extras the matrix installs (all but ``dev``/``standards``). A
    ``standards``-only conformance pin is NOT a runtime dep and stays OFF."""
    root = _root(tmp_path)
    pp = root / "mgf-lib" / "pyproject.toml"
    base = pp.read_text()  # mgf-lib has no mgf-common pin anywhere -> matrix off.
    assert "name: mgf-common-matrix" not in render_woodpecker(_member(root, "mgf-lib"), root)
    # mgf-common only in a [cli] runtime extra -> matrix ON, floor tracked from that pin.
    pp.write_text(
        base.replace(
            "dev = []\nstandards = []\n",
            'dev = []\nstandards = []\ncli = ["mgf-common>=0.49,<1.0"]\n',
        ),
        encoding="utf-8",
    )
    txt = render_woodpecker(_member(root, "mgf-lib"), root)
    assert "name: mgf-common-matrix" in txt
    assert 'pip install "mgf-common==0.49.*"' in txt
    # but a `standards`-only pin (conformance helper, not installed by the matrix) stays OFF.
    pp.write_text(
        base.replace("standards = []\n", 'standards = ["mgf-common[standards]>=0.49,<1.0"]\n'),
        encoding="utf-8",
    )
    assert "name: mgf-common-matrix" not in render_woodpecker(_member(root, "mgf-lib"), root)


def test_fleet_vuln_ignore_always_applied_and_deduped(tmp_path: Path) -> None:
    """Every pipeline ignores the fleet pytest CVE; a per-repo vuln_ignore appends its
    OWN advisory, and a per-repo list that repeats the fleet CVE never doubles it."""
    root = _root(tmp_path)
    vmc = render_woodpecker(_member(root, "mgf-vm-core"), root)
    assert "--ignore-vuln CVE-2025-71176" in vmc  # the fleet default
    assert "--ignore-vuln GHSA-xxxx-yyyy-zzzz" in vmc  # the per-repo carve-out
    assert vmc.count("CVE-2025-71176") == 2  # once per pip-audit invocation (cmd + retry)
    # a per-repo list that repeats the fleet CVE is deduped, not rendered 4×.
    pp = root / "mgf-vm-core" / "pyproject.toml"
    pp.write_text(
        pp.read_text().replace(
            '["GHSA-xxxx-yyyy-zzzz"]', '["CVE-2025-71176", "GHSA-xxxx-yyyy-zzzz"]'
        ),
        encoding="utf-8",
    )
    assert render_woodpecker(_member(root, "mgf-vm-core"), root).count("CVE-2025-71176") == 2


def test_render_all_repos_have_commit_trailers_gate(tmp_path: Path) -> None:
    """The AG-03 commit-trailers gate renders for EVERY repo (library + cornerstone),

    unlike the cornerstone-only no-pypi-surface guard — every repo's commits need
    both trailers.
    """
    root = _root(tmp_path)
    for repo in ("mgf-lib", "mgf-common"):
        txt = render_woodpecker(_member(root, repo), root)
        assert "name: commit-trailers" in txt, repo
        assert "scripts/check_commit_trailers.py" in txt, repo


def test_render_cli_has_console_script_smoke_for_mgf_common(tmp_path: Path) -> None:
    """mgf-common is a CLI: the wheel smoke step runs `mgf-common --version`."""
    root = _root(tmp_path)
    txt = render_woodpecker(_member(root, "mgf-common"), root)
    # /tmp/smoke is the in-CI-container smoke venv (a literal in the generated YAML),
    # asserted as a substring here — not a host temp path this test touches.
    assert "/tmp/smoke/bin/mgf-common --version" in txt  # noqa: S108
    assert "mgf-common --version failed" in txt


def test_render_cornerstone_has_compat_matrix_and_perf_nightly(tmp_path: Path) -> None:
    """The cornerstone gets the second-consumer compat matrix + nightly perf gate."""
    root = _root(tmp_path)
    txt = render_woodpecker(_member(root, "mgf-common"), root)
    assert "compat-matrix-example-app" in txt
    assert "perf-nightly" in txt
    # cornerstone-only extras: cron event + no-pypi-surface guard.
    assert "event: cron" in txt
    assert "no-pypi-surface" in txt
    # publish waits on the compat matrix (load-bearing release gate).
    assert "- compat-matrix-example-app" in txt
    # mgf-common depends on mgf-test-supervisor -> the supervisor lane is present.
    # Pinned on `supervisor-self-check` — the lane marker that exists in BOTH the
    # v3.2 corpus (tiered side-car steps) and the Wave-2 P1 shape (supervisor IS
    # the 3.12 gate; only the self-check step remains) — so this render test stays
    # true across the corpus transition instead of pinning a step P1 removes.
    assert "supervisor-self-check" in txt


def test_cornerstone_ping_gated_on_cornerstone(tmp_path: Path) -> None:
    """The AP-16 B-lite ping step (`mgf-fed ping-siblings`) renders ONLY in the
    cornerstone's release flow — it fans a cornerstone bump out to the siblings. A
    plain library / consumer never pings."""
    root = _root(tmp_path)
    corner = render_woodpecker(_member(root, "mgf-common"), root)
    assert "name: ping-siblings" in corner
    assert "mgf-fed ping-siblings" in corner
    assert "from_secret: woodpecker_token" in corner
    # tag-gated (fires on a release tag), depends on the devpi verify (post-publish).
    assert "depends_on: [verify-devpi]" in corner
    # a consumer / plain library never pings.
    for repo in ("mgf-vm-core", "mgf-lib"):
        assert "name: ping-siblings" not in render_woodpecker(_member(root, repo), root), repo


def test_render_zig_uses_zig_base_no_python_steps(tmp_path: Path) -> None:
    """A Zig member renders base-zig.yml: zig-build, no python publish flow."""
    root = _root(tmp_path)
    txt = render_woodpecker(_member(root, "mgf-zig"), root)
    assert "zig-build-and-test" in txt
    assert "zig fmt --check" in txt or "ziglang fmt --check" in txt
    # no python publish surface.
    assert "name: publish" not in txt
    assert "name: test-3.12" not in txt
    # zig pipeline still secret-scans with the canonical allowlist.
    assert "--config .gitleaks.toml" in txt


def test_render_carveouts_emit_ignore_vuln_and_devpi_index(tmp_path: Path) -> None:
    """vm-core-shaped [tool.mgf-fed] (vuln_ignore + extra_index) is rendered faithfully."""
    root = _root(tmp_path)
    txt = render_woodpecker(_member(root, "mgf-vm-core"), root)
    # vuln_ignore -> pip-audit --ignore-vuln <id> (NOT the default --strict line).
    assert "--ignore-vuln GHSA-xxxx-yyyy-zzzz" in txt
    assert "pip-audit --strict --desc" not in txt
    # Phase-2 RESOLVE cutover (B1): every package-install step uses a SINGLE devpi
    # --index-url, and NO public-PyPI extra-index (devpi's root/pypi cache covers
    # public deps; mgf-* are local-only ⇒ dependency-confusion structurally
    # impossible). §3.4: the index is the $DEVPI_URL server-level variable — the
    # endpoint literal never appears in a render (PAPER-113 whole).
    assert '--index-url "$DEVPI_URL"' in txt
    assert "--extra-index-url" not in txt
    assert "sslip.io" not in txt  # no hardcoded endpoint anywhere in a render
    assert "codeberg.org/api/packages" not in txt  # the Codeberg package path is dead


def test_render_devpi_url_consuming_installs_carry_the_guard(tmp_path: Path) -> None:
    """§3.4: every renderer-emitted install that references $DEVPI_URL is preceded by
    the fail-fast guard line (MGF_INFRA §2 — byte-identical to the one the template
    fragments carry), so an unset server variable fails with the reason, never with a
    cryptic pip error."""
    root = _root(tmp_path)
    txt = render_woodpecker(_member(root, "mgf-vm-core"), root)
    guard = (
        '- \'[ -n "$DEVPI_URL" ] || { echo "DEVPI_URL is unset -- set the Woodpecker '
        "server-level variable (MGF_INFRA.md §2; PAPER-113)\"; exit 1; }'"
    )
    assert guard in txt
    # every $DEVPI_URL-consuming install line has a guard in its step: the renderer
    # emits guard+install pairs, so guards ≥ steered installs (frag steps add more).
    installs = [ln for ln in txt.splitlines() if '--index-url "$DEVPI_URL"' in ln]
    guards = [ln for ln in txt.splitlines() if "DEVPI_URL is unset" in ln]
    assert installs and len(guards) >= len(installs)
    # every consuming install has a guard line EARLIER IN ITS OWN STEP (the guard
    # precedes the install; an apt-get list item may sit between them).
    lines = txt.splitlines()
    for i, ln in enumerate(lines):
        if '--index-url "$DEVPI_URL"' in ln and "pip install" in ln:
            step = []
            for back in range(i - 1, -1, -1):
                if "- name:" in lines[back]:
                    break
                step.append(lines[back])
            assert any("DEVPI_URL is unset" in s for s in step), f"unguarded install: {ln.strip()}"


def test_render_extra_index_false_has_no_devpi_reference_and_no_guard(tmp_path: Path) -> None:
    """extra_index=false (cornerstone / editable-pinned shape) renders bare installs:
    no $DEVPI_URL flags and no guard lines from the renderer. (Steps whose install
    text lives in base.yml/fragments — app-doctor, the matrix re-pin — keep their
    own unconditional guard+reference; the gate-off scope is the renderer-emitted
    install commands, i.e. everything before app-doctor.)"""
    root = _root(tmp_path)
    pp = root / "mgf-vm-core" / "pyproject.toml"
    pp.write_text(pp.read_text().replace("extra_index = true", "extra_index = false"))
    txt = render_woodpecker(_member(root, "mgf-vm-core"), root)
    body = txt.split("- name: app-doctor", 1)[0]  # lint/typecheck/tests/vuln installs
    assert "pip install --quiet -e" in body  # the bare editable installs are there…
    assert '--index-url "$DEVPI_URL"' not in body  # …with no index flags
    assert "DEVPI_URL is unset" not in body  # …and no renderer-emitted guard


def test_render_install_retry_is_steered_not_bare(tmp_path: Path) -> None:
    """The transient-failure retry re-runs the INDEX-STEERED install, never a bare
    one (a bare `|| pip install` would resolve mgf-* deps from public PyPI —
    dependency-confusion / stale-version). Finding B of the 2026-06-18 CI audit."""
    root = _root(tmp_path)
    txt = render_woodpecker(_member(root, "mgf-vm-core"), root)
    # The steered retry shape is present…
    assert "|| (sleep 15 && pip install" in txt
    # …and the old bare fallback (`<cmd> || pip install …`) is gone everywhere.
    assert "|| pip install" not in txt


def test_render_apt_packages_prefix_build_installs(tmp_path: Path) -> None:
    """[tool.mgf-fed] apt_packages renders an apt-get install before each build install."""
    root = _root(tmp_path)
    txt = render_woodpecker(_member(root, "mgf-vm-core"), root)
    apt = "apt-get update -qq && apt-get install -y -qq --no-install-recommends gcc libvirt-dev"
    # Present on the package-building installs (editable + pip-audit + sbom)…
    assert apt in txt
    # …and it precedes the editable install on its own list item, not inline.
    assert f"- {apt}\n      - pip install --quiet -e" in txt
    # A repo that declares no apt_packages renders no apt-get-install for build deps
    # (the commit-trailers `apt-get install git` is a separate, always-present frag).
    plain = render_woodpecker(_member(root, "mgf-lib"), root)
    assert "--no-install-recommends gcc" not in plain


def test_render_is_deterministic(tmp_path: Path) -> None:
    """Two renders of the same member are byte-identical (drift relies on this)."""
    root = _root(tmp_path)
    m = _member(root, "mgf-common")
    assert render_woodpecker(m, root) == render_woodpecker(m, root)


# --------------------------------------------------------------------------------
# drift wiring (generation-currency)
# --------------------------------------------------------------------------------


def test_drift_clean_when_woodpecker_matches_render(tmp_path: Path) -> None:
    """On-disk .woodpecker.yml == fresh render -> no ci-generated drift."""
    root = _root(tmp_path)
    m = _member(root, "mgf-lib")
    (m.repo_dir / ".woodpecker.yml").write_text(render_woodpecker(m, root), encoding="utf-8")
    items = check(m, load_policy(root), root).items
    assert not any(it.kind == "ci-generated" for it in items)
    # and definitely not the fallback name-list either (generation succeeded).
    assert not any(it.kind == "ci-step" for it in items)


def test_drift_flags_handedited_woodpecker(tmp_path: Path) -> None:
    """A hand-edited .woodpecker.yml yields a ci-generated DriftItem."""
    root = _root(tmp_path)
    m = _member(root, "mgf-lib")
    edited = render_woodpecker(m, root) + "\n  # hand-edited tweak\n"
    (m.repo_dir / ".woodpecker.yml").write_text(edited, encoding="utf-8")
    items = check(m, load_policy(root), root).items
    gen = [it for it in items if it.kind == "ci-generated"]
    assert gen, f"expected a ci-generated drift item, got {[i.kind for i in items]}"
    assert "mgf-fed scaffold" in gen[0].detail


def test_drift_falls_back_to_name_list_when_render_raises(tmp_path: Path) -> None:
    """An unrenderable repo (no src/**/__init__.py) degrades to the name-list check.

    The render raises FileNotFoundError (no import name); drift must NOT crash — it
    falls back to ``woodpecker_required_steps`` and, since the hand-written pipeline
    here is missing ``publish``, reports a ``ci-step`` (not ``ci-generated``) item.
    """
    root = _root(tmp_path)
    m = _member(root, "mgf-lib")
    # remove the package so derive_params -> _shallowest_import_name raises.
    shutil.rmtree(m.repo_dir / "src")
    (m.repo_dir / ".woodpecker.yml").write_text(
        "steps:\n  - name: lint\n",
        encoding="utf-8",  # missing required steps
    )
    items = check(m, load_policy(root), root).items
    assert any(it.kind == "ci-step" for it in items)
    assert not any(it.kind == "ci-generated" for it in items)


def test_drift_missing_woodpecker_is_a_gate_floor_item(tmp_path: Path) -> None:
    """A missing `.woodpecker.yml` is a GATE FLOOR failure (`ci-missing`), not a generic
    `missing`-file item (PAPER-122, superseding the old presence-check contract).

    The distinction is the whole point: a repo with no pipeline isn't merely missing a
    file the template would have given it — every Woodpecker run errors out, so NO gate
    has ever run on it, and neither the generation-currency check nor the step-name
    fallback (both of which need a file to read) can say a word.
    """
    root = _root(tmp_path)
    m = _member(root, "mgf-lib")
    # no .woodpecker.yml written.
    items = check(m, load_policy(root), root).items
    missing = [it for it in items if it.kind == "ci-missing"]
    assert missing, f"expected a ci-missing gate-floor item, got {[i.kind for i in items]}"
    assert "no gate has ever run" in missing[0].detail.lower()
    assert not any(it.kind in ("ci-generated", "ci-step", "ci-command") for it in items)
    assert not any(it.kind == "missing" and it.detail == ".woodpecker.yml" for it in items)


def test_fedres_check_installs_the_published_generator_for_a_normal_member(
    tmp_path: Path,
) -> None:
    root = _root(tmp_path)
    (root / "mgf-lib" / "fed-resources" / "source").mkdir(parents=True)
    (root / "mgf-lib" / "fed-resources" / "source" / "project.toml").write_text(
        '[project]\nid = "mgf-lib"\n', encoding="utf-8"
    )
    txt = render_woodpecker(_member(root, "mgf-lib"), root)
    # the fedres-check line pulls the PUBLISHED generator for an ordinary member
    assert 'pip install --quiet mgf-fedres "mgf-common[standards]"' in txt
    assert 'pip install --quiet -e . "mgf-common[standards]"' not in txt


def test_fedres_checks_ITSELF_with_its_own_code_not_the_published_release(
    tmp_path: Path,
) -> None:
    """The generator must verify itself with the code under review.

    Installing the PUBLISHED fedres to check the fedres repo deadlocks it: any change
    to a projector's output reds its own gate (the committed tree no longer matches
    what the OLD release regenerates), and the fix cannot publish until that gate is
    green. Found the hard way on the 0.3.0 scope change.
    """
    root = _root(tmp_path)
    repo = root / "mgf-fedres"
    (repo / "src" / "mgf" / "fedres").mkdir(parents=True)
    (repo / "src" / "mgf" / "fedres" / "__init__.py").write_text("__version__ = '1.0.0'\n")
    (repo / "pyproject.toml").write_text(
        '[project]\nname = "mgf-fedres"\nversion = "1.0.0"\n'
        "[project.optional-dependencies]\ndev = []\nstandards = []\n",
        encoding="utf-8",
    )
    (repo / "fed-resources" / "source").mkdir(parents=True)
    (repo / "fed-resources" / "source" / "project.toml").write_text(
        '[project]\nid = "mgf-fedres"\n', encoding="utf-8"
    )
    reg = json.loads((root / "mgf-standard" / "MGF_LIBRARIES.json").read_text())
    reg["libraries"].append(
        {"repo": "mgf-fedres", "lang": "python", "version": "1.0.0", "deps": {}}
    )
    (root / "mgf-standard" / "MGF_LIBRARIES.json").write_text(json.dumps(reg), encoding="utf-8")

    params = derive_params(_member(root, "mgf-fedres"), root)
    assert params.is_fedres is True
    assert params.fedres_install == "-e ."
    txt = render_woodpecker(_member(root, "mgf-fedres"), root)
    assert "fedres check" in txt
    # the generator is installed FROM THE CHECKOUT under review...
    assert 'pip install --quiet -e . "mgf-common[standards]"' in txt
    # ...and it must NOT pull its own published release to judge itself
    assert 'pip install --quiet mgf-fedres "mgf-common[standards]"' not in txt


# --------------------------------------------------------------------------------
# Unrecognised [tool.mgf-fed] carve-out keys.
#
# The regression these lock down is mgf-uitest-desktop's `qt = true` (2026-07-29):
# a knob that does not exist, written by analogy with `browser = true`. It parsed,
# printed a note to stderr in the middle of a 46-repo sweep, let `scaffold` report
# success, and rendered a Qt pipeline with no Qt libraries — three red CI runs
# before anyone read the note. The renderer's silence was the defect, not the typo.
# --------------------------------------------------------------------------------


def test_unknown_ci_keys_flags_the_real_qt_regression() -> None:
    assert unknown_ci_keys({"tool": {"mgf-fed": {"qt": True}}}) == ("qt",)


def test_qt_is_pointed_at_apt_packages_the_knob_that_does_the_job() -> None:
    # `qt` is not a typo of anything — no edit distance reaches `apt_packages`.
    # Only the semantic alias table gets the author to the right key.
    detail = unknown_ci_key_detail(("qt",))
    assert "did you mean `apt_packages`?" in detail


def test_detail_states_that_the_pipeline_is_not_shaped_by_the_key() -> None:
    # The old note said only "not read by the CI renderer", which reads like
    # bookkeeping. The consequence is what makes someone act.
    assert "NOT shaped by them" in unknown_ci_key_detail(("qt",))


def test_detail_lists_the_known_keys_so_the_fix_needs_no_source_dive() -> None:
    detail = unknown_ci_key_detail(("qt",))
    for known in ("apt_packages", "browser", "coverage_floor", "extra_index"):
        assert known in detail


def test_a_genuine_typo_is_caught_lexically() -> None:
    assert "did you mean `apt_packages`?" in unknown_ci_key_detail(("apt_packges",))


def test_a_tombstoned_knob_gets_the_reason_not_a_suggestion() -> None:
    detail = unknown_ci_key_detail(("skip_editable",))
    assert "removed in M2 §3.3" in detail
    assert "did you mean" not in detail


def test_every_known_key_is_accepted() -> None:
    table = dict.fromkeys(_KNOWN_CI_KEYS, True)
    assert unknown_ci_keys({"tool": {"mgf-fed": table}}) == ()


def test_the_policy_subtable_is_not_a_carve_out_key() -> None:
    # mgf-standard's own pyproject carries [tool.mgf-fed.policy] (the fleet policy,
    # read by _template.load_policy). Reporting it would fire on every run.
    assert unknown_ci_keys({"tool": {"mgf-fed": {"policy": {"schema": 1}}}}) == ()


def test_absent_table_and_absent_tool_section_are_both_silent() -> None:
    assert unknown_ci_keys({}) == ()
    assert unknown_ci_keys({"tool": {}}) == ()
    assert unknown_ci_keys({"tool": {"mgf-fed": {}}}) == ()


def test_keys_are_sorted_so_the_finding_text_is_stable() -> None:
    # An unstable detail string would make `drift-check` output churn between runs
    # and defeat diffing one sweep against the next.
    assert unknown_ci_keys({"tool": {"mgf-fed": {"zeta": 1, "alpha": 2}}}) == ("alpha", "zeta")


def test_unknown_ci_keys_is_pure_and_prints_nothing(capsys: pytest.CaptureFixture[str]) -> None:
    # The detector must stay silent so callers choose the loudness; `_mgf_ci` keeps
    # the stderr note, `drift-check` raises the same fact to a finding.
    unknown_ci_keys({"tool": {"mgf-fed": {"qt": True}}})
    captured = capsys.readouterr()
    assert captured.out == "" and captured.err == ""


def test_the_render_still_succeeds_despite_an_unknown_key(tmp_path: Path) -> None:
    # Non-negotiable: an unrecognised key must never break generation. The finding
    # is how it gets enforced; a crashed renderer would strand the whole fleet.
    root = _root(tmp_path)
    pyproject = root / "mgf-lib" / "pyproject.toml"
    pyproject.write_text(
        pyproject.read_text(encoding="utf-8") + "\n[tool.mgf-fed]\nqt = true\n",
        encoding="utf-8",
    )
    assert "steps:" in render_woodpecker(_member(root, "mgf-lib"), root)
