"""The federation GATE FLOOR in ``drift-check`` (PAPER-121/122).

The floor holds for EVERY registry member, in any language, exempt or not:

  1. a member must ship a ``.woodpecker.yml`` at all (``ci-missing``) — without one,
     every Woodpecker run errors out and NO gate has ever run on the repo;
  2. its pipeline must RUN the required gate COMMANDS, not merely carry steps with
     the right NAMES (``ci-command``) — the hole that let ``ruff format --check``
     miss every ``[exempt].woodpecker`` repo, mgf-standard included;
  3. a pipeline that runs the supervisor under ``coverage run`` must be paired with
     ``[tool.coverage.run] patch = ["subprocess"]`` (``ci-coverage``).

Self-contained: builds its own federation root, and the policy marks the member
exempt from generation-currency, so no render (and no sibling ``mgf-standard``
templates checkout) is needed — these tests run in a single-repo CI clone, unlike
``test_drift.py``'s federation-integration suite.
"""

from __future__ import annotations

import json
from typing import TYPE_CHECKING, Any

import pytest

from mgf.fed._drift import check, run_drift_check
from mgf.fed._roster import members
from mgf.fed._template import load_policy

if TYPE_CHECKING:
    from pathlib import Path

# Generation is ON, and `mgf-bespoke` is excused the generation-currency comparison
# (WF-13) — the exact configuration in which a gate used to be droppable in silence.
_POLICY = """\
schema = 1
present = []
woodpecker_generated = true
woodpecker_required_steps = ["lint"]
[exact]
[pyproject]
[exempt]
woodpecker = ["mgf-bespoke"]
"""

_GATE_COMMANDS = (
    "    commands:\n"
    "      - ruff check src tests\n"
    "      - ruff format --check src tests\n"
    "      - python scripts/check_commit_trailers.py\n"
)

#: The hardened clone block every committed pipeline must carry (PAPER-109 SSH clone +
#: WF-09 full clone) — without it, the gate floor now emits ``ci-clone`` (CIR-03).
_CLONE = (
    "clone:\n  - name: clone\n    image: woodpeckerci/plugin-git:2.9.2\n"
    "    settings:\n      partial: false\n      use-ssh: true\n"
)

#: A conformant bespoke pipeline: bespoke in shape, honest on gates.
_CONFORMANT = f"{_CLONE}steps:\n  - name: lint\n{_GATE_COMMANDS}  - name: zig-build-and-test\n"

_PYPROJECT = '[project]\nname = "mgf-bespoke"\nversion = "1.0.0"\n'
_PATCH_KEY = '[tool.coverage.run]\npatch = ["subprocess"]\n'


def _root(
    tmp_path: Path,
    *,
    pipeline: str | None = _CONFORMANT,
    pyproject: str = _PYPROJECT,
    extra_members: list[dict[str, Any]] | None = None,
    policy_extra: str = "",
) -> Path:
    reg: dict[str, Any] = {
        "release_waves": [["mgf-bespoke"]],
        "libraries": [
            {"repo": "mgf-bespoke", "lang": "python", "version": "1.0.0", "extras": ["dev"]},
            *(extra_members or []),
        ],
    }
    std = tmp_path / "mgf-standard" / "templates"
    std.mkdir(parents=True)
    (tmp_path / "mgf-standard" / "MGF_LIBRARIES.json").write_text(json.dumps(reg))
    # `policy_extra` appends inside the trailing [exempt] table (it is the last one).
    (std / "policy.toml").write_text(_POLICY + policy_extra, encoding="utf-8")

    repo = tmp_path / "mgf-bespoke"
    repo.mkdir()
    (repo / "pyproject.toml").write_text(pyproject, encoding="utf-8")
    if pipeline is not None:
        (repo / ".woodpecker.yml").write_text(pipeline, encoding="utf-8")
    return tmp_path


def _clone(root: Path, name: str, *, pipeline: str | None = None, git: bool = True) -> Path:
    """A checked-out sibling repo the registry does NOT list (an mgf-brand stand-in).

    ``.git`` is created as a plain directory — a path test, never a ``git`` call: the
    CI test image ships no ``git`` binary (only the commit-trailers step apt-installs
    it), so a test that shelled out here would need a skipif guard and would stop
    testing anything in CI.
    """
    d = root / name
    d.mkdir()
    if git:
        (d / ".git").mkdir()
    if pipeline is not None:
        (d / ".woodpecker.yml").write_text(pipeline, encoding="utf-8")
    return d


