#!/usr/bin/env bash
# phase3_quota_openapi.sh — Phase 3（配额 + OpenAPI 端点）真实环境 e2e 验收（任务 P3-T10）。
#
# 覆盖 docs/plan/001-agent-mcp-development-plan.md §3.3/§3.4 的验收标准，
# 全部走真实 HTTP API + 真实 Redis（server 侧配置生效）：
#   ① 造 key → 以 QUOTA_RPM+2 个请求打 /api/v1/caasm/assets：
#      前 QUOTA_RPM 个 body code 0，后 2 个 body code 429 + Retry-After 响应头存在
#      （QUOTA_EXPECTED=enabled，默认；需 QUOTA_RPM 与 server 的 agent.default_quota_rpm 一致）
#   ② 关配额模式（QUOTA_EXPECTED=disabled，运维先以 agent.quota_enabled=false 重启 server）：
#      10 个请求全部 code 0，无 429
#   ③ agent key GET /api/v1/openapi.json → 真 HTTP 200 + paths=35
#      + securityDefinitions 含 agentApiKey/bearerAuth 两键（该端点裸 JSON 输出，非信封）
#   ④ 无效 key 调 openapi.json → body code 401
#   ⑤ 回归：agent key POST /api/v1/caasm/ingest → body code 403（路由白名单）
#
# 环境变量：
#   BASE_URL          后端地址，默认 http://127.0.0.1:8080
#   USER_TOKEN        造 key 用户的 access_token（需 agent:manage_own_keys + caasm:view_assets）【必填】
#   QUOTA_EXPECTED    enabled（默认）| disabled；disabled 时需先把 server 以
#                     agent.quota_enabled=false 重启再跑本脚本
#   QUOTA_RPM         server 配置的 agent.default_quota_rpm【QUOTA_EXPECTED=enabled 时必填】
#   EXPIRES_IN_HOURS  验收 key 的过期时长（小时），默认 1
#
# 依赖：bash、curl；jq 可选（无 jq 用 grep 兜底）。
# 用法：
#   USER_TOKEN=xxx QUOTA_RPM=3 bash security-center-backend/tests/e2e/phase3_quota_openapi.sh
#   # 运维关配额重启后：
#   USER_TOKEN=xxx QUOTA_EXPECTED=disabled bash security-center-backend/tests/e2e/phase3_quota_openapi.sh
# 可重跑：每次运行创建全新 key，退出时尽量吊销（trap 清理）。
# 注意：burst 在秒级内完成，极小概率跨过真实分钟边界导致窗口重置、后 2 个请求不被 429；
#       遇到时直接重跑即可（进程内测试用虚拟窗口规避了该问题，真实环境无法注入时钟）。
set -euo pipefail

BASE_URL="${BASE_URL:-http://127.0.0.1:8080}"
USER_TOKEN="${USER_TOKEN:-}"
QUOTA_EXPECTED="${QUOTA_EXPECTED:-enabled}"
QUOTA_RPM="${QUOTA_RPM:-}"
EXPIRES_IN_HOURS="${EXPIRES_IN_HOURS:-1}"

PASS_COUNT=0
FAIL_COUNT=0

log()  { printf '[phase3-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 可达性 + 必填参数 + server 侧 Redis 生效
# ---------------------------------------------------------------------------
preflight() {
  local missing=()
  command -v curl >/dev/null 2>&1 || missing+=("curl")
  command -v jq  >/dev/null 2>&1 || log "提示: 未安装 jq, 字段提取降级为 grep 匹配"
  [[ -n "$USER_TOKEN" ]] || missing+=("USER_TOKEN 未提供（造 key 用户的 access_token）")
  case "$QUOTA_EXPECTED" in
    enabled)
      # 先正则判数字再比较: set -u 下 -gt 遇非数字串会按未绑定变量致命退出。
      [[ "$QUOTA_RPM" =~ ^[0-9]+$ ]] && ((QUOTA_RPM > 0)) ||
        missing+=("QUOTA_EXPECTED=enabled 时 QUOTA_RPM 必填且为正整数（与 server 的 agent.default_quota_rpm 一致）")
      ;;
    disabled)
      log "提示: disabled 模式, 请确认 server 已以 agent.quota_enabled=false 重启"
      ;;
    *)
      missing+=("QUOTA_EXPECTED 只能是 enabled|disabled, 当前: ${QUOTA_EXPECTED}")
      ;;
  esac

  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}, quota_mode=${QUOTA_EXPECTED}${QUOTA_RPM:+, rpm=${QUOTA_RPM}}"
}

