/**
 * Production rehearsal for EL3AB.
 *
 * Creates four throwaway guest accounts through the real auth endpoint, drives
 * them through matchmaking, a complete chess game, and a full Swiss tournament
 * round against the LIVE deployment, then deletes everything it created.
 *
 * Every request goes through the real API with a real Supabase token — no stubs.
 *
 * This WRITES TO PRODUCTION. It creates four accounts, a tournament and some
 * matches, all tagged ZZZ-REHEARSAL-DELETE-ME, and removes them in a finally
 * block that runs even when an assertion fails. Run it after a deploy:
 *
 *     node tests/rehearsal.mjs
 */
import fs from 'node:fs';

const B = 'https://el3ab-player.caprover.al-arcade.com/api';
const SB = 'https://safe-supabase-kong.caprover.al-arcade.com';
const SK = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTg5MzQ1NjAwMH0.wNfmuJNkX-bZwD7RbjxOChlRf_3Xm4I7bswEYTcDCg4';
const TAG = 'ZZZ-REHEARSAL-DELETE-ME';

const src = fs.readFileSync(new URL('../public/js/lib/chess.min.js', import.meta.url), 'utf8');
const mod = { exports: {} };
new Function('module', 'exports', 'globalThis', src)(mod, mod.exports, globalThis);
const Chess = globalThis.Chess || mod.exports.Chess || mod.exports;

const failures = [];
const created = { users: [], tournaments: [], matches: [] };

function check(label, cond, detail = '') {
  if (cond) { console.log(`  ok    ${label}`); return true; }
  failures.push(label);
  console.log(`  FAIL  ${label}${detail ? ' — ' + detail : ''}`);
  return false;
}

async function api(endpoint, body, token, method = 'POST') {
  const opts = { method, headers: { 'Content-Type': 'application/json' } };
  if (token) opts.headers.Authorization = `Bearer ${token}`;
  if (method === 'POST') opts.body = JSON.stringify(body);
  const res = await fetch(`${B}/${endpoint}`, opts);
  const text = await res.text();
  try { return { status: res.status, data: JSON.parse(text) }; }
  catch { return { status: res.status, data: { _raw: text.slice(0, 200) } }; }
}

const get = (endpoint, params, token) =>
  api(`${endpoint}?${new URLSearchParams(params)}`, null, token, 'GET');

// Direct Supabase, used only for assertions and teardown.
async function sb(path, { method = 'GET', body } = {}) {
  const res = await fetch(`${SB}/rest/v1/${path}`, {
    method,
    headers: { apikey: SK, Authorization: `Bearer ${SK}`, 'Content-Type': 'application/json', Prefer: 'return=representation' },
    body: body ? JSON.stringify(body) : undefined,
  });
  const t = await res.text();
  try { return JSON.parse(t); } catch { return t; }
}

async function teardown() {
  console.log('\n=== teardown ===');
  for (const tid of created.tournaments) {
    await sb(`matches?tournament_id=eq.${tid}`, { method: 'DELETE' });
    await sb(`el3ab_tournament_rounds?tournament_id=eq.${tid}`, { method: 'DELETE' });
    await sb(`tournament_registrations?tournament_id=eq.${tid}`, { method: 'DELETE' });
    await sb(`el3ab_tournaments?id=eq.${tid}`, { method: 'DELETE' });
  }
  console.log(`  removed ${created.tournaments.length} tournament(s) and their rows`);

  for (const m of created.matches) {
    await sb(`mp_log?match_id=eq.${m}`, { method: 'DELETE' });
    await sb(`matches?id=eq.${m}`, { method: 'DELETE' });
  }
  console.log(`  removed ${created.matches.length} standalone match(es)`);

  // Profiles go last: everything above holds a foreign key to them. The delete
  // is then confirmed rather than assumed — an earlier version fired and forgot,
  // and left two accounts behind without saying so.
  for (const u of created.users) {
    for (const path of [
      `rating_history?player_id=eq.${u.id}`,
      `matchmaking_queue?player_id=eq.${u.id}`,
      `mp_log?player_id=eq.${u.id}`,
      `player_achievements?player_id=eq.${u.id}`,
      `tournament_registrations?player_id=eq.${u.id}`,
      `matches?white_player_id=eq.${u.id}`,
      `matches?black_player_id=eq.${u.id}`,
      `el3ab_tournaments?created_by=eq.${u.id}`,
    ]) await sb(path, { method: 'DELETE' });
  }

  let stuck = [];
  for (const u of created.users) {
    for (let attempt = 0; attempt < 3; attempt++) {
      await sb(`profiles?id=eq.${u.id}`, { method: 'DELETE' });
      const left = await sb(`profiles?id=eq.${u.id}&select=id`);
      if (!Array.isArray(left) || left.length === 0) break;
      if (attempt === 2) stuck.push(u.id);
      await new Promise(r => setTimeout(r, 400));
    }
    await fetch(`${SB}/auth/v1/admin/users/${u.id}`, {
      method: 'DELETE', headers: { apikey: SK, Authorization: `Bearer ${SK}` },
    });
  }

  console.log(`  removed ${created.users.length - stuck.length} of ${created.users.length} throwaway account(s)`);
  if (stuck.length) {
    failures.push('teardown left accounts behind');
    console.log(`  COULD NOT REMOVE: ${stuck.join(', ')} — delete these by hand`);
  }
}

