import { describe, expect, it } from 'vitest';
import {
  AUDIO_PRESETS,
  BUILT_IN_PRESETS,
  MemoryUserPresetPersistence,
  UserPresetRegistry,
  VIDEO_PRESETS,
  migrateUserPreset,
  parseUserPresetJson,
  presetAvailability,
  presetEncodingArguments,
  serializeUserPreset,
  validateUserPreset,
  type UserExportPreset,
} from '../../src/presets';

const USER_PRESET: UserExportPreset = {
  schemaVersion: 1,
  id: 'my-podcast',
  name: 'My podcast',
  description: 'Speech export',
  kind: 'audio',
  container: 'mp3',
  fileExtension: 'mp3',
  audio: {
    codec: 'libmp3lame',
    bitrateKbps: 128,
    sampleRate: 44_100,
    channels: 2,
  },
  metadataPolicy: 'copy',
  chapterPolicy: 'remove',
  subtitlePolicy: 'none',
};

describe('built-in preset registry', () => {
  it('contains immutable, uniquely identified video and audio definitions', () => {
    expect(VIDEO_PRESETS.length).toBeGreaterThanOrEqual(6);
    expect(AUDIO_PRESETS.length).toBeGreaterThanOrEqual(6);
    expect(new Set(BUILT_IN_PRESETS.map((preset) => preset.id)).size).toBe(
      BUILT_IN_PRESETS.length
    );
    expect(Object.isFrozen(BUILT_IN_PRESETS[0])).toBe(true);
    expect(Object.isFrozen(BUILT_IN_PRESETS[0]?.settingsSummary)).toBe(true);
  });

  it('disables a preset with exact missing capability reasons', () => {
    const preset = BUILT_IN_PRESETS.find(
      (candidate) => candidate.id === 'webm-vp9-opus'
    )!;
    const unavailable = presetAvailability(preset, {
      muxers: new Set(['webm']),
      encoders: new Set(['libvpx-vp9']),
      decoders: new Set(),
      filters: new Set(),
    });
    expect(unavailable).toEqual({
      status: 'unavailable',
      reasons: ['Missing encoders: libopus'],
    });
    expect(
      presetAvailability(preset, {
        muxers: new Set(['webm']),
        encoders: new Set(['libvpx-vp9', 'libopus']),
        decoders: new Set(),
        filters: new Set(),
      })
    ).toEqual({ status: 'available', reasons: [] });
    expect(
      presetAvailability(
        preset,
        {
          muxers: new Set(['webm']),
          encoders: new Set(['libvpx-vp9', 'libopus']),
          decoders: new Set(),
          filters: new Set(),
        },
        { filters: ['drawtext'] }
      )
    ).toEqual({
      status: 'degraded',
      reasons: ['Optional filters unavailable: drawtext'],
    });
  });

  it('turns allowlisted settings into structured encoder arguments', () => {
    expect(presetEncodingArguments(USER_PRESET)).toEqual([
      '-vn',
      '-c:a',
      'libmp3lame',
      '-b:a',
      '128k',
      '-ar',
      '44100',
      '-ac',
      '2',
    ]);
  });
});

