#!/bin/bash
# Gate Gap API — end-to-end check.
#
# Exercises every endpoint plus the cases that matter for safety: another
# account's data, a missing token, a replayed refresh token, a wrong password
# on deletion, and the rate limiter. Run against a throwaway database.
#
#   ./tests/smoke.sh http://127.0.0.1:8787
#
# Request bodies are built into variables rather than written inline: nesting
# escaped double quotes inside "$( ... )" mangles the JSON, and a test that
# silently sends a broken body is worse than no test.

set -uo pipefail
B="${1:-http://127.0.0.1:8787}"
DB="${GG_TEST_DB:-gategap}"
PASS=0
FAIL=0

# Reads a dotted path out of a JSON document on stdin.
j() {
  php -r '$d=json_decode(stream_get_contents(STDIN),true);
          foreach(explode(".",$argv[1]) as $p){
            if(!is_array($d)||!array_key_exists($p,$d)){echo ""; exit;} $d=$d[$p];
          }
          echo is_scalar($d)?var_export($d,true):json_encode($d);' "$1" \
  | sed "s/^'//;s/'$//"
}

count() { php -r '$d=json_decode(stream_get_contents(STDIN),true); echo is_array($d[$argv[1]]??null)?count($d[$argv[1]]):"-";' "$1"; }

check() {
  if [ "$2" == "$3" ]; then PASS=$((PASS+1)); printf '  ok   %s\n' "$1"
  else FAIL=$((FAIL+1)); printf '  FAIL %s (expected %s, got %s)\n' "$1" "$2" "$3"; fi
}

code() { curl -s -o /dev/null -w '%{http_code}' "$@"; }
post() { curl -s -X POST "$1" -H 'Content-Type: application/json' -d "$2" ${3:+-H "Authorization: Bearer $3"}; }
postc() { curl -s -o /dev/null -w '%{http_code}' -X POST "$1" -H 'Content-Type: application/json' -d "$2" ${3:+-H "Authorization: Bearer $3"}; }

# A clean slate for the counters, or a run after a run trips its own limiter.
mysql -u root "$DB" -e "TRUNCATE rate_limits;" >/dev/null 2>&1

STAMP=$(date +%s)-$RANDOM
EMAIL="smoke-$STAMP@example.com"
OTHER="other-$STAMP@example.com"
PW='a long enough passphrase'
PW2='another long passphrase'

uuid() { php -r 'printf("%s-%s-4%s-a%s-%s", bin2hex(random_bytes(4)), bin2hex(random_bytes(2)), substr(bin2hex(random_bytes(2)),1), substr(bin2hex(random_bytes(2)),1), bin2hex(random_bytes(6)));'; }
urlenc() { php -r 'echo rawurlencode($argv[1]);' "$1"; }

echo "== health"
check "health" "200" "$(code $B/v1/health)"
check "unknown endpoint is a 404" "404" "$(code $B/v1/nope)"
check "wrong method is a 405" "405" "$(code -X DELETE $B/v1/health)"

echo "== registration and sign-in"
BODY=$(printf '{"email":"%s","password":"%s","display_name":"Smoke"}' "$EMAIL" "$PW")
REG=$(curl -s -X POST $B/v1/auth/register -H 'Content-Type: application/json' -H 'X-Device-Label: Smoke' -d "$BODY")
A=$(echo "$REG" | j tokens.access_token)
F=$(echo "$REG" | j tokens.refresh_token)
check "register returns a token" "yes" "$([ ${#A} -gt 20 ] && echo yes || echo no)"
check "duplicate email refused" "409" "$(postc $B/v1/auth/register "$BODY")"
check "short password refused" "422" "$(postc $B/v1/auth/register "$(printf '{"email":"x%s@example.com","password":"short"}' "$STAMP")")"
check "bad email refused" "422" "$(postc $B/v1/auth/register "$(printf '{"email":"not-an-email","password":"%s"}' "$PW")")"
check "body must be JSON" "400" "$(code -X POST $B/v1/auth/login -H 'Content-Type: application/json' -d 'not json')"

echo "== the token gate"
check "no token refused" "401" "$(code $B/v1/trips)"
check "junk token refused" "401" "$(code -H 'Authorization: Bearer aaaaaaaaaaaaaaaaaaaaaaaaaa' $B/v1/trips)"
check "token in a query string is not accepted" "401" "$(code "$B/v1/trips?access_token=$A")"
check "valid token accepted" "200" "$(code -H "Authorization: Bearer $A" $B/v1/trips)"

