import { spawnSync } from 'node:child_process';
import {
  copyFileSync,
  existsSync,
  mkdtempSync,
  mkdirSync,
  readFileSync,
  rmSync,
  statSync,
  writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { basename, extname, join, resolve } from 'node:path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import {
  buildAudioExtractionPlan,
  buildChapterMuxPlan,
  buildContactSheetPlan,
  buildConvertPlan,
  buildFadePlan,
  buildFastConcatPlan,
  buildLoudnessAnalysisPlan,
  buildLoudnessApplyPlan,
  buildMetadataEditPlan,
  buildNormalizedConcatPlan,
  buildPeakAnalysisPlan,
  buildPeakNormalizationFilter,
  buildPeakNormalizationPlan,
  buildPreviewProxyPlan,
  buildRemuxPlan,
  buildSceneChangeAnalysisPlan,
  buildSceneThumbnailSeriesPlan,
  buildSingleThumbnailPlan,
  buildSplitPlan,
  buildStaticWaveformPlan,
  buildStreamRemovalPlan,
  buildSubtitleBurnPlan,
  buildSubtitleConversionPlan,
  buildSubtitleExtractionPlan,
  buildSubtitleMuxPlan,
  buildThumbnailSeriesPlan,
  buildTimelineExportPlan,
  buildTransformPlan,
  buildTrimPlan,
  buildWaveformAnalysisPlan,
  createConcatListEntries,
  parseLoudnessMeasurement,
  parsePeakMeasurement,
  parseSceneChangeFrames,
  resolveThumbnailTimes,
  streamMapArguments,
  type ConcatSource,
  type FFmpegCommandPlan,
  type SourceReference,
} from '../../src/commands';
import { findBuiltInPreset } from '../../src/presets';

const FIXTURES = resolve(process.cwd(), 'tests/fixtures/generated');
const NATIVE_FFMPEG_AVAILABLE =
  spawnSync('ffmpeg', ['-version'], { stdio: 'ignore' }).status === 0 &&
  spawnSync('ffprobe', ['-version'], { stdio: 'ignore' }).status === 0;
const NATIVE_SUBTITLES_FILTER_AVAILABLE =
  NATIVE_FFMPEG_AVAILABLE &&
  /\bsubtitles\b/u.test(
    spawnSync('ffmpeg', ['-hide_banner', '-filters'], {
      encoding: 'utf8',
    }).stdout ?? ''
  );
const NATIVE_DRAWTEXT_FILTER_AVAILABLE =
  NATIVE_FFMPEG_AVAILABLE &&
  /\bdrawtext\b/u.test(
    spawnSync('ffmpeg', ['-hide_banner', '-filters'], {
      encoding: 'utf8',
    }).stdout ?? ''
  );
const describeNative = NATIVE_FFMPEG_AVAILABLE ? describe : describe.skip;
const itNativeSubtitles = NATIVE_SUBTITLES_FILTER_AVAILABLE ? it : it.skip;
const itNativeDrawtext = NATIVE_DRAWTEXT_FILTER_AVAILABLE ? it : it.skip;

interface NativeExecution {
  readonly outputPaths: readonly string[];
  readonly stdout: string;
  readonly stderr: string;
}

let suiteDirectory = '';
let executionSequence = 0;

beforeAll(() => {
  suiteDirectory = mkdtempSync(join(tmpdir(), 'av-tools-command-runtime-'));
});

afterAll(() => {
  if (suiteDirectory) {
    rmSync(suiteDirectory, { recursive: true, force: true });
  }
});

function fixture(name: string): string {
  const path = join(FIXTURES, name);
  if (!existsSync(path)) {
    throw new Error(`Missing generated fixture: ${name}`);
  }
  return path;
}

function source(
  id: string,
  sourceIndex: number,
  fileName: string
): SourceReference {
  return { id, sourceIndex, fileName };
}

