#!/usr/bin/env bash
# phase2_keys.sh — Phase 2（API Key 自助管理端点）真实环境 e2e 验收（任务 P2-T9）。
#
# 覆盖 docs/plan/001-agent-mcp-development-plan.md §2.5 的 6 条验收 + 2 条补充，
# 全部走真实 HTTP API（不插库），断言一律按 body code（response.Error 恒 HTTP 200）：
#   ① 用户 A 创建 key（scope=["caasm:view_assets"]，带 expires_at）→ code 0，明文返回且带 sk-agent- 前缀
#   ② A 列表只见自己；B 创建一把后，A 用 B 的 key id Get → body code 404
#   ③ A 创建时 scope 含 "auth:manage_users"（自己没有的权限）→ body code 403
#   ④ A 创建时 scope 含 "caasm:manage_assets"（只读白名单外）→ body code 403
#   ⑤ A 吊销自己的 key → code 0 + status=revoked；再用该明文调 /caasm/assets → body code 401（AgentAuth 联动）
#   ⑤b A rotate → code 0 + 新明文返回（id 换新）；旧 id Get → status=revoked；新明文调 /caasm/assets → code 0
#   ⑥ admin 列表 → 能看到 A 和 B 的 key（需提供 ADMIN_TOKEN，否则跳过）
#   ⑦ 无 agent:manage_own_keys 权限的用户调创建 → body code 403（需提供 LOW_PRIV_TOKEN，否则跳过）
#
# 环境变量：
#   BASE_URL          后端地址，默认 http://127.0.0.1:8080
#   USER_A_TOKEN      用户 A 的 access_token（需持有 agent:manage_own_keys + caasm:view_assets）【必填】
#   USER_B_TOKEN      用户 B 的 access_token（权限同 A）【必填】
#   ADMIN_TOKEN       admin 角色用户的 access_token，用于场景⑥；不提供则跳过⑥
#   LOW_PRIV_TOKEN    无 agent:manage_own_keys 权限用户的 access_token，用于场景⑦；不提供则跳过⑦
#   EXPIRES_IN_HOURS  创建 key 的过期时长（小时），默认 24（生产 allow_permanent_key=false 时 expires_at 必填）
#
# 依赖：bash、curl；jq 可选（无 jq 用 grep 兜底）。
# 用法：USER_A_TOKEN=xxx USER_B_TOKEN=yyy bash security-center-backend/tests/e2e/phase2_keys.sh
# 可重跑：每次运行创建全新 key，退出时按 owner 身份尽量吊销本轮造出的 key（trap 清理）。
set -euo pipefail

BASE_URL="${BASE_URL:-http://127.0.0.1:8080}"
USER_A_TOKEN="${USER_A_TOKEN:-}"
USER_B_TOKEN="${USER_B_TOKEN:-}"
ADMIN_TOKEN="${ADMIN_TOKEN:-}"
LOW_PRIV_TOKEN="${LOW_PRIV_TOKEN:-}"
EXPIRES_IN_HOURS="${EXPIRES_IN_HOURS:-24}"

PASS_COUNT=0
FAIL_COUNT=0

log()  { printf '[phase2-e2e] %s\n' "$*"; }
pass() { PASS_COUNT=$((PASS_COUNT + 1)); printf '  [PASS] %s\n' "$*"; }
fail() { FAIL_COUNT=$((FAIL_COUNT + 1)); printf '  [FAIL] %s\n' "$*" >&2; }

