#!/usr/bin/env bash
# Hermetic tests for the credential table. Builds its own fixture homes and never
# touches the real ~/.codex, ~/.claude or ~/.config.
set -u

SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck source=../lib/agent-credentials.sh
. "$SCRIPT_DIR/../lib/agent-credentials.sh"

PASS=0
FAIL=0
FAIL_LIST=()

ROOT=""
fixture() {
  ROOT="$(mktemp -d)"
  HOME="$ROOT/home"
  CODEX_HOME="$ROOT/codex"
  XDG_CONFIG_HOME="$ROOT/config"
  export HOME CODEX_HOME XDG_CONFIG_HOME
  mkdir -p "$HOME" "$CODEX_HOME" "$XDG_CONFIG_HOME"
}

run_case() {
  local name="$1" fn="$2" output rc
  output="$( set +e; fixture; "$fn" 2>&1; printf 'rc=%s' "$?" )"
  rc="${output##*rc=}"
  output="${output%rc=*}"
  if [[ "$rc" == 0 ]]; then
    printf 'PASS %s\n' "$name"
    PASS=$((PASS + 1))
  else
    printf 'FAIL %s (rc=%s): %s\n' "$name" "$rc" "$output"
    FAIL_LIST+=("$name: $output")
    FAIL=$((FAIL + 1))
  fi
}

fail() { printf '%s\n' "$*" >&2; exit 1; }

expect_resolve() { # runtime host_file expected_container_path
  local runtime="$1" host_file="$2" want_container="$3" out got_runtime got_host got_container want_host
  want_host="$(readlink -f "$host_file")"
  out="$(agent_cred_resolve "$runtime")" || fail "agent_cred_resolve $runtime failed"
  IFS=$'\t' read -r got_runtime got_host got_container <<<"$out"
  [[ "$got_runtime" == "$runtime" ]] || fail "runtime=$got_runtime want $runtime"
  [[ "$got_host" == "$want_host" ]] || fail "host=$got_host want $want_host"
  [[ "$got_container" == "$want_container" ]] || fail "container=$got_container want $want_container"
}

expect_unresolved() { # runtime message_fragment
  local runtime="$1" want="$2" out rc
  out="$(agent_cred_resolve "$runtime" 2>&1)"
  rc=$?
  [[ "$rc" == 10 ]] || fail "expected rc 10, got $rc"
  [[ "$out" == *"$want"* ]] || fail "wrong diagnostic: $out"
}

case_resolve_codex() {
  printf 'codex-secret' >"$CODEX_HOME/auth.json"
  expect_resolve codex "$CODEX_HOME/auth.json" .codex/auth.json
}

case_resolve_claude() {
  mkdir -p "$HOME/.claude"
  printf 'claude-secret' >"$HOME/.claude/.credentials.json"
  expect_resolve claude "$HOME/.claude/.credentials.json" .claude/.credentials.json
}

case_resolve_cursor() {
  mkdir -p "$XDG_CONFIG_HOME/cursor"
  printf 'cursor-secret' >"$XDG_CONFIG_HOME/cursor/auth.json"
  expect_resolve cursor-agent "$XDG_CONFIG_HOME/cursor/auth.json" .config/cursor/auth.json
}

# The account a codex run authenticates as is encoded only in the final realpath.
case_resolve_symlink_chain() {
  mkdir -p "$ROOT/accounts/account-a"
  printf 'codex-secret' >"$ROOT/accounts/account-a/auth.json"
  ln -s "$ROOT/accounts/account-a/auth.json" "$CODEX_HOME/auth.link"
  ln -s auth.link "$CODEX_HOME/auth.json"
  expect_resolve codex "$ROOT/accounts/account-a/auth.json" .codex/auth.json
}

case_broken_symlink() {
  ln -s "$CODEX_HOME/missing-target" "$CODEX_HOME/auth.json"
  expect_unresolved codex 'broken symlink'
}

case_missing_file() { expect_unresolved codex 'does not exist'; }

case_directory_instead_of_file() {
  mkdir -p "$CODEX_HOME/auth.json"
  expect_unresolved codex 'not a regular file'
}

case_unreadable_file() {
  if [[ "$(id -u)" == 0 ]]; then fail 'must not run as root: mode 000 stays readable'; fi
  printf 'x' >"$CODEX_HOME/auth.json"
  chmod 000 "$CODEX_HOME/auth.json"
  expect_unresolved codex 'not readable'
}

case_unknown_runtime() { expect_unresolved not-a-runtime 'unknown runtime'; }

case_runtimes_listing() {
  [[ "$(agent_cred_runtimes)" == $'codex\nclaude\ncursor-agent' ]] ||
    fail "runtimes=$(agent_cred_runtimes)"
}

case_staging_paths() {
  [[ "$(agent_cred_staging_path codex)" == /sandbox-secrets/codex/auth.json ]] || fail codex
  [[ "$(agent_cred_staging_path claude)" == /sandbox-secrets/claude/.credentials.json ]] || fail claude
  [[ "$(agent_cred_staging_path cursor-agent)" == /sandbox-secrets/cursor-agent/auth.json ]] || fail cursor
  agent_cred_staging_path not-a-runtime >/dev/null 2>&1 && fail 'unknown runtime accepted'
  return 0
}

