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

import {
  ProbeParseError,
  normalizeFfprobeReport,
  parseFfprobeJson,
  parseFraction,
  parseFractionDetailed,
  parseFrameRate,
} from '../../../src/media/probe-parser';

describe('fraction parsing', () => {
  it.each([
    ['30000/1001', 30000 / 1001],
    ['25/1', 25],
    ['-1/2', -0.5],
    [' 1 / 4 ', 0.25],
    ['23.976', 23.976],
    [5, 5],
  ])('parses %j safely', (input, expected) => {
    expect(parseFraction(input)).toBeCloseTo(expected);
  });

  it.each([
    ['0/0', 'division-by-zero'],
    ['1/0', 'division-by-zero'],
    ['NaN', 'invalid-syntax'],
    ['Infinity', 'invalid-syntax'],
    ['1e3', 'invalid-syntax'],
    ['1/2/3', 'invalid-syntax'],
    ['999999999999999999/1', 'component-too-large'],
  ])('rejects %j with a useful reason', (input, reason) => {
    expect(parseFractionDetailed(input)).toEqual({ ok: false, reason });
  });

  it('rejects implausible, zero and negative frame rates', () => {
    expect(parseFrameRate('0/0')).toBeUndefined();
    expect(parseFrameRate('0/1')).toBeUndefined();
    expect(parseFrameRate('-25/1')).toBeUndefined();
    expect(parseFrameRate('1001/1')).toBeUndefined();
  });
});

