#!/usr/bin/env bash
# run-tests.sh — agent-pool 端到端测试
# 在隔离的临时池根上跑，不影响真实的 ffworkspace-wt/.agent-pool/
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
AGENT_POOL_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
REPO_ROOT="$(git rev-parse --show-toplevel)"

# 隔离的临时池根
TEST_POOL_ROOT="$(mktemp -d -t agent-pool-test.XXXXXX)"
export FFOA_AGENT_POOL_ROOT="${TEST_POOL_ROOT}"
# 隔离 .code-workspace 文件，防止测试期 ap_write_code_workspace 污染真实
# <repo-parent>/<repo>.code-workspace（pool_root 是测试路径但 codeworkspace_file
# 默认仍解析到真实文件，会写入 /tmp/agent-pool-test.XXX/slot-N 的 dead path）。
export FFOA_CODEWORKSPACE_FILE="${TEST_POOL_ROOT}/test.code-workspace"
# 测试用更短的 stale 超时（5 秒）让测试快
export AP_STALE_TIMEOUT=5
export AP_HEARTBEAT_INTERVAL=2

PASS=0
FAIL=0
FAILED_TESTS=()

# 全局 fetch.prune 会让测试期伪造的 refs/remotes/origin/test/* 被首次 git fetch 清掉。
# 仓库级覆盖为 false，整个测试期保住伪造 ref；cleanup 里恢复（原值未必设置过）。
T_ORIG_FETCH_PRUNE="$(git -C "${REPO_ROOT}" config --local --get fetch.prune 2>/dev/null || echo UNSET)"
git -C "${REPO_ROOT}" config --local fetch.prune false

ok()   { echo "  ✓ $*"; PASS=$((PASS+1)); }
fail() { echo "  ✗ $*"; FAIL=$((FAIL+1)); FAILED_TESTS+=("$*"); }

cleanup() {
  echo
  echo "[cleanup] 清理测试 worktree 和分支"
  for s in 1 2 3; do
    slot_dir="${TEST_POOL_ROOT}/slot-${s}"
    if [[ -d "${slot_dir}" ]]; then
      git -C "${REPO_ROOT}" worktree remove --force "${slot_dir}" 2>/dev/null || true
    fi
    git -C "${REPO_ROOT}" branch -D "pool/slot-${s}" 2>/dev/null || true
  done
  for b in test/branch-a test/branch-b test/branch-c test/branch-d test/branch-e \
           test/branch-orphan-clean test/branch-orphan-dirty test/branch-orphan-fresh \
           test/branch-squash-zombie test/branch-agent-dead test/branch-agent-dead-dirty \
           test/branch-must-skip-abandoned test/branch-ff-sync test/branch-ahead-warn \
           test/branch-release-cleanup; do
    git -C "${REPO_ROOT}" branch -D "${b}" 2>/dev/null || true
  done
  git -C "${REPO_ROOT}" branch --list 'wip/slot-*-*' | xargs -r git -C "${REPO_ROOT}" branch -D 2>/dev/null || true
  # 清理伪造的 origin 远端 ref（T19/T20/T21 创建的）
  for b in test/branch-ff-sync test/branch-ahead-warn test/branch-release-cleanup; do
    git -C "${REPO_ROOT}" update-ref -d "refs/remotes/origin/${b}" 2>/dev/null || true
  done
  # T16 兜底：若 mutate 了 origin/develop 但未恢复（panic 路径），还原回原 SHA
  if [[ -n "${T16_ORIG_DEVELOP_SHA:-}" ]]; then
    git -C "${REPO_ROOT}" update-ref refs/remotes/origin/develop "${T16_ORIG_DEVELOP_SHA}" 2>/dev/null || true
  fi
  rm -rf "${TEST_POOL_ROOT}"
  # 兜底 prune：上次 SIGKILL（trap 不触发）残留的 prunable worktree 会让下次
  # pool-init 撞 "pool/slot-X is already used by worktree at..."（详见
  # .learnings/ERRORS/ERR-20260510-003）。
  git -C "${REPO_ROOT}" worktree prune 2>/dev/null || true
  # 恢复 fetch.prune（仓库级覆盖）
  if [[ "${T_ORIG_FETCH_PRUNE:-UNSET}" == "UNSET" ]]; then
    git -C "${REPO_ROOT}" config --local --unset fetch.prune 2>/dev/null || true
  else
    git -C "${REPO_ROOT}" config --local fetch.prune "${T_ORIG_FETCH_PRUNE}" 2>/dev/null || true
  fi
  echo "[cleanup] 完成"
}
trap cleanup EXIT

echo "=========================================="
echo "Agent Pool 端到端测试"
echo "=========================================="
echo "测试池根: ${TEST_POOL_ROOT}"
echo "stale 超时: ${AP_STALE_TIMEOUT}s（仅测试期）"
echo

###############################################################################
# Test 0: 池未初始化 → claim 应 exit 4
###############################################################################
echo "[T0] 池未初始化 → claim exit 4"
set +e
bash "${AGENT_POOL_DIR}/agent-claim.sh" test/branch-a develop > /tmp/t0.out 2> /tmp/t0.err
ec0=$?
set -e
[[ "${ec0}" == 4 ]] && ok "exit code 4（pool not initialized）" || { cat /tmp/t0.err; fail "exit ${ec0}（期望 4）"; }
grep -q "EXIT_REASON=not_initialized" /tmp/t0.err && ok "结构化消息含 EXIT_REASON" || fail "未输出 EXIT_REASON"

