// tests/offscreen/webm-playback.test.ts
//
// RED-gate test for debug session webm-playback-freeze.
//
// Empirically proves the playback freeze observed in
// tests/fixtures/last_30sec.webm (Phase 1, Plan 01-07 smoke retest after the
// D-12 base64-transfer fix landed at commit bf07619).
//
// Hypothesis under test (per .planning/debug/webm-playback-freeze.md):
//
//   The single-continuous MediaRecorder + 30 s age-trim approach (D-09..D-11)
//   drops VP9 P-frames' keyframe references when the buffer trims out the
//   middle of the recording. VP9's `kf_max_dist=100` (Chrome default) puts
//   keyframes every ~3-5 s. With chunks emitted every 2 s (D-09 timeslice),
//   the boundary chunks contain only P-frames referencing keyframes that have
//   been evicted. The decoder therefore fails ~1 s into playback in Chrome,
//   and `ffmpeg -v warning -i <fixture> -f null -` emits multiple
//   "Error submitting packet to decoder: Invalid data found" lines plus a
//   "File ended prematurely" tail-error.
//
// This test runs ffmpeg's CLI (an external dependency — /usr/bin/ffmpeg)
// over the COMMITTED fixture and asserts:
//   * zero "Error submitting packet to decoder" lines, AND
//   * no "File ended prematurely" line.
//
// Today (commit bf07619) the test goes RED because the fixture was produced
// by the single-continuous-recorder path. The D-13 fix (restart-segments,
// activate the pre-staged skeleton in src/offscreen/recorder.ts) will produce
// a fresh fixture whose decode is clean — at which point this test flips
// GREEN. See `tests/offscreen/segment-keyframes.test.ts` for the unit-level
// algorithmic guard that does NOT require regenerating the fixture.
//
// Skip discipline: if ffmpeg is missing from the environment the test
// auto-skips rather than failing. CI ships ffmpeg per `smoke.sh` so this is
// a developer-convenience fence, not a behavioural softening.

import { describe, it, expect } from 'vitest';
import { existsSync, statSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';

const here = dirname(fileURLToPath(import.meta.url));
const FIXTURE_PATH = resolve(here, '..', 'fixtures', 'last_30sec.webm');
const FFMPEG_BIN = '/usr/bin/ffmpeg';

// Cap: a clean 30-second WebM decoded with `-f null` finishes well under
// 10 s on commodity hardware. If we ever exceed this we want a hard failure,
// not a hung CI job.
const FFMPEG_TIMEOUT_MS = 30_000;

function ffmpegAvailable(): boolean {
  try {
    return existsSync(FFMPEG_BIN) && statSync(FFMPEG_BIN).isFile();
  } catch {
    return false;
  }
}

interface DecodeResult {
  stderr: string;
  packetErrorCount: number;
  endedPrematurely: boolean;
}

/**
 * Run ffmpeg in `-f null` mode to dry-decode a WebM fixture without writing
 * any output. Returns the captured stderr plus parsed counters for the two
 * signals we care about: per-packet decoder errors and the
 * "File ended prematurely" tail-error.
 *
 * Why spawnSync (and not execFileSync):
 *   execFileSync returns ONLY stdout — we cannot read the stderr pipe on the
 *   success path. ffmpeg exits 0 even when it emitted per-packet decode
 *   errors (with `-f null -`), so the diagnostic signal lives on stderr
 *   regardless of exit code. spawnSync exposes both pipes uniformly.
 *
 * The IN-04 fix retired the parallel `decodeDryRun(execFileSync)` helper —
 * the spawnSync path was always the actual code path used by the assertions
 * below; the execFile variant existed only as a documentation foil and
 * required a `void decodeDryRun` noUnusedLocals appeasement hack.
 *
 * Flags:
 *   -nostdin    — never block on a TTY (vitest doesn't provide one)
 *   -v warning  — drop the noise floor; signals we care about are emitted
 *                 at warning level or above
 *   -i <fix>    — input file
 *   -f null -   — swallow decoded output; stderr still carries diagnostics
 *
 * @param fixturePath - Absolute path to the WebM file under test.
 * @returns DecodeResult with `stderr`, `packetErrorCount`, `endedPrematurely`.
 * @throws If ffmpeg was killed by a signal (not a clean exit).
 */
function decodeDryRunStrict(fixturePath: string): DecodeResult {
  const proc = spawnSync(
    FFMPEG_BIN,
    ['-nostdin', '-v', 'warning', '-i', fixturePath, '-f', 'null', '-'],
    {
      stdio: ['ignore', 'ignore', 'pipe'],
      encoding: 'utf-8',
      timeout: FFMPEG_TIMEOUT_MS,
      maxBuffer: 4 * 1024 * 1024,
    },
  );
  if (proc.signal !== null) {
    throw new Error(`ffmpeg was killed by signal ${proc.signal}`);
  }
  const stderr = proc.stderr ?? '';
  return {
    stderr,
    packetErrorCount: (stderr.match(/Error submitting packet to decoder/g) ?? []).length,
    endedPrematurely: /File ended prematurely/.test(stderr),
  };
}

describe('webm playback (RED — confirms webm-playback-freeze bug)', () => {
  it.skipIf(!ffmpegAvailable())(
    'ffmpeg dry-run on last_30sec.webm produces zero decoder packet errors',
    () => {
      expect(existsSync(FIXTURE_PATH)).toBe(true);
      const result = decodeDryRunStrict(FIXTURE_PATH);
      // Document the failure in the assertion message so a regression
      // bisect lands on a useful diff, not just "expected 0 received N".
      expect(
        result.packetErrorCount,
        `ffmpeg reported ${result.packetErrorCount} "Error submitting packet to decoder" line(s). ` +
          `This means the VP9 decoder hit P-frames whose reference keyframe was missing from the ` +
          `stream — the symptom of the single-continuous-recorder + 30 s age-trim approach (D-09..D-11). ` +
          `Fix: activate the D-13 restart-segments skeleton at src/offscreen/recorder.ts:298-316 and ` +
          `regenerate the fixture via ./smoke.sh. Full ffmpeg stderr:\n${result.stderr}`,
      ).toBe(0);
    },
  );

  it.skipIf(!ffmpegAvailable())(
    'ffmpeg dry-run on last_30sec.webm does not end prematurely',
    () => {
      expect(existsSync(FIXTURE_PATH)).toBe(true);
      const result = decodeDryRunStrict(FIXTURE_PATH);
      // The "File ended prematurely" line indicates the WebM lacks proper
      // Matroska SegmentSize / Cues finalization because the SW reads the
      // in-memory buffer while the MediaRecorder is still active (no .stop()).
      // The D-13 restart-segments approach fixes this as a side effect —
      // each rotated segment gets a proper .stop() and is therefore finalized.
      expect(
        result.endedPrematurely,
        `ffmpeg reported "File ended prematurely". The WebM container was read mid-stream ` +
          `without calling MediaRecorder.stop(), so SegmentSize/Cues are unwritten. The D-13 ` +
          `restart-segments fix finalizes each segment naturally. Full ffmpeg stderr:\n${result.stderr}`,
      ).toBe(false);
    },
  );
});