function runNative(
  plan: FFmpegCommandPlan,
  sourceFiles: Readonly<Record<string, string>>,
  logLevel: 'warning' | 'info' = 'warning'
): NativeExecution {
  executionSequence += 1;
  const directory = join(
    suiteDirectory,
    `${String(executionSequence).padStart(3, '0')}-${plan.operation}`
  );
  mkdirSync(directory, { recursive: true });

  const replacements = new Map<string, string>();
  plan.inputs.forEach((input, index) => {
    const sourcePath = sourceFiles[input.id];
    if (!sourcePath) {
      throw new Error(`No fixture was supplied for planned input ${input.id}`);
    }
    const extension = extname(sourcePath) || '.bin';
    const inputPath = join(
      directory,
      `input-${String(index).padStart(3, '0')}${extension}`
    );
    copyFileSync(sourcePath, inputPath);
    replacements.set(input.path, inputPath);
  });
  const outputPaths = plan.outputs.map((output, index) => {
    const outputPath = join(
      directory,
      `${String(index).padStart(3, '0')}-${output.fileName}`
    );
    replacements.set(output.path, outputPath);
    return outputPath;
  });
  const temporaryPaths = plan.temporaryFiles.map((temporary, index) => {
    const extension = extname(temporary.path) || '.tmp';
    const temporaryPath = join(
      directory,
      `temporary-${String(index).padStart(3, '0')}${extension}`
    );
    replacements.set(temporary.path, temporaryPath);
    return temporaryPath;
  });
  const substitute = (value: string): string => {
    let result = value;
    for (const [virtualPath, nativePath] of [...replacements.entries()].sort(
      ([left], [right]) => right.length - left.length
    )) {
      result = result.replaceAll(virtualPath, nativePath);
    }
    return result;
  };
  plan.temporaryFiles.forEach((temporary, index) => {
    const path = temporaryPaths[index];
    if (!path) {
      throw new Error(`Missing native temporary path ${index}`);
    }
    if (typeof temporary.content === 'string') {
      writeFileSync(path, substitute(temporary.content), 'utf8');
    } else {
      writeFileSync(path, temporary.content);
    }
  });

  const execution = spawnSync(
    'ffmpeg',
    [
      '-hide_banner',
      '-nostdin',
      '-y',
      '-loglevel',
      logLevel,
      ...plan.args.map(substitute),
    ],
    {
      encoding: 'utf8',
      maxBuffer: 20 * 1024 * 1024,
      timeout: 60_000,
    }
  );
  const stdout = execution.stdout ?? '';
  const stderr = execution.stderr ?? '';
  if (execution.status !== 0) {
    throw new Error(
      [
        `Native FFmpeg rejected ${plan.operation} (status ${String(execution.status)}, signal ${String(execution.signal)}).`,
        `argv: ${plan.args.map(substitute).join(' ')}`,
        stderr,
        stdout,
      ].join('\n')
    );
  }
  for (const path of outputPaths) {
    expect(statSync(path).size, basename(path)).toBeGreaterThan(0);
  }
  return { outputPaths, stdout, stderr };
}

function nativeCommand(args: readonly string[], outputPath: string): void {
  const execution = spawnSync(
    'ffmpeg',
    ['-hide_banner', '-nostdin', '-y', '-loglevel', 'error', ...args],
    {
      encoding: 'utf8',
      maxBuffer: 10 * 1024 * 1024,
      timeout: 60_000,
    }
  );
  if (execution.status !== 0) {
    throw new Error(execution.stderr || `Could not create ${outputPath}`);
  }
  expect(statSync(outputPath).size).toBeGreaterThan(0);
}

function probe(path: string): {
  readonly streams?: readonly {
    readonly index?: number;
    readonly codec_name?: string;
    readonly codec_type?: string;
    readonly width?: number;
    readonly height?: number;
    readonly tags?: Readonly<Record<string, string>>;
    readonly disposition?: Readonly<Record<string, number>>;
  }[];
  readonly chapters?: readonly {
    readonly start_time?: string;
    readonly end_time?: string;
    readonly tags?: Readonly<Record<string, string>>;
  }[];
  readonly format?: {
    readonly duration?: string;
    readonly tags?: Readonly<Record<string, string>>;
  };
} {
  const result = spawnSync(
    'ffprobe',
    [
      '-v',
      'error',
      '-show_streams',
      '-show_chapters',
      '-show_format',
      '-of',
      'json',
      path,
    ],
    { encoding: 'utf8', timeout: 20_000 }
  );
  if (result.status !== 0) {
    throw new Error(result.stderr || `Could not probe ${path}`);
  }
  return JSON.parse(result.stdout) as ReturnType<typeof probe>;
}

function preset(id: string) {
  const result = findBuiltInPreset(id);
  if (!result) {
    throw new Error(`Missing built-in preset ${id}`);
  }
  return result;
}

function concatSource(
  id: string,
  sourceIndex: number,
  fileName: string,
  width: number,
  height: number,
  hasAudio = true
): ConcatSource {
  return {
    id,
    sourceIndex,
    fileName,
    durationSeconds: 1,
    streams: [
      {
        kind: 'video',
        codec: 'h264',
        width,
        height,
        pixelFormat: 'yuv420p',
        timeBase: '1/10240',
      },
      ...(hasAudio
        ? [
            {
              kind: 'audio' as const,
              codec: 'aac',
              sampleRate: 16_000,
              channelLayout: 'mono',
              timeBase: '1/16000',
            },
          ]
        : []),
    ],
  };
}

