// tests/offscreen/segment-rotation.test.ts
//
// Replaces tests/offscreen/ring-buffer.test.ts (retired). Pins the D-13
// restart-segments invariants against the production recorder module.
//
// What this exercises (per .planning/debug/webm-playback-freeze.md
// "Activation Plan" + RESEARCH.md Pattern 3 + CONTEXT.md D-13):
//   1. `getSegments()` is the canonical buffer accessor (Blob[]); the
//      legacy chunk-level `getBuffer()` API is no longer present.
//   2. The segment ring caps at MAX_SEGMENTS = 3 — the 4th push evicts
//      the oldest, preserving the most-recent 3 segments (= 30 s window
//      at SEGMENT_DURATION_MS = 10_000 ms).
//   3. `resetBuffer()` clears the segment array — used on
//      track-ended ("user stopped sharing") cleanup and between tests.
//   4. Each retained entry is a real Blob with `type === 'video/webm'`
//      so the SW's `mergeVideoSegments` concatenation produces a valid
//      multi-EBML-header WebM payload (Chrome handles this natively;
//      see Pattern 3 trade-off discussion).
//
// The test seam `pushSegmentForTest(blob)` drives rotation
// deterministically — production code reaches the same code path via
// the MediaRecorder.onstop handler, but instantiating a real
// MediaRecorder inside vitest is not portable (jsdom lacks the
// underlying capture pipeline). The seam name encodes its purpose.

import { beforeEach, describe, expect, it } from 'vitest';
import {
  getSegments,
  pushSegmentForTest,
  resetBuffer,
  MAX_SEGMENTS,
  SEGMENT_DURATION_MS,
  VIDEO_BUFFER_DURATION_MS,
} from '../../src/offscreen/recorder';

function makeSegmentBlob(seedByte: number): Blob {
  // A 4-byte placeholder is enough — these tests assert on length /
  // ordering / type, not on decodability. The real ffmpeg-driven
  // contract lives in webm-playback.test.ts and runs against the
  // committed fixture.
  return new Blob([new Uint8Array([seedByte, 0x00, 0x00, 0x00])], {
    type: 'video/webm',
  });
}

describe('segment rotation (D-13 restart-segments)', () => {
  beforeEach(() => {
    resetBuffer();
  });

  it('exposes the D-13 constants — 3 segments × 10 s = 30 s window', () => {
    expect(MAX_SEGMENTS).toBe(3);
    expect(SEGMENT_DURATION_MS).toBe(10_000);
    // The product matches the legacy VIDEO_BUFFER_DURATION_MS so the
    // operator-facing "last 30 seconds" promise stays intact.
    expect(MAX_SEGMENTS * SEGMENT_DURATION_MS).toBe(VIDEO_BUFFER_DURATION_MS);
  });

  it('starts empty before any segment is pushed', () => {
    expect(getSegments()).toEqual([]);
  });

  it('first push appends one segment', () => {
    pushSegmentForTest(makeSegmentBlob(0x1a));
    const segments = getSegments();
    expect(segments).toHaveLength(1);
    expect(segments[0]).toBeInstanceOf(Blob);
    expect(segments[0].type).toBe('video/webm');
  });

  it('preserves segments in push order (oldest first)', () => {
    pushSegmentForTest(makeSegmentBlob(0xaa));
    pushSegmentForTest(makeSegmentBlob(0xbb));
    pushSegmentForTest(makeSegmentBlob(0xcc));
    const segments = getSegments();
    expect(segments).toHaveLength(3);
    // Read out the seed byte from each blob to confirm order.
    return Promise.all(segments.map((s) => s.arrayBuffer())).then((bufs) => {
      const firstBytes = bufs.map((b) => new Uint8Array(b)[0]);
      expect(firstBytes).toEqual([0xaa, 0xbb, 0xcc]);
    });
  });

  it('caps at MAX_SEGMENTS — 4th push evicts the oldest', () => {
    pushSegmentForTest(makeSegmentBlob(0x11));
    pushSegmentForTest(makeSegmentBlob(0x22));
    pushSegmentForTest(makeSegmentBlob(0x33));
    pushSegmentForTest(makeSegmentBlob(0x44));
    const segments = getSegments();
    expect(segments).toHaveLength(MAX_SEGMENTS);
    return Promise.all(segments.map((s) => s.arrayBuffer())).then((bufs) => {
      const firstBytes = bufs.map((b) => new Uint8Array(b)[0]);
      // Oldest (0x11) is gone; the kept window is the 3 most-recent.
      expect(firstBytes).toEqual([0x22, 0x33, 0x44]);
    });
  });

  it('many pushes never exceed the cap', () => {
    for (let i = 0; i < 20; i++) {
      pushSegmentForTest(makeSegmentBlob(i));
    }
    expect(getSegments().length).toBeLessThanOrEqual(MAX_SEGMENTS);
    expect(getSegments()).toHaveLength(MAX_SEGMENTS);
  });

  it('resetBuffer clears the segment array', () => {
    pushSegmentForTest(makeSegmentBlob(0x1));
    pushSegmentForTest(makeSegmentBlob(0x2));
    expect(getSegments()).toHaveLength(2);
    resetBuffer();
    expect(getSegments()).toEqual([]);
  });

  it('resetBuffer preserves segments do NOT survive (sweep #1 stop-race adjacent)', () => {
    // Sweep #1 documented that stopRecording() previously raced
    // onSegmentStopped: the timer was cleared but onSegmentStopped's
    // mediaStream != null branch would still spawn a new segment. The
    // fix nulls mediaStream FIRST. resetBuffer is the orthogonal
    // mechanism for clearing the buffer (used by onUserStoppedSharing
    // AND between tests). This test pins the contract that resetBuffer
    // never leaves a partial segment behind.
    pushSegmentForTest(makeSegmentBlob(0xa));
    pushSegmentForTest(makeSegmentBlob(0xb));
    pushSegmentForTest(makeSegmentBlob(0xc));
    expect(getSegments()).toHaveLength(MAX_SEGMENTS);
    resetBuffer();
    expect(getSegments()).toHaveLength(0);
    // Subsequent pushes start from an empty buffer — no eviction needed.
    pushSegmentForTest(makeSegmentBlob(0xd));
    expect(getSegments()).toHaveLength(1);
  });

  it('getSegments returns a snapshot — mutating the result does not corrupt internal state', () => {
    pushSegmentForTest(makeSegmentBlob(0x1));
    pushSegmentForTest(makeSegmentBlob(0x2));
    const snapshot = getSegments();
    expect(snapshot).toHaveLength(2);
    // Caller-side mutation must not leak back into the recorder module
    // (defense against accidental aliasing in mergeVideoSegments).
    snapshot.pop();
    snapshot.push(makeSegmentBlob(0xff));
    expect(getSegments()).toHaveLength(2);
  });
});
