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 type { ImportedMediaAsset } from '../../../src/app/application-state';
import {
  QuickConvert,
  type QuickExportConfiguration,
} from '../../../src/components/QuickConvert';
import type {
  EngineState,
  FFmpegCapabilities,
} from '../../../src/ffmpeg/ffmpeg.types';
import { BUILT_IN_PRESETS, validateUserPreset } from '../../../src/presets';
import type { UserExportPreset } from '../../../src/presets';

afterEach(cleanup);

describe('QuickConvert', () => {
  it('labels a submission honestly when another operation is active', () => {
    render(
      <QuickConvert
        asset={createAsset('asset-a', 'source-a.mp4')}
        engineState={readyEngine()}
        busy={false}
        queueing
        onExport={vi.fn()}
      />
    );

    expect(screen.getByRole('button', { name: 'Add to queue' })).toBeEnabled();
  });

  it('preserves an explicitly empty stream selection and blocks export', async () => {
    const user = userEvent.setup();
    const onExport = vi.fn();
    const asset = createAsset('asset-a', 'source-a.mp4');
    const { rerender } = render(
      <QuickConvert
        asset={asset}
        engineState={readyEngine()}
        busy={false}
        onExport={onExport}
      />
    );

    const videoStream = screen.getByRole('checkbox', {
      name: /video.*#0/iu,
    });
    const audioStream = screen.getByRole('checkbox', {
      name: /audio.*#1/iu,
    });
    await user.click(videoStream);
    await user.click(audioStream);

    expect(videoStream).not.toBeChecked();
    expect(audioStream).not.toBeChecked();
    expect(
      screen.getByText('Select at least one output stream.')
    ).toBeVisible();
    expect(screen.getByRole('button', { name: 'Convert' })).toBeDisabled();

    rerender(
      <QuickConvert
        asset={{ ...asset }}
        engineState={readyEngine()}
        busy={false}
        onExport={onExport}
      />
    );

    expect(
      screen.getByRole('checkbox', { name: /video.*#0/iu })
    ).not.toBeChecked();
    expect(
      screen.getByRole('checkbox', { name: /audio.*#1/iu })
    ).not.toBeChecked();
    expect(onExport).not.toHaveBeenCalled();
  });

  it('starts a newly selected asset with all of its streams selected', async () => {
    const user = userEvent.setup();
    const firstAsset = createAsset('asset-a', 'source-a.mp4');
    const secondAsset = createAsset('asset-b', 'source-b.mp4');
    const { rerender } = render(
      <QuickConvert
        asset={firstAsset}
        engineState={readyEngine()}
        busy={false}
        onExport={vi.fn()}
      />
    );

    await user.click(screen.getByRole('checkbox', { name: /video.*#0/iu }));
    expect(
      screen.getByRole('checkbox', { name: /video.*#0/iu })
    ).not.toBeChecked();

    rerender(
      <QuickConvert
        asset={secondAsset}
        engineState={readyEngine()}
        busy={false}
        onExport={vi.fn()}
      />
    );

    expect(screen.getByText('source-b.mp4')).toBeVisible();
    expect(screen.getByRole('checkbox', { name: /video.*#0/iu })).toBeChecked();
    expect(screen.getByRole('checkbox', { name: /audio.*#1/iu })).toBeChecked();
  });

  it('disables unavailable presets and incompatible remux targets with reasons', async () => {
    const user = userEvent.setup();
    render(
      <QuickConvert
        asset={createAsset('asset-a', 'source-a.mp4')}
        engineState={readyEngine()}
        busy={false}
        onExport={vi.fn()}
      />
    );

    expect(
      screen.getByRole('option', {
        name: /WebM · VP8 \+ Vorbis — unavailable: Missing muxers: webm/iu,
      })
    ).toBeDisabled();

    await user.click(screen.getByRole('button', { name: 'Fast remux' }));
    expect(
      screen.getByText('The loaded core is missing muxer matroska.')
    ).toBeVisible();
    expect(screen.getByRole('button', { name: 'Remux' })).toBeDisabled();
    expect(
      screen.getByRole('option', {
        name: /WebM.*unavailable: missing muxer webm.*video codec h264.*audio codec aac/iu,
      })
    ).toBeDisabled();
  });

  it('enables Opus after the reviewed core passed both regression modes', () => {
    const engineState: EngineState = {
      status: 'ready',
      mode: 'single-thread',
      capabilities: {
        ...capabilities(['mp4', 'opus']),
        encoders: new Set(['libx264', 'aac', 'libopus']),
      },
    };

    render(
      <QuickConvert
        asset={createAsset('asset-a', 'source-a.mp4')}
        engineState={engineState}
        busy={false}
        onExport={vi.fn()}
      />
    );

    expect(screen.getByRole('option', { name: 'Opus' })).toBeEnabled();
  });

  it('exposes a validated user preset and returns it unchanged on export', async () => {
    const user = userEvent.setup();
    const onExport = vi.fn<(configuration: QuickExportConfiguration) => void>();
    const userPreset = validatedUserPreset();

    render(
      <QuickConvert
        asset={createAsset('asset-a', 'source-a.mp4')}
        engineState={readyEngine()}
        busy={false}
        presets={[...BUILT_IN_PRESETS, userPreset]}
        onExport={onExport}
      />
    );

    const presetSelect = screen.getByRole('combobox', {
      name: /^Export preset/iu,
    });
    expect(
      screen.getByRole('option', { name: 'My local H.264 export' })
    ).toBeInTheDocument();

    await user.selectOptions(presetSelect, userPreset.id);
    expect(
      screen.getByText('A validated user-created local export preset.')
    ).toBeVisible();
    await user.click(screen.getByRole('button', { name: 'Convert' }));

    await waitFor(() => expect(onExport).toHaveBeenCalledOnce());
    expect(onExport).toHaveBeenCalledWith({
      operation: 'convert',
      preset: userPreset,
      remuxContainer: 'matroska',
      streamSelection: {
        video: [0],
        audio: [1],
        subtitles: [],
        attachments: [],
        data: [],
      },
      removeMetadata: false,
      removeChapters: false,
    });
  });

  it('disables unsupported auxiliary streams for conversion and maps them for Matroska remux', async () => {
    const user = userEvent.setup();
    const onExport = vi.fn<(configuration: QuickExportConfiguration) => void>();
    const asset = createAsset('asset-a', 'source-a.mkv');
    asset.probe?.streams.push(
      {
        index: 2,
        type: 'attachment',
        codecName: 'ttf',
        disposition: {},
        tags: {},
      },
      {
        index: 3,
        type: 'data',
        codecName: 'bin_data',
        disposition: {},
        tags: {},
      }
    );

    render(
      <QuickConvert
        asset={asset}
        engineState={readyEngine(['mp4', 'matroska'])}
        busy={false}
        onExport={onExport}
      />
    );

    const attachment = screen.getByRole('checkbox', {
      name: /attachment.*#2/iu,
    });
    const data = screen.getByRole('checkbox', { name: /data.*#3/iu });
    expect(attachment).toBeDisabled();
    expect(attachment).not.toBeChecked();
    expect(data).toBeDisabled();
    expect(data).not.toBeChecked();
    expect(
      screen.getByText(/attachment streams are only preserved.*Matroska/iu)
    ).toBeVisible();
    expect(
      screen.getByText(/data streams are only preserved.*Matroska/iu)
    ).toBeVisible();

    await user.click(screen.getByRole('button', { name: 'Fast remux' }));
    expect(attachment).toBeEnabled();
    expect(attachment).toBeChecked();
    expect(data).toBeEnabled();
    expect(data).toBeChecked();
    await user.click(screen.getByRole('button', { name: 'Remux' }));

    await waitFor(() => expect(onExport).toHaveBeenCalledOnce());
    expect(onExport.mock.calls[0]?.[0].streamSelection).toEqual({
      video: [0],
      audio: [1],
      subtitles: [],
      attachments: [2],
      data: [3],
    });
  });
});

function createAsset(id: string, name: string): ImportedMediaAsset {
  return {
    id,
    file: new File(['media'], name, { type: 'video/mp4' }),
    objectUrl: `blob:${id}`,
    phase: 'ready',
    probe: {
      durationSeconds: 12,
      formatNames: ['mov', 'mp4'],
      tags: {},
      chapters: [],
      warnings: [],
      streams: [
        {
          index: 0,
          type: 'video',
          codecName: 'h264',
          width: 640,
          height: 360,
          disposition: {},
          tags: {},
        },
        {
          index: 1,
          type: 'audio',
          codecName: 'aac',
          sampleRate: 48_000,
          channels: 2,
          disposition: {},
          tags: {},
        },
      ],
    },
  };
}

function readyEngine(muxers: readonly string[] = ['mp4']): EngineState {
  return {
    status: 'ready',
    mode: 'single-thread',
    capabilities: capabilities(muxers),
  };
}

function capabilities(muxers: readonly string[] = ['mp4']): FFmpegCapabilities {
  return {
    versionText: 'test',
    buildConfiguration: [],
    demuxers: new Set(['mov']),
    muxers: new Set(muxers),
    decoders: new Set(['h264', 'aac']),
    encoders: new Set(['libx264', 'aac']),
    filters: new Set(),
  };
}

function validatedUserPreset(): UserExportPreset {
  const candidate: UserExportPreset = {
    schemaVersion: 1,
    id: 'my-local-h264',
    name: 'My local H.264 export',
    kind: 'video',
    container: 'mp4',
    fileExtension: 'mp4',
    video: {
      codec: 'libx264',
      qualityMode: 'balanced',
      crf: 22,
      pixelFormat: 'yuv420p',
    },
    audio: {
      codec: 'aac',
      bitrateKbps: 160,
      sampleRate: 48_000,
      channels: 2,
    },
    metadataPolicy: 'copy',
    chapterPolicy: 'keep',
    subtitlePolicy: 'none',
    fastStart: true,
  };
  const result = validateUserPreset(candidate);
  if (!result.valid || !result.preset) {
    throw new Error(`Test preset is invalid: ${result.errors.join('; ')}`);
  }
  return result.preset;
}