###############################################################################
# Test 1: pool-init --minimal --size 2 创建 2 个 slot
###############################################################################
echo
echo "[T1] pool-init --minimal --size 2"
bash "${AGENT_POOL_DIR}/pool-init.sh" --minimal --size 2 > /tmp/t1.log 2>&1 || {
  cat /tmp/t1.log
  fail "pool-init 失败"
  exit 1
}
[[ -d "${TEST_POOL_ROOT}/slot-1" ]] && ok "slot-1 目录已创建" || fail "slot-1 缺失"
[[ -d "${TEST_POOL_ROOT}/slot-2" ]] && ok "slot-2 目录已创建" || fail "slot-2 缺失"
git -C "${TEST_POOL_ROOT}/slot-1" rev-parse --abbrev-ref HEAD | grep -q "^pool/slot-1$" \
  && ok "slot-1 在 park 分支 pool/slot-1" || fail "slot-1 分支不对"

###############################################################################
# Test 2: 状态显示
###############################################################################
echo
echo "[T2] agent-status 显示两个 slot 都为 free"
bash "${AGENT_POOL_DIR}/agent-status.sh" > /tmp/t2.log 2>&1
grep -qE "^1 .*free" /tmp/t2.log && ok "slot-1 状态 free" || { cat /tmp/t2.log; fail "slot-1 状态不正确"; }
grep -qE "^2 .*free" /tmp/t2.log && ok "slot-2 状态 free" || fail "slot-2 状态不正确"

###############################################################################
# Test 3: claim branch-a
###############################################################################
echo
echo "[T3] claim test/branch-a"
CLAIM_OUT=$(bash "${AGENT_POOL_DIR}/agent-claim.sh" test/branch-a develop 2>/tmp/t3.err)
echo "${CLAIM_OUT}" | grep -q "FFOA_AGENT_SLOT=" && ok "claim 输出含 FFOA_AGENT_SLOT" \
  || { cat /tmp/t3.err; fail "claim 输出格式异常"; }

eval "${CLAIM_OUT}"
[[ -n "${FFOA_AGENT_SLOT}" ]] && ok "FFOA_AGENT_SLOT=${FFOA_AGENT_SLOT}" || fail "FFOA_AGENT_SLOT 未设置"
[[ -f "${TEST_POOL_ROOT}/slot-${FFOA_AGENT_SLOT}.lock" ]] && ok "lock 文件存在" || fail "lock 文件未创建"
git -C "${FFOA_AGENT_DIR}" rev-parse --abbrev-ref HEAD | grep -q "^test/branch-a$" \
  && ok "slot 已切到 test/branch-a" || fail "slot 分支切换失败"

SLOT_A="${FFOA_AGENT_SLOT}"
unset FFOA_AGENT_SLOT FFOA_AGENT_DIR FFOA_AGENT_LOCK FFOA_AGENT_BRANCH

###############################################################################
# Test 4: 同分支冲突
###############################################################################
echo
echo "[T4] claim test/branch-a 第二次（同分支冲突）"
set +e
bash "${AGENT_POOL_DIR}/agent-claim.sh" test/branch-a develop > /tmp/t4.out 2> /tmp/t4.err
ec4=$?
set -e
if [[ "${ec4}" == 0 ]]; then
  fail "同分支重复 claim 居然成功了"
elif [[ "${ec4}" == 2 ]]; then
  ok "退出码 2（同分支冲突）"
  grep -q "EXIT_REASON=branch_conflict" /tmp/t4.err && ok "结构化消息正确" \
    || { cat /tmp/t4.err; fail "结构化消息不对"; }
else
  cat /tmp/t4.err
  fail "退出码 ${ec4}（期望 2）"
fi

###############################################################################
# Test 5: claim branch-b → 第二个 slot
###############################################################################
echo
echo "[T5] claim test/branch-b"
CLAIM_OUT=$(bash "${AGENT_POOL_DIR}/agent-claim.sh" test/branch-b develop 2>/tmp/t5.err)
eval "${CLAIM_OUT}"
[[ "${FFOA_AGENT_SLOT}" != "${SLOT_A}" ]] && ok "落到不同 slot (${FFOA_AGENT_SLOT} vs ${SLOT_A})" || fail "撞同一 slot"
SLOT_B="${FFOA_AGENT_SLOT}"
unset FFOA_AGENT_SLOT FFOA_AGENT_DIR FFOA_AGENT_LOCK FFOA_AGENT_BRANCH

###############################################################################
# Test 6: 池满 → exit 3
###############################################################################
echo
echo "[T6] claim test/branch-c（池满）"
set +e
bash "${AGENT_POOL_DIR}/agent-claim.sh" test/branch-c develop > /tmp/t6.out 2> /tmp/t6.err
ec6=$?
set -e
if [[ "${ec6}" == 0 ]]; then
  fail "池满还能 claim 成功"
elif [[ "${ec6}" == 3 ]]; then
  ok "退出码 3（池满）"
  grep -q "EXIT_REASON=pool_full" /tmp/t6.err && ok "结构化消息正确" || fail "结构化消息不对"
else
  cat /tmp/t6.err
  fail "退出码 ${ec6}（期望 3）"
fi

###############################################################################
# Test 7: stale → sweep
###############################################################################
echo
echo "[T7] 模拟 slot-${SLOT_A} stale → sweep"
HB_PID=$(awk -F= '/^heartbeat_pid=/{print $2; exit}' "${TEST_POOL_ROOT}/slot-${SLOT_A}.lock")
if [[ -n "${HB_PID}" ]]; then
  kill "${HB_PID}" 2>/dev/null || true
