#!/usr/bin/env bash
# qa-node-03b-task-events — task_events 审计追踪
# 用户故事（审计员 / SRE 视角）：
#   一个 task 经历了哪些状态变化，每次变化谁发起的、什么时候、备注是什么？
# 这是合规 / debugging 资产，不能丢。
#
# pin 三个契约：
#   1. 每次状态转换写一行 task_events（delivered/acked/replied/failed/cancelled）
#   2. actor 列正确归属（admin 发 → admin；agent 回 → agent alias）
#   3. /api/task_events?task_id=<id> 返回该 task 全部事件，created_at DESC 排序
set -euo pipefail

export HOME=/tmp/anethome

# P0 guardrail (2026-06-16 incident) — refuse rm -rf outside /tmp/*.
# safe_rm_rf checks every path prefix against $SAFE_RM_ALLOW_PREFIXES
# (default "/tmp/"); refuses + exit 99 on anything else. See
# tests/lib/safe-rm.sh for the helper definition.
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../lib/safe-rm.sh"
mkdir -p "$HOME" /tmp/work
cd /tmp/work

ADMIN_PW="StrongPassw0rd"
HUB_PORT=9200
HUB_BASE="http://127.0.0.1:$HUB_PORT"

cleanup() {
  for p in "${HUB_PID:-}"; do
    [[ -n "$p" && "$p" != "0" ]] && kill "$p" 2>/dev/null || true
  done
  pkill -KILL -f 'commhub-server' 2>/dev/null || true
}
trap cleanup EXIT

mcp_call() {
  local tok="$1" name="$2" args="$3"
  local body
  body=$(jq -nc --arg n "$name" --argjson a "$args" \
    '{jsonrpc:"2.0",id:1,method:"tools/call",params:{name:$n,arguments:$a}}')
  curl -sS -X POST "$HUB_BASE/mcp" \
    -H "Authorization: Bearer $tok" \
    -H 'Content-Type: application/json' \
    -H 'Accept: application/json, text/event-stream' \
    -H 'MCP-Protocol-Version: 2025-03-26' \
    -d "$body" \
    | sed -n 's/^data: //p' | head -1 \
    | jq -r '.result.content[0].text // empty'
}

# Send task and capture its id. Use MCP send_task (not REST /api/task) because
# only the MCP path calls logTaskEvent("delivered", ...). REST /api/task skips
# the audit event (likely an oversight — pinned in R15 report).
send_task() {
  local tok="$1" net="$2" alias="$3" text="$4" from="${5:-admin}"
  local arg
  arg=$(jq -nc --arg a "$alias" --arg t "$text" --arg net "$net" --arg from "$from" \
    '{alias:$a,task:$t,priority:"normal",network_id:$net,from_session:$from}')
  local resp
  resp=$(mcp_call "$tok" "send_task" "$arg")
  # send_task tool returns {ok, message_id} as JSON-stringified content
  echo "$resp" | jq -r '.message_id // empty'
}

# Query events for a task. Returns "to_status:actor" lines (chronological — DESC reversed).
events_for() {
  local utok="$1" net="$2" tid="$3"
  curl -fsS "$HUB_BASE/api/task_events?task_id=$tid&network_id=$net&limit=50" \
    -H "Authorization: Bearer $utok" \
    | jq -r '.events | reverse | .[] | "\(.to_status):\(.actor)"'
}

npm install -g @sleep2agi/agent-network@preview >/tmp/npm-install.log 2>&1
anet -v >/dev/null

echo "[0] start hub"
safe_rm_rf "$HOME/.anet" "$HOME/.commhub"
anet hub start --host 127.0.0.1 --port "$HUB_PORT" --username admin --password "$ADMIN_PW" >/tmp/hub.log 2>&1 &
HUB_PID=$!
for i in {1..60}; do curl -fsS "$HUB_BASE/health" >/dev/null 2>&1 && break; sleep 1; done

