#!/usr/bin/env bash
# microtest-wake-assert.sh — #973 instrument self-test. Run BEFORE trusting any
# validate-run evidence: it proves the counted ledger and the abort mechanics on
# two generated mini-suites, so a defect in the instrument cannot silently wear
# the colour of a clean validation.
#
# What it proves (each check named C1..C8 below):
#   C1  green run: ledger set EQUALS a text-derived expected set spanning TWO
#       files (file-field discrimination), row count > 1, both sentinels emitted,
#       exit 0. Also pins the BASH_LINENO convention for backslash-continuation
#       call sites against the first-physical-line convention the denominator
#       artifact uses.
#   C2  early-exit truncation: a suite that exits before its later site yields a
#       SHORT ledger, and the expected-set comparison catches it — a counted
#       ledger must report its own truncation, never a smaller total.
#   C3  abort from inside a `( ... )` test subshell kills the WHOLE suite: no
#       sentinel, non-zero exit, loud named reason (file:line + raw rc).
#   C4  abort stays loud at a call site that appends 2>/dev/null (the preimage
#       canary shape) — the saved-fd path.
#   C5  abort escapes a `$( count_lines ... )` substitution (A6 shape): the
#       count from a failed measurement is never compared and the suite dies.
#   C6  abort escapes a pipeline tail (`printf | has_match`).
#   C7  count_lines prints 0 on grep rc 1 (zero matches is a measurement, not an
#       error) — implicit in C1's green run via the delta-count site.
#   C8  an env-prefix on the helper (`LC_ALL=C has_match ...`) reaches the grep
#       child — pins the conversion shape for the digest-hmac LC_ALL site.
#   C9  an arm that matches NO site is loud about it by omission: green run,
#       sentinel present, and NO "WAKE-ASSERT ARMED" line — so "did not abort"
#       is separable into arm-never-matched (no ARMED line) vs error-path-
#       broken (ARMED line, no abort). C3..C6 require the ARMED line AND the
#       aborting site's ledger row (append lands BEFORE the grep runs, so an
#       abort can never shorten the count it is part of).
#   C10 the BASH_LINENO pin's abort arm fires: under a probe interpreter that
#       misreports the continuation line, wake_assert_init aborts loudly and
#       nothing past init executes — a pin whose failure arm was never seen
#       firing is an undertaking, not a control.
#   C11 the FAILED-summary template executes on the red path: a mini-suite
#       driven deterministically red emits the exact converted summary shape
#       (`FAILED ($(count_lines . "$FAILFILE") assertion(s))`) with the right
#       count, exits 1, and the summary site's ledger row lands. The nine
#       real-suite summary sites are structurally unreachable in a green run
#       (guarded by [ -s "$FAILFILE" ]); their dispositions cite THIS check as
#       the measured execution of the same template, so "unexecuted in the
#       green run" never silently means "never executed anywhere".
set -uo pipefail

HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export WAKE_COMMON="$HERE/../_wake-common.sh"
[ -f "$WAKE_COMMON" ] || {
  echo "microtest: _wake-common.sh not found at $WAKE_COMMON" >&2
  exit 1
}

TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT

fails=0
check() { # check NAME COND-DESCRIPTION (pass/fail already decided by caller: $1=name $2=0|1 $3=detail)
  if [ "$2" -eq 0 ]; then
    echo "  PASS $1"
  else
    echo "  FAIL $1 — $3"
    fails=$((fails + 1))
  fi
}

# --- fixture data ----------------------------------------------------------
printf 'alpha\nbeta\nbeta\ngamma-unused\n' >"$TMP/data.txt"

# --- mini-suite A: six helper sites across every converted form ------------
cat >"$TMP/mini-a.sh" <<'MINI_A'
#!/usr/bin/env bash
set -uo pipefail
. "$WAKE_COMMON"
wake_assert_init
TMP="$1"
FAILFILE="$TMP/failures-a"
: >"$FAILFILE"
fail_msg() { echo "  FAIL: $*" >&2; echo x >>"$FAILFILE"; }
ok() { :; }
(
  has_match -q alpha "$TMP/data.txt" || fail_msg "alpha missing" # SITE:or-subshell
) && ok
(
  has_match -q FORBIDDEN "$TMP/data.txt" 2>/dev/null && fail_msg "forbidden present" # SITE:and-swallow
) && ok
(
  [ "$(count_lines beta "$TMP/data.txt")" = "2" ] || fail_msg "beta count" # SITE:count-capture
) && ok
(
  printf 'gamma\n' | has_match -q gamma || fail_msg "gamma pipeline" # SITE:pipeline
) && ok
(
  has_match -q \
    alpha "$TMP/data.txt" || fail_msg "continuation" # SITE:continuation
) && ok
(
  [ "$(count_lines delta "$TMP/data.txt")" = "0" ] || fail_msg "delta zero" # SITE:count-zero
) && ok
if [ -s "$FAILFILE" ]; then
  echo "mini-a: FAILED" >&2
  exit 1
fi
echo "mini-a: OK" >&2
MINI_A

