import assert from 'node:assert/strict';
import test from 'node:test';

import {
  DEFAULT_LAUNCHER,
  DEFAULT_MATCH,
  MODEL_METADATA_CONTRACT,
  MULTIPLAYER_MAPS,
  MenuConfigError,
  buildDedicatedMatchCommands,
  normalizeLauncherState,
  normalizeMatchRequest,
  normalizePlayerProfile,
  normalizeSettings,
  sanitizeEngineNick
} from '../public/js/menu-config.js';

test('catalog separates per-mode bot capability from legacy staged readiness', () => {
  const arena = MULTIPLAYER_MAPS.find(({ id }) => id === 'mp/q4web_arena');
  assert.deepEqual(arena.modes, ['DM', 'Team DM', 'Tourney']);
  assert.deepEqual(arena.botModes, ['DM']);
  assert.equal(arena.botReady, true);
  assert.equal(MULTIPLAYER_MAPS.some(({ id }) => id === 'mp/q4dm8'), true);
  assert.equal(MULTIPLAYER_MAPS.some(({ id }) => id === 'mp/q4dm11v1'), true);
  assert.equal(MULTIPLAYER_MAPS.some(({ id }) => id === 'mp/q4ctf5'), true);
  assert.equal(MULTIPLAYER_MAPS.some(({ id }) => id === 'mp/q4xctf6'), true);
  assert.deepEqual(MULTIPLAYER_MAPS.find(({ id }) => id === 'mp/q4ctf1').modes,
    ['Team DM', 'CTF', 'Arena CTF']);
  assert.deepEqual(MULTIPLAYER_MAPS.find(({ id }) => id === 'mp/q4ctf1').botModes, []);
  assert.deepEqual(MULTIPLAYER_MAPS.find(({ id }) => id === 'mp/q4dm1').botModes, ['DM']);
  assert.equal(MULTIPLAYER_MAPS.filter(({ botModes }) => botModes.includes('DM')).length, 18);
  assert.deepEqual(
    MULTIPLAYER_MAPS.filter(({ content, botReady }) => content === 'retail' && botReady).map(({ id }) => id),
    [
      'mp/q4dm11v1', 'mp/q4dm3', 'mp/q4dm6', 'mp/q4dm7', 'mp/q4dm8', 'mp/q4ctf5',
      'mp/q4xdm10', 'mp/q4xdm11', 'mp/q4xdm13', 'mp/q4xdm14', 'mp/q4xdm15', 'mp/q4xtourney2'
    ]
  );
  assert.equal(MODEL_METADATA_CONTRACT.defaultModel, 'auto');
  assert.equal('models' in MODEL_METADATA_CONTRACT, false);
});

test('match normalization preserves DM bots for navigation gating and disables unqualified modes', () => {
  assert.deepEqual(normalizeMatchRequest({
    map: 'mp/q4web_arena',
    mode: 'DM',
    botCount: 4,
    fragLimit: 25,
    timeLimit: 12
  }), {
    map: 'mp/q4web_arena',
    mode: 'DM',
    botCount: 4,
    fragLimit: 25,
    timeLimit: 12
  });

  assert.deepEqual(normalizeMatchRequest({
    map: 'mp/q4dm1',
    mode: 'DM',
    botCount: 8,
    fragLimit: 0,
    timeLimit: 60
  }), {
    map: 'mp/q4dm1',
    mode: 'DM',
    botCount: 8,
    fragLimit: 0,
    timeLimit: 60
  });

  assert.equal(normalizeMatchRequest({
    ...DEFAULT_MATCH,
    mode: 'Team DM',
    botCount: 8
  }).botCount, 0, 'team bot targeting is not enabled');
  assert.equal(normalizeMatchRequest({
    ...DEFAULT_MATCH,
    mode: 'Tourney',
    botCount: 8
  }).botCount, 0, 'tourney bot behavior is not release-qualified');
});

test('match normalization rejects unknown and incompatible map or mode values', () => {
  assert.throws(
    () => normalizeMatchRequest({ ...DEFAULT_MATCH, map: 'mp/q4web_arena; quit' }),
    (error) => error instanceof MenuConfigError && error.code === 'MATCH_MAP_UNKNOWN'
  );
  assert.throws(
    () => normalizeMatchRequest({ ...DEFAULT_MATCH, mode: 'DM; quit' }),
    (error) => error instanceof MenuConfigError && error.code === 'MATCH_MODE_UNKNOWN'
  );
  assert.throws(
    () => normalizeMatchRequest({ ...DEFAULT_MATCH, map: 'mp/q4ctf1', mode: 'DM' }),
    (error) => error instanceof MenuConfigError && error.code === 'MATCH_MODE_UNSUPPORTED'
  );
  assert.throws(
    () => normalizeMatchRequest({ ...DEFAULT_MATCH, command: 'quit' }),
    (error) => error instanceof MenuConfigError && error.code === 'MATCH_FIELD_UNKNOWN'
  );
  assert.throws(
    () => normalizeMatchRequest({
      ...DEFAULT_MATCH,
      map: 'mp/q4ctf1',
      mode: 'CTF',
      botCount: 0,
      fragLimit: 0
    }),
    (error) => error instanceof MenuConfigError
      && error.code === 'MATCH_INTEGER_RANGE'
      && error.details.field === 'fragLimit'
  );
});

