<?php
/**
 * Offline test of the Swiss pairing engine.
 *
 * The engine talks to Supabase through a $sdb object, so we hand it a fake one
 * backed by in-memory arrays and simulate whole tournaments: every round gets
 * paired, results are generated, and the invariants are asserted after each one.
 */

// Stub out the Supabase layer before the engine pulls it in.
if (!function_exists('supabaseService')) { function supabaseService() { return null; } }
if (!function_exists('supabaseRpc')) { function supabaseRpc($f, $p = []) { return null; } }
if (!function_exists('supabase')) { function supabase($t = null) { return null; } }

require_once __DIR__ . '/engine_shim.php';

// ---------------------------------------------------------------------------
// Fake Supabase
// ---------------------------------------------------------------------------

class FakeDb {
    public array $t = [
        'el3ab_tournaments' => [],
        'el3ab_tournament_rounds' => [],
        'tournament_registrations' => [],
        'profiles' => [],
    ];
    private int $seq = 0;

    public function get(string $table, array $params = []): array {
        $rows = $this->t[$table] ?? [];
        $out = [];
        foreach ($rows as $r) {
            if ($this->matches($r, $params)) $out[] = $r;
        }
        if (isset($params['order'])) {
            [$col, $dir] = array_pad(explode('.', $params['order']), 2, 'asc');
            usort($out, fn($a, $b) => ($dir === 'desc' ? -1 : 1) * (($a[$col] ?? null) <=> ($b[$col] ?? null)));
        }
        if (isset($params['limit'])) $out = array_slice($out, 0, (int)$params['limit']);
        return $out;
    }

    private function matches(array $row, array $params): bool {
        foreach ($params as $col => $expr) {
            if (in_array($col, ['select', 'order', 'limit'], true)) continue;
            if (!is_string($expr)) continue;
            if (str_starts_with($expr, 'eq.')) {
                if ((string)($row[$col] ?? '') !== substr($expr, 3)) return false;
            } elseif (str_starts_with($expr, 'neq.')) {
                if ((string)($row[$col] ?? '') === substr($expr, 4)) return false;
            } elseif (str_starts_with($expr, 'in.(')) {
                $vals = explode(',', substr($expr, 4, -1));
                if (!in_array((string)($row[$col] ?? ''), $vals, true)) return false;
            }
        }
        return true;
    }

    public function insert(string $table, array $data): array {
        $data['id'] = $data['id'] ?? ('id-' . (++$this->seq));
        $this->t[$table][] = $data;
        return [$data];
    }

    public function update(string $table, array $data, array $params = []): array {
        $changed = [];
        foreach ($this->t[$table] as $i => $r) {
            if (!$this->matches($r, $params)) continue;
            $this->t[$table][$i] = array_merge($r, $data);
            $changed[] = $this->t[$table][$i];
        }
        return $changed;
    }

    public function delete(string $table, array $params = []): array { return []; }
    public function rpc(string $fn, array $data = []): array { return []; }
}

// ---------------------------------------------------------------------------
// Harness
// ---------------------------------------------------------------------------

$FAILURES = [];
function check(string $label, bool $cond, string $detail = ''): void {
    global $FAILURES;
    if (!$cond) { $FAILURES[] = "$label" . ($detail ? " — $detail" : ''); echo "  FAIL  $label" . ($detail ? " — $detail" : '') . "\n"; }
}

function buildTournament(FakeDb $db, int $n, int $rounds, string $id = 'T1'): array {
    $db->t['el3ab_tournaments'][] = [
        'id' => $id, 'name' => "Test $n", 'game_key' => 'chess', 'format' => 'swiss',
        'time_control' => 'rapid_10_0', 'swiss_rounds' => $rounds, 'rounds_total' => $rounds,
        'status' => 'registration', 'current_round' => 0, 'min_players' => 2,
        'bye_value' => 1.0, 'tiebreak_rules' => ['buchholz_cut_1', 'buchholz', 'sonneborn_berger'],
    ];
    for ($i = 1; $i <= $n; $i++) {
        $pid = sprintf('p%03d', $i);
        $db->t['tournament_registrations'][] = [
            'id' => "reg-$i", 'tournament_id' => $id, 'player_id' => $pid,
            'status' => 'registered', 'seed' => null, 'is_bot' => false,
            'registered_at' => sprintf('2026-09-01T00:00:%02dZ', $i % 60),
        ];
        $db->t['profiles'][] = [
            'id' => $pid, 'display_name' => "Player $i", 'username' => "p$i",
            'avatar_url' => null, 'elo_rapid' => 2400 - $i * 17,
        ];
    }
    return $db->t['el3ab_tournaments'][0];
}

