#!/usr/bin/env bash
# phase1_auth.sh — Phase 1（Agent API Key 认证链路）真实环境 e2e 验收（任务 P1-T14）。
#
# 覆盖 docs/plan/001-agent-mcp-development-plan.md §1.9 的 5+1 条场景：
#   ① 有效 agent key 调白名单内只读端点 GET /api/v1/caasm/assets → body code 0
#   ② 同 key 调 scope 不足的 GET /api/v1/users → body code 403
#   ③ 同 key 调无权限码路由 POST /api/v1/caasm/ingest → body code 403（路由白名单拦截）
#   ④ SQL 吊销后 → body code 401（message 含 revoked）
#   ⑤ SQL 查 audit_logs：存在 actor_type='agent' 且 metadata.agent_id = 该 key id 的记录
#   ⑥ 回归（可选）：人类 JWT（ADMIN_TOKEN）调 GET /api/v1/users → body code 0
#
# 响应约定（docs/tasks/001-agent-mcp-tasks-overview.md §5 第 5 条）：
# response.Error 恒返回 HTTP 200 + body code，故所有断言一律按 body code，
# 不断言 HTTP 状态码。
#
# 环境变量：
#   BASE_URL        后端地址，默认 http://127.0.0.1:8080
#   MYSQL_DSN       形如 user:password@tcp(host:port)/dbname；不提供则用下方离散变量
#   MYSQL_HOST      默认 127.0.0.1
#   MYSQL_PORT      默认 3306
#   MYSQL_USER      默认 root
#   MYSQL_PASSWORD  默认空
#   MYSQL_DATABASE  默认 security_center
#   OWNER_USER_ID   key 归属用户 id；不提供则取 users 表最小 id
#   ADMIN_TOKEN     人类 admin access_token（OAuth 登录后取得），用于场景⑥；不提供则跳过⑥
#
# 依赖：bash、curl、mysql 客户端、sha256sum 或 shasum；jq 可选（无 jq 用 grep 兜底）。
# 用法：bash security-center-backend/tests/e2e/phase1_auth.sh
set -euo pipefail

BASE_URL="${BASE_URL:-http://127.0.0.1:8080}"
MYSQL_DSN="${MYSQL_DSN:-}"
MYSQL_HOST="${MYSQL_HOST:-127.0.0.1}"
MYSQL_PORT="${MYSQL_PORT:-3306}"
MYSQL_USER="${MYSQL_USER:-root}"
MYSQL_PASSWORD="${MYSQL_PASSWORD:-}"
MYSQL_DATABASE="${MYSQL_DATABASE:-security_center}"
OWNER_USER_ID="${OWNER_USER_ID:-}"
ADMIN_TOKEN="${ADMIN_TOKEN:-}"

PASS_COUNT=0
FAIL_COUNT=0

log()  { printf '[phase1-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/MySQL 可达性，缺失项列全后 exit 1
# ---------------------------------------------------------------------------
preflight() {
  local missing=()
  command -v curl  >/dev/null 2>&1 || missing+=("curl")
  command -v mysql >/dev/null 2>&1 || missing+=("mysql 客户端")
  if ! command -v sha256sum >/dev/null 2>&1 && ! command -v shasum >/dev/null 2>&1; then
    missing+=("sha256sum 或 shasum")
  fi
  command -v jq >/dev/null 2>&1 || log "提示: 未安装 jq, 断言降级为 grep 匹配"

  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 command -v mysql >/dev/null 2>&1; then
    run_mysql "SELECT 1" >/dev/null 2>&1 ||
      missing+=("MySQL 不可达: $(mysql_target_desc) (请确认 MySQL 已启动或调整 MYSQL_* 变量)")
  fi

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

# ---------------------------------------------------------------------------
# MySQL 访问（支持 MYSQL_DSN 或离散变量）
# ---------------------------------------------------------------------------
parse_dsn() {
  # user:password@tcp(host:port)/dbname → 覆盖离散变量
  local dsn="$1"
  local creds hostport
  creds="${dsn%%@*}"          # user:password
  MYSQL_USER="${creds%%:*}"
  MYSQL_PASSWORD="${creds#*:}"
  hostport="${dsn#*tcp(}"     # host:port)/dbname
  hostport="${hostport%%)*}"
  MYSQL_HOST="${hostport%%:*}"
  MYSQL_PORT="${hostport#*:}"
  MYSQL_DATABASE="${dsn##*/}"
  MYSQL_DATABASE="${MYSQL_DATABASE%%\?*}"
}

mysql_target_desc() {
  echo "${MYSQL_USER}@${MYSQL_HOST}:${MYSQL_PORT}/${MYSQL_DATABASE}"
}

run_mysql() {
  mysql --batch --skip-column-names \
    -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" \
    ${MYSQL_PASSWORD:+-p"$MYSQL_PASSWORD"} "$MYSQL_DATABASE" -e "$1"
}

sha256_hex() {
  if command -v sha256sum >/dev/null 2>&1; then
    printf '%s' "$1" | sha256sum | awk '{print $1}'
  else
    printf '%s' "$1" | shasum -a 256 | awk '{print $1}'
  fi
}

# ---------------------------------------------------------------------------
# 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
}

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
  fi
  if [[ -n "$want_msg" && "$msg" != *"$want_msg"* ]]; then
    fail "${name}: message=${msg}, 期望包含 '${want_msg}'; body=${body}"
    return
  fi
  pass "${name}: body code=${code}${msg:+, message=\"${msg}\"}"
}

