#!/usr/bin/env bash
# Prove review and verify repair loops fire, respect their attempt budgets, and terminate.
set -euo pipefail

HARNESS_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
REVIEW_BUDGET=2
VERIFY_BUDGET=2
LOOP_TIMEOUT_SECS=180

fail() {
  printf 'harness-loopcheck: %s\n' "$1" >&2
  exit 1
}

scratch_repo() {
  local repo="$1"
  local remote="$2"
  git -C "$repo" -c init.defaultBranch=main init -q
  git -C "$repo" config user.name 'Harness Loopcheck'
  git -C "$repo" config user.email 'loopcheck@test'
  git -C "$repo" config commit.gpgsign false
  printf 'base\n' >"$repo/README"
  printf 'test:\n\t@true\n' >"$repo/Makefile"
  git -C "$repo" add README Makefile
  git -C "$repo" commit -q -m base
  git -C "$remote" -c init.defaultBranch=main init --bare -q
  git -C "$repo" remote add origin "$remote"
  git -C "$repo" push -q origin main
}

write_plan() {
  local plan="$1"
  local slug="$2"
  local task_id="$3"
  local verify_json="$4"
  local preset="$5"
  cat >"$plan" <<EOF
{"type":"meta","slug":"$slug","land_mode":"none","gate":{"mode":"real","budget_ms":15000},"preset":"$preset","task_dispatch_budget":32}
{"type":"task","id":"$task_id","desc":"repair loop fixture","deps":[],"tier":"low","seat":"coder","files_modify":["canary.txt"],"acceptance":["task completes"],"verify":$verify_json}
EOF
}

run_cell() {
  local cell="$1"
  local slug="$2"
  local task_id="$3"
  local verify_json="$4"
  local preset="$5"
  local run_id="$6"
  local repo="$7"
  local remote="$8"
  local plan="$9"
  local stderr_file="${10}"
  local -n out_rc="${11}"

  write_plan "$plan" "$slug" "$task_id" "$verify_json" "$preset"
  out_rc=0
  HARNESS_ENGINE_DEV=1 timeout "$LOOP_TIMEOUT_SECS" \
    "$HARNESS_ROOT/bin/runplan" "$plan" \
    --repo-root "$repo" \
    --foreground \
    --run-id "$run_id" \
    --budget-ms 60000 \
    --remote origin \
    2>"$stderr_file" || out_rc=$?
}

