import assert from 'node:assert/strict';
import { execFileSync, spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';

const root = path.resolve(import.meta.dirname, '..');
const ingest = path.join(root, 'tools/ingest.mjs');

test('canonical arena source ships deterministic bot-ready compiler products', () => {
  const arena = path.join(root, 'mods/maps/q4web_arena/maps/mp');
  const expected = new Map([
    ['q4web_arena.map', '6f24f4553d7b72fbeb15db2ac4c52ee1564be92840d19a0e06ab642ae948fc6e'],
    ['q4web_arena.proc', 'f7590af2825a46e2afd3ad0ad2036bba3a85692c984f0239a134f5fbda8790cb'],
    ['q4web_arena.cm', '1e7a1df3c3eb06e92e8001c1e5c34aec60f9c7af0eedc2ca510dfc1d6c33ee4f'],
    ['q4web_arena.aas32', '5e5e47c258f73ff2b11bad9d77ca7a749160c009cb4908ba8f9f96386b1e449a'],
  ]);
  for (const [name, hash] of expected) {
    const contents = fs.readFileSync(path.join(arena, name));
    assert.equal(createHash('sha256').update(contents).digest('hex'), hash, name);
  }

  assert.match(fs.readFileSync(path.join(arena, 'q4web_arena.proc'), 'ascii'), /^PROC "4"/);
  assert.match(fs.readFileSync(path.join(arena, 'q4web_arena.cm'), 'ascii'), /^CM "3"/);
  assert.match(fs.readFileSync(path.join(arena, 'q4web_arena.aas32'), 'ascii'), /^DewmAAS "1\.08"/);
  const source = fs.readFileSync(path.join(arena, 'q4web_arena.map'), 'utf8');
  const textureMatrices = source.match(/\( \( 0\.015625 0 0 \) \( 0 0\.015625 0 \) \)/g) || [];
  assert.equal(textureMatrices.length, 36, 'arena surfaces use the intended readable texture scale');
  assert.doesNotMatch(source, /0\.03125/, 'arena must not restore the over-dense texture scale');

  const lightColors = [...source.matchAll(/"_color" "([^"]+)"/g)]
    .map((match) => match[1].split(' ').map(Number));
  assert.equal(lightColors.length, 3);
  for (const color of lightColors) {
    assert.equal(color.length, 3);
    assert.equal(color.every(Number.isFinite), true);
    assert.equal(Math.max(...color) - Math.min(...color) <= 0.01, true,
      `arena light must remain neutral: ${color.join(' ')}`);
  }

  const spawns = [...source.matchAll(/\{\s*"classname" "info_player_deathmatch"\s*"origin" "([^"]+)"/g)]
    .map((match) => match[1].split(' ').map(Number));
  assert.equal(spawns.length, 12, 'arena needs one unique spawn per public-server slot');
  assert.equal(new Set(spawns.map((origin) => origin.join(' '))).size, 12);
  for (const [x, y, z] of spawns) {
    assert.equal(Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z), true);
    assert.equal(Math.abs(x) <= 384 && Math.abs(y) <= 384 && z === 32, true);
  }
  for (let left = 0; left < spawns.length; left += 1) {
    for (let right = left + 1; right < spawns.length; right += 1) {
      const distance = Math.hypot(
        spawns[left][0] - spawns[right][0],
        spawns[left][1] - spawns[right][1]
      );
      assert.equal(distance >= 192, true, `spawn ${left} overlaps spawn ${right}`);
    }
  }
});

test('map compiler and service restart use bounded production-safe settings', () => {
  const source = fs.readFileSync(ingest, 'utf8');
  assert.match(source, /const MAP_COMPILE_TIMEOUT_MS = 90 \* 60_000;/);
  assert.match(source, /const SAFE_RESTART_TIMEOUT_MS = 15 \* 60_000;/);
  assert.match(source, /'fs_caseSensitiveOS', '0'/);
  assert.match(source, /'sys_allowMultipleInstances', '1'/);
  assert.match(source, /'fs_basepath', compilerRoot/);
  assert.match(source, /if \(ADDON_PATTERN\.test\(name\)\) continue;/);
  assert.match(source, /fs\.copyFileSync\(client, compilerClient\)/);
  assert.match(source, /spawnSync\('xvfb-run', \['-a', compilerClient/);
  assert.match(source, /cwd: compilerRoot/);
  assert.match(source, /\(\?:ERROR\|FATAL\):\/i\.test\(log\)/);
  assert.match(source, /safe_restart\.sh'[\s\S]*timeout: SAFE_RESTART_TIMEOUT_MS/);
});

function makePk4(rootDir, name, files) {
  const stage = path.join(rootDir, `${name}-stage`);
  fs.mkdirSync(stage, { recursive: true });
  for (const [relative, value] of Object.entries(files)) {
    const target = path.join(stage, relative);
    fs.mkdirSync(path.dirname(target), { recursive: true });
    fs.writeFileSync(target, value);
  }
  const archive = path.join(rootDir, `${name}.pk4`);
  execFileSync('zip', ['-rqX', archive, '.'], { cwd: stage });
  return archive;
}

function fixture(map = 'mp/q4web_test') {
  return {
    [`maps/${map}.map`]: 'Version 3\n{\n"classname" "worldspawn"\n}\n',
    [`maps/${map}.proc`]: 'PROC "4"\n\n1234567890\n',
    [`maps/${map}.cm`]: 'CM "3"\n\n1234567890\n',
    [`maps/${map}.aas32`]: 'DewmAAS "1.08"\n\n1234567890\n'
  };
}

function runIngest(archive, temporary, extra = []) {
  const dist = path.join(temporary, 'dist');
  const index = path.join(temporary, 'index.json');
  const core = path.join(temporary, 'core');
  fs.mkdirSync(core, { recursive: true });
  return spawnSync(process.execPath, [ingest, archive,
    '--license', 'CC0-1.0', '--source', 'self-authored', '--no-stage', '--no-restart', ...extra
  ], {
    cwd: root,
    encoding: 'utf8',
    env: { ...process.env, Q4_MODS_DIST: dist, Q4_MODS_INDEX: index, Q4_CORE_PATHS_DIRS: core }
  });
}

test('map ingest validates, annotates and content-hashes a bot-ready PK4', () => {
  const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'q4-ingest-test-'));
  try {
    const archive = makePk4(temporary, 'valid', fixture());
    const result = runIngest(archive, temporary, ['--author', 'Test']);
    assert.equal(result.status, 0, result.stderr);
    const output = JSON.parse(result.stdout);
    assert.equal(output.map, 'mp/q4web_test');
    assert.match(output.file, /^q4web_mp_q4web_test-[a-f0-9]{12}\.pk4$/);
    const published = path.join(temporary, 'dist', output.file);
    assert.equal(fs.existsSync(published), true);
    const listing = execFileSync('unzip', ['-Z1', published], { encoding: 'utf8' });
    assert.match(listing, /def\/q4web_mp_q4web_test\.def/);
    assert.match(listing, /q4web\/mp_q4web_test\.json/);
    const index = JSON.parse(fs.readFileSync(path.join(temporary, 'index.json'), 'utf8'));
    assert.equal(index.maps[0].sha256, output.sha256);
    assert.equal(index.maps[0].license, 'CC0-1.0');
  } finally { fs.rmSync(temporary, { recursive: true, force: true }); }
});

test('map ingest rejects missing navigation and executable payloads', () => {
  const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'q4-ingest-reject-'));
  try {
    const missing = fixture('mp/no_nav');
    delete missing['maps/mp/no_nav.aas32'];
    const missingResult = runIngest(makePk4(temporary, 'missing', missing), temporary);
    assert.notEqual(missingResult.status, 0);
    assert.match(missingResult.stderr, /aas32/);

    const executable = { ...fixture('mp/with_code'), 'gamex86.dll': 'not allowed' };
    const executableResult = runIngest(makePk4(temporary, 'executable', executable), temporary);
    assert.notEqual(executableResult.status, 0);
    assert.match(executableResult.stderr, /kodu wykonywalnego/);
  } finally { fs.rmSync(temporary, { recursive: true, force: true }); }
});
