// @vitest-environment jsdom
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';

vi.mock('../../lib/scanner-card-detection.js', () => ({
  detectCardShapesFromFrame: vi.fn(() => []),
  DETECTION_START_DELAY_MS: 10_000,
  mergeDetectedShapesIntoTrackedCards: vi.fn(() => ({ cards: [], nextCardId: 1 })),
  selectCardsReadyForVerification: vi.fn(() => []),
  SHAPE_DETECTION_INTERVAL_MS: 10_000,
  VERIFICATION_INTERVAL_MS: 10_000,
}));

import { useCameraScanner } from '../../lib/use-camera-scanner.js';

const SESSION_DEVICE_KEY = 'scanner:last-camera-device-id';

function createMockStream() {
  const track = { stop: vi.fn() };
  return {
    getTracks: () => [track],
  };
}

function attachMockVideo(hookResult) {
  const video = document.createElement('video');
  video.play = vi.fn().mockResolvedValue(undefined);
  Object.defineProperty(video, 'readyState', { value: 2, configurable: true });
  hookResult.videoRef.current = video;
  return video;
}

async function startCameraWithMetadata(hookResult) {
  const video = attachMockVideo(hookResult);
  await act(async () => {
    await hookResult.startCamera();
  });
  await act(async () => {
    video.onloadedmetadata?.();
    await Promise.resolve();
  });
}