# ---------------------------------------------------------------------------
# 清理：删除本脚本造的 key 及其审计记录（幂等，脚本异常退出也能重跑）
# ---------------------------------------------------------------------------
RAW_KEY="sk-agent-e2e-$(date +%s)-$(od -An -N4 -tx1 /dev/urandom | tr -d ' \n')"
KEY_HASH="$(sha256_hex "$RAW_KEY")"
KEY_ID=""

cleanup() {
  [[ -z "$KEY_HASH" ]] && return 0
  run_mysql "DELETE FROM audit_logs WHERE actor_type='agent'
             AND JSON_UNQUOTE(JSON_EXTRACT(metadata, '\$.agent_id')) = CAST('${KEY_ID:-0}' AS CHAR);" >/dev/null 2>&1 || true
  run_mysql "DELETE FROM api_keys WHERE key_hash='${KEY_HASH}';" >/dev/null 2>&1 || true
  log "已清理测试 key 与其审计记录"
}
trap cleanup EXIT

# ---------------------------------------------------------------------------
# 主流程
# ---------------------------------------------------------------------------
main() {
  if [[ -n "$MYSQL_DSN" ]]; then parse_dsn "$MYSQL_DSN"; fi
  preflight

  # 步骤 1: 插库造 key（Phase 1 无管理端点, plan §1.9 验收即插库）
  if [[ -z "$OWNER_USER_ID" ]]; then
    OWNER_USER_ID="$(run_mysql "SELECT id FROM users ORDER BY id LIMIT 1;")"
  fi
  if [[ -z "$OWNER_USER_ID" ]]; then
    log "users 表为空且未提供 OWNER_USER_ID, 无法造 key"
    exit 1
  fi
  run_mysql "INSERT INTO api_keys
      (key_prefix, key_hash, agent_name, owner_user_id, permission_scope, status, created_at, updated_at)
      VALUES (LEFT('${RAW_KEY}', 16), '${KEY_HASH}', 'e2e-agent', ${OWNER_USER_ID},
              '[\"caasm:view_assets\"]', 'active', NOW(6), NOW(6));"
  KEY_ID="$(run_mysql "SELECT id FROM api_keys WHERE key_hash='${KEY_HASH}';")"
  [[ -n "$KEY_ID" ]] || { log "插库造 key 失败"; exit 1; }
  log "已造 key id=${KEY_ID}, owner=${OWNER_USER_ID}, scope=[caasm:view_assets]"

  # 场景①: 有效 key 调白名单内只读端点 → body code 0
  body="$(http_call GET /api/v1/caasm/assets "X-API-Key: ${RAW_KEY}")"
  assert_body_code "① 有效 key 调 /caasm/assets" "$body" 0

  # 场景②: 同 key 调 scope 不足的 /users → body code 403
  # (链路顺序: 白名单先于 Permission 拦截, 两层任一拒绝均为 403)
  body="$(http_call GET /api/v1/users "X-API-Key: ${RAW_KEY}")"
  assert_body_code "② agent key 调 /users 被拒" "$body" 403

  # 场景③: 同 key 调无权限码路由 POST /caasm/ingest → 403 + 白名单文案
  body="$(http_call POST /api/v1/caasm/ingest "X-API-Key: ${RAW_KEY}")"
  assert_body_code "③ agent key POST /caasm/ingest 被白名单拦截" "$body" 403 "not allowed"

  # 场景④: SQL 吊销 → 401 + revoked 文案
  run_mysql "UPDATE api_keys SET status='revoked' WHERE id=${KEY_ID};"
  body="$(http_call GET /api/v1/caasm/assets "X-API-Key: ${RAW_KEY}")"
  assert_body_code "④ 吊销后调 /caasm/assets" "$body" 401 "revoked"

  # 场景⑤: audit_logs 存在 actor_type='agent' 且 metadata.agent_id=KEY_ID 的记录
  audit_count="$(run_mysql "SELECT COUNT(*) FROM audit_logs WHERE actor_type='agent'
      AND JSON_UNQUOTE(JSON_EXTRACT(metadata, '\$.agent_id')) = CAST('${KEY_ID}' AS CHAR);")"
  if [[ "${audit_count:-0}" -ge 1 ]]; then
    pass "⑤ 审计归因: actor_type='agent' 且 metadata.agent_id=${KEY_ID} 的记录 ${audit_count} 条"
  else
    fail "⑤ 审计归因: 未找到 actor_type='agent' 且 metadata.agent_id=${KEY_ID} 的 audit_logs 记录"
  fi

  # 场景⑥（可选）: 人类 JWT admin 路径不受 agent 通道影响
  if [[ -n "$ADMIN_TOKEN" ]]; then
    body="$(http_call GET /api/v1/users "Authorization: Bearer ${ADMIN_TOKEN}")"
    assert_body_code "⑥ 人类 admin JWT 调 /users 放行" "$body" 0
  else
    log "  [SKIP] ⑥ 未提供 ADMIN_TOKEN, 跳过人类 JWT 回归（登录为 OAuth, 需先取得 access_token）"
  fi

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

main "$@"
