#!/usr/bin/env bash
# e2e 一键跑通：
#   1. 启 docker compose（postgres + redis）
#   2. 复制 .env.e2e → .env，注入强随机 JWT 密钥
#   3. migrate --seed，跑 LocalDemoSeeder（admin/12345678 + demo_player）
#   4. php artisan serve 起应用
#   5. 启动 queue:work（异步开奖/广播）
#   6. 启动 reverb:start
#   7. npx playwright install chromium（仅缺浏览器缓存时）
#   8. npx playwright test
#   9. 失败时打 docker logs + Laravel 日志；结束时清理 compose

set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$ROOT_DIR"

E2E_DIR="$ROOT_DIR/e2e"
COMPOSE_FILE="$E2E_DIR/docker-compose.yml"
ENV_FILE="$ROOT_DIR/.env"
ENV_E2E="$E2E_DIR/.env.e2e"
ARTIFACT_DIR="$E2E_DIR/artifacts"
LOG_DIR="$E2E_DIR/logs"

mkdir -p "$ARTIFACT_DIR" "$LOG_DIR"

API_URL="http://127.0.0.1:8000"
PUBLIC_URL="$API_URL/api/v1"
PIDS=()

cleanup() {
  local code=$?
  trap - INT TERM EXIT
  echo
  echo "==> Cleaning up e2e processes (code=$code)…"
  for pid in "${PIDS[@]:-}"; do
    [[ -n "$pid" ]] && kill "$pid" 2>/dev/null || true
  done
  # docker compose 留给用户决定是否 down，避免误删 pg volume
  exit "$code"
}
trap cleanup INT TERM EXIT

require() {
  command -v "$1" >/dev/null 2>&1 || { echo "Missing: $1"; exit 2; }
}
require docker
require php
require node
require npx

playwright_cache_dir() {
  case "$(uname -s)" in
    Darwin) echo "$HOME/Library/Caches/ms-playwright" ;;
    Linux)  echo "${XDG_CACHE_HOME:-$HOME/.cache}/ms-playwright" ;;
    MINGW*|MSYS*|CYGWIN*) echo "${LOCALAPPDATA:-$HOME/AppData/Local}/ms-playwright" ;;
    *) echo "${XDG_CACHE_HOME:-$HOME/.cache}/ms-playwright" ;;
  esac
}

playwright_chromium_cached() {
  local cache_dir
  cache_dir="$(playwright_cache_dir)"
  # UI 项目依赖 chromium_headless_shell；API 仅 request 时可能不触发，但一并检测。
  compgen -G "${cache_dir}/chromium-"* >/dev/null 2>&1 \
    || compgen -G "${cache_dir}/chromium_headless_shell-"* >/dev/null 2>&1 \
    || [[ -d "$E2E_DIR/node_modules/playwright-core/.local-browsers" ]]
}

echo "==> [1/8] docker compose up -d"
if ! docker image inspect postgres:16-alpine >/dev/null 2>&1 \
    || ! docker image inspect redis:7-alpine >/dev/null 2>&1; then
  echo "    pulling postgres/redis images (docker-credential-desktop workaround)"
  mkdir -p /tmp/docker-e2e-nocreds
  printf '%s\n' '{"auths":{}}' > /tmp/docker-e2e-nocreds/config.json
  docker --config /tmp/docker-e2e-nocreds pull postgres:16-alpine
  docker --config /tmp/docker-e2e-nocreds pull redis:7-alpine
fi
if ! docker info >/dev/null 2>&1; then
  cat <<'EOF' >&2
!! Docker daemon 未运行。

启动 Docker Desktop / OrbStack / Colima 后重试。

  macOS Docker Desktop: 打开 Docker.app
  macOS OrbStack:        open -a OrbStack
  macOS Colima:          colima start

或者不使用 docker，直接本机起 PG（端口 15432）+ Redis（端口 16379），
跳过这一步，只跑 [4/8] 起的 .env + 后面步骤。
EOF
  exit 5
fi
docker compose -f "$COMPOSE_FILE" up -d

echo "==> [2/8] wait for pg/redis health"
for i in {1..30}; do
  if docker compose -f "$COMPOSE_FILE" ps --format json | grep -q '"Health":"healthy"' \
      || docker compose -f "$COMPOSE_FILE" ps | grep -E "(healthy|running)" >/dev/null; then
    pg_ok=$(docker compose -f "$COMPOSE_FILE" exec -T postgres pg_isready -U lottery -d lottery_e2e 2>/dev/null || true)
    redis_ok=$(docker compose -f "$COMPOSE_FILE" exec -T redis redis-cli ping 2>/dev/null || true)
    if [[ "$pg_ok" == *"accepting connections"* && "$redis_ok" == "PONG" ]]; then
      echo "    pg/redis healthy"
      break
    fi
  fi
  sleep 1
  if [[ $i -eq 30 ]]; then
    echo "!! pg/redis not healthy after 30s; dumping compose logs"
    docker compose -f "$COMPOSE_FILE" logs --no-color
    exit 3
  fi
