import { Call } from '../../Call';
import { StreamClient } from '../../coordinator/connection/client';
import { CallingState, StreamVideoWriteableStateStore } from '../../store';

import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest';
import { fromPartial } from '@total-typescript/shoehorn';
import {
  createLocalStorageMock,
  emitDeviceIds,
  mockBrowserPermission,
  mockCall,
  mockDeviceIds$,
  mockVideoDevices,
  mockVideoStream,
} from './mocks';
import { createVideoStreamForDevice } from './mediaStreamTestHelpers';
import { TrackType } from '../../gen/video/sfu/models/models';
import { CameraManager } from '../CameraManager';
import { of } from 'rxjs';
import { PermissionsContext } from '../../permissions';
import { Tracer } from '../../stats';
import {
  defaultDeviceId,
  readPreferences,
  toPreferenceList,
} from '../devicePersistence';

const getVideoStream = vi.hoisted(() =>
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  vi.fn((_trackConstraints?: MediaTrackConstraints, _tracer?: Tracer) =>
    Promise.resolve(mockVideoStream()),
  ),
);

vi.mock('../devices.ts', () => {
  console.log('MOCKING devices API');
  return {
    disposeOfMediaStream: vi.fn(),
    getVideoDevices: vi.fn(() => {
      return of(mockVideoDevices);
    }),
    getVideoStream,
    getAudioBrowserPermission: () => mockBrowserPermission,
    getVideoBrowserPermission: () => mockBrowserPermission,
    deviceIds$: mockDeviceIds$(),
    resolveDeviceId: (deviceId) => deviceId,
  };
});

vi.mock('../../Call.ts', () => {
  console.log('MOCKING Call');
  return {
    Call: vi.fn(function () {
      return mockCall();
    }),
  };
});

vi.mock('../../helpers/compatibility.ts', () => {
  console.log('MOCKING mobile device');
  return {
    isMobile: () => true,
  };
});

vi.mock('../../helpers/platforms', () => {
  console.log('MOCKING mobile device');
  return {
    isReactNative: () => false,
  };
});