# ---------------------------------------------------------------------------
# HTTP 工具（信封接口断言按 body code；openapi.json 为裸 JSON 断言真 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}"
}

http_call_full() { # method path hdr_out body_out [header|--curl-opt value]... → 响应头/体分别写入两个文件路径变量
  local method="$1" path="$2" hdr_out="$3" body_out="$4"
  shift 4
  local args=(-sS --max-time 10 -X "$method" -D "$hdr_out" -o "$body_out")
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --*) args+=("$1" "$2"); shift 2 ;;  # curl 原生选项原样透传
      *)   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_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
}

assert_body_code() { # name body want_code
  local name="$1" body="$2" want_code="$3"
  local code
  code="$(body_code "$body")"
  if [[ "$code" != "$want_code" ]]; then
    fail "${name}: body code=${code:-<无code字段>}, 期望 ${want_code}; body=${body}"
    return 1
  fi
  pass "${name}: body code=${code}"
}

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
}

# ---------------------------------------------------------------------------
# 清理：吊销本轮造出的 key（幂等）
# ---------------------------------------------------------------------------
KEY_ID=""
cleanup() {
  [[ -n "$KEY_ID" ]] || return 0
  http_call DELETE "/api/v1/agent/api-keys/${KEY_ID}" "Authorization: Bearer ${USER_TOKEN}" >/dev/null 2>&1 || true
  log "已吊销本轮验收 key (id=${KEY_ID})"
}
trap cleanup EXIT

# ---------------------------------------------------------------------------
# 场景①②: 配额 burst / 关配额直通
# ---------------------------------------------------------------------------
check_quota() { # plaintext
  local plaintext="$1"
  local hdr_file body_file i code
  hdr_file="$(mktemp)"; body_file="$(mktemp)"
  trap 'rm -f "$hdr_file" "$body_file"' RETURN

  if [[ "$QUOTA_EXPECTED" == "enabled" ]]; then
    local total=$((QUOTA_RPM + 2)) ok429=0
    for ((i = 1; i <= total; i++)); do
      http_call_full GET /api/v1/caasm/assets "$hdr_file" "$body_file" "X-API-Key: ${plaintext}"
      code="$(body_code "$(cat "$body_file")")"
      if ((i <= QUOTA_RPM)); then
        if [[ "$code" != "0" ]]; then
          fail "① 第 ${i} 个请求应在限额内放行, code=${code:-<无>}; body=$(cat "$body_file")"
          return 1
        fi
      else
        if [[ "$code" != "429" ]]; then
          fail "① 第 ${i} 个请求应 429, code=${code:-<无>}; body=$(cat "$body_file")（可能跨过分钟边界, 可重跑）"
          return 1
        fi
        if ! grep -qi '^Retry-After:' "$hdr_file"; then
          fail "① 第 ${i} 个 429 响应缺少 Retry-After 头; headers=$(cat "$hdr_file")"
          return 1
        fi
        ok429=$((ok429 + 1))
      fi
    done
    pass "① rpm=${QUOTA_RPM} burst $((QUOTA_RPM + 2)) 请求: 前 ${QUOTA_RPM} 放行, 后 ${ok429} 个 429 + Retry-After"
  else
    for ((i = 1; i <= 10; i++)); do
      http_call_full GET /api/v1/caasm/assets "$hdr_file" "$body_file" "X-API-Key: ${plaintext}"
      code="$(body_code "$(cat "$body_file")")"
      if [[ "$code" == "429" ]]; then
        fail "② 配额已关闭但第 ${i} 个请求 429; body=$(cat "$body_file")（server 未以 quota_enabled=false 重启?）"
        return 1
      fi
      if [[ "$code" != "0" ]]; then
        fail "② 第 ${i} 个请求应放行, code=${code:-<无>}; body=$(cat "$body_file")"
        return 1
      fi
    done
    pass "② 配额关闭: 10 请求全部放行, 无 429"
  fi
}

