import React from 'react'
import { useSynchronizeParticipants, useZocksActions } from './hooks'
import { setupZocksSdkMocks } from '../../testUtils'
import * as reduxHooks from '../../app/hooks'
import {
  ConnectionQuality,
  RemoteParticipant,
  RoomState,
  StreamKind,
  StreamSource,
  Track,
  RoomEvent,
  SessionEvent,
  SessionState,
} from '@zocks-communications/client-sdk-js'
import { mock } from 'jest-mock-extended'
import { ParticipantModel } from './models'

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

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

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

const testZocksConfig = {
  apiBaseUrl: 'https://test-zocks-api-base-url',
  accountId: 'test-account-id',
}

const useTestJoinRoom = () => {
  const sdkMocks = setupZocksSdkMocks()
  const {
    sessionActions: { join },
    chatActions,
  } = useZocksActions(testZocksConfig)

  join({ roomId: 'testRoomId', roomName: 'testRoomName', autoCreate: false })

  jest.clearAllMocks()

  return {
    ...sdkMocks,
    chatActions,
  }
}

test('dispatches connection quality changed action on ConnectionQualityChanged event', () => {
  const { mockEventBus, mockLocalParticipant } = useTestJoinRoom()

  mockEventBus.emit(RoomEvent.ConnectionQualityChanged, ConnectionQuality.POOR, mockLocalParticipant)

  expect(mockDispatch).toHaveBeenCalledTimes(1)
  expect(mockDispatch).toHaveBeenCalledWith({
    payload: { connectionQuality: 'poor', participantId: 'testLocalParticipantId' },
    type: 'session/connectionQualityChanged',
  })
})

test('dispatches participant connected action on ParticipantConnected event', () => {
  const { mockEventBus, mockRemoteParticipant } = useTestJoinRoom()

  mockEventBus.emit(RoomEvent.ParticipantConnected, mockRemoteParticipant)

  expect(mockDispatch).toHaveBeenCalledTimes(1)
  expect(mockDispatch).toHaveBeenCalledWith({
    payload: { participantId: 'testRemoteParticipantId', participantName: 'testRemoteParticipantName' },
    type: 'session/addRemoteParticipant',
  })
})

test('dispatches participant disconnected action on ParticipantDisconnected event', () => {
  const { mockEventBus, mockRemoteParticipant } = useTestJoinRoom()

  mockEventBus.emit(RoomEvent.ParticipantDisconnected, mockRemoteParticipant.id)

  expect(mockDispatch).toHaveBeenCalledTimes(2)
  expect(mockDispatch).toHaveBeenCalledWith({
    payload: { participantId: 'testRemoteParticipantId' },
    type: 'session/removeRemoteParticipant',
  })
  expect(mockDispatch).toHaveBeenCalledWith({
    payload: { participantId: 'testRemoteParticipantId' },
    type: 'transcript/clearRemoteParticipantTranscription',
  })
})