/** Deterministic pseudo-result so runs are reproducible. */
function fakeResult(string $w, string $b, int $round): string {
    $h = crc32($w . $b . $round);
    $m = $h % 10;
    if ($m < 4) return 'white_wins';
    if ($m < 8) return 'black_wins';
    return 'draw';
}

function simulate(int $n, int $rounds, bool $verbose = false): array {
    $db = new FakeDb();
    buildTournament($db, $n, $rounds);

    $start = tournamentStart($db, 'T1');
    check("n=$n start ok", $start['ok'], $start['error'] ?? '');
    if (!$start['ok']) return ['db' => $db, 'played' => []];

    $seenPairs = [];
    $byeCount = [];

    for ($r = 1; $r <= $rounds; $r++) {
        $t = tournamentLoad($db, 'T1');
        if ($t['status'] !== 'in_progress') break;
        $round = tournamentRound($db, 'T1', (int)$t['current_round']);
        if (!$round) break;
        $rn = (int)$round['round_number'];

        $seats = [];
        foreach ($round['pairings'] as $p) {
            if (!empty($p['is_bye'])) {
                $who = $p['white_id'] ?: $p['black_id'];
                $byeCount[$who] = ($byeCount[$who] ?? 0) + 1;
                $seats[] = $who;
                continue;
            }
            $key = implode('|', [min($p['white_id'], $p['black_id']), max($p['white_id'], $p['black_id'])]);
            check("n=$n r$rn no rematch", !isset($seenPairs[$key]), "$key repeated");
            $seenPairs[$key] = true;
            $seats[] = $p['white_id'];
            $seats[] = $p['black_id'];
        }

        check("n=$n r$rn everyone seated once", count($seats) === $n && count(array_unique($seats)) === $n,
              'seated=' . count($seats) . ' unique=' . count(array_unique($seats)) . " expected=$n");

        // Play every game.
        foreach ($round['pairings'] as $p) {
            if (!empty($p['is_bye'])) continue;
            $res = fakeResult($p['white_id'], $p['black_id'], $rn);
            $derived = ['result' => $res, 'reason' => 'checkmate', 'winner' => $res === 'white_wins' ? 'white' : ($res === 'black_wins' ? 'black' : null)];
            tournamentRecordMatchResult($db, [
                'id' => "m-$rn-{$p['board']}",
                'game_key' => 'chess',
                'tournament_id' => 'T1',
                'tournament_round' => $rn,
                'white_player_id' => $p['white_id'],
                'black_player_id' => $p['black_id'],
            ], $derived);
        }
    }

    foreach ($byeCount as $who => $c) {
        check("n=$n at most one bye per player", $c <= 1, "$who had $c byes");
    }

    $t = tournamentLoad($db, 'T1');
    check("n=$n reached completion", $t['status'] === 'completed', "status={$t['status']} round={$t['current_round']}");

    $standings = tournamentStandings($db, $t);
    $totalPoints = array_sum(array_column($standings, 'points'));
    $rs = tournamentRounds($db, 'T1');
    $expected = 0.0;
    foreach ($rs as $rr) {
        foreach ($rr['pairings'] as $p) {
            $expected += !empty($p['is_bye']) ? 1.0 : 1.0;
        }
    }
    check("n=$n points conserved", abs($totalPoints - $expected) < 0.001, "got $totalPoints expected $expected");

    // Colour balance: nobody should be more than 2 games off.
    foreach ($standings as $s) {
        $w = 0; $b = 0;
        foreach ($s['colors'] as $c) { $c === 'w' ? $w++ : $b++; }
        check("n=$n colour balance", abs($w - $b) <= 2, "{$s['player_id']} w=$w b=$b");
    }

    if ($verbose) {
        echo "  standings (top 5):\n";
        foreach (array_slice($standings, 0, 5) as $s) {
            printf("    %2d. %-10s %4.1f  bh1=%.1f sb=%.1f\n", $s['rank'], $s['player_id'], $s['points'],
                   $s['tiebreaks']['buchholz_cut_1'], $s['tiebreaks']['sonneborn_berger']);
        }
    }

    return ['db' => $db, 'standings' => $standings];
}