describe('command-plan runtime hardening', () => {
  it('serializes validated ffconcat in/out directives without source names', () => {
    expect(
      createConcatListEntries([
        {
          path: '/input/job-safe/source-0.mp4',
          inPointSeconds: 0.25,
          outPointSeconds: 0.75,
        },
      ])
    ).toBe(
      "file '/input/job-safe/source-0.mp4'\ninpoint 0.25\noutpoint 0.75\n"
    );
    expect(() =>
      createConcatListEntries([
        {
          path: '/input/job-safe/source-0.mp4',
          inPointSeconds: 1,
          outPointSeconds: 0.5,
        },
      ])
    ).toThrow(/outpoint/iu);
  });

  it('bounds and uniquely names split outputs and gives fast segments independent inputs', () => {
    const media = source('media', 0, 'pattern-av.mp4');
    const plan = buildSplitPlan({
      jobId: 'split-hardening',
      source: media,
      durationSeconds: 2,
      definition: {
        type: 'clips',
        clips: [
          { startSeconds: 0, endSeconds: 0.8, label: 'same' },
          { startSeconds: 1, endSeconds: 1.8, label: 'same' },
        ],
      },
      mode: 'fast',
      targetExtension: 'mp4',
    });
    expect(plan.outputs.map((output) => output.fileName)).toEqual([
      'pattern-av-split-001-same.mp4',
      'pattern-av-split-002-same.mp4',
    ]);
    expect(plan.args.filter((argument) => argument === '-i')).toHaveLength(2);
    expect(() =>
      buildSplitPlan({
        jobId: 'too-many-splits',
        source: media,
        durationSeconds: 100,
        definition: {
          type: 'equal-duration',
          segmentDurationSeconds: 0.1,
        },
        mode: 'fast',
        targetExtension: 'mp4',
      })
    ).toThrow(/at most 255/iu);
  });

  it('rejects container mismatches, ambiguous stream groups, and unsafe transform values', () => {
    expect(() =>
      buildMetadataEditPlan({
        jobId: 'muxer-mismatch',
        source: source('media', 0, 'pattern-av.mp4'),
        edits: {},
        targetExtension: 'mkv',
        targetMuxer: 'mp4',
      })
    ).toThrow(/does not match/iu);
    expect(() => streamMapArguments({ video: [0], audio: [0] })).toThrow(
      /more than one stream group/iu
    );
    expect(() =>
      buildTransformPlan({
        jobId: 'fractional-resize',
        source: source('media', 0, 'pattern-av.mp4'),
        preset: preset('mp4-h264-balanced'),
        settings: {
          resize: {
            sourceWidth: 160,
            sourceHeight: 90,
            width: 80.5,
            mode: 'fit',
          },
        },
      })
    ).toThrow(/integers/iu);
    expect(() =>
      buildTransformPlan({
        jobId: 'filter-injection',
        source: source('media', 0, 'pattern-av.mp4'),
        preset: preset('mp4-h264-balanced'),
        settings: {
          resize: {
            sourceWidth: 160,
            sourceHeight: 90,
            width: 80,
            height: 80,
            mode: 'contain',
            paddingColor: 'black,scale=1:1',
          },
        },
      })
    ).toThrow(/padding colour/iu);
  });

  it('requires explicit subtitle loss and bounds generated image counts', () => {
    const sources = [
      {
        ...concatSource('first', 0, 'compatible-a.mp4', 128, 72),
        streams: [
          ...concatSource('first', 0, 'compatible-a.mp4', 128, 72).streams,
          { kind: 'subtitle' as const, codec: 'mov_text', timeBase: '1/1000' },
        ],
      },
      concatSource('second', 1, 'compatible-b.mp4', 128, 72),
    ];
    const options = {
      jobId: 'subtitle-concat-policy',
      sources,
      targetExtension: 'mp4',
      targetMuxer: 'mp4',
      preset: preset('mp4-h264-balanced'),
      width: 128,
      height: 72,
      frameRate: 10,
      missingAudioPolicy: 'reject' as const,
    };
    expect(() => buildNormalizedConcatPlan(options)).toThrow(
      /cannot retain subtitle/iu
    );
    expect(
      buildNormalizedConcatPlan({
        ...options,
        subtitlePolicy: 'drop-all',
      }).diagnostics
    ).toContainEqual(expect.objectContaining({ code: 'dropped-subtitles' }));
    expect(() =>
      resolveThumbnailTimes({
        durationSeconds: 1000,
        intervalSeconds: 1,
      })
    ).toThrow(/at most 255/iu);
  });

  it('requires finite measured peaks and validates subtitle stream indexes', () => {
    expect(() => parsePeakMeasurement('max_volume: -inf dB')).toThrow(
      /silent audio/iu
    );
    expect(() =>
      buildPeakNormalizationFilter(-1, { inputPeakDbfs: -80 })
    ).toThrow(/more than 60 dB/iu);
    expect(() =>
      buildSubtitleExtractionPlan({
        jobId: 'invalid-subtitle-index',
        source: source('media', 0, 'pattern-av.mp4'),
        stream: { index: -1, codec: 'subrip' },
        outputFormat: 'srt',
      })
    ).toThrow(/stream index/iu);
  });

  it('makes the contact-sheet aspect toggle affect the typed filter graph', () => {
    const plan = buildContactSheetPlan({
      jobId: 'square-contact-cells',
      source: source('media', 0, 'pattern-av.mp4'),
      durationSeconds: 2,
      frameCount: 4,
      rows: 2,
      columns: 2,
      cellWidth: 80,
      maintainAspect: false,
      format: 'png',
    });
    expect(plan.args.find((argument) => argument.includes('tile='))).toContain(
      'scale=80:80'
    );
  });
});

