// tests/background/sw-bundle-import.test.ts
//
// Tier-1 SW-bundle-loadability gate (Debug session 01-08-sw-incompatibility).
//
// This test closes the orchestrator-side gap that produced the 01-08 init
// crash: prior to this gate, the only SW-compat assertion was
// `tests/background/webm-remux-deps.test.ts`, which loaded the SOURCE
// packages (`ts-ebml`, `webm-muxer`) under default Node globals. That test
// passed because the raw packages import fine in Node — but it never
// loaded the BUNDLED output emitted by `vite build`, and Vite's CJS-interop
// pipeline tree-shook the transitive `ebml` package out of the bundle while
// leaving a dangling destructure reference (`{tools:f}=Pc` against an empty
// placeholder `Pc`). The result: SW dies at top-level module init with
// `TypeError: Cannot read properties of undefined (reading 'readVint')`
// before any handler can register, and the operator's chrome://extensions
// "service worker" link goes inaccessible.
//
// The gate ships in TWO layers:
//
//   Layer 1 ("built SW chunk imports without throwing...") — verifies the
//   BUNDLED artifact's module-init reaches completion under SW-simulated
//   globals (Buffer/process/window/document all stripped). This is the
//   gate that catches Vite/Rollup CJS-interop bugs like the `Pc={}`-
//   placeholder tree-shake from 01-08. Buffer is stripped here because
//   `vite-plugin-node-polyfills` rewrites bundled `Buffer` references into
//   imports of the polyfill chunk's exported `Buffer` (no dependency on
//   `globalThis.Buffer`); if a future bundler change re-introduces a
//   `globalThis.Buffer` dependency, this strip catches it.
//
//   Layer 2 ("source remuxSegments invocation does not throw an SW-
//   incompatible error...") — verifies that the SOURCE `remuxSegments`
//   code path is REACHABLE under SW-simulated globals. Layer 1 only
//   proves module-init; if ts-ebml or webm-muxer reaches a code path
//   only when `remuxSegments` is INVOKED (which only happens at
//   archive-save time, deep inside `chrome.runtime.onMessage` for
//   `SAVE_ARCHIVE`), Layer 1 will pass while the real SW could still
//   crash mid-archive. Layer 2 catches that.
//
//   IMPORTANT — Layer 2 polyfill semantics. Layer 2 imports SOURCE
//   webm-remux.ts directly via Node's TS transform; the polyfill is a
//   BUNDLER-LEVEL rewrite (Buffer → imported polyfill chunk) that does
//   NOT apply when source is loaded outside Vite. To faithfully model
//   what the real SW sees at runtime (the bundle with polyfilled Buffer),
//   Layer 2 leaves `Buffer` AVAILABLE in the child env — exactly mirroring
//   what the polyfilled bundle provides. Stripping Buffer in Layer 2
//   would simulate a more-restrictive runtime than what the polyfilled
//   bundle actually faces, producing false-positive failures. `process`,
//   `window`, `document` remain stripped because the polyfill is
//   configured with `globals.process: false` and the SW genuinely has no
//   DOM.
//
// Implementation note: the strip+import happens in a SPAWNED Node child
// process, not in-process. Vitest's own RPC layer references both `Buffer`
// (`node_modules/vitest/dist/chunks/rpc.*.js`) and `process.nextTick`, so
// stripping those on the test runner's globalThis crashes vitest itself.
// A child process gives us a fresh V8 isolate where we can strip cleanly.
//
// chrome.* mock: the SW bundle's top-level module init calls
// `chrome.runtime.onMessage.addListener(...)`, `chrome.action.onClicked
// .addListener(...)`, etc. In the real Service Worker runtime, Chrome
// provides the `chrome.*` global. In our Node-simulated isolate, we must
// provide a no-op stub or those calls will throw `ReferenceError: chrome
// is not defined` — which is a TEST-ENVIRONMENT incompleteness, NOT a
// bundle bug. The mock is a recursive Proxy that returns callable no-ops
// for any `chrome.<anything>.<anything>(...)` chain. It does NOT exercise
// any chrome.* behavior — it only proves that the bundle's top-level
// module evaluation completes without throwing. Verifying actual
// chrome.* behavior is the responsibility of the offscreen-handshake
// and end-to-end smoke tests, not this gate.
//
// Layer 2 source-import: Node 24 ships native TypeScript transform via
// `--experimental-transform-types` (no `tsx` / `ts-node` install needed).
// `webm-remux.ts` imports `'../shared/logger'` without a `.ts` extension —
// vite/rollup add the extension at bundle time, but the bare Node ESM
// resolver does NOT. To bridge that, the child registers a tiny resolution
// hook that retries failed `./...`/`../...` lookups with a `.ts` suffix.
// The hook is inlined as a `data:` URL so the test stays self-contained.
//
// Layer 2 invocation: we pass `remuxSegments` a single synthetic Blob
// carrying a 4-byte EBML magic + minimal "DocType=webm" header — enough
// to push the call past entry-point validation and into ts-ebml's
// decoder. Domain errors (invalid-EBML, missing-Tracks, etc.) are
// ACCEPTABLE outcomes; they prove the runtime path is structurally
// reachable under SW-simulated globals. Only `process` / CSP-eval errors
// (and a `Buffer` ReferenceError, which would mean the polyfill itself
// regressed) indicate genuine SW-incompat that would crash the SW mid-
// archive in Chrome. The classifier in the child encodes that policy.
//
// Pre-flight contract: callers must `npm run build` first. The test fails
// fast with a clear "run npm run build" message if `dist/` is missing.
//
// Per `feedback-pre-checkpoint-bundle-gates.md` (auto-loaded memory): any
// future plan executor whose work surfaces a SW must run this test before
// any operator-empirical checkpoint.
//
// Reference for SW-restricted globals:
//   https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope
// Reference for Node ESM dynamic-import accepting file:// URLs:
//   https://nodejs.org/api/esm.html#import-expressions
// Reference for `child_process.execFile`:
//   https://nodejs.org/api/child_process.html#child_processexecfilefile-args-options-callback
// Reference for Proxy traps:
//   https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
// Reference for Node 24 native TS transform:
//   https://nodejs.org/api/typescript.html#type-stripping
// Reference for Node ESM resolution hooks (`register`):
//   https://nodejs.org/api/module.html#moduleregisterspecifier-parenturl-options
// Reference for vite-plugin-node-polyfills (Buffer rewrite policy):
//   https://github.com/davidmyersdev/vite-plugin-node-polyfills