echo "=== Swiss pairing engine ===\n";
foreach ([[2,1],[3,3],[4,3],[5,5],[6,5],[7,5],[8,5],[9,5],[16,5],[17,5],[32,7],[64,7],[100,9]] as [$n, $r]) {
    echo "-- $n players, $r rounds\n";
    simulate($n, $r, $n === 8);
}

echo "\n=== Result derivation (includes/chess.php) ===\n";
$mateFen = 'rnb1kbnr/pppp1ppp/8/4p3/6Pq/5P2/PPPPP2P/RNBQKBNR w KQkq - 1 3'; // white to move, mated
$d = chessDeriveResult('checkmate', $mateFen, null);
check('checkmate: side to move loses', $d['result'] === 'black_wins', json_encode($d));

$d = chessDeriveResult('resign', CHESS_START_FEN, 'w');
check('white resigns -> black wins', $d['result'] === 'black_wins', json_encode($d));
$d = chessDeriveResult('resign', CHESS_START_FEN, 'b');
check('black resigns -> white wins', $d['result'] === 'white_wins', json_encode($d));
$d = chessDeriveResult('timeout', CHESS_START_FEN, 'b');
check('black flags -> white wins', $d['result'] === 'white_wins', json_encode($d));
$d = chessDeriveResult('stalemate', CHESS_START_FEN, null);
check('stalemate -> draw', $d['result'] === 'draw', json_encode($d));
foreach (['checkmate','resign','timeout','abandon','stalemate','agreement','insufficient_material','threefold_repetition','fifty_moves','aborted','unknown'] as $reason) {
    $d = chessDeriveResult($reason, CHESS_START_FEN, 'w');
    check("result '$reason' is a real enum value", in_array($d['result'], MATCH_RESULT_ENUM, true), $d['result']);
    check("result '$reason' is canonical", in_array($d['result'], MATCH_RESULT_CANONICAL, true), $d['result']);
}

echo "\n=== FEN validation ===\n";
check('start position parses', chessParseFen(CHESS_START_FEN) !== null);
check('missing king rejected', chessParseFen('rnbq1bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1') === null);
check('short rank rejected', chessParseFen('rnbqkbn/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1') === null);
check('garbage rejected', chessParseFen('not a fen') === null);
check('empty rejected', chessParseFen('') === null);

$after_e4 = 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1';
[$ok, $why] = chessValidateTransition(CHESS_START_FEN, $after_e4, 'w');
check('1.e4 accepted', $ok, $why);
[$ok, $why] = chessValidateTransition(CHESS_START_FEN, $after_e4, 'b');
check('black cannot move on white turn', !$ok, $why);
[$ok, $why] = chessValidateTransition($after_e4, CHESS_START_FEN, 'b');
check('cannot rewind to a white-to-move position as black', !$ok, $why);

$extraQueen = 'rnbqkbnr/pppppppp/8/8/8/5Q2/PPPPPPPP/RNBQKBNR b KQkq - 0 1';
[$ok, $why] = chessValidateTransition(CHESS_START_FEN, $extraQueen, 'w');
check('conjuring a queen rejected', !$ok, $why);

echo "\n=== Time control ===\n";
check('blitz_3_2 initial', chessTimeControlMs('blitz_3_2')['initial'] === 180000);
check('blitz_3_2 increment', chessTimeControlMs('blitz_3_2')['increment'] === 2000);
check('rapid_10_0 initial', chessTimeControlMs('rapid_10_0')['initial'] === 600000);
check('bogus tc normalised', chessNormaliseTimeControl('standard') === 'rapid_10_0');
check('valid tc preserved', chessNormaliseTimeControl('blitz_5_0') === 'blitz_5_0');

echo "\n";
if ($FAILURES) {
    echo count($FAILURES) . " FAILURE(S)\n";
    exit(1);
}
echo "ALL CHECKS PASSED\n";