echo "== token rotation and theft detection"
R2=$(post $B/v1/auth/refresh "$(printf '{"refresh_token":"%s"}' "$F")")
A2=$(echo "$R2" | j tokens.access_token)
F2=$(echo "$R2" | j tokens.refresh_token)
check "refresh token rotates" "yes" "$([ "$F2" != "$F" ] && echo yes || echo no)"
check "the token it replaced is dead" "401" "$(code -H "Authorization: Bearer $A" $B/v1/me)"
check "the new token works" "200" "$(code -H "Authorization: Bearer $A2" $B/v1/me)"
check "replaying a spent refresh token is refused" "401" "$(postc $B/v1/auth/refresh "$(printf '{"refresh_token":"%s"}' "$F")")"
check "and the whole session dies with it" "401" "$(code -H "Authorization: Bearer $A2" $B/v1/me)"
check "including the token that had just been issued" "401" "$(postc $B/v1/auth/refresh "$(printf '{"refresh_token":"%s"}' "$F2")")"

LOGIN=$(post $B/v1/auth/login "$(printf '{"email":"%s","password":"%s"}' "$EMAIL" "$PW")")
A=$(echo "$LOGIN" | j tokens.access_token)
check "signing in again works" "200" "$(code -H "Authorization: Bearer $A" $B/v1/me)"

echo "== profile"
PROFILE='{"display_name":"Smoke","passport_country":"United Kingdom","walking_pace":"withChildren","home_airport":"LHR","travel_to_airport_minutes":55,"arrive_before_minutes":120,"applies_six_month_rule":true,"mobility_needs":["assistanceBooked","nonsense"]}'
check "profile saved" "200" "$(curl -s -o /dev/null -w '%{http_code}' -X PUT $B/v1/me/profile -H "Authorization: Bearer $A" -H 'Content-Type: application/json' -d "$PROFILE")"
ME=$(curl -s -H "Authorization: Bearer $A" $B/v1/me)
check "pace persisted" "withChildren" "$(echo "$ME" | j profile.walking_pace)"
check "an unknown mobility need is dropped" '["assistanceBooked"]' "$(echo "$ME" | j profile.mobility_needs)"
check "unknown pace rejected" "422" "$(curl -s -o /dev/null -w '%{http_code}' -X PUT $B/v1/me/profile -H "Authorization: Bearer $A" -H 'Content-Type: application/json' -d '{"walking_pace":"teleport"}')"

echo "== trips and flights"
TRIP_ID=$(uuid); FLIGHT_ID=$(uuid)
TRIP=$(printf '{"id":"%s","name":"Smoke trip","purpose":"work","start_date":"2026-09-01","end_date":"2026-09-10","ticket_structure":"separate","status":"upcoming","baggage":{"carryOnOnly":false,"checkedBagCount":1,"throughChecked":"no"},"flights":[{"id":"%s","number":"BA512","airline":"British Airways","from_code":"LHR","to_code":"MAD","from_terminal":"5","to_terminal":"4S","departure_at":"2026-09-01T07:45:00Z","arrival_at":"2026-09-01T11:05:00Z","booking_reference":"QK4ZP2","checked_bags":1}]}' "$TRIP_ID" "$FLIGHT_ID")
CREATED=$(post $B/v1/trips "$TRIP" "$A")
check "trip created" "Smoke trip" "$(echo "$CREATED" | j trip.name)"
check "flight nested" "BA512" "$(echo "$CREATED" | j trip.flights.0.number)"
check "booking reference round-trips" "QK4ZP2" "$(echo "$CREATED" | j trip.flights.0.booking_reference)"
check "arrival before departure refused" "422" "$(postc $B/v1/trips/$TRIP_ID/flights '{"number":"XX1","from_code":"MAD","to_code":"EZE","departure_at":"2026-09-01T13:00:00Z","arrival_at":"2026-09-01T12:00:00Z"}' "$A")"
check "end before start refused" "422" "$(postc $B/v1/trips '{"name":"Backwards","start_date":"2026-09-10","end_date":"2026-09-01"}' "$A")"
check "trip renamed" "200" "$(curl -s -o /dev/null -w '%{http_code}' -X PATCH $B/v1/trips/$TRIP_ID -H "Authorization: Bearer $A" -H 'Content-Type: application/json' -d '{"name":"Smoke trip renamed","start_date":"2026-09-01","end_date":"2026-09-10"}')"
check "unknown trip is a 404" "404" "$(code $B/v1/trips/11111111-1111-4111-a111-111111111111 -H "Authorization: Bearer $A")"
check "malformed id is a 404, not a 500" "404" "$(code $B/v1/trips/not-a-uuid -H "Authorization: Bearer $A")"
check "a quote in a name is stored, not executed" "201" "$(postc $B/v1/trips "$(printf '{"name":"O%sBrien%s drop","start_date":"2026-01-01","end_date":"2026-01-02"}' "'" "'; DROP TABLE trips; --")" "$A")"
check "the table is still there" "200" "$(code $B/v1/trips -H "Authorization: Bearer $A")"