import { execFile } from 'node:child_process';
import { existsSync, readFileSync } from 'node:fs';
import { resolve as resolvePath } from 'node:path';
import { pathToFileURL } from 'node:url';
import { promisify } from 'node:util';

import { describe, expect, it } from 'vitest';

const execFileAsync = promisify(execFile);

// Layer 1 strip list — applied to the spawned child BEFORE it imports the
// BUILT SW chunk. Service Workers expose ServiceWorkerGlobalScope which
// has NO `window`, NO `document`, NO `Buffer`, and NO `process`. They DO
// have standard browser-ish globals (fetch, Blob, Crypto, etc.) which
// Node provides natively in modern versions, so we don't strip those.
//
// `Buffer` is stripped here EVEN THOUGH `vite-plugin-node-polyfills`
// resolves Buffer at bundle-time: the bundler rewrites every `Buffer`
// reference into an import of the polyfill chunk's exported `Buffer`,
// so the bundle itself never reads `globalThis.Buffer`. Stripping Buffer
// from the child's globalThis is therefore an additional contract: "the
// bundle must not depend on globalThis.Buffer." A regression where the
// polyfill plugin gets removed or misconfigured would re-introduce a
// raw `Buffer.alloc(...)` somewhere in the bundle, and Layer 1 would
// catch it.
const SW_BUNDLE_STRIP_GLOBALS: ReadonlyArray<string> = [
  'window',
  'document',
  'Buffer',
  'process',
];

