#!/usr/bin/env bash
#
# FC Foz backend — API/database integration tests.
# Exercises every endpoint and every query-translation path against a running
# stack, asserting responses. Creates test data and cleans it up afterwards.
#
# Usage:
#   BASE=http://localhost ADMIN_EMAIL=admin@foz.pt ADMIN_PASSWORD=admin123 \
#     bash backend/tests/run-api-tests.sh
#
set -uo pipefail

BASE="${BASE:-http://localhost}"
ADMIN_EMAIL="${ADMIN_EMAIL:-admin@foz.pt}"
ADMIN_PASSWORD="${ADMIN_PASSWORD:-admin123}"

PASS=0
FAIL=0
CREATED_NEWS=""; CREATED_PRODUCTS=""; CREATED_PLAYERS=""; CREATED_MATCHES=""
CREATED_SCANS=""; CREATED_ORDERS=""; CREATED_MEMBERS=""; CREATED_USERS=""

green() { printf "\033[32m%s\033[0m" "$1"; }
red()   { printf "\033[31m%s\033[0m" "$1"; }

pass() { echo "  $(green PASS) $1"; PASS=$((PASS + 1)); }
fail() { echo "  $(red FAIL) $1"; FAIL=$((FAIL + 1)); }

# req METHOD PATH [DATA] [TOKEN] -> sets RESP_BODY, RESP_CODE
req() {
  local method="$1" path="$2" data="${3:-}" token="${4:-}"
  local args=(-s -X "$method" -w $'\n%{http_code}')
  [ -n "$data" ] && args+=(-H "Content-Type: application/json" -d "$data")
  [ -n "$token" ] && args+=(-H "Authorization: Bearer $token")
  local out
  out=$(curl "${args[@]}" "$BASE$path")
  RESP_CODE=$(printf '%s' "$out" | tail -n1)
  RESP_BODY=$(printf '%s' "$out" | sed '$d')
}

