import {
  cleanup,
  render,
  screen,
  waitFor,
  within,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
  AdvancedOperations,
  type AdvancedCapabilityKey,
  type AdvancedCapabilityStatus,
  type AdvancedOperationsProps,
} from '../../src/components/AdvancedOperations';

const AVAILABLE: AdvancedCapabilityStatus = { available: true };

function capabilities(
  ...keys: readonly AdvancedCapabilityKey[]
): AdvancedOperationsProps['availability'] {
  return Object.fromEntries(keys.map((key) => [key, AVAILABLE]));
}

const SOURCE: NonNullable<AdvancedOperationsProps['source']> = {
  durationSeconds: 90,
  sourceExtension: 'mp4',
  hasAudio: true,
  hasVideo: true,
  selectedAudioStreamIndex: 1,
};

const EXPORT_PRESETS: NonNullable<AdvancedOperationsProps['exportPresets']> = [
  {
    id: 'mp4-h264-balanced',
    name: 'MP4 · H.264 + AAC',
    fileExtension: 'mp4',
    supportsAudio: true,
    supportsVideo: true,
  },
  {
    id: 'audio-mp3',
    name: 'MP3',
    fileExtension: 'mp3',
    supportsAudio: true,
    supportsVideo: false,
  },
];

afterEach(cleanup);

