import React from 'react'
import { useLocalMedia, useLocalVirtualBackgrounds } from './hooks'
import { setupMockLocalMediaStream, setupMockVirtualBackground } from '../../testUtils'
import { VirtualBackgroundType } from '@zocks-communications/client-sdk-js'
import * as reduxHooks from '../../app/hooks'
import { waitFor } from '@testing-library/react'

const mockDispatch = jest.fn()
const mockSelector = jest.fn()

beforeEach(() => {
  // @ts-ignore
  jest.spyOn(React, 'useState').mockImplementation((initialState) => [initialState, jest.fn()])
  jest.spyOn(React, 'useEffect').mockImplementation((fn) => fn())
  jest.spyOn(React, 'useCallback').mockImplementation((fn) => fn)
  jest.spyOn(React, 'useMemo').mockImplementation((fn) => fn())
  jest.spyOn(React, 'useRef').mockReturnValue({ current: false })
  jest.spyOn(reduxHooks, 'useZocksDispatch').mockReturnValue(mockDispatch)
  jest.spyOn(reduxHooks, 'useZocksStore').mockImplementation((selector) => mockSelector(selector))
})

afterEach(() => {
  jest.restoreAllMocks()
})

const mockAudioDeviceInfo: MediaDeviceInfo = {
  deviceId: 'testAudioDeviceId2',
  kind: 'audioinput',
  groupId: 'test',
  label: 'test',
  toJSON(): any {},
}

const mockVideoDeviceInfo: MediaDeviceInfo = {
  deviceId: 'testVideoDeviceId2',
  kind: 'videoinput',
  groupId: 'test',
  label: 'test',
  toJSON(): any {},
}

test('toggleLocalAudioStream starts the local audio stream if it is not currently on', () => {
  const { mockCreateMediaStream } = setupMockLocalMediaStream()
  const { toggleLocalAudioStream } = useLocalMedia()

  toggleLocalAudioStream()

  expect(mockCreateMediaStream).toHaveBeenCalledTimes(1)
  expect(mockCreateMediaStream).toHaveBeenCalledWith({ audio: { deviceId: undefined }, video: false })
})

test('toggleLocalAudioStream stops the local audio stream if it is currently on', () => {
  const mockSetState = jest.fn()
  jest.spyOn(React, 'useState').mockImplementation(() => [mockLocalAudioStream, mockSetState])
  const { mockLocalMediaStream: mockLocalAudioStream } = setupMockLocalMediaStream()
  const { toggleLocalAudioStream } = useLocalMedia()

  toggleLocalAudioStream()

  expect(mockLocalAudioStream.stopStream).toHaveBeenCalledTimes(1)
  expect(mockSetState).toHaveBeenCalledTimes(1)
  expect(mockSetState).toHaveBeenCalledWith(undefined)
})

test('toggleLocalVideoStream starts the local video stream if it is not currently on', async () => {
  const { mockCreateMediaStream } = setupMockLocalMediaStream()
  const { toggleLocalVideoStream } = useLocalMedia()

  toggleLocalVideoStream()

  await waitFor(() => {
    expect(mockCreateMediaStream).toHaveBeenCalledTimes(1)
  })
  expect(mockCreateMediaStream).toHaveBeenCalledWith({ audio: false, video: { deviceId: undefined } })
})

test('toggleLocalVideoStream stops the local video stream if it is currently on', () => {
  const { mockLocalMediaStream: mockLocalVideoStream } = setupMockLocalMediaStream()
  const mockSetState = jest.fn()
  jest.spyOn(React, 'useState').mockImplementation(() => [mockLocalVideoStream, mockSetState])
  const { toggleLocalVideoStream } = useLocalMedia()

  toggleLocalVideoStream()

  expect(mockLocalVideoStream.stopStream).toHaveBeenCalledTimes(1)
  expect(mockSetState).toHaveBeenCalledTimes(1)
  expect(mockSetState).toHaveBeenCalledWith(undefined)
})

test('selectAudioDevice starts a new local audio stream on the selected device if there is already an audio stream', () => {
  const { mockLocalMediaStream: mockLocalAudioStream, mockCreateMediaStream } =
    setupMockLocalMediaStream('testAudioDeviceId')
  jest.spyOn(React, 'useState').mockImplementation(() => [mockLocalAudioStream, jest.fn()])
  const { selectAudioDevice } = useLocalMedia()

  selectAudioDevice(mockAudioDeviceInfo)

  expect(mockLocalAudioStream.stopStream).toHaveBeenCalledTimes(1)
  expect(mockCreateMediaStream).toHaveBeenCalledTimes(1)
  expect(mockCreateMediaStream).toHaveBeenCalledWith({ audio: { deviceId: 'testAudioDeviceId2' }, video: false })
})