done

echo "==> [3/8] install composer deps if missing"
if [[ ! -d vendor ]]; then
  composer install --no-interaction --prefer-dist
fi

echo "==> [4/8] prepare .env"
if [[ ! -f "$ENV_FILE" ]]; then
  cp "$ENV_E2E" "$ENV_FILE"
fi
# 同步关键 e2e 变量（不覆盖已存在的 APP_KEY，避免触发额外 key:generate 流程）
python3 - <<PY
import os, re
src = "$ENV_E2E"
dst = "$ENV_FILE"
with open(src) as f: new = f.read()
with open(dst) as f: cur = f.read()
# 用 e2e 模板值覆盖，仅保留 APP_KEY
for line in new.splitlines():
    if not line or line.lstrip().startswith("#") or "=" not in line:
        continue
    k, v = line.split("=", 1)
    if k.strip() == "APP_KEY":
        continue
    pattern = re.compile(rf"^{re.escape(k.strip())}=.*$", re.M)
    if pattern.search(cur):
        cur = pattern.sub(f"{k.strip()}={v}", cur)
    else:
        cur += ("\n" if not cur.endswith("\n") else "") + line
with open(dst, "w") as f: f.write(cur)
PY

# 强制覆盖关键 secret，避免与主站/生产共用
php -r '
$env = "'"$ENV_FILE"'";
$c = file_get_contents($env);
$c = preg_replace("/^LOTTERY_NATIVE_JWT_SECRET=.*$/m", "LOTTERY_NATIVE_JWT_SECRET=" . bin2hex(random_bytes(32)), $c);
$c = preg_replace("/^REVERB_APP_SECRET=.*$/m", "REVERB_APP_SECRET=" . bin2hex(random_bytes(16)), $c);
file_put_contents($env, $c);
'

if ! grep -q "^APP_KEY=base64:" "$ENV_FILE"; then
  php artisan key:generate --force
fi

echo "==> [5/8] lottery:db-init --fresh (e2e environment only)"
# 注意：AGENTS.md 要求 migrate:fresh 须用户确认；本脚本为 e2e 自动化专用，仅作用于
# docker compose 内的 lottery_e2e 库，绝不触及生产。
# composer dump-autoload 让 E2EPlayerSeeder（位于 e2e/database/seeders）被发现。
composer dump-autoload --quiet
# 用统一入口：migrate:fresh + FoundationSeeder + admin-auth-sync + LocalDemoSeeder
# lottery:db-init --fresh 内部已对 migrate 传 --force，无需外层加。
DB_DATABASE=lottery_e2e php artisan lottery:db-init --fresh
# e2e 专用 seeder：建可登录玩家（带 password_hash）。LocalDemoSeeder 不会建可登录玩家
DB_DATABASE=lottery_e2e php artisan db:seed --class='E2E\Seeders\E2EPlayerSeeder' --force

echo "==> [6/10] boot backend processes"
php artisan config:clear >/dev/null

export LOTTERY_NATIVE_JWT_SECRET="$(grep '^LOTTERY_NATIVE_JWT_SECRET=' "$ENV_FILE" | cut -d= -f2-)"
if [[ -z "${LOTTERY_NATIVE_JWT_SECRET}" ]]; then
  LOTTERY_NATIVE_JWT_SECRET="$(openssl rand -hex 32)"
  if grep -q '^LOTTERY_NATIVE_JWT_SECRET=' "$ENV_FILE"; then
    perl -i -pe "s/^LOTTERY_NATIVE_JWT_SECRET=.*/LOTTERY_NATIVE_JWT_SECRET=${LOTTERY_NATIVE_JWT_SECRET}/" "$ENV_FILE"
  else
    echo "LOTTERY_NATIVE_JWT_SECRET=${LOTTERY_NATIVE_JWT_SECRET}" >>"$ENV_FILE"
  fi
  export LOTTERY_NATIVE_JWT_SECRET
fi
export LOTTERY_E2E=true

php artisan serve --host=127.0.0.1 --port=8000 >"$LOG_DIR/serve.log" 2>&1 &
PIDS+=($!)
php artisan queue:work redis --queue=broadcasts:countdown,broadcasts,default --tries=3 --timeout=120 >"$LOG_DIR/queue.log" 2>&1 &
PIDS+=($!)
php artisan reverb:start --host=127.0.0.1 --hostname=127.0.0.1 --port=8080 >"$LOG_DIR/reverb.log" 2>&1 &
PIDS+=($!)

node "$E2E_DIR/scripts/mock-wallet-server.mjs" >"$LOG_DIR/mock-wallet.log" 2>&1 &
PIDS+=($!)

