import { cleanup, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
  StructuralOperations,
  type StructuralCapabilityKey,
  type StructuralCapabilityStatus,
  type StructuralOperationsProps,
} from '../../../src/components/StructuralOperations';

const AVAILABLE: StructuralCapabilityStatus = { available: true };

const SELECTED_CLIP: NonNullable<StructuralOperationsProps['selectedClip']> = {
  id: 'clip-a',
  label: 'Opening shot',
  sourceInSeconds: 5.25,
  sourceOutSeconds: 18.75,
  sourceDurationSeconds: 60,
  sourceExtension: '.MP4',
  hasAudio: true,
  hasVideo: true,
};

const TIMELINE: NonNullable<StructuralOperationsProps['timeline']> = {
  clips: [
    {
      id: 'clip-a',
      label: 'Opening shot',
      hasAudio: true,
      hasVideo: true,
      hasSubtitles: false,
    },
    {
      id: 'clip-b',
      label: 'Silent cutaway',
      hasAudio: false,
      hasVideo: true,
      hasSubtitles: false,
    },
  ],
  fastTargetExtension: 'mp4',
  fastTargetMuxer: 'mp4',
  fastCompatibilityDiagnostics: [],
};

const PRESETS: NonNullable<StructuralOperationsProps['exportPresets']> = [
  {
    id: 'video-ready',
    name: 'MP4 · H.264 + AAC',
    fileExtension: 'mp4',
    supportsAudio: true,
    supportsVideo: true,
  },
  {
    id: 'video-disabled',
    name: 'WebM · VP9 + Opus',
    fileExtension: 'webm',
    supportsAudio: true,
    supportsVideo: true,
    disabledReason: 'The Opus encoder is not available.',
  },
  {
    id: 'audio-only',
    name: 'MP3 audio',
    fileExtension: 'mp3',
    supportsAudio: true,
    supportsVideo: false,
  },
];

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

afterEach(cleanup);