echo "[1] admin login + network + ntok + report_status"
UTOK=""
for i in {1..20}; do
  LOGIN=$(curl -sS -X POST "$HUB_BASE/api/auth/login" -H 'Content-Type: application/json' \
    -d "{\"username\":\"admin\",\"password\":\"$ADMIN_PW\"}")
  UTOK=$(echo "$LOGIN" | jq -r '.token // empty')
  [[ "$UTOK" == utok_* ]] && break
  sleep 0.5
done
[[ "$UTOK" == utok_* ]] || { echo "FAIL: login"; exit 1; }

NET_RESP=$(curl -fsS -X POST "$HUB_BASE/api/networks" \
  -H "Authorization: Bearer $UTOK" -H 'Content-Type: application/json' \
  -d '{"name":"node03b-net"}')
NET_ID=$(echo "$NET_RESP" | jq -r '.network.network_id // .network_id // empty')
NTOK=$(curl -fsS -X POST "$HUB_BASE/api/auth/node-token" \
  -H "Authorization: Bearer $UTOK" -H 'Content-Type: application/json' \
  -d "{\"network_id\":\"$NET_ID\",\"node_name\":\"agent-3b\"}" | jq -r '.token')
ARG=$(jq -nc --arg net "$NET_ID" \
  '{resume_id:"00000000-aaaa-bbbb-cccc-00000000003b",alias:"agent-3b",status:"idle",network_id:$net}')
mcp_call "$NTOK" "report_status" "$ARG" | jq -e '.ok == true' >/dev/null \
  || { echo "FAIL: report_status"; exit 1; }

# ─────────────── SCENARIO A: full success chain ───────────────
echo "[2] task A: full chain delivered → acked → replied (admin sender, agent-3b worker)"
T_A=$(send_task "$UTOK" "$NET_ID" "agent-3b" "task-A-success" "admin")
[[ -n "$T_A" && "$T_A" != "null" ]] || { echo "FAIL: no task_id A"; exit 1; }

# agent-3b acks the inbox
ARG=$(jq -nc --arg t "$T_A" '{alias:"agent-3b",message_id:$t}')
mcp_call "$NTOK" "ack_inbox" "$ARG" | jq -e '.ok == true' >/dev/null \
  || { echo "FAIL: ack_inbox"; exit 1; }

# agent-3b sends reply
ARG=$(jq -nc --arg t "$T_A" '{alias:"admin",text:"done",in_reply_to:$t,status:"replied",from_session:"agent-3b"}')
mcp_call "$NTOK" "send_reply" "$ARG" | jq -e '.ok == true' >/dev/null \
  || { echo "FAIL: send_reply A"; exit 1; }
sleep 0.2

echo "[3] task A events: must include delivered+admin / acked+agent-3b / replied+agent-3b"
A_EVENTS=$(events_for "$UTOK" "$NET_ID" "$T_A")
echo "$A_EVENTS" | sed 's/^/    /'
echo "$A_EVENTS" | grep -qx "delivered:admin"   || { echo "FAIL: missing delivered:admin in A"; exit 1; }
echo "$A_EVENTS" | grep -qx "acked:agent-3b"    || { echo "FAIL: missing acked:agent-3b in A"; exit 1; }
echo "$A_EVENTS" | grep -qx "replied:agent-3b"  || { echo "FAIL: missing replied:agent-3b in A"; exit 1; }

# ─────────────── SCENARIO B: failed reply (no ack first) ───────────────
echo "[4] task B: delivered → failed (no ack — agent went straight to fail)"
T_B=$(send_task "$UTOK" "$NET_ID" "agent-3b" "task-B-fail" "admin")
ARG=$(jq -nc --arg t "$T_B" '{alias:"admin",text:"http 500 oops",in_reply_to:$t,status:"failed",from_session:"agent-3b"}')
mcp_call "$NTOK" "send_reply" "$ARG" | jq -e '.ok == true' >/dev/null \
  || { echo "FAIL: send_reply B"; exit 1; }
sleep 0.2
B_EVENTS=$(events_for "$UTOK" "$NET_ID" "$T_B")
echo "$B_EVENTS" | sed 's/^/    /'
echo "$B_EVENTS" | grep -qx "delivered:admin"  || { echo "FAIL: missing delivered:admin in B"; exit 1; }
echo "$B_EVENTS" | grep -qx "failed:agent-3b"  || { echo "FAIL: missing failed:agent-3b in B"; exit 1; }