# ---------------------------------------------------------------------------
# 场景③: openapi.json 全量断言（裸 JSON, 断言真 HTTP 状态码）
# ---------------------------------------------------------------------------
check_openapi() { # plaintext
  local plaintext="$1" hdr_file body_file status
  hdr_file="$(mktemp)"; body_file="$(mktemp)"
  trap 'rm -f "$hdr_file" "$body_file"' RETURN

  status="$(curl -sS --max-time 10 -o "$body_file" -D "$hdr_file" -w '%{http_code}' \
    -H "X-API-Key: ${plaintext}" "${BASE_URL}/api/v1/openapi.json")"
  if [[ "$status" != "200" ]]; then
    fail "③ GET /api/v1/openapi.json HTTP 状态=${status}, 期望 200; body=$(cat "$body_file")"
    return 1
  fi
  if ! grep -qi '^Content-Type:.*application/json' "$hdr_file"; then
    fail "③ Content-Type 非 JSON; headers=$(cat "$hdr_file")"
    return 1
  fi

  local paths_count
  if command -v jq >/dev/null 2>&1; then
    paths_count="$(jq -r '.paths | length' "$body_file")"
  else
    paths_count="$(grep -o '"/api/v1/[^"]*"' "$body_file" | sort -u | wc -l | tr -d ' ')"
  fi
  if [[ "$paths_count" != "35" ]]; then
    fail "③ openapi.json paths=${paths_count}, 期望 35"
    return 1
  fi

  if command -v jq >/dev/null 2>&1; then
    if [[ "$(jq -r '.securityDefinitions | has("agentApiKey") and has("bearerAuth")' "$body_file")" != "true" ]]; then
      fail "③ securityDefinitions 缺 agentApiKey/bearerAuth; body=$(head -c 500 "$body_file")"
      return 1
    fi
  else
    grep -q '"agentApiKey"' "$body_file" && grep -q '"bearerAuth"' "$body_file" || {
      fail "③ securityDefinitions 缺 agentApiKey/bearerAuth（grep 兜底）"; return 1; }
  fi
  pass "③ openapi.json: HTTP 200 + JSON + paths=35 + securityDefinitions 含 agentApiKey/bearerAuth"
}

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

  # 造 key（真实 API, 不插库）
  local body plaintext plaintext_openapi
  body="$(http_call POST /api/v1/agent/api-keys \
    "Authorization: Bearer ${USER_TOKEN}" "Content-Type: application/json" \
    --data-raw "{\"agent_name\":\"e2e-phase3-quota\",\"permission_scope\":[\"caasm:view_assets\"],\"expires_at\":\"$(expires_at_iso)\"}")"
  assert_body_code "备料: 创建验收 key" "$body" 0 || { log "创建失败, 无法继续"; exit 1; }
  KEY_ID="$(body_data_field "$body" id)"
  plaintext="$(body_data_field "$body" api_key)"
  if [[ -z "$plaintext" ]]; then
    log "创建响应未携带明文 api_key, 无法继续; body=${body}"
    exit 1
  fi

  check_quota "$plaintext"

  # quota burst 已打满当前分钟窗口（后 2 个请求就是 429），同窗口内用同一把 key 调
  # openapi.json / ingest 也会被 429（body code 429 而非预期结果）——换一把新 key
  # （独立配额计数）做后续验收，人类 JWT 创建请求不计配额。
  body="$(http_call POST /api/v1/agent/api-keys \
    "Authorization: Bearer ${USER_TOKEN}" "Content-Type: application/json" \
    --data-raw "{\"agent_name\":\"e2e-phase3-openapi\",\"permission_scope\":[\"caasm:view_assets\"],\"expires_at\":\"$(expires_at_iso)\"}")"
  assert_body_code "备料: 创建第二把验收 key（避开已打满的配额窗口）" "$body" 0 || { log "创建失败, 无法继续"; exit 1; }
  plaintext_openapi="$(body_data_field "$body" api_key)"
  if [[ -z "$plaintext_openapi" ]]; then
    log "第二把 key 创建响应未携带明文, 无法继续; body=${body}"
    exit 1
  fi

  check_openapi "$plaintext_openapi"

  # 场景④: 无效 key → body code 401
  body="$(http_call GET /api/v1/openapi.json "X-API-Key: sk-agent-e2e-phase3-invalid")"
  assert_body_code "④ 无效 key 调 openapi.json → 401" "$body" 401

  # 场景⑤: 白名单回归 —— agent key POST /caasm/ingest → body code 403
  body="$(http_call POST /api/v1/caasm/ingest "X-API-Key: ${plaintext_openapi}")"
  assert_body_code "⑤ agent key POST /caasm/ingest → 403（白名单回归）" "$body" 403

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

main "$@"
