import { Provider } from 'react-redux'
import { configureStore, EnhancedStore } from '@reduxjs/toolkit'
import { act, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import {
  addRemoteParticipant,
  participantLocalTrackPublished,
  participantTrackPublished,
} from '../../store/session/sessionSlice'
import {
  AIClient,
  AIExecuteClient,
  IAMClient,
  StreamKind,
  StreamSource,
  TranscriptionHTTPClient,
  VirtualBackgroundType,
} from '@zocks-communications/client-sdk-js'
import { zocksStoreConfig } from '../../app/store'
import { setupApiMock, setupMediaDevicesMock } from '../../testUtils'
import {
  ZocksActions,
  ZocksAIActions,
  ZocksChatActions,
  ZocksMediaActions,
  ZocksRecordingActions,
  ZocksTranscriptionActions,
} from '../../store'
import { RoomControls } from './RoomControls'
import { setCurrentVirtualBackground } from '../../store/virtualBackground/virtualBackgroundSlice'
import { setTranscriptionInProgress, setTranscriptionServiceEnabled } from '../../store/transcript/transcriptSlice'
import { mock } from 'jest-mock-extended'
import { RoomControlsProps } from './models'

const mockSetVirtualBackground = jest.fn()
const mockClearVirtualBackground = jest.fn()

const mockMediaActions: ZocksMediaActions = {
  publishVideo: jest.fn(),
  unpublishVideo: jest.fn(),
  publishAudio: jest.fn(),
  unpublishAudio: jest.fn(),
  publishScreenShare: jest.fn(),
  unpublishScreenShare: jest.fn(),
  switchActiveDevice: jest.fn(),
  setVirtualBackground: async (v) => mockSetVirtualBackground(v),
  clearVirtualBackground: async () => mockClearVirtualBackground(),
}

const mockTranscriptionActions: ZocksTranscriptionActions = {
  enableTranscription: jest.fn(),
  disableTranscription: jest.fn(),
  startTranscription: jest.fn(),
  stopTranscription: jest.fn(),
}

const mockRecordingActions: ZocksRecordingActions = {
  startRecording: jest.fn(),
  stopRecording: jest.fn(),
}

const mockAiActions: ZocksAIActions = {
  listAIFeatures: jest.fn(),
  listAIResults: jest.fn(),
}

const mockChatActions: ZocksChatActions = {
  sendPublicMessage: jest.fn(),
  sendPrivateMessage: jest.fn(),
  uploadAttachment: jest.fn(),
  getAttachmentLocation: jest.fn(),
}

const mockLeaveAction = jest.fn()

const mockZocksActions: ZocksActions = {
  mediaClient: setupApiMock().media,
  localMediaActions: mockMediaActions,
  transcriptionActions: mockTranscriptionActions,
  recordingActions: mockRecordingActions,
  sessionActions: {
    leave: mockLeaveAction,
    createRoom: jest.fn(),
    join: jest.fn(),
  },
  aiActions: mockAiActions,
  chatActions: mockChatActions,
  aiClient: mock<AIClient>(),
  iamClient: mock<IAMClient>(),
  transcriptionHTTPClient: mock<TranscriptionHTTPClient>(),
  aiExecuteClient: mock<AIExecuteClient>(),
}

beforeEach(() => setupMediaDevicesMock())

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

const renderWithState = async (store: EnhancedStore, props?: Partial<RoomControlsProps>) => {
  render(
    <Provider store={store}>
      <RoomControls zocksActions={mockZocksActions} {...props} />
    </Provider>
  )
  expect(await screen.findByRole('button', { name: 'Audio devices' })).toBeInTheDocument()
}

test('renders a muted audio control button, a muted video control button, a screen share button, device selectors, and a leave button by default', async () => {
  const store = configureStore(zocksStoreConfig)
  await renderWithState(store)

  expect(screen.getByRole('button', { name: 'Unmute' })).toBeInTheDocument()
  expect(screen.getByRole('button', { name: 'Audio devices' })).toBeInTheDocument()
  expect(screen.getByRole('button', { name: 'Enable Video' })).toBeInTheDocument()
  expect(screen.getByRole('button', { name: 'Video devices' })).toBeInTheDocument()
  expect(screen.getByRole('button', { name: 'Share Screen' })).toBeInTheDocument()
  expect(screen.getByRole('button', { name: 'Leave' })).toBeInTheDocument()

  expect(screen.getByRole('button', { name: 'Audio devices' })).toBeEnabled()
  expect(screen.getByRole('button', { name: 'Video devices' })).toBeDisabled()
})

test('clicking on the muted audio button calls the publish audio media action', async () => {
  const store = configureStore(zocksStoreConfig)
  await renderWithState(store)

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

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

test('when the local participant has unmuted audio, renders an unmuted audio button, which calls the unpublish audio media action', async () => {
  const store = configureStore(zocksStoreConfig)
  store.dispatch(
    participantLocalTrackPublished({
      trackId: 'testTrackId',
      trackKind: StreamKind.AUDIO,
      trackSource: StreamSource.MICROPHONE,
      trackIsMuted: false,
    })
  )
  await renderWithState(store)

  const audioButton = screen.getByRole('button', { name: 'Mute' })
  expect(audioButton).toBeInTheDocument()

  userEvent.click(audioButton)
  expect(mockMediaActions.unpublishAudio).toHaveBeenCalledTimes(1)
})

test('clicking on the muted video button calls the publish video media action, and displays a loader which disappears', async () => {
  const store = configureStore(zocksStoreConfig)
  await renderWithState(store)

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

  expect(screen.getByRole('status', { busy: true })).toBeInTheDocument()
  expect(mockMediaActions.publishVideo).toHaveBeenCalledTimes(1)
  expect(await screen.findByRole('status', { busy: true })).not.toBeInTheDocument()
})

test('when the local participant has unmuted video, renders an unmuted video button, which calls the unpublish video media action', async () => {
  const store = configureStore(zocksStoreConfig)
  store.dispatch(
    participantLocalTrackPublished({
      trackId: 'testTrackId',
      trackKind: StreamKind.VIDEO,
      trackSource: StreamSource.CAMERA,
      trackIsMuted: false,
    })
  )
  await renderWithState(store)
  const videoButton = screen.getByRole('button', { name: 'Disable Video' })
  expect(videoButton).toBeInTheDocument()

  userEvent.click(videoButton)

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

test('clicking on the screen share button calls the publish screen share media action', async () => {
  const store = configureStore(zocksStoreConfig)
  await renderWithState(store)

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

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

test('when the local participant is sharing their screen, renders a Stop Sharing button, which calls the unpublish screen share media action', async () => {
  const store = configureStore(zocksStoreConfig)
  store.dispatch(
    participantLocalTrackPublished({
      trackId: 'testTrackId',
      trackKind: StreamKind.VIDEO,
      trackSource: StreamSource.SCREEN_SHARE,
      trackIsMuted: false,
    })
  )
  await renderWithState(store)

  const screenShareButton = screen.getByRole('button', { name: 'Stop Sharing' })
  expect(screenShareButton).toBeInTheDocument()
  expect(screenShareButton).not.toBeDisabled()

  userEvent.click(screenShareButton)
  expect(mockMediaActions.unpublishScreenShare).toHaveBeenCalledTimes(1)
})

test('when a remote participant is sharing their screen, the Share Screen button is disabled', async () => {
  const store = configureStore(zocksStoreConfig)
  store.dispatch(
    addRemoteParticipant({ participantId: 'testRemoteParticipantId', participantName: 'testRemoteParticipantName' })
  )
  store.dispatch(
    participantTrackPublished({
      participantId: 'testRemoteParticipantId',
      trackId: 'testTrackId',
      trackKind: StreamKind.VIDEO,
      trackSource: StreamSource.SCREEN_SHARE,
      trackIsMuted: false,
      trackIsSubscribed: true,
    })
  )
  await renderWithState(store)

  expect(screen.getByRole('button', { name: 'Share Screen' })).toBeDisabled()
})

test('clicking on the leave button calls the leave action', async () => {
  const store = configureStore(zocksStoreConfig)
  await renderWithState(store)

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

  expect(mockLeaveAction).toHaveBeenCalledTimes(1)
})

test('clicking on an audio device calls the switchActiveDevice action', async () => {
  const store = configureStore(zocksStoreConfig)
  act(() => {
    store.dispatch(
      participantLocalTrackPublished({
        trackId: 'testTrackId',
        trackKind: StreamKind.AUDIO,
        trackSource: StreamSource.MICROPHONE,
        trackIsMuted: false,
      })
    )
  })
  await renderWithState(store)

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

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

test('clicking on an audio device does not call the switchActiveDevice action', async () => {
  const store = configureStore(zocksStoreConfig)
  act(() => {
    store.dispatch(
      participantLocalTrackPublished({
        trackId: 'testTrackId',
        trackKind: StreamKind.AUDIO,
        trackSource: StreamSource.MICROPHONE,
        trackIsMuted: false,
      })
    )
  })
  await renderWithState(store)

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

  await waitFor(() => expect(mockMediaActions.switchActiveDevice).not.toHaveBeenCalled())
})

test('clicking on a video device calls the switchActiveDevice action', async () => {
  const store = configureStore(zocksStoreConfig)
  act(() => {
    store.dispatch(
      participantLocalTrackPublished({
        trackId: 'testTrackId',
        trackKind: StreamKind.VIDEO,
        trackSource: StreamSource.CAMERA,
        trackIsMuted: false,
      })
    )
  })
  await renderWithState(store)

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

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

test('clicking on a video device does not call the switchActiveDevice action if the device was already selected', async () => {
  const store = configureStore(zocksStoreConfig)
  act(() => {
    store.dispatch(
      participantLocalTrackPublished({
        trackId: 'testTrackId',
        trackKind: StreamKind.VIDEO,
        trackSource: StreamSource.CAMERA,
        trackIsMuted: false,
      })
    )
  })
  await renderWithState(store)

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

  await waitFor(() => expect(mockMediaActions.switchActiveDevice).not.toBeCalled())
})

test('selecting a new virtual background calls the setVirtualBackground action, and displays a loader which disappears', async () => {
  const store = configureStore(zocksStoreConfig)
  store.dispatch(
    participantLocalTrackPublished({
      trackId: 'testTrackId',
      trackKind: StreamKind.VIDEO,
      trackSource: StreamSource.CAMERA,
      trackIsMuted: false,
    })
  )
  await renderWithState(store)

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

  expect(screen.getByRole('status', { busy: true })).toBeInTheDocument()
  expect(mockSetVirtualBackground).toHaveBeenCalledTimes(1)
  expect(mockSetVirtualBackground).toHaveBeenCalledWith(VirtualBackgroundType.BLUR)
  expect(await screen.findByRole('status', { busy: true })).not.toBeInTheDocument()
})

test('removing the virtual background calls the clearVirtualBackground action', async () => {
  const store = configureStore(zocksStoreConfig)
  store.dispatch(
    participantLocalTrackPublished({
      trackId: 'testTrackId',
      trackKind: StreamKind.VIDEO,
      trackSource: StreamSource.CAMERA,
      trackIsMuted: false,
    })
  )
  store.dispatch(setCurrentVirtualBackground({ type: VirtualBackgroundType.BLUR }))
  await renderWithState(store)

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

  expect(screen.getByRole('status', { busy: true })).toBeInTheDocument()
  expect(mockClearVirtualBackground).toHaveBeenCalledTimes(1)
  expect(await screen.findByRole('status', { busy: true })).not.toBeInTheDocument()
})

test('enabling the transcription while the audio is on calls the enableTranscription and startTranscription actions', async () => {
  const store = configureStore(zocksStoreConfig)
  store.dispatch(
    participantLocalTrackPublished({
      trackId: 'testTrackId',
      trackKind: StreamKind.AUDIO,
      trackSource: StreamSource.MICROPHONE,
      trackIsMuted: false,
    })
  )
  await renderWithState(store)

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

  await waitFor(() => expect(mockTranscriptionActions.enableTranscription).toHaveBeenCalledTimes(1))
  expect(mockTranscriptionActions.startTranscription).toHaveBeenCalledTimes(1)
  expect(mockTranscriptionActions.startTranscription).toHaveBeenCalledWith({ deviceId: 'testMicrophone1MediaDeviceId' })
})

test('disabling the transcription calls the stopTranscription and disableTranscription actions', async () => {
  const store = configureStore(zocksStoreConfig)
  store.dispatch(
    participantLocalTrackPublished({
      trackId: 'testTrackId',
      trackKind: StreamKind.AUDIO,
      trackSource: StreamSource.MICROPHONE,
      trackIsMuted: false,
    })
  )
  store.dispatch(setTranscriptionServiceEnabled({ enabled: true }))
  store.dispatch(setTranscriptionInProgress(true))
  await renderWithState(store)

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

  await waitFor(() => expect(mockTranscriptionActions.stopTranscription).toHaveBeenCalledTimes(1))
  expect(mockTranscriptionActions.disableTranscription).toHaveBeenCalledTimes(1)
})

test('automatically enables audio when audioEnabledByDefault is true and there is no audio yet', async () => {
  const store = configureStore(zocksStoreConfig)
  await renderWithState(store, { audioEnabledByDefault: true })

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

test('automatically enables video when videoEnabledByDefault is true and there is no video yet', async () => {
  const store = configureStore(zocksStoreConfig)
  await renderWithState(store, { videoEnabledByDefault: true })

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

test('when the local participant has unmuted audio, does not enable audio automatically', async () => {
  const store = configureStore(zocksStoreConfig)
  store.dispatch(
    participantLocalTrackPublished({
      trackId: 'testTrackId',
      trackKind: StreamKind.AUDIO,
      trackSource: StreamSource.MICROPHONE,
      trackIsMuted: false,
    })
  )
  await renderWithState(store, { audioEnabledByDefault: true })

  const audioButton = screen.getByRole('button', { name: 'Mute' })
  expect(audioButton).toBeInTheDocument()

  expect(mockMediaActions.publishAudio).toHaveBeenCalledTimes(0)
})

test('when the local participant has unmuted video, does not enable video automatically', async () => {
  const store = configureStore(zocksStoreConfig)
  store.dispatch(
    participantLocalTrackPublished({
      trackId: 'testTrackId',
      trackKind: StreamKind.VIDEO,
      trackSource: StreamSource.CAMERA,
      trackIsMuted: false,
    })
  )
  await renderWithState(store, { videoEnabledByDefault: true })

  const videoButton = screen.getByRole('button', { name: 'Disable Video' })
  expect(videoButton).toBeInTheDocument()

  expect(mockMediaActions.publishVideo).toHaveBeenCalledTimes(0)
})
