# Shared helpers for the memory, scheduling, I/O and IPC fixing exercises.
#
# Sourced by every exercises/*/check.sh and by run-all.sh. It contains no
# absolute paths: everything is resolved from the location of the sourcing
# script, so an extracted copy of the archive works anywhere on disk.
#
# Four properties every caller relies on:
#   1. every build happens in a FRESH temporary directory holding one source
#      file, so nothing can depend on a sibling file or a package manifest;
#   2. every run is wrapped in an external hard timeout, on top of the
#      in-process watchdog each fixture already carries;
#   3. exit code 75 means a fixture's own watchdog fired;
#   4. every number a fixture reports is emitted as `key=value` on stdout, so
#      a check asserts a parsed measurement rather than grepping prose.
#
# Timing assertions are RATIOS with generous bounds, never absolute
# milliseconds. A figure measured on one machine under one load is not a
# threshold anybody else can reuse; the shape of the curve is.

# shellcheck shell=bash

LAB_PASS=0
LAB_FAIL=0
LAB_SKIP=0

if [ -t 1 ]; then
  LAB_G=$'\033[32m'; LAB_R=$'\033[31m'; LAB_Y=$'\033[33m'; LAB_B=$'\033[1m'; LAB_0=$'\033[0m'
else
  LAB_G=''; LAB_R=''; LAB_Y=''; LAB_B=''; LAB_0=''
fi

lab_note() { printf '\n%s== %s ==%s\n' "$LAB_B" "$*" "$LAB_0"; }
lab_ok()   { LAB_PASS=$((LAB_PASS + 1)); printf '  %sok%s    %s\n' "$LAB_G" "$LAB_0" "$*"; }
lab_bad()  { LAB_FAIL=$((LAB_FAIL + 1)); printf '  %sFAIL%s  %s\n' "$LAB_R" "$LAB_0" "$*"; }
lab_skip() { LAB_SKIP=$((LAB_SKIP + 1)); printf '  %sskip%s  %s\n' "$LAB_Y" "$LAB_0" "$*"; }
lab_info() { printf '        %s\n' "$*"; }

# lab_require <tool>... — fail closed with a concrete message rather than
# producing a confusing compiler error later.
lab_require() {
  local missing=0 t
  for t in "$@"; do
    command -v "$t" >/dev/null 2>&1 || { printf 'MISSING TOOL: %s\n' "$t" >&2; missing=1; }
  done
  [ "$missing" -eq 0 ] || { printf 'Install the Xcode command line tools: xcode-select --install\n' >&2; exit 2; }
}

# lab_run_bounded <seconds> <command...>
# Runs the command with an external hard kill. Prints the command's combined
# output on stdout and returns its exit status. macOS ships no coreutils
# `timeout`, so the killer is a detached subshell.
lab_run_bounded() {
  local budget="$1"; shift
  local out rc
  out="$("$@" 2>&1 &
         pid=$!
         ( sleep "$budget"; kill -9 "$pid" 2>/dev/null ) >/dev/null 2>&1 </dev/null &
         killer=$!
         wait "$pid" 2>/dev/null; rc=$?
         kill -9 "$killer" 2>/dev/null; wait "$killer" 2>/dev/null
         printf '\n__LAB_RC__=%s' "$rc")"
  rc="${out##*__LAB_RC__=}"
  out="${out%$'\n'__LAB_RC__=*}"
  printf '%s' "$out"
  return "${rc:-70}"
}

# lab_build <source-path> <output-binary> <expected-warning-count> <compiler> <args...>
# Copies the ONE source file into a pristine temporary directory and compiles it
# there. Asserts the warning count exactly, so a new warning is a failure rather
# than something that scrolls past.
lab_build() {
  local src="$1" out="$2" expect_warn="$3"; shift 3
  local base dir log rc warns
  base="$(basename "$src")"
  dir="$(mktemp -d "${TMPDIR:-/tmp}/labbuildXXXXXX")"
  log="$dir/build.log"
  cp "$src" "$dir/$base" || { lab_bad "copy $base"; rm -rf "$dir"; return 1; }
  ( cd "$dir" && "$@" "$base" -o "$out" ) >"$log" 2>&1
  rc=$?
  warns=$(grep -c 'warning:' "$log" 2>/dev/null || true); warns=${warns:-0}
  if [ $rc -ne 0 ]; then
    lab_bad "build $base (compiler exit $rc)"
    sed 's/^/        /' "$log" | head -12
  elif [ "$warns" -ne "$expect_warn" ]; then
    lab_bad "build $base: $warns warning(s), expected $expect_warn"
    grep 'warning:' "$log" | sed 's/^/        /' | head -8
    rc=1
  else
    lab_ok "built $base -> $(basename "$out") from a clean copy (warnings=$warns, expected=$expect_warn)"
  fi
  rm -rf "$dir"
  return $rc
}

