#!/usr/bin/env node

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

import { chromium } from 'playwright-core';

import { verifyServerRetailBundleManifest } from '../public/js/importer.js';
import {
  firstSnapshotEvidence,
  makeReleaseIdentity,
  readReleaseIdentity,
  sameReleaseIdentity,
} from './e2e-evidence.mjs';
import {
  CRITICAL_ENGINE_LOG_PATTERNS,
  assertNoPayload,
  assertPayloadBatch,
  buildBundleScenarioPlan,
  bundleScenarioPhase,
  renderedPngEvidence,
  requestSummary,
} from './retail-bundle-map-gate.mjs';
import {
  RETAIL_BUNDLE_MAPS_DIAGNOSTIC_SCHEMA,
  assertRetailBundleMapDiagnosticEvidence,
  parseRetailBundleMapDiagnosticMaps,
  retailBundleMapRunOutputPaths,
  selectRetailBundleMapRun,
} from './retail-bundle-map-diagnostics.mjs';
import {
  readRetailBundleMapsGateIdentity,
  verifyRetailBundleMapsEvidence,
} from './verify-retail-bundle-maps-evidence.mjs';

const baseUrl = (process.env.Q4_E2E_URL || 'http://127.0.0.1:3280').replace(/\/$/, '');
const chrome = process.env.Q4_CHROME || '/usr/bin/google-chrome';
const gamePort = Number(process.env.Q4_E2E_GAME_PORT || 28004);
const scenarioTimeout = Number(
  process.env.Q4_BUNDLE_MAPS_E2E_SCENARIO_TIMEOUT_MS
  || process.env.Q4_BUNDLE_MAPS_E2E_ENGINE_TIMEOUT_MS
  || 15 * 60_000
);
const hashTimeout = Number(process.env.Q4_BUNDLE_MAPS_E2E_HASH_TIMEOUT_MS || 30 * 60_000);
const requestedDiagnosticMaps = parseRetailBundleMapDiagnosticMaps(
  process.env.Q4_BUNDLE_MAPS_E2E_MAPS
);
const {
  evidencePath,
  outDir,
} = retailBundleMapRunOutputPaths(requestedDiagnosticMaps);
const keepProfile = process.env.Q4_E2E_KEEP_PROFILE === '1';
const startedAt = new Date().toISOString();

assert.ok(Number.isInteger(scenarioTimeout) && scenarioTimeout >= 60_000,
  'Q4_BUNDLE_MAPS_E2E_SCENARIO_TIMEOUT_MS must be an integer of at least 60000');
assert.ok(Number.isInteger(hashTimeout) && hashTimeout >= 60_000,
  'Q4_BUNDLE_MAPS_E2E_HASH_TIMEOUT_MS must be an integer of at least 60000');
assert.ok(Number.isInteger(gamePort) && gamePort >= 1024 && gamePort <= 65535,
  'Q4_E2E_GAME_PORT is invalid');

function absoluteUrl(relativeUrl) {
  return new URL(relativeUrl, `${baseUrl}/`).href;
}

function createDeadline(label, timeoutMs) {
  const expiresAt = Date.now() + timeoutMs;
  return {
    label,
    expiresAt,
    remaining(stage = 'operation') {
      const remaining = expiresAt - Date.now();
      if (remaining <= 0) {
        throw new Error(`${label}: ${stage} exceeded the ${timeoutMs} ms deadline`);
      }
      return remaining;
    },
    async run(operation, stage = 'operation') {
      const remaining = this.remaining(stage);
      let timer;
      try {
        return await Promise.race([
          Promise.resolve().then(operation),
          new Promise((_, reject) => {
            timer = setTimeout(() => reject(
              new Error(`${label}: ${stage} exceeded the ${timeoutMs} ms deadline`)
            ), remaining);
          }),
        ]);
      } finally {
        clearTimeout(timer);
      }
    },
    async sleep(delay, stage = 'delay') {
      assert.ok(delay >= 0, `${label}: invalid ${stage}`);
      if (delay > this.remaining(stage)) {
        throw new Error(`${label}: ${stage} exceeds the ${timeoutMs} ms deadline`);
      }
      await new Promise((resolve) => setTimeout(resolve, delay));
    },
  };
}

async function status() {
  const response = await fetch(`${baseUrl}/api/status`, { cache: 'no-store' });
  assert.equal(response.status, 200, 'gateway status is unavailable');
  return response.json();
}

