#!/usr/bin/env bash
# test-cx.sh — regression suite for cx.sh (the single codex wrapper). Two groups, one counter, mock codex
# via CX_CODEX_BIN (no quota spent): A = review/integrate (codex reasons), B = commit (codex relays the REAL
# run-plan-lib.sh commit — true integration: cx.sh → merge → JSON extraction/relay).
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CDX="$HERE/cx.sh"
PASS=0; FAIL=0
ok()  { PASS=$((PASS+1)); echo "  ok: $1"; }
bad() { FAIL=$((FAIL+1)); echo "FAIL: $1"; echo "     got: $2"; }
field() { printf '%s' "$1" | python3 -c "import sys,json;print(json.load(sys.stdin).get('$2'))" 2>/dev/null; }
jget()  { CX_J="$1" python3 -c 'import os,json,sys; d=json.loads(os.environ["CX_J"]); print(d.get(sys.argv[1]))' "$2" 2>/dev/null; }

# ───────────────────────── GROUP A: review / integrate (codex reasons over a diff) ─────────────────────────
WT="$(mktemp -d)"
git -C "$WT" init -q; git -C "$WT" config user.email t@t.t; git -C "$WT" config user.name t
printf 'line1\n' > "$WT/a.txt"; git -C "$WT" add -A; git -C "$WT" commit -qm base
BASE="$(git -C "$WT" rev-parse HEAD)"
printf 'line1\nline2\n' > "$WT/a.txt"; git -C "$WT" add -A; git -C "$WT" commit -qm change
HEAD="$(git -C "$WT" rev-parse HEAD)"

RMOCKDIR="$(mktemp -d)"; RMOCK="$RMOCKDIR/codex"
cat > "$RMOCK" <<'MOCKEOF'
#!/usr/bin/env bash
case "${MOCK_MODE:-clean}" in
  clean)     echo '{"clean":true,"findings":[]}';;
  findings)  echo 'Here is my review:'; echo '{"clean":false,"findings":[{"file":"a.txt","issue":"bad","severity":"high"}]}';;
  fenced)    printf '```json\n{"clean":true,"findings":[]}\n```\n';;
  malformed) echo 'I could not produce JSON, sorry.';;
  limit)     echo "stream error: usage limit reached, try again later" >&2; exit 1;;
  crash)     echo "panic: boom" >&2; exit 2;;
  flaky)
    n=0; [[ -f "${MOCK_COUNTER:-/dev/null}" ]] && n="$(cat "$MOCK_COUNTER" 2>/dev/null || echo 0)"
    n=$((n+1)); echo "$n" > "${MOCK_COUNTER:-/dev/null}" 2>/dev/null || true
    if [[ "$n" -le "${MOCK_FAILS:-1}" ]]; then echo "stream error: transient blip" >&2; exit 1; fi
    echo '{"clean":true,"findings":[]}';;
esac
exit "${MOCK_RC:-0}"
MOCKEOF
chmod +x "$RMOCK"
export CX_CODEX_BIN="$RMOCK"

run() { MOCK_MODE="$1" bash "$CDX" review --worktree "$WT" --base "$BASE" --head "$HEAD" 2>/dev/null; }