describe('StructuralOperations', () => {
  it('uses the selected clip range for a fast stream-copy trim', async () => {
    const user = userEvent.setup();
    const onTrim = vi.fn();
    render(
      <StructuralOperations
        selectedClip={SELECTED_CLIP}
        availability={capabilities('trim-fast')}
        onTrim={onTrim}
      />
    );

    expect(screen.getByText('00:00:05.250–00:00:18.750')).toBeInTheDocument();
    expect(
      screen.getByText(/first frame may move to a nearby keyframe/iu)
    ).toBeInTheDocument();

    await user.click(screen.getByRole('button', { name: 'Export fast trim' }));

    await waitFor(() =>
      expect(onTrim).toHaveBeenCalledWith({
        operation: 'trim',
        mode: 'fast',
        clipId: 'clip-a',
        startSeconds: 5.25,
        endSeconds: 18.75,
        targetExtension: 'mp4',
      })
    );
  });

  it('requires and emits an available typed preset for an accurate trim', async () => {
    const user = userEvent.setup();
    const onTrim = vi.fn();
    render(
      <StructuralOperations
        selectedClip={SELECTED_CLIP}
        exportPresets={PRESETS}
        availability={capabilities('trim-accurate')}
        onTrim={onTrim}
      />
    );

    await user.click(
      screen.getByRole('radio', { name: /Accurate re-encode/iu })
    );
    const action = screen.getByRole('button', { name: 'Export exact trim' });
    expect(action).toBeDisabled();
    expect(
      screen.getByText(
        'Choose an available typed preset for the accurate trim.'
      )
    ).toBeInTheDocument();

    const preset = screen.getByLabelText('Accurate trim preset');
    expect(
      screen.getByRole('option', { name: /WebM · VP9 \+ Opus/iu })
    ).toBeDisabled();
    expect(screen.getByRole('option', { name: /MP3 audio/iu })).toBeDisabled();
    await user.selectOptions(preset, 'video-ready');
    await user.click(action);

    await waitFor(() =>
      expect(onTrim).toHaveBeenCalledWith({
        operation: 'trim',
        mode: 'accurate',
        clipId: 'clip-a',
        startSeconds: 5.25,
        endSeconds: 18.75,
        presetId: 'video-ready',
      })
    );
  });

  it('fails closed when a capability was not explicitly verified', () => {
    render(
      <StructuralOperations selectedClip={SELECTED_CLIP} onTrim={vi.fn()} />
    );

    expect(
      screen.getByRole('button', { name: 'Export fast trim' })
    ).toBeDisabled();
    expect(
      screen.getByText(
        'The required browser and FFmpeg capabilities have not been verified.'
      )
    ).toBeInTheDocument();
  });

  it('shows concrete incompatibilities and blocks fast concat', () => {
    render(
      <StructuralOperations
        timeline={{
          ...TIMELINE,
          fastCompatibilityDiagnostics: [
            {
              code: 'stream-property',
              severity: 'error',
              message: 'Clip 2 has incompatible width: 1280 instead of 1920.',
              path: 'sources.1.streams.0.width',
            },
          ],
        }}
        availability={capabilities('concat-fast')}
        onConcat={vi.fn()}
      />
    );

    expect(
      screen.getByRole('heading', { name: 'Fast compatibility failed' })
    ).toBeInTheDocument();
    expect(
      screen.getByText('Clip 2 has incompatible width: 1280 instead of 1920.')
    ).toBeInTheDocument();
    expect(
      screen.getByText('Checked field: sources.1.streams.0.width')
    ).toBeInTheDocument();
    expect(
      screen.getByRole('button', { name: 'Export fast concat' })
    ).toBeDisabled();
  });

  it('emits the verified sequential clip order for fast concat', async () => {
    const user = userEvent.setup();
    const onConcat = vi.fn();
    render(
      <StructuralOperations
        timeline={TIMELINE}
        availability={capabilities('concat-fast')}
        onConcat={onConcat}
      />
    );

    expect(
      screen.getByRole('heading', { name: 'Fast compatibility passed' })
    ).toBeInTheDocument();
    await user.click(
      screen.getByRole('button', { name: 'Export fast concat' })
    );

    await waitFor(() =>
      expect(onConcat).toHaveBeenCalledWith({
        operation: 'concat',
        mode: 'fast',
        clipIds: ['clip-a', 'clip-b'],
        targetExtension: 'mp4',
        targetMuxer: 'mp4',
      })
    );
  });

  it('requires an explicit missing-audio policy for normalized concat', async () => {
    const user = userEvent.setup();
    const onConcat = vi.fn();
    render(
      <StructuralOperations
        timeline={TIMELINE}
        exportPresets={PRESETS}
        availability={capabilities('concat-normalized')}
        onConcat={onConcat}
      />
    );

    expect(screen.getByText('No audio: Silent cutaway')).toBeInTheDocument();
    await user.click(
      screen.getByRole('radio', { name: /Normalized re-encode/iu })
    );
    await user.selectOptions(
      screen.getByLabelText('Normalized concat preset'),
      'video-ready'
    );

    const action = screen.getByRole('button', {
      name: 'Export normalized concat',
    });
    expect(action).toBeDisabled();
    expect(
      screen.getByText('Choose an explicit missing-audio policy.')
    ).toBeInTheDocument();

    await user.click(screen.getByRole('radio', { name: /Insert silence/iu }));
    expect(action).toBeDisabled();
    expect(
      screen.getByText('Choose an explicit subtitle policy.')
    ).toBeInTheDocument();
    await user.click(
      screen.getByRole('radio', { name: /Reject subtitle input/iu })
    );
    await user.click(action);

    await waitFor(() =>
      expect(onConcat).toHaveBeenCalledWith({
        operation: 'concat',
        mode: 'normalized',
        clipIds: ['clip-a', 'clip-b'],
        presetId: 'video-ready',
        missingAudioPolicy: 'insert-silence',
        subtitlePolicy: 'reject',
        crossfadeSeconds: 0,
      })
    );
  });

  it('requires destructive subtitle loss to be explicit for normalized concat', async () => {
    const user = userEvent.setup();
    const onConcat = vi.fn();
    render(
      <StructuralOperations
        timeline={{
          ...TIMELINE,
          clips: TIMELINE.clips.map((clip, index) =>
            index === 0 ? { ...clip, hasSubtitles: true } : clip
          ),
        }}
        exportPresets={PRESETS}
        availability={capabilities('concat-normalized')}
        onConcat={onConcat}
      />
    );

    await user.click(
      screen.getByRole('radio', { name: /Normalized re-encode/iu })
    );
    await user.selectOptions(
      screen.getByLabelText('Normalized concat preset'),
      'video-ready'
    );
    await user.click(screen.getByRole('radio', { name: /Insert silence/iu }));
    await user.click(
      screen.getByRole('radio', { name: /Reject subtitle input/iu })
    );

    const action = screen.getByRole('button', {
      name: 'Export normalized concat',
    });
    expect(action).toBeDisabled();
    expect(
      screen.getByText(/sequence contains subtitles.*explicitly drop all/iu)
    ).toBeVisible();

    await user.click(
      screen.getByRole('radio', { name: /Drop all subtitles/iu })
    );
    await user.click(action);

    await waitFor(() =>
      expect(onConcat).toHaveBeenCalledWith({
        operation: 'concat',
        mode: 'normalized',
        clipIds: ['clip-a', 'clip-b'],
        presetId: 'video-ready',
        missingAudioPolicy: 'insert-silence',
        subtitlePolicy: 'drop-all',
        crossfadeSeconds: 0,
      })
    );
  });

  it('emits a bounded crossfade only after its filters are verified', async () => {
    const user = userEvent.setup();
    const onConcat = vi.fn();
    render(
      <StructuralOperations
        timeline={{
          ...TIMELINE,
          clips: TIMELINE.clips.map((clip) => ({
            ...clip,
            durationSeconds: 3,
          })),
        }}
        exportPresets={PRESETS}
        availability={capabilities('concat-normalized', 'concat-crossfade')}
        onConcat={onConcat}
      />
    );

    await user.click(
      screen.getByRole('radio', { name: /Normalized re-encode/iu })
    );
    await user.selectOptions(
      screen.getByLabelText('Normalized concat preset'),
      'video-ready'
    );
    await user.click(screen.getByRole('radio', { name: /Insert silence/iu }));
    await user.click(
      screen.getByRole('radio', { name: /Reject subtitle input/iu })
    );
    await user.clear(screen.getByLabelText(/^Crossfade duration/iu));
    await user.type(screen.getByLabelText(/^Crossfade duration/iu), '0.5');
    await user.click(
      screen.getByRole('button', { name: 'Export normalized concat' })
    );

    await waitFor(() =>
      expect(onConcat).toHaveBeenCalledWith({
        operation: 'concat',
        mode: 'normalized',
        clipIds: ['clip-a', 'clip-b'],
        presetId: 'video-ready',
        missingAudioPolicy: 'insert-silence',
        subtitlePolicy: 'reject',
        crossfadeSeconds: 0.5,
      })
    );
  });

  it('treats an absent fast compatibility result as unchecked', () => {
    render(
      <StructuralOperations
        timeline={{ ...TIMELINE, fastCompatibilityDiagnostics: undefined }}
        availability={capabilities('concat-fast')}
        onConcat={vi.fn()}
      />
    );

    expect(
      screen.getByRole('heading', { name: 'Fast compatibility not checked' })
    ).toBeInTheDocument();
    expect(
      screen.getByText(
        'Fast concat remains disabled until every input stream is checked.'
      )
    ).toBeInTheDocument();
  });

  it('surfaces a synchronous runner rejection and releases pending state', async () => {
    const user = userEvent.setup();
    render(
      <StructuralOperations
        selectedClip={SELECTED_CLIP}
        availability={capabilities('trim-fast')}
        onTrim={() => {
          throw new Error('Queue capacity was reached.');
        }}
      />
    );

    const action = screen.getByRole('button', { name: 'Export fast trim' });
    await user.click(action);

    expect(
      await screen.findByText(
        'The operation could not be queued: Queue capacity was reached.'
      )
    ).toBeInTheDocument();
    await waitFor(() => expect(action).toBeEnabled());
  });
});
