import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import test from 'node:test';

const root = new URL('../', import.meta.url);
const enginePatchName = '0020-bounded-replay-reliable-backpressure.patch';
const gamePatchName = '0021-full-state-replay-viewers.patch';

function addedLines(patch) {
  return patch
    .split('\n')
    .filter((line) => line.startsWith('+') && !line.startsWith('+++'))
    .map((line) => line.slice(1))
    .join('\n');
}

function patchedLines(patch) {
  return patch
    .split('\n')
    .filter((line) => {
      if (line.startsWith('+++') || line.startsWith('---') || line.startsWith('@@')) return false;
      return line.startsWith('+') || line.startsWith(' ');
    })
    .map((line) => line.slice(1))
    .join('\n');
}

function patchList(source, variable) {
  const body = source.match(new RegExp(`${variable}=\\(([\\s\\S]*?)\\n\\)`));
  assert.ok(body, `missing ${variable}`);
  return [...body[1].matchAll(/(\d{4}-[^/"']+\.patch)/g)].map((match) => match[1]);
}

test('replay reliable backpressure stays within the existing wire and queue limits', async () => {
  const patch = await readFile(
    new URL(`port/native/patches/${enginePatchName}`, root),
    'utf8',
  );
  const added = addedLines(patch);

  assert.doesNotMatch(added, /#define\s+(?:MAX_MESSAGE_SIZE|MAX_MSG_QUEUE_SIZE)/);
  assert.match(added, /GetPendingReliableBytes\( void \) const \{ return reliableSend\.GetTotalSize\(\); \}/);
  assert.match(
    added,
    /return 4 \+ reliableSend\.GetTotalSize\(\) \+ 4 \+ unreliableBytes <= MAX_MESSAGE_SIZE;/,
  );
  assert.match(added, /SERVER_DEMO_RELIABLE_SNAPSHOT_LIMIT = MAX_MESSAGE_SIZE \/ 4/);
  assert.match(added, /SERVER_DEMO_RELIABLE_BATCH_LIMIT = MAX_MESSAGE_SIZE - 256/);
  assert.match(added, /SERVER_DEMO_RELIABLE_BACKLOG_LIMIT = 1024 \* 1024/);
  assert.match(added, /SERVER_DEMO_RELIABLE_STALL_MSEC = 30 \* 1000/);
});

test('replay reliable messages use an ordered bounded per-viewer FIFO', async () => {
  const patch = await readFile(
    new URL(`port/native/patches/${enginePatchName}`, root),
    'utf8',
  );
  const added = addedLines(patch);
  const patched = patchedLines(patch);
  const queue = added.match(
    /bool idAsyncServer::QueueReplayReliableMessage[\s\S]*?^\}/m,
  )?.[0] ?? '';
  const flush = added.match(
    /bool idAsyncServer::FlushReplayReliableBacklog[\s\S]*?^\}/m,
  )?.[0] ?? '';

  assert.match(added, /idList<byte>\s+replayReliableBacklog/);
  assert.match(queue, /entrySize = sizeof\( int \) \+ msg\.GetSize\(\)/);
  assert.match(queue, /entrySize > SERVER_DEMO_RELIABLE_BACKLOG_LIMIT/);
  assert.match(queue, /memmove\([\s\S]*replayReliableOffset, remaining \)/);
  assert.match(queue, /AssureSize\( offset \+ entrySize \)/);
  assert.match(queue, /memcpy\([\s\S]*&messageSize, sizeof\( messageSize \) \)/);
  assert.match(queue, /memcpy\([\s\S]*msg\.GetData\(\), messageSize \)/);
  assert.match(queue, /replayReliableBarrier = true/);

  assert.match(flush, /GetPendingReliableBytes\(\) != 0[\s\S]*return true/);
  assert.match(flush, /messageSize <= 0 \|\| messageSize > MAX_MESSAGE_SIZE/);
  assert.match(flush, /SERVER_DEMO_RELIABLE_BATCH_LIMIT/);
  assert.match(flush, /if \( !client\.channel\.SendReliableMessage\( queued \) \)[\s\S]*return false/);
  const send = flush.indexOf('SendReliableMessage( queued )');
  const advance = flush.indexOf('replayReliableOffset +=', send);
  assert.ok(send >= 0 && advance > send, 'FIFO advances only after channel admission');

  const replaySendStart = patched.indexOf(
    'if ( IsServerDemoPlaying() ) {\n\t\tconst int projectedSize',
  );
  const replaySendEnd = patched.indexOf(
    '\n\tif ( !client.channel.SendReliableMessage( msg ) )',
    replaySendStart,
  );
  assert.ok(replaySendStart >= 0 && replaySendEnd > replaySendStart);
  const replaySend = patched.slice(replaySendStart, replaySendEnd);
  assert.match(replaySend, /replayReliableOffset < client\.replayReliableBacklog\.Num\(\)/);
  assert.match(replaySend, /projectedSize > SERVER_DEMO_RELIABLE_BATCH_LIMIT/);
  assert.match(replaySend, /QueueReplayReliableMessage\( clientNum, msg \)/);
  assert.doesNotMatch(replaySend, /ClearReliableMessages/);
});

test('replay ACK barriers gate only affected viewers and fail closed on stalls', async () => {
  const patch = await readFile(
    new URL(`port/native/patches/${enginePatchName}`, root),
    'utf8',
  );
  const added = addedLines(patch);
  const patched = patchedLines(patch);
  const dropHunk = patch
    .split(/^@@/m)
    .find((hunk) => hunk.includes('client.clientState <= SCS_ZOMBIE')) ?? '';

  assert.match(added, /RefreshReplayReliableBarriers\(\);[\s\S]*HasReadyReplayViewer\(\)/);
  assert.match(
    added,
    /repeaterWaitForViewer\.GetBool\(\) && hasInGameViewer && !hasReadyViewer/,
  );
  assert.match(
    added,
    /clientState == SCS_INGAME && !clients\[ i \]\.replayReliableBarrier/,
  );
  const stall = patched.indexOf('SERVER_DEMO_RELIABLE_STALL_MSEC ) {');
  const ready = patched.indexOf('client.channel.ReadyToSend( serverTime )', stall);
  const barrier = patched.indexOf(
    'IsServerDemoPlaying() && client.replayReliableBarrier',
    ready,
  );
  assert.ok(stall >= 0 && ready > stall && barrier > ready);
  assert.match(
    added,
    /client\.channel\.GetPendingReliableBytes\(\) == 0 &&[\s\S]*FlushReplayReliableBacklog\( i \)/,
  );
  assert.match(added, /if \( client\.replayReliableBarrier \) \{[\s\S]*SendEmptyToClient\( i, true \)/);
  assert.match(dropHunk, /^\+\s*ClearReplayReliableBacklog\( clientNum \);$/m);
  assert.doesNotMatch(dropHunk, /ClearReliableMessages/);
});

test('replay snapshots are preflighted and viewers are ordinary full-state clients', async () => {
  const [enginePatch, gamePatch] = await Promise.all([
    readFile(new URL(`port/native/patches/${enginePatchName}`, root), 'utf8'),
    readFile(new URL(`port/native/patches/${gamePatchName}`, root), 'utf8'),
  ]);
  const engine = addedLines(enginePatch);
  const patchedEngine = patchedLines(enginePatch);
  const game = addedLines(gamePatch);

  const pending = patchedEngine.indexOf(
    'const int pendingReliableBytes = client.channel.GetPendingReliableBytes()',
  );
  const defer = patchedEngine.indexOf('if ( pendingReliableBytes > 0 )', pending);
  const serialize = patchedEngine.indexOf(
    'game->RepeaterWriteSnapshot( clientNum, client.snapshotSequence',
    defer,
  );
  const preflight = patchedEngine.indexOf(
    '!client.channel.CanSendMessage( msg.GetSize() )',
    serialize,
  );
  const drop = patchedEngine.indexOf('DropClient( clientNum, "#str_07136" )', preflight);
  const send = patchedEngine.indexOf(
    'client.channel.SendMessage( serverPort, serverTime, msg ) < 0',
    drop,
  );
  const sequence = patchedEngine.indexOf('client.snapshotSequence++', send);
  assert.ok(
    pending >= 0 &&
      defer > pending &&
      serialize > defer &&
      preflight > serialize &&
      drop > preflight &&
      send > drop &&
      sequence > send,
  );
  assert.match(
    engine,
    /if \( pendingReliableBytes > 0 \) \{[\s\S]*replayReliableBarrier = true;[\s\S]*return false;/,
  );
  assert.match(engine, /RepeaterAllowClient\([\s\S]*guid, false, password/);
  assert.doesNotMatch(engine, /RepeaterAllowClient\([^\n]*guid, true, password/);

  assert.match(game, /fullStatePlayback = demoState == DEMO_PLAYING && serverDemo/);
  assert.match(
    game,
    /\( viewers\[ clientNum \]\.nopvs \|\| fullStatePlayback \) \? -1\s*:/,
  );
  assert.match(
    game,
    /!\( viewers\[ clientNum \]\.nopvs \|\| fullStatePlayback \),/,
  );
  assert.match(
    game,
    /clientNum != -1[\s\S]{0,180}!viewers\[ icl \]\.nopvs &&[\s\S]{0,80}!\( demoState == DEMO_PLAYING && serverDemo \)/,
  );
  assert.match(
    game,
    /numAreas && !viewers\[ icl \]\.nopvs && !\( demoState == DEMO_PLAYING && serverDemo \)/,
  );
  assert.doesNotMatch(game, /SetRepeaterState|RepeaterAllowClient/);
});

test('every build and source-offer path applies both replay patches after their prerequisites', async () => {
  const scripts = await Promise.all([
    readFile(new URL('tools/build-wasm.sh', root), 'utf8'),
    readFile(new URL('tools/build-native.sh', root), 'utf8'),
    readFile(new URL('tools/stage-source-offer.sh', root), 'utf8'),
  ]);

  for (const script of scripts) {
    const engine = patchList(script, 'OPENQ4_PATCH_FILES');
    const game = patchList(script, 'GAMELIBS_PATCH_FILES');
    assert.equal(engine.filter((name) => name === enginePatchName).length, 1);
    assert.equal(game.filter((name) => name === gamePatchName).length, 1);
    assert.ok(
      engine.indexOf(enginePatchName) > engine.indexOf('0004-full-state-server-demos.patch'),
    );
    assert.ok(
      game.indexOf(gamePatchName) > game.indexOf('0006-enable-replay-repeater.patch'),
    );
    assert.ok(!engine.includes(gamePatchName));
    assert.ok(!game.includes(enginePatchName));
  }
});