async function waitForStatus(predicate, timeout = 60_000) {
  const deadline = Date.now() + timeout;
  let snapshot = null;
  while (Date.now() < deadline) {
    snapshot = await status();
    if (predicate(snapshot)) return snapshot;
    await new Promise((resolve) => setTimeout(resolve, 250));
  }
  throw new Error(`server status condition timed out after ${timeout} ms: ${JSON.stringify(snapshot)}`);
}

async function deployedReleaseIdentity() {
  const [engineResponse, contentResponse, retailResponse, statusResponse] = await Promise.all([
    fetch(`${baseUrl}/engine/engine-manifest.json`, { cache: 'no-store' }),
    fetch(`${baseUrl}/content/baseoq4/manifest.json`, { cache: 'no-store' }),
    fetch(`${baseUrl}/retail/manifest.json`, { cache: 'no-store' }),
    fetch(`${baseUrl}/api/status`, { cache: 'no-store' }),
  ]);
  assert.equal(engineResponse.status, 200, 'deployed engine manifest is unavailable');
  assert.equal(contentResponse.status, 200, 'deployed content manifest is unavailable');
  assert.equal(retailResponse.status, 200, 'deployed retail manifest is unavailable');
  assert.equal(statusResponse.status, 200, 'deployed native identity is unavailable');
  const [engineBytes, contentBytes, retailManifest, deployedStatus] = await Promise.all([
    engineResponse.arrayBuffer(),
    contentResponse.arrayBuffer(),
    retailResponse.json(),
    statusResponse.json(),
  ]);
  const retailBytes = Buffer.from(`${JSON.stringify(retailManifest, null, 2)}\n`);
  return makeReleaseIdentity(engineBytes, contentBytes, retailBytes, deployedStatus.native);
}

async function assertReleaseCurrent(expected) {
  const [local, deployed] = await Promise.all([readReleaseIdentity(), deployedReleaseIdentity()]);
  assert.ok(sameReleaseIdentity(local, expected),
    'release artifacts changed while the all-map E2E was running');
  assert.ok(sameReleaseIdentity(deployed, expected),
    'deployed release does not match this checkout');
}

async function servedBundleCatalog(localCatalog) {
  const response = await fetch(`${baseUrl}/retail/bundles/manifest.json`, {
    cache: 'no-store',
    headers: { Accept: 'application/json' },
  });
  assert.equal(response.status, 200, 'served retail bundle manifest is unavailable');
  const served = await verifyServerRetailBundleManifest(await response.json());
  for (const field of [
    'revision', 'fingerprint', 'sourceRevision', 'sourceFingerprint',
    'totalBytes', 'performancePreset', 'maxMachineSpec',
  ]) {
    assert.equal(served[field], localCatalog[field],
      `served retail bundle ${field} does not match this checkout`);
  }
  assert.deepEqual(served.bundles, localCatalog.bundles,
    'served retail bundle files do not match this checkout');
  assert.deepEqual(served.maps, localCatalog.maps,
    'served retail bundle map graph does not match this checkout');
  return served;
}

function pageDiagnostics(page, label) {
  const failures = [];
  page.on('pageerror', (error) => failures.push(`${label} pageerror: ${error.stack || error}`));
  page.on('console', (message) => {
    if (message.type() === 'error' && !message.text().startsWith('Q4_ENGINE_STDERR ')) {
      failures.push(`${label} console: ${message.text()}`);
    }
  });
  return failures;
}

