"""§3.6 — the shape-aware gitleaks drift comparison (closes PAPER-03).

Byte-exact comparison of ``.gitleaks.toml`` flagged every legitimately-ADDITIVE
consumer allowlist entry as drift (mgf-fastapi's own fixture allowlist being the
canonical false-positive), training maintainers to ignore the check. These pin the
replacement contract in BOTH directions: additive allowlist entries pass; removals/
edits of standard entries (and any non-allowlist mutation) still fail.

Self-contained (synthetic policy + template in tmp_path, no sibling checkout, no
renders) so the pin holds in a single-repo CI clone too.
"""

from __future__ import annotations

import json
import tomllib
from pathlib import Path

import pytest

from mgf.fed._drift import _gitleaks_shape_drift, check, gitleaks_restore_text
from mgf.fed._roster import members
from mgf.fed._scaffold import run_scaffold
from mgf.fed._template import load_policy

_TEMPLATE = """\
# Canonical gitleaks config (SH-05).
title = "mgf-common gitleaks config"

[extend]
useDefault = true

[allowlist]
description = "Files whose stated purpose is to embed secret-shaped strings."
paths = [
    '''tests/unit/test_redaction\\.py''',
    '''\\.claude/''',
]
"""

# The mgf-fastapi shape: KEEPS every standard entry, ADDS its own fixture path,
# rewrites the comments. Shape-conformant; byte-different.
_ADDITIVE = """\
# Consumer copy — extra fixture allowlisted (test constants, not real secrets).
title = "mgf-common gitleaks config"

[extend]
useDefault = true

[allowlist]
description = "Files whose stated purpose is to embed secret-shaped strings."
paths = [
    '''tests/unit/test_redaction\\.py''',
    '''\\.claude/''',
    # HmacWebhookVerifier(secret=SECRET) fixture — flagged by generic-api-key.
    '''tests/unit/test_webhooks\\.py''',
]
"""

# PAPER-03's counter-case: an additive entry BUT a standard entry dropped. This IS
# the mgf-fastapi CLOBBER event — missing the standard `\.claude/` AND carrying the
# extra `test_webhooks.py`.
_DROPS_STANDARD = _ADDITIVE.replace("    '''\\.claude/''',\n", "")

# The non-destructive restore of _DROPS_STANDARD (Finding 1, Option A): the standard
# `\.claude/` is re-added from the template and the consumer's carve-out is KEPT,
# appended in the template's '''…''' style just before the closing `]`.
_RESTORED = _TEMPLATE.replace(
    "    '''\\.claude/''',\n]",
    "    '''\\.claude/''',\n    '''tests/unit/test_webhooks\\.py''',\n]",
)

# A dropped standard entry with NO additive extra restores to the plain template.
_DROPS_STANDARD_NO_EXTRA = _TEMPLATE.replace("    '''\\.claude/''',\n", "")

# A gitleaks-only policy: this suite is about `.gitleaks.toml` shape, so the fixture
# repo ships no pipeline and is excused the gate floor's `.woodpecker.yml` requirement
# via `[exempt].no_pipeline` (see mgf.fed._pipeline) — otherwise every assertion here
# would carry an unrelated `ci-missing` item.
_POLICY = """\
schema = 1
present = [".gitleaks.toml"]
[exact]
".gitleaks.toml" = "gitleaks.toml"
[pyproject]
[exempt]
no_pipeline = ["mgf-x"]
"""


def _root(tmp_path: Path, gitleaks_text: str) -> Path:
    reg = {
        "release_waves": [["mgf-x"]],
        "libraries": [{"repo": "mgf-x", "lang": "python", "version": "1.0.0"}],
    }
    std = tmp_path / "mgf-standard"
    (std / "templates").mkdir(parents=True)
    (std / "MGF_LIBRARIES.json").write_text(json.dumps(reg), encoding="utf-8")
    (std / "templates" / "policy.toml").write_text(_POLICY, encoding="utf-8")
    (std / "templates" / "gitleaks.toml").write_text(_TEMPLATE, encoding="utf-8")
    repo = tmp_path / "mgf-x"
    repo.mkdir()
    (repo / "pyproject.toml").write_text('[project]\nname = "mgf-x"\nversion = "1.0.0"\n')
    (repo / ".gitleaks.toml").write_text(gitleaks_text, encoding="utf-8")
    return tmp_path


