#!/usr/bin/env bash
# Fixture coverage for deploy idle-work gates. Nothing here contacts the live user manager.
set -uo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
DEPLOY_SCRIPT="$ROOT/packaging/deploy-local.sh"
KANBOARD_SCRIPT="$ROOT/packaging/install-kanboard.sh"
fails=0
check() { if (( $1 == 0 )); then printf 'ok - %s\n' "$2"; else printf 'FAIL - %s\n' "$2"; fails=$((fails + 1)); fi; }

TESTROOT="${XDG_CACHE_HOME:-$HOME/.cache}/overdeck/tests/deploy-idle-work/run-$$-$RANDOM"
mkdir -p "$TESTROOT"
trap 'chmod -R u+w "$TESTROOT" 2>/dev/null; rm -rf "$TESTROOT"' EXIT

GATES="$TESTROOT/gates.sh"
sed -n '/^service_unchanged_since_stamp() (/,/^)/p' "$DEPLOY_SCRIPT" >"$GATES"
sed -n '/^PNPM_INSTALL_PATHS=(/,/^)/p' "$DEPLOY_SCRIPT" >>"$GATES"
sed -n '/^pnpm_install_unchanged_since_stamp() (/,/^)/p' "$DEPLOY_SCRIPT" >>"$GATES"

REPO="$TESTROOT/repo"
git init -q "$REPO"
mkdir -p "$REPO/packaging" "$REPO/modules/botmaster/notify" "$REPO/modules/workstation/claude/bin" \
  "$REPO/modules/workstation/systemd/user" "$REPO/modules/actions-gateway" "$REPO/node_modules/.pnpm" "$REPO/docs"
printf old >"$REPO/packaging/overdeck-web.service"
printf old >"$REPO/modules/botmaster/notify/waker.ts"
printf old >"$REPO/modules/workstation/claude/bin/_agent_reaper_lib.py"
printf old >"$REPO/modules/workstation/systemd/user/reaper-notifier.service"
printf old >"$REPO/modules/actions-gateway/server.ts"
printf '{}' >"$REPO/tsconfig.json"
printf lock >"$REPO/pnpm-lock.yaml"; printf '{}' >"$REPO/package.json"; printf 'packages: []' >"$REPO/pnpm-workspace.yaml"
printf '{"included":{"dependencies":true,"devDependencies":true,"optionalDependencies":true}}\n' >"$REPO/node_modules/.modules.yaml"
git -C "$REPO" add -A && git -C "$REPO" -c user.name=t -c user.email=t@t commit -qm base
BASE=$(git -C "$REPO" rev-parse HEAD)

commit_change() { printf '%s' "$2" >"$REPO/$1"; git -C "$REPO" add -A; git -C "$REPO" -c user.name=t -c user.email=t@t commit -qm "$1"; git -C "$REPO" rev-parse HEAD; }
DOC=$(commit_change docs/note.md doc)
WEB=$(commit_change packaging/overdeck-web.service new)
BOT=$(commit_change modules/botmaster/notify/waker.ts new)
REAPER_LIB=$(commit_change modules/workstation/claude/bin/_agent_reaper_lib.py new)
REAPER_UNIT=$(commit_change modules/workstation/systemd/user/reaper-notifier.service new)
LOCK=$(commit_change pnpm-lock.yaml new)
GATEWAY_SRC=$(commit_change modules/actions-gateway/server.ts new)