async function enableEngineEvidence(page) {
  await page.addInitScript(({ criticalPatterns }) => {
    const criticalRegexps = criticalPatterns.map((pattern) => new RegExp(pattern, 'i'));
    const evidence = {
      criticalLogs: [],
      firstSnapshots: [],
    };
    const criticalSeen = new Set();
    const snapshotSeen = new Set();
    const normalizedLine = (line) => String(line).replace(/\^[0-9]/g, '');
    const record = (stream, parts) => {
      const line = parts.map(String).join(' ');
      const normalized = normalizedLine(line);
      if (/^received first snapshot, gameInitId = -?\d+, gameFrame -?\d+ gameTime -?\d+$/.test(normalized)) {
        if (!snapshotSeen.has(normalized)) {
          snapshotSeen.add(normalized);
          evidence.firstSnapshots.push(normalized);
        }
      }
      if (!criticalRegexps.some((pattern) => pattern.test(normalized))) return;
      const key = `${stream}\0${normalized}`;
      if (criticalSeen.has(key)) return;
      criticalSeen.add(key);
      evidence.criticalLogs.push({ stream, line: normalized });
    };
    const wrappedSinks = new WeakSet();
    const wrapModule = (value) => {
      if (!value || (typeof value !== 'object' && typeof value !== 'function')) return value;
      for (const key of ['print', 'printErr']) {
        const sink = value[key];
        if (typeof sink !== 'function' || wrappedSinks.has(sink)) continue;
        const wrapped = function q4EvidenceSink(...parts) {
          record(key === 'printErr' ? 'stderr' : 'stdout', parts);
          return sink.apply(this, parts);
        };
        wrappedSinks.add(wrapped);
        value[key] = wrapped;
      }
      return value;
    };

    Object.defineProperty(globalThis, '__q4EngineEvidence', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: evidence,
    });
    let moduleValue = globalThis.Module;
    wrapModule(moduleValue);
    Object.defineProperty(globalThis, 'Module', {
      configurable: true,
      enumerable: true,
      get() {
        return moduleValue;
      },
      set(value) {
        const args = value?.arguments;
        const connectIndex = Array.isArray(args) ? args.indexOf('+connect') : -1;
        const hasVerbose = Array.isArray(args) && args.some((entry, index) => entry === '+set'
          && args[index + 1] === 'net_verbose' && args[index + 2] === '1');
        if (connectIndex >= 0 && !hasVerbose) args.splice(connectIndex, 0, '+set', 'net_verbose', '1');
        moduleValue = wrapModule(value);
      },
    });
  }, { criticalPatterns: CRITICAL_ENGINE_LOG_PATTERNS });
}

async function enableStrictTextDecoderGate(context) {
  await context.addInitScript(() => {
    if (!globalThis.TextDecoder) return;
    const originalDecode = TextDecoder.prototype.decode;
    TextDecoder.prototype.decode = function decode(input, options) {
      const buffer = ArrayBuffer.isView(input) ? input.buffer : input;
      if (buffer && (!(buffer instanceof ArrayBuffer) || buffer.resizable || buffer.growable)) {
        throw new TypeError('all-map E2E rejects resizable or shared ArrayBuffer input');
      }
      return originalDecode.call(this, input, options);
    };
  });
}

async function waitForEngine(page, label, deadline) {
  let last = null;
  let pending = null;
  while (Date.now() < deadline.expiresAt) {
    pending ||= page.evaluate(() => ({
      active: document.body.classList.contains('engine-active'),
      badge: document.querySelector('#runtimeBadge')?.dataset.level || null,
      title: document.querySelector('#runtimeTitle')?.textContent || null,
      message: document.querySelector('#runtimeMessage')?.textContent || null,
      details: document.querySelector('#runtimeDetails')?.textContent || null,
      logsReady: typeof window.Q4Shell?.logs === 'function',
    }));
    const outcome = await Promise.race([
      pending.then((snapshot) => ({ snapshot }), (error) => ({ error })),
      new Promise((resolve) => setTimeout(() => resolve(null), 5_000)),
    ]);
    if (!outcome) continue;
    pending = null;
    if (outcome.error) throw outcome.error;
    last = outcome.snapshot;
    if (last.badge === 'error') {
      throw new Error(`${label}: engine reported an error: ${JSON.stringify(last)}`);
    }
    if (last.active && last.logsReady) return last;
    await deadline.sleep(1_000, 'engine readiness polling');
  }
  throw new Error(`${label}: engine readiness exceeded the scenario deadline: ${JSON.stringify(last)}`);
}

async function waitForFirstSnapshot(page, label, deadline) {
  await page.waitForFunction(() => {
    const evidence = globalThis.__q4EngineEvidence;
    return evidence?.firstSnapshots?.length > 0
      || evidence?.criticalLogs?.some(({ line }) =>
        /(?:^|\s)(?:Aborted\(|Sys_Error:|Fatal error:|ERROR:)/i.test(String(line))
      );
  }, undefined, { timeout: deadline.remaining('first in-game snapshot') });
  const evidence = await page.evaluate(() => ({
    firstSnapshots: [...(globalThis.__q4EngineEvidence?.firstSnapshots || [])],
    criticalLogs: [...(globalThis.__q4EngineEvidence?.criticalLogs || [])],
    logs: typeof window.Q4Shell?.logs === 'function' ? window.Q4Shell.logs() : [],
  }));
  const snapshot = firstSnapshotEvidence(evidence.firstSnapshots);
  if (!snapshot) {
    const slug = label.replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '').toLowerCase();
    await fs.writeFile(path.join(outDir, `${slug}-pre-snapshot.log`), [
      ...evidence.criticalLogs.map(({ stream, line }) => `[${stream}] ${line}`),
      ...evidence.logs,
      '',
    ].join('\n'));
  }
  assert.ok(snapshot, `${label}: engine aborted before its first in-game snapshot`);
  return snapshot;
}

