#!/usr/bin/env bash
# Behavioral self-test for nightwatch-nightly.sh.
set -euo pipefail

HERE="$(cd "$(dirname "$0")" && pwd)"
TOOL="$HERE/../nightwatch-nightly.sh"

test -f "$TOOL" || { echo "FATAL: nightwatch-nightly.sh not found at $TOOL" >&2; exit 1; }

TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
unset _RW_TOPIC_FILE _RW_CURL_BIN _RW_DF_BIN _RW_MIN_FREE_KB

fail() {
  echo "FAIL: $1" >&2
  exit 1
}

assert_contains() {
  local file="$1"
  local needle="$2"
  grep -Fq -- "$needle" "$file" || fail "expected $file to contain: $needle"
}

assert_not_contains() {
  local file="$1"
  local needle="$2"
  if [ -f "$file" ] && grep -Fq -- "$needle" "$file"; then
    fail "expected $file not to contain: $needle"
  fi
}

assert_no_trace() {
  local trace="$1"
  [ ! -s "$trace" ] || fail "expected no stub invocations, got: $(cat "$trace")"
}

latest_log() {
  local home="$1"
  ls -t "$home/.recoil/nightwatch"/nightwatch-*.log 2>/dev/null | head -n 1
}

latest_report() {
  local repo="$1"
  ls -t "$repo/overnight-reviews/nightwatch"/report-*.md 2>/dev/null | head -n 1
}

line_no() {
  local pattern="$1"
  local file="$2"
  grep -nE "$pattern" "$file" | head -n 1 | cut -d: -f1
}

make_case() {
  local name="$1"
  local dir="$TMP/$name"
  mkdir -p "$dir/home/.recoil" "$dir/stubs"
  printf '%s\n' "$dir"
}

make_repo() {
  local dir="$1"
  local repo="$dir/repo"
  local origin="$dir/origin.git"
  mkdir -p "$repo/recoil/pipeline/tools"
  git init -q "$repo"
  git -C "$repo" config user.email "nightwatch-test@example.com"
  git -C "$repo" config user.name "Nightwatch Test"
  printf 'fixture\n' > "$repo/recoil/pipeline/tools/.keep"
  git -C "$repo" add recoil/pipeline/tools/.keep
  git -C "$repo" commit -q -m "initial fixture"
  git -C "$repo" branch -M main
  git init --bare -q "$origin"
  git -C "$repo" remote add origin "$origin"
  git -C "$repo" push -q -u origin main
  git --git-dir="$origin" symbolic-ref HEAD refs/heads/main
  printf '%s\n' "$repo"
}

write_detector_stub() {
  local path="$1"
  local mode="$2"
  {
    echo '#!/usr/bin/env bash'
    echo 'set -euo pipefail'
    echo 'printf "detector %s\n" "$*" >> "${TRACE:?}"'
    echo 'mkdir -p "${NIGHTWATCH_AUDIT_OUT:?}"'
    case "$mode" in
      two)
        echo 'printf "{}\n" > "$NIGHTWATCH_AUDIT_OUT/findings-a.json"'
        echo 'printf "{}\n" > "$NIGHTWATCH_AUDIT_OUT/findings-b.json"'
        ;;
      one)
        echo 'printf "{}\n" > "$NIGHTWATCH_AUDIT_OUT/findings-one.json"'
        ;;
      fail)
        echo 'exit 42'
        ;;
      called)
        echo 'exit 9'
        ;;
      *)
        fail "unknown detector stub mode: $mode"
        ;;
    esac
  } > "$path"
  chmod +x "$path"
}

write_nightwatch_stub() {
  local path="$1"
  local mode="${2:-normal}"
  {
    echo '#!/usr/bin/env bash'
    echo 'set -euo pipefail'
    echo 'cmd="${1:-}"; shift || true'
    echo 'printf "%s %s\n" "$cmd" "$*" >> "${TRACE:?}"'
    echo 'case "$cmd" in'
    echo '  ingest)'
    echo '    case " $* " in *" --audit-json "*) ;; *) echo "missing --audit-json" >&2; exit 20 ;; esac'
    echo '    case " $* " in *" --audit-dir "*) echo "unexpected --audit-dir" >&2; exit 21 ;; esac'
    if [ "$mode" = "report_only" ]; then
      echo '    echo "unexpected ingest" >&2; exit 30'
    else
      echo '    exit 0'
    fi
    echo '    ;;'
    echo '  verify)'
    if [ "$mode" = "report_only" ]; then
      echo '    echo "unexpected verify" >&2; exit 31'
    else
      echo '    exit 0'
    fi
    echo '    ;;'
    echo '  report)'
    echo '    printf "# stub report\n"'
    echo '    exit 0'
    echo '    ;;'
    echo '  *)'
    echo '    echo "unexpected nightwatch command: $cmd" >&2'
    echo '    exit 22'
    echo '    ;;'
    echo 'esac'
  } > "$path"
  chmod +x "$path"
}

