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 { WaveformEditor } from '../../../src/components/WaveformEditor';
import { aggregatePcmPeaks } from '../../../src/waveform';

describe('WaveformEditor', () => {
  afterEach(cleanup);

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

  it('renders a bounded canvas with accessible trim and marker controls', async () => {
    render(
      <WaveformEditor
        peaks={createPeaks()}
        inSeconds={1}
        outSeconds={9}
        markers={[3]}
        currentTimeSeconds={5}
        onChange={vi.fn()}
      />
    );

    const canvas = document.querySelector('canvas');
    if (!(canvas instanceof HTMLCanvasElement)) {
      throw new Error('Missing waveform canvas');
    }
    await waitFor(() =>
      expect(canvas).toHaveAttribute(
        'aria-label',
        expect.stringContaining('Selected 00:00:01.000–00:00:09.000')
      )
    );
    expect(
      screen.getByRole('slider', { name: 'In trim handle' })
    ).toHaveAttribute('aria-valuenow', '1');
    expect(
      screen.getByRole('slider', { name: 'Out trim handle' })
    ).toHaveAttribute('aria-valuenow', '9');
    expect(
      screen.getByRole('slider', { name: 'Split marker 1' })
    ).toHaveAttribute('aria-valuenow', '3');
    expect(screen.getByText('Single sequential audio track')).toBeVisible();
  });

  it('emits validated keyboard edits and current-time actions', async () => {
    const user = userEvent.setup();
    const onChange = vi.fn();
    render(
      <WaveformEditor
        peaks={createPeaks()}
        inSeconds={1}
        outSeconds={9}
        markers={[3]}
        currentTimeSeconds={4}
        onChange={onChange}
      />
    );

    fireEvent.keyDown(screen.getByRole('slider', { name: 'In trim handle' }), {
      key: 'ArrowRight',
    });
    expect(onChange).toHaveBeenLastCalledWith({
      inSeconds: 1.1,
      outSeconds: 9,
      markers: [3],
    });

    await user.click(
      screen.getByRole('button', { name: 'Add split at current time' })
    );
    expect(onChange).toHaveBeenLastCalledWith({
      inSeconds: 1,
      outSeconds: 9,
      markers: [3, 4],
    });

    fireEvent.keyDown(screen.getByRole('slider', { name: 'Split marker 1' }), {
      key: 'Delete',
    });
    expect(onChange).toHaveBeenLastCalledWith({
      inSeconds: 1,
      outSeconds: 9,
      markers: [],
    });
  });

  it('drags split markers and seeks using the visible zoomed range', async () => {
    const onChange = vi.fn();
    const onSeek = vi.fn();
    const { container } = render(
      <WaveformEditor
        peaks={createPeaks()}
        inSeconds={0}
        outSeconds={10}
        markers={[2]}
        onChange={onChange}
        onSeek={onSeek}
      />
    );
    const stage = container.querySelector('.waveform-editor__stage');
    if (!(stage instanceof HTMLElement)) {
      throw new Error('Missing waveform stage');
    }
    vi.spyOn(stage, 'getBoundingClientRect').mockReturnValue({
      x: 0,
      y: 0,
      left: 0,
      top: 0,
      right: 100,
      bottom: 168,
      width: 100,
      height: 168,
      toJSON: () => ({}),
    });
    const marker = screen.getByRole('slider', { name: 'Split marker 1' });

    fireEvent.pointerDown(marker, {
      pointerId: 11,
      clientX: 20,
      clientY: 20,
    });
    fireEvent.pointerMove(marker, {
      pointerId: 11,
      clientX: 50,
      clientY: 20,
    });
    fireEvent.pointerUp(marker, { pointerId: 11 });
    expect(onChange).toHaveBeenLastCalledWith({
      inSeconds: 0,
      outSeconds: 10,
      markers: [5],
    });

    fireEvent.pointerDown(stage, {
      button: 0,
      clientX: 75,
      clientY: 30,
    });
    expect(onSeek).toHaveBeenLastCalledWith(7.5);

    fireEvent.change(screen.getByRole('slider', { name: 'Zoom waveform' }), {
      target: { value: '2' },
    });
    expect(
      screen.getByRole('slider', { name: 'Scroll waveform' })
    ).toBeEnabled();
  });

  it('normalizes untrusted selection values and caps marker count', () => {
    render(
      <WaveformEditor
        peaks={createPeaks()}
        inSeconds={Number.NaN}
        outSeconds={999}
        markers={[
          Number.NaN,
          -1,
          2,
          2.0004,
          ...Array.from({ length: 510 }, (_, index) => 2.01 + index * 0.01),
        ]}
        onChange={vi.fn()}
      />
    );

    expect(
      screen.getByText(/Selected 00:00:00.000–00:00:10.000/u)
    ).toBeVisible();
    expect(screen.getByText('254/254')).toBeVisible();
    expect(
      screen.getAllByRole('slider', { name: /Split marker/u })
    ).toHaveLength(254);
  });
});

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;
}