fi
old_iso=$(date -u -d "@$(($(date +%s) - 60))" +%Y-%m-%dT%H:%M:%SZ)
sed -i -E "s/^heartbeat_at=.*/heartbeat_at=${old_iso}/" "${TEST_POOL_ROOT}/slot-${SLOT_A}.lock"
sed -i -E "s/^heartbeat_pid=.*/heartbeat_pid=99999999/" "${TEST_POOL_ROOT}/slot-${SLOT_A}.lock"

bash "${AGENT_POOL_DIR}/agent-status.sh" > /tmp/t7-status.log 2>&1
grep -qE "^${SLOT_A} .*stale" /tmp/t7-status.log && ok "状态识别为 stale" || { cat /tmp/t7-status.log; fail "状态未识别为 stale"; }

bash "${AGENT_POOL_DIR}/agent-sweep.sh" > /tmp/t7-sweep.log 2>&1
[[ ! -f "${TEST_POOL_ROOT}/slot-${SLOT_A}.lock" ]] && ok "sweep 已删除 stale lock" || fail "sweep 未删除"

###############################################################################
# Test 8: sweep 后复用
###############################################################################
echo
echo "[T8] sweep 后 claim test/branch-c"
CLAIM_OUT=$(bash "${AGENT_POOL_DIR}/agent-claim.sh" test/branch-c develop 2>/tmp/t8.err)
eval "${CLAIM_OUT}"
[[ "${FFOA_AGENT_SLOT}" == "${SLOT_A}" ]] && ok "复用了 sweep 后的 slot-${SLOT_A}" || fail "未复用 slot-${SLOT_A}（落到 ${FFOA_AGENT_SLOT}）"
SLOT_C="${FFOA_AGENT_SLOT}"
unset FFOA_AGENT_SLOT FFOA_AGENT_DIR FFOA_AGENT_LOCK FFOA_AGENT_BRANCH

###############################################################################
# Test 9: release → 切回 park
###############################################################################
echo
echo "[T9] release slot-${SLOT_B}"
bash "${AGENT_POOL_DIR}/agent-release.sh" "${SLOT_B}" > /tmp/t9.log 2>&1
[[ ! -f "${TEST_POOL_ROOT}/slot-${SLOT_B}.lock" ]] && ok "lock 文件已删" || fail "lock 文件还在"
park_check=$(git -C "${TEST_POOL_ROOT}/slot-${SLOT_B}" rev-parse --abbrev-ref HEAD)
[[ "${park_check}" == "pool/slot-${SLOT_B}" ]] && ok "已切回 park 分支" \
  || fail "未切回 park 分支（当前 ${park_check}）"

###############################################################################
# Test 10: dirty workspace → 标 abandoned；--force 才真释放并 stash 到 wip
###############################################################################
echo
echo "[T10] dirty → abandoned；--force → 真释放 + wip 兜底"
SLOT_C_DIR="$(realpath "${TEST_POOL_ROOT}/slot-${SLOT_C}")"
echo "dirty content" > "${SLOT_C_DIR}/test-dirty-file.txt"

# 不带 --force：应标为 abandoned，lock 保留，worktree 不动
bash "${AGENT_POOL_DIR}/agent-release.sh" "${SLOT_C}" > /tmp/t10a.log 2>&1
[[ -f "${TEST_POOL_ROOT}/slot-${SLOT_C}.lock" ]] && ok "abandoned 路径保留 lock" \
  || { cat /tmp/t10a.log; fail "lock 被错误删除"; }
abn_state=$(awk -F= '/^state=/{print $2; exit}' "${TEST_POOL_ROOT}/slot-${SLOT_C}.lock")
[[ "${abn_state}" == "abandoned" ]] && ok "state=abandoned" || fail "state=${abn_state}（期望 abandoned）"
abn_reason=$(awk -F= '/^abandoned_reason=/{print $2; exit}' "${TEST_POOL_ROOT}/slot-${SLOT_C}.lock")
[[ "${abn_reason}" == "dirty" ]] && ok "abandoned_reason=dirty" || fail "reason=${abn_reason}（期望 dirty）"
[[ -f "${SLOT_C_DIR}/test-dirty-file.txt" ]] && ok "worktree 未被 reset（脏文件还在）" \
  || fail "worktree 被错误 reset"

# agent-status 应能识别 abandoned
bash "${AGENT_POOL_DIR}/agent-status.sh" > /tmp/t10b.log 2>&1
grep -qE "^${SLOT_C} .*abandoned" /tmp/t10b.log && ok "status 显示 abandoned" \
  || { cat /tmp/t10b.log; fail "status 未显示 abandoned"; }

# 带 --force：应真释放 + wip 分支兜底（保住未提交内容）
bash "${AGENT_POOL_DIR}/agent-release.sh" "${SLOT_C}" --force > /tmp/t10c.log 2>&1
[[ ! -f "${TEST_POOL_ROOT}/slot-${SLOT_C}.lock" ]] && ok "--force 删除 lock" \
  || { cat /tmp/t10c.log; fail "--force 后 lock 还在"; }
wip_count=$(git -C "${REPO_ROOT}" branch --list "wip/slot-${SLOT_C}-*" | wc -l)
[[ "${wip_count}" -ge 1 ]] && ok "--force 路径创建 wip 分支保命" \
  || { cat /tmp/t10c.log; fail "wip 分支未创建"; }

###############################################################################
# Test 11: 显式禁用开关
###############################################################################
echo
echo "[T11] FFOA_AGENT_POOL_ENABLED=false → claim exit 5"
set +e
FFOA_AGENT_POOL_ENABLED=false bash "${AGENT_POOL_DIR}/agent-claim.sh" test/branch-d develop > /tmp/t11.out 2> /tmp/t11.err
ec11=$?
set -e
[[ "${ec11}" == 5 ]] && ok "exit 5 (disabled)" || { cat /tmp/t11.err; fail "exit ${ec11}（期望 5）"; }
grep -q "EXIT_REASON=disabled" /tmp/t11.err && ok "结构化消息" || fail "未输出 EXIT_REASON=disabled"

