#!/usr/bin/env bash
# =============================================================================
# BukidBounty Performance & Load Test Script
#
# Usage:
#   ./scripts/perf-test.sh [options] [test...]
#
# Options:
#   -u, --url URL         Base URL  (default: http://localhost:9522)
#   -t, --token TOKEN     X-Perf-Token value (default: $PERF_API_TOKEN env)
#   -s, --store HASH      Store hashkey for POS simulation (required for pos/*)
#   -c, --concurrency N   Max concurrent workers for ramp test (default: 50)
#   -o, --output FILE     Write JSON summary to FILE
#   --no-color            Disable colored output
#
# Tests (pass one or more; default: all):
#   ping          Verify connectivity and token
#   seed          Create 50 users + 20 stores + 100 products, report timing
#   pos           POS simulation: 1 → 5 → 20 items × 20 cycles
#   concurrent    Hit /api/perf/pos/simulate with N parallel workers
#   ramp          Ramp concurrency from 1 → MAX workers, report throughput
#   web           Curl-based throughput on public endpoints (no token needed)
#
# Environment:
#   PERF_API_TOKEN   Token set in .env on the server (required)
#   PERF_BASE_URL    Overrides --url
#   PERF_STORE_HASH  Overrides --store
#
# Examples:
#   # Quick health check (local dev):
#   PERF_API_TOKEN=secret ./scripts/perf-test.sh ping
#
#   # Full suite against prod:
#   ./scripts/perf-test.sh -u https://yourapp.com -t SECRET -s STORE_HASH
#
#   # Concurrent POS with 30 workers:
#   ./scripts/perf-test.sh -u https://yourapp.com -t SECRET -s STORE_HASH \
#       -c 30 concurrent
#
#   # Ramp test only, save JSON report:
#   ./scripts/perf-test.sh ... ramp -o results.json
# =============================================================================

set -euo pipefail

# ---------------------------------------------------------------------------
# Defaults / env
# ---------------------------------------------------------------------------
BASE_URL="${PERF_BASE_URL:-http://localhost:9522}"
TOKEN="${PERF_API_TOKEN:-}"
STORE_HASH="${PERF_STORE_HASH:-}"
MAX_CONCURRENCY=50
OUTPUT_FILE=""
USE_COLOR=true
RUN_TESTS=()

# ---------------------------------------------------------------------------
# Arg parsing
# ---------------------------------------------------------------------------
while [[ $# -gt 0 ]]; do
  case "$1" in
    -u|--url)       BASE_URL="$2";        shift 2 ;;
    -t|--token)     TOKEN="$2";           shift 2 ;;
    -s|--store)     STORE_HASH="$2";      shift 2 ;;
    -c|--concurrency) MAX_CONCURRENCY="$2"; shift 2 ;;
    -o|--output)    OUTPUT_FILE="$2";     shift 2 ;;
    --no-color)     USE_COLOR=false;      shift   ;;
    ping|seed|pos|concurrent|ramp|web)
                    RUN_TESTS+=("$1");    shift   ;;
    *)  echo "Unknown option: $1" >&2; exit 1 ;;
  esac
done

