import {
  cleanup,
  fireEvent,
  render,
  screen,
  waitFor,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { ImportedMediaAsset } from '../../../src/app/application-state';
import {
  MediaPreview,
  type PreviewExportResult,
} from '../../../src/components/MediaPreview';
import { aggregatePcmPeaks } from '../../../src/waveform';

beforeEach(() => {
  vi.spyOn(HTMLMediaElement.prototype, 'canPlayType').mockReturnValue(
    'probably'
  );
});

afterEach(() => {
  cleanup();
  vi.restoreAllMocks();
});

describe('MediaPreview', () => {
  it('starts declared local video playback while inspection continues and preserves the element', () => {
    const probingAsset: ImportedMediaAsset = {
      id: 'asset-probing-video',
      file: new File(['video'], 'lecture.mp4', {
        type: 'application/octet-stream',
      }),
      objectUrl: 'blob:probing-video',
      phase: 'probing',
    };
    const { container, rerender } = render(
      <MediaPreview asset={probingAsset} />
    );

    const progressiveVideo = requiredElement(container.querySelector('video'));
    expect(progressiveVideo).toHaveAttribute('src', probingAsset.objectUrl);
    expect(
      screen.getByText(
        'Local source preview · Media inspection continues in the background.'
      )
    ).toBeVisible();

    rerender(
      <MediaPreview
        asset={{
          ...videoAsset(),
          id: probingAsset.id,
          file: probingAsset.file,
          objectUrl: probingAsset.objectUrl,
        }}
      />
    );

    expect(container.querySelector('video')).toBe(progressiveVideo);
  });

  it('offers bounded configurable proxy settings after playback fails', async () => {
    const user = userEvent.setup();
    const onCreateProxy = vi.fn();
    const onProxySettingsChange = vi.fn();
    const { container } = render(
      <MediaPreview
        asset={videoAsset()}
        onCreateProxy={onCreateProxy}
        proxySettings={{ maxDurationSeconds: 30, maxWidth: 960 }}
        onProxySettingsChange={onProxySettingsChange}
      />
    );

    fireEvent.error(requiredElement(container.querySelector('video')));

    expect(screen.getByText('Create a disposable preview proxy')).toBeVisible();
    fireEvent.change(
      screen.getByRole('spinbutton', { name: 'Maximum seconds' }),
      { target: { value: '60' } }
    );
    expect(onProxySettingsChange).toHaveBeenLastCalledWith({
      maxDurationSeconds: 60,
      maxWidth: 960,
    });
    await user.selectOptions(
      screen.getByRole('combobox', { name: 'Maximum width' }),
      '1280'
    );
    expect(onProxySettingsChange).toHaveBeenLastCalledWith({
      maxDurationSeconds: 30,
      maxWidth: 1280,
    });
    await user.click(screen.getByRole('button', { name: 'Create proxy' }));
    expect(onCreateProxy).toHaveBeenCalledOnce();
  });

  it('seeks a source preview externally and reports its actual playback time', () => {
    const onCurrentTimeChange = vi.fn();
    const { container, rerender } = render(
      <MediaPreview
        asset={videoAsset()}
        currentTimeSeconds={3.25}
        onCurrentTimeChange={onCurrentTimeChange}
      />
    );

    const video = requiredElement(container.querySelector('video'));
    expect(video.currentTime).toBeCloseTo(3.25);

    video.currentTime = 7.75;
    fireEvent.timeUpdate(video);
    expect(onCurrentTimeChange).toHaveBeenLastCalledWith(7.75);

    rerender(
      <MediaPreview
        asset={videoAsset()}
        currentTimeSeconds={1.5}
        onCurrentTimeChange={onCurrentTimeChange}
      />
    );
    expect(video.currentTime).toBeCloseTo(1.5);
  });

  it('uses the result media kind, honors result seeks, and reports result time', () => {
    const onCurrentTimeChange = vi.fn();
    const result: PreviewExportResult = {
      name: 'audio-result.mp3',
      mimeType: 'audio/mpeg',
      objectUrl: 'blob:audio-result',
    };
    const { container, rerender } = render(
      <MediaPreview
        asset={videoAsset()}
        result={result}
        currentTimeSeconds={2}
        onCurrentTimeChange={onCurrentTimeChange}
      />
    );

    expect(container.querySelector('video')).toBeNull();
    const audio = requiredElement(container.querySelector('audio'));
    expect(audio).toHaveAttribute('src', result.objectUrl);
    expect(audio.currentTime).toBeCloseTo(2);

    rerender(
      <MediaPreview
        asset={videoAsset()}
        result={result}
        currentTimeSeconds={5.5}
        onCurrentTimeChange={onCurrentTimeChange}
      />
    );
    expect(audio.currentTime).toBeCloseTo(5.5);

    audio.currentTime = 8.125;
    fireEvent.timeUpdate(audio);
    expect(onCurrentTimeChange).toHaveBeenLastCalledWith(8.125);
  });

  it('uses bounded peaks for an audio-only source and offers an AAC/M4A proxy', async () => {
    vi.mocked(HTMLMediaElement.prototype.canPlayType).mockReturnValue('');
    vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(
      canvasContext()
    );
    const peaks = aggregatePcmPeaks(
      {
        container: 'raw',
        data: new Int16Array([-32_768, 0, 16_384, 32_767]),
        encoding: 's16le',
        sampleRate: 2,
      },
      { bucketCount: 4 }
    );
    const { container } = render(
      <MediaPreview
        asset={audioAsset()}
        waveformPeaks={peaks}
        currentTimeSeconds={1}
        onCreateProxy={vi.fn()}
        proxySettings={{ maxDurationSeconds: 30, maxWidth: 960 }}
      />
    );

    await waitFor(() =>
      expect(container.querySelector('canvas')).toHaveAttribute(
        'aria-label',
        'Bounded waveform preview for source.flac.'
      )
    );
    expect(container.querySelector('.audio-preview__disc')).toBeNull();
    expect(container.querySelector('audio')).toBeInTheDocument();
    expect(screen.getByText(/AAC\/M4A/iu)).toBeVisible();
    expect(
      screen.queryByRole('combobox', { name: 'Maximum width' })
    ).toBeNull();
  });

  it('shows an explicit download-only result instead of mounting unsupported playback', () => {
    vi.mocked(HTMLMediaElement.prototype.canPlayType).mockReturnValue('');
    const result: PreviewExportResult = {
      name: 'archive.flac',
      mimeType: 'audio/flac',
      objectUrl: 'blob:unsupported-result',
    };
    const { container } = render(<MediaPreview result={result} />);

    expect(container.querySelector('audio')).toBeNull();
    expect(
      screen.getByText(
        'Download only: this browser cannot preview this result.'
      )
    ).toBeVisible();
    expect(screen.getByText('Download only · archive.flac')).toBeVisible();
  });
});

function videoAsset(): ImportedMediaAsset {
  return {
    id: 'asset-video',
    file: new File(['video'], 'source.mp4', { type: 'video/mp4' }),
    objectUrl: 'blob:source-video',
    phase: 'ready',
    probe: {
      durationSeconds: 10,
      formatNames: ['mov', 'mp4'],
      tags: {},
      chapters: [],
      warnings: [],
      streams: [
        {
          index: 0,
          type: 'video',
          codecName: 'h264',
          width: 640,
          height: 360,
          disposition: {},
          tags: {},
        },
      ],
    },
  };
}

function audioAsset(): ImportedMediaAsset {
  return {
    id: 'asset-audio',
    file: new File(['audio'], 'source.flac', { type: 'audio/flac' }),
    objectUrl: 'blob:source-audio',
    phase: 'ready',
    probe: {
      durationSeconds: 2,
      formatNames: ['flac'],
      tags: {},
      chapters: [],
      warnings: [],
      streams: [
        {
          index: 0,
          type: 'audio',
          codecName: 'flac',
          sampleRate: 2,
          channels: 1,
          disposition: {},
          tags: {},
        },
      ],
    },
  };
}

function canvasContext(): CanvasRenderingContext2D {
  return {
    setTransform: vi.fn(),
    clearRect: vi.fn(),
    fillRect: vi.fn(),
    beginPath: vi.fn(),
    moveTo: vi.fn(),
    lineTo: vi.fn(),
    stroke: vi.fn(),
    fillStyle: '',
    strokeStyle: '',
    lineWidth: 1,
  } as unknown as CanvasRenderingContext2D;
}

function requiredElement<T extends Element>(element: T | null): T {
  if (!element) {
    throw new Error('Expected media element to be rendered');
  }
  return element;
}