###############################################################################
# Test 12: heartbeat 自动同步 task_branch（slot 内切分支后 lock 自动跟上）
###############################################################################
echo
echo "[T12] heartbeat 自动同步 task_branch"
CLAIM_OUT=$(bash "${AGENT_POOL_DIR}/agent-claim.sh" test/branch-d develop 2>/tmp/t12.err)
eval "${CLAIM_OUT}"
SLOT_D="${FFOA_AGENT_SLOT}"
SLOT_D_DIR="${FFOA_AGENT_DIR}"
SLOT_D_LOCK="${FFOA_AGENT_LOCK}"

# slot 内切到另一个分支（模拟 AI 干完一个 PR 后切下一个分支继续干的真实场景）
git -C "${SLOT_D_DIR}" checkout -b test/branch-e develop > /tmp/t12-checkout.log 2>&1 \
  && ok "在 slot 内切到 test/branch-e" || { cat /tmp/t12-checkout.log; fail "切分支失败"; }

# 等心跳至少跑一轮（AP_HEARTBEAT_INTERVAL=2，sleep 5 留出 race 余量）
sleep 5

synced_branch=$(awk -F= '/^task_branch=/{print $2; exit}' "${SLOT_D_LOCK}")
[[ "${synced_branch}" == "test/branch-e" ]] && ok "lock.task_branch 自动同步到 test/branch-e" \
  || { echo "lock 当前内容:"; cat "${SLOT_D_LOCK}"; fail "lock.task_branch 未同步（仍为 ${synced_branch}）"; }

bash "${AGENT_POOL_DIR}/agent-release.sh" "${SLOT_D}" > /tmp/t12-release.log 2>&1
unset FFOA_AGENT_SLOT FFOA_AGENT_DIR FFOA_AGENT_LOCK FFOA_AGENT_BRANCH

###############################################################################
# Test 13: orphan slot（远端无该分支 + 工作区干净 + commit 已合并）→ sweep 自动 release
###############################################################################
echo
echo "[T13] orphan slot → sweep 自动 release"

CLAIM_OUT=$(bash "${AGENT_POOL_DIR}/agent-claim.sh" test/branch-orphan-clean develop 2>/tmp/t13-claim.err)
eval "${CLAIM_OUT}"
SLOT_OC="${FFOA_AGENT_SLOT}"
unset FFOA_AGENT_SLOT FFOA_AGENT_DIR FFOA_AGENT_LOCK FFOA_AGENT_BRANCH

# 把 claimed_at 改成 1 小时前，绕过 AP_ORPHAN_MIN_AGE 阈值
old_iso=$(date -u -d "@$(($(date +%s) - 3600))" +%Y-%m-%dT%H:%M:%SZ)
sed -i -E "s/^claimed_at=.*/claimed_at=${old_iso}/" "${TEST_POOL_ROOT}/slot-${SLOT_OC}.lock"

# test/branch-orphan-clean 远端 origin 上不存在 → ls-remote 返回非 0 = "远端已删"
# 工作区刚 claim 干净；HEAD = origin/develop，无新 commit
bash "${AGENT_POOL_DIR}/agent-sweep.sh" > /tmp/t13-sweep.log 2>&1
[[ ! -f "${TEST_POOL_ROOT}/slot-${SLOT_OC}.lock" ]] && ok "orphan sweep 删除 lock" \
  || { cat /tmp/t13-sweep.log; fail "lock 还在"; }
park_check=$(git -C "${TEST_POOL_ROOT}/slot-${SLOT_OC}" rev-parse --abbrev-ref HEAD)
[[ "${park_check}" == "pool/slot-${SLOT_OC}" ]] && ok "工作区已切回 park" \
  || fail "未切回 park（当前 ${park_check}）"
grep -q "orphan" /tmp/t13-sweep.log && ok "sweep 日志包含 orphan 标记" \
  || { cat /tmp/t13-sweep.log; fail "sweep 日志缺 orphan 标记"; }

###############################################################################
# Test 14: orphan 老 + 工作区脏 → 不回收（保护未提交工作）
###############################################################################
echo
echo "[T14] orphan 老 + 脏工作区 → 不回收"

CLAIM_OUT=$(bash "${AGENT_POOL_DIR}/agent-claim.sh" test/branch-orphan-dirty develop 2>/tmp/t14-claim.err)
eval "${CLAIM_OUT}"
SLOT_OD="${FFOA_AGENT_SLOT}"
DIR_OD="${FFOA_AGENT_DIR}"
unset FFOA_AGENT_SLOT FFOA_AGENT_DIR FFOA_AGENT_LOCK FFOA_AGENT_BRANCH

old_iso=$(date -u -d "@$(($(date +%s) - 3600))" +%Y-%m-%dT%H:%M:%SZ)
sed -i -E "s/^claimed_at=.*/claimed_at=${old_iso}/" "${TEST_POOL_ROOT}/slot-${SLOT_OD}.lock"

echo "uncommitted-marker" > "${DIR_OD}/orphan-dirty-test.txt"

bash "${AGENT_POOL_DIR}/agent-sweep.sh" > /tmp/t14-sweep.log 2>&1
[[ -f "${TEST_POOL_ROOT}/slot-${SLOT_OD}.lock" ]] && ok "脏工作区不被 orphan sweep 回收" \
  || fail "脏工作区被错误回收"