ADMIN_DIR="$ROOT_DIR/../lotteryadmin"
FRONT_DIR="$ROOT_DIR/../lotteryfront"
if [[ "${E2E_UI:-1}" == "1" && -d "$ADMIN_DIR" ]]; then
  echo "==> [7/10] start lotteryadmin (3801)"
  (cd "$ADMIN_DIR" && LOTTERY_API_UPSTREAM="$API_URL" ALLOWED_DEV_ORIGINS=127.0.0.1 npm run dev) >"$LOG_DIR/admin-ui.log" 2>&1 &
  PIDS+=($!)
fi
if [[ "${E2E_UI:-1}" == "1" && -d "$FRONT_DIR" ]]; then
  echo "==> [7/10] start lotteryfront (3800)"
  (cd "$FRONT_DIR" && LOTTERY_API_UPSTREAM="$API_URL" NEXT_PUBLIC_PLAYER_SITE_CODE=demo ALLOWED_DEV_ORIGINS=127.0.0.1 npm run dev) >"$LOG_DIR/front-ui.log" 2>&1 &
  PIDS+=($!)
fi

echo "==> [8/10] wait for API healthy"
for i in {1..30}; do
  if curl -fsS "$PUBLIC_URL/health" >/dev/null 2>&1; then
    echo "    API up"
    break
  fi
  sleep 1
  if [[ $i -eq 30 ]]; then
    echo "!! API not healthy; logs:"
    tail -n 50 "$LOG_DIR"/*.log
    exit 4
  fi
done

if [[ "${E2E_UI:-1}" == "1" ]]; then
  echo "==> [9/10] wait for admin/front UI"
  for i in {1..60}; do
    admin_ok=false
    front_ok=false
    curl -fsS "http://localhost:3801/admin/login" >/dev/null 2>&1 && admin_ok=true
    curl -fsS "http://localhost:3800/login" >/dev/null 2>&1 && front_ok=true
    if [[ "$admin_ok" == true && "$front_ok" == true ]]; then
      echo "    UI up"
      break
    fi
    sleep 2
    if [[ $i -eq 60 ]]; then
      echo "!! UI not ready; see admin-ui.log / front-ui.log"
    fi
  done
fi

echo "==> [10/10] playwright test"
cd "$E2E_DIR"
if [[ ! -d node_modules ]]; then
  npm install
fi
# UI 默认 channel:chrome（本机 Google Chrome）；仅显式要求 bundled 时才下载 ms-playwright Chromium。
if [[ "${PLAYWRIGHT_USE_BUNDLED_CHROMIUM:-0}" == "1" ]]; then
  if ! playwright_chromium_cached; then
    export PLAYWRIGHT_BROWSERS_PATH="${PLAYWRIGHT_BROWSERS_PATH:-$(playwright_cache_dir)}"
    npx playwright install chromium
  else
    export PLAYWRIGHT_BROWSERS_PATH="${PLAYWRIGHT_BROWSERS_PATH:-$(playwright_cache_dir)}"
  fi
fi

export PLAYWRIGHT_JWT_SECRET="$(grep '^LOTTERY_NATIVE_JWT_SECRET=' "$ENV_FILE" | cut -d= -f2-)"
export PLAYWRIGHT_API_URL="$API_URL"
export PLAYWRIGHT_ADMIN_URL="http://localhost:3801"
export PLAYWRIGHT_FRONT_URL="http://localhost:3800"
export REVERB_APP_KEY="$(grep '^REVERB_APP_KEY=' "$ENV_FILE" | cut -d= -f2-)"
export REVERB_HOST=127.0.0.1
export REVERB_PORT=8080
export E2E_MOCK_WALLET_PORT=5555
export E2E_ADMIN_USERNAME=admin
export E2E_ADMIN_PASSWORD=12345678
export E2E_PLAYER_USERNAME="$(grep '^E2E_PLAYER_USERNAME=' "$ENV_E2E" | cut -d= -f2-)"
export E2E_PLAYER_PASSWORD="$(grep '^E2E_PLAYER_PASSWORD=' "$ENV_E2E" | cut -d= -f2-)"
if [[ "${E2E_UI:-1}" != "1" ]]; then
  set -- --project=api "$@"
fi
npx playwright test "$@"
rc=$?

if [[ $rc -ne 0 ]]; then
  echo "==> Playwright failed; saving artifacts"
  cp -R "$E2E_DIR/test-results" "$ARTIFACT_DIR/test-results-$(date +%s)" 2>/dev/null || true
  cp -R "$E2E_DIR/playwright-report" "$ARTIFACT_DIR/playwright-report-$(date +%s)" 2>/dev/null || true
  tail -n 200 "$LOG_DIR"/*.log > "$ARTIFACT_DIR/backend-logs-$(date +%s).log" || true
fi

exit "$rc"
