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

import { PNG } from 'pngjs';

import { MULTIPLAYER_MAPS } from '../public/js/menu-config.js';
import {
  assertPayloadBatch,
  buildBundleScenarioPlan,
  bundleScenarioPhase,
  criticalEngineLog,
  preferredMapMode,
  renderedPngEvidence,
} from '../tools/retail-bundle-map-gate.mjs';

const digest = (character) => character.repeat(64);
const bundles = [
  {
    id: 'common',
    name: 'pak001.pk4',
    size: 10,
    sha256: digest('a'),
    url: `/${digest('a')}-pak001.pk4`,
    kind: 'common',
  },
  {
    id: 'map/mp/dm',
    name: 'pak910_dm.pk4',
    size: 4,
    sha256: digest('b'),
    url: `/${digest('b')}-pak910_dm.pk4`,
    kind: 'map',
  },
  {
    id: 'map/mp/ctf',
    name: 'pak910_ctf.pk4',
    size: 6,
    sha256: digest('c'),
    url: `/${digest('c')}-pak910_ctf.pk4`,
    kind: 'map',
  },
];
const catalog = {
  bundles,
  maps: [
    { id: 'mp/dm', bundleIds: ['common', 'map/mp/dm'] },
    { id: 'mp/ctf', bundleIds: ['common', 'map/mp/ctf'] },
  ],
};
const menuMaps = [
  { id: 'mp/dm', label: 'DM', modes: ['DM', 'Team DM'] },
  { id: 'mp/ctf', label: 'CTF', modes: ['Team DM', 'CTF'] },
];

test('all-map bundle plan covers every map/mode pair and downloads once per map', () => {
  const plan = buildBundleScenarioPlan(catalog, menuMaps);
  assert.deepEqual(plan.map(({ id, mode, firstForMap }) => ({ id, mode, firstForMap })), [
    { id: 'mp/dm', mode: 'DM', firstForMap: true },
    { id: 'mp/dm', mode: 'Team DM', firstForMap: false },
    { id: 'mp/ctf', mode: 'Team DM', firstForMap: true },
    { id: 'mp/ctf', mode: 'CTF', firstForMap: false },
  ]);
  assert.deepEqual(plan[0].newBundles.map(({ id }) => id), ['common', 'map/mp/dm']);
  assert.deepEqual(plan[1].newBundles, []);
  assert.deepEqual(plan[2].newBundles.map(({ id }) => id), ['map/mp/ctf']);
  assert.deepEqual(plan[3].newBundles, []);
  assert.deepEqual(plan[3].installedBundleIds, ['common', 'map/mp/ctf', 'map/mp/dm']);
  assert.deepEqual(plan[3].installedMapIds, ['mp/ctf', 'mp/dm']);
  assert.equal(plan[3].scenario, bundleScenarioPhase('mp/ctf', 'CTF'));

  assert.throws(
    () => buildBundleScenarioPlan({ ...catalog, maps: catalog.maps.slice(0, 1) }, menuMaps),
    /complete multiplayer menu/
  );
  assert.throws(
    () => buildBundleScenarioPlan(catalog, [...menuMaps, { id: 'mp/extra', modes: ['DM'] }]),
    /complete multiplayer menu/
  );
  assert.equal(preferredMapMode({ id: 'mp/ctf', modes: ['CTF'] }), 'CTF');
});