# --- mini-suite B: second file, one site behind an early exit --------------
cat >"$TMP/mini-b.sh" <<'MINI_B'
#!/usr/bin/env bash
set -uo pipefail
. "$WAKE_COMMON"
wake_assert_init
TMP="$1"
(
  has_match -q alpha "$TMP/data.txt" || echo "b1 missing" >&2 # SITE:b-first
)
if [ "${MINI_B_EARLY_EXIT:-}" = "1" ]; then
  exit 0
fi
(
  has_match -q beta "$TMP/data.txt" || echo "b2 missing" >&2 # SITE:b-second
)
echo "mini-b: OK" >&2
MINI_B
chmod +x "$TMP/mini-a.sh" "$TMP/mini-b.sh"

# Text-derived expected set: helper-name + basename:line for every SITE-marked
# call, taken from the generated files' TEXT (independent of BASH_LINENO), with
# the continuation site expected at its FIRST physical line — the denominator
# artifact's convention.
expected_set() { # expected_set FILE
  local f="$1" base
  base="$(basename "$f")"
  awk '
    /# SITE:/ {
      line = NR
      if ($0 !~ /has_match|count_lines/) line = NR - 1  # marker on the continuation tail
      print line
    }
  ' "$f" | while read -r ln; do
    txt="$(sed -n "${ln}p" "$f")"
    case "$txt" in
    *count_lines*) printf 'count_lines %s:%s\n' "$base" "$ln" ;;
    *) printf 'has_match %s:%s\n' "$base" "$ln" ;;
    esac
  done
}

site_line() { # site_line FILE MARKER -> first physical line of that call
  local f="$1" marker="$2" ln
  ln="$(grep -n "# SITE:${marker}\$" "$f" | cut -d: -f1)"
  # continuation marker sits on the tail line; the call starts one line up
  if ! sed -n "${ln}p" "$f" | grep -Eq 'has_match|count_lines'; then
    ln=$((ln - 1))
  fi
  printf '%s' "$ln"
}

# --- C1: green run, two files, set equality --------------------------------
LEDGER="$TMP/ledger-c1"
: >"$LEDGER"
outA="$(WAKE_ASSERT_LEDGER="$LEDGER" bash "$TMP/mini-a.sh" "$TMP" 2>&1)"
rcA=$?
outB="$(WAKE_ASSERT_LEDGER="$LEDGER" bash "$TMP/mini-b.sh" "$TMP" 2>&1)"
rcB=$?
{ expected_set "$TMP/mini-a.sh"; expected_set "$TMP/mini-b.sh"; } | sort >"$TMP/expected-c1"
sort "$LEDGER" >"$TMP/got-c1"
n_expected="$(grep -c . "$TMP/expected-c1")"
if [ "$rcA" -eq 0 ] && [ "$rcB" -eq 0 ] &&
  printf '%s' "$outA" | grep -q 'mini-a: OK' &&
  printf '%s' "$outB" | grep -q 'mini-b: OK' &&
  [ "$n_expected" -gt 1 ] &&
  cmp -s "$TMP/expected-c1" "$TMP/got-c1"; then
  check C1 0 ""
else
  check C1 1 "rcA=$rcA rcB=$rcB expected($n_expected)/got diff: $(diff "$TMP/expected-c1" "$TMP/got-c1" 2>&1 | head -n 10 | tr '\n' ' ')"
fi

# --- C2: early exit -> short ledger, comparison catches it -----------------
LEDGER="$TMP/ledger-c2"
: >"$LEDGER"
WAKE_ASSERT_LEDGER="$LEDGER" MINI_B_EARLY_EXIT=1 bash "$TMP/mini-b.sh" "$TMP" >/dev/null 2>&1
expected_set "$TMP/mini-b.sh" | sort >"$TMP/expected-c2"
sort "$LEDGER" >"$TMP/got-c2"
if ! cmp -s "$TMP/expected-c2" "$TMP/got-c2" &&
  grep -q "has_match mini-b.sh:$(site_line "$TMP/mini-b.sh" b-first)" "$TMP/got-c2" &&
  ! grep -q "mini-b.sh:$(site_line "$TMP/mini-b.sh" b-second)" "$TMP/got-c2"; then
  check C2 0 ""
else
  check C2 1 "truncated ledger was not detected as short"
fi

# --- C3..C6: per-shape abort proofs ----------------------------------------
abort_case() { # abort_case NAME MARKER HELPER
  local name="$1" marker="$2" helper="$3" ln site out rc ledger
  ln="$(site_line "$TMP/mini-a.sh" "$marker")"
  site="mini-a.sh:${ln}"
  ledger="$TMP/ledger-${name}"
  : >"$ledger"
  out="$(WAKE_ASSERT_LEDGER="$ledger" WAKE_ASSERT_FORCE_GREP_ERROR_AT="$site" \
    bash "$TMP/mini-a.sh" "$TMP" 2>&1)"
  rc=$?
  if [ "$rc" -ne 0 ] &&
    ! printf '%s' "$out" | grep -q 'mini-a: OK' &&
    ! printf '%s' "$out" | grep -q 'mini-a: FAILED' &&
    printf '%s' "$out" | grep -q "WAKE-ASSERT ARMED: forcing real grep error at $site" &&
    printf '%s' "$out" | grep -q "WAKE-ASSERT ABORT" &&
    printf '%s' "$out" | grep -q "$site" &&
    printf '%s' "$out" | grep -q "grep exit 2" &&
    grep -q "^${helper} ${site}\$" "$ledger"; then
    check "$name" 0 ""
  else
    check "$name" 1 "rc=$rc site=$site ledger=$(grep -c . "$ledger") out=$(printf '%s' "$out" | tail -n 3 | tr '\n' ' ')"
  fi
}
abort_case C3 or-subshell has_match
abort_case C4 and-swallow has_match
abort_case C5 count-capture count_lines
abort_case C6 pipeline has_match