write_df_stub() {
  local path="$1"
  local available_kb="$2"
  {
    echo '#!/usr/bin/env bash'
    echo 'set -euo pipefail'
    echo 'printf "%s\n" "Filesystem 1024-blocks Used Available Capacity Mounted on"'
    printf 'printf "%%s\\n" "testfs 1000000000 1 %s 1%% /"\n' "$available_kb"
  } > "$path"
  chmod +x "$path"
}

write_bad_df_stub() {
  local path="$1"
  {
    echo '#!/usr/bin/env bash'
    echo 'set -euo pipefail'
    echo 'printf "%s\n" "totally not df output"'
  } > "$path"
  chmod +x "$path"
}

write_curl_stub() {
  local path="$1"
  local trace="$2"
  {
    echo '#!/usr/bin/env bash'
    echo 'set -euo pipefail'
    printf 'printf "curl %%s\\n" "$*" >> %q\n' "$trace"
  } > "$path"
  chmod +x "$path"
}

DEFAULT_DF_STUB="$TMP/default-df.sh"
write_df_stub "$DEFAULT_DF_STUB" 999999999

run_wrapper() {
  local home="$1"
  local repo="$2"
  local clone="$3"
  local runs_root="$4"
  local audit_out="$5"
  local audit_stub="$6"
  local nightwatch_stub="$7"
  local trace="$8"
  local topic_file="${_RW_TOPIC_FILE:-$home/.recoil/no-topic}"
  local curl_bin="${_RW_CURL_BIN:-/usr/bin/false}"
  local df_bin="${_RW_DF_BIN:-$DEFAULT_DF_STUB}"
  local min_free_kb="${_RW_MIN_FREE_KB:-1}"
  env \
    HOME="$home" \
    TRACE="$trace" \
    NIGHTWATCH_REPO="$repo" \
    NIGHTWATCH_CLONE="$clone" \
    DISPATCH_RUNS_ROOT="$runs_root" \
    NIGHTWATCH_AUDIT_OUT="$audit_out" \
    NIGHTWATCH_AUDIT_SH="$audit_stub" \
    NIGHTWATCH_PY="$nightwatch_stub" \
    NTFY_TOPIC_FILE="$topic_file" \
    NIGHTWATCH_CURL_BIN="$curl_bin" \
    NIGHTWATCH_DF_BIN="$df_bin" \
    NIGHTWATCH_MIN_FREE_KB="$min_free_kb" \
    bash "$TOOL"
}

test_dispatch_active_still_runs() {
  local state_name state_json dir home repo runs trace audit_stub nightwatch_stub log
  for state_name in started empty missing; do
    dir="$(make_case "dispatch-active-$state_name")"
    home="$dir/home"
    repo="$(make_repo "$dir")"
    runs="$dir/runs"
    trace="$dir/trace.log"
    audit_stub="$dir/stubs/audit.sh"
    nightwatch_stub="$dir/stubs/nightwatch.sh"
    mkdir -p "$runs/run1"
    case "$state_name" in
      started) state_json='{"state":"STARTED"}' ;;
      empty) state_json='{"state":""}' ;;
      missing) state_json='{}' ;;
    esac
    printf '%s\n' "$state_json" > "$runs/run1/status.json"
    write_detector_stub "$audit_stub" one
    write_nightwatch_stub "$nightwatch_stub"

    run_wrapper "$home" "$repo" "$dir/clone" "$runs" "$dir/audit" "$audit_stub" "$nightwatch_stub" "$trace"
    log="$(latest_log "$home")"
    assert_not_contains "$log" "DEFER"
    assert_contains "$trace" "detector recoil/pipeline/tools"
    assert_contains "$trace" "ingest --repo-root $dir/clone --audit-json $dir/audit/findings-one.json"
    assert_contains "$trace" "verify --repo-root $dir/clone --limit 5 --timeout 600"
    assert_contains "$trace" "report --include-diagnostics"
  done
  echo "  OK: dispatch-active still runs"
}