def _items(root: Path, repo: str = "mgf-bespoke", policy: dict[str, Any] | None = None) -> Any:
    m = next(x for x in members(root) if x.repo == repo)
    return check(m, policy if policy is not None else load_policy(root), root).items


# ---------------------------------------------------------------------------
# 1. ci-command — a step NAME is not a gate (PAPER-121)
# ---------------------------------------------------------------------------


def test_conformant_bespoke_pipeline_is_clean(tmp_path: Path) -> None:
    assert _items(_root(tmp_path)) == ()


def test_exempt_repo_may_not_silently_drop_a_gate_command(tmp_path: Path) -> None:
    """THE hole: the exempt pipeline keeps the `lint` step (so the step-name fallback
    is satisfied) but never runs `ruff format --check`. That is what mgf-hrb /
    mgf-test-supervisor / mgf-standard actually looked like after PAPER-115 shipped
    the gate — drift-check green, gate not running, real format drift on main.
    """
    pipeline = (
        f"{_CLONE}steps:\n"
        "  - name: lint\n"
        "    commands:\n"
        "      - ruff check src tests\n"  # the format half was never added
        "      - python scripts/check_commit_trailers.py\n"
    )
    items = _items(_root(tmp_path, pipeline=pipeline))
    assert [i.kind for i in items] == ["ci-command"]
    assert "ruff format --check" in items[0].detail
    assert "ci-generated" not in {i.kind for i in items}  # still exempt from the render check


def test_commented_out_gate_is_not_a_gate(tmp_path: Path) -> None:
    pipeline = (
        f"{_CLONE}steps:\n"
        "  - name: lint\n"
        "    commands:\n"
        "      - ruff check src tests\n"
        "      # - ruff format --check src tests\n"
        "      - python scripts/check_commit_trailers.py\n"
    )
    items = _items(_root(tmp_path, pipeline=pipeline))
    assert [i.kind for i in items] == ["ci-command"]


def test_required_commands_are_policy_sourced(tmp_path: Path) -> None:
    """`required_commands` in the policy overrides the built-in default. (The key
    belongs in mgf-standard's policy.toml; mgf-fed defaults until it lands.)"""
    root = _root(tmp_path)
    policy = load_policy(root)
    policy["required_commands"] = ["gitleaks detect"]  # a gate this pipeline does not run
    items = _items(root, policy=policy)
    assert [i.kind for i in items] == ["ci-command"]
    assert "gitleaks detect" in items[0].detail

    policy["required_commands"] = ["ruff check"]  # …one it does
    assert _items(root, policy=policy) == ()


# ---------------------------------------------------------------------------
# 1b. ci-clone — the hardened clone on EVERY member (CIR-03 / PAPER-128)
# ---------------------------------------------------------------------------


def test_ci_clone_fires_for_a_pipeline_missing_the_block(tmp_path: Path) -> None:
    """A pipeline with NO clone block silently inherits Woodpecker's default (partial
    clone + OAuth) — and mgf-bespoke is `[exempt].woodpecker`, proving the clone floor
    holds exempt-or-not: "we don't regenerate your file" ≠ "ride the flaky default"."""
    pipeline = f"steps:\n  - name: lint\n{_GATE_COMMANDS}  - name: zig-build-and-test\n"
    items = _items(_root(tmp_path, pipeline=pipeline))
    assert [i.kind for i in items] == ["ci-clone"]
    assert "is missing partial: false, use-ssh: true" in items[0].detail


def test_ci_clone_names_the_missing_half(tmp_path: Path) -> None:
    """partial:false present, use-ssh missing → the finding names ONLY the missing half."""
    partial_only = "clone:\n  - name: clone\n    settings:\n      partial: false\n"
    pipeline = f"{partial_only}steps:\n  - name: lint\n{_GATE_COMMANDS}"
    items = _items(_root(tmp_path, pipeline=pipeline))
    assert [i.kind for i in items] == ["ci-clone"]
    assert "is missing use-ssh: true" in items[0].detail
    assert "is missing partial: false" not in items[0].detail


def test_ci_clone_commented_out_block_is_not_a_setting(tmp_path: Path) -> None:
    """A commented-out setting is not a setting — and the canonical block's own prose
    mentions `partial: false`, so a raw-text scan would wrongly pass this."""
    pipeline = (
        "clone:\n  - name: clone\n    settings:\n"
        "      # partial: false\n      # use-ssh: true\n"
        f"steps:\n  - name: lint\n{_GATE_COMMANDS}"
    )
    items = _items(_root(tmp_path, pipeline=pipeline))
    assert [i.kind for i in items] == ["ci-clone"]
    assert "is missing partial: false, use-ssh: true" in items[0].detail