describeNative('native execution of structured command plans', () => {
  it('executes conversion, remux, fast/accurate trim, transform, fades, and preview plans', () => {
    const media = source('media', 0, 'pattern-av.mp4');
    const mediaFile = fixture('pattern-av.mp4');
    const h264 = preset('mp4-h264-balanced');

    const converted = runNative(
      buildConvertPlan({
        jobId: 'convert-native',
        source: media,
        preset: h264,
        streamSelection: { video: [0], audio: [1] },
        expectedDurationSeconds: 2,
      }),
      { media: mediaFile }
    );
    expect(probe(converted.outputPaths[0]!).streams?.map(streamType)).toEqual([
      'video:h264',
      'audio:aac',
    ]);

    const gif = runNative(
      buildConvertPlan({
        jobId: 'gif-native',
        source: media,
        preset: preset('animated-gif'),
        streamSelection: { video: [0] },
      }),
      { media: mediaFile }
    );
    expect(probe(gif.outputPaths[0]!).streams?.[0]?.codec_name).toBe('gif');

    const attachedPictureGif = runNative(
      buildConvertPlan({
        jobId: 'gif-absolute-stream-native',
        source: source('cover-audio', 0, 'attached-cover.mp3'),
        preset: preset('animated-gif'),
        streamSelection: { video: [1] },
      }),
      { 'cover-audio': fixture('attached-cover.mp3') }
    );
    expect(
      probe(attachedPictureGif.outputPaths[0]!).streams?.[0]?.codec_name
    ).toBe('gif');

    const remuxed = runNative(
      buildRemuxPlan({
        jobId: 'remux-native',
        source: media,
        targetContainer: 'matroska',
        fileExtension: 'mkv',
        description: {
          streams: [
            { index: 0, kind: 'video', codec: 'h264' },
            { index: 1, kind: 'audio', codec: 'aac' },
          ],
        },
        expectedDurationSeconds: 2,
      }),
      { media: mediaFile }
    );
    expect(probe(remuxed.outputPaths[0]!).streams).toHaveLength(2);

    const fastTrim = runNative(
      buildTrimPlan({
        jobId: 'trim-fast-native',
        source: media,
        mode: 'fast',
        startSeconds: 0.4,
        endSeconds: 1.6,
        sourceDurationSeconds: 2,
        targetExtension: 'mp4',
      }),
      { media: mediaFile }
    );
    expect(
      Number(probe(fastTrim.outputPaths[0]!).format?.duration)
    ).toBeGreaterThan(0);

    const accurateTrim = runNative(
      buildTrimPlan({
        jobId: 'trim-accurate-native',
        source: media,
        mode: 'accurate',
        startSeconds: 0.4,
        endSeconds: 1.6,
        sourceDurationSeconds: 2,
        preset: h264,
      }),
      { media: mediaFile }
    );
    expect(
      Number(probe(accurateTrim.outputPaths[0]!).format?.duration)
    ).toBeGreaterThanOrEqual(1.15);
    expect(
      Number(probe(accurateTrim.outputPaths[0]!).format?.duration)
    ).toBeLessThanOrEqual(1.3);

    const transformed = runNative(
      buildTransformPlan({
        jobId: 'transform-native',
        source: media,
        preset: h264,
        expectedDurationSeconds: 2,
        settings: {
          crop: {
            x: 0,
            y: 0,
            width: 120,
            height: 80,
            sourceWidth: 160,
            sourceHeight: 90,
            requireEvenDimensions: true,
          },
          resize: {
            sourceWidth: 120,
            sourceHeight: 80,
            width: 96,
            height: 72,
            mode: 'contain',
          },
          rotation: 180,
          frameRate: 8,
        },
      }),
      { media: mediaFile }
    );
    expect(probe(transformed.outputPaths[0]!).streams?.[0]).toMatchObject({
      codec_type: 'video',
      width: 96,
      height: 72,
    });

    runNative(
      buildFadePlan({
        jobId: 'fades-native',
        source: media,
        preset: h264,
        durationSeconds: 2,
        audioFadeInSeconds: 0.2,
        audioFadeOutSeconds: 0.2,
        videoFadeInSeconds: 0.2,
        videoFadeOutSeconds: 0.2,
      }),
      { media: mediaFile }
    );

    const preview = runNative(
      buildPreviewProxyPlan({
        jobId: 'preview-native',
        source: media,
        sourceDurationSeconds: 2,
        startSeconds: 0.25,
        maxDurationSeconds: 1,
        maxWidth: 160,
      }),
      { media: mediaFile }
    );
    expect(
      Number(probe(preview.outputPaths[0]!).format?.duration)
    ).toBeLessThanOrEqual(1.1);

    const audioPreview = runNative(
      buildPreviewProxyPlan({
        jobId: 'audio-preview-native',
        source: source('tone', 0, 'tone.wav'),
        sourceDurationSeconds: 1,
        maxDurationSeconds: 0.5,
        hasVideo: false,
      }),
      { tone: fixture('tone.wav') }
    );
    expect(audioPreview.outputPaths[0]).toMatch(/\.m4a$/u);
    expect(probe(audioPreview.outputPaths[0]!).streams?.[0]).toMatchObject({
      codec_type: 'audio',
      codec_name: 'aac',
    });
  }, 120_000);

  it('executes fast and accurate multi-output split plans', () => {
    const media = source('media', 0, 'pattern-av.mp4');
    const mediaFile = fixture('pattern-av.mp4');
    for (const mode of ['fast', 'accurate'] as const) {
      const plan = buildSplitPlan({
        jobId: `split-${mode}-native`,
        source: media,
        durationSeconds: 2,
        definition: { type: 'markers', markers: [0.7, 1.3] },
        mode,
        targetExtension: 'mp4',
        ...(mode === 'accurate' ? { preset: preset('mp4-h264-balanced') } : {}),
      });
      const execution = runNative(plan, { media: mediaFile });
      expect(execution.outputPaths).toHaveLength(3);
      for (const path of execution.outputPaths) {
        expect(
          probe(path).streams?.some((stream) => stream.codec_type === 'video'),
          `${mode} split output ${basename(path)} must retain video`
        ).toBe(true);
      }
    }
  }, 120_000);

  it('executes fast and normalized concat, including generated silence', () => {
    const first = concatSource('first', 8, 'compatible-a.mp4', 128, 72);
    const second = concatSource('second', 3, 'compatible-b.mp4', 128, 72);
    const firstRange = {
      ...first,
      sourceInSeconds: 0.2,
      sourceOutSeconds: 0.8,
    };
    const secondRange = {
      ...second,
      sourceInSeconds: 0.1,
      sourceOutSeconds: 0.9,
    };
    const fastPlan = buildFastConcatPlan({
      jobId: 'fast-concat-native',
      sources: [firstRange, secondRange],
      targetExtension: 'mp4',
      targetMuxer: 'mp4',
    });
    expect(String(fastPlan.temporaryFiles[0]?.content)).toContain(
      'inpoint 0.2'
    );
    expect(fastPlan.expectedDurationSeconds).toBeCloseTo(1.4, 5);
    const fast = runNative(fastPlan, {
      first: fixture('compatible-a.mp4'),
      second: fixture('compatible-b.mp4'),
    });
    expect(
      Number(probe(fast.outputPaths[0]!).format?.duration)
    ).toBeGreaterThan(1.2);
    expect(Number(probe(fast.outputPaths[0]!).format?.duration)).toBeLessThan(
      2.1
    );

    const normalized = runNative(
      buildNormalizedConcatPlan({
        jobId: 'normalized-concat-native',
        sources: [
          concatSource('first', 0, 'compatible-a.mp4', 128, 72),
          {
            ...concatSource('different', 1, 'different-dimensions.mp4', 96, 96),
            sourceInSeconds: 0.2,
            sourceOutSeconds: 0.8,
          },
        ],
        targetExtension: 'mp4',
        targetMuxer: 'mp4',
        preset: preset('mp4-h264-balanced'),
        width: 128,
        height: 72,
        frameRate: 10,
        sampleRate: 16_000,
        channelLayout: 'mono',
        missingAudioPolicy: 'reject',
      }),
      {
        first: fixture('compatible-a.mp4'),
        different: fixture('different-dimensions.mp4'),
      }
    );
    expect(probe(normalized.outputPaths[0]!).streams?.[0]).toMatchObject({
      width: 128,
      height: 72,
    });
    expect(
      Number(probe(normalized.outputPaths[0]!).format?.duration)
    ).toBeCloseTo(1.6, 1);

    const crossfaded = runNative(
      buildNormalizedConcatPlan({
        jobId: 'crossfade-concat-native',
        sources: [
          concatSource('first', 0, 'compatible-a.mp4', 128, 72),
          concatSource('second', 1, 'compatible-b.mp4', 128, 72),
        ],
        targetExtension: 'mp4',
        targetMuxer: 'mp4',
        preset: preset('mp4-h264-balanced'),
        width: 128,
        height: 72,
        frameRate: 10,
        sampleRate: 16_000,
        channelLayout: 'mono',
        missingAudioPolicy: 'reject',
        crossfadeSeconds: 0.2,
      }),
      {
        first: fixture('compatible-a.mp4'),
        second: fixture('compatible-b.mp4'),
      }
    );
    expect(
      Number(probe(crossfaded.outputPaths[0]!).format?.duration)
    ).toBeCloseTo(1.8, 1);

    const videoOnly = join(suiteDirectory, 'compatible-video-only.mp4');
    nativeCommand(
      [
        '-i',
        fixture('compatible-b.mp4'),
        '-map',
        '0:v:0',
        '-c',
        'copy',
        videoOnly,
      ],
      videoOnly
    );
    const silence = runNative(
      buildNormalizedConcatPlan({
        jobId: 'silence-concat-native',
        sources: [
          concatSource('first', 0, 'compatible-a.mp4', 128, 72),
          concatSource(
            'video-only',
            1,
            'compatible-video-only.mp4',
            128,
            72,
            false
          ),
        ],
        targetExtension: 'mp4',
        targetMuxer: 'mp4',
        preset: preset('mp4-h264-balanced'),
        width: 128,
        height: 72,
        frameRate: 10,
        sampleRate: 16_000,
        channelLayout: 'mono',
        missingAudioPolicy: 'insert-silence',
        crossfadeSeconds: 0.2,
      }),
      { first: fixture('compatible-a.mp4'), 'video-only': videoOnly }
    );
    expect(
      probe(silence.outputPaths[0]!).streams?.some(
        (stream) => stream.codec_type === 'audio'
      )
    ).toBe(true);
  }, 120_000);

  it('executes audio extraction, waveform, loudness, and normalization plans', () => {
    const media = source('media', 0, 'pattern-av.mp4');
    const mediaFile = fixture('pattern-av.mp4');

    const copiedAudio = runNative(
      buildAudioExtractionPlan({
        jobId: 'audio-copy-native',
        source: media,
        audioStreamIndex: 1,
        mode: 'copy',
        inputCodec: 'aac',
        targetMuxer: 'ipod',
        targetExtension: 'm4a',
      }),
      { media: mediaFile }
    );
    expect(probe(copiedAudio.outputPaths[0]!).streams?.[0]?.codec_name).toBe(
      'aac'
    );

    const encodedAudio = runNative(
      buildAudioExtractionPlan({
        jobId: 'audio-encode-native',
        source: media,
        audioStreamIndex: 1,
        mode: 'encode',
        preset: preset('audio-wav-pcm'),
      }),
      { media: mediaFile }
    );
    expect(probe(encodedAudio.outputPaths[0]!).streams?.[0]?.codec_name).toBe(
      'pcm_s16le'
    );

    runNative(
      buildWaveformAnalysisPlan({
        jobId: 'waveform-data-native',
        source: media,
        audioStreamIndex: 1,
        sampleRate: 8_000,
      }),
      { media: mediaFile }
    );
    const staticWaveform = runNative(
      buildStaticWaveformPlan({
        jobId: 'waveform-static-native',
        source: media,
        audioStreamIndex: 1,
        width: 320,
        height: 80,
        color: '#ffffff',
        background: '#000000',
      }),
      { media: mediaFile }
    );
    expect(probe(staticWaveform.outputPaths[0]!).streams?.[0]).toMatchObject({
      codec_name: 'png',
      width: 320,
      height: 80,
    });

    const targets = {
      integratedLufs: -16,
      truePeakDbtp: -1,
      loudnessRangeLu: 11,
    };
    const analysis = runNative(
      buildLoudnessAnalysisPlan({
        jobId: 'loudness-analysis-native',
        source: media,
        targets,
        audioStreamIndex: 1,
      }),
      { media: mediaFile },
      'info'
    );
    const measurement = parseLoudnessMeasurement(
      `${analysis.stderr}\n${analysis.stdout}`
    );
    expect(measurement.inputIntegratedLufs).toBeTypeOf('number');

    runNative(
      buildLoudnessApplyPlan({
        jobId: 'loudness-apply-native',
        source: media,
        targets,
        audioStreamIndex: 1,
        measurement,
        preset: preset('audio-m4a-aac'),
      }),
      { media: mediaFile }
    );
    const peakAnalysis = runNative(
      buildPeakAnalysisPlan({
        jobId: 'peak-analysis-native',
        source: media,
        audioStreamIndex: 1,
        preset: preset('audio-m4a-aac'),
      }),
      { media: mediaFile },
      'info'
    );
    const peakMeasurement = parsePeakMeasurement(
      `${peakAnalysis.stderr}\n${peakAnalysis.stdout}`
    );
    expect(peakMeasurement.inputPeakDbfs).toBeLessThanOrEqual(0);
    const peakApplied = runNative(
      buildPeakNormalizationPlan({
        jobId: 'peak-native',
        source: media,
        audioStreamIndex: 1,
        targetPeakDb: -1,
        measurement: peakMeasurement,
        preset: preset('audio-m4a-aac'),
      }),
      { media: mediaFile }
    );
    const verifiedPeak = runNative(
      buildPeakAnalysisPlan({
        jobId: 'peak-verify-native',
        source: source('normalized', 0, 'peak-normalized.m4a'),
        audioStreamIndex: 0,
        preset: preset('audio-m4a-aac'),
      }),
      { normalized: peakApplied.outputPaths[0]! },
      'info'
    );
    const outputPeak = parsePeakMeasurement(
      `${verifiedPeak.stderr}\n${verifiedPeak.stdout}`
    ).inputPeakDbfs;
    expect(outputPeak).toBeGreaterThanOrEqual(-2);
    expect(outputPeak).toBeLessThanOrEqual(0);
  }, 120_000);

  it('executes stream removal, metadata editing, and chapter mux plans', () => {
    const media = source('media', 0, 'metadata.mp4');
    const mediaFile = fixture('metadata.mp4');
    const removed = runNative(
      buildStreamRemovalPlan({
        jobId: 'streams-native',
        source: media,
        selection: { audio: [1] },
        targetExtension: 'm4a',
        targetMuxer: 'ipod',
      }),
      { media: mediaFile }
    );
    expect(probe(removed.outputPaths[0]!).streams?.map(streamType)).toEqual([
      'audio:aac',
    ]);

    const metadataPlan = buildMetadataEditPlan({
      jobId: 'metadata-native',
      source: media,
      edits: {
        format: { title: 'Runtime title', artist: 'Local artist' },
        streams: { 1: { language: 'deu' } },
        dispositions: { 0: [], 1: ['forced'] },
      },
      targetExtension: 'mp4',
      targetMuxer: 'mp4',
      sourceMetadataPolicy: 'remove',
      chapterPolicy: 'remove',
    });
    expect(metadataPlan.args).toEqual(
      expect.arrayContaining(['-disposition:0', '0'])
    );
    const metadata = runNative(metadataPlan, { media: mediaFile });
    expect(probe(metadata.outputPaths[0]!).format?.tags).toMatchObject({
      title: 'Runtime title',
      artist: 'Local artist',
    });
    expect(probe(metadata.outputPaths[0]!).streams).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          index: 1,
          disposition: expect.objectContaining({ forced: 1 }),
        }),
      ])
    );

    const chaptered = runNative(
      buildChapterMuxPlan({
        jobId: 'chapters-native',
        source: source('media', 0, 'pattern-av.mp4'),
        chapters: [
          {
            id: 'intro',
            title: 'Intro',
            startSeconds: 0,
            endSeconds: 0.75,
            timeBase: '1/48000',
          },
          {
            id: 'main',
            title: 'Main',
            startSeconds: 0.75,
            endSeconds: 2,
            timeBase: '1/1000',
          },
        ],
        targetExtension: 'mp4',
        targetMuxer: 'mp4',
        durationSeconds: 2,
      }),
      { media: fixture('pattern-av.mp4') }
    );
    const chapterProbe = probe(chaptered.outputPaths[0]!).chapters;
    expect(chapterProbe?.map(chapterTitle)).toEqual(['Intro', 'Main']);
    expect(Number(chapterProbe?.[0]?.end_time)).toBeCloseTo(0.75, 3);
    expect(Number(chapterProbe?.[1]?.start_time)).toBeCloseTo(0.75, 3);
  }, 120_000);

  it('executes subtitle extraction, conversion, mux, and burn-in plans', () => {
    const subtitledSourcePath = join(suiteDirectory, 'subtitled-source.mp4');
    nativeCommand(
      [
        '-i',
        fixture('pattern-av.mp4'),
        '-i',
        fixture('subtitle.srt'),
        '-map',
        '0',
        '-map',
        '1:s:0',
        '-c',
        'copy',
        '-c:s',
        'mov_text',
        subtitledSourcePath,
      ],
      subtitledSourcePath
    );

    const subtitledSource = source('media', 0, 'subtitled-source.mp4');
    const convertedMedia = runNative(
      buildConvertPlan({
        jobId: 'convert-selected-subtitle-native',
        source: subtitledSource,
        preset: preset('mp4-h264-balanced'),
        streamSelection: { video: [0], audio: [1], subtitles: [2] },
      }),
      { media: subtitledSourcePath }
    );
    expect(
      probe(convertedMedia.outputPaths[0]!).streams?.map(streamType)
    ).toEqual(['video:h264', 'audio:aac', 'subtitle:mov_text']);

    const extracted = runNative(
      buildSubtitleExtractionPlan({
        jobId: 'subtitle-extract-native',
        source: subtitledSource,
        stream: { index: 2, codec: 'mov_text', language: 'eng' },
        outputFormat: 'srt',
      }),
      { media: subtitledSourcePath }
    );
    expect(readFileSync(extracted.outputPaths[0]!, 'utf8')).toContain(
      'Generated subtitle'
    );

    const externalSubtitle = source('subtitle', 1, 'subtitle.srt');
    const converted = runNative(
      buildSubtitleConversionPlan({
        jobId: 'subtitle-convert-native',
        subtitle: externalSubtitle,
        outputFormat: 'vtt',
        offsetSeconds: 0.1,
      }),
      { subtitle: fixture('subtitle.srt') }
    );
    expect(readFileSync(converted.outputPaths[0]!, 'utf8')).toContain('WEBVTT');

    const muxed = runNative(
      buildSubtitleMuxPlan({
        jobId: 'subtitle-mux-native',
        source: subtitledSource,
        subtitle: externalSubtitle,
        subtitleCodec: 'subrip',
        sourceStreams: [
          { index: 0, kind: 'video', codec: 'h264' },
          { index: 1, kind: 'audio', codec: 'aac' },
          { index: 2, kind: 'subtitle', codec: 'mov_text' },
        ],
        targetContainer: 'mp4',
        targetExtension: 'mp4',
        language: 'eng',
        title: 'English',
        default: true,
      }),
      {
        media: subtitledSourcePath,
        subtitle: fixture('subtitle.srt'),
      }
    );
    expect(
      probe(muxed.outputPaths[0]!).streams?.filter(
        (stream) => stream.codec_name === 'mov_text'
      )
    ).toHaveLength(2);
    expect(
      probe(muxed.outputPaths[0]!).streams?.filter(
        (stream) => stream.codec_type === 'subtitle'
      )[1]?.tags
    ).toMatchObject({ language: 'eng', handler_name: 'English' });
  }, 120_000);

  itNativeSubtitles(
    'executes subtitle burn-in when the native test binary has libass',
    () => {
      const burned = runNative(
        buildSubtitleBurnPlan({
          jobId: 'subtitle-burn-native',
          source: source('media', 0, 'pattern-av.mp4'),
          subtitle: source('subtitle', 1, 'subtitle.srt'),
          preset: preset('mp4-h264-balanced'),
          expectedDurationSeconds: 2,
        }),
        {
          media: fixture('pattern-av.mp4'),
          subtitle: fixture('subtitle.srt'),
        }
      );
      expect(
        probe(burned.outputPaths[0]!).streams?.some(
          (stream) => stream.codec_type === 'subtitle'
        )
      ).toBe(false);
    },
    120_000
  );

  it('executes single/series thumbnail and contact-sheet plans', () => {
    const media = source('media', 0, 'pattern-av.mp4');
    const mediaFile = fixture('pattern-av.mp4');
    const single = runNative(
      buildSingleThumbnailPlan({
        jobId: 'thumbnail-native',
        source: media,
        timeSeconds: 0.5,
        size: { width: 80 },
        format: 'png',
      }),
      { media: mediaFile }
    );
    expect(probe(single.outputPaths[0]!).streams?.[0]).toMatchObject({
      codec_name: 'png',
      width: 80,
    });

    const series = runNative(
      buildThumbnailSeriesPlan({
        jobId: 'thumbnail-series-native',
        source: media,
        durationSeconds: 2,
        timesSeconds: [0.2, 0.8, 1.4],
        size: { width: 80 },
        format: 'jpeg',
        quality: 75,
      }),
      { media: mediaFile }
    );
    expect(series.outputPaths).toHaveLength(3);
    expect(
      series.outputPaths.map((path) => probe(path).streams?.[0]?.codec_name)
    ).toEqual(['mjpeg', 'mjpeg', 'mjpeg']);

    const sheet = runNative(
      buildContactSheetPlan({
        jobId: 'contact-sheet-native',
        source: media,
        durationSeconds: 2,
        frameCount: 4,
        rows: 2,
        columns: 2,
        cellWidth: 80,
        spacing: 2,
        timestampLabels: false,
        sourceLabel: false,
        format: 'png',
      }),
      { media: mediaFile }
    );
    expect(probe(sheet.outputPaths[0]!).streams?.[0]).toMatchObject({
      codec_name: 'png',
      width: 166,
    });

    const sceneAnalysis = runNative(
      buildSceneChangeAnalysisPlan({
        jobId: 'scene-analysis-native',
        source: media,
        durationSeconds: 2,
        threshold: 0.01,
        maxFrames: 4,
      }),
      { media: mediaFile },
      'info'
    );
    const sceneFrames = parseSceneChangeFrames(sceneAnalysis.stderr);
    expect(sceneFrames.length).toBeGreaterThan(0);
    const sceneSeries = runNative(
      buildSceneThumbnailSeriesPlan({
        jobId: 'scene-series-native',
        source: media,
        durationSeconds: 2,
        frames: sceneFrames,
        size: { width: 80 },
        format: 'png',
      }),
      { media: mediaFile }
    );
    expect(sceneSeries.outputPaths).toHaveLength(sceneFrames.length);

    const sceneSheet = runNative(
      buildContactSheetPlan({
        jobId: 'scene-contact-sheet-native',
        source: media,
        durationSeconds: 2,
        frameCount: sceneFrames.length,
        rows: 1,
        columns: sceneFrames.length,
        cellWidth: 80,
        spacing: 2,
        sceneFrames,
        format: 'png',
      }),
      { media: mediaFile }
    );
    expect(statSync(sceneSheet.outputPaths[0]!).size).toBeGreaterThan(500);
  }, 120_000);

  itNativeDrawtext(
    'executes contact-sheet labels with the reviewed bundled font',
    () => {
      const media = source('media', 0, 'pattern-av.mp4');
      const labeledSheet = runNative(
        buildContactSheetPlan({
          jobId: 'labeled-contact-sheet-native',
          source: media,
          durationSeconds: 2,
          frameCount: 2,
          rows: 1,
          columns: 2,
          cellWidth: 120,
          timestampLabels: true,
          sourceLabel: true,
          drawtextAvailable: true,
          font: source('font', 1, 'DejaVuSans.ttf'),
          format: 'png',
        }),
        {
          media: fixture('pattern-av.mp4'),
          font: resolve(process.cwd(), 'public/fonts/DejaVuSans.ttf'),
        }
      );
      expect(statSync(labeledSheet.outputPaths[0]!).size).toBeGreaterThan(500);
    },
    120_000
  );

  it('executes a transformed, faded, normalized sequential timeline', () => {
    const execution = runNative(
      buildTimelineExportPlan({
        jobId: 'timeline-native',
        clips: [
          {
            id: 'clip-a',
            source: source('first', 0, 'compatible-a.mp4'),
            sourceInSeconds: 0.1,
            sourceOutSeconds: 0.9,
            sourceDurationSeconds: 1,
            hasVideo: true,
            hasAudio: true,
            transform: { rotation: 180 },
            audioGainDb: -1,
            audioNormalization: {
              mode: 'peak',
              targetPeakDb: -1,
              measurement: { inputPeakDbfs: -18 },
            },
            audioFadeInSeconds: 0.1,
            audioFadeCurve: 'qsin',
            videoFadeOutSeconds: 0.1,
            videoFadeCurve: 'exp',
          },
          {
            id: 'clip-b',
            source: source('second', 1, 'different-dimensions.mp4'),
            sourceInSeconds: 0,
            sourceOutSeconds: 0.8,
            sourceDurationSeconds: 1,
            hasVideo: true,
            hasAudio: true,
            transform: {
              crop: {
                x: 0,
                y: 0,
                width: 80,
                height: 80,
                sourceWidth: 96,
                sourceHeight: 96,
                requireEvenDimensions: true,
              },
            },
            audioFadeOutSeconds: 0.1,
            audioFadeCurve: 'exp',
            videoFadeInSeconds: 0.1,
            videoFadeCurve: 'qsin',
          },
        ],
        preset: preset('mp4-h264-balanced'),
        outputWidth: 128,
        outputHeight: 72,
        frameRate: 10,
        sampleRate: 16_000,
        channelLayout: 'mono',
        missingAudioPolicy: 'reject',
      }),
      {
        first: fixture('compatible-a.mp4'),
        second: fixture('different-dimensions.mp4'),
      }
    );
    const result = probe(execution.outputPaths[0]!);
    expect(result.streams?.[0]).toMatchObject({ width: 128, height: 72 });
    expect(
      result.streams?.some((stream) => stream.codec_type === 'audio')
    ).toBe(true);
    expect(Number(result.format?.duration)).toBeCloseTo(1.6, 1);
  }, 120_000);
});

function streamType(stream: {
  readonly codec_type?: string;
  readonly codec_name?: string;
}): string {
  return `${stream.codec_type ?? 'unknown'}:${stream.codec_name ?? 'unknown'}`;
}

function chapterTitle(chapter: {
  readonly tags?: Readonly<Record<string, string>>;
}): string | undefined {
  return chapter.tags?.title;
}