o="$(run clean)";     [[ "$(field "$o" clean)" == "True" && "$(field "$o" exhausted)" == "None" ]] && ok "A1 clean verdict" || bad "A1 clean verdict" "$o"
o="$(run findings)";  [[ "$(field "$o" clean)" == "False" ]] && printf '%s' "$o" | grep -q '"file":"a.txt"' && ok "A2 findings passthrough" || bad "A2 findings passthrough" "$o"
o="$(run fenced)";    [[ "$(field "$o" clean)" == "True" && "$(field "$o" exhausted)" == "None" ]] && ok "A3 fenced json extracted" || bad "A3 fenced json extracted" "$o"
o="$(run malformed)"; [[ "$(field "$o" exhausted)" == "True" ]] && ok "A4 malformed -> exhausted" || bad "A4 malformed -> exhausted" "$o"
o="$(run limit)";     [[ "$(field "$o" exhausted)" == "True" ]] && printf '%s' "$o" | grep -qi 'limit' && ok "A5 limit -> exhausted" || bad "A5 limit -> exhausted" "$o"
o="$(run crash)";     [[ "$(field "$o" exhausted)" == "True" ]] && ok "A6 crash -> exhausted" || bad "A6 crash -> exhausted" "$o"
MOCK_MODE=crash bash "$CDX" review --worktree "$WT" --base "$BASE" --head "$HEAD" >/dev/null 2>&1 && ok "A7 exit 0 on codex failure" || bad "A7 exit 0 on codex failure" "rc=$?"
o="$(MOCK_MODE=crash bash "$CDX" review --worktree "$WT" --base "$HEAD" --head "$HEAD" 2>/dev/null)"; [[ "$(field "$o" clean)" == "True" ]] && ok "A8 empty diff -> clean (codex not called)" || bad "A8 empty diff -> clean" "$o"
bash "$CDX" review --worktree "$WT" --base "$BASE" >/dev/null 2>&1; [[ $? -eq 2 ]] && ok "A9 missing --head -> exit 2" || bad "A9 missing --head -> exit 2" "rc=$?"
bash "$CDX" frobnicate >/dev/null 2>&1; [[ $? -eq 2 ]] && ok "A10 bad mode -> exit 2" || bad "A10 bad mode -> exit 2" "rc=$?"
o="$(MOCK_MODE=clean bash "$CDX" integrate --worktree "$WT" --base "$BASE" --head "$HEAD" 2>/dev/null)"; [[ "$(field "$o" clean)" == "True" ]] && ok "A11 integrate mode verdict" || bad "A11 integrate mode verdict" "$o"
o="$(MOCK_MODE=clean bash "$CDX" review --worktree "$WT" --base "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef" --head "$HEAD" 2>/dev/null)"; [[ "$(field "$o" clean)" == "False" && "$(field "$o" exhausted)" == "None" ]] && ok "A12 bad ref -> clean=false no exhausted" || bad "A12 bad ref -> clean=false no exhausted" "$o"
CNT="$(mktemp)"; : > "$CNT"
o="$(MOCK_MODE=flaky MOCK_FAILS=2 MOCK_COUNTER="$CNT" CX_RETRIES=3 CX_RETRY_SLEEP=0 bash "$CDX" review --worktree "$WT" --base "$BASE" --head "$HEAD" 2>/dev/null)"
[[ "$(field "$o" clean)" == "True" && "$(field "$o" exhausted)" == "None" && "$(cat "$CNT")" == "3" ]] && ok "A13 transient retry recovers (3 attempts, no fallback)" || bad "A13 transient retry recovers" "$o counter=$(cat "$CNT")"
CNT2="$(mktemp)"; : > "$CNT2"
o="$(MOCK_MODE=flaky MOCK_FAILS=9 MOCK_COUNTER="$CNT2" CX_RETRIES=2 CX_RETRY_SLEEP=0 bash "$CDX" review --worktree "$WT" --base "$BASE" --head "$HEAD" 2>/dev/null)"
[[ "$(field "$o" exhausted)" == "True" && "$(cat "$CNT2")" == "2" ]] && printf '%s' "$o" | grep -q 'after 2 attempts' && ok "A14 persistent failure -> exhausted after retries" || bad "A14 persistent failure -> exhausted after retries" "$o counter=$(cat "$CNT2")"
CNT3="$(mktemp)"; : > "$CNT3"
o="$(MOCK_MODE=flaky MOCK_FAILS=9 MOCK_COUNTER="$CNT3" CX_RETRIES=1 CX_RETRY_SLEEP=0 bash "$CDX" review --worktree "$WT" --base "$BASE" --head "$HEAD" 2>/dev/null)"
[[ "$(field "$o" exhausted)" == "True" && "$(cat "$CNT3")" == "1" ]] && ok "A15 CX_RETRIES=1 -> single attempt" || bad "A15 CX_RETRIES=1 -> single attempt" "$o counter=$(cat "$CNT3")"
rm -f "$CNT" "$CNT2" "$CNT3"; rm -rf "$WT" "$RMOCKDIR"