# 清理：脏 slot 必须 --force 才真删（默认会标 abandoned）
bash "${AGENT_POOL_DIR}/agent-release.sh" "${SLOT_OD}" --force > /dev/null 2>&1

###############################################################################
# Test 15: orphan 但 claim 时间新（< AP_ORPHAN_MIN_AGE）→ 不回收（年龄保护）
###############################################################################
echo
echo "[T15] orphan 新 claim → 年龄保护，不回收"

# 用 60s 阈值（仍小于默认 300s，但够大让 claim 后立刻 sweep 时低于阈值）
export AP_ORPHAN_MIN_AGE=60

CLAIM_OUT=$(bash "${AGENT_POOL_DIR}/agent-claim.sh" test/branch-orphan-fresh develop 2>/tmp/t15-claim.err)
eval "${CLAIM_OUT}"
SLOT_OF="${FFOA_AGENT_SLOT}"
unset FFOA_AGENT_SLOT FFOA_AGENT_DIR FFOA_AGENT_LOCK FFOA_AGENT_BRANCH

bash "${AGENT_POOL_DIR}/agent-sweep.sh" > /tmp/t15-sweep.log 2>&1
[[ -f "${TEST_POOL_ROOT}/slot-${SLOT_OF}.lock" ]] && ok "新 claim 受年龄保护不被回收" \
  || { cat /tmp/t15-sweep.log; fail "新 claim 被误回收"; }

bash "${AGENT_POOL_DIR}/agent-release.sh" "${SLOT_OF}" > /dev/null 2>&1
unset AP_ORPHAN_MIN_AGE

###############################################################################
# Test 16: squash merge zombie → L2 累积 patch-id 识别 orphan
#
# 场景：feature 分支多 commit → squash merge 进 develop 形成单个累积 commit。
# squash 后 develop 上的全新 SHA 跟本地 HEAD 没祖先关系，L1 ancestor 失败；
# L2 累积 diff patch-id 应识别为已吸收。
###############################################################################
echo
echo "[T16] squash merge zombie → L2 累积 patch-id"

# 备份 origin/develop，测试结束后恢复（也在 cleanup trap 里兜底）
T16_ORIG_DEVELOP_SHA=$(git -C "${REPO_ROOT}" rev-parse refs/remotes/origin/develop 2>/dev/null || echo '')
if [[ -z "${T16_ORIG_DEVELOP_SHA}" ]]; then
  fail "T16: refs/remotes/origin/develop 不存在，跳过测试"
  T16_ORIG_DEVELOP_SHA=""
fi
export T16_ORIG_DEVELOP_SHA

CLAIM_OUT=$(bash "${AGENT_POOL_DIR}/agent-claim.sh" test/branch-squash-zombie develop 2>/tmp/t16-claim.err)
eval "${CLAIM_OUT}"
SLOT_SZ="${FFOA_AGENT_SLOT}"
DIR_SZ="${FFOA_AGENT_DIR}"
unset FFOA_AGENT_SLOT FFOA_AGENT_DIR FFOA_AGENT_LOCK FFOA_AGENT_BRANCH

# 在 slot 内造 3 个 commit（模拟 feature 分支累积变更）
for i in 1 2 3; do
  echo "squash-zombie line ${i}" >> "${DIR_SZ}/squash-zombie-test.txt"
  git -C "${DIR_SZ}" add squash-zombie-test.txt
  git -C "${DIR_SZ}" -c user.email=t16@test -c user.name=t16 commit -q -m "feat: squash zombie commit ${i}"
done

# slot HEAD 此刻领先 origin/develop 3 个 commit，merge-base = T16_ORIG_DEVELOP_SHA
SLOT_HEAD_TREE=$(git -C "${DIR_SZ}" rev-parse HEAD^{tree})

# 在主仓库造一个 squash 等价 commit：parent=原 develop，tree=slot HEAD 的累积 tree。
# 该 commit 跟原 develop 的 diff 完全等于 slot 的累积 diff → patch-id --stable 相等。
SQUASH_COMMIT_SHA=$(git -C "${REPO_ROOT}" -c user.email=t16@test -c user.name=t16 \
  commit-tree "${SLOT_HEAD_TREE}" -p "${T16_ORIG_DEVELOP_SHA}" -m "feat: squashed feature (#test)" 2>/dev/null || echo '')
if [[ -z "${SQUASH_COMMIT_SHA}" ]]; then
  fail "T16: commit-tree 失败，无法构造 squash commit"
else
  git -C "${REPO_ROOT}" update-ref refs/remotes/origin/develop "${SQUASH_COMMIT_SHA}"
fi

# 老化 claim 越过 AP_ORPHAN_MIN_AGE
old_iso=$(date -u -d "@$(($(date +%s) - 3600))" +%Y-%m-%dT%H:%M:%SZ)
sed -i -E "s/^claimed_at=.*/claimed_at=${old_iso}/" "${TEST_POOL_ROOT}/slot-${SLOT_SZ}.lock"

# sweep 应识别 orphan：远端无 test/branch-squash-zombie + 工作区干净 + L2 patch-id 命中
bash "${AGENT_POOL_DIR}/agent-sweep.sh" > /tmp/t16-sweep.log 2>&1

if [[ ! -f "${TEST_POOL_ROOT}/slot-${SLOT_SZ}.lock" ]]; then
  ok "L2 累积 patch-id 识别 squash zombie，sweep 回收 lock"
