import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';

import { loadConfig } from '../server/config.js';
import { listReplays, pruneReplays, replayId, resolveReplay } from '../server/replays.js';

function fixtureDir(t) {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'q4-replays-'));
  t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
  return dir;
}

function writeReplay(dir, name, ageSeconds, metadata = null) {
  const replay = path.join(dir, name);
  const durationMs = Number.isInteger(metadata?.durationMs) ? metadata.durationMs : 0;
  const contents = Buffer.alloc(20);
  contents.write('Q4SD', 0, 'ascii');
  contents.writeInt32LE(Number.isInteger(metadata?.version) ? metadata.version : 1, 4);
  contents.writeInt32LE(3, 8);
  contents.writeInt32LE(durationMs, 12);
  contents.writeInt32LE(0, 16);
  fs.writeFileSync(replay, contents);
  const stamp = new Date(Date.now() - ageSeconds * 1000);
  fs.utimesSync(replay, stamp, stamp);
  if (metadata) fs.writeFileSync(`${replay}.meta.json`, JSON.stringify(metadata));
}

test('replay listing publishes only finalized OpenQ4 server demos', (t) => {
  const dir = fixtureDir(t);
  const metadata = {
    format: 'openq4-server-demo',
    version: 1,
    complete: true,
    durationMs: 90_000,
    map: 'mp/q4dm1',
  };
  writeReplay(dir, 'match.q4sd', 1, metadata);
  fs.writeFileSync(path.join(dir, 'recording.q4sd.part'), 'partial');
  fs.writeFileSync(path.join(dir, 'legacy.q4r'), 'wire capture');
  fs.writeFileSync(path.join(dir, 'render.demo'), 'not a server demo');

  const replays = listReplays(dir);
  assert.equal(replays.length, 1);
  assert.equal(replays[0].name, 'match.q4sd');
  assert.deepEqual(replays[0].metadata, metadata);
  assert.match(replays[0].deepLink, /^\/\?replay=/);
  assert.equal(replays[0].watchUrl, `/api/replays/${replays[0].id}/watch`);
});

test('replay listing and direct resolution reject renamed, incomplete, or inconsistent recordings', (t) => {
  const dir = fixtureDir(t);
  const complete = {
    format: 'openq4-server-demo',
    version: 1,
    complete: true,
    durationMs: 60_000,
  };

  writeReplay(dir, 'valid.q4sd', 1, complete);
  writeReplay(dir, 'missing-sidecar.q4sd', 2);
  writeReplay(dir, 'incomplete.q4sd', 3, { ...complete, complete: false });
  writeReplay(dir, 'duration-mismatch.q4sd', 4, complete);
  fs.writeFileSync(path.join(dir, 'duration-mismatch.q4sd.meta.json'),
    JSON.stringify({ ...complete, durationMs: complete.durationMs + 1 }));
  writeReplay(dir, 'bad-magic.q4sd', 5, complete);
  const badMagic = fs.openSync(path.join(dir, 'bad-magic.q4sd'), 'r+');
  fs.writeSync(badMagic, Buffer.from('NOPE'), 0, 4, 0);
  fs.closeSync(badMagic);
  writeReplay(dir, 'missing-end.q4sd', 6, complete);
  const missingEnd = fs.openSync(path.join(dir, 'missing-end.q4sd'), 'r+');
  fs.writeSync(missingEnd, Buffer.alloc(4), 0, 4, 8);
  fs.closeSync(missingEnd);

  assert.deepEqual(listReplays(dir).map((entry) => entry.name), ['valid.q4sd']);
  assert.ok(resolveReplay(dir, replayId('valid.q4sd')));
  for (const name of ['missing-sidecar.q4sd', 'incomplete.q4sd', 'duration-mismatch.q4sd',
    'bad-magic.q4sd', 'missing-end.q4sd']) {
    assert.equal(resolveReplay(dir, replayId(name)), null, `${name} must not be directly addressable`);
  }
});