test('does not dispatch participant disconnected action without participant id', () => {
  const { mockEventBus } = useTestJoinRoom()

  mockEventBus.emit(RoomEvent.ParticipantDisconnected, undefined)

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

test('dispatches state change when the session state changes to connected', () => {
  const { mockEventBus } = useTestJoinRoom()

  mockEventBus.emit(SessionEvent.SessionStateChanged, RoomState.CONNECTED)

  expect(mockDispatch).toHaveBeenCalledTimes(1)
  expect(mockDispatch).toHaveBeenCalledWith({ payload: 'connected', type: 'session/setSessionState' })
})

test('dispatches room id and room name on the SessionConnected event', () => {
  const { mockEventBus } = useTestJoinRoom()
  mockEventBus.emit(SessionEvent.SessionConnected)

  expect(mockDispatch).toHaveBeenCalledTimes(3)
  expect(mockDispatch).toHaveBeenCalledWith({ payload: 'testRoomId', type: 'session/setRoomId' })
  expect(mockDispatch).toHaveBeenCalledWith({ payload: 'testRoomName', type: 'session/setRoomName' })
  expect(mockDispatch).toHaveBeenCalledWith({ payload: {}, type: 'session/setActiveSession' })
})

test('dispatches the local participant id on the LocalParticipantConnected event', () => {
  const { mockLocalParticipant, mockEventBus } = useTestJoinRoom()

  mockEventBus.emit(SessionEvent.LocalParticipantConnected, mockLocalParticipant)

  expect(mockDispatch).toHaveBeenCalledTimes(2)
  expect(mockDispatch).toHaveBeenCalledWith({
    payload: 'testLocalParticipantId',
    type: 'session/setLocalParticipantId',
  })
  expect(mockDispatch).toHaveBeenCalledWith({
    payload: 'testLocalParticipantName',
    type: 'session/setLocalParticipantName',
  })
})

test('dispatches state change and clears the session id, remote participants and chat when the session state changes to disconnected', () => {
  const { mockEventBus } = useTestJoinRoom()

  mockEventBus.emit(SessionEvent.SessionStateChanged, SessionState.DISCONNECTED)

  expect(mockDispatch).toHaveBeenCalledTimes(8)
  expect(mockDispatch).toHaveBeenCalledWith({ payload: 'disconnected', type: 'session/setSessionState' })
  expect(mockDispatch).toHaveBeenCalledWith({ type: 'session/clearRoomId' })
  expect(mockDispatch).toHaveBeenCalledWith({ type: 'session/clearRoomName' })
  expect(mockDispatch).toHaveBeenCalledWith({ type: 'session/clearActiveSession' })
  expect(mockDispatch).toHaveBeenCalledWith({ type: 'session/clearParticipants' })
  expect(mockDispatch).toHaveBeenCalledWith({ type: 'chat/clearChatMessages' })
  expect(mockDispatch).toHaveBeenCalledWith({ type: 'virtualBackground/clearCurrentVirtualBackground' })
  expect(mockDispatch).toHaveBeenCalledWith({ type: 'transcript/clearTranscription' })
})

test('dispatches participant track subscribed action on ParticipantTrackSubscribed', () => {
  const { mockEventBus, mockRemoteParticipant, mockRemoteAudioTrack } = useTestJoinRoom()

  mockEventBus.emit(SessionEvent.ParticipantTrackSubscribed, mockRemoteAudioTrack, mockRemoteParticipant)

  expect(mockDispatch).toHaveBeenCalledTimes(1)
  expect(mockDispatch).toHaveBeenCalledWith({
    payload: {
      participantId: 'testRemoteParticipantId',
      trackId: 'testRemoteAudioTrackId',
      trackIsMuted: false,
    },
    type: 'session/participantTrackSubscribed',
  })
})

test('does not dispatch a subscribe action if either the participant or the track is not available', () => {
  const { mockEventBus, mockRemoteParticipant, mockRemoteAudioTrack } = useTestJoinRoom()

  mockEventBus.emit(RoomEvent.ParticipantTrackSubscribed, mockRemoteAudioTrack, { ...mockRemoteParticipant, id: null })

  expect(mockDispatch).not.toHaveBeenCalled()

  mockEventBus.emit(
    RoomEvent.ParticipantTrackSubscribed,
    { ...mockRemoteAudioTrack, isAvailable: false },
    mockRemoteParticipant
  )

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

test('dispatches participant track unsubscribed action on ParticipantTrackUnSubscribed', () => {
  const { mockEventBus, mockRemoteParticipant, mockRemoteAudioTrack } = useTestJoinRoom()

  mockEventBus.emit(SessionEvent.ParticipantTrackUnsubscribed, mockRemoteAudioTrack, mockRemoteParticipant)

  expect(mockDispatch).toHaveBeenCalledTimes(1)
  expect(mockDispatch).toHaveBeenCalledWith({
    payload: {
      participantId: 'testRemoteParticipantId',
      trackId: 'testRemoteAudioTrackId',
    },
    type: 'session/participantTrackUnsubscribed',
  })
})

test('does not dispatch a unsubscribe action if either the participant or the track is not available', () => {
  const { mockEventBus, mockRemoteParticipant, mockRemoteAudioTrack } = useTestJoinRoom()

  mockEventBus.emit(SessionEvent.ParticipantTrackUnsubscribed, mockRemoteAudioTrack, {
    ...mockRemoteParticipant,
    id: null,
  })

  expect(mockDispatch).not.toHaveBeenCalled()

  mockEventBus.emit(
    RoomEvent.ParticipantTrackUnsubscribed,
    { ...mockRemoteAudioTrack, isAvailable: false },
    mockRemoteParticipant
  )

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

test('dispatches local track published action when a local track is published and the track is available', () => {
  const { mockEventBus, mockLocalAudioTrack } = useTestJoinRoom()

  mockEventBus.emit(SessionEvent.ParticipantLocalTrackPublished, mockLocalAudioTrack)

  expect(mockDispatch).toHaveBeenCalledTimes(1)
  expect(mockDispatch).toHaveBeenCalledWith({
    payload: {
      trackId: 'testLocalAudioTrackId',
      trackSource: 'microphone',
      trackKind: 'audio',
      trackIsMuted: false,
    },
    type: 'session/participantLocalTrackPublished',
  })
})

test('does not dispatch anything when a local track is published, but the track is not available', () => {
  const { mockEventBus, mockLocalAudioTrack } = useTestJoinRoom()

  mockEventBus.emit(SessionEvent.ParticipantLocalTrackPublished, { ...mockLocalAudioTrack, isAvailable: false })

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

test('dispatches local track unpublished action when a local track is unpublished and the track is available', () => {
  const { mockEventBus, mockLocalAudioTrack } = useTestJoinRoom()

  mockEventBus.emit(SessionEvent.ParticipantLocalTrackUnpublished, mockLocalAudioTrack)

  expect(mockDispatch).toHaveBeenCalledTimes(1)
  expect(mockDispatch).toHaveBeenCalledWith({
    payload: {
      trackId: 'testLocalAudioTrackId',
      trackSource: 'microphone',
      trackKind: 'audio',
    },
    type: 'session/participantLocalTrackUnPublished',
  })
})

test('does not dispatch anything when a local track is unpublished, but the track is not available', () => {
  const { mockEventBus, mockLocalAudioTrack } = useTestJoinRoom()

  mockEventBus.emit(SessionEvent.ParticipantLocalTrackUnpublished, { ...mockLocalAudioTrack, isAvailable: false })

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

test('dispatches remote track published action when a remote track is published and the track is available', () => {
  const { mockEventBus, mockRemoteParticipant, mockRemoteAudioTrack } = useTestJoinRoom()

  mockEventBus.emit(SessionEvent.ParticipantTrackPublished, mockRemoteAudioTrack, mockRemoteParticipant)

  expect(mockDispatch).toHaveBeenCalledTimes(1)
  expect(mockDispatch).toHaveBeenCalledWith({
    payload: {
      participantId: 'testRemoteParticipantId',
      trackSource: 'microphone',
      trackId: 'testRemoteAudioTrackId',
      trackKind: 'audio',
      trackIsMuted: false,
      trackIsSubscribed: true,
    },
    type: 'session/participantTrackPublished',
  })
})

test('does not dispatch anything when a remote track is unpublished, but the track is not available', () => {
  const { mockEventBus, mockRemoteParticipant, mockRemoteAudioTrack } = useTestJoinRoom()

  mockEventBus.emit(
    RoomEvent.ParticipantTrackPublished,
    {
      ...mockRemoteAudioTrack,
      isAvailable: false,
    },
    mockRemoteParticipant
  )

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

test('dispatches remote track unpublished action when a remote track is unpublished and the track is available', () => {
  const { mockEventBus, mockRemoteParticipant, mockRemoteAudioTrack } = useTestJoinRoom()

  mockEventBus.emit(SessionEvent.ParticipantTrackUnpublished, mockRemoteAudioTrack, mockRemoteParticipant)

  expect(mockDispatch).toHaveBeenCalledTimes(1)
  expect(mockDispatch).toHaveBeenCalledWith({
    payload: {
      participantId: 'testRemoteParticipantId',
      trackSource: 'microphone',
      trackId: 'testRemoteAudioTrackId',
      trackKind: 'audio',
    },
    type: 'session/participantTrackUnPublished',
  })
})

test('does not dispatch anything when a remote track is published, but the track is not available', () => {
  const { mockEventBus, mockRemoteParticipant, mockRemoteAudioTrack } = useTestJoinRoom()

  mockEventBus.emit(
    RoomEvent.ParticipantTrackUnpublished,
    {
      ...mockRemoteAudioTrack,
      isAvailable: false,
    },
    mockRemoteParticipant
  )

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

test('dispatches a participant track muted action when a participant track is muted', () => {
  const { mockRemoteAudioTrack, mockRemoteParticipant, mockEventBus } = useTestJoinRoom()

  mockEventBus.emit(SessionEvent.ParticipantTrackMuted, mockRemoteAudioTrack, mockRemoteParticipant)

  expect(mockDispatch).toHaveBeenCalledTimes(1)
  expect(mockDispatch).toHaveBeenCalledWith({
    payload: {
      participantId: 'testRemoteParticipantId',
      trackId: 'testRemoteAudioTrackId',
    },
    type: 'session/participantTrackMuted',
  })
})

test('dispatches a participant track unmuted action when a participant track is unmuted', () => {
  const { mockRemoteAudioTrack, mockRemoteParticipant, mockEventBus } = useTestJoinRoom()

  mockEventBus.emit(SessionEvent.ParticipantTrackUnmuted, mockRemoteAudioTrack, mockRemoteParticipant)

  expect(mockDispatch).toHaveBeenCalledTimes(1)
  expect(mockDispatch).toHaveBeenCalledWith({
    payload: {
      participantId: 'testRemoteParticipantId',
      trackId: 'testRemoteAudioTrackId',
    },
    type: 'session/participantTrackUnMuted',
  })
})

test('dispatches new active speakers when active speakers have changed', () => {
  const { mockEventBus, mockRemoteParticipant } = useTestJoinRoom()

  mockEventBus.emit(SessionEvent.ActiveSpeakersChanged, [mockRemoteParticipant])

  expect(mockDispatch).toHaveBeenCalledTimes(1)
  expect(mockDispatch).toHaveBeenCalledWith({
    payload: ['testRemoteParticipantId'],
    type: 'session/activeSpeakersChanged',
  })
})

test('dispatches save public chat message when a public chat message is received', () => {
  const { mockEventBus, mockRemoteParticipant } = useTestJoinRoom()

  mockEventBus.emit(SessionEvent.PublicChatMessageReceived, {
    message: 'test public message',
    timestamp: 1,
    from: {
      id: mockRemoteParticipant.id,
      name: mockRemoteParticipant.name,
    },
  })

  expect(mockDispatch).toHaveBeenCalledTimes(1)
  expect(mockDispatch).toHaveBeenCalledWith({
    type: 'chat/publicChatMessageReceived',
    payload: {
      message: 'test public message',
      timestamp: 1,
      from: {
        id: mockRemoteParticipant.id,
        name: mockRemoteParticipant.name,
      },
    },
  })
})

test('dispatches new private chat message received when a private chat message is received', () => {
  const { mockEventBus, mockLocalParticipant, mockRemoteParticipant } = useTestJoinRoom()

  mockEventBus.emit(SessionEvent.PrivateChatMessageReceived, {
    message: 'test private message',
    timestamp: 1,
    from: {
      id: mockRemoteParticipant.id,
      name: mockRemoteParticipant.name,
    },
  })

  expect(mockDispatch).toHaveBeenCalledTimes(1)
  expect(mockDispatch).toHaveBeenCalledWith({
    type: 'chat/privateChatMessageReceived',
    payload: {
      message: 'test private message',
      timestamp: 1,
      from: {
        id: mockRemoteParticipant.id,
        name: mockRemoteParticipant.name,
      },
      to: {
        id: mockLocalParticipant.id,
        name: mockLocalParticipant.name,
      },
    },
  })
})

test('throws error when sending a public or private message without a session', () => {
  const { chatActions } = useZocksActions(testZocksConfig)

  expect(() => chatActions.sendPublicMessage('test public message')).toThrowError(
    'Cannot send public message without a session!'
  )
  expect(() =>
    chatActions.sendPrivateMessage('test private message', mock<ParticipantModel>({ id: 'testRemoteParticipantId' }))
  ).toThrowError('Cannot send private message without a session or recipient!')
  expect(mockDispatch).not.toHaveBeenCalled()
})

test('throws error when sending a private message to an unknown remote participant', () => {
  const { chatActions } = useTestJoinRoom()

  expect(() =>
    chatActions.sendPrivateMessage('test private message', mock<ParticipantModel>({ id: null }))
  ).toThrowError('Cannot send private message without a session or recipient!')
  expect(mockDispatch).not.toHaveBeenCalled()
})

test('throws error when uploading and attachment without a session', async () => {
  const { chatActions } = useZocksActions(testZocksConfig)

  await expect(() => chatActions.uploadAttachment(mock<File>())).rejects.toThrowError(
    'Cannot upload attachment without a session or with empty recipient id!'
  )
})

test('throws error when uploading an attachment to a participant without id', async () => {
  const { chatActions } = useTestJoinRoom()

  await expect(() =>
    chatActions.uploadAttachment(mock<File>(), mock<ParticipantModel>({ id: null }))
  ).rejects.toThrowError('Cannot upload attachment without a session or with empty recipient id!')
})

test('throws error when trying to download an attachment without a session', async () => {
  const { chatActions } = useZocksActions(testZocksConfig)

  await expect(() => chatActions.getAttachmentLocation('test-file-id')).rejects.toThrowError(
    'Cannot get attachment location without a session!'
  )
})

test('dispatches transcription in progress when starting transcription', () => {
  const { mockEventBus } = useTestJoinRoom()

  mockEventBus.emit(SessionEvent.LocalTranscriptionStarted)

  expect(mockDispatch).toHaveBeenCalledTimes(1)
  expect(mockDispatch).toHaveBeenCalledWith({
    type: 'transcript/setTranscriptionInProgress',
    payload: true,
  })
})

test('dispatches transcription not in progress when transcription is finished', () => {
  const { mockEventBus } = useTestJoinRoom()

  mockEventBus.emit(SessionEvent.LocalTranscriptionFinished)

  expect(mockDispatch).toHaveBeenCalledTimes(1)
  expect(mockDispatch).toHaveBeenCalledWith({
    type: 'transcript/setTranscriptionInProgress',
    payload: false,
  })
})

test('dispatches transcription result when a transcription result is received', () => {
  const { mockEventBus, mockLocalParticipant } = useTestJoinRoom()

  mockEventBus.emit(
    SessionEvent.TranscriptionReceived,
    {
      resultId: 'testResultId',
      startTime: 0,
      endTime: 2,
      isFinal: true,
      transcript: 'test',
      items: [{ content: 'test', isStable: true }],
    },
    mockLocalParticipant
  )

  expect(mockDispatch).toHaveBeenCalledTimes(1)
  expect(mockDispatch).toHaveBeenCalledWith({
    type: 'transcript/transcriptionReceived',
    payload: {
      participantId: mockLocalParticipant.id,
      transcriptionResult: {
        resultId: 'testResultId',
        startTime: 0,
        endTime: 2,
        isFinal: true,
        transcript: 'test',
        items: [{ content: 'test', isStable: true }],
      },
    },
  })
})

test('synchronization adds a missing participant to the state and adds all tracks of the participant', () => {
  const { mockSession, mockStore } = useTestJoinRoom()
  mockSelector.mockImplementation((selector) => selector(mockStore))
  const missingParticipant = mock<RemoteParticipant>({
    id: 'missingParticipantId',
    name: 'missingParticipantName',
    tracks: [
      mock<Track>({
        id: 'missingVideoTrackId',
        source: StreamSource.CAMERA,
        kind: StreamKind.VIDEO,
        isAvailable: true,
        isMuted: false,
        isSubscribed: true,
      }),
    ],
  })
  mockSession.participants.set(missingParticipant.id, missingParticipant)
  const { synchronizeParticipants } = useSynchronizeParticipants()

  synchronizeParticipants()

  expect(mockDispatch).toHaveBeenCalledTimes(2)
  expect(mockDispatch).toHaveBeenCalledWith({
    type: 'session/addRemoteParticipant',
    payload: {
      participantId: 'missingParticipantId',
      participantName: 'missingParticipantName',
    },
  })
  expect(mockDispatch).toHaveBeenLastCalledWith({
    type: 'session/participantTrackPublished',
    payload: {
      participantId: 'missingParticipantId',
      trackId: 'missingVideoTrackId',
      trackIsMuted: false,
      trackIsSubscribed: true,
      trackKind: 'video',
      trackSource: 'camera',
    },
  })
})

test('synchronization removes an obsolete participant from the state', () => {
  const { mockStore } = useTestJoinRoom()
  mockSelector.mockImplementation((selector) => selector(mockStore))
  mockStore.session.remoteParticipants['obsoleteParticipantId'] = {
    id: 'obsoleteParticipantId',
    name: 'obsoleteParticipantName',
    tracks: {
      audioTracks: [],
      videoTracks: [],
      screenShareTrack: null!,
    },
    connectionQuality: ConnectionQuality.EXCELLENT,
  }
  const { synchronizeParticipants } = useSynchronizeParticipants()

  synchronizeParticipants()

  expect(mockDispatch).toHaveBeenCalledTimes(1)
  expect(mockDispatch).toHaveBeenCalledWith({
    type: 'session/removeRemoteParticipant',
    payload: {
      participantId: 'obsoleteParticipantId',
    },
  })
})

test('synchronization adds a missing track to the state', () => {
  const { mockStore, mockRemoteParticipant } = useTestJoinRoom()
  mockSelector.mockImplementation((selector) => selector(mockStore))
  const missingTrack = mock<Track>({
    id: 'missingVideoTrackId',
    source: StreamSource.CAMERA,
    kind: StreamKind.VIDEO,
    isAvailable: true,
    isMuted: false,
    isSubscribed: true,
  })
  mockRemoteParticipant.tracks.push(missingTrack)
  const { synchronizeParticipants } = useSynchronizeParticipants()

  synchronizeParticipants()

  expect(mockDispatch).toHaveBeenCalledTimes(1)
  expect(mockDispatch).toHaveBeenCalledWith({
    type: 'session/participantTrackPublished',
    payload: {
      participantId: 'testRemoteParticipantId',
      trackId: 'missingVideoTrackId',
      trackIsMuted: false,
      trackIsSubscribed: true,
      trackKind: 'video',
      trackSource: 'camera',
    },
  })
})

test('synchronization removes obsolete audio, video and screen share tracks from the state', () => {
  const { mockStore, mockRemoteParticipant, mockRemoteScreenShareTrack } = useTestJoinRoom()
  mockSelector.mockImplementation((selector) => selector(mockStore))
  const stateTracks = mockStore.session.remoteParticipants[mockRemoteParticipant.id].tracks
  stateTracks.videoTracks.push({
    id: 'obsoleteVideoTrackId',
    isMuted: false,
    isSubscribed: true,
  })
  stateTracks.audioTracks.push({
    id: 'obsoleteAudioTrackId',
    isMuted: false,
    isSubscribed: true,
  })
  stateTracks.screenShareTrack = {
    id: 'obsoleteScreenShareTrackId',
    isMuted: false,
    isSubscribed: true,
  }
  mockRemoteParticipant.tracks = mockRemoteParticipant.tracks.filter((t) => t.id !== mockRemoteScreenShareTrack.id)
  const { synchronizeParticipants } = useSynchronizeParticipants()

  synchronizeParticipants()

  expect(mockDispatch).toHaveBeenCalledTimes(3)
  expect(mockDispatch).toHaveBeenCalledWith({
    type: 'session/participantTrackUnPublished',
    payload: {
      participantId: 'testRemoteParticipantId',
      trackId: 'obsoleteAudioTrackId',
      trackKind: 'audio',
      trackSource: 'microphone',
    },
  })
  expect(mockDispatch).toHaveBeenCalledWith({
    type: 'session/participantTrackUnPublished',
    payload: {
      participantId: 'testRemoteParticipantId',
      trackId: 'obsoleteScreenShareTrackId',
      trackKind: 'video',
      trackSource: 'screen_share',
    },
  })
  expect(mockDispatch).toHaveBeenCalledWith({
    type: 'session/participantTrackUnPublished',
    payload: {
      participantId: 'testRemoteParticipantId',
      trackId: 'obsoleteVideoTrackId',
      trackKind: 'video',
      trackSource: 'camera',
    },
  })
})

test('synchronization mutes unmuted audio and video tracks in the state', () => {
  const { mockStore, mockRemoteAudioTrack, mockRemoteVideoTrack } = useTestJoinRoom()
  mockSelector.mockImplementation((selector) => selector(mockStore))
  mockRemoteAudioTrack.isMuted = true
  mockRemoteVideoTrack.isMuted = true
  const { synchronizeParticipants } = useSynchronizeParticipants()

  synchronizeParticipants()

  expect(mockDispatch).toHaveBeenCalledTimes(2)
  expect(mockDispatch).toHaveBeenCalledWith({
    type: 'session/participantTrackMuted',
    payload: {
      participantId: 'testRemoteParticipantId',
      trackId: 'testRemoteAudioTrackId',
    },
  })
  expect(mockDispatch).toHaveBeenCalledWith({
    type: 'session/participantTrackMuted',
    payload: {
      participantId: 'testRemoteParticipantId',
      trackId: 'testRemoteVideoTrackId',
    },
  })
})

test('synchronization unmutes muted audio and video tracks in the state', () => {
  const { mockStore, mockRemoteParticipant } = useTestJoinRoom()
  mockSelector.mockImplementation((selector) => selector(mockStore))
  mockStore.session.remoteParticipants[mockRemoteParticipant.id].tracks.audioTracks[0].isMuted = true
  mockStore.session.remoteParticipants[mockRemoteParticipant.id].tracks.videoTracks[0].isMuted = true
  const { synchronizeParticipants } = useSynchronizeParticipants()

  synchronizeParticipants()

  expect(mockDispatch).toHaveBeenCalledTimes(2)
  expect(mockDispatch).toHaveBeenCalledWith({
    type: 'session/participantTrackUnMuted',
    payload: {
      participantId: 'testRemoteParticipantId',
      trackId: 'testRemoteAudioTrackId',
    },
  })
  expect(mockDispatch).toHaveBeenCalledWith({
    type: 'session/participantTrackUnMuted',
    payload: {
      participantId: 'testRemoteParticipantId',
      trackId: 'testRemoteVideoTrackId',
    },
  })
})

test('synchronization subscribes unsubscribed audio and video tracks in the state', () => {
  const { mockStore, mockRemoteParticipant } = useTestJoinRoom()
  mockSelector.mockImplementation((selector) => selector(mockStore))
  mockStore.session.remoteParticipants[mockRemoteParticipant.id].tracks.audioTracks[0].isSubscribed = false
  mockStore.session.remoteParticipants[mockRemoteParticipant.id].tracks.videoTracks[0].isSubscribed = false
  const { synchronizeParticipants } = useSynchronizeParticipants()

  synchronizeParticipants()

  expect(mockDispatch).toHaveBeenCalledTimes(2)
  expect(mockDispatch).toHaveBeenCalledWith({
    type: 'session/participantTrackSubscribed',
    payload: {
      participantId: 'testRemoteParticipantId',
      trackId: 'testRemoteAudioTrackId',
      trackIsMuted: false,
    },
  })
  expect(mockDispatch).toHaveBeenCalledWith({
    type: 'session/participantTrackSubscribed',
    payload: {
      participantId: 'testRemoteParticipantId',
      trackId: 'testRemoteVideoTrackId',
      trackIsMuted: false,
    },
  })
})

test('synchronization unsubscribes subscribed audio and video tracks in the state', () => {
  const { mockStore, mockRemoteAudioTrack, mockRemoteVideoTrack } = useTestJoinRoom()
  mockSelector.mockImplementation((selector) => selector(mockStore))
  mockRemoteAudioTrack.isSubscribed = false
  mockRemoteVideoTrack.isSubscribed = false
  const { synchronizeParticipants } = useSynchronizeParticipants()

  synchronizeParticipants()

  expect(mockDispatch).toHaveBeenCalledTimes(2)
  expect(mockDispatch).toHaveBeenCalledWith({
    type: 'session/participantTrackUnsubscribed',
    payload: {
      participantId: 'testRemoteParticipantId',
      trackId: 'testRemoteAudioTrackId',
    },
  })
  expect(mockDispatch).toHaveBeenCalledWith({
    type: 'session/participantTrackUnsubscribed',
    payload: {
      participantId: 'testRemoteParticipantId',
      trackId: 'testRemoteVideoTrackId',
    },
  })
})

test('synchronization does not do anything without a session', () => {
  const mockStore = {
    session: {
      sessionId: null,
      remoteParticipants: {
        testRemoteParticipantId: {
          id: 'testRemoteParticipantId',
          name: 'testRemoteParticipantName',
          tracks: {
            audioTracks: [{ id: 'testRemoteAudioTrackId', isMuted: false }],
            videoTracks: [],
            screenShareTrack: null,
          },
          connectionQuality: ConnectionQuality.EXCELLENT,
        },
      },
    },
  }
  mockSelector.mockImplementation((selector) => selector(mockStore))
  const { synchronizeParticipants } = useSynchronizeParticipants()

  synchronizeParticipants()

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