// ---------------------------------------------------------------------------
try {
  console.log('=== creating four throwaway accounts ===');
  for (let i = 0; i < 4; i++) {
    const r = await api('auth.php', { action: 'guest' });
    if (!r.data.access_token || !r.data.profile?.id) {
      console.log('  guest creation failed:', JSON.stringify(r.data).slice(0, 200));
      break;
    }
    created.users.push({ id: r.data.profile.id, token: r.data.access_token });
    await sb(`profiles?id=eq.${r.data.profile.id}`, {
      method: 'PATCH',
      body: { display_name: `${TAG} ${i + 1}`, elo_rapid: 1800 - i * 60, elo_blitz: 1750 - i * 55 },
    });
  }
  check('four accounts created with real tokens', created.users.length === 4, `${created.users.length} created`);
  if (created.users.length < 4) throw new Error('cannot continue without four accounts');
  const P = created.users;

  // -------------------------------------------------------------------------
  console.log('\n=== 1. matchmaking, live ===');
  let r = await api('matchmaking.php', { action: 'queue', game_key: 'chess', time_control: 'blitz_5_0' }, P[0].token);
  check('first player queues', r.data.queued === true, JSON.stringify(r.data));

  r = await api('matchmaking.php', { action: 'queue', game_key: 'chess', time_control: 'blitz_5_0' }, P[1].token);
  const m1 = r.data;
  check('second player is matched immediately', !!m1.match_id, JSON.stringify(m1));
  if (m1.match_id) created.matches.push(m1.match_id);

  r = await api('matchmaking.php', { action: 'status', game_key: 'chess' }, P[0].token);
  check('the waiting player receives the same match', r.data.match_id === m1.match_id, `${r.data.match_id} vs ${m1.match_id}`);
  check('THE TWO PLAYERS HAVE OPPOSITE COLOURS', r.data.color && m1.color && r.data.color !== m1.color,
    `p1=${r.data.color} p2=${m1.color}`);

  const c0 = (await api('game.php', { action: 'get', match_id: m1.match_id }, P[0].token)).data;
  const c1 = (await api('game.php', { action: 'get', match_id: m1.match_id }, P[1].token)).data;
  check('server tells each client its own colour', c0.my_color !== c1.my_color, `${c0.my_color} / ${c1.my_color}`);
  check('and each is given the correct opponent', c0.opponent_id === P[1].id && c1.opponent_id === P[0].id);

  // Spectating: a live board is meant to be watchable, without leaking the
  // private side of the game.
  await sb(`matches?id=eq.${m1.match_id}`, { method: 'PATCH', body: { game_state: { draw_offer: P[0].id } } });
  const outsider = await api('game.php', { action: 'get', match_id: m1.match_id }, P[2].token);
  check('a spectator can watch the live board', outsider.status === 200 && outsider.data.spectating === true,
    `status ${outsider.status}`);
  check('the spectator sees the position', !!outsider.data.current_fen);
  check('the spectator is given no colour of their own', outsider.data.my_color === undefined);
  check('a standing draw offer does not leak to the spectator',
    JSON.stringify(outsider.data).indexOf('draw_offer') === -1);
  await sb(`matches?id=eq.${m1.match_id}`, { method: 'PATCH', body: { game_state: {} } });

  const watch = await api('game.php', { action: 'find-active-match', player_id: P[0].id }, P[2].token);
  check('"watch this player" finds THEIR game, not the viewer\'s',
    watch.data.match_id === m1.match_id, `${watch.data.match_id} vs ${m1.match_id}`);

  const white = c0.my_color === 'w' ? P[0] : P[1];
  const black = white === P[0] ? P[1] : P[0];

  // -------------------------------------------------------------------------
  console.log('\n=== 2. a complete game, move by move, to checkmate ===');
  const game = new Chess();
  const OPERA = 'e4 e5 Nf3 d6 d4 Bg4 dxe5 Bxf3 Qxf3 dxe5 Bc4 Nf6 Qb3 Qe7 Nc3 c6 Bg5 b5 Nxb5 cxb5 Bxb5+ Nbd7 O-O-O Rd8 Rxd7 Rxd7 Rd1 Qe6 Bxd7+ Nxd7 Qb8+ Nxb8 Rd8#'.split(' ');
  const history = [];
  let mc = 0, rejected = 0, wrongTurnRefused = 0;

  for (const san of OPERA) {
    const mover = game.turn();
    const m = game.move(san);
    history.push({ from: m.from, to: m.to, san: m.san });
    mc++;

    // Every third move, the WRONG player tries it first.
    if (mc % 3 === 0) {
      const imposter = mover === 'w' ? black : white;
      const bad = await api('game.php', {
        action: 'move', match_id: m1.match_id, fen: game.fen(), move_count: mc,
        move: JSON.stringify(history),
      }, imposter.token);
      if (bad.status === 409) wrongTurnRefused++;
    }

    const who = mover === 'w' ? white : black;
    const res = await api('game.php', {
      action: 'move', match_id: m1.match_id, fen: game.fen(), move_count: mc,
      move: JSON.stringify(history),
    }, who.token);
    if (res.data.success !== true) { rejected++; if (rejected < 4) console.log(`      move ${mc} ${san}: ${JSON.stringify(res.data)}`); }
  }
  check(`all ${OPERA.length} legal moves accepted by the live server`, rejected === 0, `${rejected} rejected`);
  check('every out-of-turn attempt was refused', wrongTurnRefused === Math.floor(OPERA.length / 3),
    `${wrongTurnRefused} of ${Math.floor(OPERA.length / 3)}`);
  check('the position is checkmate', game.isCheckmate ? game.isCheckmate() : game.in_checkmate());

  console.log('\n=== 3. the server decides the winner ===');
  const loser = game.turn() === 'w' ? white : black;
  const winner = loser === white ? black : white;
  const eloBefore = (await sb(`profiles?id=eq.${winner.id}&select=elo_blitz`))[0].elo_blitz;

  // The LOSER reports first, falsely claiming a win.
  r = await api('game.php', {
    action: 'complete', match_id: m1.match_id, reason: 'checkmate', result: 'win',
    fen: game.fen(), pgn: game.pgn(),
  }, loser.token);
  check('a losing player cannot claim the win',
    r.data.winner === (winner === white ? 'white' : 'black'), JSON.stringify(r.data));

  const row = (await sb(`matches?id=eq.${m1.match_id}&select=status,result,metadata,rating_change_white,rating_change_black,white_rating_after`))[0];
  check('the match is recorded as completed', row.status === 'completed', row.status);
  check('the result is persisted', row.result === (winner === white ? 'white_wins' : 'black_wins'), String(row.result));
  check('the reason is recorded', row.metadata?.end_reason === 'checkmate', JSON.stringify(row.metadata));
  check('RATINGS WERE WRITTEN (never happened before today)',
    row.rating_change_white !== null && row.white_rating_after !== null,
    JSON.stringify({ w: row.rating_change_white, b: row.rating_change_black }));

  const eloAfter = (await sb(`profiles?id=eq.${winner.id}&select=elo_blitz`))[0].elo_blitz;
  check('the winner gained rating on their profile', eloAfter > eloBefore, `${eloBefore} -> ${eloAfter}`);

  // -------------------------------------------------------------------------
  console.log('\n=== 4. a full tournament ===');
  r = await api('tournament-admin.php', {
    action: 'create', name: TAG, format: 'swiss', time_control: 'blitz_3_0',
    rounds: 2, min_players: 4, max_players: 8, auto_start: false, is_rated: false,
  }, P[0].token);
  const tid = r.data.tournament?.id;
  check('tournament created', !!tid, JSON.stringify(r.data).slice(0, 200));
  if (!tid) throw new Error('cannot continue without a tournament');
  created.tournaments.push(tid);

  for (const p of P) {
    const rr = await api('tournaments.php', { action: 'register', tournament_id: tid }, p.token);
    if (!rr.data.success) check(`register ${p.id.slice(0, 8)}`, false, JSON.stringify(rr.data));
  }
  const regs = await sb(`tournament_registrations?tournament_id=eq.${tid}&status=eq.registered&select=id`);
  check('all four players registered', regs.length === 4, `${regs.length}`);

  r = await api('tournament-admin.php', { action: 'start', tournament_id: tid }, P[0].token);
  check('the tournament starts and pairs round 1', r.data.ok === true, JSON.stringify(r.data).slice(0, 200));
  check('two boards for four players', r.data.round?.pairings?.length === 2, String(r.data.round?.pairings?.length));

  const notAdmin = await api('tournament-admin.php', { action: 'start', tournament_id: tid }, P[1].token);
  check('a non-organiser cannot start it', notAdmin.status === 403, `status ${notAdmin.status}`);

  const pairings = (await get('swiss.php', { action: 'pairings', tournament_id: tid }, P[0].token)).data.pairings;
  check('pairings are served with player names', pairings.length === 2 && !!pairings[0].white_name,
    JSON.stringify(pairings.map(p => `${p.white_name} vs ${p.black_name}`)));

  // Both paired players open their board at the same instant.
  const board = pairings[0];
  const wTok = P.find(p => p.id === board.white_id).token;
  const bTok = P.find(p => p.id === board.black_id).token;
  const [ja, jb] = await Promise.all([
    api('tournament-match.php', { action: 'create-or-join', tournament_id: tid }, wTok),
    api('tournament-match.php', { action: 'create-or-join', tournament_id: tid }, bTok),
  ]);
  check('BOTH PLAYERS LAND ON THE SAME BOARD', ja.data.match_id === jb.data.match_id,
    `${ja.data.match_id} vs ${jb.data.match_id}`);
  check('with opposite colours', ja.data.color === 'w' && jb.data.color === 'b',
    `${ja.data.color} / ${jb.data.color}`);
  const nMatches = await sb(`matches?tournament_id=eq.${tid}&select=id`);
  check('exactly one match row exists for that pairing', nMatches.length === 1, `${nMatches.length}`);

  // Play both boards out.
  for (const p of pairings) {
    const wt = P.find(u => u.id === p.white_id).token;
    const bt = P.find(u => u.id === p.black_id).token;
    const j = await api('tournament-match.php', { action: 'create-or-join', tournament_id: tid }, wt);
    if (j.data.match_id) await api('game.php', { action: 'resign', match_id: j.data.match_id }, bt);
  }

  const t2 = (await sb(`el3ab_tournaments?id=eq.${tid}&select=status,current_round`))[0];
  check('THE ROUND ADVANCED BY ITSELF once its last game finished',
    t2.current_round === 2, JSON.stringify(t2));

  const st = (await get('swiss.php', { action: 'standings', tournament_id: tid }, P[0].token)).data.standings;
  check('standings are populated', st.length === 4, `${st.length} rows`);
  check('the two winners lead on one point', st[0].points === 1 && st[1].points === 1,
    JSON.stringify(st.map(s => `${s.name?.slice(-1)}:${s.points}`)));
  check('tiebreaks are computed', typeof st[0].tiebreaks?.buchholz_cut_1 === 'number');

  const r2 = (await get('swiss.php', { action: 'pairings', tournament_id: tid, round: 2 }, P[0].token)).data.pairings;
  check('round 2 is paired and ready', r2.length === 2, `${r2.length} boards`);
  const seen = new Set(pairings.map(p => [p.white_id, p.black_id].sort().join('|')));
  const repeat = r2.some(p => seen.has([p.white_id, p.black_id].sort().join('|')));
  check('round 2 does not repeat a round 1 pairing', !repeat);

} catch (e) {
  failures.push('threw: ' + e.message);
  console.log('\nERROR:', e.message);
} finally {
  await teardown();
}

console.log('');
if (failures.length) {
  console.log(`${failures.length} FAILURE(S):`);
  failures.forEach(f => console.log('  - ' + f));
  process.exit(1);
}
console.log('PRODUCTION REHEARSAL PASSED');