// Layer 2 strip list — applied to the spawned child BEFORE it dynamically
// imports the SOURCE `src/background/webm-remux.ts`. The polyfill is a
// BUNDLER-LEVEL rewrite that DOES NOT apply when ts-ebml's source is
// loaded outside Vite (via Node's TS transform). So in Layer 2, `Buffer`
// is left available in the child env — exactly mirroring what the
// polyfilled BUNDLE provides at SW runtime. Stripping it here would
// simulate a more-restrictive environment than the polyfilled bundle
// actually faces in Chrome, producing false-positive failures.
//
// `process`, `window`, `document` remain stripped: the polyfill is
// configured with `globals.process: false` (not provided), and the SW
// genuinely has no DOM regardless of polyfills.
const SW_SOURCE_STRIP_GLOBALS: ReadonlyArray<string> = [
  'window',
  'document',
  'process',
];

// Sentinel strings emitted by the child process; checked by the parent
// to distinguish success from failure without resorting to exit codes
// (the child may exit 0 on a caught throw).
const CHILD_OK_SENTINEL = '__SW_BUNDLE_IMPORT_OK__';
const CHILD_FAIL_SENTINEL = '__SW_BUNDLE_IMPORT_FAILED__';

// Layer 2 sentinels. The runtime invocation distinguishes three outcomes:
// success (returned a Blob), domain-error (threw but the error is the
// expected shape of a parse failure on our synthetic input — acceptable),
// and SW-incompat (threw a process/CSP error — RED; or a Buffer
// ReferenceError, which would mean the polyfill itself regressed).
const CHILD_REMUX_OK_SENTINEL = '__SW_REMUX_OK__';
const CHILD_REMUX_DOMAIN_ERROR_SENTINEL = '__SW_REMUX_DOMAIN_ERROR__';
const CHILD_REMUX_SW_INCOMPAT_SENTINEL = '__SW_REMUX_SW_INCOMPAT__';

// Cap how long the child has to import. Real SW init takes < 1 s; if the
// import takes longer than this, treat it as a hang (functionally the
// same as a top-level throw from the operator's perspective).
const CHILD_TIMEOUT_MS = 10_000;

// Source path for Layer 2. Absolute to avoid `process.cwd()` quirks inside
// the spawned child (whose CWD may diverge from the test runner's).
const WEBM_REMUX_SOURCE_PATH = resolvePath(
  process.cwd(),
  'src/background/webm-remux.ts',
);

interface ChildImportResult {
  readonly ok: boolean;
  readonly errorMessage: string;
  readonly errorStackFirstLine: string;
}

/** Layer 2 outcome categories — see file header for semantics. */
type RemuxOutcome = 'ok' | 'domain_error' | 'sw_incompat';

interface ChildRemuxResult {
  readonly outcome: RemuxOutcome;
  readonly errorName: string;
  readonly errorMessage: string;
  readonly errorStack: string;
}

/**
 * Resolve the file:// URL of the built SW chunk by parsing the
 * `dist/service-worker-loader.js` shim that crxjs emits. The shim is a
 * one-liner of the form `import './assets/index.ts-<hash>.js';` whose hash
 * changes per build; parsing it avoids hard-coding a content hash that
 * would break on every rebuild.
 *
 * @returns Absolute file:// URL of the SW chunk, ready for dynamic import.
 * @throws If `dist/` is missing or the loader shim cannot be parsed.
 */
