#!/bin/sh
# deploy-test.sh — run deploy.sh, quarry-alert.sh and quarry-deploy-watch.sh
# through their paths against temp directories and stubs: no root, no
# host, no network, nothing outside one mktemp directory. Runs on Linux
# and macOS (a perl stand-in for util-linux flock where there is none).
#
#   sh scripts/deploy-test.sh      prints ok/FAIL per case; exit 1 on any FAIL
# Stub bodies are single-quoted on purpose (they expand when the stub
# runs), and each case line is a deliberate "check && ok || bad".
# shellcheck disable=SC2016,SC2015,SC2030,SC2031
set -u
here=$(cd "$(dirname "$0")" && pwd)
T=$(mktemp -d "${TMPDIR:-/tmp}/deploy-test.XXXXXX")
trap 'rm -rf "$T"' EXIT
fails=0
ok() { echo "ok:   $1"; }
bad() { echo "FAIL: $1"; fails=$((fails + 1)); }

H=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
OTHER=fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210

# ---- stubs --------------------------------------------------------------
S=$T/stub
mkdir -p "$S"
C=$T  # the current case's directory; stubs write into it
export C
stub() { printf '#!/bin/sh\n%s\n' "$2" >"$S/$1"; chmod +x "$S/$1"; }
if ! command -v flock >/dev/null 2>&1; then
  # util-linux `flock -n FD`, via flock(2) on the inherited descriptor.
  stub flock '[ "$1" = -n ] || exit 64
exec perl -MFcntl=:flock -e '"'"'open(my $fh, ">&=", $ARGV[0]) or die; flock($fh, LOCK_EX|LOCK_NB) or exit 1'"'"' "$2"'
fi
stub sudo '[ "$1" = -u ] && shift 2; exec "$@"'
stub chown ':'
stub sleep ':'
stub df 'echo "Filesystem 1K-blocks Used Available"; echo "/dev/x 1 1 999999999"'
stub logger '[ "$1" = -t ] && shift 2; echo "$*" >>"$C/journal"'
stub systemctl 'echo "$*" >>"$C/systemctl"; case "$1" in cat) exit 0 ;; esac'
stub systemd-run 'echo "$*" >>"$C/systemd-run"; exit "$(cat "$C/systemd-run.rc" 2>/dev/null || echo 0)"'
# curl: the forge's API answers from files in the case directory.
stub curl 'url=; for a; do case "$a" in http*) url=$a ;; esac; done
case "$url" in
  */api/v1/r/*/checkin/*) [ -f "$C/checkin.json" ] || exit 22; cat "$C/checkin.json" ;;
  */api/v1/version) echo "{\"ready\":true}" ;;
  http://127.0.0.1:8950/) echo "<p>quarry 0.1.0</p>" ;;
esac
exit 0'
# docker: "builds" into the mounted tree as $C/build says:
#   full | partial (no SHA256SUMS, one missing) | none (dies before bin/runner)
stub docker 'w=; prev=; for a; do [ "$prev" = -v ] && case "$a" in *:/src) w=${a%:/src} ;; esac; prev=$a; done
mkdir -p "$w/bin"
printf "#!/bin/sh\necho quarry\n" >"$w/bin/quarry"; printf "#!/bin/sh\n" >"$w/bin/zit"; chmod +x "$w/bin/quarry" "$w/bin/zit"
mode=$(cat "$C/build")
[ "$mode" = none ] && exit 0
mkdir -p "$w/bin/runner"
for f in linux-amd64 linux-arm64 darwin-arm64 darwin-amd64 windows-amd64.exe; do echo "$f" >"$w/bin/runner/quarry-runner-$f"; done
if [ "$mode" = partial ]; then rm "$w/bin/runner/quarry-runner-linux-arm64"; exit 0; fi
cd "$w/bin/runner" && sha256sum quarry-runner-* >SHA256SUMS'

# One case = a fresh directory: a BIN holding a stub zit that opens a
# fixture tree at hash $C/hash, and a published runner set to keep.
setup() {
  C=$T/case.$1
  mkdir -p "$C/bin/runner" "$C/tmp" "$C/tree/scripts"
  echo old >"$C/bin/runner/SHA256SUMS"
  printf '#!/bin/sh\necho old\n' >"$C/bin/quarry"; chmod +x "$C/bin/quarry"
  echo "$H" >"$C/hash"
  echo full >"$C/build"
  echo "go 1.25" >"$C/tree/go.mod"
  cp "$here/build-linux.sh" "$here/runner-check.sh" "$here/deploy.sh" "$C/tree/scripts/"
  printf '#!/bin/sh\ncase "$1" in\n  open) cp -R "%s/tree/." . ;;\n  info) cat "%s/hash"; echo "branch:  trunk" ;;\nesac\n' "$C" "$C" >"$C/bin/zit"
  chmod +x "$C/bin/zit"
}
deploy() {
  (
    export PATH="$S:$PATH" C QUARRY_DEPLOY_BIN="$C/bin" QUARRY_DEPLOY_STORE="$C/store" \
      QUARRY_DEPLOY_ZITREL="$C/zitrel" QUARRY_DEPLOY_TMP="$C/tmp" \
      QUARRY_DEPLOY_LOCK="$C/lock" QUARRY_DEPLOY_STATE="$C/state"
    sh "$here/deploy.sh" "$@" >"$C/out" 2>&1
  )
  rc=$?
}
journal() { grep -q "$1" "$C/journal" 2>/dev/null; }