else
  echo "--- t16-sweep.log ---"
  cat /tmp/t16-sweep.log
  echo "--- slot-${SLOT_SZ}.lock ---"
  cat "${TEST_POOL_ROOT}/slot-${SLOT_SZ}.lock"
  echo "--- ap_branch_absorbed 诊断 ---"
  bash -c "source ${REPO_ROOT}/scripts/dev/lib/agent-pool-lib.sh; ap_branch_absorbed ${DIR_SZ} && echo absorbed=YES || echo absorbed=NO"
  fail "lock 还在（L2 累积 patch-id 未识别）"
fi

park_check=$(git -C "${TEST_POOL_ROOT}/slot-${SLOT_SZ}" rev-parse --abbrev-ref HEAD 2>/dev/null || echo '?')
[[ "${park_check}" == "pool/slot-${SLOT_SZ}" ]] && ok "工作区切回 park (pool/slot-${SLOT_SZ})" \
  || fail "未切回 park（当前 ${park_check}）"

# 恢复 origin/develop
git -C "${REPO_ROOT}" update-ref refs/remotes/origin/develop "${T16_ORIG_DEVELOP_SHA}"
unset T16_ORIG_DEVELOP_SHA

###############################################################################
# Test 17: sweep agent-dead path（隔离测试：先杀心跳避免心跳自检抢先）
###############################################################################
echo
echo "[T17] FFOA_AGENT_PPID 显式 + 父死 → sweep agent-dead 路径"

# 用 kill -0 探测找一个真不存在的 PID（兼容 Linux + macOS）。
# 从高位往下扫，第一个 kill -0 失败的就用它。范围 [99000, 100000) 足够分散。
find_dead_pid() {
  local p=99999
  while (( p > 99000 )); do
    if ! kill -0 "${p}" 2>/dev/null; then echo "${p}"; return; fi
    p=$((p - 1))
  done
  # 极端情况兜底（这个范围全占满的概率几乎为 0）
  echo 99999
}
DUMMY_PID=$(find_dead_pid)

# 关键：claim 时不带 FFOA_AGENT_PPID。否则心跳第一轮自检会先把 lock 清掉，
# sweep 跑时 lock 已经不在，agent-dead 路径不可能命中。
CLAIM_OUT=$(bash "${AGENT_POOL_DIR}/agent-claim.sh" test/branch-agent-dead develop 2>/tmp/t17-claim.err)
eval "${CLAIM_OUT}"
SLOT_AD="${FFOA_AGENT_SLOT}"
SLOT_AD_LOCK="${FFOA_AGENT_LOCK}"
unset FFOA_AGENT_SLOT FFOA_AGENT_DIR FFOA_AGENT_LOCK FFOA_AGENT_BRANCH

# 杀心跳，避免它继续刷新 heartbeat_at 影响其他判定
HB_PID=$(awk -F= '/^heartbeat_pid=/{print $2; exit}' "${SLOT_AD_LOCK}")
[[ -n "${HB_PID}" ]] && kill -9 "${HB_PID}" 2>/dev/null || true

# 心跳没了，手工往 lock 里注入 agent_ppid（用 dead PID）、老化 claim、清 heartbeat_pid
sed -i -E "s/^agent_ppid=.*/agent_ppid=${DUMMY_PID}/" "${SLOT_AD_LOCK}"
old_iso=$(date -u -d "@$(($(date +%s) - 3600))" +%Y-%m-%dT%H:%M:%SZ)
sed -i -E "s/^claimed_at=.*/claimed_at=${old_iso}/" "${SLOT_AD_LOCK}"
sed -i -E "s/^heartbeat_at=.*/heartbeat_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)/" "${SLOT_AD_LOCK}"
sed -i -E "s/^heartbeat_pid=.*/heartbeat_pid=/" "${SLOT_AD_LOCK}"

ap_ppid_lock=$(awk -F= '/^agent_ppid=/{print $2; exit}' "${SLOT_AD_LOCK}")
[[ "${ap_ppid_lock}" == "${DUMMY_PID}" ]] && ok "agent_ppid=${DUMMY_PID} 已注入 lock" \
  || fail "agent_ppid 字段错（${ap_ppid_lock}）"

bash "${AGENT_POOL_DIR}/agent-sweep.sh" > /tmp/t17-sweep.log 2>&1
[[ ! -f "${SLOT_AD_LOCK}" ]] && ok "sweep 回收 lock" \
  || { cat /tmp/t17-sweep.log; fail "lock 还在"; }
grep -q "agent-dead" /tmp/t17-sweep.log && ok "sweep 日志含 agent-dead 标记" \
  || { echo "--- t17-sweep.log ---"; cat /tmp/t17-sweep.log; fail "sweep 日志缺 agent-dead"; }

###############################################################################
# Test 18: agent-dead + dirty workspace → abandoned（sweep 路径走 release 分级）
###############################################################################
echo
echo "[T18] agent-dead + 脏 workspace → sweep 转 abandoned"

DUMMY_PID2=$(find_dead_pid)
# 确保跟 T17 的 DUMMY_PID 不撞（哪怕极端情况下都被分配）
[[ "${DUMMY_PID2}" == "${DUMMY_PID:-}" ]] && DUMMY_PID2=$((DUMMY_PID2 - 1))

CLAIM_OUT=$(bash "${AGENT_POOL_DIR}/agent-claim.sh" \
  test/branch-agent-dead-dirty develop 2>/tmp/t18-claim.err)
eval "${CLAIM_OUT}"
SLOT_ADD="${FFOA_AGENT_SLOT}"
DIR_ADD="${FFOA_AGENT_DIR}"
LOCK_ADD="${FFOA_AGENT_LOCK}"
unset FFOA_AGENT_SLOT FFOA_AGENT_DIR FFOA_AGENT_LOCK FFOA_AGENT_BRANCH