describe('useCameraScanner — device picker', () => {
  let getUserMedia;
  let enumerateDevices;

  beforeEach(() => {
    sessionStorage.clear();
    getUserMedia = vi.fn().mockResolvedValue(createMockStream());
    enumerateDevices = vi.fn().mockResolvedValue([
      {
        deviceId: 'cam-a',
        kind: 'videoinput',
        label: 'Desk Webcam',
      },
      {
        deviceId: 'cam-b',
        kind: 'videoinput',
        label: 'USB Camera',
      },
      {
        deviceId: 'mic-1',
        kind: 'audioinput',
        label: 'Built-in Mic',
      },
    ]);

    Object.defineProperty(global.navigator, 'mediaDevices', {
      configurable: true,
      value: {
        getUserMedia,
        enumerateDevices,
      },
    });
  });

  afterEach(() => {
    vi.clearAllMocks();
    sessionStorage.clear();
  });

  it('starts with loading picker status and message', () => {
    const { result } = renderHook(() => useCameraScanner({ onError: vi.fn() }));

    expect(result.current.devicePickerStatus).toBe('loading');
    expect(result.current.devicePickerMessage).toBe('Detecting cameras…');
  });

  it('enumerates video inputs after stream start', async () => {
    const { result } = renderHook(() => useCameraScanner({ onError: vi.fn() }));

    await startCameraWithMetadata(result.current);

    expect(enumerateDevices).toHaveBeenCalled();
    expect(result.current.videoDevices).toEqual([
      {
        deviceId: 'cam-a',
        kind: 'videoinput',
        label: 'Desk Webcam',
      },
      {
        deviceId: 'cam-b',
        kind: 'videoinput',
        label: 'USB Camera',
      },
    ]);
    expect(result.current.devicePickerStatus).toBe('ready');
    expect(result.current.devicePickerMessage).toBe('');
  });

  it('uses deviceId exact constraints when selectedDeviceId is set', async () => {
    sessionStorage.setItem(SESSION_DEVICE_KEY, 'cam-b');

    const { result } = renderHook(() => useCameraScanner({ onError: vi.fn() }));

    expect(result.current.selectedDeviceId).toBe('cam-b');

    await startCameraWithMetadata(result.current);

    expect(getUserMedia).toHaveBeenCalledWith({
      video: {
        deviceId: { exact: 'cam-b' },
        width: { ideal: 1280 },
        height: { ideal: 720 },
      },
    });
  });

  it('uses facingMode constraints when no selectedDeviceId is set', async () => {
    const { result } = renderHook(() =>
      useCameraScanner({ onError: vi.fn(), facingMode: 'user' })
    );

    await startCameraWithMetadata(result.current);

    expect(getUserMedia).toHaveBeenCalledWith({
      video: {
        facingMode: 'user',
        width: { ideal: 1280 },
        height: { ideal: 720 },
        aspectRatio: { ideal: 16 / 9 },
      },
    });
  });

  it('persists selectedDeviceId to sessionStorage', async () => {
    const { result } = renderHook(() => useCameraScanner({ onError: vi.fn() }));

    await act(async () => {
      result.current.setSelectedDeviceId('cam-a');
    });

    expect(result.current.selectedDeviceId).toBe('cam-a');
    expect(sessionStorage.getItem(SESSION_DEVICE_KEY)).toBe('cam-a');
  });

  it('restarts the stream when selectedDeviceId changes while streaming', async () => {
    const { result } = renderHook(() => useCameraScanner({ onError: vi.fn() }));

    await startCameraWithMetadata(result.current);
    getUserMedia.mockClear();

    await act(async () => {
      result.current.setSelectedDeviceId('cam-b');
    });

    expect(getUserMedia).toHaveBeenCalledWith({
      video: {
        deviceId: { exact: 'cam-b' },
        width: { ideal: 1280 },
        height: { ideal: 720 },
      },
    });
  });

  it('switchFacingMode still toggles facingMode when no deviceId is forced', async () => {
    const { result } = renderHook(() =>
      useCameraScanner({ onError: vi.fn(), facingMode: 'environment' })
    );

    await startCameraWithMetadata(result.current);
    getUserMedia.mockClear();

    await act(async () => {
      result.current.switchFacingMode();
    });

    expect(result.current.facingMode).toBe('user');
    expect(getUserMedia).toHaveBeenCalledWith({
      video: {
        facingMode: 'user',
        width: { ideal: 1280 },
        height: { ideal: 720 },
        aspectRatio: { ideal: 16 / 9 },
      },
    });
  });

  it('reports empty picker status when no video inputs are found', async () => {
    enumerateDevices.mockResolvedValue([
      { deviceId: 'mic-1', kind: 'audioinput', label: 'Mic' },
    ]);

    const { result } = renderHook(() => useCameraScanner({ onError: vi.fn() }));

    await startCameraWithMetadata(result.current);

    expect(result.current.devicePickerStatus).toBe('empty');
    expect(result.current.devicePickerMessage).toBe(
      'No camera found. Connect a webcam or use Upload Image.'
    );
    expect(result.current.videoDevices).toEqual([]);
  });

  it('reports denied picker status when getUserMedia throws NotAllowedError', async () => {
    const deniedError = new Error('Permission denied');
    deniedError.name = 'NotAllowedError';
    getUserMedia.mockRejectedValueOnce(deniedError);

    const onError = vi.fn();
    const { result } = renderHook(() => useCameraScanner({ onError }));

    await act(async () => {
      attachMockVideo(result.current);
      await result.current.startCamera();
    });

    expect(result.current.devicePickerStatus).toBe('denied');
    expect(result.current.devicePickerMessage).toBe(
      'Camera access blocked. Allow camera permission or use Upload Image.'
    );
    expect(onError).toHaveBeenCalled();
  });

  it('reports error picker status when enumerateDevices is unavailable', async () => {
    Object.defineProperty(global.navigator, 'mediaDevices', {
      configurable: true,
      value: {
        getUserMedia,
      },
    });

    const { result } = renderHook(() => useCameraScanner({ onError: vi.fn() }));

    await startCameraWithMetadata(result.current);

    expect(result.current.devicePickerStatus).toBe('error');
    expect(result.current.devicePickerMessage).toBe(
      "Couldn't list cameras. Try again or use Upload Image."
    );
  });

  it('falls back to the first device when persisted id is missing', async () => {
    sessionStorage.setItem(SESSION_DEVICE_KEY, 'missing-device');

    const { result } = renderHook(() => useCameraScanner({ onError: vi.fn() }));

    await startCameraWithMetadata(result.current);

    expect(result.current.selectedDeviceId).toBe('cam-a');
    expect(sessionStorage.getItem(SESSION_DEVICE_KEY)).toBe('cam-a');
  });
});