# --- C7: covered by C1 (delta-count site prints 0 on grep rc 1) ------------
check C7 0 ""

# --- C8: env-prefix on a function reaches the grep child -------------------
envprobe() { command env | command grep -c '^LC_ALL=xx_wake_test$'; }
got="$(LC_ALL=xx_wake_test envprobe 2>/dev/null)" # bash's setlocale warning about the fake locale is itself proof the prefix landed
if [ "$got" = "1" ]; then check C8 0 ""; else check C8 1 "env-prefix did not reach child (got=$got)"; fi

# --- C9: arm matching NO site -> green run, no ARMED line ------------------
out="$(WAKE_ASSERT_FORCE_GREP_ERROR_AT="mini-a.sh:9999" bash "$TMP/mini-a.sh" "$TMP" 2>&1)"
rc=$?
if [ "$rc" -eq 0 ] &&
  printf '%s' "$out" | grep -q 'mini-a: OK' &&
  ! printf '%s' "$out" | grep -q 'WAKE-ASSERT ARMED'; then
  check C9 0 ""
else
  check C9 1 "rc=$rc out=$(printf '%s' "$out" | tail -n 3 | tr '\n' ' ')"
fi

# --- C10: lineno pin aborts under an interpreter that breaks the convention -
cat >"$TMP/fake-bash" <<'FAKE'
#!/usr/bin/env bash
# stand-in for a bash whose BASH_LINENO convention differs: misreports the
# continuation call one line low (the exact skew the pin exists to catch)
printf '3\n5\n'
FAKE
chmod +x "$TMP/fake-bash"
out="$(WAKE_ASSERT_PIN_BASH="$TMP/fake-bash" bash -c '. "$WAKE_COMMON" && wake_assert_init && echo REACHED-PAST-INIT' 2>&1)"
rc=$?
if [ "$rc" -ne 0 ] &&
  ! printf '%s' "$out" | grep -q 'REACHED-PAST-INIT' &&
  printf '%s' "$out" | grep -q 'WAKE-ASSERT INIT ABORT: BASH_LINENO convention violated'; then
  check C10 0 ""
else
  check C10 1 "rc=$rc out=$(printf '%s' "$out" | tail -n 2 | tr '\n' ' ')"
fi

# --- C11: red path executes the converted FAILED-summary template -----------
cat >"$TMP/mini-c.sh" <<'MINI_C'
#!/usr/bin/env bash
set -uo pipefail
. "$WAKE_COMMON"
wake_assert_init
TMP="$1"
FAILFILE="$TMP/failures-c"
: >"$FAILFILE"
fail_msg() { echo "  FAIL: $*" >&2; echo x >>"$FAILFILE"; }
ok() { :; }
(
  has_match -q alpha "$TMP/data.txt" && fail_msg "alpha present" # SITE:c-inverted (deterministically red: alpha IS in the fixture)
) && ok
echo
if [ -s "$FAILFILE" ]; then
  echo "wake mini-c harness: FAILED ($(count_lines . "$FAILFILE") assertion(s))" >&2 # SITE:c-summary
  exit 1
fi
echo "wake mini-c harness: all invariants passed (1 group)"
MINI_C
chmod +x "$TMP/mini-c.sh"
LEDGER="$TMP/ledger-c11"
: >"$LEDGER"
out="$(WAKE_ASSERT_LEDGER="$LEDGER" bash "$TMP/mini-c.sh" "$TMP" 2>&1)"
rc=$?
summary_ln="$(site_line "$TMP/mini-c.sh" c-summary)"
if [ "$rc" -eq 1 ] &&
  printf '%s' "$out" | grep -q 'wake mini-c harness: FAILED (1 assertion(s))' &&
  ! printf '%s' "$out" | grep -q 'all invariants passed' &&
  grep -q "^count_lines mini-c.sh:${summary_ln}\$" "$LEDGER"; then
  check C11 0 ""
else
  check C11 1 "rc=$rc summary_ln=$summary_ln ledger=$(tr '\n' ' ' <"$LEDGER") out=$(printf '%s' "$out" | tail -n 2 | tr '\n' ' ')"
fi

echo
if [ "$fails" -gt 0 ]; then
  echo "microtest-wake-assert: FAILED ($fails check(s))" >&2
  exit 1
fi
echo "microtest-wake-assert: OK (all checks passed)"
