#!/usr/bin/env bash
# The converge timer's whole contract: fix drift before judging it, speak once per
# crossing into drift, and never let a tailnet blip look like a config change.
set -uo pipefail

REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
SCRIPT="$REPO/modules/workstation/claude/bin/buildbox-parity.sh"

fails=0
t() { # name expected actual
  if [ "$2" = "$3" ]; then printf 'ok    %s\n' "$1"
  else printf 'FAIL  %s: expected %s, got %s\n' "$1" "$2" "$3"; fails=$((fails + 1)); fi
}

root="$(mktemp -d)"; trap 'rm -rf "$root"' EXIT
export PARITY_STATE_DIR="$root/state"
export PARITY_LOG_FILE="$root/log"
export PARITY_NOTIFY_CMD="$root/fake-notify"
export BUILDBOX_BIN="$root/fake-buildbox"
export PARITY_PROVISION_BIN="$root/fake-provision"

cat >"$root/fake-notify" <<'EOF'
#!/usr/bin/env bash
printf '%s\n' "$*" >>"$PARITY_STATE_DIR/../notifications"
EOF
chmod 755 "$root/fake-notify"

# Verdicts come from files the case sets: pre/<host> before a bootstrap, post/<host>
# after one. Every invocation is recorded so "did it try to converge" is observable.
cat >"$root/fake-provision" <<'EOF'
#!/usr/bin/env bash
printf 'provision %s\n' "$*" >>"$FAKE_CALLS"
printf '1' >"$FAKE_DIR/provisioned"
exit "${FAKE_PROVISION_RC:-0}"
EOF
chmod 755 "$root/fake-provision"

cat >"$root/fake-buildbox" <<'EOF'
#!/usr/bin/env bash
cmd="$1"; shift
printf '%s %s\n' "$cmd" "$*" >>"$FAKE_CALLS"
if [ "$cmd" = bootstrap ]; then
  printf '1' >"$FAKE_DIR/bootstrapped"
  exit 0
fi
phase=pre; { [ -f "$FAKE_DIR/bootstrapped" ] || [ -f "$FAKE_DIR/provisioned" ]; } && phase=post
hosts=("$@"); [ "$#" -gt 0 ] || readarray -t hosts <"$FAKE_DIR/all-hosts"
rc=0
for h in "${hosts[@]}"; do
  line="$(cat "$FAKE_DIR/$phase/$h" 2>/dev/null || cat "$FAKE_DIR/pre/$h")"
  printf '%-10s %s\n' "$h" "$line"
  case "$line" in converged*) ;; *) rc=1;; esac
done
exit "$rc"
EOF
chmod 755 "$root/fake-buildbox"

export FAKE_DIR="$root/fake"
export FAKE_CALLS="$root/calls"

setup() { # host:pre-line... — resets everything the previous case left behind
  rm -rf "$FAKE_DIR" "$FAKE_CALLS"
  mkdir -p "$FAKE_DIR/pre" "$FAKE_DIR/post"
  : >"$FAKE_CALLS"; : >"$FAKE_DIR/all-hosts"
  local spec host
  for spec in "$@"; do
    host="${spec%%:*}"
    printf '%s\n' "${spec#*:}" >"$FAKE_DIR/pre/$host"
    printf '%s\n' "$host" >>"$FAKE_DIR/all-hosts"
  done
}
after() { printf '%s\n' "${1#*:}" >"$FAKE_DIR/post/${1%%:*}"; }
verdict() { jq -r --arg h "$1" '.hosts[$h].verdict' "$PARITY_STATE_DIR/state.json"; }
announced() { jq -r --arg h "$1" '.announced[$h] // "none"' "$PARITY_STATE_DIR/state.json"; }
notifications() { [ -f "$root/notifications" ] && wc -l <"$root/notifications" || echo 0; }
bootstraps() { grep -c '^bootstrap ' "$FAKE_CALLS"; }
provisions() { grep -c '^provision ' "$FAKE_CALLS"; }

CONVERGED='converged b057be1cca654a1b config-only (credentials not distributed)'
DRIFTED='drifted config=87819cd0867f67bb/want=b057be1cca654a1b'
UNREACHABLE='unreachable (registry marks it reachable — that is itself drift)'

# 1. A converged fleet must cost nothing: no push, no message.
setup "debian1:$CONVERGED" "debian2:$CONVERGED"
bash "$SCRIPT"
t "converged fleet records converged" converged "$(verdict debian1)"
t "converged fleet announces ok" ok "$(announced debian2)"
t "converged fleet runs no bootstrap" 0 "$(bootstraps)"
t "converged fleet sends nothing" 0 "$(notifications)"