# ---- --expect -----------------------------------------------------------
setup e12; deploy --dry-run --expect "$(echo "$H" | cut -c1-12)"
[ "$rc" = 0 ] && ok "a 12-character --expect matching trunk deploys" || bad "12-char expect match: rc=$rc $(tail -1 "$C/out")"
setup e64; deploy --dry-run --expect "$H"
[ "$rc" = 0 ] && ok "a full 64-character --expect matching trunk deploys" || bad "64-char expect match: rc=$rc $(tail -1 "$C/out")"
setup m12; deploy --dry-run --expect "$(echo "$OTHER" | cut -c1-12)"
[ "$rc" = 1 ] && journal 'FAILED stage=open .*trunk moved' && ok "a 12-character --expect of another check-in is refused" || bad "12-char moved: rc=$rc"
setup m64; deploy --dry-run --expect "$OTHER"
[ "$rc" = 1 ] && journal 'FAILED stage=open .*trunk moved' && ok "a 64-character --expect of another check-in is refused" || bad "64-char moved: rc=$rc"
setup short; deploy --dry-run --expect 0123456789a
[ "$rc" = 2 ] && ok "an --expect under 12 characters is a usage error" || bad "short expect: rc=$rc"
setup nothex; deploy --dry-run --expect 0123456789aZ
[ "$rc" = 2 ] && ok "a non-hex --expect is a usage error" || bad "non-hex expect: rc=$rc"

# ---- the lock -----------------------------------------------------------
setup lock
( exec 9>>"$C/lock"; PATH="$S:$PATH" flock -n 9 && echo "since then (pid 1, id 1-1)" >"$C/lock" && touch "$C/held" && /bin/sleep 3 ) &
while [ ! -f "$C/held" ]; do /bin/sleep 0.1; done
deploy --dry-run
wait
[ "$rc" = 1 ] && grep -q "another deploy is running — since then" "$C/out" && ok "a second deploy is refused, saying since when" || bad "lock: rc=$rc $(cat "$C/out")"

# ---- --require-green ----------------------------------------------------
setup red; echo '{"required":["quarry","race"],"unmet":["race"]}' >"$C/checkin.json"; deploy --dry-run --require-green
[ "$rc" = 1 ] && journal 'FAILED stage=green .*race' && ok "--require-green refuses a red required check" || bad "red: rc=$rc"
setup green; echo '{"required":["quarry","race"],"unmet":[]}' >"$C/checkin.json"; deploy --dry-run --require-green
[ "$rc" = 0 ] && ok "--require-green passes when every required check is green" || bad "green: rc=$rc $(tail -1 "$C/out")"
setup noreq; echo '{"required":[],"unmet":[]}' >"$C/checkin.json"; deploy --dry-run --require-green
[ "$rc" = 1 ] && journal 'FAILED stage=green .*requires no checks' && ok "--require-green refuses a trunk that requires nothing" || bad "noreq: rc=$rc"
setup oldforge; echo '{"checks":[]}' >"$C/checkin.json"; deploy --dry-run --require-green
[ "$rc" = 1 ] && journal 'FAILED stage=green' && ok "--require-green refuses a forge that does not say" || bad "oldforge: rc=$rc"

# ---- the build and the runner set ---------------------------------------
setup died; echo none >"$C/build"; deploy
[ "$rc" = 1 ] && journal 'FAILED stage=build .*died part-way' && grep -q "echo old" "$C/bin/quarry" \
  && ok "a build that dies before the runner downloads fails, installing nothing" || bad "died: rc=$rc $(tail -1 "$C/out")"
setup pre; echo none >"$C/build"; grep -v '^RUNNER_TARGETS=' "$here/build-linux.sh" >"$C/tree/scripts/build-linux.sh"; deploy
[ "$rc" = 0 ] && journal '^OK ' && [ "$(cat "$C/bin/runner/SHA256SUMS")" = old ] && ok "a tree that builds no runner downloads deploys and keeps the published set" || bad "pre: rc=$rc $(tail -1 "$C/out")"
setup partial; echo partial >"$C/build"; deploy
[ "$rc" = 1 ] && journal 'FAILED stage=runner ' && [ "$(cat "$C/bin/runner/SHA256SUMS")" = old ] && grep -q "echo quarry" "$C/bin/quarry" \
  && ok "an incomplete runner set is not published; the forge deploys; the deploy ends FAILED stage=runner" || bad "partial: rc=$rc $(tail -1 "$C/out")"