def _check_items(tmp_path: Path, gitleaks_text: str) -> list[str]:
    root = _root(tmp_path, gitleaks_text)
    m = next(x for x in members(root) if x.repo == "mgf-x")
    return [f"{i.kind}: {i.detail}" for i in check(m, load_policy(root), root).items]


# --- tolerated direction: additive allowlists pass -------------------------------


def test_additive_allowlist_entry_is_tolerated() -> None:
    assert _gitleaks_shape_drift(_ADDITIVE, _TEMPLATE) is None


def test_additive_passes_through_drift_check(tmp_path: Path) -> None:
    assert _check_items(tmp_path, _ADDITIVE) == []


def test_comment_only_difference_is_tolerated() -> None:
    """Shape, not bytes: comment/whitespace edits alone are no longer drift."""
    relabelled = _TEMPLATE.replace("# Canonical gitleaks config (SH-05).", "# local note")
    assert _gitleaks_shape_drift(relabelled, _TEMPLATE) is None


def test_additive_new_allowlist_list_key_is_tolerated() -> None:
    """A whole extra allowlist LIST key (e.g. regexes) is additive too."""
    extra = _ADDITIVE + "regexes = ['''EXAMPLE-[0-9]+''']\n"
    assert _gitleaks_shape_drift(extra, _TEMPLATE) is None


# --- failing direction: removals/edits of standard entries still fail ------------


def test_removed_standard_entry_still_fails() -> None:
    detail = _gitleaks_shape_drift(_DROPS_STANDARD, _TEMPLATE)
    assert detail is not None and "allowlist.paths" in detail
    assert "\\.claude/" in detail  # names what was dropped


def test_removed_standard_entry_flags_through_drift_check(tmp_path: Path) -> None:
    items = _check_items(tmp_path, _DROPS_STANDARD)
    assert any(i.startswith("exact:") and ".gitleaks.toml" in i for i in items)


def test_edited_standard_entry_still_fails() -> None:
    edited = _ADDITIVE.replace("'''\\.claude/'''", "'''\\.claude-EDITED/'''")
    detail = _gitleaks_shape_drift(edited, _TEMPLATE)
    assert detail is not None and "allowlist.paths" in detail


def test_non_allowlist_mutation_still_fails() -> None:
    # scanning behavior off: [extend] useDefault flipped.
    no_default = _ADDITIVE.replace("useDefault = true", "useDefault = false")
    assert _gitleaks_shape_drift(no_default, _TEMPLATE) == "[extend] differs from the template"
    # title edited.
    retitled = _ADDITIVE.replace('title = "mgf-common gitleaks config"', 'title = "mine"')
    assert _gitleaks_shape_drift(retitled, _TEMPLATE) == "[title] differs from the template"
    # a consumer ADDING scan rules is surfaced (only allowlists are additive).
    with_rules = _ADDITIVE + '\n[[rules]]\nid = "custom"\nregex = "x"\n'
    assert _gitleaks_shape_drift(with_rules, _TEMPLATE) == "[rules] differs from the template"


def test_allowlist_scalar_edit_still_fails() -> None:
    reworded = _ADDITIVE.replace(
        'description = "Files whose stated purpose is to embed secret-shaped strings."',
        'description = "whatever"',
    )
    assert _gitleaks_shape_drift(reworded, _TEMPLATE) == (
        "allowlist.description differs from the template"
    )