function resolveBuiltSwChunkUrl(): string {
  const distDir = resolvePath(process.cwd(), 'dist');
  const loaderPath = resolvePath(distDir, 'service-worker-loader.js');

  if (!existsSync(loaderPath)) {
    throw new Error(
      `dist/service-worker-loader.js not found at ${loaderPath}. ` +
        `Run \`npm run build\` before running this test.`,
    );
  }

  const loaderSource = readFileSync(loaderPath, 'utf8');
  // crxjs emits exactly: `import './assets/index.ts-<hash>.js';\n`
  const importMatch = loaderSource.match(/^\s*import\s+['"](.+?)['"]\s*;?\s*$/m);
  if (importMatch === null) {
    throw new Error(
      `Could not parse SW import path from service-worker-loader.js. ` +
        `Loader content was: ${JSON.stringify(loaderSource)}`,
    );
  }

  const chunkRelativePath = importMatch[1];
  const chunkAbsolutePath = resolvePath(distDir, chunkRelativePath);
  return pathToFileURL(chunkAbsolutePath).href;
}

/**
 * Build the source for the recursive `chrome.*` Proxy mock the child
 * installs on its own `globalThis` before importing the SW bundle.
 *
 * The mock proves a minimal contract: any `chrome.<api>.<method>(...)` chain
 * evaluated at module-init time returns a callable no-op (or no-op object
 * with `addListener`/`removeListener` shims) rather than throwing
 * `ReferenceError: chrome is not defined` or `TypeError: ... is not a
 * function`. It does NOT exercise any chrome.* behavior — this is purely
 * a gate that the bundle's top-level evaluation reaches completion.
 *
 * Kept as a string-emitting helper so the mock is in scope inside the
 * spawned child's ESM body, where it must execute BEFORE the dynamic
 * import of the SW chunk.
 *
 * @returns A line of ESM source that installs `globalThis.chrome` as a
 *   recursive Proxy returning no-op callables.
 */
function buildChromeMockSource(): string {
  // The Proxy's `get` trap returns another Proxy whose target is a
  // no-op function, so the returned value is both callable (`chrome.api()`)
  // and indexable (`chrome.api.subprop`). The inner `get` shims well-known
  // event-listener methods to silent no-ops; everything else recurses.
  //
  // Symbol.toPrimitive is short-circuited to `undefined` so the JS engine
  // doesn't try to coerce the Proxy itself when, e.g., string-concatenating
  // a chrome.* reference — it would throw "Cannot convert object to
  // primitive value" without this guard.
  return [
    `const chromeNoop = () => undefined;`,
    `const makeProxy = () => new Proxy(chromeNoop, {`,
    `  get(_, p) {`,
    `    if (p === Symbol.toPrimitive) return undefined;`,
    `    if (p === 'then') return undefined;`,
    `    if (p === 'addListener' || p === 'removeListener' || p === 'hasListener') return () => {};`,
    `    return makeProxy();`,
    `  },`,
    `  apply() { return undefined; },`,
    `});`,
    `globalThis.chrome = makeProxy();`,
  ].join(' ');
}

/**
 * Build the inline ESM source that the spawned child will execute. The
 * child strips the listed globals on its own isolate, installs a no-op
 * `chrome.*` mock, then dynamically imports the SW chunk, reporting back
 * via stdout sentinels.
 *
 * Kept as a function (not a template literal at module top) so the
 * sentinel/global lists stay synced with the constants above.
 *
 * @param chunkUrl - file:// URL the child will import.
 * @returns ESM source the child runs under `node --input-type=module`.
 */
function buildChildSource(chunkUrl: string): string {
  const stripLines = SW_BUNDLE_STRIP_GLOBALS.map(
    (key) => `delete globalThis['${key}'];`,
  ).join(' ');
  const chromeMockLine = buildChromeMockSource();
  return [
    stripLines,
    chromeMockLine,
    `try {`,
    `  await import(${JSON.stringify(chunkUrl)});`,
    `  console.log(${JSON.stringify(CHILD_OK_SENTINEL)});`,
    `} catch (e) {`,
    `  const msg = e && e.message ? String(e.message) : String(e);`,
    `  const stack = e && e.stack ? String(e.stack).split('\\n')[0] : '(no stack)';`,
    `  console.log(${JSON.stringify(CHILD_FAIL_SENTINEL)});`,
    `  console.log('MSG:' + msg);`,
    `  console.log('STK:' + stack);`,
    `}`,
  ].join(' ');
}

/**
 * Build the inline ESM source for Layer 2: dynamic-import the SOURCE
 * `webm-remux.ts` under stripped globals and invoke `remuxSegments`
 * against a synthetic single-segment input. The child registers a
 * resolution hook so that `import '../shared/logger'` (no `.ts`
 * extension) resolves via the `.ts` source, mimicking what
 * vite/rollup does at bundle time.
 *
 * The synthetic segment is the minimum EBML doctype header
 * (`1A 45 DF A3` magic + `42 82 84 'webm'` DocType element). ts-ebml
 * may accept this and emit zero frames, or throw on missing
 * Segment/Cluster — both are domain-correct outcomes. Only `process`,
 * `Buffer` (which would indicate polyfill regression), or eval-related
 * errors prove SW-incompat.
 *
 * @param remuxSourceUrl - file:// URL of `src/background/webm-remux.ts`.
 * @returns ESM source the child runs under `node --experimental-transform-types`.
 */
function buildRemuxRuntimeChildSource(remuxSourceUrl: string): string {
  // Loader hook: when a relative specifier (./ or ../) has NO extension,
  // try the default resolver first; if it returns ERR_MODULE_NOT_FOUND,
  // retry with `.ts` appended. This mirrors vite/rollup's extension
  // resolution policy for the project's TS source tree.
  const loaderSource = [
    `export async function resolve(specifier, context, next) {`,
    `  const isRelative = specifier.startsWith('./') || specifier.startsWith('../');`,
    `  const hasKnownExt = specifier.endsWith('.ts') || specifier.endsWith('.js')`,
    `    || specifier.endsWith('.mjs') || specifier.endsWith('.cjs')`,
    `    || specifier.endsWith('.json');`,
    `  if (isRelative && !hasKnownExt) {`,
    `    try { return await next(specifier, context); }`,
    `    catch (err) {`,
    `      if (err && err.code === 'ERR_MODULE_NOT_FOUND') {`,
    `        return await next(specifier + '.ts', context);`,
    `      }`,
    `      throw err;`,
    `    }`,
    `  }`,
    `  return next(specifier, context);`,
    `}`,
  ].join(' ');

  // Encode the loader as a base64 data: URL so the child does not need any
  // on-disk loader file. Buffer is available HERE (parent context) because
  // we only call this helper before the strip happens in the child.
  const loaderDataUrl =
    'data:text/javascript;base64,' +
    Buffer.from(loaderSource, 'utf8').toString('base64');

  const chromeMockLine = buildChromeMockSource();
  const stripLines = SW_SOURCE_STRIP_GLOBALS.map(
    (key) => `delete globalThis['${key}'];`,
  ).join(' ');

  // Classifier for the runtime outcome. The "SW-incompat" set is precisely
  // the failure modes that would crash the real SW in Chrome at
  // archive-save time, GIVEN the current bundler config (polyfilled Buffer):
  //   - ReferenceError: Buffer is not defined  (would mean the polyfill
  //     itself has regressed; we still flag this as SW-incompat because
  //     it would crash the real SW if the polyfill were removed)
  //   - ReferenceError: process is not defined (polyfill is configured
  //     `globals.process: false`, so process truly is missing at SW
  //     runtime)
  //   - EvalError / CSP errors (would-be `new Function(...)` in encoder
  //     blocked by MV3 SW CSP)
  //   - SyntaxError from eval (same CSP class)
  // Everything else (parse failures, missing Tracks, etc.) is a domain
  // error — the runtime path is structurally reachable, the SW would
  // surface a clean error to the operator instead of crashing the worker.
  //
  // NOTE: Layer 2 leaves `Buffer` AVAILABLE in the child env (see
  // SW_SOURCE_STRIP_GLOBALS), mirroring the polyfilled bundle. A
  // `Buffer is not defined` here would only happen if the child Node
  // environment itself lost Buffer (essentially impossible) or if a
  // future test refactor accidentally re-stripped it — in both cases
  // we want to surface that as SW-incompat-class noise rather than
  // mask it as a domain error.
  const classifyExprParts = [
    `(err) => {`,
    `  const name = err && err.name ? String(err.name) : '';`,
    `  const msg = err && err.message ? String(err.message) : String(err);`,
    `  if (name === 'ReferenceError' && /\\b(Buffer|process)\\b/.test(msg)) return 'sw_incompat';`,
    `  if (name === 'EvalError') return 'sw_incompat';`,
    `  if (/Refused to evaluate|unsafe-eval/.test(msg)) return 'sw_incompat';`,
    `  return 'domain_error';`,
    `}`,
  ];
  const classifyExpr = classifyExprParts.join(' ');

  return [
    // STEP 1: register the resolution hook FIRST, so subsequent imports
    // (including the dynamic import of webm-remux.ts) go through it.
    `import { register } from 'node:module';`,
    `import { pathToFileURL } from 'node:url';`,
    `register(${JSON.stringify(loaderDataUrl)}, pathToFileURL('./'));`,

    // STEP 2: strip SW-forbidden globals. Must happen AFTER register()
    // because register() touches `process` internally. Note that
    // SW_SOURCE_STRIP_GLOBALS does NOT include 'Buffer' — see classifier
    // comment above for rationale.
    stripLines,

    // STEP 3: install chrome.* mock. webm-remux.ts itself does not touch
    // chrome.*, but its `Logger` import chain might transitively, so we
    // keep symmetry with Layer 1.
    chromeMockLine,

    // STEP 4: import and invoke. Wrap everything in try/catch and emit
    // sentinels via console.log.
    `const classify = ${classifyExpr};`,
    `try {`,
    `  const mod = await import(${JSON.stringify(remuxSourceUrl)});`,
    `  if (typeof mod.remuxSegments !== 'function') {`,
    `    console.log(${JSON.stringify(CHILD_REMUX_SW_INCOMPAT_SENTINEL)});`,
    `    console.log('NAME:Error');`,
    `    console.log('MSG:remuxSegments export missing from source module');`,
    `    console.log('STK:(no stack)');`,
    `  } else {`,
    // Synthetic single-segment payload: EBML magic + DocType=webm.
    `    const segBytes = new Uint8Array([0x1a, 0x45, 0xdf, 0xa3, 0x42, 0x82, 0x84, 0x77, 0x65, 0x62, 0x6d]);`,
    `    const segBlob = new Blob([segBytes], { type: 'video/webm' });`,
    `    const segments = [{ data: segBlob, timestamp: 1 }];`,
    `    try {`,
    `      const out = await mod.remuxSegments(segments);`,
    `      console.log(${JSON.stringify(CHILD_REMUX_OK_SENTINEL)});`,
    `      console.log('SIZE:' + String(out && typeof out.size === 'number' ? out.size : -1));`,
    `    } catch (innerErr) {`,
    `      const outcome = classify(innerErr);`,
    `      const sentinel = outcome === 'sw_incompat'`,
    `        ? ${JSON.stringify(CHILD_REMUX_SW_INCOMPAT_SENTINEL)}`,
    `        : ${JSON.stringify(CHILD_REMUX_DOMAIN_ERROR_SENTINEL)};`,
    `      console.log(sentinel);`,
    `      console.log('NAME:' + (innerErr && innerErr.name ? String(innerErr.name) : 'Error'));`,
    `      console.log('MSG:' + (innerErr && innerErr.message ? String(innerErr.message) : String(innerErr)));`,
    `      console.log('STK:' + (innerErr && innerErr.stack ? String(innerErr.stack).split('\\n').slice(0, 5).join(' | ') : '(no stack)'));`,
    `    }`,
    `  }`,
    `} catch (importErr) {`,
    // An error here means the SOURCE module failed to LOAD under stripped
    // globals — classify it the same way (process/CSP class at module-
    // init level in webm-remux.ts's transitive deps would be a real
    // SW-incompat).
    `  const outcome = classify(importErr);`,
    `  const sentinel = outcome === 'sw_incompat'`,
    `    ? ${JSON.stringify(CHILD_REMUX_SW_INCOMPAT_SENTINEL)}`,
    `    : ${JSON.stringify(CHILD_REMUX_DOMAIN_ERROR_SENTINEL)};`,
    `  console.log(sentinel);`,
    `  console.log('NAME:' + (importErr && importErr.name ? String(importErr.name) : 'Error'));`,
    `  console.log('MSG:' + (importErr && importErr.message ? String(importErr.message) : String(importErr)));`,
    `  console.log('STK:' + (importErr && importErr.stack ? String(importErr.stack).split('\\n').slice(0, 5).join(' | ') : '(no stack)'));`,
    `}`,
  ].join(' ');
}

/**
 * Spawn a Node child process that imports the SW chunk under stripped
 * globals and returns the result. Resolves rather than rejects on import
 * failure — the failure is the data the test asserts on.
 *
 * @param chunkUrl - file:// URL the child will import.
 * @returns Structured result; `ok: false` means the bundle threw.
 */
async function runSwBundleImportInChild(chunkUrl: string): Promise<ChildImportResult> {
  const source = buildChildSource(chunkUrl);
  const { stdout } = await execFileAsync(
    process.execPath,
    ['--input-type=module', '-e', source],
    {
      timeout: CHILD_TIMEOUT_MS,
      maxBuffer: 4 * 1024 * 1024,
    },
  );

  if (stdout.includes(CHILD_OK_SENTINEL)) {
    return { ok: true, errorMessage: '', errorStackFirstLine: '' };
  }

  if (stdout.includes(CHILD_FAIL_SENTINEL)) {
    const msgMatch = stdout.match(/^MSG:(.*)$/m);
    const stkMatch = stdout.match(/^STK:(.*)$/m);
    return {
      ok: false,
      errorMessage: msgMatch?.[1] ?? '(no message)',
      errorStackFirstLine: stkMatch?.[1] ?? '(no stack)',
    };
  }

  throw new Error(
    `Child process produced neither OK nor FAIL sentinel. ` +
      `stdout was: ${JSON.stringify(stdout)}`,
  );
}

/**
 * Spawn a Node child process that source-imports `src/background/
 * webm-remux.ts` under SW-simulated globals and invokes `remuxSegments`
 * against a synthetic single-segment payload. Resolves with a structured
 * outcome — `sw_incompat` is the test failure mode.
 *
 * @param remuxSourceUrl - file:// URL of the webm-remux.ts source.
 * @returns Structured outcome with name/message/stack on errors.
 */
async function runRemuxRuntimeInChild(remuxSourceUrl: string): Promise<ChildRemuxResult> {
  const source = buildRemuxRuntimeChildSource(remuxSourceUrl);
  const { stdout } = await execFileAsync(
    process.execPath,
    // `--experimental-transform-types` enables Node 24's native TS support
    // (strip types + handle enums/namespaces + auto-resolve type-only
    // imports). No tsx/ts-node install required.
    ['--experimental-transform-types', '--input-type=module-typescript', '-e', source],
    {
      timeout: CHILD_TIMEOUT_MS,
      maxBuffer: 4 * 1024 * 1024,
      env: {
        ...process.env,
        // Suppress the ExperimentalWarning noise on stderr; the sentinel
        // protocol only inspects stdout, but a clean stderr keeps the
        // vitest output readable when this test fails.
        NODE_NO_WARNINGS: '1',
      },
    },
  );

  const okHit = stdout.includes(CHILD_REMUX_OK_SENTINEL);
  const domainHit = stdout.includes(CHILD_REMUX_DOMAIN_ERROR_SENTINEL);
  const swIncompatHit = stdout.includes(CHILD_REMUX_SW_INCOMPAT_SENTINEL);

  // Exactly one outcome sentinel should appear. SW-incompat takes
  // precedence in the (impossible) event of multiple — it's the failure
  // mode we want to surface.
  if (swIncompatHit) {
    const nameMatch = stdout.match(/^NAME:(.*)$/m);
    const msgMatch = stdout.match(/^MSG:(.*)$/m);
    const stkMatch = stdout.match(/^STK:(.*)$/m);
    return {
      outcome: 'sw_incompat',
      errorName: nameMatch?.[1] ?? '(no name)',
      errorMessage: msgMatch?.[1] ?? '(no message)',
      errorStack: stkMatch?.[1] ?? '(no stack)',
    };
  }

  if (domainHit) {
    const nameMatch = stdout.match(/^NAME:(.*)$/m);
    const msgMatch = stdout.match(/^MSG:(.*)$/m);
    const stkMatch = stdout.match(/^STK:(.*)$/m);
    return {
      outcome: 'domain_error',
      errorName: nameMatch?.[1] ?? '(no name)',
      errorMessage: msgMatch?.[1] ?? '(no message)',
      errorStack: stkMatch?.[1] ?? '(no stack)',
    };
  }

  if (okHit) {
    return { outcome: 'ok', errorName: '', errorMessage: '', errorStack: '' };
  }

  throw new Error(
    `Child produced no outcome sentinel. stdout was: ${JSON.stringify(stdout)}`,
  );
}

describe('SW bundle loadability (Tier-1 gate — closes the 01-08 orchestrator gap)', () => {
  // Resolved at module-level (BEFORE the spawn) so `process.cwd()` and
  // `process.execPath` are still available.
  const swChunkUrl = resolveBuiltSwChunkUrl();
  const remuxSourceUrl = pathToFileURL(WEBM_REMUX_SOURCE_PATH).href;

  it('built SW chunk imports without throwing under SW-simulated globals', async () => {
    const result = await runSwBundleImportInChild(swChunkUrl);

    expect(
      result.ok,
      result.ok
        ? 'unreachable'
        : `Built SW bundle throws at top-level module init under ` +
            `SW-simulated globals (${SW_BUNDLE_STRIP_GLOBALS.join(', ')} ` +
            `stripped). This is exactly what kills the SW in Chrome and ` +
            `makes chrome://extensions "service worker" inaccessible.\n\n` +
            `First line of stack: ${result.errorStackFirstLine}\n` +
            `Full message: ${result.errorMessage}\n\n` +
            `Bundle URL: ${swChunkUrl}`,
    ).toBe(true);
  });

  it('source remuxSegments invocation does not throw an SW-incompatible error', async () => {
    if (!existsSync(WEBM_REMUX_SOURCE_PATH)) {
      throw new Error(
        `webm-remux source not found at ${WEBM_REMUX_SOURCE_PATH}. ` +
          `The Tier-1 gate's runtime layer requires the SOURCE file ` +
          `(not just the built bundle).`,
      );
    }

    const result = await runRemuxRuntimeInChild(remuxSourceUrl);

    // Domain errors (invalid-EBML parse, missing-Tracks, etc.) on our
    // intentionally-synthetic input are ACCEPTABLE — they prove the
    // runtime path is structurally reachable. Success is also fine.
    // Only `sw_incompat` (process/CSP/eval — or a Buffer error, which
    // would mean the polyfill itself regressed) is a real failure.
    expect(
      result.outcome !== 'sw_incompat',
      result.outcome === 'sw_incompat'
        ? `remuxSegments throws an SW-INCOMPATIBLE error when invoked ` +
            `under SW-simulated globals. This means the SW would crash ` +
            `mid-archive-save in real Chrome — exactly the kind of bug ` +
            `Layer 1 (module-init only) cannot catch.\n\n` +
            `Error name:    ${result.errorName}\n` +
            `Error message: ${result.errorMessage}\n` +
            `Stack (first 5 frames, ' | '-separated):\n  ${result.errorStack}\n\n` +
            `Source URL: ${remuxSourceUrl}`
        : 'unreachable',
    ).toBe(true);
  });
});