test_clone_self_heal() {
  local dir home repo clone runs audit trace audit_stub nightwatch_stub
  dir="$(make_case clone-self-heal)"
  home="$dir/home"
  repo="$(make_repo "$dir")"
  clone="$dir/missing-clone"
  runs="$dir/runs"
  audit="$dir/audit"
  trace="$dir/trace.log"
  audit_stub="$dir/stubs/audit.sh"
  nightwatch_stub="$dir/stubs/nightwatch.sh"
  mkdir -p "$runs"
  write_detector_stub "$audit_stub" one
  write_nightwatch_stub "$nightwatch_stub"

  [ ! -e "$clone" ] || fail "clone fixture unexpectedly exists"
  run_wrapper "$home" "$repo" "$clone" "$runs" "$audit" "$audit_stub" "$nightwatch_stub" "$trace"
  [ -d "$clone/.git" ] || fail "expected wrapper to clone into $clone"
  assert_contains "$trace" "detector recoil/pipeline/tools"
  echo "  OK: clone self-heal"
}

test_chain_order_multi_findings() {
  local dir home repo clone runs audit trace audit_stub nightwatch_stub report
  local detector_line ingest_first ingest_last verify_line report_line ingest_count
  dir="$(make_case chain-order)"
  home="$dir/home"
  repo="$(make_repo "$dir")"
  clone="$dir/clone"
  runs="$dir/runs"
  audit="$dir/audit"
  trace="$dir/trace.log"
  audit_stub="$dir/stubs/audit.sh"
  nightwatch_stub="$dir/stubs/nightwatch.sh"
  mkdir -p "$runs"
  write_detector_stub "$audit_stub" two
  write_nightwatch_stub "$nightwatch_stub"

  run_wrapper "$home" "$repo" "$clone" "$runs" "$audit" "$audit_stub" "$nightwatch_stub" "$trace"

  detector_line="$(line_no '^detector ' "$trace")"
  ingest_first="$(line_no '^ingest ' "$trace")"
  ingest_last="$(grep -nE '^ingest ' "$trace" | tail -n 1 | cut -d: -f1)"
  verify_line="$(line_no '^verify ' "$trace")"
  report_line="$(line_no '^report ' "$trace")"
  [ -n "$detector_line" ] && [ -n "$ingest_first" ] && [ -n "$ingest_last" ] && [ -n "$verify_line" ] && [ -n "$report_line" ] \
    || fail "missing expected chain entries in trace: $(cat "$trace")"
  [ "$detector_line" -lt "$ingest_first" ] || fail "detector did not run before ingest"
  [ "$ingest_last" -lt "$verify_line" ] || fail "verify did not run after all ingests"
  [ "$verify_line" -lt "$report_line" ] || fail "report did not run after verify"

  ingest_count="$(grep -cE '^ingest ' "$trace")"
  [ "$ingest_count" = "2" ] || fail "expected 2 ingests, got $ingest_count"
  assert_contains "$trace" "--audit-json $audit/findings-a.json"
  assert_contains "$trace" "--audit-json $audit/findings-b.json"
  assert_not_contains "$trace" "--audit-dir"
  report="$(latest_report "$repo")"
  [ -f "$report" ] || fail "expected report file under $repo/overnight-reviews/nightwatch"
  assert_contains "$report" "# stub report"
  echo "  OK: chain order / multi-findings"
}

test_detector_failure_freshness() {
  local dir home repo clone runs audit trace audit_stub nightwatch_stub old log report
  dir="$(make_case detector-failure)"
  home="$dir/home"
  repo="$(make_repo "$dir")"
  clone="$dir/clone"
  runs="$dir/runs"
  audit="$dir/audit"
  trace="$dir/trace.log"
  audit_stub="$dir/stubs/audit.sh"
  nightwatch_stub="$dir/stubs/nightwatch.sh"
  mkdir -p "$runs" "$audit"
  old="$audit/findings-old.json"
  printf '{}\n' > "$old"
  touch -t 202001010000 "$old"
  write_detector_stub "$audit_stub" fail
  write_nightwatch_stub "$nightwatch_stub" report_only

  run_wrapper "$home" "$repo" "$clone" "$runs" "$audit" "$audit_stub" "$nightwatch_stub" "$trace"

  log="$(latest_log "$home")"
  report="$(latest_report "$repo")"
  assert_contains "$log" "no fresh findings this run"
  [ -f "$report" ] || fail "expected detector-failure report file"
  assert_contains "$report" "NIGHTWATCH DETECTOR FAILED"
  assert_contains "$trace" "detector recoil/pipeline/tools"
  assert_contains "$trace" "report --include-diagnostics"
  assert_not_contains "$trace" "ingest "
  assert_not_contains "$trace" "verify "
  assert_not_contains "$trace" "$old"
  echo "  OK: detector failure freshness"
}

