import { act, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import {
  addRemoteParticipant,
  setLocalParticipantId,
  setLocalParticipantName,
  setSessionState,
} from '../../store/session/sessionSlice'
import { Meeting } from './Meeting'
import { configureStore, EnhancedStore } from '@reduxjs/toolkit'
import { zocksStoreConfig } from '../../app/store'
import { Provider } from 'react-redux'
import {
  setupMediaDevicesMock,
  setupMockLocalMediaStream,
  setupMockVirtualBackground,
  setupZocksSdkMocks,
} from '../../testUtils'
import { LocalMediaStream, RoomEvent, SessionState } from '@zocks-communications/client-sdk-js'
import { mock } from 'jest-mock-extended'

beforeEach(() => {
  setupMediaDevicesMock()
  window.HTMLElement.prototype.scrollIntoView = function () {}
})

const renderWithState = (store: EnhancedStore) => {
  render(
    <Provider store={store}>
      <Meeting />
    </Provider>
  )
}

const joinRoom = async (enableAudio?: boolean, enableVideo?: boolean, withVirtualBackground?: boolean) => {
  jest.spyOn(LocalMediaStream, 'create').mockReturnValue(
    Promise.resolve(
      mock<LocalMediaStream>({
        mediaStreamTrack: mock<MediaStreamTrack>({
          getSettings: jest.fn().mockImplementation(() => ({ deviceId: 'testMediaDeviceId' })),
        }),
      })
    )
  )
  const sdkMocks = setupZocksSdkMocks()
  const store = configureStore(zocksStoreConfig)
  renderWithState(store)

  const roomIdInput = screen.getByLabelText('Enter a Meeting ID')
  userEvent.clear(roomIdInput)
  userEvent.type(roomIdInput, 'test room id')

  if (enableAudio) {
    const enableAudioButton = screen.getByRole('button', { name: 'Unmute' })
    userEvent.click(enableAudioButton)
    expect(await screen.findByRole('button', { name: 'Mute' })).toBeInTheDocument()
  }

  if (enableVideo) {
    const enableVideoButton = screen.getByRole('button', { name: 'Enable Video' })
    userEvent.click(enableVideoButton)
    expect(await screen.findByRole('button', { name: 'Disable Video' })).toBeInTheDocument()
  }

  if (withVirtualBackground) {
    setupMockLocalMediaStream()
    const { mockStartVirtualBackground } = setupMockVirtualBackground()
    userEvent.click(screen.getByRole('button', { name: 'Video devices' }))
    userEvent.click(screen.getByText('Blur My Background'))
    await waitFor(() => expect(mockStartVirtualBackground).toHaveBeenCalledTimes(1))
  }

  const joinButton = screen.getByRole('button', { name: 'Join the Meeting' })
  await userEvent.click(joinButton)

  const { mockLocalParticipant, mockRemoteParticipant } = sdkMocks
  await act(async () => {
    await Promise.resolve()
    store.dispatch(setLocalParticipantId(mockLocalParticipant.id))
    store.dispatch(setLocalParticipantName(mockLocalParticipant.name))
    store.dispatch(setSessionState(SessionState.CONNECTED))
    store.dispatch(
      addRemoteParticipant({
        participantId: mockRemoteParticipant.id,
        participantName: mockRemoteParticipant.name,
      })
    )
  })

  return sdkMocks
}

test('renders the join page if the local participant is not connected', async () => {
  const store = configureStore(zocksStoreConfig)
  renderWithState(store)

  expect(await screen.findByLabelText('Enter a Meeting ID')).toBeInTheDocument()
  expect(await screen.findByRole('button', { name: 'Join the Meeting' })).toBeInTheDocument()
})

test('renders the participants and media controls if the local participant is connected', async () => {
  await joinRoom()

  expect(screen.getByRole('button', { name: 'Unmute' })).toBeInTheDocument()
  expect(screen.getByRole('button', { name: 'Enable Video' })).toBeInTheDocument()
  expect(screen.getByRole('button', { name: 'Share Screen' })).toBeInTheDocument()
  expect(screen.getByRole('button', { name: 'Leave' })).toBeInTheDocument()
  expect(screen.getByRole('button', { name: 'Show Participants' })).toBeInTheDocument()
  expect(screen.getByRole('button', { name: 'Show Chat' })).toBeInTheDocument()
  expect(screen.getByText('testLocalParticipantName')).toBeInTheDocument()
})

test('disconnects from the session when the Leave button is clicked', async () => {
  const { mockSession } = await joinRoom()

  const leaveButton = screen.getByRole('button', { name: 'Leave' })
  userEvent.click(leaveButton)

  expect(mockSession.disconnect).toHaveBeenCalledTimes(1)
})

test('disconnects from the session and stops the virtual background when the Leave button is clicked with a virtual background on', async () => {
  const { mockSession, mockLocalParticipant } = await joinRoom(false, true, true)
  mockLocalParticipant.hasVirtualBackground.mockReturnValue(true)

  const leaveButton = screen.getByRole('button', { name: 'Leave' })
  userEvent.click(leaveButton)

  expect(mockSession.disconnect).toHaveBeenCalledTimes(1)
  expect(mockSession.localParticipant.clearVirtualBackground).toHaveBeenCalledTimes(1)
})

test('automatically leaves the room on window unload', async () => {
  const { mockSession } = await joinRoom()

  window.dispatchEvent(new Event('unload'))

  expect(mockSession.disconnect).toHaveBeenCalledTimes(1)
})

test('publishes local video stream when clicking on the Enable Video button', async () => {
  const { mockLocalParticipant } = await joinRoom()

  const enableVideoButton = screen.getByRole('button', { name: 'Enable Video' })
  userEvent.click(enableVideoButton)

  expect(mockLocalParticipant.publishVideo).toHaveBeenCalledTimes(1)
})

test('unpublishes local video stream when clicking on the Disable Video button', async () => {
  const { mockEventBus, mockLocalVideoTrack, mockLocalParticipant } = await joinRoom()
  act(() => {
    mockEventBus.emit(RoomEvent.ParticipantLocalTrackPublished, mockLocalVideoTrack)
  })

  const disableVideoButton = screen.getByRole('button', { name: 'Disable Video' })
  userEvent.click(disableVideoButton)

  expect(mockLocalParticipant.unpublishVideo).toHaveBeenCalledTimes(1)
})

test('publishes local audio stream when clicking on the Unmute button', async () => {
  const { mockLocalParticipant } = await joinRoom()

  const unmuteAudioButton = screen.getByRole('button', { name: 'Unmute' })
  userEvent.click(unmuteAudioButton)

  expect(mockLocalParticipant.publishAudio).toHaveBeenCalledTimes(1)
})

test('unpublishes local audio stream when clicking on the Mute button', async () => {
  const { mockEventBus, mockLocalAudioTrack, mockLocalParticipant } = await joinRoom()
  act(() => {
    mockEventBus.emit(RoomEvent.ParticipantLocalTrackPublished, mockLocalAudioTrack)
  })

  const muteAudioButton = screen.getByRole('button', { name: 'Mute' })
  userEvent.click(muteAudioButton)

  expect(mockLocalParticipant.unpublishAudio).toHaveBeenCalledTimes(1)
})

test('publishes screen share when clicking on the Share Screen button', async () => {
  const { mockLocalParticipant } = await joinRoom()

  const startScreenShareButton = screen.getByRole('button', { name: 'Share Screen' })
  userEvent.click(startScreenShareButton)

  expect(mockLocalParticipant.publishScreenShare).toHaveBeenCalledTimes(1)
})

test('unpublishes screen share when clicking on the Stop Sharing button', async () => {
  const { mockEventBus, mockLocalScreenShareTrack, mockLocalParticipant } = await joinRoom()
  act(() => {
    mockEventBus.emit(RoomEvent.ParticipantLocalTrackPublished, mockLocalScreenShareTrack)
  })

  const stopScreenShareButton = screen.getByRole('button', { name: 'Stop Sharing' })
  userEvent.click(stopScreenShareButton)

  expect(mockLocalParticipant.unpublishScreenShare).toHaveBeenCalledTimes(1)
})

test('sets the virtual background when selecting a new virtual background', async () => {
  const { mockEventBus, mockLocalVideoTrack, mockLocalParticipant } = await joinRoom()
  act(() => {
    mockEventBus.emit(RoomEvent.ParticipantLocalTrackPublished, mockLocalVideoTrack)
  })

  userEvent.click(screen.getByRole('button', { name: 'Video devices' }))
  await userEvent.click(screen.getByText('Blur My Background'))

  await waitFor(() => expect(mockLocalParticipant.setVirtualBackground).toHaveBeenCalledTimes(1))
  expect(mockLocalParticipant.setVirtualBackground).toHaveBeenCalledWith('blur', undefined, {
    deviceId: 'testCamera1MediaDeviceId',
  })
})

test('removes the virtual background when selecting the option for no virtual background', async () => {
  const { mockEventBus, mockLocalVideoTrack, mockLocalParticipant } = await joinRoom()
  act(() => {
    mockEventBus.emit(RoomEvent.ParticipantLocalTrackPublished, mockLocalVideoTrack)
  })
  const videoDeviceButton = screen.getByRole('button', { name: 'Video devices' })
  userEvent.click(videoDeviceButton)
  await userEvent.click(screen.getByText('Blur My Background'))
  await waitFor(() => expect(mockLocalParticipant.setVirtualBackground).toHaveBeenCalledTimes(1))

  userEvent.click(videoDeviceButton)
  await userEvent.click(screen.getByText('Unblur My Background'))

  await waitFor(() => expect(mockLocalParticipant.clearVirtualBackground).toHaveBeenCalledTimes(1))
})

test('automatically turns on audio and video after joining when media was enabled on the join page', async () => {
  const { mockLocalParticipant } = await joinRoom(true, true)

  expect(mockLocalParticipant.publishVideo).toHaveBeenCalledTimes(1)
  expect(mockLocalParticipant.publishVideo).toHaveBeenCalledWith({ deviceId: 'testMediaDeviceId' })
  expect(mockLocalParticipant.publishAudio).toHaveBeenCalledTimes(1)
  expect(mockLocalParticipant.publishAudio).toHaveBeenCalledWith({ deviceId: 'testMediaDeviceId' })
})

test('automatically enables virtual background when virtual background was enabled on the join page', async () => {
  const { mockLocalParticipant } = await joinRoom(false, true, true)

  expect(mockLocalParticipant.setVirtualBackground).toHaveBeenCalledTimes(1)
  expect(mockLocalParticipant.setVirtualBackground).toHaveBeenCalledWith('blur', undefined, {
    deviceId: 'testMediaDeviceId',
  })
  expect(mockLocalParticipant.publishVideo).not.toHaveBeenCalled()
})

test('shows the online participant list when clicking on the Show Participants button', async () => {
  await joinRoom()

  const showParticipantsButton = screen.getByRole('button', { name: 'Show Participants' })
  userEvent.click(showParticipantsButton)

  expect(showParticipantsButton).toBeDisabled()
  expect(screen.getByRole('button', { name: 'Dismiss' })).toBeInTheDocument()
  expect(screen.getAllByText('testLocalParticipantName')).toHaveLength(2)
  expect(screen.getAllByText('testRemoteParticipantName')).toHaveLength(2)
})

test('clicking the Dismiss button in the online participant list dismisses the online participants', async () => {
  await joinRoom()

  const showParticipantsButton = screen.getByRole('button', { name: 'Show Participants' })
  userEvent.click(showParticipantsButton)

  const dismissButton = screen.getByRole('button', { name: 'Dismiss' })
  userEvent.click(dismissButton)

  expect(showParticipantsButton).toBeEnabled()
  expect(screen.queryByRole('button', { name: 'Dismiss' })).not.toBeInTheDocument()
  expect(screen.getAllByText('testLocalParticipantName')).toHaveLength(1)
  expect(screen.getAllByText('testRemoteParticipantName')).toHaveLength(1)
})

test('switches audio devices when changing to another audio device', async () => {
  const { mockSession, mockEventBus, mockLocalAudioTrack } = await joinRoom()
  act(() => {
    mockEventBus.emit(RoomEvent.ParticipantLocalTrackPublished, mockLocalAudioTrack)
  })
  expect(await screen.findByRole('button', { name: 'Audio devices' })).not.toBeDisabled()

  userEvent.click(screen.getByRole('button', { name: 'Audio devices' }))
  userEvent.click(screen.getByText('Test Microphone 2'))

  await waitFor(() => expect(mockSession.switchActiveDevice).toHaveBeenCalledTimes(1))
  expect(mockSession.switchActiveDevice).toHaveBeenCalledWith('audioinput', 'testMicrophone2MediaDeviceId')
})

test('switches video devices when changing to another video device', async () => {
  const { mockSession, mockEventBus, mockLocalVideoTrack } = await joinRoom()
  act(() => {
    mockEventBus.emit(RoomEvent.ParticipantLocalTrackPublished, mockLocalVideoTrack)
  })
  expect(await screen.findByRole('button', { name: 'Video devices' })).not.toBeDisabled()

  userEvent.click(screen.getByRole('button', { name: 'Video devices' }))
  userEvent.click(screen.getByText('Test Camera 2'))

  await waitFor(() => expect(mockSession.switchActiveDevice).toHaveBeenCalledTimes(1))
  expect(mockSession.switchActiveDevice).toHaveBeenCalledWith('videoinput', 'testCamera2MediaDeviceId')
})

test('when joining fails, the room id input will be displayed with an error', async () => {
  const { mockSession } = setupZocksSdkMocks()
  mockSession.join.mockRejectedValue({ errors: [{ message: 'test error' }] })
  const store = configureStore(zocksStoreConfig)
  renderWithState(store)
  const roomIdInput = screen.getByLabelText('Enter a Meeting ID')
  userEvent.clear(roomIdInput)
  userEvent.type(roomIdInput, 'test room id')
  userEvent.click(screen.getByRole('button', { name: 'Join the Meeting' }))

  await waitFor(() => {
    expect(roomIdInput).toHaveClass('zocks__join-page__room-input--error')
  })
  userEvent.hover(roomIdInput)
  expect(screen.getByText('test error')).toBeInTheDocument()
  userEvent.unhover(roomIdInput)
  expect(screen.queryByText('test error')).not.toBeInTheDocument()
})

test('when clicking on the Create New Meeting button, the API is called and the new room info is displayed', async () => {
  const { mockApi } = setupZocksSdkMocks()
  const store = configureStore(zocksStoreConfig)
  renderWithState(store)

  const createNewMeetingButton = screen.getByRole('button', { name: 'Create New Meeting for Later' })
  userEvent.click(createNewMeetingButton)

  expect(mockApi.media.createRoom).toHaveBeenCalledTimes(1)
  expect(await screen.findByText('New room created', { exact: false })).toBeInTheDocument()
})

test('shows the chat when clicking on the Show Chat button', async () => {
  await joinRoom()

  const showChatButton = screen.getByRole('button', { name: 'Show Chat' })
  userEvent.click(showChatButton)

  expect(showChatButton).toBeDisabled()
  expect(screen.getByRole('button', { name: 'Dismiss' })).toBeInTheDocument()
  expect(screen.getByRole('button', { name: 'Send' })).toBeInTheDocument()
  expect(screen.getByRole('textbox')).toBeInTheDocument()
})

test('clicking the Dismiss button in the chat dismisses the chat', async () => {
  await joinRoom()

  const showChatButton = screen.getByRole('button', { name: 'Show Chat' })
  userEvent.click(showChatButton)

  const dismissButton = screen.getByRole('button', { name: 'Dismiss' })
  userEvent.click(dismissButton)

  expect(showChatButton).toBeEnabled()
  expect(screen.queryByRole('button', { name: 'Dismiss' })).not.toBeInTheDocument()
  expect(screen.queryByRole('button', { name: 'Send' })).not.toBeInTheDocument()
  expect(screen.queryByRole('textbox')).not.toBeInTheDocument()
})

test('sends a public chat message via the chat', async () => {
  const { mockSession } = await joinRoom()
  userEvent.click(screen.getByRole('button', { name: 'Show Chat' }))

  const chatInput = screen.getByRole('textbox')
  userEvent.type(chatInput, 'test public chat message{enter}')

  expect(screen.getByText('Me')).toBeInTheDocument()
  expect(screen.getByText('test public chat message')).toBeInTheDocument()
  expect(chatInput).toHaveTextContent('')
  expect(mockSession.sendMessageToRoom).toHaveBeenCalledTimes(1)
  expect(mockSession.sendMessageToRoom).toHaveBeenCalledWith('test public chat message', undefined)
})

test('sends a private chat message via the chat', async () => {
  const { mockSession, mockRemoteParticipant } = await joinRoom()
  userEvent.click(screen.getByRole('button', { name: 'Show Chat' }))

  userEvent.selectOptions(screen.getByRole('combobox'), mockRemoteParticipant.id)
  const chatInput = screen.getByRole('textbox')
  userEvent.type(chatInput, 'test private chat message{enter}')

  expect(screen.getByText('Me To testRemoteParticipantName')).toBeInTheDocument()
  expect(screen.getByText('test private chat message')).toBeInTheDocument()
  expect(chatInput).toHaveTextContent('')
  expect(mockSession.sendMessageToParticipant).toHaveBeenCalledTimes(1)
  expect(mockSession.sendMessageToParticipant).toHaveBeenCalledWith(
    'test private chat message',
    mockRemoteParticipant.id,
    undefined
  )
})

test('uploads an attachment in the chat', async () => {
  const { mockSession } = await joinRoom()
  mockSession.uploadAttachment.mockResolvedValue('test-file-id')
  userEvent.click(screen.getByRole('button', { name: 'Show Chat' }))

  const mockFile = mock<File>({ name: 'test-file-name.jpg' })
  const fileInput = screen.getByTestId('hidden-file-input')
  userEvent.upload(fileInput, mockFile)
  await screen.findByText('test-file-name.jpg')

  expect(mockSession.uploadAttachment).toHaveBeenCalledTimes(1)
  expect(mockSession.uploadAttachment).toHaveBeenCalledWith(mockFile, undefined)
})

test('downloading the attachment gets the download link for the attachment', async () => {
  const { mockSession } = await joinRoom()
  mockSession.uploadAttachment.mockResolvedValue('test-file-id')
  userEvent.click(screen.getByRole('button', { name: 'Show Chat' }))
  const mockFile = mock<File>({ name: 'test-file-name.jpg', type: 'image/jpeg', size: 5 })
  const fileInput = screen.getByTestId('hidden-file-input')
  userEvent.upload(fileInput, mockFile)
  await screen.findByText('test-file-name.jpg')
  userEvent.click(screen.getByRole('button', { name: 'Send' }))

  const downloadButton = screen.getByRole('button', { name: 'Download' })
  userEvent.click(downloadButton)
  await waitFor(() => expect(screen.getByRole('button', { name: 'Download' })).not.toBeDisabled())

  expect(mockSession.getFileDownloadLocation).toHaveBeenCalledTimes(1)
  expect(mockSession.getFileDownloadLocation).toHaveBeenCalledWith('test-file-id')
})

test('enables the transcription service and starts transcription when selecting the option to start transcription', async () => {
  const { mockSession, mockEventBus, mockLocalAudioTrack } = await joinRoom()
  act(() => {
    mockEventBus.emit(RoomEvent.ParticipantLocalTrackPublished, mockLocalAudioTrack)
  })
  expect(await screen.findByRole('button', { name: 'Audio devices' })).not.toBeDisabled()

  userEvent.click(screen.getByRole('button', { name: 'Audio devices' }))
  userEvent.click(screen.getByText('Enable Transcription'))

  await waitFor(() => expect(mockSession.transcriptionService.enable).toHaveBeenCalledTimes(1))
  expect(mockSession.transcriptionService.startTranscription).toHaveBeenCalledTimes(1)
})

test('disables the transcription service and stops transcription when selecting the option to stop transcription', async () => {
  const { mockSession, mockEventBus, mockLocalAudioTrack } = await joinRoom()
  act(() => {
    mockEventBus.emit(RoomEvent.ParticipantLocalTrackPublished, mockLocalAudioTrack)
  })
  expect(await screen.findByRole('button', { name: 'Audio devices' })).not.toBeDisabled()
  userEvent.click(screen.getByRole('button', { name: 'Audio devices' }))
  userEvent.click(screen.getByText('Enable Transcription'))
  await waitFor(() => expect(mockSession.transcriptionService.enable).toHaveBeenCalledTimes(1))

  userEvent.click(screen.getByRole('button', { name: 'Audio devices' }))
  userEvent.click(screen.getByText('Disable Transcription'))

  await waitFor(() => expect(mockSession.transcriptionService.disable).toHaveBeenCalledTimes(1))
  expect(mockSession.transcriptionService.stopTranscription).toHaveBeenCalledTimes(1)
})