# 杀心跳 + 注入 agent_ppid（dead）+ 脏工作区
HB_PID=$(awk -F= '/^heartbeat_pid=/{print $2; exit}' "${LOCK_ADD}")
[[ -n "${HB_PID}" ]] && kill -9 "${HB_PID}" 2>/dev/null || true
sed -i -E "s/^agent_ppid=.*/agent_ppid=${DUMMY_PID2}/" "${LOCK_ADD}"
echo "dirty content" > "${DIR_ADD}/agent-dead-dirty.txt"

old_iso=$(date -u -d "@$(($(date +%s) - 3600))" +%Y-%m-%dT%H:%M:%SZ)
sed -i -E "s/^claimed_at=.*/claimed_at=${old_iso}/" "${LOCK_ADD}"
sed -i -E "s/^heartbeat_at=.*/heartbeat_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)/" "${LOCK_ADD}"
sed -i -E "s/^heartbeat_pid=.*/heartbeat_pid=/" "${LOCK_ADD}"

bash "${AGENT_POOL_DIR}/agent-sweep.sh" > /tmp/t18-sweep.log 2>&1
[[ -f "${LOCK_ADD}" ]] && ok "lock 保留（脏 workspace 转 abandoned）" \
  || { cat /tmp/t18-sweep.log; fail "lock 被错误删除"; }
state_v=$(awk -F= '/^state=/{print $2; exit}' "${LOCK_ADD}")
[[ "${state_v}" == "abandoned" ]] && ok "state=abandoned" \
  || { cat "${LOCK_ADD}"; fail "state=${state_v}"; }
reason_v=$(awk -F= '/^abandoned_reason=/{print $2; exit}' "${LOCK_ADD}")
[[ "${reason_v}" == "dirty" ]] && ok "reason=dirty" || fail "reason=${reason_v}"
[[ -f "${DIR_ADD}/agent-dead-dirty.txt" ]] && ok "脏文件保留（worktree 未 reset）" \
  || fail "脏文件被错误清掉"

# claim 不应分配到 abandoned slot
set +e
bash "${AGENT_POOL_DIR}/agent-claim.sh" test/branch-must-skip-abandoned develop \
  > /tmp/t18-claim2.out 2> /tmp/t18-claim2.err
ec18=$?
set -e
if [[ "${ec18}" == 3 ]]; then
  ok "claim 跳过 abandoned slot（池满 exit 3）"
elif [[ "${ec18}" == 0 ]]; then
  new_slot=$(grep '^export FFOA_AGENT_SLOT=' /tmp/t18-claim2.out | cut -d= -f2)
  [[ "${new_slot}" != "${SLOT_ADD}" ]] && ok "claim 落到非 abandoned slot (${new_slot} ≠ ${SLOT_ADD})" \
    || fail "claim 错误分配到 abandoned slot"
  bash "${AGENT_POOL_DIR}/agent-release.sh" "${new_slot}" > /dev/null 2>&1 || true
else
  fail "claim 退出码异常: ${ec18}"
fi

bash "${AGENT_POOL_DIR}/agent-release.sh" "${SLOT_ADD}" --force > /dev/null 2>&1 || true

for b in test/branch-agent-dead test/branch-agent-dead-dirty test/branch-must-skip-abandoned; do
  git -C "${REPO_ROOT}" branch -D "${b}" 2>/dev/null || true
done

###############################################################################
# Test 19: claim 本地分支落后远端 → fast-forward 同步
#
# 场景：上次干完留了本地 ref；之后远端有新 commit（CI / 别人 push 了 fixup）。
# claim 进来后 HEAD 应自动 ff 到远端最新，不能拿 stale HEAD 继续干活。
###############################################################################
echo
echo "[T19] claim 本地落后远端 → ff 同步"

# 老 SHA = origin/develop；新 SHA = 在 develop 上多造 1 个 commit
DEV_SHA=$(git -C "${REPO_ROOT}" rev-parse refs/remotes/origin/develop)
TMP_WT=$(mktemp -d -t t19-wt.XXXXXX)
git -C "${REPO_ROOT}" worktree add --quiet "${TMP_WT}" "${DEV_SHA}" 2>/dev/null
echo "t19-ff content" > "${TMP_WT}/t19-ff-marker.txt"
git -C "${TMP_WT}" add t19-ff-marker.txt
git -C "${TMP_WT}" -c user.email=t19@test -c user.name=t19 commit -q -m "feat: t19 ff marker"
NEW_SHA=$(git -C "${TMP_WT}" rev-parse HEAD)

# 准备：本地 ref @ 老 SHA，远端 ref @ 新 SHA（本地是远端 ancestor）
git -C "${REPO_ROOT}" update-ref refs/heads/test/branch-ff-sync "${DEV_SHA}"
git -C "${REPO_ROOT}" update-ref "refs/remotes/origin/test/branch-ff-sync" "${NEW_SHA}"

CLAIM_OUT=$(bash "${AGENT_POOL_DIR}/agent-claim.sh" test/branch-ff-sync develop 2>/tmp/t19-claim.err)
eval "${CLAIM_OUT}"
SLOT_FF="${FFOA_AGENT_SLOT}"
DIR_FF="${FFOA_AGENT_DIR}"
unset FFOA_AGENT_SLOT FFOA_AGENT_DIR FFOA_AGENT_LOCK FFOA_AGENT_BRANCH

post_head=$(git -C "${DIR_FF}" rev-parse HEAD)
[[ "${post_head}" == "${NEW_SHA}" ]] && ok "HEAD 已 ff 到远端最新 (${NEW_SHA:0:8})" \
  || { cat /tmp/t19-claim.err; fail "HEAD 未 ff（当前 ${post_head:0:8}，期望 ${NEW_SHA:0:8}）"; }