# 2. Drift that bootstrap repairs is the normal post-land state, not an incident.
setup "debian1:$DRIFTED" "debian2:$CONVERGED"
after "debian1:$CONVERGED"
bash "$SCRIPT"
t "repaired host provisions only the drifted one" "provision debian1" "$(grep '^provision ' "$FAKE_CALLS")"
t "repaired host never falls back to the tar push" 0 "$(bootstraps)"
t "repaired host records converged" converged "$(verdict debian1)"
t "repaired host sends nothing" 0 "$(notifications)"

# 3. Drift that survives a bootstrap is news — exactly once.
setup "debian1:$DRIFTED" "debian2:$CONVERGED"
after "debian1:$DRIFTED"
bash "$SCRIPT"
t "unrepaired host records drifted" drifted "$(verdict debian1)"
t "unrepaired host announces bad" bad "$(announced debian1)"
t "unrepaired host notifies once" 1 "$(notifications)"
bash "$SCRIPT"
t "still-drifted host does not notify again" 1 "$(notifications)"
t "still-drifted host retries the converge" 2 "$(bootstraps)"

# 4. A provisioner refusal is a source-integrity boundary, not permission to
#    destroy managed seat links with the legacy tar bootstrap fallback.
setup "debian1:$CONVERGED" "debian2:$CONVERGED"
bash "$SCRIPT"
setup "debian1:$DRIFTED" "debian2:$CONVERGED"
after "debian1:$CONVERGED"
before_notifs="$(notifications)"
FAKE_PROVISION_RC=23 bash "$SCRIPT"
t "failed provision still attempts the managed repair" 1 "$(provisions)"
t "failed provision never falls back to bootstrap" 0 "$(bootstraps)"
t "failed provision preserves drift verdict" drifted "$(verdict debian1)"
t "failed provision announces the unresolved drift" "$((before_notifs + 1))" "$(notifications)"

# 5. Recovery re-arms the announcement, so the next episode is heard.
setup "debian1:$CONVERGED" "debian2:$CONVERGED"
bash "$SCRIPT"
t "recovered host announces ok again" ok "$(announced debian1)"
setup "debian1:$DRIFTED" "debian2:$CONVERGED"
after "debian1:$DRIFTED"
bash "$SCRIPT"
t "a new drift episode notifies again" 3 "$(notifications)"

# 6. A host that did not answer says nothing about its config: the announcement it already
#    carries must survive, or every tailnet blip manufactures a fresh drift message.
setup "debian1:$UNREACHABLE" "debian2:$CONVERGED"
bash "$SCRIPT"
t "unreachable host records unreachable" unreachable "$(verdict debian1)"
t "unreachable host keeps its announcement" bad "$(announced debian1)"
t "unreachable host does not notify" 3 "$(notifications)"
t "unreachable host is never bootstrapped" 0 "$(bootstraps)"

# 7. A probe that produced nothing must not overwrite the last known-good verdicts with
#    silence: an empty deck panel reading "converged" is worse than a stale one.
before="$(cat "$PARITY_STATE_DIR/state.json")"
setup
bash "$SCRIPT"; rc=$?
t "an empty probe exits 3" 3 "$rc"
t "an empty probe leaves the recorded state untouched" "$before" "$(cat "$PARITY_STATE_DIR/state.json")"

# 8. Two runs must not push into the same box at once.
setup "debian1:$CONVERGED"
exec 8>"$PARITY_STATE_DIR/converge.lock"; flock -n 8
bash "$SCRIPT"; rc=$?
exec 8>&-
t "a second run exits while one holds the lock" 0 "$rc"
t "a second run probes nothing" 0 "$(grep -c 'claude-parity' "$FAKE_CALLS")"

# 9. The box took the push but main landed again mid-run, so the re-probe still reads
#    drifted against a newer expectation. Convergence worked; that is not an incident.
setup "debian1:$CONVERGED"
bash "$SCRIPT"
t "churn case starts from ok" ok "$(announced debian1)"
setup "debian1:drifted config=87819cd0867f67bb/want=b057be1cca654a1b"
after "debian1:drifted config=b057be1cca654a1b/want=2ffd0df5ec7a5f90"
bash "$SCRIPT"
t "a moved target still records drifted" drifted "$(verdict debian1)"
t "a moved target does not announce bad" ok "$(announced debian1)"
t "a moved target sends nothing" 3 "$(notifications)"

# 10. A box whose digest did not move took no push: that is a real failure to converge.
setup "debian1:drifted config=87819cd0867f67bb/want=b057be1cca654a1b"
after "debian1:drifted config=87819cd0867f67bb/want=2ffd0df5ec7a5f90"
bash "$SCRIPT"
t "a stuck box announces bad" bad "$(announced debian1)"
t "a stuck box notifies" 4 "$(notifications)"

[ "$fails" = 0 ] || { printf '\n%d failed\n' "$fails"; exit 1; }
printf '\nall passed\n'