run_service_gate() { local service=$1 base=$2 head=$3; shift 3; ( DEPLOY="$REPO"; systemctl() { return 0; }; source "$GATES"; service_unchanged_since_stamp "$service" "$base" "$head" "$@" ); }
run_service_gate overdeck-web.service "$BASE" "$DOC" packaging/overdeck-web.service packaging/install-web.sh
check $? 'web: nothing changed and rendered unit equal => kept'
! run_service_gate overdeck-web.service "$BASE" "$WEB" packaging/overdeck-web.service packaging/install-web.sh
check $? 'web: unit-only change with WEB_BUILD_NEEDED=0 => restart'
! run_service_gate botmaster-proxy.service "$WEB" "$BOT" packaging/botmaster-proxy.ts packaging/botmaster-proxy.service packaging/install-botmaster-proxy.sh modules/botmaster/notify/
check $? 'botmaster: notify runtime import change => restart'
! run_service_gate reaper-notifier.service "$BOT" "$REAPER_LIB" modules/workstation/claude/bin/reaper-notifier.py 'modules/workstation/claude/bin/_agent_reaper_lib*' modules/workstation/systemd/user/reaper-notifier.service
check $? 'reaper: _agent_reaper_lib change => restart'
! run_service_gate reaper-notifier.service "$REAPER_LIB" "$REAPER_UNIT" modules/workstation/claude/bin/reaper-notifier.py 'modules/workstation/claude/bin/_agent_reaper_lib*' modules/workstation/systemd/user/reaper-notifier.service
check $? 'reaper: unit change => restart'
run_service_gate actions-gateway.service "$REAPER_UNIT" "$LOCK" modules/actions-gateway/ tsconfig.json packaging/actions-gateway.service packaging/install-actions-gateway.sh packaging/stage-backend-release.sh
check $? 'gateway: dependency lockfile-only change keeps the self-contained immutable runtime'
! run_service_gate actions-gateway.service "$LOCK" "$GATEWAY_SRC" modules/actions-gateway/ tsconfig.json packaging/actions-gateway.service packaging/install-actions-gateway.sh packaging/stage-backend-release.sh
check $? 'gateway: source change requires immutable activation'
cfg="$TESTROOT/gateway.env"; state="$TESTROOT/gateway.sha"; printf a >"$cfg"; sha256sum "$cfg" | cut -d' ' -f1 >"$state"; printf b >"$cfg"
[[ "$(<"$state")" != "$(sha256sum "$cfg" | cut -d' ' -f1)" ]]
check $? 'gateway: config-byte change requires authenticated activation'
old_hash=$(<"$state"); candidate_hash=$(sha256sum "$cfg" | cut -d' ' -f1); false && printf '%s\n' "$candidate_hash" >"$state.tmp" && mv "$state.tmp" "$state"
[[ "$(<"$state")" == "$old_hash" && "$(<"$state")" != "$candidate_hash" ]]
check $? 'gateway: failed activation does not publish the candidate config hash'

( DEPLOY="$REPO"; source "$GATES"; pnpm_install_unchanged_since_stamp "$BASE" "$BASE" )
check $? 'pnpm: unchanged manifests and complete build dependencies => keep installation'
printf '{"included":{"dependencies":true,"devDependencies":false,"optionalDependencies":true}}\n' >"$REPO/node_modules/.modules.yaml"
! ( DEPLOY="$REPO"; source "$GATES"; pnpm_install_unchanged_since_stamp "$BASE" "$BASE" )
check $? 'pnpm: production-only node_modules => restore build dependencies'

rm -rf "$REPO/node_modules/.pnpm"
( DEPLOY="$REPO"; source "$GATES"; pnpm_install_unchanged_since_stamp "$REAPER_UNIT" "$LOCK" )
check $(( $? == 0 ? 1 : 0 )) 'pnpm: unchanged manifests but incomplete node_modules => install'

grep -q 'release_restart_cmd=true' "$DEPLOY_SCRIPT" && grep -q 'OVERDECK_WEB_RELEASE_RESTART_CMD="$release_restart_cmd"' "$DEPLOY_SCRIPT"
check $? 'empty restart set: activation command is literal true'
grep -q 'restoration restarted:' "$DEPLOY_SCRIPT" && grep -q 'web-release.sh" activate' "$DEPLOY_SCRIPT"
check $? 'empty restart set: rollback accounting path accepts literal true'