setup partialdry; echo partial >"$C/build"; deploy --dry-run
[ "$rc" = 1 ] && journal 'FAILED stage=build .*runner' && ok "a dry run fails on an incomplete runner set" || bad "partialdry: rc=$rc"
setup full; deploy
[ "$rc" = 0 ] && journal '^OK ' && [ -f "$C/bin/runner/quarry-runner-linux-arm64" ] && grep -q "on-active=5" "$C/systemd-run" \
  && ok "a complete deploy publishes the set, schedules the runner restart and logs OK" || bad "full: rc=$rc $(tail -1 "$C/out")"
setup restart; echo 1 >"$C/systemd-run.rc"; deploy
[ "$rc" = 1 ] && journal 'FAILED stage=runner-restart ' && ! journal '^OK ' && ok "a runner restart that cannot be scheduled ends FAILED stage=runner-restart" || bad "restart: rc=$rc $(tail -1 "$C/out")"

# ---- quarry-alert.sh ----------------------------------------------------
C=$T/alert; mkdir -p "$C"
stub curl 'printf "%s\n" "$@" >"$C/argv"; cat >"$C/stdin"; exit "$(cat "$C/curl.rc" 2>/dev/null || echo 0)"'
stub systemctl 'case "$*" in *Result*) echo exit-code ;; *InvocationID*) echo inv1 ;; esac'
stub journalctl 'echo "FAILED stage=ready tip=abc id=1-2 via=unit: not ready"'
alert() { PATH="$S:$PATH" QUARRY_ALERT_RUN="$C/run" QUARRY_NOTIFY_KEY=sekrit-key-123 sh "$here/quarry-alert.sh" "$@" >/dev/null 2>&1; rc=$?; }
alert --key k1 --message "hello"
if [ "$rc" = 0 ] && ! grep -q sekrit "$C/argv" && grep -q sekrit "$C/stdin"; then
  ok "the notify key reaches curl on stdin, never on its argv"
else
  bad "alert argv: rc=$rc argv=$(tr '\n' ' ' <"$C/argv")"
fi
echo 7 >"$C/curl.rc"; alert quarry
[ "$rc" = 1 ] && [ ! -s "$C/run/quarry.service" ] && ok "a failed send writes no quiet mark" || bad "mark after failed send: rc=$rc"
echo 0 >"$C/curl.rc"; : >"$C/argv"; alert quarry
[ "$rc" = 0 ] && [ -s "$C/argv" ] && [ -s "$C/run/quarry.service" ] && ok "the next failure sends, and marks" || bad "send after failed: rc=$rc"
: >"$C/argv"; alert quarry
[ "$rc" = 0 ] && [ ! -s "$C/argv" ] && ok "a failure inside the quiet period is not sent" || bad "quiet: rc=$rc"
echo garbage >"$C/run/quarry.service"; alert quarry
[ "$rc" = 0 ] && [ -s "$C/argv" ] && ok "a corrupt quiet mark reads as absent" || bad "corrupt mark: rc=$rc"
: >"$C/run/quarry.service"; : >"$C/argv"; alert quarry
[ "$rc" = 0 ] && [ -s "$C/argv" ] && ok "an empty quiet mark reads as absent" || bad "empty mark: rc=$rc"

# ---- quarry-deploy-watch.sh ---------------------------------------------
C=$T/watch; mkdir -p "$C"
stub journalctl 'cat "$C/journal" 2>/dev/null'
printf '#!/bin/sh\necho "$2" >>"%s/alerts"\n' "$C" >"$C/alert"; chmod +x "$C/alert"
watch() { PATH="$S:$PATH" QUARRY_DEPLOY_LOCK="$C/lock" QUARRY_DEPLOY_STATE="$C/state" QUARRY_DEPLOY_WATCH="$C/w" QUARRY_ALERT="$C/alert" sh "$here/quarry-deploy-watch.sh"; }
old=$(( $(date +%s) - 900 ))
# A live holder whose command line says deploy, as the real one's does.
sh -c '/bin/sleep 30' deploy.sh & pid=$!
echo "since then (pid $pid, id $old-$pid)" >"$C/lock"
watch; watch
kill "$pid" 2>/dev/null; wait "$pid" 2>/dev/null
[ "$(grep -c "deploy-long:$old-$pid" "$C/alerts" 2>/dev/null)" = 1 ] && ok "a deploy past ten minutes alerts once" || bad "long: $(cat "$C/alerts" 2>/dev/null)"
# The watcher must never take the lock to look: a deploy that starts
# while it looks would be refused. Hold nothing, and check a flock by
# the watcher would have been seen: the stub fails the watch if called.
stub flock 'echo "flock $*" >>"$C/flocked"; exit 1'
: >"$C/alerts"; watch
[ ! -f "$C/flocked" ] && ok "the watcher never takes the deploy lock" || bad "watcher called flock: $(cat "$C/flocked")"
echo "since then (pid $pid, id $old-$pid)" >"$C/lock"
echo "$old-$pid rollback abc" >"$C/state"; : >"$C/journal"; watch; watch
[ "$(grep -c "deploy-lost:$old-$pid" "$C/alerts")" = 1 ] && ok "a deploy that died mid-rollback with no result line alerts once" || bad "lost: $(cat "$C/alerts")"

if [ "$fails" -ne 0 ]; then echo "$fails FAILED"; exit 1; fi
echo "all ok"
