"""Generated CI guard-lines (SPEC-P4 §2 / PROPOSAL §2.6, the in-pipe net).

Every generated step that consumes a ``from_secret`` as a shell ENV VAR and REQUIRES
it carries a fail-fast guard in the ``$DEVPI_URL`` shape, so an empty injection (the
CIR-02 silent-empty class) turns LOUD at the first affected step instead of running
zero steps in error-status. "Guard the guard": a step that legitimately tolerates the
secret's absence carries NONE (no false-red), and a clone plugin's ``settings``
from_secret (not a shell env var) carries none either — there is no shell to guard.

These are static template-content pins; the fleet re-render is verified separately by
the drift-check going clean (Fable's court step, not run here).
"""

from __future__ import annotations

from pathlib import Path

import pytest

_TEMPLATES = Path(__file__).resolve().parents[2] / "templates" / "woodpecker"


def _read(name: str) -> str:
    return (_TEMPLATES / name).read_text(encoding="utf-8")


def _step_block(text: str, step_name: str) -> str:
    """The lines of the ``- name: <step_name>`` step, up to the next 2-space step."""
    lines = text.splitlines()
    start = next((i for i, ln in enumerate(lines) if ln.strip() == f"- name: {step_name}"), None)
    assert start is not None, f"step {step_name!r} not found"
    indent = len(lines[start]) - len(lines[start].lstrip())
    out = [lines[start]]
    for ln in lines[start + 1 :]:
        if ln.strip().startswith("- name:") and (len(ln) - len(ln.lstrip())) == indent:
            break
        out.append(ln)
    return "\n".join(out)


def _guard_for(env_var: str, secret: str) -> str:
    """The exact fail-fast guard line the template must carry for ``env_var``."""
    return (
        f'[ -n "${env_var}" ] || {{ echo "FATAL: {secret} injected empty '
        '-- check its Woodpecker scope for this event (mgf-secrets doctor)"; exit 1; }'
    )


# --------------------------------------------------------------------------- #
# Required env-var from_secret steps carry a correctly-scoped guard.
# --------------------------------------------------------------------------- #


def test_publish_step_guards_devpi_password() -> None:
    block = _step_block(_read("base.yml"), "publish")
    # The step consumes devpi_password as $TWINE_PASSWORD and REQUIRES it (tag-only).
    assert "from_secret: devpi_password" in block
    assert _guard_for("TWINE_PASSWORD", "devpi_password") in block
    # The guard precedes the consuming command (twine upload).
    assert block.index("TWINE_PASSWORD") < block.index("twine upload")


def test_ping_siblings_step_guards_woodpecker_token() -> None:
    block = _step_block(_read("frag-cornerstone-ping.yml"), "ping-siblings")
    assert "from_secret: woodpecker_token" in block
    assert _guard_for("WOODPECKER_TOKEN", "woodpecker_token") in block
    assert block.index("WOODPECKER_TOKEN") < block.index("mgf-fed ping-siblings")


@pytest.mark.parametrize("template", ["base.yml", "base-zig.yml"])
@pytest.mark.parametrize(
    ("env_var", "secret"),
    [
        ("CLONE_SSH_KEY", "codeberg_ssh_clone_key"),
        ("CLONE_SSH_HOST_KEY", "codeberg_ssh_host_key"),
    ],
)
def test_fetch_default_branch_guards_its_clone_secrets(
    template: str, env_var: str, secret: str
) -> None:
    """P3-F2 — the regression this pins.

    ``fetch-default-branch`` is a *clone-section* step, but unlike ``clone`` it runs
    ``commands:`` and pulls both ssh secrets in as shell env vars — so the plugin
    exemption above does NOT cover it and SC-23 applies in full. It shipped without
    guards to 43 repos; only ``first-light``'s own ``verify_ci`` objected.

    The step REQUIRES both secrets (it cannot fetch without them), so neither is a
    "guard the guard" tolerated case.
    """
    block = _step_block(_read(template), "fetch-default-branch")
    assert f"from_secret: {secret}" in block
    assert _guard_for(env_var, secret) in block
    # The guard precedes the command that consumes the value (the key-file write).
    assert block.index(_guard_for(env_var, secret)) < block.index("printf")