test('selectAudioDevice does not start a new local stream if the same device was selected', () => {
  const { mockLocalMediaStream: mockLocalAudioStream, mockCreateMediaStream } =
    setupMockLocalMediaStream('testAudioDeviceId')
  const mockSetState = jest.fn()
  jest.spyOn(React, 'useState').mockImplementation(() => [mockLocalAudioStream, mockSetState])
  const { selectAudioDevice } = useLocalMedia()

  selectAudioDevice({ ...mockAudioDeviceInfo, deviceId: 'testAudioDeviceId' })

  expect(mockLocalAudioStream.stopStream).not.toHaveBeenCalled()
  expect(mockCreateMediaStream).not.toHaveBeenCalled()
})

test('selectAudioDevice does not start a new local stream if there is currently no audio stream', () => {
  const { mockLocalMediaStream: mockLocalAudioStream, mockCreateMediaStream } =
    setupMockLocalMediaStream('testAudioDeviceId')
  const mockSetState = jest.fn()
  jest.spyOn(React, 'useState').mockImplementation(() => [undefined, mockSetState])
  const { selectAudioDevice } = useLocalMedia()

  selectAudioDevice(mockAudioDeviceInfo)

  expect(mockLocalAudioStream.stopStream).not.toHaveBeenCalled()
  expect(mockCreateMediaStream).not.toHaveBeenCalled()
})

test('selectVideoDevice starts a new local video stream on the selected device if there is already a video stream', async () => {
  const { mockLocalMediaStream: mockLocalVideoStream, mockCreateMediaStream } =
    setupMockLocalMediaStream('testVideoDeviceId')
  jest.spyOn(React, 'useState').mockImplementation(() => [mockLocalVideoStream, jest.fn()])
  const { selectVideoDevice } = useLocalMedia()

  selectVideoDevice(mockVideoDeviceInfo)

  expect(mockLocalVideoStream.stopStream).toHaveBeenCalledTimes(1)
  await waitFor(() => {
    expect(mockCreateMediaStream).toHaveBeenCalledTimes(1)
  })
  expect(mockCreateMediaStream).toHaveBeenCalledWith({ audio: false, video: { deviceId: 'testVideoDeviceId2' } })
})

test('selectVideoDevice does not start a new local stream if the same device was selected', () => {
  const { mockLocalMediaStream: mockLocalVideoStream, mockCreateMediaStream } =
    setupMockLocalMediaStream('testVideoDeviceId')
  const mockSetState = jest.fn()
  jest.spyOn(React, 'useState').mockImplementation(() => [mockLocalVideoStream, mockSetState])
  const { selectVideoDevice } = useLocalMedia()

  selectVideoDevice({ ...mockVideoDeviceInfo, deviceId: 'testVideoDeviceId' })

  expect(mockLocalVideoStream.stopStream).not.toHaveBeenCalled()
  expect(mockCreateMediaStream).not.toHaveBeenCalled()
})

test('selectVideoDevice does not start a new local stream if there is currently no video stream', () => {
  const { mockLocalMediaStream: mockLocalVideoStream, mockCreateMediaStream } =
    setupMockLocalMediaStream('testVideoDeviceId')
  const mockSetState = jest.fn()
  jest.spyOn(React, 'useState').mockImplementation(() => [undefined, mockSetState])
  const { selectVideoDevice } = useLocalMedia()

  selectVideoDevice(mockVideoDeviceInfo)

  expect(mockLocalVideoStream.stopStream).not.toHaveBeenCalled()
  expect(mockCreateMediaStream).not.toHaveBeenCalled()
})

test('automatically start the local audio stream if audio is enabled by default', () => {
  const { mockCreateMediaStream } = setupMockLocalMediaStream()

  useLocalMedia(false, true)

  expect(mockCreateMediaStream).toHaveBeenCalledWith({ audio: { deviceId: undefined }, video: false })
})

test('automatically start the local video stream if video is enabled by default', async () => {
  const { mockCreateMediaStream } = setupMockLocalMediaStream()

  useLocalMedia(true, false)

  await waitFor(() => {
    expect(mockCreateMediaStream).toHaveBeenCalledWith({ audio: false, video: { deviceId: undefined } })
  })
})

test('changeVirtualBackground creates a new virtual background and starts it', async () => {
  const { mockCreateMediaStream, mockLocalMediaStream } = setupMockLocalMediaStream()
  const { mockCreateVirtualBackground, mockStartVirtualBackground } = setupMockVirtualBackground()

  const { changeVirtualBackground } = useLocalVirtualBackgrounds()
  await changeVirtualBackground(VirtualBackgroundType.BLUR)

  await waitFor(() => expect(mockCreateMediaStream).toHaveBeenCalledTimes(1))
  expect(mockCreateMediaStream).toHaveBeenCalledWith({
    audio: false,
    video: { deviceId: undefined, height: { ideal: 720 }, width: { ideal: 1280 } },
  })
  expect(mockCreateVirtualBackground).toHaveBeenCalledTimes(1)
  expect(mockCreateVirtualBackground).toHaveBeenCalledWith(
    VirtualBackgroundType.BLUR,
    mockLocalMediaStream.mediaStream,
    undefined
  )
  expect(mockStartVirtualBackground).toHaveBeenCalledTimes(1)
  expect(mockDispatch).toHaveBeenCalledTimes(3)
  expect(mockDispatch).toHaveBeenNthCalledWith(1, {
    payload: true,
    type: 'mediaDevice/setVideoIsLoading',
  })
  expect(mockDispatch).toHaveBeenNthCalledWith(2, {
    payload: { type: 'blur', url: undefined },
    type: 'virtualBackground/setCurrentVirtualBackground',
  })
  expect(mockDispatch).toHaveBeenNthCalledWith(3, {
    payload: false,
    type: 'mediaDevice/setVideoIsLoading',
  })
})