# ---------------------------------------------------------------------------
# 前置检查：依赖命令 + server 可达性 + 必填 token，缺失项列全后 exit 1
# ---------------------------------------------------------------------------
preflight() {
  local missing=()
  command -v curl >/dev/null 2>&1 || missing+=("curl")
  command -v jq  >/dev/null 2>&1 || log "提示: 未安装 jq, 字段提取降级为 grep 匹配"
  [[ -n "$USER_A_TOKEN" ]] || missing+=("USER_A_TOKEN 未提供（用户 A 的 access_token）")
  [[ -n "$USER_B_TOKEN" ]] || missing+=("USER_B_TOKEN 未提供（用户 B 的 access_token）")

  if command -v curl >/dev/null 2>&1; then
    curl -sf --max-time 5 "${BASE_URL}/health" >/dev/null 2>&1 ||
      missing+=("server 不可达: ${BASE_URL}/health (请确认后端已启动, 或用 BASE_URL 指定)")
  fi

  if ((${#missing[@]} > 0)); then
    log "前置检查未通过, 缺失/不可达项:"
    printf '  - %s\n' "${missing[@]}" >&2
    exit 1
  fi
  log "前置检查通过: server=${BASE_URL}"
}

# ---------------------------------------------------------------------------
# HTTP 断言（一律按 body code，不断言 HTTP 状态码）
# ---------------------------------------------------------------------------
http_call() { # method path [header|--curl-opt value]... → stdout = response body
  local method="$1" path="$2"
  shift 2
  local args=(-sS --max-time 10 -X "$method")
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --*) args+=("$1" "$2"); shift 2 ;;  # curl 原生选项（如 --data-raw <json>）原样透传
      *)   args+=(-H "$1"); shift ;;      # 其余按请求头处理
    esac
  done
  curl "${args[@]}" "${BASE_URL}${path}"
}

body_code() { # body → code 字段值
  local body="$1"
  if command -v jq >/dev/null 2>&1; then
    jq -r '.code // empty' <<<"$body"
  else
    grep -o '"code":[0-9-]*' <<<"$body" | head -1 | cut -d: -f2
  fi
}

body_message() {
  local body="$1"
  if command -v jq >/dev/null 2>&1; then
    jq -r '.message // empty' <<<"$body"
  else
    grep -o '"message":"[^"]*"' <<<"$body" | head -1 | sed 's/"message":"//; s/"$//'
  fi
}

body_data_field() { # body field → .data.<field> 字符串值（仅适用于字符串/数字字段）
  local body="$1" field="$2"
  if command -v jq >/dev/null 2>&1; then
    jq -r ".data.${field} // empty" <<<"$body"
  else
    grep -o "\"${field}\":\"[^\"]*\"" <<<"$body" | head -1 | sed "s/\"${field}\":\"//; s/\"\$//"
  fi
}

list_contains_id() { # body id → 列表 data 中是否含该 id
  local body="$1" id="$2"
  if command -v jq >/dev/null 2>&1; then
    [[ "$(jq --argjson want "$id" '[.data[]?.id] | index($want) != null' <<<"$body")" == "true" ]]
  else
    grep -q "\"id\":${id}" <<<"$body"
  fi
}

assert_body_code() { # name body want_code [want_message_substr]
  local name="$1" body="$2" want_code="$3" want_msg="${4:-}"
  local code msg
  code="$(body_code "$body")"
  msg="$(body_message "$body")"
  if [[ "$code" != "$want_code" ]]; then
    fail "${name}: body code=${code:-<无code字段>}, 期望 ${want_code}; body=${body}"
    return 1
  fi
  if [[ -n "$want_msg" && "$msg" != *"$want_msg"* ]]; then
    fail "${name}: message=${msg}, 期望包含 '${want_msg}'; body=${body}"
    return 1
  fi
  pass "${name}: body code=${code}${msg:+, message=\"${msg}\"}"
}

# ---------------------------------------------------------------------------
# 小工具：过期时间（兼容 BSD/GNU date）
# ---------------------------------------------------------------------------
expires_at_iso() {
  if date -u -v+1H +%Y-%m-%dT%H:%M:%SZ >/dev/null 2>&1; then
    date -u -v+"${EXPIRES_IN_HOURS}"H +%Y-%m-%dT%H:%M:%SZ      # macOS (BSD date)
  else
    date -u -d "+${EXPIRES_IN_HOURS} hours" +%Y-%m-%dT%H:%M:%SZ # GNU date
  fi
}

# ---------------------------------------------------------------------------
# 清理：按 owner 身份吊销本轮造出且仍 active 的 key（幂等，异常退出也能重跑）
# ---------------------------------------------------------------------------
A_KEY_ID=""        # 场景① A 的 key（场景⑥ 中应可见）
B_KEY_ID=""        # 场景② B 的 key
A_ROTATE_NEW_ID="" # 场景⑤b rotate 产生的新 key

revoke_quiet() { # token id
  local token="$1" id="$2"
  [[ -n "$id" ]] || return 0
  http_call DELETE "/api/v1/agent/api-keys/${id}" "Authorization: Bearer ${token}" >/dev/null 2>&1 || true
}

cleanup() {
  [[ -n "$A_KEY_ID" || -n "$A_ROTATE_NEW_ID" || -n "$B_KEY_ID" ]] || return 0
  revoke_quiet "$USER_A_TOKEN" "$A_KEY_ID"
  revoke_quiet "$USER_A_TOKEN" "$A_ROTATE_NEW_ID"
  revoke_quiet "$USER_B_TOKEN" "$B_KEY_ID"
  log "已尽量清理本轮创建的 key（已 revoked 的保持原状）"
}
trap cleanup EXIT

# ---------------------------------------------------------------------------
# 主流程
# ---------------------------------------------------------------------------
main() {
  preflight

  # 场景①: A 创建 key → code 0 + 明文返回
  body="$(http_call POST /api/v1/agent/api-keys \
    "Authorization: Bearer ${USER_A_TOKEN}" "Content-Type: application/json" \
    --data-raw "{\"agent_name\":\"e2e-a-bot\",\"permission_scope\":[\"caasm:view_assets\"],\"expires_at\":\"$(expires_at_iso)\"}")"
  assert_body_code "① A 创建 key" "$body" 0 || { log "创建失败, 无法继续"; exit 1; }
  A_KEY_ID="$(body_data_field "$body" id)"
  a_plaintext="$(body_data_field "$body" api_key)"
  if [[ -n "$a_plaintext" && "$a_plaintext" == sk-agent-* ]]; then
    pass "① 明文一次性返回且带 sk-agent- 前缀 (id=${A_KEY_ID})"
  else
    fail "① 明文缺失或前缀异常: api_key='${a_plaintext}'; body=${body}"
  fi

  # 场景②: 列表隔离 + 越权 Get 404
  body="$(http_call GET /api/v1/agent/api-keys "Authorization: Bearer ${USER_A_TOKEN}")"
  assert_body_code "② A 列表可拉取" "$body" 0
  if list_contains_id "$body" "$A_KEY_ID"; then
    pass "② A 列表包含 A 自己的 key (id=${A_KEY_ID})"
  else
    fail "② A 列表缺少 A 的 key id=${A_KEY_ID}; body=${body}"
  fi

  body="$(http_call POST /api/v1/agent/api-keys \
    "Authorization: Bearer ${USER_B_TOKEN}" "Content-Type: application/json" \
    --data-raw "{\"agent_name\":\"e2e-b-bot\",\"permission_scope\":[\"caasm:view_assets\"],\"expires_at\":\"$(expires_at_iso)\"}")"
  assert_body_code "② B 创建 key（为越权用例备料）" "$body" 0 || { log "B 创建失败, 无法继续"; exit 1; }
  B_KEY_ID="$(body_data_field "$body" id)"

  body="$(http_call GET /api/v1/agent/api-keys "Authorization: Bearer ${USER_A_TOKEN}")"
  if list_contains_id "$body" "$B_KEY_ID"; then
    fail "② A 列表不应包含 B 的 key id=${B_KEY_ID}; body=${body}"
  else
    pass "② A 列表不包含 B 的 key (id=${B_KEY_ID})"
  fi
  body="$(http_call GET "/api/v1/agent/api-keys/${B_KEY_ID}" "Authorization: Bearer ${USER_A_TOKEN}")"
  assert_body_code "② A 用 B 的 key id Get → 404" "$body" 404

  # 场景③: scope 含自己没有的 auth:manage_users → 403
  body="$(http_call POST /api/v1/agent/api-keys \
    "Authorization: Bearer ${USER_A_TOKEN}" "Content-Type: application/json" \
    --data-raw "{\"agent_name\":\"e2e-a-escalation\",\"permission_scope\":[\"caasm:view_assets\",\"auth:manage_users\"],\"expires_at\":\"$(expires_at_iso)\"}")"
  assert_body_code "③ scope 含 auth:manage_users 被拒" "$body" 403 "scope exceeds"

  # 场景④: scope 含只读白名单外的 caasm:manage_assets → 403
  body="$(http_call POST /api/v1/agent/api-keys \
    "Authorization: Bearer ${USER_A_TOKEN}" "Content-Type: application/json" \
    --data-raw "{\"agent_name\":\"e2e-a-write\",\"permission_scope\":[\"caasm:manage_assets\"],\"expires_at\":\"$(expires_at_iso)\"}")"
  assert_body_code "④ scope 含 caasm:manage_assets 被拒" "$body" 403 "scope exceeds"

  # 场景⑤: A 吊销自己的 key → code 0 + status=revoked；该明文调接口 → 401（AgentAuth 联动）
  body="$(http_call POST /api/v1/agent/api-keys \
    "Authorization: Bearer ${USER_A_TOKEN}" "Content-Type: application/json" \
    --data-raw "{\"agent_name\":\"e2e-a-revoke\",\"permission_scope\":[\"caasm:view_assets\"],\"expires_at\":\"$(expires_at_iso)\"}")"
  assert_body_code "⑤ A 创建待吊销 key" "$body" 0 || { log "创建失败, 无法继续"; exit 1; }
  revoke_id="$(body_data_field "$body" id)"
  revoke_plaintext="$(body_data_field "$body" api_key)"

  body="$(http_call DELETE "/api/v1/agent/api-keys/${revoke_id}" "Authorization: Bearer ${USER_A_TOKEN}")"
  assert_body_code "⑤ A 吊销自己的 key" "$body" 0
  if [[ "$(body_data_field "$body" status)" == "revoked" ]]; then
    pass "⑤ 吊销响应 status=revoked (id=${revoke_id})"
  else
    fail "⑤ 吊销响应 status 应为 revoked; body=${body}"
  fi
  body="$(http_call GET /api/v1/caasm/assets "X-API-Key: ${revoke_plaintext}")"
  assert_body_code "⑤ 被吊销明文调 /caasm/assets → 401" "$body" 401 "revoked"

  # 场景⑤b: rotate → 新明文 + id 换新 + 旧 id revoked + 新明文可用
  body="$(http_call POST /api/v1/agent/api-keys \
    "Authorization: Bearer ${USER_A_TOKEN}" "Content-Type: application/json" \
    --data-raw "{\"agent_name\":\"e2e-a-rotate\",\"permission_scope\":[\"caasm:view_assets\"],\"expires_at\":\"$(expires_at_iso)\"}")"
  assert_body_code "⑤b A 创建待 rotate key" "$body" 0 || { log "创建失败, 无法继续"; exit 1; }
  rotate_old_id="$(body_data_field "$body" id)"

  body="$(http_call POST "/api/v1/agent/api-keys/${rotate_old_id}/rotate" "Authorization: Bearer ${USER_A_TOKEN}")"
  assert_body_code "⑤b A rotate" "$body" 0
  A_ROTATE_NEW_ID="$(body_data_field "$body" id)"
  rotate_new_plaintext="$(body_data_field "$body" api_key)"
  if [[ -n "$A_ROTATE_NEW_ID" && "$A_ROTATE_NEW_ID" != "$rotate_old_id" && -n "$rotate_new_plaintext" ]]; then
    pass "⑤b rotate 返回新明文且 id 换新 (${rotate_old_id} → ${A_ROTATE_NEW_ID})"
  else
    fail "⑤b rotate 响应异常: new_id=${A_ROTATE_NEW_ID}, old_id=${rotate_old_id}; body=${body}"
  fi
  body="$(http_call GET "/api/v1/agent/api-keys/${rotate_old_id}" "Authorization: Bearer ${USER_A_TOKEN}")"
  assert_body_code "⑤b 旧 id Get 成功" "$body" 0
  if [[ "$(body_data_field "$body" status)" == "revoked" ]]; then
    pass "⑤b 旧 id status=revoked"
  else
    fail "⑤b 旧 id status 应为 revoked; body=${body}"
  fi
  if [[ -n "$rotate_new_plaintext" ]]; then
    body="$(http_call GET /api/v1/caasm/assets "X-API-Key: ${rotate_new_plaintext}")"
    assert_body_code "⑤b 新明文调 /caasm/assets → 0（AgentAuth 联动）" "$body" 0
  fi

  # 场景⑥（可选）: admin 列表能看到 A 和 B 的 key
  if [[ -n "$ADMIN_TOKEN" ]]; then
    body="$(http_call GET /api/v1/agent/api-keys "Authorization: Bearer ${ADMIN_TOKEN}")"
    assert_body_code "⑥ admin 列表可拉取" "$body" 0
    if list_contains_id "$body" "$A_KEY_ID"; then
      pass "⑥ admin 列表包含 A 的 key (id=${A_KEY_ID})"
    else
      fail "⑥ admin 列表缺少 A 的 key id=${A_KEY_ID}; body=${body}"
    fi
    if list_contains_id "$body" "$B_KEY_ID"; then
      pass "⑥ admin 列表包含 B 的 key (id=${B_KEY_ID})"
    else
      fail "⑥ admin 列表缺少 B 的 key id=${B_KEY_ID}; body=${body}"
    fi
  else
    log "  [SKIP] ⑥ 未提供 ADMIN_TOKEN, 跳过 admin 跨 owner 列表（OAuth 登录后取得 access_token 再跑）"
  fi

  # 场景⑦（可选）: 无 agent:manage_own_keys 权限的用户调创建 → 403
  if [[ -n "$LOW_PRIV_TOKEN" ]]; then
    body="$(http_call POST /api/v1/agent/api-keys \
      "Authorization: Bearer ${LOW_PRIV_TOKEN}" "Content-Type: application/json" \
      --data-raw "{\"agent_name\":\"e2e-lowpriv\",\"permission_scope\":[\"caasm:view_assets\"],\"expires_at\":\"$(expires_at_iso)\"}")"
    assert_body_code "⑦ 无 agent:manage_own_keys 用户创建被拒" "$body" 403
  else
    log "  [SKIP] ⑦ 未提供 LOW_PRIV_TOKEN, 跳过 Permission 层拦截用例（进程内测试已覆盖）"
  fi

  log "结果: PASS=${PASS_COUNT}, FAIL=${FAIL_COUNT}"
  ((FAIL_COUNT == 0)) || exit 1
  log "Phase 2 真实环境验收全部通过"
}

main "$@"