describe('CameraManager', () => {
  let manager: CameraManager;
  let call: Call;

  beforeEach(() => {
    const devicePersistence = { enabled: false, storageKey: '' };
    call = new Call({
      id: '',
      type: '',
      streamClient: new StreamClient('abc123', { devicePersistence }),
      clientStore: new StreamVideoWriteableStateStore(),
    });
    manager = new CameraManager(call, devicePersistence);
  });

  it('list devices', () => {
    const spy = vi.fn();
    manager.listDevices().subscribe(spy);

    expect(spy).toHaveBeenCalledWith(mockVideoDevices);
  });

  it('get stream', async () => {
    await manager.enable();

    expect(getVideoStream).toHaveBeenCalledWith(
      {
        deviceId: undefined,
        width: 1280,
        height: 720,
      },
      expect.any(Tracer),
    );
  });

  it('should get device id from stream', async () => {
    expect(manager.state.selectedDevice).toBeUndefined();

    await manager.enable();

    expect(manager.state.selectedDevice).toBeDefined();
  });

  it('publish stream', async () => {
    manager['call'].state.setCallingState(CallingState.JOINED);

    await manager.enable();

    expect(manager['call'].publish).toHaveBeenCalledWith(
      manager.state.mediaStream,
      TrackType.VIDEO,
      undefined,
    );
  });

  it('stop publish stream', async () => {
    manager['call'].state.setCallingState(CallingState.JOINED);
    await manager.enable();

    await manager.disable();

    expect(manager['call'].stopPublish).toHaveBeenCalledWith(TrackType.VIDEO);
  });

  it('flip', async () => {
    getVideoStream.mockReturnValue(Promise.resolve(mockVideoStream()));
    await manager.selectDirection('front');
    expect(manager.state.direction).toBe('front');
    vi.spyOn(manager, 'selectDirection');
    getVideoStream.mockReturnValue(
      Promise.resolve(mockVideoStream('environment')),
    );
    await manager.flip();
    expect(manager.selectDirection).toHaveBeenCalledWith('back');
    expect(manager.state.direction).toBe('back');
    // reset the mock
    getVideoStream.mockReturnValue(Promise.resolve(mockVideoStream()));
  });

  it('select camera direction', async () => {
    expect(manager.state.direction).toBe(undefined);

    await manager.enable();

    expect(getVideoStream).toHaveBeenCalledWith(
      {
        deviceId: undefined,
        width: 1280,
        height: 720,
      },
      expect.any(Tracer),
    );

    await manager.selectDirection('front');

    expect(getVideoStream).toHaveBeenCalledWith(
      {
        deviceId: undefined,
        width: 1280,
        height: 720,
        facingMode: 'user',
      },
      expect.any(Tracer),
    );

    await manager.selectDirection('back');

    expect(getVideoStream).toHaveBeenCalledWith(
      {
        deviceId: undefined,
        facingMode: 'environment',
        width: 1280,
        height: 720,
      },
      expect.any(Tracer),
    );
  });

  it(`shouldn't set deviceId and facingMode at the same time`, async () => {
    await manager.enable();

    await manager.flip();

    expect(getVideoStream).toHaveBeenCalledWith(
      {
        facingMode: 'environment',
        width: 1280,
        height: 720,
      },
      expect.any(Tracer),
    );

    const deviceId = mockVideoDevices[1].deviceId;
    await manager.select(deviceId);

    expect((getVideoStream as Mock).mock.lastCall[0]).toEqual({
      deviceId: { exact: deviceId },
      width: 1280,
      height: 720,
    });
  });

  it('should pass resolved camera constraints to virtual devices', async () => {
    const virtualStream = mockVideoStream();
    const getUserMedia = vi.fn(() => ({ stream: virtualStream }));

    const { deviceId } = manager.registerVirtualDevice({
      label: 'Virtual camera',
      getUserMedia,
    });

    await manager.select(deviceId);
    await manager.enable();

    expect(getUserMedia).toHaveBeenCalledWith({
      deviceId: { exact: deviceId },
      width: 1280,
      height: 720,
    });
  });

  it(`should set target resolution, but shouldn't change device status`, async () => {
    manager['targetResolution'] = { width: 640, height: 480 };

    expect(manager.state.status).toBe(undefined);

    await manager.selectTargetResolution({ width: 1280, height: 720 });

    const targetResolution = manager['targetResolution'];

    expect(targetResolution.width).toBe(1280);
    expect(targetResolution.height).toBe(720);
    expect(manager.state.status).toBe(undefined);
  });

  it('should apply target resolution to existing media stream track', async () => {
    await manager.enable();
    await manager.selectTargetResolution({ width: 640, height: 480 });

    expect((getVideoStream as Mock).mock.lastCall[0]).toEqual({
      deviceId: { exact: mockVideoDevices[0].deviceId },
      width: 640,
      height: 480,
    });
  });

  it(`should do nothing if existing track has the correct resolution`, async () => {
    await manager.enable();

    expect(getVideoStream).toHaveBeenCalledOnce();

    await manager.selectTargetResolution({ width: 1280, height: 720 });

    expect(getVideoStream).toHaveBeenCalledOnce();
  });

  describe('Video Settings', () => {
    beforeEach(() => {
      // @ts-expect-error - read only property
      call.permissionsContext = new PermissionsContext();
      call.permissionsContext.hasPermission = vi.fn().mockReturnValue(true);
    });

    it('should enable the camera when set on the dashboard', async () => {
      vi.spyOn(manager, 'enable');
      await manager.apply(
        fromPartial({
          enabled: true,
          target_resolution: { width: 640, height: 480 },
          camera_facing: 'front',
          camera_default_on: true,
        }),
        true,
      );

      expect(manager.state.direction).toBe('front');
      expect(manager.state.status).toBe('enabled');
      expect(manager['targetResolution']).toEqual({ width: 640, height: 480 });
      expect(manager.enable).toHaveBeenCalled();
    });

    it('should enable the camera when enabled is not provided', async () => {
      vi.spyOn(manager, 'enable');
      await manager.apply(
        fromPartial({
          target_resolution: { width: 640, height: 480 },
          camera_facing: 'front',
          camera_default_on: true,
        }),
        true,
      );

      expect(manager.state.direction).toBe('front');
      expect(manager.state.status).toBe('enabled');
      expect(manager['targetResolution']).toEqual({ width: 640, height: 480 });
      expect(manager.enable).toHaveBeenCalled();
    });

    it('should not enable the camera when set on the dashboard', async () => {
      vi.spyOn(manager, 'enable');
      await manager.apply(
        fromPartial({
          enabled: true,
          target_resolution: { width: 640, height: 480 },
          camera_facing: 'front',
          camera_default_on: false,
        }),
        true,
      );

      expect(manager.state.direction).toBe('front');
      expect(manager.state.status).toBe(undefined);
      expect(manager['targetResolution']).toEqual({ width: 640, height: 480 });
      expect(manager.enable).not.toHaveBeenCalled();
    });

    it('should skip defaults when preferences are applied', async () => {
      vi.spyOn(mockBrowserPermission, 'asStateObservable').mockReturnValue(
        of('granted'),
      );
      const devicePersistence = { enabled: true, storageKey: '' };
      const persistedManager = new CameraManager(call, devicePersistence);
      const applySpy = vi
        .spyOn(persistedManager as never, 'applyPersistedPreferences')
        .mockResolvedValue(true);
      const selectDirectionSpy = vi.spyOn(persistedManager, 'selectDirection');
      const enableSpy = vi.spyOn(persistedManager, 'enable');

      await persistedManager.apply(
        fromPartial({
          enabled: true,
          target_resolution: { width: 640, height: 480 },
          camera_facing: 'front',
          camera_default_on: true,
        }),
        true,
      );

      expect(applySpy).toHaveBeenCalledWith(true);
      expect(selectDirectionSpy).not.toHaveBeenCalled();
      expect(enableSpy).not.toHaveBeenCalled();
    });

    it('should skip persisted preferences when permission is not granted', async () => {
      vi.spyOn(mockBrowserPermission, 'asStateObservable').mockReturnValue(
        of('prompt'),
      );
      const devicePersistence = { enabled: true, storageKey: '' };
      const persistedManager = new CameraManager(call, devicePersistence);
      const applySpy = vi.spyOn(
        persistedManager as never,
        'applyPersistedPreferences',
      );
      const enableSpy = vi.spyOn(persistedManager, 'enable');

      await persistedManager.apply(
        fromPartial({
          enabled: true,
          target_resolution: { width: 640, height: 480 },
          camera_facing: 'front',
          camera_default_on: true,
        }),
        true,
      );

      expect(applySpy).not.toHaveBeenCalled();
      expect(enableSpy).toHaveBeenCalled();
    });

    it('should not apply defaults when device is not pristine', async () => {
      manager.state.setStatus('enabled');
      const selectDirectionSpy = vi.spyOn(manager, 'selectDirection');
      const enableSpy = vi.spyOn(manager, 'enable');

      await manager.apply(
        fromPartial({
          enabled: true,
          target_resolution: { width: 640, height: 480 },
          camera_facing: 'front',
          camera_default_on: true,
        }),
        true,
      );

      expect(selectDirectionSpy).not.toHaveBeenCalled();
      expect(enableSpy).not.toHaveBeenCalled();
    });

    it('should on the camera but not publish when publish is false', async () => {
      manager['call'].state.setCallingState(CallingState.IDLE);
      vi.spyOn(manager, 'enable');
      // @ts-expect-error - private api
      vi.spyOn(manager, 'publishStream');
      await manager.apply(
        fromPartial({
          enabled: true,
          target_resolution: { width: 640, height: 480 },
          camera_facing: 'front',
          camera_default_on: true,
        }),
        false,
      );

      expect(manager.state.direction).toBe('front');
      expect(manager.state.status).toBe('enabled');
      expect(manager['targetResolution']).toEqual({ width: 640, height: 480 });
      expect(manager.enable).toHaveBeenCalled();
      // @ts-expect-error - private api
      expect(manager.publishStream).not.toHaveBeenCalled();
    });

    it('should not enable the camera when the user does not have permission', async () => {
      call.permissionsContext.canPublish = vi.fn().mockReturnValue(false);
      vi.spyOn(manager, 'enable');
      await manager.apply(
        fromPartial({
          target_resolution: { width: 640, height: 480 },
          camera_facing: 'front',
          camera_default_on: true,
          enabled: true,
        }),
        true,
      );

      expect(manager.state.direction).toBe('front');
      expect(manager.state.status).toBe(undefined);
      expect(manager['targetResolution']).toEqual({ width: 640, height: 480 });
      expect(manager.enable).not.toHaveBeenCalled();
    });

    it('should publish the stream when the camera is already enabled', async () => {
      await manager.enable();
      // @ts-expect-error - private api
      vi.spyOn(manager, 'publishStream');
      await manager.apply(
        fromPartial({
          target_resolution: { width: 640, height: 480 },
          camera_facing: 'front',
          camera_default_on: true,
          enabled: true,
        }),
        true,
      );

      expect(manager['publishStream']).toHaveBeenCalled();
    });

    it('should not turn on the camera when video is disabled', async () => {
      vi.spyOn(manager, 'enable');
      await manager.apply(
        fromPartial({
          enabled: false,
          target_resolution: { width: 640, height: 480 },
          camera_facing: 'front',
          camera_default_on: true,
        }),
        false,
      );

      expect(manager.state.status).toBe(undefined);
      expect(manager.enable).not.toHaveBeenCalled();
    });
  });

  describe('Device Persistence Stress', () => {
    it('persists the final camera and muted state after rapid toggles, switches, and unplug', async () => {
      const storageKey = '@test/device-preferences-camera-stress';
      const localStorageMock = createLocalStorageMock();
      const originalWindow = globalThis.window;
      Object.defineProperty(globalThis, 'window', {
        configurable: true,
        value: { localStorage: localStorageMock },
      });

      const getVideoStreamMock = vi.mocked(getVideoStream);
      getVideoStreamMock.mockImplementation((constraints) => {
        const requestedDeviceId = (constraints?.deviceId as { exact?: string })
          ?.exact;
        const selectedDevice =
          mockVideoDevices.find((d) => d.deviceId === requestedDeviceId) ??
          mockVideoDevices[0];
        return Promise.resolve(
          createVideoStreamForDevice(selectedDevice.deviceId),
        );
      });
      vi.spyOn(mockBrowserPermission, 'asStateObservable').mockReturnValue(
        of('granted'),
      );

      const stressManager = new CameraManager(call, {
        enabled: true,
        storageKey,
      });

      try {
        const finalDevice = mockVideoDevices[2];
        emitDeviceIds(mockVideoDevices);

        await Promise.allSettled([
          stressManager.enable(),
          stressManager.select(mockVideoDevices[1].deviceId),
          stressManager.toggle(),
          stressManager.select(finalDevice.deviceId),
          stressManager.toggle(),
          stressManager.enable(),
        ]);
        await stressManager.statusChangeSettled();
        await stressManager.select(finalDevice.deviceId);
        await stressManager.enable();
        await stressManager.statusChangeSettled();

        expect(stressManager.state.selectedDevice).toBe(finalDevice.deviceId);
        expect(stressManager.state.status).toBe('enabled');

        const persistedBeforeUnplug = toPreferenceList(
          readPreferences(storageKey).camera,
        );
        expect(persistedBeforeUnplug[0]).toEqual({
          selectedDeviceId: finalDevice.deviceId,
          selectedDeviceLabel: finalDevice.label,
          muted: false,
        });

        emitDeviceIds(
          mockVideoDevices.filter((d) => d.deviceId !== finalDevice.deviceId),
        );

        await vi.waitFor(() => {
          expect(stressManager.state.selectedDevice).toBe(undefined);
          expect(stressManager.state.status).toBe('disabled');
        });

        const persistedAfterUnplug = toPreferenceList(
          readPreferences(storageKey).camera,
        );
        expect(persistedAfterUnplug[0]).toEqual({
          selectedDeviceId: defaultDeviceId,
          selectedDeviceLabel: '',
          muted: true,
        });
        expect(persistedAfterUnplug).toContainEqual({
          selectedDeviceId: finalDevice.deviceId,
          selectedDeviceLabel: finalDevice.label,
          muted: true,
        });
      } finally {
        stressManager.dispose();
        Object.defineProperty(globalThis, 'window', {
          configurable: true,
          value: originalWindow,
        });
      }
    });
  });

  afterEach(() => {
    vi.clearAllMocks();
    vi.resetModules();
  });
});