def test_guards_match_the_devpi_url_fail_fast_shape() -> None:
    """One greppable guard shape across the fleet: `[ -n "$X" ] || { echo …; exit 1; }`."""
    base = _read("base.yml")
    assert '[ -n "$DEVPI_URL" ]' in base  # the reference shape
    for env_var in ("TWINE_PASSWORD",):
        assert f'[ -n "${env_var}" ] || {{ echo "FATAL:' in base
        assert "exit 1; }" in base


# --------------------------------------------------------------------------- #
# "Guard the guard": tolerated / non-shell secrets carry NO fatal guard.
# --------------------------------------------------------------------------- #


def test_zig_clone_token_is_not_fatally_guarded() -> None:
    """A leaf Zig repo legitimately tolerates an empty codeberg_clone_token (netrc
    no-op) — a fatal guard would false-red it, so there must be none."""
    block = _step_block(_read("base-zig.yml"), "zig-build-and-test")
    assert "from_secret: codeberg_clone_token" in block
    # No FATAL fail-fast command for this secret (a comment MENTIONING the pattern is
    # fine — we check for the actual guard: the `|| { echo "FATAL: …` command form).
    assert '[ -n "$CODEBERG_CLONE_TOKEN" ] || {' not in block
    assert "FATAL: codeberg_clone_token" not in block
    # …and the reason is documented so the WF-13 re-render never "fixes" it.
    assert "guard the guard" in block.lower()


@pytest.mark.parametrize("template", ["base.yml", "base-zig.yml"])
def test_clone_plugin_ssh_secrets_carry_no_shell_guard(template: str) -> None:
    """The clone step feeds its ssh secrets through plugin ``settings:`` (no shell
    ``commands:``), so a `[ -n … ]` guard is neither possible nor present — plugin-git
    fails loud on a bad key on its own; this is not the silent-empty class."""
    block = _step_block(_read(template), "clone")
    assert "from_secret: codeberg_ssh_clone_key" in block
    assert '[ -n "$' not in block  # no shell guard in a plugin-settings step


# --------------------------------------------------------------------------- #
# Structural pins added 2026-08-05 after a fresh-context court chair observed
# that NOTHING in this repo failed when the two highest-blast-radius edits of the
# day were reverted. Both are three lines. Their absence is the whole finding:
# knowledge captured, enforcement not — this repo's own standing rule.
# --------------------------------------------------------------------------- #

_REPO = Path(__file__).resolve().parents[2]


def test_base_zig_can_render_the_ALWAYS_ON_commit_trailers_gate() -> None:
    """F-41's mechanism, pinned.

    `gates["commit-trailers"]` is unconditionally True for every member, and
    `_apply_markers` substitutes only the markers it FINDS — so a base template
    without the marker drops the gate in silence. `base-zig.yml` shipped that way,
    and six Zig members were generated with a byte-canonical, fail-closed checker
    and no step to run it.
    """
    for tmpl in ("base.yml", "base-zig.yml"):
        assert "# <<INSERT:commit-trailers>>" in _read(tmpl), (
            f"{tmpl} cannot render the AG-03 gate every member is declared to have"
        )


def test_this_repo_runs_its_own_canonical_fetch_step() -> None:
    """Canon lacking its own canon. mgf-standard's pipeline is bespoke, so the
    scaffold wave never touched it, and the repo that DEFINES the AG-03 checker
    ran that checker in its reduced-scope path for the step's whole life."""
    own = (_REPO / ".woodpecker.yml").read_text(encoding="utf-8")
    assert "- name: fetch-default-branch" in own


def test_no_pipeline_here_fetches_behind_a_fail_open_guard() -> None:
    """The shape the canonical fragment documents as AG-03's ORIGINAL defect.

    It fails with "cannot run ssh" on every run, the fail-open guard hides it, and
    the checker then compares a branch NAME that on a pull_request is the PR head.
    It survived in this repo's own commit-trailers step until 2026-08-05.

    Scanned over COMMAND LINES rather than the whole file, because the line is
    quoted in a comment two screens up — a raw-text search would condemn the
    explanation along with the defect. Deliberately stdlib-only: the first version
    imported `yaml`, which is not a dependency of this suite, so it failed
    everywhere for a reason that had nothing to do with the property.
    """
    for path in (_REPO / ".woodpecker.yml", _TEMPLATES / "base.yml"):
        for ln in path.read_text(encoding="utf-8").splitlines():
            s = ln.strip()
            if not s.startswith("- "):
                continue
            assert not ("git fetch" in s and ("|| true" in s or "||true" in s)), (
                f"{path.name} fetches behind a fail-open guard: {s[:90]}"
            )
