"""5b REGRESSION-SENSITIVITY CONTROLS -- the PRODUCT breaks, the TESTS are untouched.

SOURCE-ONLY. NOT RUN. No 5b producer is authorized.

5a moved the tests' INPUTS and proved each assertion executes. It did NOT show
these cells are sensitive to a broken product comparison -- that is this battery,
and doyle has kept the two apart since the beginning.

REUSES THE CORRECTED 5a RUNNER rather than starting a second framework: judge(),
git_ok(), diff_state(), verify_state(), preconditions(), restore() are imported
from controls_5a_v5, so every fix that battery earned (anchored summary parsing,
failing-set equality, nextest exit 100, narrowed compile predicate, exact-byte
restore, fail-closed git) applies here unchanged.

WHY THE MUTATIONS ARE SELECTIVE, which is doyle's ruling and the whole design:
`spec_satisfied_by` checks program, THEN profile, THEN remotes, each with an
early return. Restoring all three raw comparisons at once would make every cell
fail AT THE PROGRAM CHECK, and a red that never reached the profile or remote
code proves nothing about profile or remote sensitivity. So each mutation
restores exactly ONE pre-FOLD-4 comparison, and each is run ONLY against the
cells whose claim is that axis. The cells that would short-circuit are
deliberately NOT run under that mutation, and are named below so the omission is
a stated choice rather than a gap.

TEST BYTES ARE VERIFIED PINNED. Every mutation here edits PRODUCT code only. After
each one the `mod tests` region is compared byte-for-byte against the pinned blob
-- if a mutation ever touched a cell, the run stops. A regression control whose
test moved is not a regression control.
"""
import os
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)

from controls_5a_v5 import (  # noqa: E402  (path set above)
    F, MOD, PIN, RAW_DIR, git_ok, diff_state, judge, pinned_content,
    preconditions, restore, verify_state,
)

TESTS_MARKER = "#[cfg(test)]"

E1 = MOD + "an_unrestricted_program_spelled_any_satisfies_a_spec_wanting_none"
E2 = MOD + "a_profile_set_compares_by_membership_not_by_rendering"
E3 = MOD + "an_ipv4_network_compares_by_value_in_prefix_or_mask_form"
E4 = MOD + "the_captured_pair_decides_reconciled_once_spellings_compare_semantically"
H1 = MOD + "a_scope_that_differs_is_rejected_at_the_pair_verdict_not_only_at_the_token"
H2 = MOD + "an_unenforced_rule_is_refused_loudly_after_the_pair_matches"

# Each entry: ONE product edit, the cells whose OWN claim that edit should break,
# and the assertion text each must fail at. `skipped` records the cells that
# would fail for a SHORT-CIRCUIT reason under this mutation and are therefore not
# run -- stated, not silently omitted.
MUTATIONS = [
    {
        "label": "m1_program_raw",
        "what": "restore the pre-FOLD-4 program comparison: empty-string only, no `Any`",
        "anchor": "        None => program_unrestricted(&observed.program),",
        "replace": "        None => observed.program.is_empty(),",
        "cells": [
            (E1, "Program=Any is how NetSecurity spells"),
            (E4, "Ok(true)"),
        ],
        "skipped": "E2 and E3 are NOT run here: with the program axis broken they return false "
                   "before profile or remotes are consulted, so their reds would be short-circuits.",
    },
    {
        "label": "m2_profile_raw",
        "what": "restore the pre-FOLD-4 profile comparison: exact string, no set semantics",
        "anchor": "    if profile_set(&observed.profile) != profile_set(&want.profile) {",
        "replace": "    if observed.profile != want.profile {",
        "cells": [
            (E2, "one set in two renderings"),
            (E4, "Ok(true)"),
        ],
        "skipped": "E3 is NOT run here: the captured LAN profile differs raw, so E3 would fail at "
                   "the profile check before reaching its own remote claim.",
    },
    {
        "label": "m3_remote_raw",
        "what": "restore the pre-FOLD-4 remote comparison: lowercased sorted string set",
        "anchor": "        Some(wanted) => remote_set(&observed.remotes) == wanted,",
        "replace": ("        Some(_) => { let mut o: Vec<String> = observed.remotes.iter()"
                    ".map(|r| r.to_lowercase()).collect(); let mut w: Vec<String> = want.remotes"
                    ".iter().map(|r| r.to_lowercase()).collect(); o.sort(); w.sort(); o == w }"),
        "cells": [
            (E3, "mask form and prefix form name the same network"),
            (E4, "Ok(true)"),
        ],
        "skipped": "E1 and E2 are NOT run here: their axes are untouched by this mutation, so they "
                   "are expected to stay GREEN and are not evidence either way.",
    },
    {
        "label": "m4_remote_ignored",
        "what": "a normalizer that WIDENS: any remote satisfies the spec",
        "anchor": "        Some(wanted) => remote_set(&observed.remotes) == wanted,",
        "replace": "        Some(_) => true,",
        "cells": [
            (H1, "is a different scope from the derived prefix"),
        ],
        "skipped": "This is H1's mutation specifically: the comparison restorations above cannot "
                   "reach it, because a raw string compare still REJECTS a different scope.",
    },
    {
        "label": "m5_enforcement_disabled",
        "what": "remove decide()'s ActiveStore enforcement arm",
        "anchor": '        if rule.enforcement.as_slice() != ["Full"] {',
        "replace": "        if false {",
        "cells": [
            (H2, "the refusal names the enforcement arm"),
        ],
        "skipped": "H2's own mutation: enforcement is decided on a field the FOLD-4 normalizers "
                   "never see, so no comparison restoration can exercise it.",
    },
]


