"""SC-* negative-pattern pins (TS-10) + the standing negatives that prove they bite (TS-29).

A grep-style guard over ``src/mgf/common`` so a future change cannot
reintroduce a forbidden-by-SECURITY.md pattern (shell injection, unsafe
deserialisation, weak crypto) without a test going red. This is the
"forbidden pattern absent in src" half of TS-10 / SECURITY.md §15; the
positive behaviour (redaction, constant-time compares, KDF) is pinned by
the vault + redaction suites.

**Why the fixtures at the tail exist.** A pin that asserts an *absence* looks
identical whether the regex works or the tree is simply clean — a green run
distinguishes the two not at all. So each pattern is also run against a
``tmp_path`` tree with the defect deliberately planted in it, through the
**same** helper the pin uses. A dead pattern then fails here instead of
passing quietly there. That is not hypothetical: it is how PAPER-149 found
that ``yaml\\.load\\s*\\(`` could never reach ``yaml.load_all(`` and that the
SC-05 arm had no ``hashlib.new(...)`` case at all.
"""

from __future__ import annotations

import re
from pathlib import Path

import pytest

_PKG = Path(__file__).resolve().parents[3] / "src" / "mgf" / "common"
_SRC_ROOT = _PKG.parents[2]  # the `src` dir, for tidy relative paths

# The forbidden-pattern regexes live here rather than inline in the test
# bodies, so the planted-defect fixtures at the bottom exercise the SAME
# expression the live sweep runs. A fixture written against a hand-copied
# regex proves nothing about the gate.
_SHELL_TRUE_PATTERN = r"shell\s*=\s*True"
_OS_SYSTEM_PATTERN = r"\bos\.system\s*\("
# `load_all` is inside the rule, and was outside the regex until 2026-08-06
# (PAPER-149): `_all` sits between `load` and the paren, so `yaml\.load\s*\(`
# could not reach `yaml.load_all(stream)` — the same CWE-502 execution vector
# SC-03 exists to stop. Note what is deliberately NOT added: a `Loader=`
# escape hatch. This package's rule is stricter than the corpus minimum —
# `yaml.safe_load` / `yaml.safe_load_all` are the only allowed forms, and
# they are spared by construction (neither matches `yaml\.load`), not by a
# filter that could later be relaxed.
_YAML_LOAD_PATTERN = r"yaml\.(load|load_all)\s*\("
_PICKLE_PATTERN = r"\bpickle\.(load|loads)\s*\("
_EVAL_PATTERN = r"(?<![.\w])eval\s*\("
_EXEC_PATTERN = r"(?<![.\w])exec\s*\("
# The second arm is the one PAPER-149 found missing entirely: `hashlib.new(
# "md5", payload)` is the standard spelling of a *dynamic* algorithm choice,
# which is precisely the shape most likely to end up carrying "md5" from a
# config value. It matches bare `new(` rather than `hashlib.new(` so that
# `from hashlib import new` is covered too. The `\b` anchor is load-bearing
# in both arms: it is what keeps `sha256` / `sha512` / `md5sum_helper` out of
# the findings.
_WEAK_HASH_PATTERN = r"hashlib\.(md5|sha1)\b|new\(['\"]?(md5|sha1)\b"
_WEAK_HASH_NOQA = "S324"


def _src_files(root: Path) -> list[Path]:
    """Every ``*.py`` under ``root``, ``__pycache__`` excluded.

    ``root`` is required and has no default, deliberately. A defaulted root
    is how a planted-defect fixture forgets to pass its ``tmp_path`` tree and
    sweeps production source instead — where the planted pattern finds
    nothing and the fixture passes for exactly the wrong reason, which is the
    failure mode this file exists to make impossible.
    """
    return [p for p in root.rglob("*.py") if "__pycache__" not in p.parts]


def _display(path: Path) -> str:
    """``src/``-relative for production findings; absolute for a fixture tree."""
    try:
        return str(path.relative_to(_SRC_ROOT))
    except ValueError:
        return str(path)


def _grep(pattern: str, *, root: Path, allow_noqa: str | None = None) -> list[str]:
    """Lines under ``root`` matching ``pattern``. Lines carrying a
    ``# noqa: <allow_noqa>`` suppression (an intentional, reviewed use) are
    excluded."""
    rx = re.compile(pattern)
    hits: list[str] = []
    for p in _src_files(root):
        for i, line in enumerate(p.read_text(encoding="utf-8").splitlines(), 1):
            if not rx.search(line):
                continue
            if allow_noqa and f"noqa: {allow_noqa}" in line:
                continue
            hits.append(f"{_display(p)}:{i}")
    return hits


