#!/usr/bin/env bash
# End-to-end checks against the local podman stack: imports, the single deck form, hand import, league rename/delete.
# usage: scripts/local.sh up && test/e2e.sh [dir]
# dir holds Melee exports named *matches*.json / *tandings*.json (default test/fixtures; real ones: test/e2e.sh sampledata).
# Creates a throwaway league and deletes it again at the end.
set -euo pipefail
cd "$(dirname "$0")/.."
DIR=${1:-test/fixtures}
set -a; source .env.local; set +a
B=http://127.0.0.1:$HOST_PORT
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
fails=0
check() { if [[ "$2" == "$3" ]]; then echo "ok $1"; else echo "FAIL $1: got '$2', want '$3'"; fails=$((fails + 1)); fi; }
req() { curl -s -b "$T/jar" -c "$T/jar" -o "$T/out" -w '%{http_code}' "$@"; }
anon() { curl -s -o /dev/null -w '%{http_code}' "$@"; }
db() { podman exec mtg-local-db-1 psql -U league -tAc "$1"; }
files() { for f in "$DIR"/$1; do printf -- '-F\nfiles=@%s\n' "$f"; done; }
mapfile -t MATCHES < <(files '*atches*.json')
mapfile -t STANDINGS < <(files '*tandings*.json')
decks_of() { # distinct decks of player $2 in session $1, across match rows and the standings row
db "SELECT string_agg(DISTINCT coalesce(d, '-'), ',') FROM (SELECT p1_deck d FROM matches WHERE session_id=$1 AND p1_id=$2
UNION ALL SELECT p2_deck FROM matches WHERE session_id=$1 AND p2_id=$2
UNION ALL SELECT deck FROM session_results WHERE session_id=$1 AND player_id=$2) x"
}
echo "== auth and leagues"
check "admin route needs login" "$(anon -d name=x "$B/admin/league")" 403
check "login" "$(req -d "password=$ADMIN_PASSWORD" "$B/login")" 303
req -d name=e2e-league -d best_n=6 "$B/admin/league" >/dev/null
LG=$(db "SELECT max(id) FROM leagues")
check "rename needs login" "$(anon -d name=x "$B/admin/league/$LG/rename")" 403
check "rename rejects empty name" "$(req -d 'name= ' "$B/admin/league/$LG/rename")" 400
check "rename" "$(req -d 'name=e2e renamed' "$B/admin/league/$LG/rename")" 303
check "renamed in db" "$(db "SELECT name FROM leagues WHERE id=$LG")" "e2e renamed"
req "$B/league/$LG" >/dev/null; check "league page shows new name" "$(grep -c '
e2e renamed
' "$T/out")" 1
check "rename unknown league" "$(req -d name=x "$B/admin/league/2147483647/rename")" 404
echo "== session A: standings, then a deck, then matches"
req -d date=2026-10-14 "$B/admin/league/$LG/session" >/dev/null; SA=$(db "SELECT max(id) FROM sessions")
check "import standings" "$(req "${STANDINGS[@]}" "$B/admin/session/$SA/import-melee")" 303
P1=$(db "SELECT player_id FROM session_results r JOIN players p ON p.id=r.player_id WHERE session_id=$SA ORDER BY p.name LIMIT 1")
check "deck form saves to standings" "$(req -d "deck_$P1=Burn" "$B/admin/session/$SA/decks")" 303
check "standings deck" "$(decks_of "$SA" "$P1")" Burn
check "import all matches at once" "$(req "${MATCHES[@]}" "$B/admin/session/$SA/import-melee")" 303
check "matches picked up the standings deck" "$(decks_of "$SA" "$P1")" Burn
req "$B/session/$SA" >/dev/null
check "one deck form on the page" "$(grep -c '/decks"' "$T/out")" 1
check "one deck input per session player" "$(grep -o 'name="deck_' "$T/out" | wc -l)" "$(db "SELECT count(*) FROM ($(sed 's/\$1/'"$SA"'/g' <<<"SELECT pid FROM (SELECT p1_id pid FROM matches WHERE session_id=\$1 UNION SELECT p2_id FROM matches WHERE session_id=\$1 AND p2_id IS NOT NULL UNION SELECT player_id FROM session_results WHERE session_id=\$1) x")) y")"
check "deck form saves to matches and standings" "$(req -d "deck_$P1=Control" "$B/admin/session/$SA/decks")" 303
check "both updated" "$(decks_of "$SA" "$P1")" Control
check "clearing a deck" "$(req -d "deck_$P1=" "$B/admin/session/$SA/decks")" 303
check "cleared everywhere" "$(decks_of "$SA" "$P1")" -
req -d "deck_$P1=Control" "$B/admin/session/$SA/decks" >/dev/null
echo "== session A: hand import"
csv() { db "COPY (SELECT m.round, a.name, CASE WHEN m.p1_id=$P1 THEN '$1' ELSE '' END, coalesce(b.name, ''),
CASE WHEN m.p2_id=$P1 THEN '$1' ELSE '' END, m.p1_wins, m.p2_wins, m.draws FROM matches m JOIN players a ON a.id=m.p1_id
LEFT JOIN players b ON b.id=m.p2_id WHERE m.session_id=$SA ORDER BY m.round, m.id) TO STDOUT WITH CSV"; }
before=$(db "SELECT count(*) FROM matches WHERE session_id=$SA")
check "hand import with blank decks" "$(req --data-urlencode "data=$(csv '')" "$B/admin/session/$SA/import")" 303
check "match count unchanged" "$(db "SELECT count(*) FROM matches WHERE session_id=$SA")" "$before"
check "blank keeps the entered deck" "$(decks_of "$SA" "$P1")" Control
check "hand import with a new deck" "$(req --data-urlencode "data=$(csv Elves)" "$B/admin/session/$SA/import")" 303
check "new deck reaches standings too" "$(decks_of "$SA" "$P1")" Elves
echo "== session B: matches, then a deck, then standings"
req -d date=2026-10-15 "$B/admin/league/$LG/session" >/dev/null; SB=$(db "SELECT max(id) FROM sessions")
check "import matches" "$(req "${MATCHES[@]}" "$B/admin/session/$SB/import-melee")" 303
P2=$(db "SELECT p1_id FROM matches WHERE session_id=$SB AND p2_id IS NOT NULL ORDER BY id LIMIT 1")
check "deck on matches" "$(req -d "deck_$P2=Elves" "$B/admin/session/$SB/decks")" 303
check "import standings" "$(req "${STANDINGS[@]}" "$B/admin/session/$SB/import-melee")" 303
if [[ -n "$(db "SELECT 1 FROM session_results WHERE session_id=$SB AND player_id=$P2")" ]]; then
check "standings picked up the match deck" "$(decks_of "$SB" "$P2")" Elves
else echo "skip standings deck check: player not in the standings export"; fi
echo "== scores"
# only meaningful when the matches exports cover every round of the standings export
python3 -I - "$DIR" "$(curl -s "$B/api/league/$LG/leaderboard")" "$(db "SELECT json_agg(json_build_object('name', p.name, 'points', r.points))
FROM session_results r JOIN players p ON p.id=r.player_id WHERE r.session_id=$SA")" <<'EOF' || fails=$((fails + 1))
import glob, json, sys
d, board, final = sys.argv[1], json.loads(sys.argv[2]), json.loads(sys.argv[3])
rounds = {m["RoundNumber"] for f in glob.glob(d + "/*atches*.json") for m in json.load(open(f))}
last = max(r["RoundNumber"] for f in glob.glob(d + "/*tandings*.json") for r in json.load(open(f)))
if rounds != set(range(1, last + 1)):
print("skip leaderboard check: matches exports don't cover rounds 1 to", last); sys.exit(0)
got = {r["name"]: r["total"] / 2 for r in board} # sessions A and B hold the same night
bad = [f"{r['name']} {got.get(r['name'])} != {r['points']}" for r in final if got.get(r["name"]) != r["points"]]
if bad: print("FAIL leaderboard vs Melee standings:", bad); sys.exit(1)
print("ok leaderboard matches Melee's points for", len(final), "players")
EOF
echo "== delete league"
check "delete league" "$(req -X POST "$B/admin/league/$LG/delete")" 303
check "league gone" "$(db "SELECT count(*) FROM leagues WHERE id=$LG")" 0
check "its sessions gone" "$(db "SELECT count(*) FROM sessions WHERE id IN ($SA, $SB)")" 0
check "its matches gone" "$(db "SELECT count(*) FROM matches WHERE session_id IN ($SA, $SB)")" 0
echo; [[ $fails == 0 ]] && echo "all e2e checks passed" || { echo "$fails e2e check(s) failed"; exit 1; }