test('changeVirtualBackground creates a new virtual background with the specified device id and starts it', async () => {
  const { mockCreateMediaStream, mockLocalMediaStream } = setupMockLocalMediaStream()
  const { mockCreateVirtualBackground, mockStartVirtualBackground } = setupMockVirtualBackground()
  mockSelector.mockReturnValue('customDeviceId')

  const { changeVirtualBackground } = useLocalVirtualBackgrounds()
  await changeVirtualBackground(VirtualBackgroundType.BLUR, 'customDeviceId')

  await waitFor(() => expect(mockCreateMediaStream).toHaveBeenCalledTimes(1))
  expect(mockCreateMediaStream).toHaveBeenCalledWith({
    audio: false,
    video: { deviceId: 'customDeviceId', height: { ideal: 720 }, width: { ideal: 1280 } },
  })
  expect(mockCreateVirtualBackground).toHaveBeenCalledTimes(1)
  expect(mockCreateVirtualBackground).toHaveBeenCalledWith(
    VirtualBackgroundType.BLUR,
    mockLocalMediaStream.mediaStream,
    undefined
  )
  expect(mockStartVirtualBackground).toHaveBeenCalledTimes(1)
  expect(mockDispatch).toHaveBeenCalledTimes(3)
  expect(mockDispatch).toHaveBeenNthCalledWith(1, {
    payload: true,
    type: 'mediaDevice/setVideoIsLoading',
  })
  expect(mockDispatch).toHaveBeenNthCalledWith(2, {
    payload: { type: 'blur', url: undefined },
    type: 'virtualBackground/setCurrentVirtualBackground',
  })
  expect(mockDispatch).toHaveBeenNthCalledWith(3, {
    payload: false,
    type: 'mediaDevice/setVideoIsLoading',
  })
})

test('changeVirtualBackground clears the virtual background', async () => {
  const { mockCreateMediaStream } = setupMockLocalMediaStream()
  const { mockCreateVirtualBackground, mockVirtualBackground, mockStopVirtualBackground } = setupMockVirtualBackground()
  jest.spyOn(React, 'useState').mockImplementation(() => [mockVirtualBackground, jest.fn()])

  const { changeVirtualBackground } = useLocalVirtualBackgrounds()
  await changeVirtualBackground(null)

  await waitFor(() => expect(mockStopVirtualBackground).toHaveBeenCalledTimes(1))
  expect(mockCreateMediaStream).not.toHaveBeenCalled()
  expect(mockCreateVirtualBackground).not.toHaveBeenCalled()
  expect(mockDispatch).toHaveBeenCalledTimes(3)
  expect(mockDispatch).toHaveBeenNthCalledWith(1, {
    payload: true,
    type: 'mediaDevice/setVideoIsLoading',
  })
  expect(mockDispatch).toHaveBeenNthCalledWith(2, { type: 'virtualBackground/clearCurrentVirtualBackground' })
  expect(mockDispatch).toHaveBeenNthCalledWith(3, {
    payload: false,
    type: 'mediaDevice/setVideoIsLoading',
  })
})

test('stopLocalVirtualBackground stops the virtual background', async () => {
  setupMockLocalMediaStream()
  const { mockVirtualBackground, mockStopVirtualBackground } = setupMockVirtualBackground()
  jest.spyOn(React, 'useState').mockImplementation(() => [mockVirtualBackground, jest.fn()])

  const { stopLocalVirtualBackground } = useLocalVirtualBackgrounds()
  await stopLocalVirtualBackground()

  expect(mockStopVirtualBackground).toHaveBeenCalledTimes(1)
  expect(mockDispatch).not.toHaveBeenCalled()
})

test('stopLocalVirtualBackground does not do anything if there is no virtual background', () => {
  setupMockLocalMediaStream()
  const { mockStopVirtualBackground } = setupMockVirtualBackground()

  const { stopLocalVirtualBackground } = useLocalVirtualBackgrounds()
  stopLocalVirtualBackground()

  expect(mockStopVirtualBackground).not.toHaveBeenCalled()
  expect(mockDispatch).not.toHaveBeenCalled()
})
