import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';

import {
  readRetailBundleManifestIdentity,
  verifyRetailBundleCacheEvidence,
  verifyRetailBundleCacheEvidenceFile,
} from '../tools/verify-retail-bundle-cache-evidence.mjs';

function passedEvidence(current) {
  const [firstMap, secondMap] = current.catalog.maps;
  assert.ok(firstMap && secondMap, 'test manifest must contain at least two maps');
  const firstIds = new Set(firstMap.bundleIds);
  const switchedIds = secondMap.bundleIds.filter((id) => !firstIds.has(id));
  const retainedIds = [...new Set([...firstMap.bundleIds, ...secondMap.bundleIds])].sort();
  const bundlesById = new Map(current.catalog.bundles.map((bundle) => [bundle.id, bundle]));
  const downloadedIds = [...firstMap.bundleIds, ...switchedIds];
  const payloadRequests = downloadedIds.map((id) => {
    const bundle = bundlesById.get(id);
    return {
      url: `http://127.0.0.1:3280${bundle.url}`,
      method: 'GET',
      range: `bytes=0-${bundle.size - 1}`,
      ifRange: `\"${bundle.sha256}\"`,
    };
  });

  return {
    schema: 3,
    status: 'passed',
    startedAt: '2026-07-16T00:00:00.000Z',
    checkedAt: '2026-07-16T00:01:00.000Z',
    baseUrl: 'http://127.0.0.1:3280',
    revision: current.catalog.revision,
    sourceRevision: current.catalog.sourceRevision,
    manifestSha256: current.manifestSha256,
    manifestBytes: current.manifestBytes,
    runtimeIdentitySha256: current.identitySha256,
    codeSha256: { ...current.codeSha256 },
    phases: {
      first: {
        map: firstMap.id,
        bundles: [...firstMap.bundleIds],
        requests: firstMap.bundleIds.length,
      },
      repeated: { map: firstMap.id, requests: 0 },
      switched: {
        map: secondMap.id,
        bundles: switchedIds,
        requests: switchedIds.length,
      },
      returned: { map: firstMap.id, requests: 0 },
    },
    retainedMaps: [firstMap.id, secondMap.id].sort(),
    retainedBundles: retainedIds,
    opfsBytes: retainedIds.reduce((sum, id) => sum + bundlesById.get(id).size, 0),
    opfsFiles: retainedIds.map((id) => {
      const { name, size, sha256 } = bundlesById.get(id);
      return { id, name, size, sha256 };
    }),
    payloadRequests,
  };
}

test('publication accepts cache evidence bound to the exact retail bundle manifest bytes', async () => {
  const current = await readRetailBundleManifestIdentity();
  assert.equal(verifyRetailBundleCacheEvidence(passedEvidence(current), current), current);
});

test('publication rejects stale, failed, incomplete, or non-persistent cache evidence', async (t) => {
  const current = await readRetailBundleManifestIdentity();
  const fixtures = [
    ['old schema', (value) => { value.schema = 2; }, /schema 3/],
    ['failed run', (value) => { value.status = 'failed'; }, /did not pass/],
    ['stale bundle revision', (value) => { value.revision = 'q4bundles-' + '0'.repeat(20); }, /revision/],
    ['stale retail revision', (value) => { value.sourceRevision = 'q4retail-' + '0'.repeat(20); }, /sourceRevision/],
    ['stale manifest bytes', (value) => { value.manifestSha256 = '0'.repeat(64); }, /SHA-256/],
    ['stale runtime identity', (value) => { value.runtimeIdentitySha256 = '0'.repeat(64); }, /importer\/gateway identity/],
    ['stale importer hash', (value) => { value.codeSha256.importer = '0'.repeat(64); }, /code hashes/],
    ['stale supervisor hash', (value) => { value.codeSha256.dedicatedSupervisor = '0'.repeat(64); }, /code hashes/],
    ['missing phase', (value) => { delete value.phases.returned; }, /exactly the required phases/],
    ['reload fetched payload', (value) => { value.phases.repeated.requests = 1; }, /repeated phase/],
    ['return fetched payload', (value) => { value.phases.returned.requests = 1; }, /returned phase/],
    ['wrong first bundles', (value) => { value.phases.first.bundles.pop(); }, /first phase bundles/],
    ['missing retained map', (value) => { value.retainedMaps.pop(); }, /retained maps/],
    ['missing retained bundle', (value) => { value.retainedBundles.pop(); }, /retained bundles/],
    ['wrong OPFS bytes', (value) => { value.opfsBytes -= 1; }, /OPFS byte count/],
    ['wrong actual OPFS hash', (value) => { value.opfsFiles[0].sha256 = '0'.repeat(64); }, /actual OPFS file hashes/],
    ['unaccounted payload request', (value) => {
      value.payloadRequests.push(structuredClone(value.payloadRequests[0]));
    }, /payload request list/],
    ['range gap', (value) => { value.payloadRequests[0].range = 'bytes=1-1'; }, /gap or duplicate/],
    ['wrong payload ETag', (value) => { value.payloadRequests[0].ifRange = '"' + '0'.repeat(64) + '"'; }, /SHA-256/],
  ];

  for (const [name, mutate, expected] of fixtures) {
    await t.test(name, () => {
      const evidence = passedEvidence(current);
      mutate(evidence);
      assert.throws(() => verifyRetailBundleCacheEvidence(evidence, current), expected);
    });
  }
});

test('file verifier hashes the complete manifest and rejects malformed or byte-stale evidence', async (t) => {
  const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'q4-bundle-cache-evidence-'));
  t.after(() => fs.rm(directory, { recursive: true, force: true }));
  const sourceManifest = new URL('../runtime/retail-bundle-manifest.json', import.meta.url);
  const manifestBytes = await fs.readFile(sourceManifest);
  const manifestPath = path.join(directory, 'retail-bundle-manifest.json');
  const evidencePath = path.join(directory, 'retail-bundle-cache-e2e.json');
  await fs.writeFile(manifestPath, manifestBytes);

  const current = await readRetailBundleManifestIdentity({ manifestPath });
  assert.equal(current.manifestSha256, createHash('sha256').update(manifestBytes).digest('hex'));
  await fs.writeFile(evidencePath, `${JSON.stringify(passedEvidence(current))}\n`);
  assert.deepEqual(
    await verifyRetailBundleCacheEvidenceFile({ evidencePath, manifestPath }),
    await verifyRetailBundleCacheEvidenceFile({ evidencePath, currentIdentity: current })
  );

  await fs.appendFile(manifestPath, '\n');
  await assert.rejects(
    verifyRetailBundleCacheEvidenceFile({ evidencePath, manifestPath }),
    /SHA-256/
  );

  await fs.writeFile(evidencePath, '{not-json\n');
  await assert.rejects(
    verifyRetailBundleCacheEvidenceFile({ evidencePath, currentIdentity: current }),
    /not valid JSON/
  );
});