# ───────────────────────── GROUP B: commit (codex relays the REAL run-plan-lib.sh commit) ─────────────────────────
# repo with integration worktree on plan/s + a task branch with a non-overlapping change (mirrors lib test).
mk_commit_repo() {
  local d; d=$(mktemp -d /tmp/cx-commit-XXXX)
  git -C "$d" init -q -b main; git -C "$d" config user.email t@t.t; git -C "$d" config user.name t; git -C "$d" config commit.gpgsign false
  echo base > "$d/f.txt"; git -C "$d" add f.txt; git -C "$d" commit -q -m seed
  git -C "$d" branch "plan/s" main
  git -C "$d" worktree add "$d/.wt-s-int" "plan/s" >/dev/null 2>&1
  git -C "$d" worktree add -b plan/s--t0 "/tmp/wt-s-t0" "plan/s" >/dev/null 2>&1
  printf '%s' "$d"
}
# mock codex: relay/flaky EXTRACT the commit command from the prompt (last arg) and eval it — exactly what a
# compliant codex would do — exercising cx.sh against real run-plan-lib.sh output.
CMOCKDIR="$(mktemp -d)"; CMOCK="$CMOCKDIR/codex"
cat > "$CMOCK" <<'MOCKEOF'
#!/usr/bin/env bash
prompt="${!#}"
runcmd() { local c; c=$(printf '%s\n' "$prompt" | grep -m1 'run-plan-lib.sh commit'); eval "$c"; }
case "${MOCK_MODE:-relay}" in
  relay)     runcmd;;
  fenced)    printf '```json\n{"committed":true,"detail":"merged"}\n```\n';;
  prose)     echo 'Sure, running it:'; echo '{"committed":false,"detail":"merge conflict"}';;
  malformed) echo 'I cannot run commands right now, sorry.';;
  limit)     echo "stream error: usage limit reached, try again later" >&2; exit 1;;
  crash)     echo "panic: boom" >&2; exit 2;;
  flaky)
    n=0; [[ -f "${MOCK_COUNTER:-/dev/null}" ]] && n="$(cat "$MOCK_COUNTER" 2>/dev/null || echo 0)"
    n=$((n+1)); echo "$n" > "${MOCK_COUNTER:-/dev/null}" 2>/dev/null || true
    if [[ "$n" -le "${MOCK_FAILS:-1}" ]]; then echo "stream error: transient blip" >&2; exit 1; fi
    runcmd;;
esac
exit "${MOCK_RC:-0}"
MOCKEOF
chmod +x "$CMOCK"
export CX_CODEX_BIN="$CMOCK" CX_RETRY_SLEEP=0
cleanup() {
  git -C "$R" worktree remove "$R/.wt-s-int" --force >/dev/null 2>&1 || true
  git -C "$R" worktree remove "/tmp/wt-s-t0" --force >/dev/null 2>&1 || true
  rm -rf "$R" "$jdir"
}

# B1. relay clean: codex runs the real merge → committed:true, branch deleted, jsonl COMMITTED.
R=$(mk_commit_repo)
echo new > /tmp/wt-s-t0/g.txt; git -C /tmp/wt-s-t0 add g.txt; git -C /tmp/wt-s-t0 commit -q -m "task work"
jdir=$(mktemp -d); jf="$jdir/p.jsonl"; printf '%s\n' '{"type":"task","id":"t0","wave":1,"desc":"d","status":"WORKING","deps":[],"lease_ts":1}' > "$jf"
j=$(MOCK_MODE=relay bash "$CDX" commit --repo "$R" --intwt "$R/.wt-s-int" --branch "plan/s" --slug "s" --task "t0" --jsonl "$jf")
[[ "$(jget "$j" committed)" == "True" \
   && "$(git -C "$R" rev-parse --verify refs/heads/plan/s--t0 2>/dev/null)" == "" \
   && -f "$R/.wt-s-int/g.txt" \
   && "$(grep '"id":"t0"' "$jf")" == *'"status":"COMMITTED"'* ]] \
  && ok "B1 relay clean: codex runs real merge, committed:true, branch deleted" || bad "B1 relay clean" "$j"
cleanup

# B2. relay conflict: real merge fails → committed:false, NO exhausted (real verdict that must STOP the chain).
R=$(mk_commit_repo)
echo "task version" > /tmp/wt-s-t0/f.txt; git -C /tmp/wt-s-t0 add f.txt; git -C /tmp/wt-s-t0 commit -q -m "task edits f"
echo "int version" > "$R/.wt-s-int/f.txt"; git -C "$R/.wt-s-int" add f.txt; git -C "$R/.wt-s-int" commit -q -m "int edits f"
jdir=$(mktemp -d)
j=$(MOCK_MODE=relay bash "$CDX" commit --repo "$R" --intwt "$R/.wt-s-int" --branch "plan/s" --slug "s" --task "t0" --jsonl "")
[[ "$(jget "$j" committed)" == "False" && "$(jget "$j" exhausted)" == "None" \
   && "$(git -C "$R" rev-parse --verify refs/heads/plan/s--t0 2>/dev/null)" != "" ]] \
  && ok "B2 relay conflict: committed:false, NOT exhausted (real verdict stops chain)" || bad "B2 relay conflict" "$j"
