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 patchName = '0019-report-registered-bot-status.patch';

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('botStatus reports slot-correlated, fully spawned bot entities', async () => {
  const patch = await readFile(new URL(`port/native/patches/${patchName}`, root), 'utf8');
  const command = patch.match(/\+void Cmd_BotStatus_f[\s\S]*?^\+\}/m)?.[0] ?? '';
  const count = patch.match(/\+int idGameLocal::GetSpawnedBotEntityCount[\s\S]*?^\+\}/m)?.[0] ?? '';

  assert.match(patch, /HasSpawned\(void\) const \{ return hasSpawned; \}/);
  assert.match(count, /for \( int clientNum = 0; clientNum < MAX_CLIENTS; \+\+clientNum \)/);
  assert.match(count, /networkSystem->ServerIsBotClient\( clientNum \)/);
  assert.match(count, /idEntity\* entity = entities\[ clientNum \]/);
  assert.match(count, /entity->IsType\( rvmBot::GetClassType\(\) \)/);
  assert.match(count, /static_cast<const rvmBot\*>\( entity \)/);
  assert.match(count, /bot->HasSpawned\(\)/);
  assert.match(count, /registeredBots\[ botIndex \] == bot/);
  assert.doesNotMatch(count, /return registeredBots\.Num\(\)/);
  assert.match(command, /botStatus slots=%d entities=%d active=%d\\n/);
  assert.match(command, /ServerGetNumBotClients\(\)/);
  assert.match(command, /GetSpawnedBotEntityCount\(\)/);
  assert.match(command, /ServerGetNumActiveClients\(\)/);
  assert.doesNotMatch(command, /Set|Allocate|Remove|AddBot/);
  assert.match(
    patch,
    /AddCommand\( "botStatus", Cmd_BotStatus_f, CMD_FL_GAME, "reports bot slots, fully spawned bot entities, and active clients" \)/
  );
});

test('OpenQ4 exposes a read-only per-slot bot query to the GameLib', async () => {
  const enginePatch = await readFile(
    new URL('port/native/patches/0018-preserve-bot-userinfo-across-map-change.patch', root),
    'utf8'
  );

  assert.match(enginePatch, /bool idAsyncServer::IsBotClient\( int clientNum \) const/);
  assert.match(enginePatch, /clientNum < 0 \|\| clientNum >= MAX_ASYNC_CLIENTS/);
  assert.match(enginePatch, /clients\[clientNum\]\.clientState >= SCS_PUREWAIT/);
  assert.match(enginePatch, /clients\[clientNum\]\.channel\.GetRemoteAddress\(\)\.type == NA_BOT/);
  assert.match(enginePatch, /bool idNetworkSystem::ServerIsBotClient\(\s*int clientNum\s*\)/);
  assert.match(enginePatch, /idAsyncNetwork::server\.IsBotClient\(\s*clientNum\s*\)/);
  assert.match(enginePatch, /virtual bool\s+ServerIsBotClient\(int clientNum\)/);
});

test('every GameLib release path applies botStatus directly after entity guards', 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 gameLibPatches = patchList(script, 'GAMELIBS_PATCH_FILES');
    const botStatusIndex = gameLibPatches.indexOf(patchName);
    assert.ok(botStatusIndex > 0, `${patchName} is missing from GameLib patches`);
    assert.equal(gameLibPatches[botStatusIndex - 1], '0017-guard-bot-entity-slots.patch');
    assert.ok(!patchList(script, 'OPENQ4_PATCH_FILES').includes(patchName));
  }
});

test('native smokes gate registered bot entities in every population phase', async () => {
  const [dedicated, capacity] = await Promise.all([
    readFile(new URL('port/native/smoke-dedicated.sh', root), 'utf8'),
    readFile(new URL('port/native/smoke-capacity.sh', root), 'utf8'),
  ]);

  assert.match(
    dedicated,
    /EXPECTED_BOT_STATUS="botStatus slots=\$\{BOT_MINPLAYERS\} entities=\$\{BOT_MINPLAYERS\} active=\$\{BOT_MINPLAYERS\}"/
  );
  assert.match(dedicated, /printf 'serverInfo\\nbotStatus\\n'/);
  assert.match(dedicated, /initial_bot_status="\$\{bot_status_evidence\}"/);
  assert.match(dedicated, /surviving_bot_status="\$\{bot_status_evidence\}"/);
  assert.match(dedicated, /rotated_bot_status="\$\{bot_status_evidence\}"/);
  assert.match(dedicated, /BOT_STATUS_FILE=.*bot-status\.txt/);

  assert.match(capacity, /serverInfo\\nbotStatus\\necho/);
  assert.match(capacity, /sub\(\/\[\[:space:\]\]\+\$\/, "", \$0\)/);
  assert.match(
    capacity,
    /expected_status="botStatus slots=\$\{expected_bots\} entities=\$\{expected_bots\} active=\$\{expected_clients\}"/
  );
  assert.match(capacity, /grep -Fxq -- "\$\{expected_status\}"/);
  assert.match(capacity, /assert_population[^\n]+ 8 8 0/);
  assert.match(capacity, /assert_population[^\n]+ 8 10 1/);
});