grep -q '^resolve_real_pnpm()' "$DEPLOY_SCRIPT" && \
  grep -q '^without_session_shims()' "$DEPLOY_SCRIPT" && \
  grep -q '^run_pnpm()' "$DEPLOY_SCRIPT" && \
  grep -q 'env CI=true PATH=' "$DEPLOY_SCRIPT" && \
  grep -q '"\$NODE_BIN" "\$PNPM_BIN" "\$@"' "$DEPLOY_SCRIPT" && \
  grep -q 'run_pnpm --filter web build' "$DEPLOY_SCRIPT" && \
  grep -q 'run_pnpm --filter web deploy' "$DEPLOY_SCRIPT" && \
  ! grep -q '^pnpm --filter web' "$DEPLOY_SCRIPT"
check $? 'web release: every pnpm and node phase bypasses session shims with pinned config'

grep -q 'DEPLOY_DEADLINE_SECONDS="${OVERDECK_DEPLOY_DEADLINE_SECONDS:-1200}"' "$DEPLOY_SCRIPT" && \
  grep -q 'FATAL phase=deploy-timeout deadline=%ss exit=124' "$DEPLOY_SCRIPT"
check $? 'deadline: every deployment owns a loud 20-minute exit-124 deadline'

grep -q '^verify_private_registry()' "$DEPLOY_SCRIPT" && \
  grep -q "'@platform-modules/query-react@0.1.0' '@platform-modules/ui-tokens@0.2.0'" "$DEPLOY_SCRIPT" && \
  grep -q 'run_pnpm install --frozen-lockfile --force' "$DEPLOY_SCRIPT"
check $? 'registry: exact private packages preflight before a forced clean install'

snapshot_model() { local settle=$1; shift; local snapshot="" request; for request in "$@"; do (( request <= settle )) && snapshot+=" $request"; done; printf '%s' "$snapshot"; }
[[ "$(snapshot_model 20 0 10 20 21)" == ' 0 10 20' ]]
check $? 'debounce: before/during-window requests enter authoritative snapshot; after stays queued'
drained="$TESTROOT/drained"; : >"$drained"; rm "$drained"; [[ ! -e "$drained" ]]
check $? 'debounce: second caller whose request was drained exits coalesced'
debounce_line=$(grep -n 'settle_started=' "$DEPLOY_SCRIPT" | cut -d: -f1); snapshot_line=$(grep -n 'queue_snapshot=(' "$DEPLOY_SCRIPT" | tail -1 | cut -d: -f1); coalesced_line=$(grep -n 'status":"deployed-coalesced' "$DEPLOY_SCRIPT" | cut -d: -f1)
[[ "$coalesced_line" -lt "$debounce_line" && "$debounce_line" -lt "$snapshot_line" ]]
check $? 'debounce: under lock after coalesced check and before snapshot'

! run_service_gate overdeck-web.service '' "$DOC" packaging/overdeck-web.service
check $? 'base: absent convergence base => restart'
run_service_gate overdeck-web.service "$BASE" "$LOCK" packaging/overdeck-web.service pnpm-lock.yaml
retry_rc=$?; [[ "$retry_rc" -ne 0 ]]
check $? 'base: an older rollout fallback still detects changed work'