class TestNoShellInjection:
    def test_no_shell_true(self) -> None:  # SC-02
        assert _grep(_SHELL_TRUE_PATTERN, root=_PKG) == []

    def test_no_os_system(self) -> None:  # SC-02
        assert _grep(_OS_SYSTEM_PATTERN, root=_PKG) == []


class TestSafeDeserialisation:
    def test_yaml_safe_load_only(self) -> None:  # SC-03
        # `yaml.load(` and `yaml.load_all(` are forbidden; `yaml.safe_load(` /
        # `yaml.safe_load_all(` are the only allowed forms.
        assert _grep(_YAML_LOAD_PATTERN, root=_PKG) == []

    def test_no_pickle(self) -> None:  # SC-03
        assert _grep(_PICKLE_PATTERN, root=_PKG) == []

    def test_no_eval_exec(self) -> None:  # SC-03
        assert _grep(_EVAL_PATTERN, root=_PKG) == []
        assert _grep(_EXEC_PATTERN, root=_PKG) == []


class TestCryptoChoices:
    def test_no_weak_crypto_hash(self) -> None:  # SC-05
        # md5/sha1 are forbidden for SECURITY purposes — by whichever
        # constructor spells them, `hashlib.md5(...)` or `hashlib.new("md5")`.
        # A non-crypto use (e.g. a filesystem id) must carry an explicit S324
        # ruff waiver.
        offenders = _grep(_WEAK_HASH_PATTERN, root=_PKG, allow_noqa=_WEAK_HASH_NOQA)
        assert offenders == [], f"weak crypto hash without S324 waiver: {offenders}"


# ---------------------------------------------------------------------------
# Standing negative fixtures (TS-29)
#
# Every pin above is silent until a forbidden pattern lands in `src/`, and from
# the outside that is indistinguishable from a sweep whose regex, whose file
# walk, or whose root stopped matching anything at all. The fixtures below
# plant each defect class into a `tmp_path` tree and run the SAME `_grep` the
# pins run, so a dead pattern fails HERE instead of passing quietly THERE.
#
# TS-29's own close-out sweep for this repo is still pending
# (`docs/inprogress/MGF_STANDARDS_CONFORMANCE.md`, TS-29 row). These are its
# first rows arriving early, contributed with PAPER-149 by the consumer whose
# descendant of this sweep is where the two dead arms were found.
# ---------------------------------------------------------------------------

_PLANTED_DEFECTS: tuple[tuple[str, str, str], ...] = (
    ("shell_true", _SHELL_TRUE_PATTERN, "subprocess.run(cmd, shell=True)"),
    ("os_system", _OS_SYSTEM_PATTERN, 'os.system("rm -rf /tmp/x")'),
    ("yaml_load", _YAML_LOAD_PATTERN, "cfg = yaml.load(handle)"),
    ("yaml_load_all", _YAML_LOAD_PATTERN, "docs = list(yaml.load_all(stream))"),
    ("pickle_load", _PICKLE_PATTERN, "obj = pickle.load(handle)"),
    ("pickle_loads", _PICKLE_PATTERN, "obj = pickle.loads(payload)"),
    ("eval_call", _EVAL_PATTERN, "value = eval(user_input)"),
    ("exec_call", _EXEC_PATTERN, "exec(user_input)"),
    ("hashlib_md5", _WEAK_HASH_PATTERN, "digest = hashlib.md5(payload).hexdigest()"),
    ("hashlib_sha1", _WEAK_HASH_PATTERN, "digest = hashlib.sha1(payload).hexdigest()"),
    ("hashlib_new_md5", _WEAK_HASH_PATTERN, 'digest = hashlib.new("md5", payload).hexdigest()'),
    ("hashlib_new_sha1", _WEAK_HASH_PATTERN, "digest = hashlib.new('sha1').hexdigest()"),
)

