#!/usr/bin/env bash
# The digest is the whole assertion behind "this box is converged". These cases pin what it
# is allowed to miss: nothing that changes what the box would execute.
set -uo pipefail

MOD="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=../lib/claude-parity.sh
. "$MOD/lib/claude-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
}
tne() { # name a b — must differ
  if [ "$2" != "$3" ]; then printf 'ok    %s\n' "$1"
  else printf 'FAIL  %s: both sides are %s but should differ\n' "$1" "$2"; fails=$((fails + 1)); fi
}

root=$(mktemp -d); trap 'rm -rf "$root"' EXIT
mk() { # dir
  mkdir -p "$1/hooks" "$1/bin"
  printf 'gate\n' >"$1/hooks/a.mjs"; chmod 755 "$1/hooks/a.mjs"
  printf 'cfg\n' >"$1/settings.json"; chmod 644 "$1/settings.json"
  printf 'shim\n' >"$1/bin/_shim.sh"; chmod 755 "$1/bin/_shim.sh"
  ln -sfn _shim.sh "$1/bin/bun"
}
ENTRIES=(hooks bin settings.json)
d() { claude_home_digest "$1" "${ENTRIES[@]}"; }

mk "$root/a"; mk "$root/b"
t "identical trees agree" "$(d "$root/a")" "$(d "$root/b")"

printf 'gate2\n' >"$root/b/hooks/a.mjs"
tne "changed content is caught" "$(d "$root/a")" "$(d "$root/b")"

mk "$root/c"; chmod 644 "$root/c/hooks/a.mjs"
tne "changed mode is caught (a hook that cannot execute)" "$(d "$root/a")" "$(d "$root/c")"

# The failure the owner would never see: bin/bun materialized as a copy of the shim
# answers --version and runs builds unguarded, while every regular-file hash matches.
mk "$root/e"; rm "$root/e/bin/bun"; cp "$root/e/bin/_shim.sh" "$root/e/bin/bun"
tne "symlink flattened to a copy is caught" "$(d "$root/a")" "$(d "$root/e")"

mk "$root/f"; ln -sfn ../elsewhere.sh "$root/f/bin/bun"
tne "symlink retargeted is caught" "$(d "$root/a")" "$(d "$root/f")"

mk "$root/g"; rm -rf "$root/g/hooks"
tne "missing entry is caught" "$(d "$root/a")" "$(d "$root/g")"

# An entry absent on both sides must not silently read as agreement with a populated tree.
mk "$root/h"; rm -rf "$root/h/hooks"
t "two boxes missing the same entry still agree" "$(d "$root/g")" "$(d "$root/h")"

# A file the manifest does not declare is machine-local by construction: a box's own
# transcripts must never make it look drifted.
mk "$root/i"; mkdir -p "$root/i/projects"; printf 'transcript\n' >"$root/i/projects/x.jsonl"
t "undeclared machine-local files are ignored" "$(d "$root/a")" "$(d "$root/i")"

t "an unreadable root does not pass as converged" "0" "$( [ "$(d "$root/a")" != "$(d "$root/nope")" ] && echo 0 || echo 1)"

# Seat-managed layout: an ENTRY that is a symlink into a runtime store reads as
# converged when the content behind it matches — this is what lets the parity
# converger and the seat provisioner coexist instead of rewriting each other.
mk "$root/store-src"
mkdir -p "$root/seat"
ln -sfn "$root/store-src/hooks" "$root/seat/hooks"
ln -sfn "$root/store-src/bin" "$root/seat/bin"
ln -sfn "$root/store-src/settings.json" "$root/seat/settings.json"
t "seat-managed entry symlinks with equal content agree" "$(d "$root/a")" "$(d "$root/seat")"

printf 'cfg2\n' >"$root/store-src/settings.json"
tne "seat-managed content change is still caught" "$(d "$root/a")" "$(d "$root/seat")"
printf 'cfg\n' >"$root/store-src/settings.json"

mkdir -p "$root/dangling"
ln -sfn "$root/store-src/hooks" "$root/dangling/hooks"
ln -sfn "$root/store-src/bin" "$root/dangling/bin"
ln -sfn "$root/gone/settings.json" "$root/dangling/settings.json"
tne "a dangling seat entry link is caught" "$(d "$root/a")" "$(d "$root/dangling")"

# item_claude_hooks is the box-side half of the hook-path contract: the workstation cannot
# see this box's ~/.dev-tools when it stages the payload, so a registration that resolves
# nowhere has to be caught here. Claude Code starts such a hook and says nothing.
hooks_verdict() { # settings-command -> the audit line the box would print
  local home="$root/box"; rm -rf "$home"; mkdir -p "$home/.claude/hooks"
  printf 'gate\n' >"$home/.claude/hooks/present.sh"; chmod 755 "$home/.claude/hooks/present.sh"
  jq -n --arg c "$1" '{hooks:{PreToolUse:[{hooks:[{type:"command",command:$c}]}]}}' >"$home/.claude/settings.json"
  HOME="$home" bash -c '
    ok()  { printf "OK %s\n" "$2"; }
    bad() { printf "DRIFT %s\n" "$2"; }
    . "$1/lib/claude-parity.sh"
    item_claude_hooks' _ "$MOD"
}

t "every registered path present reads as converged" \
  "OK every registered hook path present" "$(hooks_verdict "bash $root/box/.claude/hooks/present.sh")"

script_verdict() { # hook-body -> the audit line the box would print for a hook running it
  local body="$1" home="$root/box"
  hooks_verdict "bash $home/.claude/hooks/present.sh" >/dev/null
  printf '%s\n' "$body" >"$home/.claude/hooks/present.sh"
  HOME="$home" bash -c '
    ok()  { printf "OK %s\n" "$2"; }
    bad() { printf "DRIFT %s\n" "$2"; }
    . "$1/lib/claude-parity.sh"
    item_claude_hooks' _ "$MOD"
}

case "$(script_verdict 'exec /opt/nonexistent/bun "$0.mjs"')" in
  DRIFT*"/opt/nonexistent/bun"*) printf 'ok    %s\n' "a hook starting a missing interpreter drifts" ;;
  *) printf 'FAIL  a hook starting a missing interpreter drifts: %s\n' \
       "$(script_verdict 'exec /opt/nonexistent/bun "$0.mjs"')"; fails=$((fails + 1)) ;;
esac

t "a guarded fallback candidate is not a dependency" \
  "OK every registered hook path present" \
  "$(script_verdict '[ -x /opt/nonexistent/bun ] && RT=/opt/nonexistent/bun || RT=$(command -v node); exec "$RT" -e ""')"

t "a regex literal in a hook body is not a path" \
  "OK every registered hook path present" \
  "$(script_verdict 'node -e "if (/^[A-Za-z_]/.test(x)) {}"; sed -e "s|a|b|" /dev/null')"

case "$(hooks_verdict "$root/box/.dev-tools/slopgate/hooks/commit-hook.sh")" in
  DRIFT*"$root/box/.dev-tools/slopgate/hooks/commit-hook.sh"*"silently"*)
    printf 'ok    %s\n' "a registered path absent on the box is named and drifts" ;;
  *) printf 'FAIL  a registered path absent on the box is named and drifts: %s\n' \
       "$(hooks_verdict "$root/box/.dev-tools/slopgate/hooks/commit-hook.sh")"; fails=$((fails + 1)) ;;
esac

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