#!/usr/bin/env python3
"""Gate-side mutation cycles for the #234 respin (doyle), at 27a14c1c.

Each cycle: apply ONE mutation (exact-anchor, count==1 asserted), print the
mutated region, run the 4-cell leg, record which cells red/passed, restore via
git checkout (baseline IS committed) and assert byte-identity against HEAD.
Verdicts land in G234M_<cycle>.exit / .log / .verdict; this script's own exit
status is not a verdict (exit-after-capture rule).
"""
import hashlib
import subprocess
import sys
from pathlib import Path

ROOT = Path(__file__).parent
SHA = "27a14c1cdd379598d57f7545a6d55fa3a2ea7ba4"

CELLS = {
    "commune": "a_consumed_commune_reaches_the_log_and_polls_back",
    "commune_fail": "a_failed_commune_ingest_polls_back_as_its_own_kind",
    "msg_in": "the_delivery_edge_polls_back_as_msg_in_on_the_receiver",
    "msg_out": "the_send_edge_polls_back_as_msg_out_on_the_sender",
}

BUS_LINE = "    let bus = crate::iobus::default_bus(&owlery, now_ms());\n"
BUS_REVERT = (
    "    let mut bus = crate::iobus::IoBus::new();\n"
    "    bus.register(Box::new(crate::iobus::ShellLinkSink::new(&owlery)));\n"
)
FAIL_KIND_ANCHOR = (
    "                            publish_commune_io(\n"
    "                                &self.id,\n"
    "                                spt_proto::ioevent::IO_KIND_COMMUNE_FAIL,\n"
)
FAIL_KIND_MUT = FAIL_KIND_ANCHOR.replace("IO_KIND_COMMUNE_FAIL", "IO_KIND_COMMUNE")
MSG_IN_LINE = "        publish_msg_io_local(id, spt_proto::ioevent::IO_KIND_MSG_IN, &m.from, &m.body);\n"
MSG_IN_MUT = "        if false { publish_msg_io_local(id, spt_proto::ioevent::IO_KIND_MSG_IN, &m.from, &m.body); }\n"
MSG_OUT_LINE = "        publish_msg_io(owner, spt_proto::ioevent::IO_KIND_MSG_OUT, &target, body);\n"
MSG_OUT_MUT = "        if false { publish_msg_io(owner, spt_proto::ioevent::IO_KIND_MSG_OUT, &target, body); }\n"

# cycle -> (file, old, new, cells expected RED)
CYCLES = {
    "M1_bus_revert": ("crates/spt-daemon/src/lifecycle.rs", BUS_LINE, BUS_REVERT,
                      {"commune", "commune_fail"}),
    "M2_fail_kind_swap": ("crates/spt-daemon/src/lifecycle.rs", FAIL_KIND_ANCHOR, FAIL_KIND_MUT,
                          {"commune_fail"}),
    "M3_msg_out_suppress": ("crates/spt/src/cli.rs", MSG_OUT_LINE, MSG_OUT_MUT,
                            {"msg_out"}),
    "M4_msg_in_suppress": ("crates/spt/src/api/delivery.rs", MSG_IN_LINE, MSG_IN_MUT,
                           {"msg_in"}),
}


def sh(*args, **kw):
    return subprocess.run(args, cwd=ROOT, capture_output=True, text=True, **kw)


def blob_sha(rel):
    return hashlib.sha256((ROOT / rel).read_bytes()).hexdigest()


def committed_sha(rel):
    out = sh("git", "show", f"{SHA}:{rel}")
    return hashlib.sha256(out.stdout.encode() if isinstance(out.stdout, str) else out.stdout).hexdigest()


def run_cycle(name, rel, old, new, expect_red):
    log = ROOT / f"G234M_{name}.log"
    lines = [f"=== CYCLE {name} on {rel} ==="]
    raw = (ROOT / rel).read_bytes()
    crlf = b"\r\n" in raw
    src = raw.decode("utf-8").replace("\r\n", "\n")
    count = src.count(old)
    if count != 1:
        lines.append(f"REFUSED: anchor count {count} != 1, nothing applied")
        log.write_text("\n".join(lines), encoding="utf-8")
        return f"{name}: REFUSED anchor_count={count}"
    mutated = src.replace(old, new, 1)
    out_bytes = (mutated.replace("\n", "\r\n") if crlf else mutated).encode("utf-8")
    (ROOT / rel).write_bytes(out_bytes)
    idx = mutated.index(new)
    lines.append("MUTATION LANDED, region:")
    lines.append(mutated[max(0, idx - 120): idx + len(new) + 120])
    r = sh("bash", "-c",
           "CARGO_INCREMENTAL=0 cargo nextest run -p spt --test io_events_undriven_kinds_e2e "
           "--test-threads 1 --no-fail-fast 2>&1")
    lines.append(r.stdout)
    reds, greens = set(), set()
    for key, cell in CELLS.items():
        for ln in r.stdout.splitlines():
            if cell in ln and "FAIL" in ln:
                reds.add(key)
            elif cell in ln and "PASS" in ln:
                greens.add(key)
    # restore (baseline committed at SHA — the precondition that makes checkout safe)
    sh("git", "checkout", "--", rel)
    ident = blob_sha(rel) == committed_sha(rel)
    verdict = (f"{name}: red={sorted(reds)} green={sorted(greens)} "
               f"expected_red={sorted(expect_red)} "
               f"selective={'YES' if reds == expect_red and greens == set(CELLS) - expect_red else 'NO'} "
               f"restore_byte_identical={'YES' if ident else 'NO'}")
    lines.append(verdict)
    log.write_text("\n".join(lines), encoding="utf-8")
    return verdict


def main():
    verdicts = []
    for name, (rel, old, new, expect) in CYCLES.items():
        verdicts.append(run_cycle(name, rel, old, new, expect))
    clean = sh("git", "status", "--porcelain")
    tracked_dirty = [l for l in clean.stdout.splitlines() if not l.startswith("??")]
    verdicts.append(f"tree_clean_of_tracked_changes={'YES' if not tracked_dirty else tracked_dirty}")
    (ROOT / "G234M_table.txt").write_text("\n".join(verdicts), encoding="utf-8")
    print("\n".join(verdicts))


if __name__ == "__main__":
    main()
