import { cleanup, fireEvent, render, screen } 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 { Inspector } from '../../../src/components/Inspector';
import { Timeline } from '../../../src/components/Timeline';
import type { AvProjectV1, TimelineClip } from '../../../src/project';
import { aggregatePcmPeaks } from '../../../src/waveform';

describe('visual editor integration', () => {
  beforeEach(() => {
    vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(
      createCanvasContext()
    );
  });

  afterEach(cleanup);

  it('shows encoded dimensions separately from normalized display metadata', () => {
    const project = createProject();

    render(
      <Inspector
        asset={createImportedAsset()}
        project={project}
        clip={project.timeline[0]}
        onClipUpdate={vi.fn()}
        onMetadataChange={vi.fn()}
        onChaptersChange={vi.fn()}
        onSubtitlesChange={vi.fn()}
        onThumbnail={vi.fn()}
      />
    );

    expect(
      screen.getByText('Encoded pixels 101×99 · coded 102×100 · 25.000 fps')
    ).toBeVisible();
    expect(
      screen.getByText('SAR 4:3 · DAR 16:9 · rotation -90°')
    ).toBeVisible();
  });

  it('uses the local video object URL in the rotation-aware crop editor', async () => {
    const user = userEvent.setup();
    const onClipUpdate = vi.fn();
    const project = createProject();
    const clip = {
      ...project.timeline[0]!,
      crop: { x: 0, y: 0, width: 100, height: 98 },
      rotation: 90 as const,
    };
    const asset = createImportedAsset();

    render(
      <Inspector
        asset={asset}
        project={project}
        clip={clip}
        disabled
        onClipUpdate={onClipUpdate}
        onMetadataChange={vi.fn()}
        onChaptersChange={vi.fn()}
        onSubtitlesChange={vi.fn()}
        onThumbnail={vi.fn()}
      />
    );

    await user.click(screen.getByRole('tab', { name: 'Transform' }));

    const cropEditor = screen.getByRole('region', {
      name: 'Crop local-video.mp4',
    });
    expect(cropEditor).toHaveAttribute('data-orientation', '90');
    expect(
      screen.getByLabelText('Local preview of local-video.mp4')
    ).toHaveAttribute('src', 'blob:local-video');
    expect(
      screen.getByRole('button', { name: 'Move crop rectangle' })
    ).toBeDisabled();
    expect(
      screen.getByRole('group', { name: 'Encoded source pixels' })
    ).toBeVisible();
  });

  it('keeps numeric crop inputs when a visual video source is unavailable', async () => {
    const user = userEvent.setup();
    const onClipUpdate = vi.fn();
    const project = createProject();

    render(
      <Inspector
        project={project}
        clip={project.timeline[0]}
        onClipUpdate={onClipUpdate}
        onMetadataChange={vi.fn()}
        onChaptersChange={vi.fn()}
        onSubtitlesChange={vi.fn()}
        onThumbnail={vi.fn()}
      />
    );

    await user.click(screen.getByRole('tab', { name: 'Transform' }));
    expect(
      screen.getByText(/visual crop preview is unavailable/i)
    ).toBeVisible();

    fireEvent.change(screen.getByRole('spinbutton', { name: 'WIDTH' }), {
      target: { value: '101' },
    });
    expect(onClipUpdate).toHaveBeenLastCalledWith({
      crop: { x: 0, y: 0, width: 100, height: 2 },
    });
  });

  it('edits allowlisted fade curves and persists chapter time bases', async () => {
    const user = userEvent.setup();
    const onClipUpdate = vi.fn();
    const onChaptersChange = vi.fn();
    const baseProject = createProject();
    const project: AvProjectV1 = {
      ...baseProject,
      chapters: [
        {
          id: 'chapter-1',
          title: 'Intro',
          startSeconds: 0,
          endSeconds: 1,
          timeBase: '1/1000',
        },
      ],
    };

    render(
      <Inspector
        project={project}
        clip={project.timeline[0]}
        onClipUpdate={onClipUpdate}
        onMetadataChange={vi.fn()}
        onChaptersChange={onChaptersChange}
        onSubtitlesChange={vi.fn()}
        onThumbnail={vi.fn()}
      />
    );

    await user.click(screen.getByRole('tab', { name: 'Transform' }));
    await user.selectOptions(
      screen.getByRole('combobox', { name: 'Video fade curve' }),
      'exp'
    );
    expect(onClipUpdate).toHaveBeenLastCalledWith({
      video: { enabled: true, fadeCurve: 'exp' },
    });

    await user.click(screen.getByRole('tab', { name: 'Audio' }));
    await user.selectOptions(
      screen.getByRole('combobox', { name: 'Audio fade curve' }),
      'qsin'
    );
    expect(onClipUpdate).toHaveBeenLastCalledWith({
      audio: { enabled: true, fadeCurve: 'qsin' },
    });

    await user.click(screen.getByRole('tab', { name: 'Chapters' }));
    fireEvent.change(screen.getByLabelText('Chapter 1 time base'), {
      target: { value: '1/48000' },
    });
    expect(onChaptersChange).toHaveBeenLastCalledWith([
      expect.objectContaining({ id: 'chapter-1', timeBase: '1/48000' }),
    ]);
  });

  it('updates the selected clip and forwards validated waveform selections', () => {
    const project = createProject();
    const onUpdate = vi.fn();
    const onSelectionChange = vi.fn();

    render(
      <Timeline
        project={project}
        selectedClipId="clip-1"
        onSelect={vi.fn()}
        onMove={vi.fn()}
        onUpdate={onUpdate}
        onRemove={vi.fn()}
        waveformPeaks={createPeaks()}
        currentTimeSeconds={4}
        splitMarkers={[3]}
        onSelectionChange={onSelectionChange}
      />
    );

    expect(screen.getByText('local-video.mp4')).toBeVisible();
    expect(screen.getByText('Single sequential audio track')).toBeVisible();

    fireEvent.keyDown(screen.getByRole('slider', { name: 'In trim handle' }), {
      key: 'ArrowRight',
    });

    expect(onUpdate).toHaveBeenLastCalledWith('clip-1', {
      sourceInSeconds: 1.1,
      sourceOutSeconds: 9,
    });
    expect(onSelectionChange).toHaveBeenLastCalledWith({
      inSeconds: 1.1,
      outSeconds: 9,
      markers: [3],
    });
  });

  it('preserves the original clip-card timeline when waveform data is omitted', () => {
    const project = createProject();

    render(
      <Timeline
        project={project}
        selectedClipId="clip-1"
        onSelect={vi.fn()}
        onMove={vi.fn()}
        onUpdate={vi.fn()}
        onRemove={vi.fn()}
      />
    );

    expect(screen.getByText('local-video.mp4')).toBeVisible();
    expect(
      screen.queryByText('Single sequential audio track')
    ).not.toBeInTheDocument();
    expect(screen.getByRole('spinbutton', { name: 'In' })).toHaveValue(1);
    expect(screen.getByRole('spinbutton', { name: 'Out' })).toHaveValue(9);
  });

  it('resets the selected clip trim to its full probed source duration', async () => {
    const user = userEvent.setup();
    const project = createProject();
    const onUpdate = vi.fn();

    render(
      <Timeline
        project={project}
        selectedClipId="clip-1"
        onSelect={vi.fn()}
        onMove={vi.fn()}
        onUpdate={onUpdate}
        onRemove={vi.fn()}
      />
    );

    await user.click(
      screen.getByRole('button', { name: 'Reset trim to full source' })
    );

    expect(onUpdate).toHaveBeenCalledWith('clip-1', {
      sourceInSeconds: 0,
      sourceOutSeconds: 10,
    });
  });
});