echo "== the reference number is encrypted at rest"
RAW=$(mysql -u root "$DB" -N -B -e "SELECT HEX(booking_reference_enc) FROM flights WHERE id='$FLIGHT_ID';" 2>/dev/null)
check "ciphertext is present" "yes" "$([ ${#RAW} -gt 40 ] && echo yes || echo no)"
check "ciphertext is not the plaintext" "no" "$(echo "$RAW" | grep -qi "$(printf 'QK4ZP2' | xxd -p)" && echo yes || echo no)"

echo "== documents, airports, transit"
DOC=$(post $B/v1/documents '{"type":"passport","belongs_to":"Smoke","valid_until":"2030-01-01","reference_number":"P1234567"}' "$A")
DOC_ID=$(echo "$DOC" | j document.id)
check "document created" "P1234567" "$(echo "$DOC" | j document.reference_number)"
check "airport card saved" "200" "$(curl -s -o /dev/null -w '%{http_code}' -X PUT $B/v1/airports -H "Authorization: Bearer $A" -H 'Content-Type: application/json' -d '{"code":"MAD","city":"Madrid","transfer_mode":"bus","transfer_minutes":28,"own_timings":[{"step":"passportControl","minutes":42,"hour_of_day":11}]}')"
AIR=$(curl -s -H "Authorization: Bearer $A" $B/v1/airports/MAD)
check "own timing stored" "42" "$(echo "$AIR" | j airport.own_timings.0.minutes)"
check "bad time zone refused" "422" "$(curl -s -o /dev/null -w '%{http_code}' -X PUT $B/v1/airports -H "Authorization: Bearer $A" -H 'Content-Type: application/json' -d '{"code":"MAD","time_zone_id":"Mars/Olympus"}')"
REQ=$(post $B/v1/transit-requirements "$(printf '{"trip_id":"%s","country":"Spain","airport_code":"MAD","state":"confirmed"}' "$TRIP_ID")" "$A")
REQ_ID=$(echo "$REQ" | j transit_requirement.id)
check "requirement created" "Spain" "$(echo "$REQ" | j transit_requirement.country)"

echo "== another account cannot reach any of it"
REG2=$(post $B/v1/auth/register "$(printf '{"email":"%s","password":"%s"}' "$OTHER" "$PW")")
A2=$(echo "$REG2" | j tokens.access_token)
check "second account registered" "yes" "$([ ${#A2} -gt 20 ] && echo yes || echo no)"
check "other account: trip hidden" "404" "$(code $B/v1/trips/$TRIP_ID -H "Authorization: Bearer $A2")"
check "other account: cannot edit trip" "404" "$(curl -s -o /dev/null -w '%{http_code}' -X PATCH $B/v1/trips/$TRIP_ID -H "Authorization: Bearer $A2" -H 'Content-Type: application/json' -d '{"name":"stolen","start_date":"2026-09-01","end_date":"2026-09-10"}')"
check "other account: cannot delete trip" "404" "$(code -X DELETE $B/v1/trips/$TRIP_ID -H "Authorization: Bearer $A2")"
check "other account: document hidden" "404" "$(code $B/v1/documents/$DOC_ID -H "Authorization: Bearer $A2")"
check "other account: airport card hidden" "404" "$(code $B/v1/airports/MAD -H "Authorization: Bearer $A2")"
check "other account: sees no trips" "0" "$(curl -s -H "Authorization: Bearer $A2" $B/v1/trips | count trips)"
check "other account: cannot delete a flight" "404" "$(code -X DELETE $B/v1/trips/$TRIP_ID/flights/$FLIGHT_ID -H "Authorization: Bearer $A2")"