def test_ci_clone_clean_when_both_present(tmp_path: Path) -> None:
    """The default `_CONFORMANT` fixture carries the hardened clone — no finding."""
    assert not any(i.kind == "ci-clone" for i in _items(_root(tmp_path)))


def test_ci_clone_applies_to_a_non_python_member(tmp_path: Path) -> None:
    """The hardened clone is language-agnostic — a Zig member with a clone-less pipeline
    is flagged too (the check runs before the python-only ci-command gate)."""
    root = _root(tmp_path, extra_members=[{"repo": "mgf-zig", "lang": "zig", "version": "0.1.0"}])
    zig = root / "mgf-zig"
    zig.mkdir()
    (zig / ".woodpecker.yml").write_text(  # governance steps present, clone block absent
        "steps:\n  - name: commit-trailers\n  - name: secret-scan\n"
        "  - name: zig-build\n    commands:\n      - zig build test\n",
        encoding="utf-8",
    )
    items = _items(root, repo="mgf-zig")
    assert [i.kind for i in items] == ["ci-clone"]


def test_ci_clone_is_excusable_only_via_exempt_clone(tmp_path: Path) -> None:
    """`[exempt].woodpecker` never excuses the clone; only an explicit `[exempt].clone`
    entry does (read defensively, empty by default)."""
    pipeline = f"steps:\n  - name: lint\n{_GATE_COMMANDS}"  # clone-less
    root = _root(tmp_path, pipeline=pipeline, policy_extra='clone = ["mgf-bespoke"]\n')
    policy = load_policy(root)
    assert "mgf-bespoke" in policy["exempt"]["clone"]
    assert _items(root, policy=policy) == ()


# ---------------------------------------------------------------------------
# 1c. ci-uv-frozen — CI `uv` verbs must not re-resolve (WF-14 / CIR-04 / PAPER-125)
# ---------------------------------------------------------------------------


def test_ci_uv_frozen_fires_for_a_bare_uv_run(tmp_path: Path) -> None:
    """A CI `uv run` without `--frozen` re-resolves and rewrites uv.lock mid-pipeline
    (PAPER-125). The pipeline carries the clone block + gate commands, so ci-uv-frozen
    is the ONLY finding."""
    pipeline = f"{_CLONE}steps:\n  - name: lint\n{_GATE_COMMANDS}      - uv run pytest -q\n"
    items = _items(_root(tmp_path, pipeline=pipeline))
    assert [i.kind for i in items] == ["ci-uv-frozen"]
    assert "--frozen" in items[0].detail


def test_ci_uv_frozen_clean_when_pinned(tmp_path: Path) -> None:
    pipeline = (
        f"{_CLONE}steps:\n  - name: lint\n{_GATE_COMMANDS}      - uv run --frozen pytest -q\n"
    )
    assert not any(i.kind == "ci-uv-frozen" for i in _items(_root(tmp_path, pipeline=pipeline)))


def test_ci_uv_frozen_export_needs_no_emit_project(tmp_path: Path) -> None:
    pipeline = (
        f"{_CLONE}steps:\n  - name: lint\n{_GATE_COMMANDS}"
        "      - uv export --frozen --no-hashes -o reqs.txt\n"
    )
    items = _items(_root(tmp_path, pipeline=pipeline))
    assert [i.kind for i in items] == ["ci-uv-frozen"]
    assert "--no-emit-project" in items[0].detail


def test_ci_uv_frozen_silent_on_the_pip_fleet(tmp_path: Path) -> None:
    """The current fleet installs with pip and runs no resolving `uv` verb — no finding.
    (`_CONFORMANT` is exactly such a pipeline.)"""
    assert not any(i.kind == "ci-uv-frozen" for i in _items(_root(tmp_path)))


def test_ci_uv_frozen_applies_to_a_non_python_member(tmp_path: Path) -> None:
    """WF-14 is language-agnostic — a Zig member whose bespoke pipeline runs a bare
    `uv sync` is flagged too (the check runs before the python-only ci-command gate)."""
    root = _root(tmp_path, extra_members=[{"repo": "mgf-zig", "lang": "zig", "version": "0.1.0"}])
    zig = root / "mgf-zig"
    zig.mkdir()
    (zig / ".woodpecker.yml").write_text(
        f"{_CLONE}steps:\n  - name: commit-trailers\n  - name: secret-scan\n"
        "  - name: build\n    commands:\n      - uv sync --extra dev\n",
        encoding="utf-8",
    )
    items = _items(root, repo="mgf-zig")
    assert [i.kind for i in items] == ["ci-uv-frozen"]