function createProject(): AvProjectV1 {
  const clip: TimelineClip = {
    id: 'clip-1',
    assetId: 'asset-1',
    sourceInSeconds: 1,
    sourceOutSeconds: 9,
  };
  return {
    schemaVersion: 1,
    id: 'project-1',
    name: 'Test project',
    createdAt: '2026-07-24T12:00:00.000Z',
    updatedAt: '2026-07-24T12:00:00.000Z',
    assets: [
      {
        id: 'asset-1',
        originalName: 'local-video.mp4',
        size: 4,
        lastModified: 0,
        mimeType: 'video/mp4',
        sourceStatus: 'attached',
        probe: createProbe(),
      },
    ],
    timeline: [clip],
    output: {
      presetId: 'video-h264',
      fileName: 'output.mp4',
      container: 'mp4',
      videoEnabled: true,
      audioEnabled: true,
    },
    metadata: { custom: {} },
    chapters: [],
    subtitles: [],
  };
}

function createImportedAsset(): ImportedMediaAsset {
  return {
    id: 'asset-1',
    file: new File(['test'], 'local-video.mp4', { type: 'video/mp4' }),
    objectUrl: 'blob:local-video',
    phase: 'ready',
    probe: createProbe(),
  };
}

function createProbe() {
  return {
    durationSeconds: 10,
    formatNames: ['mov', 'mp4'],
    tags: {},
    chapters: [],
    warnings: [],
    streams: [
      {
        index: 0,
        type: 'video' as const,
        codecName: 'h264',
        width: 101,
        height: 99,
        codedWidth: 102,
        codedHeight: 100,
        sampleAspectRatio: '4:3',
        displayAspectRatio: '16:9',
        rotationDegrees: -90,
        frameRate: 25,
        disposition: {},
        tags: {},
      },
    ],
  };
}

function createPeaks() {
  return aggregatePcmPeaks(
    {
      container: 'raw',
      data: new Int16Array(
        Array.from({ length: 100 }, (_, index) =>
          Math.round(Math.sin(index / 5) * 20_000)
        )
      ),
      encoding: 's16le',
      sampleRate: 10,
    },
    { bucketCount: 100 }
  );
}

function createCanvasContext(): 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;
}