test('retention keeps newest demos and removes their metadata sidecars', (t) => {
  const dir = fixtureDir(t);
  for (let i = 0; i < 5; i++) writeReplay(dir, `match-${i}.q4sd`, i, {
    format: 'openq4-server-demo',
    version: 1,
    complete: true,
    durationMs: i * 1000,
    index: i,
  });

  const removed = pruneReplays(dir, 3);
  assert.deepEqual(removed, ['match-3.q4sd', 'match-4.q4sd']);
  assert.deepEqual(listReplays(dir, 3).map((item) => item.name), [
    'match-0.q4sd',
    'match-1.q4sd',
    'match-2.q4sd',
  ]);
  assert.equal(fs.existsSync(path.join(dir, 'match-4.q4sd.meta.json')), false);
});

test('retention counts hidden invalid recordings and removes their sidecars beyond the limit', (t) => {
  const dir = fixtureDir(t);
  const complete = {
    format: 'openq4-server-demo',
    version: 1,
    complete: true,
    durationMs: 60_000,
  };
  writeReplay(dir, 'newest.q4sd', 1, complete);
  writeReplay(dir, 'second.q4sd', 2, complete);
  writeReplay(dir, 'bad-magic.q4sd', 3, complete);
  const badMagic = fs.openSync(path.join(dir, 'bad-magic.q4sd'), 'r+');
  fs.writeSync(badMagic, Buffer.from('NOPE'), 0, 4, 0);
  fs.closeSync(badMagic);
  const badMagicStamp = new Date(Date.now() - 3000);
  fs.utimesSync(path.join(dir, 'bad-magic.q4sd'), badMagicStamp, badMagicStamp);
  writeReplay(dir, 'missing-sidecar.q4sd', 4);

  assert.deepEqual(pruneReplays(dir, 2), ['bad-magic.q4sd', 'missing-sidecar.q4sd']);
  assert.equal(fs.existsSync(path.join(dir, 'bad-magic.q4sd')), false);
  assert.equal(fs.existsSync(path.join(dir, 'bad-magic.q4sd.meta.json')), false);
  assert.equal(fs.existsSync(path.join(dir, 'missing-sidecar.q4sd')), false);
  assert.deepEqual(listReplays(dir, 2).map((entry) => entry.name), ['newest.q4sd', 'second.q4sd']);
});

test('dedicated defaults enable full-state demos in the configured replay directory', () => {
  const config = loadConfig({ env: {}, rootDir: '/tmp/q4-config-test' });
  assert.equal(config.replayRetention, 300);
  assert.deepEqual(config.replayAllocation, { maxNewPerIp: 2, windowMs: 10 * 60_000 });
  assert.deepEqual([...config.replayPlayback.ports], [28040, 28041, 28042, 28043, 28044, 28045, 28046, 28047]);
  assert.equal(config.replayPlayback.enabled, true);
  assert.equal(config.replayPlayback.startTimeoutMs, 120_000);
  const demoIndex = config.dedicated.args.indexOf('net_serverDemo') - 1;
  assert.deepEqual(config.dedicated.args.slice(demoIndex, demoIndex + 6), [
    '+set', 'net_serverDemo', '1',
    '+set', 'net_serverDemoPath', '/tmp/q4-config-test/replays',
  ]);
  const lanIndex = config.dedicated.args.indexOf('net_LANServer') - 1;
  assert.deepEqual(config.dedicated.args.slice(lanIndex, lanIndex + 9), [
    '+set', 'net_LANServer', '1',
    '+set', 'net_serverClientTimeout', '180',
    '+set', 'si_pure', '0',
  ]);
  const valueAfter = (name) => config.dedicated.args[config.dedicated.args.indexOf(name) + 1];
  assert.equal(valueAfter('bot_debug'), '0');
  assert.equal(valueAfter('si_maxPlayers'), '12');
  assert.equal(valueAfter('net_serverAllowDataChecksumMismatch'), '1');
  assert.equal(valueAfter('net_serverClientTimeout'), '180');
  assert.equal(valueAfter('fs_caseSensitiveOS'), '0');
  assert.equal(valueAfter('s_noSound'), '1');
  assert.equal(valueAfter('fs_savepath'), '/tmp/q4-config-test/runtime/native/home');
  for (const name of [
    'bot_minplayers',
    'si_fragLimit',
    'si_captureLimit',
    'si_timeLimit',
    'si_map',
    'si_gameType',
  ]) {
    assert.equal(config.dedicated.args.includes(name), false,
      `${name} must come only from the supervisor-managed autoexec`);
  }
  assert.equal(config.dedicated.savePath, '/tmp/q4-config-test/runtime/native/home');
  assert.equal(config.dedicated.port, 28004);
  assert.equal(
    config.dedicated.reloadConfigFile,
    '/tmp/q4-config-test/runtime/native/home/baseoq4/autoexec.cfg',
  );
});