describe('AdvancedOperations', () => {
  it('fails closed when a runtime capability was not explicitly verified', () => {
    render(
      <AdvancedOperations
        source={SOURCE}
        splitMarkers={[30]}
        onSplit={vi.fn()}
      />
    );

    const action = screen.getByRole('button', { name: 'Queue 2 segments' });
    expect(action).toBeDisabled();
    expect(
      screen.getByText(
        'The required browser and FFmpeg capabilities have not been verified.'
      )
    ).toBeInTheDocument();
  });

  it('builds a validated marker request and explains fast versus accurate mode', async () => {
    const user = userEvent.setup();
    const onSplit = vi.fn();
    render(
      <AdvancedOperations
        source={SOURCE}
        splitMarkers={[60, 30]}
        exportPresets={EXPORT_PRESETS}
        availability={capabilities('split-fast', 'split-accurate')}
        onSplit={onSplit}
      />
    );

    expect(
      screen.getByText(/boundaries may move to nearby keyframes/iu)
    ).toBeInTheDocument();
    expect(screen.getByText(/practical frame accuracy/iu)).toBeInTheDocument();
    const ranges = screen.getByLabelText('Exact planned segment ranges');
    expect(within(ranges).getByText('0 s–30 s')).toBeVisible();
    expect(within(ranges).getByText('30 s–60 s')).toBeVisible();
    expect(within(ranges).getByText('60 s–90 s')).toBeVisible();

    await user.click(screen.getByRole('button', { name: 'Queue 3 segments' }));
    await waitFor(() =>
      expect(onSplit).toHaveBeenCalledWith({
        mode: 'fast',
        definition: { type: 'markers', markers: [30, 60] },
        targetExtension: 'mp4',
      })
    );

    await user.click(
      screen.getByRole('radio', { name: /Accurate re-encode/iu })
    );
    await user.click(screen.getByRole('button', { name: 'Queue 3 segments' }));
    await waitFor(() =>
      expect(onSplit).toHaveBeenLastCalledWith({
        mode: 'accurate',
        definition: { type: 'markers', markers: [30, 60] },
        targetExtension: 'mp4',
        presetId: 'mp4-h264-balanced',
      })
    );
  });

  it('shows exact latest-batch status and individual segment failure details', () => {
    render(
      <AdvancedOperations
        source={SOURCE}
        splitMarkers={[30]}
        splitBatch={{
          planId: 'split-plan',
          segments: [
            {
              outputId: 'segment-1',
              fileName: 'source-split-001.mp4',
              startSeconds: 0,
              endSeconds: 30,
              state: 'verified',
            },
            {
              outputId: 'segment-2',
              fileName: 'source-split-002.mp4',
              startSeconds: 30,
              endSeconds: 90,
              state: 'failed',
              message: 'FFmpeg did not return this expected output.',
            },
          ],
        }}
      />
    );

    const status = screen.getByLabelText('Split segment status');
    expect(within(status).getByText('source-split-001.mp4')).toBeVisible();
    expect(within(status).getByText('0 s–30 s')).toBeVisible();
    expect(within(status).getByText('verified')).toBeVisible();
    expect(
      within(status).getByText('FFmpeg did not return this expected output.')
    ).toBeVisible();
  });

  it('supports keyboard tab navigation and emits typed loudness targets', async () => {
    const user = userEvent.setup();
    const onAudioAction = vi.fn();
    render(
      <AdvancedOperations
        source={SOURCE}
        exportPresets={EXPORT_PRESETS}
        availability={capabilities(
          'loudness-measure',
          'loudness-normalize',
          'peak-normalize',
          'waveform-static',
          'waveform-peaks'
        )}
        onAudioAction={onAudioAction}
      />
    );

    const splitTab = screen.getByRole('tab', { name: 'Split media' });
    splitTab.focus();
    await user.keyboard('{ArrowRight}');
    expect(
      screen.getByRole('tab', { name: 'Waveform and loudness' })
    ).toHaveAttribute('aria-selected', 'true');

    await user.selectOptions(
      screen.getByLabelText(/^Target profile/iu),
      'podcast'
    );
    await user.click(screen.getByRole('button', { name: 'Measure EBU R128' }));
    await waitFor(() =>
      expect(onAudioAction).toHaveBeenCalledWith({
        type: 'loudness-measurement',
        audioStreamIndex: 1,
        targets: {
          integratedLufs: -16,
          truePeakDbtp: -1,
          loudnessRangeLu: 11,
        },
      })
    );

    const normalize = screen.getByRole('button', {
      name: 'Apply measured normalization',
    });
    expect(normalize).toBeDisabled();
    expect(
      screen.getByText(
        'Run the first-pass loudness measurement before normalization.'
      )
    ).toBeInTheDocument();
  });

  it('uses an audio-only preset for measured normalization', async () => {
    const user = userEvent.setup();
    const onAudioAction = vi.fn();
    const measurement = {
      inputIntegratedLufs: -19.7,
      inputTruePeakDbtp: -2.1,
      inputLoudnessRangeLu: 4.2,
      inputThresholdLufs: -30,
      targetOffsetLu: -0.3,
    };
    render(
      <AdvancedOperations
        source={SOURCE}
        exportPresets={EXPORT_PRESETS}
        loudnessMeasurement={measurement}
        availability={capabilities('loudness-normalize')}
        onAudioAction={onAudioAction}
      />
    );

    await user.click(
      screen.getByRole('tab', { name: 'Waveform and loudness' })
    );
    await user.click(
      screen.getByRole('button', { name: 'Apply measured normalization' })
    );
    await waitFor(() =>
      expect(onAudioAction).toHaveBeenCalledWith({
        type: 'loudness-normalization',
        audioStreamIndex: 1,
        targets: {
          integratedLufs: -14,
          truePeakDbtp: -1,
          loudnessRangeLu: 11,
        },
        measurement,
        presetId: 'audio-mp3',
      })
    );
  });

  it('keeps unverified contact-sheet labels off and emits a bounded series request', async () => {
    const user = userEvent.setup();
    const onThumbnailSeries = vi.fn();
    render(
      <AdvancedOperations
        source={SOURCE}
        availability={capabilities('thumbnail-series', 'contact-sheet')}
        imageFormatAvailability={{ jpeg: AVAILABLE }}
        onThumbnailSeries={onThumbnailSeries}
        onContactSheet={vi.fn()}
      />
    );

    await user.click(
      screen.getByRole('tab', {
        name: 'Thumbnails and contact sheets',
      })
    );
    expect(screen.getByLabelText('Timestamp labels')).toBeDisabled();
    expect(screen.getByLabelText('Source filename label')).toBeDisabled();
    expect(
      screen.getByText(
        'Labels stay off until drawtext and a safe local font are verified.'
      )
    ).toBeInTheDocument();

    await user.click(
      screen.getByRole('button', { name: 'Generate thumbnail series' })
    );
    await waitFor(() =>
      expect(onThumbnailSeries).toHaveBeenCalledWith({
        distribution: { type: 'evenly-spaced', count: 12 },
        width: 480,
        format: 'jpeg',
        quality: 85,
      })
    );
  });

  it('emits bounded scene analysis settings for series and contact sheets', async () => {
    const user = userEvent.setup();
    const onThumbnailSeries = vi.fn();
    const onContactSheet = vi.fn();
    render(
      <AdvancedOperations
        source={SOURCE}
        availability={capabilities(
          'thumbnail-series',
          'contact-sheet',
          'scene-change-analysis',
          'contact-sheet-labels'
        )}
        imageFormatAvailability={{ jpeg: AVAILABLE }}
        onThumbnailSeries={onThumbnailSeries}
        onContactSheet={onContactSheet}
      />
    );

    await user.click(
      screen.getByRole('tab', {
        name: 'Thumbnails and contact sheets',
      })
    );
    const selectionFields = screen.getAllByLabelText('Frame selection');
    await user.selectOptions(selectionFields[0]!, 'scene-changes');
    await user.clear(screen.getByLabelText('Scene sensitivity threshold'));
    await user.type(
      screen.getByLabelText('Scene sensitivity threshold'),
      '0.42'
    );
    await user.click(
      screen.getByRole('button', { name: 'Generate thumbnail series' })
    );
    await waitFor(() =>
      expect(onThumbnailSeries).toHaveBeenCalledWith({
        distribution: {
          type: 'scene-changes',
          count: 12,
          threshold: 0.42,
        },
        width: 480,
        format: 'jpeg',
        quality: 85,
      })
    );

    await user.selectOptions(selectionFields[1]!, 'scene-changes');
    await user.clear(screen.getByLabelText('Contact-sheet scene threshold'));
    await user.type(
      screen.getByLabelText('Contact-sheet scene threshold'),
      '0.3'
    );
    await user.click(
      screen.getByRole('button', { name: 'Generate contact sheet' })
    );
    await waitFor(() =>
      expect(onContactSheet).toHaveBeenCalledWith(
        expect.objectContaining({
          frameCount: 12,
          selection: { type: 'scene-changes', threshold: 0.3 },
        })
      )
    );
  });

  it('classifies bitmap subtitles conservatively before enabling extraction', async () => {
    const user = userEvent.setup();
    const onSubtitleAction = vi.fn();
    render(
      <AdvancedOperations
        source={SOURCE}
        subtitleStreams={[
          { index: 2, codec: 'dvd_subtitle', language: 'deu' },
          { index: 3, codec: 'subrip', language: 'eng' },
        ]}
        availability={capabilities('subtitle-extract')}
        onSubtitleAction={onSubtitleAction}
      />
    );

    await user.click(screen.getByRole('tab', { name: 'Subtitle operations' }));
    expect(
      screen.getByRole('button', { name: 'Extract subtitle' })
    ).toBeDisabled();
    expect(
      screen.getByText(/bitmap subtitle extraction is not text conversion/iu)
    ).toBeInTheDocument();

    await user.selectOptions(screen.getByLabelText('Subtitle stream'), '3');
    await user.click(screen.getByRole('button', { name: 'Extract subtitle' }));
    await waitFor(() =>
      expect(onSubtitleAction).toHaveBeenCalledWith({
        type: 'extract',
        stream: { index: 3, codec: 'subrip', language: 'eng' },
        outputFormat: 'srt',
      })
    );
  });

  it('retains validated stream metadata JSON for apply and export', async () => {
    const user = userEvent.setup();
    const onMetadataAction = vi.fn();
    render(
      <AdvancedOperations
        source={SOURCE}
        availability={capabilities('metadata-write')}
        onMetadataAction={onMetadataAction}
      />
    );

    await user.click(screen.getByRole('tab', { name: 'Metadata policies' }));
    await user.upload(
      screen.getByLabelText('Import metadata JSON'),
      new File(
        [
          JSON.stringify({
            format: { title: 'Imported title', genre: 'Synthetic' },
            streams: {
              1: { language: 'deu', title: 'German dialogue' },
            },
          }),
        ],
        'metadata.json',
        { type: 'application/json' }
      )
    );
    await waitFor(() =>
      expect(onMetadataAction).toHaveBeenCalledWith({
        type: 'import',
        edits: {
          format: { title: 'Imported title', genre: 'Synthetic' },
          streams: {
            1: { language: 'deu', title: 'German dialogue' },
          },
        },
      })
    );

    expect(screen.getByText(/1 stream metadata edit loaded/iu)).toBeVisible();
    await user.click(
      screen.getByRole('button', { name: 'Apply metadata policies' })
    );
    await waitFor(() =>
      expect(onMetadataAction).toHaveBeenLastCalledWith({
        type: 'apply',
        edits: {
          format: { genre: 'Synthetic', title: 'Imported title' },
          streams: {
            1: { language: 'deu', title: 'German dialogue' },
          },
        },
        sourceMetadataPolicy: 'copy',
        chapterPolicy: 'keep',
        coverArtPolicy: 'keep',
      })
    );
  });

  it('edits probed stream language, title, and typed dispositions directly', async () => {
    const user = userEvent.setup();
    const onMetadataAction = vi.fn();
    render(
      <AdvancedOperations
        source={{ ...SOURCE, sourceId: 'source-a' }}
        metadataStreams={[
          {
            index: 1,
            type: 'audio',
            codec: 'aac',
            language: 'eng',
            title: 'Original',
            disposition: { default: false, forced: false },
          },
        ]}
        availability={capabilities('metadata-write')}
        onMetadataAction={onMetadataAction}
      />
    );

    await user.click(screen.getByRole('tab', { name: 'Metadata policies' }));
    const language = screen.getByLabelText('Stream #1 language');
    await user.clear(language);
    await user.type(language, 'deu');
    const title = screen.getByLabelText('Stream #1 title');
    await user.clear(title);
    await user.type(title, 'German dialogue');
    await user.click(screen.getByLabelText('Default stream #1'));
    await user.click(
      screen.getByRole('button', { name: 'Apply metadata policies' })
    );

    await waitFor(() =>
      expect(onMetadataAction).toHaveBeenCalledWith({
        type: 'apply',
        edits: {
          format: {},
          streams: {
            1: { language: 'deu', title: 'German dialogue' },
          },
          dispositions: { 1: ['default'] },
        },
        sourceMetadataPolicy: 'copy',
        chapterPolicy: 'keep',
        coverArtPolicy: 'keep',
      })
    );
  });

  it('does not clear untouched source container tags', async () => {
    const user = userEvent.setup();
    const onMetadataAction = vi.fn();
    render(
      <AdvancedOperations
        source={{ ...SOURCE, sourceId: 'source-tags' }}
        metadataFormatTags={{
          title: 'Original title',
          artist: 'Original artist',
          album: 'Original album',
        }}
        availability={capabilities('metadata-write')}
        onMetadataAction={onMetadataAction}
      />
    );

    await user.click(screen.getByRole('tab', { name: 'Metadata policies' }));
    expect(screen.getByLabelText('Artist')).toHaveValue('Original artist');
    const title = screen.getByLabelText('Title');
    await user.clear(title);
    await user.type(title, 'Replacement title');
    await user.click(
      screen.getByRole('button', { name: 'Apply metadata policies' })
    );

    await waitFor(() =>
      expect(onMetadataAction).toHaveBeenCalledWith({
        type: 'apply',
        edits: { format: { title: 'Replacement title' } },
        sourceMetadataPolicy: 'copy',
        chapterPolicy: 'keep',
        coverArtPolicy: 'keep',
      })
    );
  });

  it('creates only an allowlisted typed user preset', async () => {
    const user = userEvent.setup();
    const onUserPresetAction = vi.fn();
    render(
      <AdvancedOperations
        availability={capabilities('preset-storage')}
        onUserPresetAction={onUserPresetAction}
      />
    );

    await user.click(screen.getByRole('tab', { name: 'User export presets' }));
    const editor = screen
      .getByRole('heading', { name: 'Create typed preset' })
      .closest('section');
    expect(editor).not.toBeNull();
    const editorQueries = within(editor as HTMLElement);
    await user.type(editorQueries.getByLabelText('Safe ID'), 'my-web-export');
    await user.type(editorQueries.getByLabelText('Name'), 'My web export');
    await user.click(
      editorQueries.getByRole('button', { name: 'Create preset' })
    );

    await waitFor(() =>
      expect(onUserPresetAction).toHaveBeenCalledWith({
        type: 'create',
        preset: {
          schemaVersion: 1,
          id: 'my-web-export',
          name: 'My web export',
          kind: 'video',
          container: 'mp4',
          fileExtension: 'mp4',
          video: {
            codec: 'libx264',
            qualityMode: 'balanced',
            crf: 23,
          },
          audio: {
            codec: 'aac',
            bitrateKbps: 160,
            sampleRate: 48000,
            channels: 2,
          },
          metadataPolicy: 'copy',
          chapterPolicy: 'keep',
          subtitlePolicy: 'none',
          fastStart: true,
        },
      })
    );
    expect(JSON.stringify(onUserPresetAction.mock.calls)).not.toMatch(
      /(?:args|filter|protocol|inputPath)/u
    );
  });

  it('does not offer preset policies that require explicit advanced inputs', async () => {
    const user = userEvent.setup();
    render(
      <AdvancedOperations
        availability={capabilities('preset-storage')}
        onUserPresetAction={vi.fn()}
      />
    );

    await user.click(screen.getByRole('tab', { name: 'User export presets' }));
    const editor = screen
      .getByRole('heading', { name: 'Create typed preset' })
      .closest('section');
    expect(editor).not.toBeNull();
    const editorQueries = within(editor as HTMLElement);

    expect(
      within(editorQueries.getByLabelText('Metadata')).queryByRole('option', {
        name: 'Apply edits',
      })
    ).not.toBeInTheDocument();
    expect(
      within(editorQueries.getByLabelText('Chapters')).queryByRole('option', {
        name: 'Replace',
      })
    ).not.toBeInTheDocument();
    expect(
      within(editorQueries.getByLabelText('Subtitles')).queryByRole('option', {
        name: 'Burn in',
      })
    ).not.toBeInTheDocument();
    expect(
      editorQueries.getByText(/remain explicit operations/iu)
    ).toBeVisible();
  });
});