case_stage_script_rejects_quoted_root() {
  agent_cred_stage_script codex "$ROOT/bad'root" >/dev/null 2>&1 && fail 'quote in staging root accepted'
  return 0
}

case_stage_script_is_transport_safe() {
  local script
  script="$(agent_cred_stage_script codex)" || fail 'stage script generation failed'
  [[ "$script" != *$'\n'* ]] || fail 'stage script contains a transport-fragile newline'
  [[ "$script" != *'${HOME-'* ]] || fail 'stage script contains default-value expansion'
  [[ "$script" != *'${HOME%'* ]] || fail 'stage script contains suffix-removal expansion'
  [[ "$script" != *'${agent_cred_target%'* ]] || fail 'stage script contains dirname suffix expansion'
  [[ "$script" == *'dirname "$agent_cred_target"'* ]] || fail 'stage script does not use dirname'
}

# Proves the staging source is the authority and the in-container copy is
# ephemeral: a refresh written inside must not reach the mounted host file.
case_stage_script_exec_and_cleanup() {
  local secret='agent-credential-bytes' staging_root="$ROOT/secrets" script runner out
  mkdir -p "$staging_root/codex"
  printf '%s' "$secret" >"$staging_root/codex/auth.json"
  chmod 400 "$staging_root/codex/auth.json"

  script="$(agent_cred_stage_script codex "$staging_root")" || fail 'stage script generation failed'
  [[ "$script" != *"$secret"* ]] || fail 'secret leaked into the stage script'

  runner="$ROOT/run.sh"
  # shellcheck disable=SC2016  # literal sh assertions run by the generated script
  {
    printf '%s\n' "$script"
    printf '%s\n' \
      '[ -f "$HOME/.codex/auth.json" ] || { echo missing_target; exit 1; }' \
      '[ "$(stat -c %a "$HOME/.codex/auth.json")" = 600 ] || { echo wrong_mode; exit 1; }' \
      '[ "$(cat "$HOME/.codex/auth.json")" = "'"$secret"'" ] || { echo wrong_content; exit 1; }' \
      'printf refreshed > "$HOME/.codex/auth.json"'
  } >"$runner"

  out="$(sh "$runner" 2>&1)" || fail "stage script run failed: $out"

  [[ ! -e "$HOME/.codex/auth.json" ]] || fail 'in-container copy survived the shell'
  [[ "$(cat "$staging_root/codex/auth.json")" == "$secret" ]] ||
    fail 'refresh inside the container reached the staged host credential'
}

# The trap must survive the signal that ends a container, not just a clean exit.
case_stage_script_cleanup_on_signal() {
  local staging_root="$ROOT/secrets" script runner pid
  mkdir -p "$staging_root/codex"
  printf 'secret' >"$staging_root/codex/auth.json"
  script="$(agent_cred_stage_script codex "$staging_root")" || fail 'stage script generation failed'
  runner="$ROOT/run.sh"
  # shellcheck disable=SC2016  # literal sh run by the generated script
  { printf '%s\n' "$script"; printf '%s\n' 'echo ready > "$0.ready"' 'sleep 30'; } >"$runner"

  sh "$runner" &
  pid=$!
  for _ in $(seq 100); do [[ -e "$runner.ready" ]] && break; sleep 0.05; done
  [[ -e "$runner.ready" ]] || { kill "$pid" 2>/dev/null; fail 'stage script never became ready'; }
  [[ -f "$HOME/.codex/auth.json" ]] || { kill "$pid" 2>/dev/null; fail 'credential was not staged'; }
  kill -TERM "$pid" 2>/dev/null
  wait "$pid" 2>/dev/null
  [[ ! -e "$HOME/.codex/auth.json" ]] || fail 'in-container copy survived SIGTERM'
}

run_case 'resolve codex' case_resolve_codex
run_case 'resolve claude' case_resolve_claude
run_case 'resolve cursor-agent' case_resolve_cursor
run_case 'resolve codex through a two-hop symlink chain' case_resolve_symlink_chain
run_case 'broken symlink is unresolvable' case_broken_symlink
run_case 'missing file is unresolvable' case_missing_file
run_case 'directory where a file is expected is unresolvable' case_directory_instead_of_file
run_case 'unreadable file is unresolvable' case_unreadable_file
run_case 'unknown runtime is unresolvable' case_unknown_runtime
run_case 'agent_cred_runtimes lists exactly the three runtimes' case_runtimes_listing
run_case 'agent_cred_staging_path is under /sandbox-secrets' case_staging_paths
run_case 'stage script rejects a quoted staging root' case_stage_script_rejects_quoted_root
run_case 'stage script survives argv transport without fragile expansions' case_stage_script_is_transport_safe
run_case 'stage script copies, restricts, cleans up, and discards a refresh' case_stage_script_exec_and_cleanup
run_case 'stage script cleans up on SIGTERM' case_stage_script_cleanup_on_signal

printf '\nSummary: PASS=%d FAIL=%d\n' "$PASS" "$FAIL"
if ((FAIL > 0)); then
  printf 'Failures:\n'
  for item in "${FAIL_LIST[@]}"; do printf ' - %s\n' "$item"; done
  exit 1
fi
