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

const root = new URL('../content-src/baseoq4/pak0/botfiles/', import.meta.url);
const requiredFiles = [
  'bots/default_c.c',
  'bots/major_c.c',
  'bots/major_i.c',
  'bots/major_w.c',
  'items.c',
  'weapons.c',
];

function namedBlocks(source, keyword) {
  const expression = new RegExp(`${keyword}\\s+"([^"]+)"\\s*\\{([^}]*)\\}`, 'g');
  return new Map([...source.matchAll(expression)].map((match) => [match[1], match[2]]));
}

function anonymousBlocks(source, keyword) {
  const expression = new RegExp(`${keyword}\\s*\\{([^}]*)\\}`, 'g');
  return [...source.matchAll(expression)].map((match) => match[1]);
}

function quotedField(body, name) {
  return body.match(new RegExp(`^\\s*${name}\\s+"([^"]*)"`, 'm'))?.[1];
}

function integerField(body, name) {
  const value = body.match(new RegExp(`^\\s*${name}\\s+(-?\\d+)`, 'm'))?.[1];
  return value === undefined ? undefined : Number(value);
}

test('the free runtime contains a complete self-authored bot configuration', async () => {
  const contents = new Map(await Promise.all(requiredFiles.map(async (relative) => [
    relative,
    await readFile(new URL(relative, root), 'utf8'),
  ])));

  for (const [relative, source] of contents) {
    assert.match(source, /^\/\/ SPDX-License-Identifier: CC0-1\.0$/m, `${relative} has no CC0 marker`);
  }
  assert.doesNotMatch([...contents.values()].join('\n'), /(?:models|effects)\/|\.(?:ase|dds|lwo|tga)\b/i);

  const baseline = contents.get('bots/default_c.c');
  const baselineBody = baseline.match(/skill\s+1\s*\{([^}]*)\}/)?.[1] ?? '';
  const indices = [...baselineBody.matchAll(/^\s*(\d+)\s+/gm)].map((match) => Number(match[1]));
  assert.deepEqual(indices, Array.from({ length: 49 }, (_value, index) => index));
  assert.equal(quotedField(baselineBody, '3'), 'bots/major_w.c');
  assert.equal(quotedField(baselineBody, '40'), 'bots/major_i.c');
  for (const reference of ['bots/major_w.c', 'bots/major_i.c']) {
    assert.ok(contents.has(reference), `missing characteristic reference ${reference}`);
  }

  const major = contents.get('bots/major_c.c');
  assert.deepEqual(
    [...major.matchAll(/skill\s+(\d+)\s*\{/g)].map((match) => Number(match[1])),
    [1, 2, 3, 4, 5],
  );

  const items = namedBlocks(contents.get('items.c'), 'iteminfo');
  const expectedItems = new Map([
    ['weapon_shotgun', [9, 5]],
    ['weapon_hyperblaster', [15, 11]],
    ['weapon_rocketlauncher', [12, 8]],
    ['item_health_large', [6, 29]],
    ['item_armor_large', [3, 1]],
  ]);
  assert.deepEqual([...items.keys()], [...expectedItems.keys()]);
  for (const [name, [modelIndex, inventoryIndex]] of expectedItems) {
    const body = items.get(name);
    for (const field of ['name', 'model', 'modelindex', 'type', 'index', 'respawntime', 'mins', 'maxs']) {
      assert.match(body, new RegExp(`^\\s*${field}\\s+`, 'm'), `${name} is missing ${field}`);
    }
    assert.equal(integerField(body, 'modelindex'), modelIndex);
    assert.equal(integerField(body, 'index'), inventoryIndex);
  }

  const itemWeights = [...contents.get('bots/major_i.c').matchAll(/^weight\s+"([^"]+)"\s+return\s+\d+\s*;$/gm)]
    .map((match) => match[1]);
  assert.deepEqual(itemWeights, [...expectedItems.keys()]);

  const projectiles = anonymousBlocks(contents.get('weapons.c'), 'projectileinfo');
  const projectileNames = new Set(projectiles.map((body) => quotedField(body, 'name')));
  assert.deepEqual(projectileNames, new Set(['q4web_bullet', 'q4web_pellet', 'q4web_pulse', 'q4web_rocket']));
  for (const body of projectiles) {
    for (const field of [
      'name', 'model', 'flags', 'gravity', 'damage', 'radius', 'visdamage', 'damagetype',
      'healthinc', 'push', 'detonation', 'bounce', 'bouncefric', 'bouncestop',
    ]) {
      assert.match(body, new RegExp(`^\\s*${field}\\s+`, 'm'), `projectile is missing ${field}`);
    }
  }

  const weapons = anonymousBlocks(contents.get('weapons.c'), 'weaponinfo');
  const expectedWeapons = new Map([
    ['weapon_machinegun', [1, 6]],
    ['weapon_shotgun', [2, 5]],
    ['weapon_hyperblaster', [3, 11]],
    ['weapon_rocketlauncher', [6, 8]],
  ]);
  assert.equal(weapons.length, expectedWeapons.size);
  for (const body of weapons) {
    const name = quotedField(body, 'name');
    const expected = expectedWeapons.get(name);
    assert.ok(expected, `unexpected weapon ${name}`);
    assert.equal(integerField(body, 'number'), expected[0]);
    assert.equal(integerField(body, 'weaponindex'), expected[1]);
    assert.ok(projectileNames.has(quotedField(body, 'projectile')), `${name} has an undefined projectile`);
  }

  const weaponWeights = [...contents.get('bots/major_w.c').matchAll(/^weight\s+"([^"]+)"\s+return\s+\d+\s*;$/gm)]
    .map((match) => match[1]);
  assert.deepEqual(weaponWeights, [...expectedWeapons.keys()]);
});