# Shapes the sweep MUST leave alone. A pin that cries wolf on the sanctioned
# form gets suppressed rather than fixed, so the false-positive half is pinned
# with the same weight as the false-negative half.
_SPARED_SHAPES: tuple[tuple[str, str, str], ...] = (
    ("yaml_safe_load", _YAML_LOAD_PATTERN, "cfg = yaml.safe_load(handle)"),
    ("yaml_safe_load_all", _YAML_LOAD_PATTERN, "docs = list(yaml.safe_load_all(stream))"),
    ("hashlib_sha256", _WEAK_HASH_PATTERN, "digest = hashlib.sha256(payload).hexdigest()"),
    ("hashlib_sha512", _WEAK_HASH_PATTERN, "digest = hashlib.sha512(payload).hexdigest()"),
    ("hashlib_new_sha256", _WEAK_HASH_PATTERN, 'digest = hashlib.new("sha256").hexdigest()'),
    # `\b` is what spares this one: a helper whose NAME merely starts with the
    # algorithm string is not a weak-hash construction.
    ("md5_named_helper", _WEAK_HASH_PATTERN, 'digest = hashlib.new("md5sum_helper")'),
    # `(?<![.\\w])` is what spares these two: a method named `eval` / `exec` on
    # some object is not the builtin.
    ("method_named_eval", _EVAL_PATTERN, "result = engine.eval(expression)"),
    ("method_named_exec", _EXEC_PATTERN, "cursor.exec(statement)"),
)


def _plant(root: Path, name: str, line: str) -> Path:
    """Write a one-line module carrying ``line`` under a package-shaped tree."""
    path = root / "mgf" / "common" / f"{name}.py"
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(f'"""Planted defect — {name}."""\n{line}\n', encoding="utf-8")
    return path


@pytest.mark.parametrize(
    ("name", "pattern", "planted_line"),
    _PLANTED_DEFECTS,
    ids=[name for name, _, _ in _PLANTED_DEFECTS],
)
def test_sweep_catches_a_planted_forbidden_pattern(
    name: str, pattern: str, planted_line: str, tmp_path: Path
) -> None:
    """Each SC-* pattern still bites when the shape it forbids is present."""
    planted = _plant(tmp_path / "src", name, planted_line)

    findings = _grep(pattern, root=tmp_path / "src")

    assert findings, (
        f"the SC-* sweep found nothing after `{planted_line}` was planted in "
        f"{planted} — pattern {pattern!r} is dead for this shape, and the pin "
        f"that uses it is passing vacuously."
    )
    report = "\n".join(findings)
    assert str(planted) in report, f"finding does not name the offending file:\n{report}"


@pytest.mark.parametrize(
    ("name", "pattern", "spared_line"),
    _SPARED_SHAPES,
    ids=[name for name, _, _ in _SPARED_SHAPES],
)
def test_sweep_spares_the_sanctioned_shape(
    name: str, pattern: str, spared_line: str, tmp_path: Path
) -> None:
    """The safe form of each rule must NOT be reported."""
    spared = _plant(tmp_path / "src", name, spared_line)

    findings = _grep(pattern, root=tmp_path / "src")

    assert not findings, (
        f"the SC-* sweep flagged the SANCTIONED shape `{spared_line}` in "
        f"{spared} — pattern {pattern!r} is over-broad, and a pin that cries "
        f"wolf on the approved form gets suppressed, not fixed.\n"
        f"findings:\n" + "\n".join(findings)
    )


def test_weak_hash_waiver_is_honoured_and_scoped(tmp_path: Path) -> None:
    """The S324 escape hatch spares a reviewed use — and only a reviewed one."""
    root = tmp_path / "src"
    waived = _plant(root, "waived_md5", "cache_key = hashlib.md5(blob).hexdigest()  # noqa: S324")
    bare = _plant(root, "bare_md5", "digest = hashlib.md5(secret).hexdigest()")

    findings = _grep(_WEAK_HASH_PATTERN, root=root, allow_noqa=_WEAK_HASH_NOQA)

    report = "\n".join(findings)
    assert str(waived) not in report, (
        f"the reviewed `# noqa: {_WEAK_HASH_NOQA}` use was reported anyway; the "
        f"waiver is the documented escape hatch for a non-crypto hash.\n{report}"
    )
    assert str(bare) in report, (
        f"the UNwaived use was not reported — `allow_noqa` is swallowing "
        f"everything, so the waiver is not an exception but a bypass.\n{report}"
    )


def test_sweep_actually_reaches_production_source() -> None:
    """Vacuity pin: every assertion above is trivially true if the walk sees no files.

    A moved package, a broken ``rglob``, or a root that stopped resolving would
    leave every pin in this file green while enforcing nothing.
    """
    assert _PKG.is_dir(), f"production package tree missing at {_PKG}"
    modules = _src_files(_PKG)
    assert modules, f"the SC-* sweep walked zero modules under {_PKG}"
    census = _grep(r"^\s*(import|from)\s", root=_PKG)
    assert census, (
        f"the SC-* sweep matched zero lines across {len(modules)} modules under "
        f"{_PKG} — the census it enforces against is empty, so every pin above "
        f"is passing on nothing."
    )
