// tests/offscreen/port-serialization.test.ts
//
// RED-gate test for debug session d12-blob-port-transfer-fails.
//
// Empirically proves the failure mode behind the 75-byte "[object Object]"
// WebM payload observed in /home/parf/Downloads/session_report_2026-05-15_19-42-01.zip.
//
// Hypothesis under test (per .planning/debug/d12-blob-port-transfer-fails.md):
//   1. chrome.runtime.Port.postMessage serializes payloads as JSON across
//      extension contexts (offscreen ↔ SW). JSON.stringify(blob) === "{}"
//      because Blob has no enumerable own properties.
//   2. The current REQUEST_BUFFER handler in src/offscreen/recorder.ts:174-178
//      sends `{ type: 'BUFFER', chunks: getBuffer() }` where each chunk has
//      `.data: Blob`. After the JSON round-trip the SW receives chunks with
//      `.data === {}`.
//   3. mergeVideoChunks in src/background/index.ts:204-222 then calls
//      `new Blob([{}, {}, ...])`. The Blob constructor coerces each non-Blob
//      member via String(member) → "[object Object]", concatenated with no
//      separator → exactly 75 bytes for 5 chunks (5 × 15 = 75).
//
// Goal: make these tests RED first (proving the bug is real), then write the
// fix (Blob → base64 string transfer-format) to flip them GREEN.

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

// Simulates Chrome's documented chrome.runtime.Port serialization across
// extension contexts: JSON round-trip with no structured-clone fallback.
// Ref: https://developer.chrome.com/docs/extensions/develop/concepts/messaging
function chromeRuntimePortRoundTrip<T>(message: T): unknown {
  return JSON.parse(JSON.stringify(message));
}

describe('port serialization (RED — confirms d12 bug)', () => {
  it('JSON.stringify(blob) drops binary content (root cause part 1)', () => {
    const blob = new Blob([new Uint8Array([0x1a, 0x45, 0xdf, 0xa3])], {
      type: 'video/webm',
    });
    // EBML magic = 0x1A 0x45 0xDF 0xA3 — the bytes the real WebM should start with.
    expect(blob.size).toBe(4);

    const serialized = JSON.stringify(blob);
    // The smoking gun: Blob has no enumerable own properties, so JSON drops it.
    expect(serialized).toBe('{}');
  });

  it('Port round-trip strips Blob from chunks (reproduces the SW receive-side)', () => {
    const blob = new Blob([new Uint8Array([0x1a, 0x45, 0xdf, 0xa3])], {
      type: 'video/webm',
    });
    const portMessage = {
      type: 'BUFFER',
      chunks: [
        { data: blob, timestamp: 1, isFirst: true },
        { data: blob, timestamp: 2 },
      ],
    };

    const received = chromeRuntimePortRoundTrip(portMessage) as {
      type: string;
      chunks: Array<{ data: unknown; timestamp: number; isFirst?: boolean }>;
    };

    expect(received.type).toBe('BUFFER');
    expect(received.chunks).toHaveLength(2);
    // The bug: data is no longer a Blob — it's a plain empty object.
    expect(received.chunks[0].data).not.toBeInstanceOf(Blob);
    expect(received.chunks[0].data).toEqual({});
    expect(received.chunks[1].data).toEqual({});
    // Metadata survives because timestamp/isFirst are JSON-friendly.
    expect(received.chunks[0].timestamp).toBe(1);
    expect(received.chunks[0].isFirst).toBe(true);
  });

  it('new Blob([{}, {}, ...]) coerces each member to "[object Object]" (root cause part 2)', () => {
    // This is the smoking gun for the merge step. Once the SW gets {} for each
    // chunk.data, mergeVideoChunks does `new Blob(blobs, { type: 'video/webm' })`
    // and the Blob ctor stringifies non-Blob members.
    const merged = new Blob([{}, {}, {}, {}, {}], { type: 'video/webm' });
    expect(merged.size).toBe(75); // 5 × 15 = 75 — matches the observed payload!
    return merged.text().then((text) => {
      expect(text).toBe('[object Object][object Object][object Object][object Object][object Object]');
    });
  });

  it('end-to-end: simulate full failure path — Blob → port → merge → 75-byte garbage', async () => {
    // Build a fake recorder buffer of 5 real WebM-ish blobs.
    const realChunks = [0x1a, 0x45, 0xdf, 0xa3, 0xa3].map((firstByte, i) => ({
      data: new Blob([new Uint8Array([firstByte, 0x00, 0x00, 0x00])], {
        type: 'video/webm',
      }),
      timestamp: 1000 + i,
      isFirst: i === 0,
    }));

    // Send through the simulated port.
    const message = { type: 'BUFFER', chunks: realChunks };
    const received = chromeRuntimePortRoundTrip(message) as {
      chunks: Array<{ data: unknown; timestamp: number }>;
    };

    // SW-side: mergeVideoChunks does this exact operation.
    const blobs = received.chunks
      .sort((a, b) => a.timestamp - b.timestamp)
      .map((c) => c.data as Blob);
    const merged = new Blob(blobs, { type: 'video/webm' });

    // The observed failure: 75 bytes of "[object Object]" repeated.
    expect(merged.size).toBe(75);
    const text = await merged.text();
    expect(text).toBe(
      '[object Object][object Object][object Object][object Object][object Object]'
    );
    // The original 5 × 4 = 20 bytes of WebM data are GONE.
    expect(merged.size).not.toBe(20);
  });
});