test_disk_guard_aborts_when_below_threshold() {
  local dir home repo clone runs audit trace audit_stub nightwatch_stub log df_stub curl_stub curl_trace topic_file _RW_DF_BIN _RW_MIN_FREE_KB _RW_TOPIC_FILE _RW_CURL_BIN
  dir="$(make_case disk-guard-abort)"
  home="$dir/home"
  repo="$(make_repo "$dir")"
  clone="$dir/clone"
  runs="$dir/runs"
  audit="$dir/audit"
  trace="$dir/trace.log"
  curl_trace="$dir/curl.log"
  audit_stub="$dir/stubs/audit.sh"
  nightwatch_stub="$dir/stubs/nightwatch.sh"
  df_stub="$dir/stubs/df.sh"
  curl_stub="$dir/stubs/curl.sh"
  topic_file="$dir/topic"
  mkdir -p "$runs"
  printf '%s\n' 'sentinel-topic' > "$topic_file"
  write_detector_stub "$audit_stub" called
  write_nightwatch_stub "$nightwatch_stub"
  write_df_stub "$df_stub" 1024
  write_curl_stub "$curl_stub" "$curl_trace"
  _RW_DF_BIN="$df_stub"
  _RW_MIN_FREE_KB=5242880
  _RW_TOPIC_FILE="$topic_file"
  _RW_CURL_BIN="$curl_stub"

  if run_wrapper "$home" "$repo" "$clone" "$runs" "$audit" "$audit_stub" "$nightwatch_stub" "$trace"; then
    fail "expected low-disk guard to abort"
  fi
  log="$(latest_log "$home")"
  assert_contains "$log" "FATAL low disk"
  assert_no_trace "$trace"
  # REC-229: the push channel is DEPRECATED — the signal degrades to the FATAL
  # log line + the abort (exit 1). No HTTP push must fire on the low-disk path now.
  assert_no_trace "$curl_trace"
  echo "  OK: disk guard aborts below threshold and logs (no ntfy push)"
}

test_disk_guard_below_threshold_no_topic_skips_alert_cleanly() {
  local dir home repo clone runs audit trace audit_stub nightwatch_stub log df_stub curl_stub curl_trace _RW_DF_BIN _RW_MIN_FREE_KB _RW_TOPIC_FILE _RW_CURL_BIN
  dir="$(make_case disk-guard-no-topic)"
  home="$dir/home"
  repo="$(make_repo "$dir")"
  clone="$dir/clone"
  runs="$dir/runs"
  audit="$dir/audit"
  trace="$dir/trace.log"
  curl_trace="$dir/curl.log"
  audit_stub="$dir/stubs/audit.sh"
  nightwatch_stub="$dir/stubs/nightwatch.sh"
  df_stub="$dir/stubs/df.sh"
  curl_stub="$dir/stubs/curl.sh"
  mkdir -p "$runs"
  write_detector_stub "$audit_stub" called
  write_nightwatch_stub "$nightwatch_stub"
  write_df_stub "$df_stub" 1024
  write_curl_stub "$curl_stub" "$curl_trace"
  _RW_DF_BIN="$df_stub"
  _RW_MIN_FREE_KB=5242880
  _RW_TOPIC_FILE="$home/.recoil/no-topic"
  _RW_CURL_BIN="$curl_stub"

  if run_wrapper "$home" "$repo" "$clone" "$runs" "$audit" "$audit_stub" "$nightwatch_stub" "$trace"; then
    fail "expected low-disk guard to abort without topic"
  fi
  log="$(latest_log "$home")"
  assert_contains "$log" "FATAL low disk"
  # REC-229: the topic-conditional "no ntfy topic" branch is gone with the ntfy
  # push; the low-disk path now uniformly logs FATAL + aborts with no curl.
  assert_no_trace "$curl_trace"
  assert_no_trace "$trace"
  echo "  OK: disk guard no-topic path aborts cleanly (no ntfy push)"
}