async function waitForLauncher(page, deadline) {
  await page.locator('#launcher').waitFor({
    state: 'visible',
    timeout: deadline.remaining('launcher visibility'),
  });
  await page.locator('#multiplayerButton').click();
  await page.locator('[data-launcher-panel="multiplayer"]').waitFor({
    state: 'visible',
    timeout: deadline.remaining('multiplayer panel visibility'),
  });
  await page.waitForFunction(() => {
    const play = document.querySelector('#multiplayerPlayButton');
    return play && !play.disabled && document.querySelectorAll('#multiplayerMaps [data-map]').length > 0;
  }, undefined, { timeout: deadline.remaining('multiplayer catalog readiness') });
}

async function importState(page) {
  return page.evaluate(async () => {
    const active = await window.Q4App.importer.getActiveImport();
    return active && {
      state: active.state,
      source: active.source,
      revision: active.revision,
      activeMap: active.activeMap,
      installedMaps: active.installedMaps,
      installedBundleIds: active.installedBundleIds,
    };
  });
}

function assertImportState(active, expected, catalog, label) {
  assert.equal(active?.state, 'ready', `${label}: bundle import is not ready`);
  assert.equal(active?.source, 'operator-server-bundles', `${label}: full retail fallback was used`);
  assert.equal(active?.revision, catalog.revision, `${label}: wrong bundle revision is active`);
  assert.equal(active?.activeMap, expected.id, `${label}: wrong OPFS map is active`);
  assert.deepEqual([...active.installedMaps].sort(), [...expected.installedMaps].sort(),
    `${label}: earlier maps were not retained`);
  assert.deepEqual([...active.installedBundleIds].sort(), [...expected.installedBundles].sort(),
    `${label}: earlier bundles were not retained`);
}

async function readCriticalLogs(page) {
  return page.evaluate(() => [...(globalThis.__q4EngineEvidence?.criticalLogs || [])]);
}

async function saveCanvas(page, scenario, deadline, suffix = '') {
  const slug = `${scenario.id}_${scenario.mode}`
    .replace(/[^a-z0-9]+/gi, '_')
    .replace(/^_|_$/g, '')
    .toLowerCase();
  const name = `${slug}${suffix}.png`;
  const canvas = page.locator('#gameCanvas');
  await canvas.waitFor({
    state: 'visible',
    timeout: deadline.remaining('game canvas visibility'),
  });
  const buffer = await deadline.run(() => canvas.screenshot({
    path: path.join(outDir, name),
    animations: 'disabled',
  }), 'game canvas capture');
  return renderedPngEvidence(buffer, name);
}

async function leaveToLauncher(page, deadline) {
  const navigation = page.waitForEvent('framenavigated', {
    predicate: (frame) => frame === page.mainFrame(),
    timeout: deadline.remaining('launcher return navigation'),
  });
  await page.keyboard.press('Escape');
  await navigation;
  await page.locator('#launcher').waitFor({
    state: 'visible',
    timeout: deadline.remaining('launcher return visibility'),
  });
  await waitForStatus((snapshot) =>
    snapshot.connections.filter(({ port }) => port === gamePort).length === 0,
  deadline.remaining('browser bridge cleanup'));
}