describe('user preset validation and persistence', () => {
  it('round-trips a versioned preset as JSON', () => {
    const json = serializeUserPreset(USER_PRESET);
    expect(parseUserPresetJson(json)).toEqual(USER_PRESET);
    expect(validateUserPreset(USER_PRESET)).toMatchObject({ valid: true });
  });

  it('migrates an allowlisted legacy schema without accepting raw fields', () => {
    expect(
      migrateUserPreset({
        schemaVersion: 0,
        id: 'legacy',
        name: 'Legacy',
        kind: 'audio',
        format: 'ipod',
        audio: { codec: 'aac', bitrateKbps: 128 },
        includeChapters: true,
      })
    ).toEqual({
      schemaVersion: 1,
      id: 'legacy',
      name: 'Legacy',
      kind: 'audio',
      container: 'ipod',
      fileExtension: 'm4a',
      audio: { codec: 'aac', bitrateKbps: 128 },
      metadataPolicy: 'copy',
      chapterPolicy: 'keep',
      subtitlePolicy: 'none',
    });
    expect(() =>
      migrateUserPreset({
        schemaVersion: 0,
        id: 'legacy',
        name: 'Legacy',
        kind: 'audio',
        format: 'mp3',
        args: ['-i', 'remote'],
      })
    ).toThrow(/unsupported field/iu);
  });

  it.each([
    ['args', ['-i', 'https://example.invalid/a']],
    ['filter', 'movie=https://example.invalid'],
    ['inputPath', '/etc/passwd'],
    ['protocol', 'https'],
  ])('rejects imported arbitrary %s fields', (field, value) => {
    expect(
      validateUserPreset({ ...USER_PRESET, [field]: value })
    ).toMatchObject({
      valid: false,
      errors: [expect.stringContaining(`"${field}"`)],
    });
  });

  it('rejects unknown codecs and unsafe identifiers', () => {
    expect(
      validateUserPreset({
        ...USER_PRESET,
        id: '../../unsafe',
        audio: { codec: 'shell', bitrateKbps: 128 },
      })
    ).toMatchObject({ valid: false });
  });

  it.each([
    [
      'MP4 H.264/AAC video',
      {
        ...USER_PRESET,
        kind: 'video',
        container: 'mp4',
        fileExtension: 'mp4',
        video: { codec: 'libx264', qualityMode: 'balanced', crf: 23 },
        audio: { codec: 'aac', bitrateKbps: 160 },
        chapterPolicy: 'keep',
        subtitlePolicy: 'convert',
        fastStart: true,
      },
    ],
    [
      'WebM VP9/Opus video',
      {
        ...USER_PRESET,
        kind: 'video',
        container: 'webm',
        fileExtension: 'webm',
        video: { codec: 'libvpx-vp9', qualityMode: 'smaller-file', crf: 32 },
        audio: { codec: 'libopus', bitrateKbps: 128 },
        chapterPolicy: 'keep',
        subtitlePolicy: 'convert',
      },
    ],
    [
      'Matroska VP8/FLAC video',
      {
        ...USER_PRESET,
        kind: 'video',
        container: 'matroska',
        fileExtension: 'mkv',
        video: { codec: 'libvpx', qualityMode: 'compatibility', crf: 18 },
        audio: { codec: 'flac' },
        chapterPolicy: 'keep',
        subtitlePolicy: 'copy-compatible',
      },
    ],
    [
      'animated WebP derivative',
      {
        ...USER_PRESET,
        kind: 'derivative',
        container: 'webp',
        fileExtension: 'webp',
        video: {
          codec: 'libwebp_anim',
          qualityMode: 'balanced',
          crf: 60,
        },
        audio: undefined,
        metadataPolicy: 'remove',
        chapterPolicy: 'remove',
        subtitlePolicy: 'none',
      },
    ],
    [
      'GIF derivative',
      {
        ...USER_PRESET,
        kind: 'derivative',
        container: 'gif',
        fileExtension: 'gif',
        video: { codec: 'gif', qualityMode: 'compatibility' },
        audio: undefined,
        metadataPolicy: 'remove',
        chapterPolicy: 'remove',
        subtitlePolicy: 'none',
      },
    ],
    ['MP3 audio', USER_PRESET],
    [
      'M4A/AAC audio',
      {
        ...USER_PRESET,
        container: 'ipod',
        fileExtension: 'm4a',
        audio: { codec: 'aac', bitrateKbps: 160 },
        fastStart: true,
      },
    ],
    [
      'Ogg/Vorbis audio',
      {
        ...USER_PRESET,
        container: 'ogg',
        fileExtension: 'ogg',
        audio: { codec: 'libvorbis', bitrateKbps: 160 },
      },
    ],
    [
      'Opus audio',
      {
        ...USER_PRESET,
        container: 'opus',
        fileExtension: 'opus',
        audio: { codec: 'libopus', bitrateKbps: 128 },
      },
    ],
    [
      'WAV/PCM audio',
      {
        ...USER_PRESET,
        container: 'wav',
        fileExtension: 'wav',
        audio: { codec: 'pcm_s16le' },
      },
    ],
    [
      'FLAC audio',
      {
        ...USER_PRESET,
        container: 'flac',
        fileExtension: 'flac',
        audio: { codec: 'flac' },
      },
    ],
  ])('accepts the reviewed %s combination', (_label, preset) => {
    expect(validateUserPreset(preset)).toMatchObject({
      valid: true,
      errors: [],
    });
  });

  it.each([
    [
      'an unknown container',
      { ...USER_PRESET, container: 'avi', fileExtension: 'avi' },
      /container "avi".*allowlist/iu,
    ],
    [
      'a mismatched extension',
      {
        ...USER_PRESET,
        container: 'ipod',
        fileExtension: 'mp4',
        audio: { codec: 'aac' },
      },
      /fileExtension must be "m4a"/u,
    ],
    [
      'the wrong media kind',
      {
        ...USER_PRESET,
        kind: 'audio',
        container: 'webm',
        fileExtension: 'webm',
        video: undefined,
        audio: { codec: 'libopus' },
      },
      /container "webm".*kind "audio"/u,
    ],
    [
      'an incompatible video codec',
      {
        ...USER_PRESET,
        kind: 'video',
        container: 'mp4',
        fileExtension: 'mp4',
        video: { codec: 'libvpx-vp9', qualityMode: 'balanced' },
        audio: { codec: 'aac' },
      },
      /video codec "libvpx-vp9".*container "mp4"/u,
    ],
    [
      'an incompatible audio codec',
      {
        ...USER_PRESET,
        kind: 'video',
        container: 'webm',
        fileExtension: 'webm',
        video: { codec: 'libvpx', qualityMode: 'balanced' },
        audio: { codec: 'aac' },
      },
      /audio codec "aac".*container "webm"/u,
    ],
    [
      'audio in an image derivative',
      {
        ...USER_PRESET,
        kind: 'derivative',
        container: 'gif',
        fileExtension: 'gif',
        video: { codec: 'gif', qualityMode: 'compatibility' },
      },
      /container "gif" does not support audio settings/u,
    ],
    [
      'a video container without video',
      {
        ...USER_PRESET,
        kind: 'video',
        container: 'mp4',
        fileExtension: 'mp4',
        audio: { codec: 'aac' },
        video: undefined,
      },
      /container "mp4" requires video settings/u,
    ],
    [
      'fast-start outside MP4/M4A',
      { ...USER_PRESET, fastStart: true },
      /fastStart is only supported.*not "mp3"/u,
    ],
  ])('rejects %s with a clear compatibility error', (_label, preset, error) => {
    const result = validateUserPreset(preset);
    expect(result.valid).toBe(false);
    expect(result.errors.join('; ')).toMatch(error);
  });

  it.each([
    ['metadataPolicy', 'edit', /metadataPolicy "edit".*Quick presets/u],
    ['chapterPolicy', 'replace', /chapterPolicy "replace".*Quick presets/u],
    [
      'subtitlePolicy',
      'burn-in',
      /subtitlePolicy "burn-in".*explicit subtitle source/u,
    ],
  ])(
    'rejects the unsupported Quick preset policy %s=%s',
    (field, value, error) => {
      const result = validateUserPreset({
        ...USER_PRESET,
        [field]: value,
      });
      expect(result.valid).toBe(false);
      expect(result.errors.join('; ')).toMatch(error);
    }
  );

  it('applies relational compatibility checks to imported JSON', () => {
    expect(() =>
      parseUserPresetJson(
        JSON.stringify({
          ...USER_PRESET,
          kind: 'video',
          container: 'mp4',
          fileExtension: 'mp4',
          video: { codec: 'libvpx-vp9', qualityMode: 'balanced' },
          audio: { codec: 'aac' },
        })
      )
    ).toThrow(/video codec "libvpx-vp9".*container "mp4"/iu);
  });

  it('supports create, clone, rename, export, import, delete and reset', async () => {
    const persistence = new MemoryUserPresetPersistence();
    const registry = new UserPresetRegistry(persistence);
    await registry.initialize();
    await registry.save(USER_PRESET);
    const clone = await registry.clone('my-podcast', 'my-podcast-copy', 'Copy');
    expect(clone.name).toBe('Copy');
    await registry.rename(clone.id, 'Renamed');
    expect(registry.get(clone.id)?.name).toBe('Renamed');
    const exported = registry.export('my-podcast');
    await registry.delete('my-podcast');
    expect(registry.get('my-podcast')).toBeUndefined();
    await registry.import(exported);
    expect(registry.get('my-podcast')).toEqual(USER_PRESET);
    await registry.reset();
    expect(registry.list()).toEqual([]);

    const reloaded = new UserPresetRegistry(persistence);
    await reloaded.initialize();
    expect(reloaded.list()).toEqual([]);
  });
});