json_id() { printf '%s' "$1" | sed -n 's/.*"id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1; }
json_token() { printf '%s' "$1" | sed -n 's/.*"token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1; }

expect_code() { # desc expected
  if [ "$RESP_CODE" = "$2" ]; then pass "$1 -> $RESP_CODE"; else fail "$1 (expected $2, got $RESP_CODE: $(printf '%s' "$RESP_BODY" | head -c 140))"; fi
}
expect_body() { # desc substring
  if printf '%s' "$RESP_BODY" | grep -q "$2"; then pass "$1"; else fail "$1 (missing '$2' in: $(printf '%s' "$RESP_BODY" | head -c 160))"; fi
}
expect_not_body() { # desc substring
  if printf '%s' "$RESP_BODY" | grep -q "$2"; then fail "$1 (unexpected '$2')"; else pass "$1"; fi
}

section() { echo; echo "== $1 =="; }

# URL-encode a JSON constraints array into a ?q= param (node is always available here)
q() { node -e 'process.stdout.write("q="+encodeURIComponent(process.argv[1]))' "$1"; }

echo "FC Foz backend integration tests -> $BASE"

# ---------------------------------------------------------------- health
section "Health"
req GET /api/health
expect_code "health" 200
expect_body "health payload" '"status"'

# ---------------------------------------------------------------- auth
section "Auth"
req POST /api/auth/login "{\"email\":\"$ADMIN_EMAIL\",\"password\":\"$ADMIN_PASSWORD\"}"
expect_code "admin login" 200
ADMIN_TOKEN=$(json_token "$RESP_BODY")
[ -n "$ADMIN_TOKEN" ] && pass "admin token issued" || fail "admin token issued"

req POST /api/auth/login "{\"email\":\"$ADMIN_EMAIL\",\"password\":\"wrong\"}"
expect_code "wrong password rejected" 401

TEST_EMAIL="test+$(date +%s)@example.com"
req POST /api/auth/register "{\"email\":\"$TEST_EMAIL\",\"password\":\"secret123\",\"displayName\":\"Test User\"}"
expect_code "register member" 201
MEMBER_TOKEN=$(json_token "$RESP_BODY")
MEMBER_UID=$(json_id "$RESP_BODY")
expect_body "register returns profile role member" '"role" : "member"'
expect_not_body "register does not leak passwordHash" 'passwordHash'
CREATED_USERS="$MEMBER_UID"

req POST /api/auth/register "{\"email\":\"$TEST_EMAIL\",\"password\":\"secret123\",\"displayName\":\"dup\"}"
expect_code "duplicate register rejected" 409

req GET /api/auth/me "" "$MEMBER_TOKEN"
expect_code "auth/me self" 200
req GET "/api/auth/role/$MEMBER_UID" "" "$MEMBER_TOKEN"
expect_body "role endpoint" '"role"'
req GET "/api/auth/profile/$MEMBER_UID" "" "$ADMIN_TOKEN"
expect_code "admin reads any profile" 200

# password change
req PATCH /api/auth/password "{\"currentPassword\":\"wrong\",\"newPassword\":\"newsecret1\"}" "$MEMBER_TOKEN"
expect_code "password change wrong current rejected" 401
req PATCH /api/auth/password "{\"currentPassword\":\"secret123\",\"newPassword\":\"newsecret1\"}" "$MEMBER_TOKEN"
expect_code "password change ok" 200
req POST /api/auth/login "{\"email\":\"$TEST_EMAIL\",\"password\":\"newsecret1\"}"
expect_code "login with new password" 200
MEMBER_TOKEN=$(json_token "$RESP_BODY")

# ---------------------------------------------------------------- authz
section "Authorization"
req POST /api/news '{"title":"x","slug":"x"}'
expect_code "unauthenticated write rejected" 401
req GET /api/users "" "$MEMBER_TOKEN"
expect_code "non-admin users list forbidden" 403
req GET /api/users "" "$ADMIN_TOKEN"
expect_code "admin users list allowed" 200
req GET /api/news
expect_code "public read allowed (news)" 200

# ---------------------------------------------------------------- CRUD lifecycle (news)
section "Generic CRUD lifecycle (news)"
req POST /api/news '{"title":"Test News","slug":"test-news-crud","excerpt":"e","content":"c","category":"comunicado","publishedAt":"2026-07-02T10:00:00Z","author":"R","source":"managed"}' "$ADMIN_TOKEN"
expect_code "create" 201
NID=$(json_id "$RESP_BODY"); CREATED_NEWS="$NID"
expect_body "create returns id" '"id"'
req GET "/api/news/$NID"
expect_code "get by id" 200
expect_body "get by id content" 'test-news-crud'
req PATCH "/api/news/$NID" '{"title":"Patched"}' "$ADMIN_TOKEN"
expect_body "patch merged" '"title" : "Patched"'
expect_body "patch preserved slug" 'test-news-crud'
req PATCH "/api/news/does-not-exist" '{"x":1}' "$ADMIN_TOKEN"
expect_code "patch missing -> 404" 404
req PUT "/api/news/$NID" '{"title":"Upserted","slug":"test-news-crud"}' "$ADMIN_TOKEN"
expect_body "put upsert" '"title" : "Upserted"'
req DELETE "/api/news/$NID" "" "$ADMIN_TOKEN"
expect_code "delete" 200
req GET "/api/news/$NID"
expect_code "deleted -> 404" 404
CREATED_NEWS=""

# ---------------------------------------------------------------- query: where(string) + orderBy + limit (matches)
section "Query: where + orderBy(asc) + limit (matches)"
req POST /api/matches '{"homeTeam":"A","awayTeam":"B","date":"2026-09-01","time":"18:00","venue":"V","competition":"C","status":"scheduled"}' "$ADMIN_TOKEN"; M1=$(json_id "$RESP_BODY")
req POST /api/matches '{"homeTeam":"C","awayTeam":"D","date":"2026-08-01","time":"18:00","venue":"V","competition":"C","status":"scheduled"}' "$ADMIN_TOKEN"; M2=$(json_id "$RESP_BODY")
req POST /api/matches '{"homeTeam":"E","awayTeam":"F","date":"2026-07-01","time":"18:00","venue":"V","competition":"C","status":"finished"}' "$ADMIN_TOKEN"; M3=$(json_id "$RESP_BODY")
CREATED_MATCHES="$M1 $M2 $M3"
req GET "/api/matches?$(q '[{"type":"where","field":"status","op":"==","value":"scheduled"},{"type":"orderBy","field":"date","direction":"asc"},{"type":"limit","count":1}]')"
expect_body "scheduled+asc+limit returns earliest scheduled (Aug)" '"date" : "2026-08-01"'
expect_not_body "limit excludes finished match" '2026-07-01'
req GET "/api/matches?$(q '[{"type":"where","field":"status","op":"==","value":"finished"}]')"
expect_body "where finished returns finished only" '2026-07-01'
expect_not_body "where finished excludes scheduled" '2026-09-01'

# ---------------------------------------------------------------- query: where(boolean) (products)
section "Query: where boolean (products featured)"
req POST /api/products '{"name":"Feat","description":"d","price":10,"images":[],"category":"K","inStock":true,"featured":true}' "$ADMIN_TOKEN"; P1=$(json_id "$RESP_BODY")
req POST /api/products '{"name":"Plain","description":"d","price":10,"images":[],"category":"K","inStock":true,"featured":false}' "$ADMIN_TOKEN"; P2=$(json_id "$RESP_BODY")
CREATED_PRODUCTS="$P1 $P2"
req GET "/api/products?$(q '[{"type":"where","field":"featured","op":"==","value":true}]')"
expect_body "featured==true returns featured" '"name" : "Feat"'
expect_not_body "featured==true excludes non-featured" '"name" : "Plain"'

# ---------------------------------------------------------------- query: orderBy numeric asc (players)
section "Query: orderBy numeric asc (players)"
req POST /api/players '{"name":"Ten","number":10,"position":"Defesa","nationality":"PT","age":25,"goals":0,"assists":0,"yellowCards":0,"redCards":0}' "$ADMIN_TOKEN"; PL1=$(json_id "$RESP_BODY")
req POST /api/players '{"name":"Seven","number":7,"position":"Avançado","nationality":"PT","age":25,"goals":0,"assists":0,"yellowCards":0,"redCards":0}' "$ADMIN_TOKEN"; PL2=$(json_id "$RESP_BODY")
CREATED_PLAYERS="$PL1 $PL2"
req GET "/api/players?$(q '[{"type":"orderBy","field":"number","direction":"asc"}]')"
# Extract the FIRST name (grep -o is non-greedy; sed's .* would grab the last).
FIRST=$(printf '%s' "$RESP_BODY" | grep -o '"name" : "[^"]*"' | head -1 | sed 's/.*: "\(.*\)"/\1/')
NUMS=$(printf '%s' "$RESP_BODY" | grep -o '"number" : [0-9]*' | grep -o '[0-9]*' | tr '\n' ' ')
[ "$FIRST" = "Seven" ] && pass "numeric asc sort (7 before 10; order: $NUMS)" || fail "numeric asc sort (first was '$FIRST'; order: $NUMS)"
req GET "/api/players?$(q '[{"type":"where","field":"position","op":"==","value":"Defesa"}]')"
expect_body "where position string" '"name" : "Ten"'
expect_not_body "where position excludes other" '"name" : "Seven"'

# ---------------------------------------------------------------- query: range merge (>= AND <) (qrCodeScans by date)
section "Query: range merge >= AND < (qrCodeScans getByDate)"
req POST /api/qrCodeScans '{"memberNumber":1,"userId":"u1","email":"a@a.pt","memberName":"A","matchId":"m","matchDate":"d","matchHomeTeam":"H","matchAwayTeam":"A","scannedAt":"2026-06-01T10:00:00Z","scannedByStaffId":"s","scannedByStaffName":"S","isValid":true}' "$ADMIN_TOKEN"; S1=$(json_id "$RESP_BODY")
req POST /api/qrCodeScans '{"memberNumber":2,"userId":"u2","email":"b@b.pt","memberName":"B","matchId":"m","matchDate":"d","matchHomeTeam":"H","matchAwayTeam":"A","scannedAt":"2026-06-05T10:00:00Z","scannedByStaffId":"s","scannedByStaffName":"S","isValid":true}' "$ADMIN_TOKEN"; S2=$(json_id "$RESP_BODY")
CREATED_SCANS="$S1 $S2"
req GET "/api/qrCodeScans?$(q '[{"type":"where","field":"scannedAt","op":">=","value":"2026-06-01T00:00:00Z"},{"type":"where","field":"scannedAt","op":"<","value":"2026-06-02T00:00:00Z"},{"type":"orderBy","field":"scannedAt","direction":"desc"}]')" "" "$ADMIN_TOKEN"
expect_body "range query includes in-range scan (Jun 01)" '2026-06-01T10:00:00Z'
expect_not_body "range query excludes out-of-range scan (Jun 05)" '2026-06-05T10:00:00Z'

# ---------------------------------------------------------------- upsert singletons
section "Upsert singletons (club_profile / membership_page)"
req PUT /api/club_profile/main '{"membershipEnabled":true}' "$ADMIN_TOKEN"
expect_code "upsert club_profile" 200
req GET /api/club_profile/main
expect_body "upsert merged flag" '"membershipEnabled" : true'
expect_body "upsert preserved existing field (email)" 'secretaria@foz.pt'
req PUT /api/club_profile/main '{"membershipEnabled":false}' "$ADMIN_TOKEN"
req GET /api/club_profile/main
expect_body "upsert can flip flag back" '"membershipEnabled" : false'

# ---------------------------------------------------------------- XP aggregation (orders + refresh-progress)
section "XP aggregation (orders -> refresh-progress)"
req POST /api/orders "{\"userId\":\"$MEMBER_UID\",\"email\":\"$TEST_EMAIL\",\"status\":\"completed\",\"total\":50,\"items\":[]}" "$ADMIN_TOKEN"; O1=$(json_id "$RESP_BODY")
CREATED_ORDERS="$O1"
req POST "/api/auth/refresh-progress/$MEMBER_UID" "" "$ADMIN_TOKEN"
expect_code "refresh-progress" 200
# 50 EUR * 10 xp/EUR = 500
expect_body "xp computed from completed order (500)" '"xp" : 500'

# ---------------------------------------------------------------- contact + upload
section "Contact + upload"
req POST /api/contact-submit '{"name":"Ana","email":"ana@test.pt","subject":"Hi","message":"Hello"}'
expect_code "contact submit (public)" 200
expect_body "contact success" '"success"'

TMPF="/tmp/fozfc-upload-test.txt"; printf 'unit-test-upload' > "$TMPF"
UP=$(curl -s -X POST "$BASE/api/upload" -H "Authorization: Bearer $ADMIN_TOKEN" -F "file=@$TMPF")
UURL=$(printf '%s' "$UP" | sed -n 's/.*"url"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
UPATH=$(printf '%s' "$UP" | sed -n 's/.*"path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
if [ -n "$UURL" ]; then pass "upload returns url"; else fail "upload returns url ($UP)"; fi
# Fetch via the tested host (the returned URL may use a different PUBLIC_BASE_URL host).
FETCHED=$(curl -s "$BASE$UPATH")
[ "$FETCHED" = "unit-test-upload" ] && pass "uploaded file served correctly" || fail "uploaded file served ('$FETCHED')"
rm -f "$TMPF"
req POST /api/upload '' "$MEMBER_TOKEN"   # no file
expect_code "upload without file -> 400" 400

# ---------------------------------------------------------------- error handling
section "Error handling"
req GET /api/not_a_collection
expect_code "unknown collection -> 404" 404
req POST /api/news 'not-json' "$ADMIN_TOKEN"
expect_code "invalid JSON body -> 400" 400

# ---------------------------------------------------------------- cleanup
section "Cleanup"
for id in $CREATED_MATCHES; do req DELETE "/api/matches/$id" "" "$ADMIN_TOKEN"; done
for id in $CREATED_PRODUCTS; do req DELETE "/api/products/$id" "" "$ADMIN_TOKEN"; done
for id in $CREATED_PLAYERS; do req DELETE "/api/players/$id" "" "$ADMIN_TOKEN"; done
for id in $CREATED_SCANS; do req DELETE "/api/qrCodeScans/$id" "" "$ADMIN_TOKEN"; done
for id in $CREATED_ORDERS; do req DELETE "/api/orders/$id" "" "$ADMIN_TOKEN"; done
for id in $CREATED_USERS; do req DELETE "/api/users/$id" "" "$ADMIN_TOKEN"; done
echo "  cleaned up test data"

# ---------------------------------------------------------------- summary
echo
echo "======================================"
echo "  Passed: $(green "$PASS")   Failed: $([ "$FAIL" -eq 0 ] && green "$FAIL" || red "$FAIL")"
echo "======================================"
[ "$FAIL" -eq 0 ]
