#!/usr/bin/env bash
#
# MUTATION CONTROL — break a guard, prove a test notices, put it back exactly.
#
# Round 717. This harness has existed for scores of rounds and lived in a
# scratchpad, which means it was never in the repository a human inherits and
# never itself under review. Committed now, with the two lessons the scratchpad
# version learned the hard way built in.
#
# THE COST THIS FIXES. Every mutant used to run the whole Unit suite: 67
# seconds, against about one second for the file that could actually catch it.
# Round 714 ran thirty-seven mutants in one round. ADR 0021 decided a graduated
# policy and then nothing enforced it, so the expensive default kept being
# taken by habit. Here the cheap run is the DEFAULT and the expensive one must
# be asked for by name.
#
#   bin/mutate.sh FILE OLD NEW --filter 'SomeTest'     narrowest suite (~1s)
#   bin/mutate.sh FILE OLD NEW --full                  whole Unit suite (~67s)
#   bin/mutate.sh FILE OLD NEW --full --suite Functional
#
# WHEN --full IS THE RIGHT ANSWER, AND IT GENUINELY IS SOMETIMES. Security,
# tenancy, safeguarding, compliance, audit and authentication guards keep the
# whole suite, because the FINDING there is usually "the entire suite is
# indifferent to this guard" — which is how INV-050, INV-126, INV-128 and
# INV-133 were all found. Narrowing those removes the finding, not the cost.
# For everything else the whole suite is a flat tax.
#
# TWO GUARDS THE SCRATCHPAD VERSION EARNED:
#
#  1. The restore is hash-verified. `git checkout` does NOT restore an
#     untracked file — it reports success and does nothing, which is how a
#     mutated rule survived into a later run in round 714. This copies the
#     snapshot back and compares sha256.
#  2. The verdict is the test runner's EXIT CODE, not text in its output. A run
#     that dies before printing prints no failure either.
#
# Exit 0 = mutant KILLED (a test failed, which is what you want).
# Exit 1 = mutant SURVIVED (nothing noticed) — or the mutation never applied.

set -uo pipefail
cd "$(dirname "$0")/.." || exit 1

FILE="${1:-}"; OLD="${2:-}"; NEW="${3:-}"
shift 3 2>/dev/null || { echo "usage: bin/mutate.sh FILE OLD NEW (--filter X | --full) [--suite S]"; exit 2; }

SUITE=Unit
FILTER=""
FULL=0
while [ $# -gt 0 ]; do
    case "$1" in
        --filter) FILTER="${2:-}"; shift 2 ;;
        --full)   FULL=1; shift ;;
        --suite)  SUITE="${2:-}"; shift 2 ;;
        *) echo "unknown option: $1"; exit 2 ;;
    esac
done

[ -f "$FILE" ] || { echo "no such file: $FILE"; exit 2; }
if [ "$FULL" -eq 0 ] && [ -z "$FILTER" ]; then
    echo "Refusing to run: pass --filter 'TheTest' for the narrowest suite, or --full to spend the whole"
    echo "suite deliberately. The expensive run is never the default (ADR 0021)."
    exit 2
fi

SNAP=$(mktemp)
cp "$FILE" "$SNAP"
BEFORE=$(sha256sum "$FILE" | cut -d' ' -f1)

restore() {
    cp "$SNAP" "$FILE"
    local after
    after=$(sha256sum "$FILE" | cut -d' ' -f1)
    rm -f "$SNAP"
    if [ "$BEFORE" != "$after" ]; then
        printf '\033[31mRESTORE FAILED\033[0m %s is not byte-identical to its pre-mutation state.\n' "$FILE"
        printf '  before %s\n  after  %s\nFix this before doing anything else.\n' "$BEFORE" "$after"
        exit 3
    fi
}
trap restore EXIT

# Apply the mutation through PHP so the needle is treated literally — sed would
# read regex metacharacters in real code ($this->, [], ?->) as syntax.
applied=$(OLD="$OLD" NEW="$NEW" php -r '
    $f = $argv[1];
    $s = file_get_contents($f);
    $old = getenv("OLD"); $new = getenv("NEW");
    $n = substr_count($s, $old);
    if ($n === 0) { echo "0"; exit; }
    file_put_contents($f, str_replace($old, $new, $s));
    echo $n;
' "$FILE")

if [ "$applied" = "0" ]; then
    printf '\033[31mNOT APPLIED\033[0m the OLD text does not appear in %s — nothing was mutated, so a\n' "$FILE"
    printf 'green result below would mean nothing. Check the snippet, including whitespace.\n'
    exit 1
fi

if [ "$FULL" -eq 1 ]; then
    printf '== mutant applied (%s occurrence(s)); running the WHOLE %s suite\n' "$applied" "$SUITE"
    vendor/bin/codecept run "$SUITE" > /tmp/mutate.log 2>&1
else
    printf '== mutant applied (%s occurrence(s)); running %s --filter %s\n' "$applied" "$SUITE" "$FILTER"
    vendor/bin/codecept run "$SUITE" --filter "$FILTER" > /tmp/mutate.log 2>&1
fi
verdict=$?

# ROUND 719: A RUN THAT EXECUTED NOTHING IS NOT A SURVIVAL.
#
# Codeception exits 0 on "No tests executed!", and round 717 built this
# harness to take its verdict from the exit code precisely because output text
# is unreliable — so a filter that matched no test read as SURVIVED. That is
# the worst possible answer: it names a real problem (a guard nothing covers)
# that does not exist, and the reader goes looking for the missing test they
# already wrote. Codeception filters match the METHOD name, not the humanised
# title the runner prints, which is exactly how the mismatch happens.
#
# Same shape as `gates.sh`'s "exited 0, no parseable result" check, and the
# same lesson: a tool that cannot tell success from silence must report
# neither.
if grep -q 'No tests executed!' /tmp/mutate.log; then
    printf '\033[33mNO TESTS RAN\033[0m the filter matched nothing, so this says nothing about the mutant.\n'
    printf 'Codeception matches the METHOD name (movingAWorkspaceRecordsIt), not the printed title\n'
    printf '("Moving a workspace records it"). Check the filter against the method.\n'
    exit 2
fi

if [ "$verdict" -ne 0 ]; then
    printf '\033[32mKILLED\033[0m  a test failed, as it should. Failing tests:\n'
    grep -aE '^[0-9]+\) ' /tmp/mutate.log | head -8
    exit 0
fi

printf '\033[31mSURVIVED\033[0m nothing failed. Either the tests do not cover this guard, or the mutant\n'
printf 'cannot reach the behaviour — and those are different findings. Record which.\n'
grep -aE 'OK \(' /tmp/mutate.log | head -2
exit 1