def test_unparseable_consumer_is_drift() -> None:
    detail = _gitleaks_shape_drift("title = 'unterminated\n[allowlist", _TEMPLATE)
    assert detail is not None and "unparseable" in detail


def test_unparseable_template_degrades_to_byte_exact() -> None:
    broken_template = "[allowlist\npaths = ["
    assert _gitleaks_shape_drift(broken_template, broken_template) is None
    assert _gitleaks_shape_drift(_ADDITIVE, broken_template) is not None


# --- scaffold mirrors the same shape rule (never clobber tolerated extras) -------


def test_scaffold_leaves_shape_conformant_gitleaks_untouched(
    tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
    root = _root(tmp_path, _ADDITIVE)
    rc = run_scaffold(["mgf-x"], root=root)
    assert rc == 0
    # the consumer's additive allowlist survived (no template clobber).
    assert (root / "mgf-x" / ".gitleaks.toml").read_text(encoding="utf-8") == _ADDITIVE
    assert "applied 0 byte-exact" in capsys.readouterr().out


def test_scaffold_restores_mutated_gitleaks_nondestructively(tmp_path: Path) -> None:
    """The mgf-fastapi clobber, fixed (Finding 1 Option A): a file missing a standard
    entry IS restored — but the consumer's additive carve-out is carried forward, not
    clobbered back to the bare template."""
    root = _root(tmp_path, _DROPS_STANDARD)
    rc = run_scaffold(["mgf-x"], root=root)
    assert rc == 0
    written = (root / "mgf-x" / ".gitleaks.toml").read_text(encoding="utf-8")
    assert written == _RESTORED
    # the standard entry is back AND the extra survived (the whole point).
    assert "'''\\.claude/'''" in written
    assert "'''tests/unit/test_webhooks\\.py'''" in written
    # and the restored file is now itself shape-conformant to the template.
    assert _gitleaks_shape_drift(written, _TEMPLATE) is None


def test_scaffold_restores_no_extra_mutation_to_plain_template(tmp_path: Path) -> None:
    """A dropped standard entry with no additive extra restores to the plain template
    (the non-destructive path collapses to the old behavior when there is nothing to
    carry forward)."""
    root = _root(tmp_path, _DROPS_STANDARD_NO_EXTRA)
    rc = run_scaffold(["mgf-x"], root=root)
    assert rc == 0
    assert (root / "mgf-x" / ".gitleaks.toml").read_text(encoding="utf-8") == _TEMPLATE


# --- gitleaks_restore_text: the non-destructive restore helper (Option A) ---------


def test_gitleaks_restore_readds_standard_and_keeps_extra() -> None:
    """Mirrors the mgf-fastapi event directly: MISSING the standard `\\.claude/` +
    CARRYING the extra `test_webhooks.py` → restore re-adds the standard entry AND
    retains the carve-out, and the merged file parses to both paths."""
    restored = gitleaks_restore_text(_DROPS_STANDARD, _TEMPLATE)
    assert restored == _RESTORED
    paths = tomllib.loads(restored)["allowlist"]["paths"]
    assert "\\.claude/" in paths  # standard entry re-added
    assert "tests/unit/test_webhooks\\.py" in paths  # consumer carve-out retained
    assert "tests/unit/test_redaction\\.py" in paths  # template entry intact


def test_gitleaks_restore_is_template_when_no_extras() -> None:
    """No additive extras → the template is returned byte-for-byte."""
    assert gitleaks_restore_text(_DROPS_STANDARD_NO_EXTRA, _TEMPLATE) == _TEMPLATE
    assert gitleaks_restore_text(_TEMPLATE, _TEMPLATE) == _TEMPLATE


def test_gitleaks_restore_unparseable_falls_back_to_template() -> None:
    """An unparseable consumer file must never corrupt the restore — fall back to the
    template unchanged (the safe, byte-exact canonical)."""
    assert gitleaks_restore_text("title = 'unterminated\n[allowlist", _TEMPLATE) == _TEMPLATE