async function hashOpfsFiles(page) {
  return page.evaluate(async () => {
    const [{ RetailImporter }, { sha256Blob }] = await Promise.all([
      import('/js/importer.js'),
      import('/js/sha256.js'),
    ]);
    const importer = new RetailImporter();
    const active = await importer.getActiveImport();
    if (!active) throw new Error('no active retail revision');
    const storageRoot = await navigator.storage.getDirectory();
    const q4base = await storageRoot
      .getDirectoryHandle('q4-retail')
      .then((root) => root.getDirectoryHandle('revisions'))
      .then((revisions) => revisions.getDirectoryHandle(active.revision))
      .then((revision) => revision.getDirectoryHandle('runtime'))
      .then((runtime) => runtime.getDirectoryHandle('q4base'));
    const recordsByName = new Map((active.files || []).map((record) => [record.name, record]));
    const files = [];
    for await (const [name, handle] of q4base.entries()) {
      const record = recordsByName.get(name);
      if (handle.kind !== 'file') {
        files.push({
          id: record?.id || null,
          name,
          kind: handle.kind,
          size: null,
          sha256: null,
        });
        continue;
      }
      const file = await handle.getFile();
      files.push({
        id: record?.id || null,
        name,
        kind: handle.kind,
        size: file.size,
        sha256: await sha256Blob(file),
      });
    }
    files.sort((left, right) => left.name.localeCompare(right.name, 'en'));
    return { active, files };
  });
}

const initialIdentity = await readRetailBundleMapsGateIdentity();
const [catalog, initialStatus] = await Promise.all([
  servedBundleCatalog(initialIdentity.catalog),
  status(),
]);
assert.ok(initialStatus.dedicated?.ready, 'native dedicated server is not ready');
assert.deepEqual(
  initialStatus.catalog.maps.map(({ id, label, modes }) => ({ id, label, modes })),
  initialIdentity.menuMaps,
  'served multiplayer menu catalog does not match this checkout'
);
const run = selectRetailBundleMapRun(
  catalog,
  initialStatus.catalog.maps,
  requestedDiagnosticMaps
);
const plan = buildBundleScenarioPlan(run.catalog, run.menuMaps);
await assertReleaseCurrent(initialIdentity.releaseIdentity);

await fs.mkdir(outDir, { recursive: true });
const profile = await fs.mkdtemp(path.join(
  os.tmpdir(),
  run.diagnostic ? 'q4-retail-bundle-maps-diagnostic-' : 'q4-retail-bundle-maps-'
));
const knownPayloadUrls = new Set(catalog.bundles.map(({ url }) => absoluteUrl(url)));
const payloadRequests = [];
const fullRetailRequests = [];
let activePhase = null;
let context = null;
let lastMatchRequestAt = 0;