[[ ${#RUN_TESTS[@]} -eq 0 ]] && RUN_TESTS=(ping seed pos concurrent ramp web)

BASE_URL="${BASE_URL%/}"  # strip trailing slash

# ---------------------------------------------------------------------------
# Colors
# ---------------------------------------------------------------------------
if $USE_COLOR && [[ -t 1 ]]; then
  C_RESET="\033[0m"; C_BOLD="\033[1m"
  C_GREEN="\033[32m"; C_YELLOW="\033[33m"; C_RED="\033[31m"
  C_CYAN="\033[36m"; C_BLUE="\033[34m"; C_DIM="\033[2m"
else
  C_RESET=""; C_BOLD=""; C_GREEN=""; C_YELLOW=""; C_RED=""
  C_CYAN=""; C_BLUE=""; C_DIM=""
fi

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
NOW() { date +"%Y-%m-%d %H:%M:%S"; }
log_section() { echo -e "\n${C_BOLD}${C_BLUE}══ $1 ══${C_RESET}"; }
log_ok()   { echo -e "  ${C_GREEN}✔${C_RESET}  $*"; }
log_warn() { echo -e "  ${C_YELLOW}⚠${C_RESET}  $*"; }
log_err()  { echo -e "  ${C_RED}✖${C_RESET}  $*"; }
log_dim()  { echo -e "  ${C_DIM}$*${C_RESET}"; }

# Check required tools
need() { command -v "$1" &>/dev/null || { log_err "Required tool not found: $1 — install it first"; exit 1; }; }
need curl
need jq
need bc
need awk

# curl wrapper: returns HTTP body; exits 1 on HTTP error or curl failure
# Usage: perf_get PATH
perf_get() {
  local path="$1"
  curl -sf --max-time 30 \
    -H "X-Perf-Token: ${TOKEN}" \
    -H "Accept: application/json" \
    "${BASE_URL}${path}"
}

# POST JSON body
perf_post() {
  local path="$1"
  local body="$2"
  curl -sf --max-time 60 \
    -H "X-Perf-Token: ${TOKEN}" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json" \
    -d "$body" \
    "${BASE_URL}${path}"
}

# POST silently into a tmp file; echo that file path
perf_post_async() {
  local path="$1" body="$2" out="$3"
  curl -s --max-time 60 \
    -H "X-Perf-Token: ${TOKEN}" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json" \
    -d "$body" \
    "${BASE_URL}${path}" -o "$out" 2>/dev/null
}

# Extract a numeric field from JSON
jnum() { echo "$1" | jq -r ".$2 // 0"; }

# Pretty-print a ms value
ms_label() {
  local v
  v=$(printf "%.1f" "${1:-0}")
  if (( $(echo "$v < 100" | bc -l) )); then
    echo -e "${C_GREEN}${v}ms${C_RESET}"
  elif (( $(echo "$v < 500" | bc -l) )); then
    echo -e "${C_YELLOW}${v}ms${C_RESET}"
  else
    echo -e "${C_RED}${v}ms${C_RESET}"
  fi
}

# ---------------------------------------------------------------------------
# JSON results accumulator
# ---------------------------------------------------------------------------
RESULTS='{"run_at":"","tests":{}}'
result_set() {
  # result_set <test> <key> <value(json)>
  RESULTS=$(echo "$RESULTS" | jq --arg t "$1" --arg k "$2" --argjson v "$3" \
    '.tests[$t][$k] = $v')
}
result_section() {
  RESULTS=$(echo "$RESULTS" | jq --arg t "$1" '.tests[$t] //= {}')
}

# ---------------------------------------------------------------------------
# Guard: token must be set for API tests
# ---------------------------------------------------------------------------
require_token() {
  if [[ -z "$TOKEN" ]]; then
    log_err "PERF_API_TOKEN / --token is required for this test"
    exit 1
  fi
}

require_store() {
  if [[ -z "$STORE_HASH" ]]; then
    log_err "--store / PERF_STORE_HASH is required for this test"
    exit 1
  fi
}

# =============================================================================
# TEST: ping
# =============================================================================
test_ping() {
  log_section "PING — Connectivity & token check"
  require_token
  result_section "ping"

  local t0 body
  t0=$(date +%s%N)
  if ! body=$(perf_get "/api/perf/ping" 2>&1); then
    log_err "Ping failed: $body"
    result_set ping ok false
    return 1
  fi
  local elapsed_ms
  elapsed_ms=$(( ( $(date +%s%N) - t0 ) / 1000000 ))

  local ts php
  ts=$(echo "$body" | jq -r '.ts // "?"')
  php=$(echo "$body" | jq -r '.php // "?"')

  log_ok "Server responded in $(ms_label $elapsed_ms)"
  log_ok "Server time: ${ts}   PHP: ${php}"
  log_dim "Target: ${BASE_URL}"

  result_set ping ok true
  result_set ping latency_ms "$elapsed_ms"
  result_set ping server_time "\"$ts\""
}

# =============================================================================
# TEST: seed
# =============================================================================
test_seed() {
  log_section "SEED — Create synthetic users / stores / products"
  require_token
  result_section "seed"

  # 1. Seed users
  local r
  echo -e "  Seeding 50 users..."
  r=$(perf_post "/api/perf/seed/users" '{"count":50,"prefix":"loadtest"}')
  local u_ms u_avg
  u_ms=$(jnum "$r" total_ms)
  u_avg=$(jnum "$r" avg_ms)
  log_ok "Users   — 50 created  total: $(ms_label $u_ms)  avg/user: $(ms_label $u_avg)"
  result_set seed users_total_ms "$u_ms"
  result_set seed users_avg_ms   "$u_avg"

  # 2. Seed stores
  echo -e "  Seeding 20 stores..."
  r=$(perf_post "/api/perf/seed/stores" '{"count":20,"prefix":"LoadTestStore"}')
  local s_ms s_avg
  s_ms=$(jnum "$r" total_ms)
  s_avg=$(jnum "$r" avg_ms)
  log_ok "Stores  — 20 created  total: $(ms_label $s_ms)  avg/store: $(ms_label $s_avg)"
  result_set seed stores_total_ms "$s_ms"
  result_set seed stores_avg_ms   "$s_avg"

  # 3. Seed products (global, no store attach)
  echo -e "  Seeding 100 products..."
  r=$(perf_post "/api/perf/seed/products" '{"count":100,"prefix":"LoadTestPrd","attach_to_store":false}')
  local p_ms p_avg
  p_ms=$(jnum "$r" total_ms)
  p_avg=$(jnum "$r" avg_ms)
  log_ok "Products— 100 created total: $(ms_label $p_ms)  avg/prod:  $(ms_label $p_avg)"
  result_set seed products_total_ms "$p_ms"
  result_set seed products_avg_ms   "$p_avg"
}

# =============================================================================
# TEST: pos — sequential POS simulation at various item counts
# =============================================================================
test_pos() {
  log_section "POS — Sequential simulation (1 / 5 / 20 items × 20 cycles)"
  require_token
  require_store
  result_section "pos"

  local scenario items cycles
  for scenario in "1items:1:20" "5items:5:20" "20items:20:20"; do
    label="${scenario%%:*}"
    rest="${scenario#*:}"
    items="${rest%%:*}"
    cycles="${rest#*:}"

    echo -e "  Running ${cycles} cycles × ${items} items..."
    local r avg_ms min_ms max_ms
    r=$(perf_post "/api/perf/pos/simulate" \
      "{\"store_hash\":\"${STORE_HASH}\",\"items\":${items},\"cycles\":${cycles},\"complete\":true}")

    avg_ms=$(jnum "$r" avg_cycle_ms)
    min_ms=$(jnum "$r" min_cycle_ms)
    max_ms=$(jnum "$r" max_cycle_ms)

    log_ok "${label}   avg: $(ms_label $avg_ms)  min: $(ms_label $min_ms)  max: $(ms_label $max_ms)"
    result_set pos "${label}_avg_ms" "$avg_ms"
    result_set pos "${label}_min_ms" "$min_ms"
    result_set pos "${label}_max_ms" "$max_ms"
  done
}

# =============================================================================
# TEST: concurrent — N parallel POS simulations at once
# =============================================================================
test_concurrent() {
  log_section "CONCURRENT — Parallel POS workers (5 items / session)"
  require_token
  require_store
  result_section "concurrent"

  local workers_list=(1 5 10 25 $MAX_CONCURRENCY)
  # deduplicate and sort
  workers_list=($(printf '%s\n' "${workers_list[@]}" | sort -nu))

  local TMPDIR_PERF
  TMPDIR_PERF=$(mktemp -d)
  trap "rm -rf '$TMPDIR_PERF'" EXIT

  for workers in "${workers_list[@]}"; do
    [[ $workers -gt $MAX_CONCURRENCY ]] && continue

    local pids=() out_files=()
    local t0
    t0=$(date +%s%N)

    for ((w=1; w<=workers; w++)); do
      local out="${TMPDIR_PERF}/w${workers}_${w}.json"
      out_files+=("$out")
      perf_post_async "/api/perf/pos/simulate" \
        "{\"store_hash\":\"${STORE_HASH}\",\"items\":5,\"cycles\":3,\"complete\":true}" \
        "$out" &
      pids+=($!)
    done

    # Wait for all workers
    local failed=0
    for pid in "${pids[@]}"; do
      wait "$pid" 2>/dev/null || (( failed++ )) || true
    done

    local wall_ms errors=0 total_avg=0 count=0
    wall_ms=$(( ( $(date +%s%N) - t0 ) / 1000000 ))

    for f in "${out_files[@]}"; do
      if [[ ! -f "$f" ]]; then (( errors++ )); continue; fi
      local ok avg
      ok=$(jq -r '.success // false' "$f" 2>/dev/null)
      if [[ "$ok" != "true" ]]; then (( errors++ )); continue; fi
      avg=$(jq -r '.avg_cycle_ms // 0' "$f" 2>/dev/null)
      total_avg=$(echo "$total_avg + $avg" | bc)
      (( count++ ))
    done

    local mean_avg=0
    [[ $count -gt 0 ]] && mean_avg=$(echo "scale=1; $total_avg / $count" | bc)

    local status_icon="${C_GREEN}✔${C_RESET}"
    [[ $errors -gt 0 ]] && status_icon="${C_RED}✖${C_RESET}"

    printf "  ${status_icon}  %3d workers  wall: $(ms_label $wall_ms)  mean cycle: $(ms_label $mean_avg)  errors: %d\n" \
      "$workers" "$errors"

    result_set concurrent "w${workers}_wall_ms" "$wall_ms"
    result_set concurrent "w${workers}_mean_cycle_ms" "$mean_avg"
    result_set concurrent "w${workers}_errors" "$errors"
  done

  rm -rf "$TMPDIR_PERF"
  trap - EXIT
}

# =============================================================================
# TEST: ramp — gradually increase concurrency to find the saturation point
# =============================================================================
test_ramp() {
  log_section "RAMP — Concurrency ramp 1 → ${MAX_CONCURRENCY} workers"
  require_token
  require_store
  result_section "ramp"

  # Steps: 1, 5, 10, 20, 30, MAX (if not already listed)
  local steps=(1 5 10 20 30)
  steps+=($MAX_CONCURRENCY)
  steps=($(printf '%s\n' "${steps[@]}" | sort -nu | awk -v m=$MAX_CONCURRENCY '$1<=m'))

  local TMPDIR_RAMP
  TMPDIR_RAMP=$(mktemp -d)
  trap "rm -rf '$TMPDIR_RAMP'" EXIT

  echo -e "  ${C_DIM}workers | wall_ms | mean_cycle_ms | throughput(cyc/s) | errors${C_RESET}"
  echo -e "  ${C_DIM}--------|---------|---------------|-----------------|-------${C_RESET}"

  local prev_throughput=0
  local saturation_workers=""

  for workers in "${steps[@]}"; do
    local pids=() out_files=()
    local t0
    t0=$(date +%s%N)

    for ((w=1; w<=workers; w++)); do
      local out="${TMPDIR_RAMP}/ramp_${workers}_${w}.json"
      out_files+=("$out")
      perf_post_async "/api/perf/pos/simulate" \
        "{\"store_hash\":\"${STORE_HASH}\",\"items\":5,\"cycles\":5,\"complete\":true}" \
        "$out" &
      pids+=($!)
    done

    for pid in "${pids[@]}"; do wait "$pid" 2>/dev/null || true; done

    local wall_ms errors=0 total_cycles=0 total_avg=0 count=0
    wall_ms=$(( ( $(date +%s%N) - t0 ) / 1000000 ))

    for f in "${out_files[@]}"; do
      [[ ! -f "$f" ]] && (( errors++ )) && continue
      local ok
      ok=$(jq -r '.success // false' "$f" 2>/dev/null)
      [[ "$ok" != "true" ]] && (( errors++ )) && continue
      local cyc avg
      cyc=$(jq -r '.cycles // 0' "$f" 2>/dev/null)
      avg=$(jq -r '.avg_cycle_ms // 0' "$f" 2>/dev/null)
      total_cycles=$(( total_cycles + cyc ))
      total_avg=$(echo "$total_avg + $avg" | bc)
      (( count++ ))
    done

    local mean_avg=0 throughput=0
    [[ $count -gt 0 ]] && mean_avg=$(echo "scale=1; $total_avg / $count" | bc)
    [[ $wall_ms -gt 0 ]] && throughput=$(echo "scale=1; $total_cycles * 1000 / $wall_ms" | bc)

    # Saturation detection: throughput stopped growing by >5%
    if [[ -n "$prev_throughput" ]] && (( $(echo "$prev_throughput > 0" | bc -l) )); then
      local growth
      growth=$(echo "scale=2; ($throughput - $prev_throughput) / $prev_throughput * 100" | bc 2>/dev/null || echo 0)
      if (( $(echo "$growth < 5 && $workers > 1" | bc -l) )) && [[ -z "$saturation_workers" ]]; then
        saturation_workers=$workers
      fi
    fi
    prev_throughput=$throughput

    local err_col="${C_GREEN}${errors}${C_RESET}"
    [[ $errors -gt 0 ]] && err_col="${C_RED}${errors}${C_RESET}"

    printf "  %7d | %7d | %13.1f | %17.1f | ${err_col}\n" \
      "$workers" "$wall_ms" "$mean_avg" "$throughput"

    result_set ramp "w${workers}" "{\"wall_ms\":$wall_ms,\"mean_cycle_ms\":$mean_avg,\"throughput\":$throughput,\"errors\":$errors}"
  done

  if [[ -n "$saturation_workers" ]]; then
    log_warn "Throughput plateaued around ${saturation_workers} workers — saturation point"
    result_set ramp saturation_workers "$saturation_workers"
  else
    log_ok "No saturation detected at ${MAX_CONCURRENCY} workers — system still scaling"
  fi

  rm -rf "$TMPDIR_RAMP"
  trap - EXIT
}

# =============================================================================
# TEST: web — curl burst on public/session endpoints (no token needed)
# =============================================================================
test_web() {
  log_section "WEB — Public endpoint throughput (curl burst, no token)"
  result_section "web"

  local endpoints=(
    "GET|/|Public home"
    "GET|/login|Login page"
    "GET|/api/public/landing-page|Public landing page API"
  )

  local TMPDIR_WEB
  TMPDIR_WEB=$(mktemp -d)
  trap "rm -rf '$TMPDIR_WEB'" EXIT

  for entry in "${endpoints[@]}"; do
    local method path label
    method="${entry%%|*}"; rest="${entry#*|}"; path="${rest%%|*}"; label="${rest#*|}"

    # Burst: 20 sequential requests, measure p50/p95/p99
    local times=()
    local errors=0
    for i in $(seq 1 20); do
      local t0 t1 ms
      t0=$(date +%s%N)
      local http_code
      http_code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 \
        -X "$method" "${BASE_URL}${path}" 2>/dev/null || echo "000")
      t1=$(date +%s%N)
      ms=$(( ( t1 - t0 ) / 1000000 ))
      if [[ "$http_code" == "000" || "$http_code" == "5"* ]]; then
        (( errors++ ))
      else
        times+=($ms)
      fi
    done

    if [[ ${#times[@]} -eq 0 ]]; then
      log_err "${label} — all 20 requests failed"
      result_set web "$path" '{"errors":20}'
      continue
    fi

    # Sort times
    local sorted
    sorted=($(printf '%s\n' "${times[@]}" | sort -n))
    local n=${#sorted[@]}
    local p50_idx p95_idx p99_idx
    p50_idx=$(( (n * 50) / 100 ))
    p95_idx=$(( (n * 95) / 100 ))
    p99_idx=$(( (n * 99) / 100 ))
    [[ $p50_idx -ge $n ]] && p50_idx=$(( n - 1 ))
    [[ $p95_idx -ge $n ]] && p95_idx=$(( n - 1 ))
    [[ $p99_idx -ge $n ]] && p99_idx=$(( n - 1 ))

    local p50=${sorted[$p50_idx]}
    local p95=${sorted[$p95_idx]}
    local p99=${sorted[$p99_idx]}
    local avg
    avg=$(printf '%s\n' "${times[@]}" | awk '{sum+=$1} END {printf "%.0f", sum/NR}')

    log_ok "${label}"
    log_dim "   avg: $(ms_label $avg)  p50: $(ms_label $p50)  p95: $(ms_label $p95)  p99: $(ms_label $p99)  errors: $errors / 20"

    result_set web "$path" "{\"avg_ms\":$avg,\"p50_ms\":$p50,\"p95_ms\":$p95,\"p99_ms\":$p99,\"errors\":$errors}"
  done

  rm -rf "$TMPDIR_WEB"
  trap - EXIT
}

# =============================================================================
# MAIN
# =============================================================================
echo -e "\n${C_BOLD}${C_CYAN}BukidBounty Performance Test${C_RESET}  $(NOW)"
echo -e "  Target : ${C_BOLD}${BASE_URL}${C_RESET}"
echo -e "  Tests  : ${RUN_TESTS[*]}"
[[ -n "$STORE_HASH" ]] && echo -e "  Store  : ${STORE_HASH}"

RESULTS=$(echo "$RESULTS" | jq --arg d "$(NOW)" '.run_at = $d')

for t in "${RUN_TESTS[@]}"; do
  case "$t" in
    ping)       test_ping       || true ;;
    seed)       test_seed       || true ;;
    pos)        test_pos        || true ;;
    concurrent) test_concurrent || true ;;
    ramp)       test_ramp       || true ;;
    web)        test_web        || true ;;
    *) log_warn "Unknown test: $t (skipped)" ;;
  esac
done

# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
log_section "SUMMARY"
RESULTS=$(echo "$RESULTS" | jq --arg url "$BASE_URL" '.target = $url')

if [[ -n "$OUTPUT_FILE" ]]; then
  echo "$RESULTS" | jq . > "$OUTPUT_FILE"
  log_ok "Results written to: ${OUTPUT_FILE}"
fi

echo -e "\n${C_DIM}Tip: run with -o results.json to capture structured results${C_RESET}"
echo -e "${C_DIM}Tip: set PERF_API_TOKEN in .env on the server to enable the perf API${C_RESET}\n"