def tests_region(blob):
    """The `mod tests` region, as bytes. Product mutations must never move it."""
    i = blob.find(TESTS_MARKER.encode("utf-8"))
    if i < 0:
        raise SystemExit("cannot locate the test module marker")
    return blob[i:]


def assert_tests_pinned(label):
    now = tests_region(open(F, "rb").read()).replace(b"\r\n", b"\n")
    pinned = tests_region(pinned_content()).replace(b"\r\n", b"\n")
    if now != pinned:
        raise SystemExit("ABORT: %s changed the TEST region; a regression control whose test "
                         "moved is not a regression control" % label)
    print("      test region byte-identical to the pin: YES")


def apply_product_mutation(mut):
    src = open(F, "rb").read().decode("utf-8")
    tests_at = src.find(TESTS_MARKER)
    product = src[:tests_at]
    n = product.count(mut["anchor"])
    if n != 1:
        raise SystemExit("ANCHOR matched %d times in the PRODUCT region for %s, must be exactly 1"
                         % (n, mut["label"]))
    open(F, "wb").write((product.replace(mut["anchor"], mut["replace"]) + src[tests_at:]).encode("utf-8"))
    if diff_state() != 1:
        raise SystemExit("MUTATION produced no diff for " + mut["label"])


def main():
    os.makedirs(RAW_DIR, exist_ok=True)
    for mut in MUTATIONS:
        print("\n=== 5b %s -- %s" % (mut["label"], mut["what"]))
        print("    %s" % mut["skipped"])
        original = preconditions(mut["label"])
        results = []
        try:
            apply_product_mutation(mut)
            assert_tests_pinned(mut["label"])
            for test, expect in mut["cells"]:
                import subprocess
                r = subprocess.run(
                    'cargo nextest run -p spt-daemon --lib -E "test(/^%s$/)" '
                    "--build-jobs 2 --test-threads 2 --success-output immediate" % test,
                    shell=True, capture_output=True, text=True, errors="replace")
                raw = r.stdout + r.stderr
                path = os.path.join(RAW_DIR, "%s__%s.raw.txt" % (mut["label"], test.split("::")[-1]))
                open(path, "w", encoding="utf-8", errors="replace").write(raw)
                ok, checks = judge(raw, r.returncode, {"test": test, "expect": expect})
                print("    cell %s" % test.split("::")[-1])
                for k, v in checks.items():
                    print("      %-46s %s" % (k, "PASS" if v else "FAIL"))
                print("      raw: %s (exit %d)" % (path, r.returncode))
                results.append((test, ok))
        finally:
            restored = restore(mut["label"], original)
        if not restored:
            raise SystemExit("ABORT: tree not restored after " + mut["label"])
        for test, ok in results:
            if not ok:
                raise SystemExit("ABORT: %s did not make %s fail at its own assertion"
                                 % (mut["label"], test.split("::")[-1]))
        print("    VERDICT %s: every named cell went RED at its own assertion" % mut["label"])
    print("\n=== 5b complete: the cells are sensitive to a broken PRODUCT comparison, "
          "axis by axis, with the tests byte-identical to the pin throughout")


if __name__ == "__main__":
    if "--dry-run" in sys.argv:
        ok, _ = verify_state()
        print("tree at pin: %s" % ok)
        for mut in MUTATIONS:
            src = open(F, "rb").read().decode("utf-8")
            product = src[:src.find(TESTS_MARKER)]
            print("%-24s product-region anchor count = %d  cells: %s"
                  % (mut["label"], product.count(mut["anchor"]),
                     ", ".join(t.split("::")[-1] for t, _ in mut["cells"])))
    else:
        main()