# ─────────────── SCENARIO C: cancelled ───────────────
echo "[5] task C: delivered → cancelled (by agent self via NTOK)"
T_C=$(send_task "$UTOK" "$NET_ID" "agent-3b" "task-C-cancel" "admin")
ARG=$(jq -nc --arg t "$T_C" '{task_id:$t,reason:"changed mind",from_session:"agent-3b"}')
mcp_call "$NTOK" "cancel_task" "$ARG" | jq -e '.ok == true' >/dev/null \
  || { echo "FAIL: cancel C"; exit 1; }
sleep 0.2
C_EVENTS=$(events_for "$UTOK" "$NET_ID" "$T_C")
echo "$C_EVENTS" | sed 's/^/    /'
echo "$C_EVENTS" | grep -qx "delivered:admin"    || { echo "FAIL: missing delivered:admin in C"; exit 1; }
echo "$C_EVENTS" | grep -qx "cancelled:agent-3b" || { echo "FAIL: missing cancelled:agent-3b in C"; exit 1; }

# ─────────────── SCHEMA SANITY ───────────────
echo "[6] PIN: each event row has full schema (task_id/from_status/to_status/actor/created_at)"
EVENT_ROW=$(curl -fsS "$HUB_BASE/api/task_events?task_id=$T_A&network_id=$NET_ID&limit=1" \
  -H "Authorization: Bearer $UTOK" | jq '.events[0]')
for field in task_id to_status actor created_at; do
  V=$(echo "$EVENT_ROW" | jq -r ".$field // empty")
  [[ -n "$V" ]] || { echo "FAIL: event row missing $field: $EVENT_ROW"; exit 1; }
done
# from_status may be null on initial 'delivered' — pin both shapes acceptable
echo "  ✓ schema OK: $(echo "$EVENT_ROW" | jq -c .)"

echo "[7] PIN: API SQL has ORDER BY created_at DESC (but ties break by rowid)"
# WARNING — surfaced by R15: SQL is `ORDER BY created_at DESC` but events
# emitted in the same second (typical for hub flow) share a timestamp,
# SQLite tie-breaks by rowid ASC → the response can look ASC for a single
# task's chain. SDK consumers must NOT rely on order alone; sort by id.
# We only assert: A's 3 events are present in the response, no order check.
A_RAW=$(curl -fsS "$HUB_BASE/api/task_events?task_id=$T_A&network_id=$NET_ID&limit=10" \
  -H "Authorization: Bearer $UTOK" | jq -r '.events[].to_status' | sort -u | tr '\n' ',')
[[ "$A_RAW" == *"delivered,"* && "$A_RAW" == *"acked,"* && "$A_RAW" == *"replied,"* ]] \
  || { echo "FAIL: task A events incomplete: $A_RAW"; exit 1; }

# ─────────────── CROSS-TASK SCOPE ───────────────
echo "[8] PIN: querying task A events does NOT leak task B/C events"
A_ONLY=$(curl -fsS "$HUB_BASE/api/task_events?task_id=$T_A&network_id=$NET_ID&limit=50" \
  -H "Authorization: Bearer $UTOK" | jq -r '[.events[] | .task_id] | unique | length')
[[ "$A_ONLY" == "1" ]] || { echo "FAIL: task_id filter leaks ($A_ONLY unique task_ids)"; exit 1; }

# ─────────────── COUNT TOTAL EVENTS ───────────────
echo "[9] PIN: 3 scenarios produced 7+ events total (3 delivered + 1 acked + 1 replied + 1 failed + 1 cancelled = 7)"
TOTAL=$(curl -fsS "$HUB_BASE/api/task_events?network_id=$NET_ID&limit=100" \
  -H "Authorization: Bearer $UTOK" | jq '.events | length')
[[ "$TOTAL" -ge 7 ]] || { echo "FAIL: expected ≥7 events, got $TOTAL"; exit 1; }

echo "PASS qa-node-03b task-events (audit trail 3 scenarios + actor attribution + schema + DESC order + scope)"