cleanup

# B3. fenced JSON: extraction strips code fences → committed:true.
R=$(mk_commit_repo); jdir=$(mktemp -d)
j=$(MOCK_MODE=fenced bash "$CDX" commit --repo "$R" --intwt "$R/.wt-s-int" --branch "plan/s" --slug "s" --task "t0" --jsonl "")
[[ "$(jget "$j" committed)" == "True" ]] && ok "B3 fenced JSON extracted" || bad "B3 fenced JSON" "$j"
cleanup

# B4. prose-wrapped JSON: pulls the trailing object → committed:false (no exhausted).
R=$(mk_commit_repo); jdir=$(mktemp -d)
j=$(MOCK_MODE=prose bash "$CDX" commit --repo "$R" --intwt "$R/.wt-s-int" --branch "plan/s" --slug "s" --task "t0" --jsonl "")
[[ "$(jget "$j" committed)" == "False" && "$(jget "$j" exhausted)" == "None" ]] \
  && ok "B4 prose-wrapped JSON extracted" || bad "B4 prose JSON" "$j"
cleanup

# B5. malformed (no JSON): every attempt fails → exhausted:true (fall back to Claude floor).
R=$(mk_commit_repo); jdir=$(mktemp -d)
j=$(MOCK_MODE=malformed CX_RETRIES=2 bash "$CDX" commit --repo "$R" --intwt "$R/.wt-s-int" --branch "plan/s" --slug "s" --task "t0" --jsonl "")
[[ "$(jget "$j" committed)" == "False" && "$(jget "$j" exhausted)" == "True" ]] \
  && ok "B5 malformed → exhausted:true" || bad "B5 malformed exhausted" "$j"
cleanup

# B6. limit (rc=1 every attempt) → exhausted:true with reason.
R=$(mk_commit_repo); jdir=$(mktemp -d)
j=$(MOCK_MODE=limit CX_RETRIES=2 bash "$CDX" commit --repo "$R" --intwt "$R/.wt-s-int" --branch "plan/s" --slug "s" --task "t0" --jsonl "")
[[ "$(jget "$j" exhausted)" == "True" && "$(jget "$j" reason)" == *"rc=1"* ]] \
  && ok "B6 usage-limit → exhausted:true with reason" || bad "B6 limit exhausted" "$j"
cleanup

# B7. flaky: first attempt fails, retry runs the real merge → committed:true (retry beats single-shot).
R=$(mk_commit_repo)
echo new > /tmp/wt-s-t0/g.txt; git -C /tmp/wt-s-t0 add g.txt; git -C /tmp/wt-s-t0 commit -q -m "task work"
jdir=$(mktemp -d); jf="$jdir/p.jsonl"; printf '%s\n' '{"type":"task","id":"t0","wave":1,"desc":"d","status":"WORKING","deps":[],"lease_ts":1}' > "$jf"
ctr="$jdir/ctr"
j=$(MOCK_MODE=flaky MOCK_FAILS=1 MOCK_COUNTER="$ctr" CX_RETRIES=3 bash "$CDX" commit --repo "$R" --intwt "$R/.wt-s-int" --branch "plan/s" --slug "s" --task "t0" --jsonl "$jf")
[[ "$(jget "$j" committed)" == "True" ]] && ok "B7 flaky recovers on retry → committed:true" || bad "B7 flaky retry" "$j"
cleanup

# B8. usage error: wrong subcommand args → exit 2.
rc=0; bash "$CDX" commit --repo /tmp >/dev/null 2>&1 || rc=$?
[[ "$rc" -eq 2 ]] && ok "B8 usage error (missing commit args) → exit 2" || bad "B8 usage exit 2" "rc=$rc"

# B9. usage error: missing --branch → exit 2.
R=$(mk_commit_repo); jdir=$(mktemp -d); rc=0
bash "$CDX" commit --repo "$R" --intwt "$R/.wt-s-int" --slug s --task t0 --jsonl "" >/dev/null 2>&1 || rc=$?
[[ "$rc" -eq 2 ]] && ok "B9 usage error (missing --branch) → exit 2" || bad "B9 usage missing branch" "rc=$rc"
cleanup

rm -rf "$CMOCKDIR"
echo "------"; echo "PASS=$PASS FAIL=$FAIL"
[[ "$FAIL" -eq 0 ]]
