import React from 'react'
import userEvent from '@testing-library/user-event'
import { render, screen, waitFor } from '@testing-library/react'
import { AudioInputOutputDeviceSelectButton, DeviceSelectButton } from './DeviceSelectButton'
import { setupMediaDevicesMock } from '../../testUtils'
import * as reduxHooks from '../../app/hooks'

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

beforeEach(() => {
  jest.spyOn(reduxHooks, 'useZocksDispatch').mockReturnValue(mockDispatch)
  jest.spyOn(reduxHooks, 'useZocksStore').mockImplementation((selector) => mockSelector(selector))
})

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

test('gathers all devices on render and sets up an event listener for plugged in devices', async () => {
  const { mockGetDevices, mockNavigatorMediaDevices } = setupMediaDevicesMock()
  const { unmount } = render(<DeviceSelectButton deviceKind='audioinput' onDeviceSelected={jest.fn()} label='Test' />)

  expect(await screen.findByRole('button', { name: 'Test' })).toBeInTheDocument()
  userEvent.click(screen.getByRole('button'))
  expect(screen.getByText('Test Microphone 1')).toBeInTheDocument()
  expect(screen.getByText('Test Microphone 2')).toBeInTheDocument()

  expect(mockGetDevices).toHaveBeenCalledTimes(1)
  expect(mockNavigatorMediaDevices.addEventListener).toHaveBeenCalledTimes(1)
  expect(mockNavigatorMediaDevices.addEventListener).toHaveBeenCalledWith('devicechange', expect.any(Function))
  expect(mockGetDevices).toHaveBeenCalledTimes(1)
  expect(mockGetDevices).toHaveBeenCalledWith('audioinput')
  unmount()
  expect(mockNavigatorMediaDevices.removeEventListener).toHaveBeenCalledTimes(1)
  expect(mockNavigatorMediaDevices.removeEventListener).toHaveBeenCalledWith('devicechange', expect.any(Function))
})

test('calls the onDeviceSelected callback with the media info of the device and sets the global selected device id when selecting a device', async () => {
  setupMediaDevicesMock()
  const mockOnDeviceSelected = jest.fn()
  render(<DeviceSelectButton deviceKind='audioinput' onDeviceSelected={mockOnDeviceSelected} label='Test' />)
  expect(await screen.findByRole('button', { name: 'Test' })).toBeInTheDocument()

  userEvent.click(screen.getByRole('button'))
  userEvent.click(screen.getByText('Test Microphone 1'))

  expect(mockOnDeviceSelected).toHaveBeenCalledTimes(1)
  expect(mockOnDeviceSelected).toHaveBeenCalledWith({
    deviceId: 'testMicrophone1MediaDeviceId',
    label: 'Test Microphone 1',
    kind: 'audioinput',
    groupId: 'test',
    toJSON: expect.any(Function),
  })
  expect(mockDispatch).toHaveBeenLastCalledWith({
    type: 'mediaDevice/setSelectedMediaDevice',
    payload: { deviceId: 'testMicrophone1MediaDeviceId', kind: 'audioinput' },
  })
})

test('does not render anything when there is only a single device', async () => {
  const { mockGetDevices, mockNavigatorMediaDevices } = setupMediaDevicesMock(1, 0)
  const { container } = render(<DeviceSelectButton deviceKind='audioinput' onDeviceSelected={jest.fn()} label='Test' />)

  await waitFor(() => expect(mockGetDevices).toHaveBeenCalledTimes(1))
  await waitFor(() => expect(mockNavigatorMediaDevices.addEventListener).toHaveBeenCalledTimes(1))

  expect(container).toBeEmptyDOMElement()
})

test('does not render anything when there are no devices', async () => {
  const { mockGetDevices, mockNavigatorMediaDevices } = setupMediaDevicesMock(0, 0)
  const { container } = render(<DeviceSelectButton deviceKind='audioinput' onDeviceSelected={jest.fn()} label='Test' />)

  await waitFor(() => expect(mockGetDevices).toHaveBeenCalledTimes(1))
  await waitFor(() => expect(mockNavigatorMediaDevices.addEventListener).toHaveBeenCalledTimes(1))

  expect(container).toBeEmptyDOMElement()
})

test('when there are no devices, clears the global selected device', async () => {
  setupMediaDevicesMock(0, 0)
  render(<DeviceSelectButton deviceKind='audioinput' onDeviceSelected={jest.fn()} label='Test' />)

  await waitFor(() => expect(mockDispatch).toHaveBeenCalledTimes(1))
  expect(mockDispatch).toHaveBeenCalledWith({
    type: 'mediaDevice/setSelectedMediaDevice',
    payload: { deviceId: null, kind: 'audioinput' },
  })
})

test('automatically selects the default (first) device initially', async () => {
  setupMediaDevicesMock()
  render(<DeviceSelectButton deviceKind='audioinput' onDeviceSelected={jest.fn()} label='Test' />)

  expect(await screen.findByRole('button', { name: 'Test' })).toBeInTheDocument()
  userEvent.click(screen.getByRole('button'))

  expect(mockDispatch).toHaveBeenLastCalledWith({
    type: 'mediaDevice/setSelectedMediaDevice',
    payload: { deviceId: 'testMicrophone1MediaDeviceId', kind: 'audioinput' },
  })
})