test('current multiplayer menu expands to all 62 map/mode scenarios', () => {
  const currentBundles = [
    {
      id: 'common',
      name: 'pak001.pk4',
      size: 10,
      sha256: digest('f'),
      url: `/${digest('f')}-pak001.pk4`,
      kind: 'common',
    },
    ...MULTIPLAYER_MAPS.map((map, index) => ({
      id: `map/${map.id}`,
      name: `pak9${String(index).padStart(2, '0')}.pk4`,
      size: index + 1,
      sha256: String(index + 1).padStart(64, '0'),
      url: `/map-${index}.pk4`,
      kind: 'map',
    })),
  ];
  const currentCatalog = {
    bundles: currentBundles,
    maps: MULTIPLAYER_MAPS.map((map) => ({
      id: map.id,
      bundleIds: ['common', `map/${map.id}`],
    })),
  };
  const plan = buildBundleScenarioPlan(currentCatalog, MULTIPLAYER_MAPS);

  assert.equal(MULTIPLAYER_MAPS.length, 23);
  assert.equal(plan.length, 62);
  assert.equal(new Set(plan.map(({ scenario }) => scenario)).size, 62);
  for (const map of MULTIPLAYER_MAPS) {
    const scenarios = plan.filter(({ id }) => id === map.id);
    assert.deepEqual(scenarios.map(({ mode }) => mode), [...map.modes]);
    assert.equal(scenarios.filter(({ firstForMap }) => firstForMap).length, 1);
    assert.ok(scenarios[0].newBundles.length > 0);
    assert.ok(scenarios.slice(1).every(({ newBundles }) => newBundles.length === 0));
  }
});

test('critical engine diagnostics cover persistent fatal and runtime asset failures', () => {
  const critical = [
    'Sys_Error: Failed to load map',
    "^3WARNING: ^1InvalidGUI: 'guis/mphud.gui'",
    "^3WARNING: ^1Couldn't load cube image: gfx/env/sheen",
    "^3WARNING: ^1Couldn't load anim: 'models/weapons/fire.md5anim'",
    '^3WARNING: ^1Starting defaulted sound sample sound/weapons/fire.wav',
    "^3WARNING: ^1Couldn't find GLSL sources for program 'bad'",
    "^3WARNING: ^1Modern GL program 'bad' is missing uMainTexture",
  ];
  assert.ok(critical.every(criticalEngineLog));
  assert.equal(criticalEngineLog("Can't find generated/images/optional.bimage"), false);
  assert.equal(criticalEngineLog('Postprocess materials missing or invalid; falling back to direct render.'), false);
});

test('payload proof accepts complete bounded ranges exactly once', () => {
  const baseUrl = 'https://q4.example';
  const bundle = bundles[0];
  const request = (start, end) => ({
    url: new URL(bundle.url, `${baseUrl}/`).href,
    method: 'GET',
    range: `bytes=${start}-${end}`,
    ifRange: `\"${bundle.sha256}\"`,
  });
  assert.doesNotThrow(() => assertPayloadBatch(
    [request(5, 9), request(0, 4)],
    [bundle],
    baseUrl,
    'common'
  ));
  assert.throws(
    () => assertPayloadBatch([request(0, 3), request(5, 9)], [bundle], baseUrl, 'common'),
    /gap or duplicate/
  );
  assert.throws(
    () => assertPayloadBatch([{ ...request(0, 9), ifRange: `\"${digest('0')}\"` }], [bundle], baseUrl, 'common'),
    /SHA-256/
  );
});

test('central canvas proof rejects black frames and accepts a varied rendered image', () => {
  const black = new PNG({ width: 800, height: 450 });
  black.data.fill(0);
  for (let offset = 3; offset < black.data.length; offset += 4) black.data[offset] = 255;
  assert.throws(() => renderedPngEvidence(PNG.sync.write(black), 'black'), /blank or black/);

  const rendered = new PNG({ width: 800, height: 450 });
  for (let y = 0; y < rendered.height; y += 1) {
    for (let x = 0; x < rendered.width; x += 1) {
      const offset = (y * rendered.width + x) * 4;
      rendered.data[offset] = (x * 7 + y * 3) & 255;
      rendered.data[offset + 1] = (x * 2 + y * 11) & 255;
      rendered.data[offset + 2] = (x * 13 + y * 5) & 255;
      rendered.data[offset + 3] = 255;
    }
  }
  const evidence = renderedPngEvidence(PNG.sync.write(rendered), 'rendered');
  assert.ok(evidence.nonDark > evidence.samples * 0.9);
  assert.ok(evidence.colors > 30);
});