grep -q "fast-forward" /tmp/t19-claim.err && ok "claim stderr 含 fast-forward 提示" \
  || { cat /tmp/t19-claim.err; fail "缺 ff 提示"; }

# --force：测试构造的远端 ref 在真实 origin 不存在 → release 会判 unpushed 标 abandoned。
# 测试场景下用 --force 强制真释放，保证 slot 回 free 让后续测试能 claim。
bash "${AGENT_POOL_DIR}/agent-release.sh" "${SLOT_FF}" --force > /dev/null 2>&1 || true
git -C "${REPO_ROOT}" worktree remove --force "${TMP_WT}" 2>/dev/null || true
git -C "${REPO_ROOT}" update-ref -d refs/remotes/origin/test/branch-ff-sync 2>/dev/null || true

###############################################################################
# Test 20: claim 本地领先远端 → 不动 HEAD + 警告
###############################################################################
echo
echo "[T20] claim 本地领先远端 → 不动 + 警告"

# 准备：先 claim 拿到 slot 状态，造一个 commit，release（abandon path 保住本地），
# 然后人工把状态修成"远端是老的、本地是新的"
DEV_SHA=$(git -C "${REPO_ROOT}" rev-parse refs/remotes/origin/develop)

# 直接构造：本地 ref @ DEV_SHA+1 commit；远端 ref @ DEV_SHA
TMP_WT2=$(mktemp -d -t t20-wt.XXXXXX)
git -C "${REPO_ROOT}" worktree add --quiet "${TMP_WT2}" "${DEV_SHA}" 2>/dev/null
echo "t20-ahead content" > "${TMP_WT2}/t20-ahead-marker.txt"
git -C "${TMP_WT2}" add t20-ahead-marker.txt
git -C "${TMP_WT2}" -c user.email=t20@test -c user.name=t20 commit -q -m "feat: t20 local ahead"
AHEAD_SHA=$(git -C "${TMP_WT2}" rev-parse HEAD)

git -C "${REPO_ROOT}" update-ref refs/heads/test/branch-ahead-warn "${AHEAD_SHA}"
git -C "${REPO_ROOT}" update-ref refs/remotes/origin/test/branch-ahead-warn "${DEV_SHA}"

CLAIM_OUT=$(bash "${AGENT_POOL_DIR}/agent-claim.sh" test/branch-ahead-warn develop 2>/tmp/t20-claim.err)
eval "${CLAIM_OUT}"
SLOT_AHD="${FFOA_AGENT_SLOT}"
DIR_AHD="${FFOA_AGENT_DIR}"
unset FFOA_AGENT_SLOT FFOA_AGENT_DIR FFOA_AGENT_LOCK FFOA_AGENT_BRANCH

post_head=$(git -C "${DIR_AHD}" rev-parse HEAD)
[[ "${post_head}" == "${AHEAD_SHA}" ]] && ok "HEAD 保持本地领先 SHA (${AHEAD_SHA:0:8})" \
  || fail "本地领先 SHA 被错改（当前 ${post_head:0:8}）"
grep -q "领先远端" /tmp/t20-claim.err && ok "claim stderr 含 '领先远端' 警告" \
  || { cat /tmp/t20-claim.err; fail "缺领先警告"; }

bash "${AGENT_POOL_DIR}/agent-release.sh" "${SLOT_AHD}" --force > /dev/null 2>&1 || true
git -C "${REPO_ROOT}" worktree remove --force "${TMP_WT2}" 2>/dev/null || true
git -C "${REPO_ROOT}" update-ref -d refs/remotes/origin/test/branch-ahead-warn 2>/dev/null || true

###############################################################################
# Test 21: release clean + 已吸收 → 删本地 task 分支 ref
###############################################################################
echo
echo "[T21] release clean + 已吸收 → 本地 ref 被删"

# claim 一个新分支（直接 from develop，HEAD = origin/develop tip → L1 ancestor 命中）
CLAIM_OUT=$(bash "${AGENT_POOL_DIR}/agent-claim.sh" test/branch-release-cleanup develop 2>/tmp/t21-claim.err)
eval "${CLAIM_OUT}"
SLOT_RC="${FFOA_AGENT_SLOT}"
unset FFOA_AGENT_SLOT FFOA_AGENT_DIR FFOA_AGENT_LOCK FFOA_AGENT_BRANCH

# 校验本地 ref 存在
git -C "${REPO_ROOT}" show-ref --verify --quiet refs/heads/test/branch-release-cleanup \
  && ok "release 前本地 ref 存在" || fail "claim 后本地 ref 应存在"

bash "${AGENT_POOL_DIR}/agent-release.sh" "${SLOT_RC}" > /tmp/t21-release.log 2>&1

if git -C "${REPO_ROOT}" show-ref --verify --quiet refs/heads/test/branch-release-cleanup; then
  cat /tmp/t21-release.log
  fail "release 后本地 ref 应被删（已被 develop 吸收）"
else
  ok "release 后本地 ref 已删除"
fi
grep -q "删除本地分支" /tmp/t21-release.log && ok "release stderr 含删除提示" \
  || { cat /tmp/t21-release.log; fail "缺删除提示"; }

###############################################################################
echo
echo "=========================================="
echo "测试结果: 通过 ${PASS} / 失败 ${FAIL}"
if [[ "${FAIL}" -gt 0 ]]; then
  echo
  echo "失败列表:"
  for t in "${FAILED_TESTS[@]}"; do echo "  - ${t}"; done
  exit 1
fi
echo "全部通过 ✓"
