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

import { buildBundleScenarioPlan } from '../tools/retail-bundle-map-gate.mjs';
import { verifyRetailBundleMapsEvidence } from '../tools/verify-retail-bundle-maps-evidence.mjs';

const digest = (character) => character.repeat(64);
const menuMaps = [
  { id: 'mp/one', label: 'One', modes: ['DM'] },
  { id: 'mp/two', label: 'Two', modes: ['Team DM', 'CTF'] },
  { id: 'mp/three', label: 'Three', modes: ['DM', 'Tourney'] },
];
const bundles = [
  ['common', 'pak001.pk4', 10, 'a', 'common'],
  ['map/mp/one', 'pak910_one.pk4', 3, 'b', 'map'],
  ['map/mp/two', 'pak910_two.pk4', 4, 'c', 'map'],
  ['map/mp/three', 'pak910_three.pk4', 5, 'd', 'map'],
].map(([id, name, size, hash, kind]) => ({
  id,
  name,
  size,
  sha256: digest(hash),
  url: `/retail/bundles/${digest(hash)}-${name}`,
  kind,
}));
const catalog = {
  revision: 'q4bundles-' + '1'.repeat(20),
  sourceRevision: 'q4retail-' + '2'.repeat(20),
  bundles,
  maps: menuMaps.map(({ id }) => ({ id, bundleIds: ['common', `map/${id}`] })),
};
const current = {
  catalog,
  menuMaps,
  manifestSha256: digest('e'),
  manifestBytes: 1234,
  identitySha256: digest('f'),
  codeSha256: { mapGate: digest('1'), importer: digest('2') },
  releaseIdentity: { identitySha256: digest('3') },
};
const render = {
  label: 'map.png',
  width: 1280,
  height: 720,
  region: { left: 256, top: 130, right: 1024, bottom: 591 },
  samples: 60000,
  opaque: 60000,
  nonDark: 40000,
  colors: 500,
  mean: 80,
  deviation: 25,
};
const snapshot = {
  line: 'received first snapshot, gameInitId = 1, gameFrame 2 gameTime 3',
  gameInitId: 1,
  gameFrame: 2,
  gameTime: 3,
  clientState: 'CS_INGAME',
  serverStateRequiredToSendSnapshot: 'SCS_INGAME',
};

function passedEvidence() {
  const plan = buildBundleScenarioPlan(catalog, menuMaps);
  const payloadRequests = [];
  const scenarios = plan.map((entry, index) => {
    for (const bundle of entry.newBundles) {
      payloadRequests.push({
        phase: entry.scenario,
        url: `http://127.0.0.1:3280${bundle.url}`,
        method: 'GET',
        range: `bytes=0-${bundle.size - 1}`,
        ifRange: `\"${bundle.sha256}\"`,
      });
    }
    const match = {
      map: entry.id,
      mode: entry.mode,
      botCount: 0,
      fragLimit: 10,
      timeLimit: 0,
    };
    return {
      scenario: entry.scenario,
      map: entry.id,
      mode: entry.mode,
      firstForMap: entry.firstForMap,
      match,
      serverMap: entry.id,
      newBundles: entry.newBundles.map(({ id }) => id),
      requests: entry.newBundles.length,
      installedMaps: entry.installedMapIds,
      installedBundles: entry.installedBundleIds,
      firstSnapshot: { ...snapshot, gameInitId: index + 1 },
      criticalLogs: [],
      bridge: { id: index + 1, rxFrames: 10, txFrames: 12 },
      render: { ...render, label: `${entry.id}-${entry.mode}.png` },
    };
  });
  const retainedIds = plan.at(-1).installedBundleIds;
  const byId = new Map(bundles.map((bundle) => [bundle.id, bundle]));
  return {
    schema: 2,
    status: 'passed',
    startedAt: '2026-07-16T00:00:00.000Z',
    checkedAt: '2026-07-16T00:30:00.000Z',
    baseUrl: 'http://127.0.0.1:3280',
    revision: catalog.revision,
    sourceRevision: catalog.sourceRevision,
    manifestSha256: current.manifestSha256,
    manifestBytes: current.manifestBytes,
    runtimeIdentitySha256: current.identitySha256,
    codeSha256: { ...current.codeSha256 },
    releaseIdentitySha256: current.releaseIdentity.identitySha256,
    scenarios,
    returned: {
      ...structuredClone(scenarios[0]),
      firstForMap: false,
      newBundles: [],
      requests: 0,
      installedMaps: plan.at(-1).installedMapIds,
      installedBundles: retainedIds,
      render: { ...render, label: 'returned.png' },
    },
    retainedMaps: plan.at(-1).installedMapIds,
    retainedBundles: retainedIds,
    opfsBytes: retainedIds.reduce((sum, id) => sum + byId.get(id).size, 0),
    opfsFiles: retainedIds.map((id) => {
      const { name, size, sha256 } = byId.get(id);
      return { id, name, kind: 'file', size, sha256 };
    }),
    payloadRequests,
    fullRetailRequests: [],
  };
}

test('all-map publication evidence covers every map/mode scenario and accumulated bundle', () => {
  assert.equal(verifyRetailBundleMapsEvidence(passedEvidence(), current), current);
});

test('all-map evidence rejects missing scenarios, critical logs, bad renders and repeated payloads', async (t) => {
  const fixtures = [
    ['missing scenario', (value) => { value.scenarios.pop(); }, /every multiplayer map\/mode pair/],
    ['wrong native map', (value) => { value.scenarios[1].serverMap = 'mp/one'; }, /dedicated server/],
    ['critical asset log', (value) => {
      value.scenarios[0].criticalLogs.push({ stream: 'stderr', line: 'InvalidGUI: hud' });
    }, /critical diagnostics/],
    ['black render', (value) => { value.scenarios[0].render.nonDark = 0; }, /blank or black/],
    ['lost first snapshot', (value) => {
      value.scenarios[0].firstSnapshot.clientState = 'CS_CONNECTED';
    }, /CS_INGAME/],
    ['payload in cached mode', (value) => {
      const cached = value.scenarios.find((entry) => !entry.firstForMap);
      value.payloadRequests.push({
        ...value.payloadRequests[0],
        phase: cached.scenario,
      });
      cached.requests += 1;
    }, /cached map fetched/],
    ['repeated return payload', (value) => {
      value.payloadRequests.push({
        ...value.payloadRequests[0],
        phase: `return:${value.returned.scenario}`,
      });
    }, /cached map fetched/],
    ['wrong OPFS hash', (value) => {
      value.opfsFiles[0].sha256 = digest('0');
    }, /enumerated OPFS entries/],
    ['extra OPFS file', (value) => {
      value.opfsFiles.push({
        id: null,
        name: 'pak999_extra.pk4',
        kind: 'file',
        size: 1,
        sha256: digest('9'),
      });
      value.opfsBytes += 1;
    }, /OPFS byte count|enumerated OPFS entries/],
  ];
  for (const [name, mutate, pattern] of fixtures) {
    await t.test(name, () => {
      const evidence = passedEvidence();
      mutate(evidence);
      assert.throws(() => verifyRetailBundleMapsEvidence(evidence, current), pattern);
    });
  }
});