test('when a device is removed and it was selected previously, selects the new default device and calls the onDeviceSelected callback', async () => {
  mockSelector.mockReturnValue('testMicrophone2MediaDeviceId')
  const { mockNavigatorMediaDevices } = setupMediaDevicesMock()
  const mockOnDeviceSelected = jest.fn()
  render(<DeviceSelectButton deviceKind='audioinput' onDeviceSelected={mockOnDeviceSelected} label='Test' />)
  expect(await screen.findByRole('button', { name: 'Test' })).toBeInTheDocument()
  userEvent.click(screen.getByRole('button'))
  userEvent.click(screen.getByText('Test Microphone 2'))

  mockOnDeviceSelected.mockClear()
  setupMediaDevicesMock(1)
  const listDevices = mockNavigatorMediaDevices.addEventListener.mock.calls[0][1]
  listDevices()

  await waitFor(() => expect(mockOnDeviceSelected).toHaveBeenCalledTimes(1))
  expect(mockOnDeviceSelected).toHaveBeenCalledWith({
    deviceId: 'testMicrophone1MediaDeviceId',
    label: 'Test Microphone 1',
    kind: 'audioinput',
    groupId: 'test',
    toJSON: expect.any(Function),
  })
})

test('Gathers all audio input and output devices', async () => {
  const { mockGetDevices, mockNavigatorMediaDevices } = setupMediaDevicesMock()
  const { unmount } = render(<AudioInputOutputDeviceSelectButton onDeviceSelected={jest.fn()} label='Test' />)

  expect(await screen.findByRole('button', { name: 'Test' })).toBeInTheDocument()
  userEvent.click(screen.getByRole('button'))
  expect(screen.getByText('Test Microphone 1')).toBeInTheDocument()
  expect(screen.getByText('Test Microphone 2')).toBeInTheDocument()
  expect(screen.getByText('Test Speaker 1')).toBeInTheDocument()
  expect(screen.getByText('Test Speaker 2')).toBeInTheDocument()

  expect(mockGetDevices).toHaveBeenCalledTimes(2)
  expect(mockNavigatorMediaDevices.addEventListener).toHaveBeenCalledTimes(2)
  expect(mockNavigatorMediaDevices.addEventListener).toHaveBeenCalledWith('devicechange', expect.any(Function))
  expect(mockGetDevices).toHaveBeenCalledWith('audioinput')
  expect(mockGetDevices).toHaveBeenCalledWith('audiooutput')
  unmount()
  expect(mockNavigatorMediaDevices.removeEventListener).toHaveBeenCalledTimes(2)
  expect(mockNavigatorMediaDevices.removeEventListener).toHaveBeenCalledWith('devicechange', expect.any(Function))
})

test('AudioInputOutputDeviceSelectButton calls the onDeviceSelected callback with the media info of the device and sets the global selected device id when selecting a device', async () => {
  setupMediaDevicesMock()
  const mockOnDeviceSelected = jest.fn()
  render(<AudioInputOutputDeviceSelectButton onDeviceSelected={mockOnDeviceSelected} label='Test' />)
  expect(await screen.findByRole('button', { name: 'Test' })).toBeInTheDocument()

  userEvent.click(screen.getByRole('button'))
  userEvent.click(screen.getByText('Test Microphone 1'))

  expect(mockOnDeviceSelected).toHaveBeenCalledTimes(1)
  expect(mockOnDeviceSelected).toHaveBeenCalledWith({
    deviceId: 'testMicrophone1MediaDeviceId',
    label: 'Test Microphone 1',
    kind: 'audioinput',
    groupId: 'test',
    toJSON: expect.any(Function),
  })
  expect(mockDispatch).toHaveBeenLastCalledWith({
    type: 'mediaDevice/setSelectedMediaDevice',
    payload: { deviceId: 'testMicrophone1MediaDeviceId', kind: 'audioinput' },
  })

  expect(await screen.findByRole('button', { name: 'Test' })).toBeInTheDocument()
  userEvent.click(screen.getByRole('button'))
  userEvent.click(screen.getByText('Test Speaker 1'))

  expect(mockOnDeviceSelected).toHaveBeenCalledTimes(2)
  expect(mockOnDeviceSelected).toHaveBeenLastCalledWith({
    deviceId: 'testSpeaker1MediaDeviceId',
    label: 'Test Speaker 1',
    kind: 'audiooutput',
    groupId: 'test',
    toJSON: expect.any(Function),
  })
  expect(mockDispatch).toHaveBeenLastCalledWith({
    type: 'mediaDevice/setSelectedMediaDevice',
    payload: { deviceId: 'testSpeaker1MediaDeviceId', kind: 'audiooutput' },
  })
})

test('AudioInputOutputDeviceSelectButton does not render anything when there are no devices', async () => {
  const { mockGetDevices, mockNavigatorMediaDevices } = setupMediaDevicesMock(0, 0, 0)
  const { container } = render(<AudioInputOutputDeviceSelectButton onDeviceSelected={jest.fn()} label='Test' />)

  await waitFor(() => expect(mockGetDevices).toHaveBeenCalledTimes(2))
  await waitFor(() => expect(mockNavigatorMediaDevices.addEventListener).toHaveBeenCalledTimes(2))

  expect(container).toBeEmptyDOMElement()
})