test('match integer ranges are strict at both boundaries', () => {
  for (const [field, value] of [
    ['botCount', -1],
    ['botCount', 9],
    ['botCount', '8'],
    ['fragLimit', -1],
    ['fragLimit', 101],
    ['timeLimit', -1],
    ['timeLimit', 61],
    ['timeLimit', 1.5]
  ]) {
    assert.throws(
      () => normalizeMatchRequest({ ...DEFAULT_MATCH, [field]: value }),
      (error) => error instanceof MenuConfigError
        && error.code === 'MATCH_INTEGER_RANGE'
        && error.details.field === field
    );
  }

  assert.equal(normalizeMatchRequest({ ...DEFAULT_MATCH, botCount: 0 }).botCount, 0);
  assert.equal(normalizeMatchRequest({ ...DEFAULT_MATCH, botCount: 8 }).botCount, 8);
  assert.equal(normalizeMatchRequest({ ...DEFAULT_MATCH, fragLimit: 100 }).fragLimit, 100);
  assert.equal(normalizeMatchRequest({ ...DEFAULT_MATCH, timeLimit: 60 }).timeLimit, 60);
});

test('dedicated commands contain only normalized complete lines without newlines', () => {
  const commands = buildDedicatedMatchCommands({
    map: 'mp/q4web_arena',
    mode: 'DM',
    botCount: 6,
    fragLimit: 30,
    timeLimit: 20
  });
  assert.deepEqual(commands, [
    'set bot_minplayers 6',
    'set si_fragLimit 30',
    'set si_captureLimit 30',
    'set si_timeLimit 20',
    'set si_gameType "DM"',
    'set si_map mp/q4web_arena',
    'spawnServer mp/q4web_arena'
  ]);
  assert.equal(commands.every((command) => !command.includes('\n') && !command.includes('\r')), true);
});

test('engine nick sanitization removes argv and console injection characters', () => {
  assert.equal(sanitizeEngineNick('+quit'), 'quit');
  assert.equal(sanitizeEngineNick('Pawel; quit\n+exec evil.cfg'), 'Pawel quit exec evil.cfg');
  assert.equal(sanitizeEngineNick('";disconnect;"'), 'disconnect');
  assert.equal(sanitizeEngineNick('\u0000\u001b'), 'Gracz');
  assert.equal(sanitizeEngineNick('-exec autoexec.cfg'), 'exec autoexec.cfg');
  assert.equal(sanitizeEngineNick('a'.repeat(40)).length, 24);
});

test('player models are accepted only from staged allowed metadata', () => {
  const allowedModels = [
    { id: 'model_player_staged', label: 'Staged model', team: 'Marine' },
    { id: 'bad;quit', label: 'Invalid metadata' }
  ];
  assert.deepEqual(normalizePlayerProfile({
    nick: '+Pawel;quit',
    team: 'strogg',
    model: 'model_player_staged'
  }, allowedModels), {
    nick: 'Pawel quit',
    team: 'Strogg',
    model: 'model_player_staged'
  });
  assert.equal(normalizePlayerProfile({ model: 'model_player_unstaged' }, allowedModels).model, 'auto');
  assert.equal(normalizePlayerProfile({ model: 'bad;quit' }, allowedModels).model, 'auto');
});

test('settings and launcher state recover persisted values without escaping supported ranges', () => {
  assert.deepEqual(normalizeSettings({
    sensitivity: '99',
    invertY: 'true',
    fov: 40,
    masterVolume: -5,
    musicVolume: '0.35'
  }), {
    sensitivity: 20,
    invertY: true,
    fov: 70,
    masterVolume: 0,
    musicVolume: 0.35
  });

  assert.deepEqual(normalizeLauncherState({ difficulty: '3' }), {
    ...DEFAULT_LAUNCHER,
    difficulty: 3
  });
  assert.equal(normalizeLauncherState({ difficulty: 99 }).difficulty, 1);
});