test('dedicated reload configuration follows the exact explicit or XDG save path', () => {
  const xdg = loadConfig({
    env: { XDG_DATA_HOME: '/tmp/q4-xdg' },
    rootDir: '/tmp/q4-config-xdg',
  });
  const valueAfter = (config, name) =>
    config.dedicated.args[config.dedicated.args.indexOf(name) + 1];
  assert.equal(valueAfter(xdg, 'fs_savepath'), '/tmp/q4-xdg');
  assert.equal(xdg.dedicated.reloadConfigFile, '/tmp/q4-xdg/baseoq4/autoexec.cfg');

  const explicit = loadConfig({
    env: { XDG_DATA_HOME: '/tmp/q4-unused-xdg' },
    rootDir: '/tmp/q4-config-explicit',
    dedicatedCwd: '/tmp/q4-native',
    dedicatedArgs: ['+set', 'fs_savepath', 'save', '+spawnServer'],
  });
  assert.equal(explicit.dedicated.savePath, '/tmp/q4-native/save');
  assert.equal(explicit.dedicated.reloadConfigFile, '/tmp/q4-native/save/baseoq4/autoexec.cfg');
  assert.equal(explicit.dedicated.args.filter((value) => value === 'fs_savepath').length, 1);

  const sanitized = loadConfig({
    env: {},
    rootDir: '/tmp/q4-config-sanitized',
    gamePort: 28014,
    dedicatedArgs: [
      '+set', 'net_port', '28014',
      '+set', 'bot_minplayers', '3',
      '+set', 'si_fragLimit', '99',
      '+set', 'si_captureLimit', '98',
      '+set', 'si_timeLimit', '97',
      '+set', 'si_map', 'mp/q4dm1',
      '+set', 'si_gameType', 'Tourney',
      '+spawnServer',
    ],
  });
  assert.deepEqual(sanitized.dedicated.args, [
    '+set', 'net_port', '28014',
    '+set', 'fs_savepath', '/tmp/q4-config-sanitized/runtime/native/home',
    '+spawnServer',
  ]);
});

test('replay allocation guard configuration clamps env and override values', () => {
  const minimum = loadConfig({
    env: {
      Q4_REPLAY_NEW_ALLOCATIONS_PER_IP: '0',
      Q4_REPLAY_ALLOCATION_WINDOW_MS: '1',
    },
    rootDir: '/tmp/q4-config-minimum',
  });
  assert.deepEqual(minimum.replayAllocation, { maxNewPerIp: 1, windowMs: 10_000 });

  const maximum = loadConfig({
    env: {},
    rootDir: '/tmp/q4-config-maximum',
    replayAllocationLimit: 1_000,
    replayAllocationWindowMs: 100 * 24 * 60 * 60_000,
  });
  assert.deepEqual(maximum.replayAllocation, { maxNewPerIp: 100, windowMs: 24 * 60 * 60_000 });
});