describe('ffprobe normalization', () => {
  it('normalizes format, streams, dispositions, tags and chapters', () => {
    const report = {
      format: {
        duration: '12.500000',
        start_time: '-0.021',
        bit_rate: '192000',
        format_name: 'mov,mp4,m4a,3gp,3g2,mj2',
        format_long_name: 'QuickTime / MOV',
        tags: { title: 'Demo' },
      },
      streams: [
        {
          index: 2,
          codec_type: 'audio',
          codec_name: 'aac',
          sample_rate: '48000',
          channels: 2,
          channel_layout: 'stereo',
          disposition: { default: 1, forced: 0 },
          tags: { language: 'deu', title: 'German' },
        },
        {
          index: 0,
          codec_type: 'video',
          codec_name: 'h264',
          width: 1920,
          height: 1080,
          coded_width: 1920,
          coded_height: 1088,
          pix_fmt: 'yuv420p',
          sample_aspect_ratio: '8:6',
          display_aspect_ratio: '16/9',
          time_base: '1/30000',
          avg_frame_rate: '30000/1001',
          r_frame_rate: '30/1',
          side_data_list: [
            {
              side_data_type: 'Display Matrix',
              rotation: -90,
            },
          ],
          disposition: { default: true },
          tags: {},
        },
      ],
      chapters: [
        {
          id: 4,
          time_base: '1/1000',
          start: 1000,
          end: 2500,
          tags: { title: 'Opening' },
        },
      ],
    };

    const probe = normalizeFfprobeReport(report);

    expect(probe).toMatchObject({
      durationSeconds: 12.5,
      startTimeSeconds: -0.021,
      bitRate: 192000,
      formatNames: ['mov', 'mp4', 'm4a', '3gp', '3g2', 'mj2'],
      formatLongName: 'QuickTime / MOV',
      tags: { title: 'Demo' },
      warnings: [],
    });
    expect(probe.streams.map((stream) => stream.index)).toEqual([0, 2]);
    expect(probe.streams[0]).toMatchObject({
      type: 'video',
      codecName: 'h264',
      width: 1920,
      height: 1080,
      sampleAspectRatio: '4:3',
      displayAspectRatio: '16:9',
      rotationDegrees: -90,
      frameRate: 30000 / 1001,
      timeBase: '1/30000',
      disposition: { default: true },
    });
    expect(probe.streams[1]).toMatchObject({
      type: 'audio',
      sampleRate: 48000,
      channels: 2,
      language: 'deu',
      title: 'German',
      disposition: { default: true, forced: false },
    });
    expect(probe.chapters).toEqual([
      {
        id: 4,
        startSeconds: 1,
        endSeconds: 2.5,
        timeBase: '1/1000',
        title: 'Opening',
        tags: { title: 'Opening' },
      },
    ]);
  });

  it('falls back to r_frame_rate and records malformed data as warnings', () => {
    const probe = normalizeFfprobeReport({
      format: { duration: 'not-a-number', tags: { title: 42 } },
      streams: [
        {
          index: 0,
          codec_type: 'video',
          avg_frame_rate: '0/0',
          r_frame_rate: '25/1',
          disposition: { default: 'sometimes' },
          tags: [],
        },
        { index: 1, codec_type: 'telepathy', tags: {}, disposition: {} },
        { index: 1, codec_type: 'audio', tags: {}, disposition: {} },
        { codec_type: 'audio' },
        'not a stream',
      ],
      chapters: [{ id: 0, start_time: '2', end_time: '1', tags: {} }],
    });

    expect(probe.durationSeconds).toBeUndefined();
    expect(probe.streams).toHaveLength(2);
    expect(probe.streams[0]?.frameRate).toBe(25);
    expect(probe.streams[1]?.type).toBe('unknown');
    expect(probe.chapters).toEqual([]);
    expect(probe.warnings.map((warning) => warning.code)).toEqual(
      expect.arrayContaining([
        'invalid-field',
        'invalid-fraction',
        'unknown-stream-type',
        'duplicate-stream-index',
        'missing-stream-index',
        'invalid-stream',
        'invalid-chapter',
      ])
    );
  });

  it('parses legacy rotation and rejects misleading aspect ratios', () => {
    const probe = normalizeFfprobeReport({
      streams: [
        {
          index: 0,
          codec_type: 'video',
          sample_aspect_ratio: '0:0',
          display_aspect_ratio: 'not-available',
          side_data_list: [{ rotation: 'not-a-number' }],
          disposition: {},
          tags: { rotate: '270' },
        },
        {
          index: 1,
          codec_type: 'video',
          sample_aspect_ratio: ' 32 / 18 ',
          display_aspect_ratio: '64:36',
          disposition: {},
          tags: { rotate: '540' },
        },
      ],
    });

    expect(probe.streams[0]).toMatchObject({
      rotationDegrees: -90,
    });
    expect(probe.streams[0]).not.toHaveProperty('sampleAspectRatio');
    expect(probe.streams[0]).not.toHaveProperty('displayAspectRatio');
    expect(probe.streams[1]).toMatchObject({
      sampleAspectRatio: '16:9',
      displayAspectRatio: '16:9',
      rotationDegrees: 180,
    });
    expect(probe.warnings).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          code: 'invalid-fraction',
          path: 'streams[0].sample_aspect_ratio',
        }),
        expect.objectContaining({
          code: 'invalid-fraction',
          path: 'streams[0].display_aspect_ratio',
        }),
        expect.objectContaining({
          code: 'invalid-field',
          path: 'streams[0].side_data_list[0].rotation',
        }),
      ])
    );
  });

  it('keeps raw diagnostics separate from the normalized model', () => {
    const rawReport = '{"format":{"format_name":"wav"},"streams":[]}';
    const result = parseFfprobeJson(rawReport);

    expect(result.rawReport).toBe(rawReport);
    expect(result.probe.formatNames).toEqual(['wav']);
    expect(result.probe).not.toHaveProperty('rawReport');
  });

  it('throws a typed error for malformed JSON or a non-object root', () => {
    expect(() => parseFfprobeJson('{')).toThrow(ProbeParseError);
    expect(() => normalizeFfprobeReport([])).toThrow(
      'The ffprobe report must be a JSON object.'
    );
  });
});