# End-to-end Kanboard fixture. PATH stubs are the only systemctl/curl/php seen by the script.
run_kanboard() {
  local name=$1 active=$2 unit=$3 mode=$4 expected=$5
  local home="$TESTROOT/k-$name/home" deploy="$TESTROOT/k-$name/deploy" bin="$TESTROOT/k-$name/bin" log="$TESTROOT/k-$name/log"
  mkdir -p "$home/.config/overdeck" "$home/.config/containers/systemd" "$home/.local/share/overdeck/kanboard-plugin-releases/old" \
    "$deploy/packaging/kanboard/plugins/OverdeckIncidents" "$bin"
  cp "$ROOT/packaging/kanboard/overdeck-kanboard.container" "$deploy/packaging/kanboard/"
  cp "$ROOT/packaging/kanboard/plugins/OverdeckIncidents/Plugin.php" "$ROOT/packaging/kanboard/plugins/OverdeckIncidents/BoundedTaskPage.php" "$deploy/packaging/kanboard/plugins/OverdeckIncidents/"
  printf 'KANBOARD_API_TOKEN=x\n' >"$home/.config/overdeck/kanboard-app.env"
  if [[ "$name" == fallback ]]; then
    khash=$(find "$deploy/packaging/kanboard/plugins/OverdeckIncidents" -type f -print0 | sort -z | xargs -0 sha256sum | sha256sum | cut -d' ' -f1)
    mkdir -p "$home/.local/share/overdeck/kanboard-plugin-releases/$khash"
    cp -a "$deploy/packaging/kanboard/plugins/OverdeckIncidents/". "$home/.local/share/overdeck/kanboard-plugin-releases/$khash/"
    ln -s "$home/.local/share/overdeck/kanboard-plugin-releases/$khash" "$home/.local/share/overdeck/kanboard-plugin-active"
  elif [[ "$active" == yes ]]; then
    ln -s "$home/.local/share/overdeck/kanboard-plugin-releases/old" "$home/.local/share/overdeck/kanboard-plugin-active"
  fi
  [[ "$unit" == yes ]] && cp "$deploy/packaging/kanboard/overdeck-kanboard.container" "$home/.config/containers/systemd/overdeck-kanboard.container"
  printf '#!/usr/bin/env bash\nprintf "%%s\\n" "$*" >>"$STUB_LOG"\n[[ "$*" == *"is-active"* ]] && [[ "$STUB_ACTIVE" == yes ]]\nexit 0\n' >"$bin/systemctl"
  printf '#!/usr/bin/env bash\nprintf "{}"\nexit 0\n' >"$bin/curl"
  printf '#!/usr/bin/env bash\n[[ "${1:-}" == -l ]] && exit 0\nc=$(cat "$STUB_COUNT" 2>/dev/null || echo 0); c=$((c+1)); echo "$c" >"$STUB_COUNT"\ncase "$STUB_MODE" in success) exit 0;; fail-first) ((c==1)) && exit 1 || exit 0;; fail-all) exit 1;; esac\n' >"$bin/php"
  printf '#!/usr/bin/env bash\nexit 0\n' >"$bin/sleep"
  chmod +x "$bin"/*
  STUB_LOG="$log" STUB_COUNT="$log.count" STUB_ACTIVE="$active" STUB_MODE="$mode" HOME="$home" PATH="$bin:$PATH" OVERDECK_DEPLOY_DIR="$deploy" bash "$KANBOARD_SCRIPT" >"$log.out" 2>"$log.err"
  rc=$?
  grep -q "$expected" "$log.out" || return 1
  if [[ "$expected" == 'kanboard: rolled-back' ]]; then
    [[ "$rc" -ne 0 ]]
  else
    [[ "$rc" -eq 0 ]]
  fi
}
run_kanboard absent-link no yes success 'kanboard: restarted'; check $? 'kanboard: absent active link => restart'
run_kanboard absent-unit yes no success 'kanboard: restarted'; check $? 'kanboard: absent installed unit => restart'
run_kanboard inactive yes yes success 'kanboard: restarted'; check $? 'kanboard: inactive service => restart'
run_kanboard fallback yes yes fail-first 'kanboard: fallback-restarted'; check $? 'kanboard: unchanged release + failed canary => fallback restart'
run_kanboard candidate-prev yes no fail-first 'kanboard: rolled-back'; check $? 'kanboard: candidate failure with previous release rolls back'
run_kanboard candidate-none no no fail-first 'kanboard: rolled-back'; check $? 'kanboard: candidate failure without previous release activates empty fallback'
run_kanboard rollback-fails yes no fail-all 'kanboard: rolled-back'
check $? 'kanboard: rollback failure surfaces non-zero and rolled-back status'
grep -q "fallback-restarted.*restart_services+=(overdeck-kanboard.service)" <(tr '\n' ' ' <"$DEPLOY_SCRIPT")
check $? 'evidence: Kanboard fallback restart is restarted, not kept'

(( fails == 0 )) || { printf '%s check(s) failed\n' "$fails" >&2; exit 1; }
printf 'deploy-idle-work: all checks passed\n'