# lab_check_patch <exercise-dir> <filename>
# Proves solution.patch turns the broken file into the fixed file byte for byte.
# The patch is applied to a COPY, never to the shipped broken starting point.
lab_check_patch() {
  local exdir="$1" name="$2"
  local dir rc
  dir="$(mktemp -d "${TMPDIR:-/tmp}/labpatchXXXXXX")"
  cp "$exdir/broken/$name" "$dir/$name"
  ( cd "$dir" && patch -s -p1 <"$exdir/solution.patch" ) >"$dir/patch.log" 2>&1
  rc=$?
  if [ $rc -ne 0 ]; then
    lab_bad "solution.patch did not apply cleanly to a copy of broken/$name"
    sed 's/^/        /' "$dir/patch.log" | head -8
    rm -rf "$dir"; return 1
  fi
  if cmp -s "$dir/$name" "$exdir/fixed/$name"; then
    lab_ok "solution.patch applies cleanly and reproduces fixed/$name byte for byte"
  else
    lab_bad "solution.patch applied but the result differs from fixed/$name"
    rc=1
  fi
  rm -rf "$dir"
  return $rc
}

# lab_expect_rc <expected> <label> <budget> <command...>
# Prints the run's output indented, then asserts the exit status.
lab_expect_rc() {
  local want="$1" label="$2" budget="$3"; shift 3
  local out rc
  out="$(lab_run_bounded "$budget" "$@")"; rc=$?
  printf '%s\n' "$out" | sed 's/^/        /'
  if [ "$rc" = "$want" ]; then lab_ok "$label [exit $rc]"; else lab_bad "$label [exit $rc, expected $want]"; fi
  LAB_LAST_OUTPUT="$out"
  return 0
}

# lab_expect_match <label> <extended-regex> — tests the last captured output.
lab_expect_match() {
  local label="$1" re="$2"
  if printf '%s' "${LAB_LAST_OUTPUT:-}" | grep -Eq "$re"; then
    lab_ok "$label"
  else
    lab_bad "$label (no line matching /$re/)"
  fi
}

# lab_expect_nomatch <label> <extended-regex>
lab_expect_nomatch() {
  local label="$1" re="$2"
  if printf '%s' "${LAB_LAST_OUTPUT:-}" | grep -Eq "$re"; then
    lab_bad "$label (found an unexpected line matching /$re/)"
  else
    lab_ok "$label"
  fi
}

# lab_field <key> — pulls key=value out of the last captured output.
lab_field() {
  printf '%s' "${LAB_LAST_OUTPUT:-}" | grep -Eo "(^|[^A-Za-z_])$1=[^ ]+" | tail -1 | sed "s/.*$1=//"
}

# lab_cmp <label> <observed> <op> <bound> — numeric bound, integer or decimal,
# evaluated with awk so a MISSING value fails rather than silently passing.
lab_cmp() {
  local label="$1" a="$2" op="$3" b="$4"
  if [ -z "$a" ] || [ -z "$b" ]; then lab_bad "$label (missing measurement)"; return 1; fi
  if awk -v a="$a" -v b="$b" "BEGIN{exit !(a $op b)}"; then
    lab_ok "$label — observed $a $op $b"
  else
    lab_bad "$label — observed $a, which is NOT $op $b"
  fi
}

# lab_ratio <a> <b> — prints a/b to two decimals, or the empty string.
lab_ratio() {
  [ -n "${1:-}" ] && [ -n "${2:-}" ] || return 0
  awk -v a="$1" -v b="$2" 'BEGIN{ if (b+0==0) exit 0; printf "%.2f", a/b }'
}

lab_summary() {
  printf '\n  %d passed, %d failed' "$LAB_PASS" "$LAB_FAIL"
  [ "$LAB_SKIP" -gt 0 ] && printf ', %d skipped' "$LAB_SKIP"
  printf '\n'
  [ "$LAB_FAIL" -eq 0 ] || return 1
  return 0
}