assert_cell() {
  local cell="$1"
  local slug="$2"
  local run_id="$3"
  local repo="$4"
  local stderr_file="$5"
  local review_budget="$6"
  local verify_budget="$7"

  if ! grep -Fq 'WARNING: HARNESS_ENGINE_DEV=1' "$stderr_file"; then
    fail "$cell: expected dev-engine WARNING on stderr"
  fi
  if ! grep -Fq "engine checkout=$HARNESS_ROOT" "$stderr_file"; then
    fail "$cell: expected dev-engine checkout path on stderr"
  fi

  HARNESS_ROOT="$HARNESS_ROOT" CELL="$cell" SLUG="$slug" RUN_ID="$run_id" REPO="$repo" \
    REVIEW_BUDGET="$review_budget" VERIFY_BUDGET="$verify_budget" node <<'NODE' || fail "$cell: journal assertion failed (see above)"
'use strict';

const fs = require('node:fs');
const path = require('node:path');
const { readJournal } = require(path.join(process.env.HARNESS_ROOT, 'v2/journal.js'));

const cell = process.env.CELL;
const slug = process.env.SLUG;
const runId = process.env.RUN_ID;
const repoRoot = process.env.REPO;
const reviewBudget = Number(process.env.REVIEW_BUDGET);
const verifyBudget = Number(process.env.VERIFY_BUDGET);

function die(message) {
  console.error(`${cell}: ${message}`);
  process.exit(1);
}

const events = readJournal(repoRoot, slug, runId);
if (events.length === 0) die('journal is empty');

const taskEnd = [...events].reverse().find((event) => event.kind === 'task-end');
if (!taskEnd) die('missing task-end event');

const reviewRevises = events.filter((event) => event.kind === 'review.revise');
const verifyRetries = events.filter((event) => event.kind === 'verify.retry');
const quarantines = events.filter((event) => event.kind === 'quarantine');

const BUDGET_HINT = 'budgets are engine constants (MAX_REVIEW_REVISE_ATTEMPTS in v2/quality.js, MAX_VERIFY_FIXER_ATTEMPTS in v2/run.js), not plan fields — a count mismatch may mean the engine default moved';

if (cell === 'review-never-heals') {
  if (taskEnd.status !== 'blocked') die(`expected blocked task-end, got ${JSON.stringify(taskEnd.status)}`);
  if (reviewRevises.length === 0) die('review repair loop never fired (missing review.revise)');
  if (reviewRevises.length !== reviewBudget) {
    die(`expected exactly ${reviewBudget} review.revise events, got ${reviewRevises.length} — ${BUDGET_HINT}`);
  }
  if (reviewRevises.some((event, index) => event.attempt !== index + 1)) {
    die(`review.revise attempts are not 1..${reviewBudget}`);
  }
  const terminal = quarantines.find((event) => event.phase === 'coder' && event.reason === 'review failed');
  if (!terminal) die('missing coder quarantine with reason "review failed"');
  if (taskEnd.failure?.failureClass !== 'quality-quarantined') {
    die(`expected quality-quarantined failure, got ${JSON.stringify(taskEnd.failure?.failureClass)}`);
  }
  if (verifyRetries.length !== 0) die(`verify loop should not run in review cell, got ${verifyRetries.length} verify.retry`);
  process.exit(0);
}

if (cell === 'verify-never-heals') {
  if (taskEnd.status !== 'blocked') die(`expected blocked task-end, got ${JSON.stringify(taskEnd.status)}`);
  if (verifyRetries.length === 0) die('verify repair loop never fired (missing verify.retry)');
  if (verifyRetries.length !== verifyBudget) {
    die(`expected exactly ${verifyBudget} verify.retry events, got ${verifyRetries.length} — ${BUDGET_HINT}`);
  }
  if (verifyRetries.some((event, index) => event.attempt !== index + 1)) {
    die(`verify.retry attempts are not 1..${verifyBudget}`);
  }
  const verifyFailed = events.filter((event) => event.kind === 'verify.failed');
  if (verifyFailed.length !== verifyBudget + 1) {
    die(`expected ${verifyBudget + 1} verify.failed events, got ${verifyFailed.length}`);
  }
  if (taskEnd.failure?.failureClass !== 'acceptance-failed') {
    die(`expected acceptance-failed failure, got ${JSON.stringify(taskEnd.failure?.failureClass)}`);
  }
  if (reviewRevises.length !== 0) die(`review loop should not run in verify cell, got ${reviewRevises.length} review.revise`);
  if (quarantines.length !== 0) die(`verify cell must not quarantine, got ${quarantines.length} quarantine event(s)`);
  process.exit(0);
}

if (cell === 'review-heals') {
  if (reviewRevises.length !== 1) {
    die(`expected exactly 1 review.revise (heal on first fix), got ${reviewRevises.length}`);
  }
  if (quarantines.length !== 0) die(`healed review must not quarantine, got ${quarantines.length} quarantine event(s)`);
  if (taskEnd.status !== 'succeeded') die(`expected succeeded task-end, got ${JSON.stringify(taskEnd.status)}`);
  if (taskEnd.failure) die(`healed task must carry no failure, got ${JSON.stringify(taskEnd.failure)}`);
  if (verifyRetries.length !== 0) die(`verify loop should not run in review cell, got ${verifyRetries.length} verify.retry`);
  process.exit(0);
}

if (cell === 'verify-heals') {
  if (verifyRetries.length !== 1) {
    die(`expected exactly 1 verify.retry (heal on first fix), got ${verifyRetries.length}`);
  }
  const verifyFailed = events.filter((event) => event.kind === 'verify.failed');
  if (verifyFailed.length !== 1) die(`expected 1 verify.failed before the heal, got ${verifyFailed.length}`);
  if (quarantines.length !== 0) die(`healed verify must not quarantine, got ${quarantines.length} quarantine event(s)`);
  if (taskEnd.status !== 'succeeded') die(`expected succeeded task-end, got ${JSON.stringify(taskEnd.status)}`);
  if (taskEnd.failure) die(`healed task must carry no failure, got ${JSON.stringify(taskEnd.failure)}`);
  process.exit(0);
}

die(`unknown cell ${JSON.stringify(cell)}`);
NODE
}

TMP_DIR=""
cleanup() { [[ -n "$TMP_DIR" ]] && rm -rf "$TMP_DIR"; }

cell_run() {
  local cell="$1"
  local slug="$2"
  local task_id="$3"
  local verify_json="$4"
  local preset="$5"
  local repo="$TMP_DIR/$cell-repo"
  local remote="$TMP_DIR/$cell-remote"
  local plan="$TMP_DIR/$cell-plan.jsonl"
  local stderr_file="$TMP_DIR/$cell.stderr"
  local rc=0

  mkdir -p "$repo" "$remote"
  scratch_repo "$repo" "$remote"
  run_cell "$cell" "$slug" "$task_id" "$verify_json" "$preset" "$cell-run" \
    "$repo" "$remote" "$plan" "$stderr_file" rc
  if [[ "$rc" -eq 124 ]]; then
    fail "$cell: runplan timed out after ${LOOP_TIMEOUT_SECS}s"
  fi
  if [[ "$rc" -eq 2 ]]; then
    fail "$cell: runplan coordinator error (exit 2)"
  fi
  assert_cell "$cell" "$slug" "$cell-run" "$repo" "$stderr_file" "$REVIEW_BUDGET" "$VERIFY_BUDGET"
}

main() {
  TMP_DIR="$(mktemp -d)"
  trap cleanup EXIT

  cell_run review-never-heals loopcheck-review review-loop '["true"]' canary-repair
  cell_run verify-never-heals loopcheck-verify verify-loop '["exit 4"]' canary-repair
  cell_run review-heals loopcheck-review-heals review-heal '["true"]' canary-repair-heals
  cell_run verify-heals loopcheck-verify-heals verify-heal '["test $(wc -l < canary.txt) -ge 2"]' canary-repair-heals

  printf 'harness-loopcheck: ok (review-never-heals, verify-never-heals, review-heals, verify-heals)\n'
}

main "$@"