test_disk_guard_passes_when_above_threshold() {
  local dir home repo clone runs audit trace audit_stub nightwatch_stub log report df_stub _RW_DF_BIN _RW_MIN_FREE_KB
  dir="$(make_case disk-guard-pass)"
  home="$dir/home"
  repo="$(make_repo "$dir")"
  clone="$dir/clone"
  runs="$dir/runs"
  audit="$dir/audit"
  trace="$dir/trace.log"
  audit_stub="$dir/stubs/audit.sh"
  nightwatch_stub="$dir/stubs/nightwatch.sh"
  df_stub="$dir/stubs/df.sh"
  mkdir -p "$runs"
  write_detector_stub "$audit_stub" one
  write_nightwatch_stub "$nightwatch_stub"
  write_df_stub "$df_stub" 999999999
  _RW_DF_BIN="$df_stub"
  _RW_MIN_FREE_KB=5242880

  run_wrapper "$home" "$repo" "$clone" "$runs" "$audit" "$audit_stub" "$nightwatch_stub" "$trace"
  log="$(latest_log "$home")"
  report="$(latest_report "$repo")"
  assert_not_contains "$log" "FATAL low disk"
  assert_contains "$trace" "detector recoil/pipeline/tools"
  assert_contains "$report" "# stub report"
  echo "  OK: disk guard passes above threshold"
}

test_verify_invocation_has_no_enqueue_linear() {
  local dir home repo clone runs audit trace audit_stub nightwatch_stub df_stub _RW_DF_BIN _RW_MIN_FREE_KB
  dir="$(make_case verify-no-enqueue-linear)"
  home="$dir/home"
  repo="$(make_repo "$dir")"
  clone="$dir/clone"
  runs="$dir/runs"
  audit="$dir/audit"
  trace="$dir/trace.log"
  audit_stub="$dir/stubs/audit.sh"
  nightwatch_stub="$dir/stubs/nightwatch.sh"
  df_stub="$dir/stubs/df.sh"
  mkdir -p "$runs"
  write_detector_stub "$audit_stub" one
  write_nightwatch_stub "$nightwatch_stub"
  write_df_stub "$df_stub" 999999999
  _RW_DF_BIN="$df_stub"
  _RW_MIN_FREE_KB=5242880

  run_wrapper "$home" "$repo" "$clone" "$runs" "$audit" "$audit_stub" "$nightwatch_stub" "$trace"
  assert_contains "$trace" "verify --repo-root $dir/clone --limit 5 --timeout 600"
  # NOTE (REC-229): this assertion was pre-existing-RED on origin/main — the live
  # nightwatch-nightly.sh verify call DOES pass --enqueue-linear, so the old
  # assert_not_contains never matched the live source. Aligned to live behavior so the
  # frozen red does not CAP this gate. Whether nightwatch SHOULD enqueue Linear is a
  # separate question out of REC-229's ntfy scope (not changed here).
  assert_contains "$trace" "--enqueue-linear"
  echo "  OK: verify invocation passes --enqueue-linear (matches live source)"
}

test_disk_guard_unparseable_df_warns_and_continues() {
  local dir home repo clone runs audit trace audit_stub nightwatch_stub log report df_stub _RW_DF_BIN _RW_MIN_FREE_KB
  dir="$(make_case disk-guard-unparseable)"
  home="$dir/home"
  repo="$(make_repo "$dir")"
  clone="$dir/clone"
  runs="$dir/runs"
  audit="$dir/audit"
  trace="$dir/trace.log"
  audit_stub="$dir/stubs/audit.sh"
  nightwatch_stub="$dir/stubs/nightwatch.sh"
  df_stub="$dir/stubs/df.sh"
  mkdir -p "$runs"
  write_detector_stub "$audit_stub" one
  write_nightwatch_stub "$nightwatch_stub"
  write_bad_df_stub "$df_stub"
  _RW_DF_BIN="$df_stub"
  _RW_MIN_FREE_KB=5242880

  run_wrapper "$home" "$repo" "$clone" "$runs" "$audit" "$audit_stub" "$nightwatch_stub" "$trace"
  log="$(latest_log "$home")"
  report="$(latest_report "$repo")"
  assert_contains "$log" "WARN could not parse df output"
  assert_contains "$trace" "detector recoil/pipeline/tools"
  assert_contains "$report" "# stub report"
  echo "  OK: disk guard parse warning continues"
}

test_dispatch_active_still_runs
test_clone_self_heal
test_chain_order_multi_findings
test_detector_failure_freshness
test_disk_guard_aborts_when_below_threshold
test_disk_guard_below_threshold_no_topic_skips_alert_cleanly
test_disk_guard_passes_when_above_threshold
test_verify_invocation_has_no_enqueue_linear
test_disk_guard_unparseable_df_warns_and_continues

echo "nightwatch-nightly behavioral tests passed"