describe('port serialization (GREEN — pins the eventual fix contract)', () => {
  // These tests are forward-pinning: they describe the wire-format the fix
  // MUST implement. They will run today and PASS — they test pure helpers, not
  // the (still-buggy) recorder.ts handler.
  //
  // The fix must:
  //   1. Define a TransferredVideoChunk wire-format that is JSON-friendly:
  //        { dataBase64: string; type: string; timestamp: number; isFirst?: boolean }
  //   2. Convert Blob → base64 in offscreen before postMessage.
  //   3. Convert base64 → Blob in SW after receive.
  //
  // Until the fix lands, these helpers don't exist in src/. These tests live
  // here as a contract the fix's reviewer can check against.

  async function blobToBase64(blob: Blob): Promise<string> {
    const buf = await blob.arrayBuffer();
    const bytes = new Uint8Array(buf);
    // Node + browser both support Buffer / btoa. Use a portable conversion.
    let binary = '';
    for (let i = 0; i < bytes.length; i++) {
      binary += String.fromCharCode(bytes[i]);
    }
    // btoa exists in vitest's node env via globalThis polyfill in modern Node.
    return typeof btoa === 'function'
      ? btoa(binary)
      : Buffer.from(binary, 'binary').toString('base64');
  }

  function base64ToBlob(b64: string, type: string): Blob {
    const binary =
      typeof atob === 'function'
        ? atob(b64)
        : Buffer.from(b64, 'base64').toString('binary');
    const bytes = new Uint8Array(binary.length);
    for (let i = 0; i < binary.length; i++) {
      bytes[i] = binary.charCodeAt(i);
    }
    return new Blob([bytes], { type });
  }

  it('base64 round-trip preserves binary content across JSON serialization', async () => {
    const original = new Blob([new Uint8Array([0x1a, 0x45, 0xdf, 0xa3])], {
      type: 'video/webm',
    });
    const b64 = await blobToBase64(original);
    expect(b64).toBe('GkXfow=='); // base64 of EBML magic
    // JSON round-trip preserves the string.
    const wire = { dataBase64: b64, type: 'video/webm' };
    const received = JSON.parse(JSON.stringify(wire)) as {
      dataBase64: string;
      type: string;
    };
    const restored = base64ToBlob(received.dataBase64, received.type);
    expect(restored.size).toBe(4);
    const text = await restored.text();
    // EBML magic ≈ "Eß£" — bytes preserved.
    expect(new Uint8Array(await restored.arrayBuffer())).toEqual(
      new Uint8Array([0x1a, 0x45, 0xdf, 0xa3])
    );
  });

  it('merging base64-decoded chunks produces a real WebM-prefixed blob', async () => {
    const original = new Blob([new Uint8Array([0x1a, 0x45, 0xdf, 0xa3])], {
      type: 'video/webm',
    });
    const b64 = await blobToBase64(original);
    const wireChunks = [
      { dataBase64: b64, type: 'video/webm', timestamp: 1, isFirst: true },
    ];
    const received = JSON.parse(JSON.stringify({ chunks: wireChunks })) as {
      chunks: Array<{ dataBase64: string; type: string; timestamp: number }>;
    };
    const blobs = received.chunks.map((c) => base64ToBlob(c.dataBase64, c.type));
    const merged = new Blob(blobs, { type: 'video/webm' });
    expect(merged.size).toBe(4);
    expect(merged.size).not.toBe(75); // the bug is gone in the fixed form.
  });
});

describe('binary.ts adversarial input (WR-07 + sweep target #6)', () => {
  // These tests exercise the PRODUCTION helpers in src/shared/binary.ts,
  // not local re-implementations. They pin the WR-07 + sweep-#6
  // defensive contracts:
  //   1. Empty-string input → zero-byte Blob (early-return shortcut,
  //      no atob('') round-trip).
  //   2. Invalid base64 input → throws DOMException-like InvalidCharacterError,
  //      caller is expected to wrap in try/catch.
  //
  // The empty-segment filter in SW's getVideoBufferFromOffscreen relies
  // on (1) so the multi-EBML-header concat output never includes a stray
  // zero-byte segment. The error-classification step relies on (2) being
  // a regular throw so the per-segment try/catch can drop a single bad
  // entry without aborting the whole transfer.

  it('base64ToBlob("") returns a zero-byte Blob with the requested MIME', async () => {
    const { base64ToBlob } = await import('../../src/shared/binary');
    const blob = base64ToBlob('', 'video/webm');
    expect(blob).toBeInstanceOf(Blob);
    expect(blob.size).toBe(0);
    expect(blob.type).toBe('video/webm');
  });

  it('blobToBase64(emptyBlob) round-trips through base64ToBlob to zero bytes', async () => {
    const { blobToBase64, base64ToBlob } = await import('../../src/shared/binary');
    const original = new Blob([], { type: 'video/webm' });
    const b64 = await blobToBase64(original);
    expect(b64).toBe('');
    const restored = base64ToBlob(b64, 'video/webm');
    expect(restored.size).toBe(0);
  });

  it('base64ToBlob throws on invalid base64 alphabet (caller wraps with try/catch)', async () => {
    const { base64ToBlob } = await import('../../src/shared/binary');
    // Characters outside the base64 alphabet — atob throws InvalidCharacterError
    // synchronously. The SW caller (getVideoBufferFromOffscreen) has a
    // try/catch that drops a single bad segment without aborting the batch.
    expect(() => base64ToBlob('!!!not-base64!!!', 'video/webm')).toThrow();
  });
});
