"""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.ci]`` 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 render_woodpecker

# 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.ci] 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: declares [tool.mgf-fed] carve-outs -------------------------
    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'
        "[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_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_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 -> supervisor steps present.
    assert "supervisor-tier-0" in txt


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). The publish/verify steps still target Codeberg until B2.
    assert "--index-url https://195.15.203.250.sslip.io/magogi/prod/+simple/" in txt
    assert "--extra-index-url" not in txt
    # the only remaining codeberg reference is the tag-gated publish/verify (B2):
    assert "codeberg.org/api/packages" not in txt.split("# <<INSERT", 1)[0].split("PUBLISH FLOW")[0]


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_mgf_ci_reads_only_tool_mgf_fed() -> None:
    """Carve-outs come from [tool.mgf-fed] only. The transitional [tool.mgf.ci]
    fallback was dropped once the fleet finished migrating — a legacy [tool.mgf.ci]
    is no longer read (and MUST NOT be, since it breaks MgfSettings)."""
    from mgf.fed._woodpecker import _mgf_ci

    assert _mgf_ci({"tool": {"mgf-fed": {"coverage_floor": 90}}}) == {"coverage_floor": 90}
    # legacy [tool.mgf.ci] is NO LONGER read
    assert _mgf_ci({"tool": {"mgf": {"ci": {"coverage_floor": 80}}}}) == {}
    assert _mgf_ci({"tool": {}}) == {}


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_present_item_not_ci(tmp_path: Path) -> None:
    """A missing .woodpecker.yml is reported by the presence check, not the ci check."""
    root = _root(tmp_path)
    m = _member(root, "mgf-lib")
    # no .woodpecker.yml written.
    items = check(m, load_policy(root), root).items
    assert any(it.kind == "missing" and it.detail == ".woodpecker.yml" for it in items)
    assert not any(it.kind in ("ci-generated", "ci-step") for it in items)
