/* @vitest-environment happy-dom */
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { fromPartial } from '@total-typescript/shoehorn';
import {
  createLocalStorageMock,
  emitDeviceIds,
  LocalStorageMock,
  mockAudioDevices,
  mockBrowserPermission,
  mockDeviceIds$,
} from './mocks';
import { of } from 'rxjs';
import { SpeakerManager } from '../SpeakerManager';
import { checkIfAudioOutputChangeSupported } from '../devices';
import { Call } from '../../Call';
import { StreamClient } from '../../coordinator/connection/client';
import { StreamVideoWriteableStateStore } from '../../store';
import { defaultDeviceId } from '../devicePersistence';

vi.mock('../devices.ts', () => {
  console.log('MOCKING devices');
  return {
    getAudioOutputDevices: vi.fn(() => of(mockAudioDevices)),
    checkIfAudioOutputChangeSupported: vi.fn(() => true),
    getAudioBrowserPermission: () => mockBrowserPermission,
    getVideoBrowserPermission: () => mockBrowserPermission,
    deviceIds$: mockDeviceIds$(),
    resolveDeviceId: (deviceId) => deviceId,
  };
});

describe('SpeakerManager.test', () => {
  let manager: SpeakerManager;
  let storageKey: string;
  let localStorageMock: LocalStorageMock;

  beforeEach(() => {
    storageKey = '@test/speaker-preferences';
    localStorageMock = createLocalStorageMock();
    vi.spyOn(mockBrowserPermission, 'asStateObservable').mockReturnValue(
      of('granted'),
    );
    Object.defineProperty(window, 'localStorage', {
      configurable: true,
      value: localStorageMock,
    });
    const devicePersistence = { enabled: false, storageKey };
    manager = new SpeakerManager(
      new Call({
        id: '',
        type: '',
        streamClient: new StreamClient('abc123'),
        clientStore: new StreamVideoWriteableStateStore(),
      }),
      devicePersistence,
    );
  });

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

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

  it('tell is browser supports audio output selection', async () => {
    expect(checkIfAudioOutputChangeSupported).toHaveBeenCalled();
    expect(manager.state.isDeviceSelectionSupported).toBe(true);
  });

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

    manager.select('new-device');

    expect(manager.state.selectedDevice).toBe('new-device');
  });

  it('set volume', async () => {
    expect(manager.state.volume).toBe(1);

    expect(() => manager.setVolume(2)).toThrowError();

    expect(manager.state.volume).toBe(1);

    manager.setVolume(0);

    expect(manager.state.volume).toBe(0);

    manager.setVolume(1);

    expect(manager.state.volume).toBe(1);

    manager.setVolume(0.5);

    expect(manager.state.volume).toBe(0.5);
  });

  it('set participant volume', () => {
    const call = manager['call'];
    call.state.updateOrAddParticipant(
      'session-id',
      fromPartial({
        audioVolume: undefined,
        sessionId: 'session-id',
      }),
    );

    manager.setParticipantVolume('session-id', 0.5);
    let participant = call.state.findParticipantBySessionId('session-id');
    expect(participant!.audioVolume).toBe(0.5);

    manager.setParticipantVolume('session-id', undefined);
    participant = call.state.findParticipantBySessionId('session-id');
    expect(participant!.audioVolume).toBe(undefined);

    expect(() => manager.setParticipantVolume('session-id', 2)).toThrowError();
    expect(() => manager.setParticipantVolume('session-id', -1)).toThrowError();
  });

  it('should disable device if selected device is disconnected', () => {
    emitDeviceIds(mockAudioDevices);
    const deviceId = mockAudioDevices[1].deviceId;
    manager.select(deviceId);

    emitDeviceIds(mockAudioDevices.slice(2));

    expect(manager.state.selectedDevice).toBe('');
  });

  it('persists speaker selection when permission is granted', async () => {
    const persistedManager = new SpeakerManager(
      new Call({
        id: '',
        type: '',
        streamClient: new StreamClient('abc123'),
        clientStore: new StreamVideoWriteableStateStore(),
      }),
      { enabled: true, storageKey },
    );
    const listDevicesSpy = vi.spyOn(persistedManager, 'listDevices');
    const audioOutputDevice = {
      deviceId: 'speaker-1',
      kind: 'audiooutput',
      label: 'Speaker 1',
      groupId: 'speaker-group',
    } as MediaDeviceInfo;

    emitDeviceIds([audioOutputDevice]);
    persistedManager.select(audioOutputDevice.deviceId);

    expect(listDevicesSpy).toHaveBeenCalled();
    expect(persistedManager.state.selectedDevice).toBe('speaker-1');
  });

  describe('apply (web)', () => {
    it('does nothing when persistence is disabled', () => {
      const selectSpy = vi.spyOn(manager, 'select');
      // @ts-expect-error - partial data
      manager.apply({});
      expect(selectSpy).not.toHaveBeenCalled();
    });

    it('selects the persisted speaker device', () => {
      const persistedManager = new SpeakerManager(
        new Call({
          id: '',
          type: '',
          streamClient: new StreamClient('abc123'),
          clientStore: new StreamVideoWriteableStateStore(),
        }),
        { enabled: true, storageKey },
      );

      localStorageMock.setItem(
        storageKey,
        JSON.stringify({
          speaker: [
            {
              selectedDeviceId: 'speaker-1',
              selectedDeviceLabel: 'Speaker 1',
            },
          ],
        }),
      );

      const selectSpy = vi.spyOn(persistedManager, 'select');
      // @ts-expect-error - partial data
      persistedManager.apply({});

      expect(selectSpy).toHaveBeenCalledWith('speaker-1');
      expect(persistedManager.state.selectedDevice).toBe('speaker-1');
    });

    it('selects system default when persisted device is default', () => {
      const persistedManager = new SpeakerManager(
        new Call({
          id: '',
          type: '',
          streamClient: new StreamClient('abc123'),
          clientStore: new StreamVideoWriteableStateStore(),
        }),
        { enabled: true, storageKey },
      );
      persistedManager.select('previous-device');

      localStorageMock.setItem(
        storageKey,
        JSON.stringify({
          speaker: [
            {
              selectedDeviceId: defaultDeviceId,
              selectedDeviceLabel: '',
            },
          ],
        }),
      );

      const selectSpy = vi.spyOn(persistedManager, 'select');
      // @ts-expect-error - partial data
      persistedManager.apply({});

      expect(selectSpy).toHaveBeenCalledWith('');
      expect(persistedManager.state.selectedDevice).toBe('');
    });
  });

  afterEach(() => {
    vi.clearAllMocks();
    vi.resetModules();
    Object.defineProperty(window, 'localStorage', {
      configurable: true,
      value: undefined,
    });
  });
});