echo "== sync"
# The cursor has whole-second resolution and the `since` filter is inclusive,
# so a cursor taken in the same second as a write legitimately returns that
# write again. Letting the second turn over first is what makes the assertion
# below about the server rather than about the clock.
sleep 1
PULL=$(curl -s -H "Authorization: Bearer $A" "$B/v1/sync")
check "pull returns the trips" "2" "$(echo "$PULL" | count trips)"
CURSOR=$(echo "$PULL" | j cursor)
check "pull with a cursor returns nothing new" "0" "$(curl -s -H "Authorization: Bearer $A" "$B/v1/sync?since=$(urlenc "$CURSOR")" | count trips)"
PUSH=$(post $B/v1/sync "$(printf '{"documents":[{"type":"insurance","belongs_to":"Smoke"}],"deleted":[{"entity":"requirement","id":"%s"}]}' "$REQ_ID")" "$A")
check "push applied the document" "1" "$(echo "$PUSH" | j applied.documents)"
check "push applied the deletion" "1" "$(echo "$PUSH" | j applied.deleted)"
BAD=$(post $B/v1/sync '{"trips":[{"name":"no dates"}],"documents":[{"type":"visa"}]}' "$A")
check "one bad item does not fail the batch" "1" "$(echo "$BAD" | j applied.documents)"
check "and the bad item is reported back" "1" "$(echo "$BAD" | count rejected)"
check "the deletion comes back as a tombstone" "requirement" "$(curl -s -H "Authorization: Bearer $A" "$B/v1/sync?since=$(urlenc "$CURSOR")" | j deleted.0.entity)"
check "a deleted trip cannot be pushed back" "409" "$(code -X DELETE $B/v1/trips/$TRIP_ID -H "Authorization: Bearer $A" -o /dev/null -w '' ; postc $B/v1/trips "$TRIP" "$A")"

echo "== sessions"
SESSIONS=$(curl -s -H "Authorization: Bearer $A" $B/v1/me/sessions)
SESSION_ID=$(echo "$SESSIONS" | j sessions.0.id)
check "current session listed" "true" "$(echo "$SESSIONS" | j sessions.0.current)"
check "another account cannot revoke it" "404" "$(code -X DELETE $B/v1/me/sessions/$SESSION_ID -H "Authorization: Bearer $A2")"

echo "== password change"
check "wrong current password refused" "401" "$(postc $B/v1/me/password "$(printf '{"current_password":"not the password","new_password":"%s"}' "$PW2")" "$A")"
CHANGED=$(post $B/v1/me/password "$(printf '{"current_password":"%s","new_password":"%s"}' "$PW" "$PW2")" "$A")
A_NEW=$(echo "$CHANGED" | j tokens.access_token)
check "password changed and a fresh token issued" "yes" "$([ ${#A_NEW} -gt 20 ] && echo yes || echo no)"
check "the old token died with the change" "401" "$(code -H "Authorization: Bearer $A" $B/v1/me)"
A=$A_NEW

echo "== deletion"
check "deletion needs the password" "401" "$(code -X DELETE $B/v1/me -H "Authorization: Bearer $A" -H 'Content-Type: application/json' -d "$(printf '{"password":"wrong password here","confirm":"DELETE"}')")"
check "deletion needs the confirmation" "422" "$(code -X DELETE $B/v1/me -H "Authorization: Bearer $A" -H 'Content-Type: application/json' -d "$(printf '{"password":"%s"}' "$PW2")")"
check "account deleted" "200" "$(code -X DELETE $B/v1/me -H "Authorization: Bearer $A" -H 'Content-Type: application/json' -d "$(printf '{"password":"%s","confirm":"DELETE"}' "$PW2")")"
check "the token is dead" "401" "$(code -H "Authorization: Bearer $A" $B/v1/me)"
check "signing in again is refused" "401" "$(postc $B/v1/auth/login "$(printf '{"email":"%s","password":"%s"}' "$EMAIL" "$PW2")")"
LEFT=$(mysql -u root "$DB" -N -B -e "SELECT COUNT(*) FROM users WHERE email_normalised='$EMAIL';" 2>/dev/null)
check "the account row is gone" "0" "${LEFT:-x}"
ORPHANS=$(mysql -u root "$DB" -N -B -e "SELECT COUNT(*) FROM trips t LEFT JOIN users u ON u.id=t.user_id WHERE u.id IS NULL;" 2>/dev/null)
check "their trips went with them" "0" "${ORPHANS:-x}"

echo "== logout"
REG3=$(post $B/v1/auth/register "$(printf '{"email":"out-%s@example.com","password":"%s"}' "$STAMP" "$PW")")
A3=$(echo "$REG3" | j tokens.access_token)
check "logout accepted" "200" "$(postc $B/v1/auth/logout '{}' "$A3")"
check "the token dies immediately" "401" "$(code -H "Authorization: Bearer $A3" $B/v1/me)"

echo "== rate limiting"
LIMITED="ratelimit-$STAMP@example.com"
BAD_LOGIN=$(printf '{"email":"%s","password":"wrong password here"}' "$LIMITED")
for _ in $(seq 1 11); do postc $B/v1/auth/login "$BAD_LOGIN" >/dev/null; done
check "repeated sign-in attempts are throttled" "429" "$(postc $B/v1/auth/login "$BAD_LOGIN")"

echo
echo "passed: $PASS   failed: $FAIL"
[ "$FAIL" -eq 0 ]