# ---------------------------------------------------------------------------
# 2. ci-missing — a member with NO pipeline at all (PAPER-122 / mgf-brand)
# ---------------------------------------------------------------------------


def test_member_with_no_pipeline_at_all_is_drift(tmp_path: Path) -> None:
    items = _items(_root(tmp_path, pipeline=None))
    assert [i.kind for i in items] == ["ci-missing"]
    assert "no gate has ever run" in items[0].detail.lower()


def test_no_pipeline_is_excusable_only_explicitly(tmp_path: Path) -> None:
    """`[exempt].woodpecker` excuses generation-currency — it does NOT excuse having
    no pipeline. Only an explicit `[exempt].no_pipeline` entry does."""
    root = _root(tmp_path, pipeline=None)
    policy = load_policy(root)
    assert "mgf-bespoke" in policy["exempt"]["woodpecker"]
    assert [i.kind for i in _items(root, policy=policy)] == ["ci-missing"]

    policy["exempt"]["no_pipeline"] = ["mgf-bespoke"]
    assert _items(root, policy=policy) == ()


def test_out_of_shape_member_still_gets_the_gate_floor(
    tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
    """A Zig member is excused the Python scaffolding policy under the default
    `--shape python` — but no pipeline means no gates in ANY language, and the shape
    filter used to hide that member from the sweep entirely."""
    root = _root(tmp_path, extra_members=[{"repo": "mgf-zig", "lang": "zig", "version": "0.1.0"}])
    (root / "mgf-zig").mkdir()  # a checked-out member with no pipeline at all

    assert run_drift_check(shape="python", root=root) == 1
    out = capsys.readouterr().out
    assert "✗ mgf-zig" in out
    assert "ci-missing" in out
    assert "GATE FLOOR" in out
    assert "✓ mgf-bespoke" in out  # the in-shape member is clean
    # The Zig member is NOT held to the Python gate set (it has no ruff).
    assert "ci-command" not in out


def test_clean_sweep_prints_no_gate_floor_section(
    tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
    assert run_drift_check(shape="python", root=_root(tmp_path)) == 0
    out = capsys.readouterr().out
    assert "GATE FLOOR" not in out
    assert "✓ mgf-bespoke" in out


# ---------------------------------------------------------------------------
# 3. ci-coverage — supervisor under coverage ↔ patch = ["subprocess"] (PAPER-122)
# ---------------------------------------------------------------------------

_SUPERVISOR_PIPELINE = (
    f"{_CLONE}steps:\n  - name: lint\n{_GATE_COMMANDS}"
    "  - name: test-3.12\n"
    "    commands:\n"
    "      - coverage run --source=src/mgf -m mgf.test_supervisor --tier 0-2\n"
    "      - coverage combine\n"
    "      - coverage report --fail-under=80\n"
)


def test_supervisor_coverage_without_the_patch_key_is_drift(tmp_path: Path) -> None:
    items = _items(_root(tmp_path, pipeline=_SUPERVISOR_PIPELINE))
    assert [i.kind for i in items] == ["ci-coverage"]
    assert 'patch = ["subprocess"]' in items[0].detail
    assert "EVERY TEST PASSING" in items[0].detail


def test_clean_once_the_coupled_key_lands(tmp_path: Path) -> None:
    root = _root(tmp_path, pipeline=_SUPERVISOR_PIPELINE, pyproject=_PYPROJECT + _PATCH_KEY)
    assert _items(root) == ()


def test_no_supervisor_no_coupling(tmp_path: Path) -> None:
    pipeline = (
        f"{_CLONE}steps:\n  - name: lint\n{_GATE_COMMANDS}"
        "  - name: test\n    commands:\n      - pytest -q\n"
    )
    assert _items(_root(tmp_path, pipeline=pipeline)) == ()


# ---------------------------------------------------------------------------
# 4. ci-ungoverned — a cloned repo the REGISTRY never listed (the real mgf-brand)
# ---------------------------------------------------------------------------


def test_ungoverned_clone_with_no_pipeline_is_drift(
    tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
    """THE mgf-brand hole, precisely. mgf-brand carries no publishable package, so
    MGF_LIBRARIES.json never listed it — and every check we own iterates *registry
    members*, so no tool ever looked at it. It is activated on the forge all the same:
    every run errored ("could not load config from forge") and NO gate has ever run.
    A member-scoped gate floor cannot see this repo; only a scan of the root can.
    """
    root = _root(tmp_path)
    _clone(root, "mgf-assets")  # cloned, no pipeline, not in the registry

    assert run_drift_check(shape="python", root=root) == 1
    out = capsys.readouterr().out
    assert "✗ mgf-assets" in out
    assert "ci-ungoverned" in out
    assert "GATE FLOOR" in out


def test_ungoverned_clone_that_runs_a_pipeline_is_not_flagged(tmp_path: Path) -> None:
    """Narrow on purpose: a non-registry repo that HAS a pipeline is running gates and is
    none of drift-check's business (mgf-website, mgf-cost-watch). This is what keeps the
    check at exactly one finding fleet-wide instead of a wall of noise."""
    root = _root(tmp_path)
    _clone(root, "mgf-website-ish", pipeline=_CONFORMANT)

    assert run_drift_check(shape="python", root=root) == 0


def test_a_directory_that_is_not_a_checkout_is_ignored(tmp_path: Path) -> None:
    """A scratch/build dir named `mgf-*` is not a repo — no `.git`, no finding. (The
    tmp root's own `mgf-standard` templates dir is such a non-checkout, and stays silent.)
    """
    root = _root(tmp_path)
    _clone(root, "mgf-scratch", git=False)

    assert run_drift_check(shape="python", root=root) == 0


def test_ungoverned_clone_is_excusable_explicitly(tmp_path: Path) -> None:
    """Deliberately CI-less is a *decision*, recorded in `[exempt].no_pipeline` — the same
    key that excuses a registry member. Never an accident, and never silent by default."""
    root = _root(tmp_path, policy_extra='no_pipeline = ["mgf-assets"]\n')
    _clone(root, "mgf-assets")

    assert run_drift_check(shape="python", root=root) == 0


# --------------------------------------------------------------------------- #
# ci-ui-gate (WUI-07) — the browser specs exist and NOTHING RUNS THEM          #
# --------------------------------------------------------------------------- #
_UITEST_PYPROJECT: dict[str, Any] = {
    "project": {"optional-dependencies": {"dev": ["mgf-uitest-web>=0.1,<0.2"]}}
}


def test_ui_gate_gap_fires_when_the_harness_is_installed_but_never_run() -> None:
    """The failure this tier exists to prevent: a console gains the browser specs +
    the harness dependency, its pipeline is never re-rendered, and the gate that
    looks present runs nothing (a console's addopts deselect `e2e` by default)."""
    from mgf.fed._pipeline import ui_gate_gap

    pipeline = "steps:\n  - name: test\n    commands:\n      - pytest --tb=short -q\n"
    assert ui_gate_gap(pipeline, _UITEST_PYPROJECT) is True


def test_ui_gate_gap_is_clean_when_the_tier_is_explicitly_selected() -> None:
    from mgf.fed._pipeline import ui_gate_gap

    pipeline = (
        "steps:\n"
        "  - name: test\n    commands:\n      - pytest --tb=short -q\n"
        "  - name: ui-gate\n    commands:\n      - pytest tests/e2e -m e2e --tb=short -q\n"
    )
    assert ui_gate_gap(pipeline, _UITEST_PYPROJECT) is False


def test_ui_gate_gap_ignores_repos_that_do_not_consume_the_harness() -> None:
    """Every other member must stay untouched — no harness dep, no finding."""
    from mgf.fed._pipeline import ui_gate_gap

    pipeline = "steps:\n  - name: test\n    commands:\n      - pytest --tb=short -q\n"
    plain: dict[str, Any] = {"project": {"optional-dependencies": {"dev": ["pytest>=8"]}}}
    assert ui_gate_gap(pipeline, plain) is False
    assert ui_gate_gap(pipeline, {}) is False


def test_ui_gate_gap_does_not_credit_a_step_merely_named_ui_gate() -> None:
    """PAPER-121: a step NAME is not a gate — the tier must actually be selected."""
    from mgf.fed._pipeline import ui_gate_gap

    pipeline = (
        "steps:\n  - name: ui-gate\n    commands:\n      - echo 'ui gate'\n      - pytest -q\n"
    )
    assert ui_gate_gap(pipeline, _UITEST_PYPROJECT) is True


def test_ui_gate_gap_does_not_credit_a_commented_out_gate() -> None:
    from mgf.fed._pipeline import ui_gate_gap

    pipeline = (
        "steps:\n  - name: ui-gate\n    commands:\n"
        "      # - pytest tests/e2e -m e2e --tb=short -q\n      - pytest -q\n"
    )
    assert ui_gate_gap(pipeline, _UITEST_PYPROJECT) is True