try {
  context = await chromium.launchPersistentContext(profile, {
    executablePath: chrome,
    headless: true,
    args: [
      '--no-sandbox',
      '--disable-dev-shm-usage',
      '--enable-unsafe-swiftshader',
      '--use-angle=swiftshader',
    ],
    viewport: { width: 1280, height: 720 },
    locale: 'pl-PL',
  });
  await enableStrictTextDecoderGate(context);
  context.on('request', (request) => {
    const url = request.url();
    const headers = request.headers();
    if (knownPayloadUrls.has(url)) {
      payloadRequests.push(requestSummary({
        phase: activePhase,
        url,
        method: request.method(),
        range: headers.range || null,
        ifRange: headers['if-range'] || null,
      }, activePhase));
    }
    if (new URL(url).pathname.startsWith('/retail/q4base/')) {
      fullRetailRequests.push({ phase: activePhase, url, method: request.method() });
    }
  });

  for (const extra of context.pages()) await extra.close();
  const scenarioEvidence = [];

  const runScenario = async (entry, { returned = false } = {}) => {
    const label = returned
      ? `return to ${entry.id} / ${entry.mode}`
      : `scenario ${entry.id} / ${entry.mode}`;
    const phase = returned ? `return:${entry.scenario}` : entry.scenario;
    const deadline = createDeadline(label, scenarioTimeout);
    const page = await context.newPage();
    const failures = pageDiagnostics(page, label);
    await enableEngineEvidence(page);
    try {
      return await deadline.run(async () => {
        await page.goto(
          `${baseUrl}/?nodirect=1&nick=${encodeURIComponent(`Q4MODE-${scenarioEvidence.length + 1}`)}`,
          {
            waitUntil: 'domcontentloaded',
            timeout: deadline.remaining('launcher navigation'),
          }
        );
        await page.bringToFront();
        await waitForLauncher(page, deadline);
        await page.locator(`[data-mode="${entry.mode}"]`).click();
        const mapButton = page.locator(`[data-map="${entry.id}"]`);
        await mapButton.waitFor({
          state: 'visible',
          timeout: deadline.remaining('map selection visibility'),
        });
        await mapButton.click();
        await deadline.run(() => page.evaluate(() => {
          for (const [selector, value] of [
            ['#multiplayerBots', '0'],
            ['#multiplayerFragLimit', '10'],
            ['#multiplayerTimeLimit', '0'],
          ]) {
            const input = document.querySelector(selector);
            input.value = value;
            input.dispatchEvent(new Event('input', { bubbles: true }));
          }
        }), 'match controls');
        const match = await deadline.run(
          () => page.evaluate(() => window.Q4App.launcher.getState().match),
          'launcher match state'
        );
        assert.deepEqual(match, {
          map: entry.id,
          mode: entry.mode,
          botCount: 0,
          fragLimit: 10,
          timeLimit: 0,
        }, `${label}: HTML launcher selected another fixture`);

        const offset = payloadRequests.length;
        activePhase = phase;
        const rateLimitDelay = 10_500 - (Date.now() - lastMatchRequestAt);
        if (rateLimitDelay > 0) {
          await deadline.sleep(rateLimitDelay, 'match mutation rate-limit delay');
        }
        const responsePromise = page.waitForResponse((response) => {
          const url = new URL(response.url());
          return url.pathname === '/api/match' && response.request().method() === 'POST';
        }, { timeout: deadline.remaining('match configuration response') });
        await page.locator('#multiplayerPlayButton').click();
        const response = await responsePromise;
        lastMatchRequestAt = Date.now();
        const matchResponse = {
          status: response.status(),
          body: await deadline.run(() => response.json(), 'match response JSON'),
        };
        assert.equal(matchResponse.status, 200, `${label}: match configuration failed`);
        assert.deepEqual(matchResponse.body.match, match, `${label}: server normalized another match`);

        await waitForEngine(page, label, deadline);
        const firstSnapshot = await waitForFirstSnapshot(page, label, deadline);
        await deadline.sleep(3_000, 'post-snapshot render settle');
        const server = await waitForStatus((snapshot) =>
          snapshot.dedicated?.ready
          && snapshot.match?.current?.map === entry.id
          && snapshot.connections.some(({ port }) => port === gamePort),
        deadline.remaining('dedicated scenario readiness'));
        assert.deepEqual(server.match.current, match, `${label}: dedicated server runs another fixture`);
        const bridges = server.connections.filter(({ port }) => port === gamePort);
        assert.equal(bridges.length, 1, `${label}: expected exactly one browser bridge`);
        const bridge = bridges[0];
        assert.ok(bridge.rxFrames > 0 && bridge.txFrames > 0, `${label}: browser bridge is idle`);

        const render = await saveCanvas(page, entry, deadline, returned ? '_returned' : '');
        const criticalLogs = await deadline.run(() => readCriticalLogs(page), 'critical log evidence');
        assert.deepEqual(criticalLogs, [], `${label}: critical engine diagnostics`);
        const active = await deadline.run(() => importState(page), 'active OPFS state');
        const expectedInstalledMaps = returned
          ? plan.at(-1).installedMapIds
          : entry.installedMapIds;
        const expectedInstalledBundles = returned
          ? plan.at(-1).installedBundleIds
          : entry.installedBundleIds;
        assertImportState(active, {
          id: entry.id,
          installedMaps: expectedInstalledMaps,
          installedBundles: expectedInstalledBundles,
        }, catalog, label);
        const requests = payloadRequests
          .slice(offset)
          .filter(({ phase: requestPhase }) => requestPhase === phase);
        if (returned || entry.newBundles.length === 0) assertNoPayload(requests, label);
        else assertPayloadBatch(requests, entry.newBundles, baseUrl, label);

        const result = {
          scenario: entry.scenario,
          map: entry.id,
          mode: entry.mode,
          firstForMap: returned ? false : entry.firstForMap,
          match,
          serverMap: server.match.current.map,
          newBundles: returned ? [] : entry.newBundles.map(({ id }) => id),
          requests: requests.length,
          installedMaps: active.installedMaps,
          installedBundles: active.installedBundleIds,
          firstSnapshot,
          criticalLogs,
          bridge: {
            id: bridge.id,
            rxFrames: bridge.rxFrames,
            txFrames: bridge.txFrames,
          },
          render,
        };
        await leaveToLauncher(page, deadline);
        assert.deepEqual(failures, [], failures.join('\n'));
        return result;
      }, 'complete map/mode scenario');
    } finally {
      activePhase = null;
      await page.close().catch(() => {});
    }
  };

  for (const entry of plan) {
    process.stdout.write(
      `[bundle-maps] ${scenarioEvidence.length + 1}/${plan.length} ${entry.id} / ${entry.mode}: ` +
      `${entry.newBundles.map(({ name }) => name).join(', ') || 'cache only'}\n`
    );
    scenarioEvidence.push(await runScenario(entry));
  }

  const returned = await runScenario(plan[0], { returned: true });
  assert.deepEqual(fullRetailRequests, [],
    'all-map bundle gate unexpectedly fetched the full retail catalog');

  const hashDeadline = createDeadline('final OPFS enumeration and hashing', hashTimeout);
  const hashPage = await context.newPage();
  let finalCache;
  try {
    await hashPage.goto(`${baseUrl}/legal/NOTICE.txt?retail-bundle-maps-e2e=1`, {
      waitUntil: 'domcontentloaded',
      timeout: hashDeadline.remaining('hash page navigation'),
    });
    finalCache = await hashDeadline.run(() => hashOpfsFiles(hashPage), 'OPFS enumeration and hashing');
  } finally {
    await hashPage.close().catch(() => {});
  }
  assertImportState({
    ...finalCache.active,
    installedBundleIds: finalCache.active.installedBundleIds,
  }, {
    id: plan[0].id,
    installedMaps: plan.at(-1).installedMapIds,
    installedBundles: plan.at(-1).installedBundleIds,
  }, catalog, 'final OPFS cache');

  const finalIdentity = await readRetailBundleMapsGateIdentity();
  assert.equal(finalIdentity.identitySha256, initialIdentity.identitySha256,
    'bundle map gate/runtime code changed while E2E was running');
  assert.equal(finalIdentity.manifestSha256, initialIdentity.manifestSha256,
    'retail bundle manifest changed while E2E was running');
  await assertReleaseCurrent(initialIdentity.releaseIdentity);

  const evidence = {
    schema: run.diagnostic ? RETAIL_BUNDLE_MAPS_DIAGNOSTIC_SCHEMA : 2,
    ...(run.diagnostic ? {
      diagnostic: true,
      selectedMaps: run.menuMaps.map(({ id, modes }) => ({ id, modes: [...modes] })),
    } : {}),
    status: 'passed',
    startedAt,
    checkedAt: new Date().toISOString(),
    baseUrl,
    revision: catalog.revision,
    sourceRevision: catalog.sourceRevision,
    manifestSha256: initialIdentity.manifestSha256,
    manifestBytes: initialIdentity.manifestBytes,
    runtimeIdentitySha256: initialIdentity.identitySha256,
    codeSha256: initialIdentity.codeSha256,
    releaseIdentitySha256: initialIdentity.releaseIdentity.identitySha256,
    scenarios: scenarioEvidence,
    returned,
    retainedMaps: finalCache.active.installedMaps,
    retainedBundles: finalCache.active.installedBundleIds,
    opfsBytes: finalCache.files.reduce((sum, file) => sum + file.size, 0),
    opfsFiles: finalCache.files,
    payloadRequests,
    fullRetailRequests,
  };
  if (run.diagnostic) {
    assertRetailBundleMapDiagnosticEvidence(evidence, run.menuMaps);
  } else {
    verifyRetailBundleMapsEvidence(evidence, initialIdentity);
  }
  await fs.mkdir(path.dirname(evidencePath), { recursive: true });
  const temporary = `${evidencePath}.${process.pid}.tmp`;
  await fs.writeFile(temporary, `${JSON.stringify(evidence, null, 2)}\n`);
  await fs.rename(temporary, evidencePath);
  if (run.diagnostic) {
    process.stdout.write(
      `Q4 bundle-map diagnostic passed: ${plan.length} selected map/mode scenarios rendered ` +
      `from one fresh persistent OPFS cache; evidence: ${evidencePath}\n`
    );
  } else {
    process.stdout.write(
      `Q4 all-map bundle E2E passed: ${plan.length} map/mode scenarios rendered from one ` +
      `persistent OPFS cache; return to ${bundleScenarioPhase(plan[0].id, plan[0].mode)} ` +
      'fetched 0 PK4 payloads.\n'
    );
  }
} finally {
  await context?.close().catch(() => {});
  if (keepProfile) process.stderr.write(`Retained Chromium profile: ${profile}\n`);
  else await fs.rm(profile, { recursive: true, force: true });
}
