import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { CropOverlay } from '../../../src/components/CropOverlay';

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

  it('maps a clockwise-oriented preview back to encoded source pixels', () => {
    const onChange = vi.fn();
    const { container } = render(
      <CropOverlay
        sourceWidth={100}
        sourceHeight={80}
        orientation={90}
        value={{ x: 10, y: 20, width: 30, height: 40 }}
        onChange={onChange}
      >
        <img src="blob:local-preview" alt="Local preview" />
      </CropOverlay>
    );

    const rectangle = container.querySelector('.crop-editor__rectangle');
    expect(rectangle).toHaveStyle({
      left: '25%',
      top: '10%',
      width: '50%',
      height: '30%',
    });

    fireEvent.keyDown(
      screen.getByRole('button', { name: 'Move crop rectangle' }),
      { key: 'ArrowRight' }
    );

    expect(onChange).toHaveBeenLastCalledWith({
      x: 10,
      y: 19,
      width: 30,
      height: 40,
    });
  });

  it('supports pointer dragging with bounded percentage-to-pixel math', () => {
    const onChange = vi.fn();
    const { container } = render(
      <CropOverlay
        sourceWidth={200}
        sourceHeight={100}
        value={{ x: 20, y: 10, width: 100, height: 60 }}
        onChange={onChange}
      >
        <div>Preview</div>
      </CropOverlay>
    );
    const stage = container.querySelector('.crop-editor__stage');
    if (!(stage instanceof HTMLElement)) throw new Error('Missing crop stage');
    vi.spyOn(stage, 'getBoundingClientRect').mockReturnValue({
      x: 0,
      y: 0,
      left: 0,
      top: 0,
      right: 200,
      bottom: 100,
      width: 200,
      height: 100,
      toJSON: () => ({}),
    });
    const move = screen.getByRole('button', { name: 'Move crop rectangle' });

    fireEvent.pointerDown(move, {
      pointerId: 7,
      clientX: 10,
      clientY: 10,
    });
    fireEvent.pointerMove(move, {
      pointerId: 7,
      clientX: 60,
      clientY: 30,
    });
    fireEvent.pointerUp(move, { pointerId: 7 });

    expect(onChange).toHaveBeenLastCalledWith({
      x: 70,
      y: 30,
      width: 100,
      height: 60,
    });
  });

  it('locks common display ratios without confusing rotated dimensions', async () => {
    const user = userEvent.setup();
    const onChange = vi.fn();
    render(
      <CropOverlay
        sourceWidth={100}
        sourceHeight={80}
        orientation={90}
        value={{ x: 0, y: 0, width: 100, height: 80 }}
        onChange={onChange}
      >
        <div>Preview</div>
      </CropOverlay>
    );

    await user.selectOptions(
      screen.getByRole('combobox', { name: 'Common ratio' }),
      '16:9'
    );

    const crop = onChange.mock.lastCall?.[0] as
      { width: number; height: number } | undefined;
    expect(crop).toBeDefined();
    expect((crop?.width ?? 0) / (crop?.height ?? 1)).toBeCloseTo(9 / 16, 2);
    expect(
      screen.getByRole('checkbox', { name: 'Lock aspect ratio' })
    ).toBeChecked();
  });

  it('rejects invalid numeric edits and emits even reset bounds when required', async () => {
    const user = userEvent.setup();
    const onChange = vi.fn();
    render(
      <CropOverlay
        sourceWidth={101}
        sourceHeight={99}
        requireEvenDimensions
        value={{ x: 0, y: 0, width: 100, height: 98 }}
        onChange={onChange}
      >
        <div>Preview</div>
      </CropOverlay>
    );

    await user.clear(screen.getByRole('spinbutton', { name: 'Width' }));
    expect(screen.getByRole('alert')).toHaveTextContent(
      'All crop fields are required.'
    );
    expect(onChange).not.toHaveBeenCalled();

    await user.click(screen.getByRole('button', { name: 'Reset crop' }));
    expect(onChange).toHaveBeenLastCalledWith({
      x: 0,
      y: 0,
      width: 100,
      height: 98,
    });
  });
});
