/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.internal.audio

import android.content.Context
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.media.AudioRecord
import android.media.AudioTrack
import android.util.Log
import com.amazonaws.services.chime.sdk.meetings.TestConstant
import com.amazonaws.services.chime.sdk.meetings.analytics.EventAnalyticsController
import com.amazonaws.services.chime.sdk.meetings.analytics.EventName
import com.amazonaws.services.chime.sdk.meetings.analytics.MeetingStatsCollector
import com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.AudioMode
import com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.AudioRecordingPresetOverride
import com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.AudioStreamType
import com.amazonaws.services.chime.sdk.meetings.internal.utils.AppInfoUtil
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger
import com.xodee.client.audio.audioclient.AppInfo
import com.xodee.client.audio.audioclient.AudioClient
import com.xodee.client.audio.audioclient.AudioClient.AudioModeInternal
import com.xodee.client.audio.audioclient.AudioClient.AudioRecordingPreset
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.just
import io.mockk.mockkClass
import io.mockk.mockkObject
import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.verify
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test

@ExperimentalCoroutinesApi
class DefaultAudioClientControllerTest {
    @MockK
    private lateinit var context: Context

    @MockK
    private lateinit var mockLogger: Logger

    @MockK
    private lateinit var mockAudioClient: AudioClient

    @MockK
    private lateinit var mockEventAnlyticsStateController: MeetingStatsCollector

    @MockK
    private lateinit var mockEventAnalyticsController: EventAnalyticsController

    @MockK
    private lateinit var mockAudioClientObserver: AudioClientObserver

    private lateinit var audioManager: AudioManager

    private lateinit var audioClientController: DefaultAudioClientController

    private val testDispatcher = TestCoroutineDispatcher()

    private val testAudioClientSuccessCode = 0
    private val testAudioClientFailureCode = -1
    private val testNewRoute = 5
    private val testMicMute = true
    private val testAudioFallbackUrl = "audioFallbackUrl"
    private val testAudioHostUrl = "https://audiohost.com:500"
    private val testMeetingId = "meetingId"
    private val testAttendeeId = "aliceId"
    private val testJoinToken = "joinToken"
    private val testSampleBuffer = 32 // Used for sample rate and buffer size

    @Before
    fun setup() {
        // It appears that mocking Log.d needs to happen before MockKAnnotations.init, or else
        // test will complain that Log.d is not mocked
        mockkStatic(System::class, Log::class)
        every { Log.d(any(), any()) } returns 0
        every { System.loadLibrary(any()) } just runs

        MockKAnnotations.init(this, relaxUnitFun = true)

        setupAudioManager()

        audioClientController = DefaultAudioClientController(
            context,
            mockLogger,
            mockAudioClientObserver,
            mockAudioClient,
            mockEventAnlyticsStateController,
            mockEventAnalyticsController
        )
        Dispatchers.setMain(testDispatcher)
    }

    @After
    fun teardown() {
        Dispatchers.resetMain()
        testDispatcher.cleanupTestCoroutines()
    }

    private fun setupAudioManager() {
        audioManager = mockkClass(AudioManager::class)
        every { audioManager.mode } returns AudioManager.MODE_NORMAL
        every { audioManager.isSpeakerphoneOn } returns false
        every { audioManager.setBluetoothScoOn(any()) } just runs
        every { audioManager.stopBluetoothSco() } just runs
        every { audioManager.setMode(any()) } just runs
        every { audioManager.setSpeakerphoneOn(any()) } just runs
        every { context.getSystemService(any()) } returns audioManager
    }

    private fun setupRouteTests(audioClientStatusCode: Int) {
        every { mockAudioClient.route } returns AudioDeviceInfo.TYPE_BUILTIN_SPEAKER
        every { mockAudioClient.setRoute(any()) } returns audioClientStatusCode
    }

    private fun setupStartTests() {
        every { mockAudioClient.sendMessage(any(), any()) } returns testAudioClientSuccessCode
        every {
            mockAudioClient.startSession(any())
        } returns testAudioClientSuccessCode
        every { mockAudioClient.setVoiceFocusNoiseSuppression(any()) } returns AudioClient.AUDIO_CLIENT_OK
        every { mockAudioClient.getVoiceFocusNoiseSuppression() } returns true

        mockkStatic(AudioTrack::class, AudioRecord::class)
        every { AudioTrack.getNativeOutputSampleRate(any()) } returns testSampleBuffer
        every { AudioTrack.getMinBufferSize(any(), any(), any()) } returns testSampleBuffer
        every { AudioRecord.getMinBufferSize(any(), any(), any()) } returns testSampleBuffer
        DefaultAudioClientController.audioClientState = AudioClientState.INITIALIZED

        val testAppInfo = AppInfo(
            "name",
            "versionCode",
            "make",
            "model",
            "version",
            "amazon-chime-sdk",
            "sdkVersion",
            "-07:00"
        )

        mockkObject(AppInfoUtil)
        every { AppInfoUtil.initializeAudioClientAppInfo(any()) } returns testAppInfo
    }

    @Test
    fun `getRoute should call AudioClient getRoute`() {
        setupRouteTests(testAudioClientSuccessCode)

        audioClientController.getRoute()

        verify { audioClientController.getRoute() }
    }

    @Test
    fun `setRoute should call AudioClient setRoute new route is different from current route`() {
        setupRouteTests(testAudioClientSuccessCode)

        audioClientController.setRoute(testNewRoute)

        verify(exactly = 1) { mockAudioClient.setRoute(any()) }
    }

    @Test
    fun `setRoute should NOT call AudioClient setRoute when new route is same as current route`() {
        every { mockAudioClient.route } returns AudioDeviceInfo.TYPE_BUILTIN_SPEAKER

        audioClientController.setRoute(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER)

        verify(exactly = 0) { mockAudioClient.route = any() }
    }

    @Test
    fun `setRoute should return true when AudioClient setRoute returns success code`() {
        setupRouteTests(testAudioClientSuccessCode)

        val testOutput: Boolean = audioClientController.setRoute(testNewRoute)

        assertTrue(testOutput)
    }

    @Test
    fun `setRoute should return false when AudioClient setRoute does NOT return success code`() {
        setupRouteTests(testAudioClientFailureCode)

        val testOutput: Boolean = audioClientController.setRoute(testNewRoute)

        assertFalse(testOutput)
    }

    @Test
    fun `setMute should call AudioClient setMicMute`() {
        val state = DefaultAudioClientController.audioClientState
        DefaultAudioClientController.audioClientState = AudioClientState.STARTED
        every { mockAudioClient.setMicMute(any()) } returns testAudioClientSuccessCode

        audioClientController.setMute(testMicMute)

        verify { mockAudioClient.setMicMute(any()) }

        DefaultAudioClientController.audioClientState = state
    }

    @Test
    fun `setMute should return true upon success`() {
        val state = DefaultAudioClientController.audioClientState
        DefaultAudioClientController.audioClientState = AudioClientState.STARTED
        every { mockAudioClient.setMicMute(any()) } returns testAudioClientSuccessCode

        val testOutput: Boolean = audioClientController.setMute(testMicMute)

        assertTrue(testOutput)

        DefaultAudioClientController.audioClientState = state
    }

    @Test
    fun `setMute should return false upon failure`() {
        val state = DefaultAudioClientController.audioClientState
        DefaultAudioClientController.audioClientState = AudioClientState.STARTED
        every { mockAudioClient.setMicMute(any()) } returns testAudioClientFailureCode

        val testOutput: Boolean = audioClientController.setMute(testMicMute)

        assertFalse(testOutput)

        DefaultAudioClientController.audioClientState = state
    }

    @Test
    fun `setMute should return true when audio client state is started`() {
        val state = DefaultAudioClientController.audioClientState
        DefaultAudioClientController.audioClientState = AudioClientState.STARTED
        every { mockAudioClient.setMicMute(any()) } returns testAudioClientSuccessCode

        val testOutput: Boolean = audioClientController.setMute(testMicMute)

        assertTrue(testOutput)

        DefaultAudioClientController.audioClientState = state
    }

    @Test
    fun `setMute should return false when audio client state is initialized`() {
        val state = DefaultAudioClientController.audioClientState
        DefaultAudioClientController.audioClientState = AudioClientState.INITIALIZED
        val testOutput: Boolean = audioClientController.setMute(testMicMute)

        assertFalse(testOutput)

        DefaultAudioClientController.audioClientState = state
    }

    @Test
    fun `setMute should return false when audio client state is stopped`() {
        val state = DefaultAudioClientController.audioClientState
        DefaultAudioClientController.audioClientState = AudioClientState.STOPPED
        val testOutput: Boolean = audioClientController.setMute(testMicMute)

        assertFalse(testOutput)

        DefaultAudioClientController.audioClientState = state
    }

    @Test
    fun `start with Stereo 48KHz should call AudioClient startSession with Stereo 48KHz and mute mic and speaker as false`() {
        setupStartTests()

        audioClientController.start(
            testAudioFallbackUrl,
            testAudioHostUrl,
            testMeetingId,
            testAttendeeId,
            testJoinToken,
            AudioMode.Stereo48K,
            AudioStreamType.VoiceCall,
            AudioRecordingPresetOverride.None
        )

        verify {
            mockAudioClient.startSession(withArg {
                assertEquals(AudioModeInternal.STEREO_48K, it.audioMode)
            })
        }
    }

    @Test
    fun `start with Mono 16KHz should call AudioClient startSession with Mono 16KHz and mute mic and speaker as false`() {
        setupStartTests()

        audioClientController.start(
                testAudioFallbackUrl,
                testAudioHostUrl,
                testMeetingId,
                testAttendeeId,
                testJoinToken,
                AudioMode.Mono16K,
                AudioStreamType.VoiceCall,
                AudioRecordingPresetOverride.None
        )

        verify {
            mockAudioClient.startSession(withArg {
                assertFalse(it.micMute)
                assertFalse(it.spkMute)
                assertEquals(AudioModeInternal.MONO_16K, it.audioMode)
            })
        }
    }

    @Test
    fun `start with Mono 48KHz should call AudioClient startSession with Mono 48KHz and mute mic and speaker as false`() {
        setupStartTests()

        audioClientController.start(
                testAudioFallbackUrl,
                testAudioHostUrl,
                testMeetingId,
                testAttendeeId,
                testJoinToken,
                AudioMode.Mono48K,
                AudioStreamType.VoiceCall,
                AudioRecordingPresetOverride.None
        )

        verify {
            mockAudioClient.startSession(withArg {
                assertFalse(it.micMute)
                assertFalse(it.spkMute)
                assertEquals(AudioModeInternal.MONO_48K, it.audioMode)
            })
        }
    }

    @Test
    fun `start() with voice call stream should call startSession(AudioClientSessionConfig) with AudioStreamType VOICE_CALL`() {
        setupStartTests()

        audioClientController.start(
            testAudioFallbackUrl,
            testAudioHostUrl,
            testMeetingId,
            testAttendeeId,
            testJoinToken,
            AudioMode.Mono48K,
            AudioStreamType.VoiceCall,
            AudioRecordingPresetOverride.None
        )

        verify {
            mockAudioClient.startSession(withArg {
                assertEquals(AudioClient.AudioStreamType.VOICE_CALL, it.audioStreamType)
            })
        }
    }

    @Test
    fun `start() with music stream should call startSession(AudioClientSessionConfig) with AudioStreamType MUSIC`() {
        setupStartTests()

        audioClientController.start(
            testAudioFallbackUrl,
            testAudioHostUrl,
            testMeetingId,
            testAttendeeId,
            testJoinToken,
            AudioMode.Mono48K,
            AudioStreamType.Music,
            AudioRecordingPresetOverride.None
        )

        verify {
            mockAudioClient.startSession(withArg {
                assertEquals(AudioClient.AudioStreamType.MUSIC, it.audioStreamType)
            })
        }
    }

    @Test
    fun `start should call AudioManger setMode`() {
        setupStartTests()

        audioClientController.start(
                testAudioFallbackUrl,
                testAudioHostUrl,
                testMeetingId,
                testAttendeeId,
                testJoinToken,
                AudioMode.Stereo48K,
                AudioStreamType.VoiceCall,
                AudioRecordingPresetOverride.None
        )

        verify {
            audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
        }
    }

    @Test
    fun `start should notify audioClientObserver about audio client connection events`() {
        setupStartTests()

        audioClientController.start(
            testAudioFallbackUrl,
            testAudioHostUrl,
            testMeetingId,
            testAttendeeId,
            testJoinToken,
            AudioMode.Stereo48K,
            AudioStreamType.VoiceCall,
            AudioRecordingPresetOverride.None
        )

        verify(exactly = 1) { mockAudioClientObserver.notifyAudioClientObserver(any()) }
    }

    @Test
    fun `start should notify EventAnalyticsController about meeting start requested event`() {
        setupStartTests()
        audioClientController.start(
            testAudioFallbackUrl,
            testAudioHostUrl,
            testMeetingId,
            testAttendeeId,
            testJoinToken,
            AudioMode.Stereo48K,
            AudioStreamType.VoiceCall,
            AudioRecordingPresetOverride.None
        )

        verify(exactly = 1) { mockEventAnalyticsController.publishEvent(EventName.meetingStartRequested, any()) }
    }

    @Test
    fun `stop should not call AudioClient stopSession when audio client status is not started`() {
        setupStartTests()
        every { mockAudioClient.stopSession() } returns testAudioClientSuccessCode

        audioClientController.stop()

        verify(exactly = 0) { mockAudioClient.stopSession() }
    }

    @Test
    fun `stop should notify EventAnalyticsController about meeting end event`() {
        setupStartTests()
        audioClientController.start(
            testAudioFallbackUrl,
            testAudioHostUrl,
            testMeetingId,
            testAttendeeId,
            testJoinToken,
            AudioMode.Stereo48K,
            AudioStreamType.VoiceCall,
            AudioRecordingPresetOverride.None
        )
        every { mockAudioClient.stopSession() } returns testAudioClientSuccessCode

        audioClientController.stop()

        verify(exactly = 1, timeout = TestConstant.globalScopeTimeoutMs) { mockEventAnalyticsController.publishEvent(EventName.meetingEnded, any()) }
    }

    @Test
    fun `stop should call AudioClient stopSession when audio client status is started`() {
        setupStartTests()
        audioClientController.start(
            testAudioFallbackUrl,
            testAudioHostUrl,
            testMeetingId,
            testAttendeeId,
            testJoinToken,
            AudioMode.Stereo48K,
            AudioStreamType.VoiceCall,
            AudioRecordingPresetOverride.None
        )
        every { mockAudioClient.stopSession() } returns testAudioClientSuccessCode

        audioClientController.stop()

        verify(exactly = 1, timeout = TestConstant.globalScopeTimeoutMs) { mockAudioClient.stopSession() }
        verify { audioManager.setBluetoothScoOn(false) }
        verify { audioManager.stopBluetoothSco() }
        verify { audioManager.setMode(AudioManager.MODE_NORMAL) }
        verify { audioManager.setSpeakerphoneOn(false) }
    }

    @Test
    fun `setVoiceEnabled should call AudioClient setVoiceFocusNoiseSuppression when audio client status is started`() {
        setupStartTests()
        audioClientController.start(
            testAudioFallbackUrl,
            testAudioHostUrl,
            testMeetingId,
            testAttendeeId,
            testJoinToken,
            AudioMode.Stereo48K,
            AudioStreamType.VoiceCall,
            AudioRecordingPresetOverride.None
        )

        val enableOutput: Boolean = audioClientController.setVoiceFocusEnabled(true)
        verify(exactly = 1) { mockAudioClient.setVoiceFocusNoiseSuppression(true) }
        assertTrue(enableOutput)

        val disableOutput: Boolean = audioClientController.setVoiceFocusEnabled(false)
        verify(exactly = 1) { mockAudioClient.setVoiceFocusNoiseSuppression(false) }
        assertTrue(disableOutput)
    }

    @Test
    fun `setVoiceEnabled should not call AudioClient setVoiceFocusNoiseSuppression when audio client status is not started`() {
        setupStartTests()

        val enableOutput: Boolean = audioClientController.setVoiceFocusEnabled(true)
        verify(exactly = 0) { mockAudioClient.setVoiceFocusNoiseSuppression(any()) }
        assertFalse(enableOutput)

        val disableOutput: Boolean = audioClientController.setVoiceFocusEnabled(false)
        verify(exactly = 0) { mockAudioClient.setVoiceFocusNoiseSuppression(any()) }
        assertFalse(disableOutput)
    }

    @Test
    fun `isVoiceEnabled should call AudioClient getVoiceFocusNoiseSuppression when audio client status is started`() {
        setupStartTests()
        audioClientController.start(
            testAudioFallbackUrl,
            testAudioHostUrl,
            testMeetingId,
            testAttendeeId,
            testJoinToken,
            AudioMode.Stereo48K,
            AudioStreamType.VoiceCall,
            AudioRecordingPresetOverride.None
        )

        audioClientController.isVoiceFocusEnabled()
        verify(exactly = 1) { mockAudioClient.getVoiceFocusNoiseSuppression() }
    }

    @Test
    fun `isVoiceEnabled should not call AudioClient getVoiceFocusNoiseSuppression when audio client status is not started`() {
        setupStartTests()

        audioClientController.isVoiceFocusEnabled()
        verify(exactly = 0) { mockAudioClient.getVoiceFocusNoiseSuppression() }
    }

    @Test
    fun `start with default recording preset when no override provided`() {
        setupStartTests()

        audioClientController.start(
                testAudioFallbackUrl,
                testAudioHostUrl,
                testMeetingId,
                testAttendeeId,
                testJoinToken,
                AudioMode.Mono16K,
                AudioStreamType.VoiceCall,
                AudioRecordingPresetOverride.None
        )

        verify {
            mockAudioClient.startSession(withArg {
                assertEquals(AudioRecordingPreset.VOICE_COMMUNICATION, it.audioRecordingPreset)
            })
        }
    }

    @Test
    fun `start with recording preset of GENERIC if provided by builder`() {
        setupStartTests()

        audioClientController.start(
                testAudioFallbackUrl,
                testAudioHostUrl,
                testMeetingId,
                testAttendeeId,
                testJoinToken,
                AudioMode.Mono16K,
                AudioStreamType.VoiceCall,
                AudioRecordingPresetOverride.Generic
        )

        verify {
            mockAudioClient.startSession(withArg {
                assertEquals(AudioRecordingPreset.GENERIC, it.audioRecordingPreset)
            })
        }
    }

    @Test
    fun `start with recording preset of CAMCORDER if provided by builder`() {
        setupStartTests()

        audioClientController.start(
                testAudioFallbackUrl,
                testAudioHostUrl,
                testMeetingId,
                testAttendeeId,
                testJoinToken,
                AudioMode.Mono16K,
                AudioStreamType.VoiceCall,
                AudioRecordingPresetOverride.Camcorder
        )

        verify {
            mockAudioClient.startSession(withArg {
                assertEquals(AudioRecordingPreset.CAMCORDER, it.audioRecordingPreset)
            })
        }
    }

    @Test
    fun `start with recording preset of VOICE_RECOGNITION if provided by builder`() {
        setupStartTests()

        audioClientController.start(
                testAudioFallbackUrl,
                testAudioHostUrl,
                testMeetingId,
                testAttendeeId,
                testJoinToken,
                AudioMode.Mono16K,
                AudioStreamType.VoiceCall,
                AudioRecordingPresetOverride.VoiceRecognition
        )

        verify {
            mockAudioClient.startSession(withArg {
                assertEquals(AudioRecordingPreset.VOICE_RECOGNITION, it.audioRecordingPreset)
            })
        }
    }

    @Test
    fun `start with recording preset of VOICE_COMMUNICATION if provided by builder`() {
        setupStartTests()

        audioClientController.start(
                testAudioFallbackUrl,
                testAudioHostUrl,
                testMeetingId,
                testAttendeeId,
                testJoinToken,
                AudioMode.Mono16K,
                AudioStreamType.VoiceCall,
                AudioRecordingPresetOverride.VoiceCommunication
        )

        verify {
            mockAudioClient.startSession(withArg {
                assertEquals(AudioRecordingPreset.VOICE_COMMUNICATION, it.audioRecordingPreset)
            })
        }
    }
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.internal.audio

import android.util.Log
import com.amazonaws.services.chime.sdk.meetings.TestConstant
import com.amazonaws.services.chime.sdk.meetings.analytics.EventAnalyticsController
import com.amazonaws.services.chime.sdk.meetings.analytics.EventName
import com.amazonaws.services.chime.sdk.meetings.analytics.MeetingHistoryEventName
import com.amazonaws.services.chime.sdk.meetings.analytics.MeetingStatsCollector
import com.amazonaws.services.chime.sdk.meetings.audiovideo.AttendeeInfo
import com.amazonaws.services.chime.sdk.meetings.audiovideo.AudioVideoObserver
import com.amazonaws.services.chime.sdk.meetings.audiovideo.SignalStrength
import com.amazonaws.services.chime.sdk.meetings.audiovideo.SignalUpdate
import com.amazonaws.services.chime.sdk.meetings.audiovideo.Transcript
import com.amazonaws.services.chime.sdk.meetings.audiovideo.TranscriptAlternative
import com.amazonaws.services.chime.sdk.meetings.audiovideo.TranscriptEntity
import com.amazonaws.services.chime.sdk.meetings.audiovideo.TranscriptItem
import com.amazonaws.services.chime.sdk.meetings.audiovideo.TranscriptItemType
import com.amazonaws.services.chime.sdk.meetings.audiovideo.TranscriptLanguageWithScore
import com.amazonaws.services.chime.sdk.meetings.audiovideo.TranscriptResult
import com.amazonaws.services.chime.sdk.meetings.audiovideo.TranscriptionStatus
import com.amazonaws.services.chime.sdk.meetings.audiovideo.TranscriptionStatusType
import com.amazonaws.services.chime.sdk.meetings.audiovideo.VolumeLevel
import com.amazonaws.services.chime.sdk.meetings.audiovideo.VolumeUpdate
import com.amazonaws.services.chime.sdk.meetings.internal.AttendeeStatus
import com.amazonaws.services.chime.sdk.meetings.internal.SessionStateControllerAction
import com.amazonaws.services.chime.sdk.meetings.internal.metric.ClientMetricsCollector
import com.amazonaws.services.chime.sdk.meetings.realtime.RealtimeObserver
import com.amazonaws.services.chime.sdk.meetings.realtime.TranscriptEventObserver
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionConfiguration
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionStatus
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionStatusCode
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger
import com.xodee.client.audio.audioclient.AttendeeInfo as AttendeeInfoInternal
import com.xodee.client.audio.audioclient.AttendeeUpdate
import com.xodee.client.audio.audioclient.AudioClient
import com.xodee.client.audio.audioclient.transcript.Transcript as TranscriptInternal
import com.xodee.client.audio.audioclient.transcript.TranscriptAlternative as TranscriptAlternativeInternal
import com.xodee.client.audio.audioclient.transcript.TranscriptEntity as TranscriptEntityInternal
import com.xodee.client.audio.audioclient.transcript.TranscriptEvent as TranscriptEventInternal
import com.xodee.client.audio.audioclient.transcript.TranscriptItem as TranscriptItemInternal
import com.xodee.client.audio.audioclient.transcript.TranscriptItemType as TranscriptItemTypeInternal
import com.xodee.client.audio.audioclient.transcript.TranscriptLanguageWithScore as TranscriptLanguageWithScoreInternal
import com.xodee.client.audio.audioclient.transcript.TranscriptResult as TranscriptResultInternal
import com.xodee.client.audio.audioclient.transcript.TranscriptionStatus as TranscriptionStatusInternal
import com.xodee.client.audio.audioclient.transcript.TranscriptionStatusType as TranscriptionStatusTypeInternal
import io.mockk.MockKAnnotations
import io.mockk.coVerify
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.mockkStatic
import io.mockk.verify
import io.mockk.verifyOrder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runBlockingTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Before
import org.junit.Test

@ExperimentalCoroutinesApi
class DefaultAudioClientObserverTest {
    @MockK
    private lateinit var mockLogger: Logger

    @MockK
    private lateinit var mockClientMetricsCollector: ClientMetricsCollector

    @MockK
    private lateinit var mockConfiguration: MeetingSessionConfiguration

    @MockK
    private lateinit var mockAudioVideoObserver: AudioVideoObserver

    @MockK
    private lateinit var mockRealtimeObserver: RealtimeObserver

    @MockK
    private lateinit var mockTranscriptEventObserver: TranscriptEventObserver

    @MockK
    private lateinit var mockAudioClient: AudioClient

    @MockK
    private lateinit var mockMeetingStatsCollector: MeetingStatsCollector

    @MockK
    private lateinit var mockEventAnalyticsController: EventAnalyticsController

    private lateinit var audioClientObserver: DefaultAudioClientObserver

    private val testObserverFun = { observer: AudioVideoObserver ->
        observer.onAudioSessionStartedConnecting(
            false
        )
    }

    private val testId1 = "aliceId"
    private val testId2 = "bobId"
    private val testIdLocal = "me myself & I"
    private val invalidInput = 1000

    private val testAttendeeVolumeUpdate =
        AttendeeUpdate(testId1, testId1, VolumeLevel.High.value)
    private val testAttendeeSignalUpdate =
        AttendeeUpdate(testId1, testId1, SignalStrength.High.value)
    private val testAttendeeVolumeMuted =
        AttendeeUpdate(testId1, testId1, VolumeLevel.Muted.value)
    private val testAttendeeUpdateJoined =
        AttendeeUpdate(testId1, testId1, AttendeeStatus.Joined.value)
    private val testAttendeeUpdateLeft =
        AttendeeUpdate(testId1, testId1, AttendeeStatus.Left.value)
    private val testAttendeeUpdateDropped =
        AttendeeUpdate(testId1, testId1, AttendeeStatus.Dropped.value)
    private val testAttendeeInfo =
        AttendeeInfo(testId1, testId1)
    private val testVolumeUpdate =
        VolumeUpdate(
            testAttendeeInfo,
            VolumeLevel.High
        )
    private val testSignalUpdate =
        SignalUpdate(
            testAttendeeInfo,
            SignalStrength.High
        )
    private val testDispatcher = TestCoroutineDispatcher()

    private val expectedAttendeeInfos: Array<AttendeeInfo> = arrayOf(testAttendeeInfo)

    private val timestampMs = 1633040215L
    private val transcriptionRegion = "us-east-1"
    private val transcriptionConfiguration = "transcription-configuration"

    @Before
    fun setup() {
        // Mock Log.d first because initializing the AudioClient mock appears to fail otherwise
        mockkStatic(Log::class)
        every { Log.d(any(), any()) } returns 0
        MockKAnnotations.init(this, relaxUnitFun = true)
        audioClientObserver =
            DefaultAudioClientObserver(
                mockLogger,
                mockClientMetricsCollector,
                mockConfiguration,
                mockMeetingStatsCollector,
                mockEventAnalyticsController
            )
        audioClientObserver.subscribeToAudioClientStateChange(mockAudioVideoObserver)
        audioClientObserver.subscribeToRealTimeEvents(mockRealtimeObserver)
        audioClientObserver.audioClient = mockAudioClient
        every { mockConfiguration.credentials.attendeeId } returns testIdLocal
        every { mockConfiguration.credentials.externalUserId } returns testIdLocal

        Dispatchers.setMain(testDispatcher)
    }

    @After
    fun tearDown() {
        Dispatchers.resetMain()
        testDispatcher.cleanupTestCoroutines()
    }

    @Test
    fun `notifyAudioClientObserver should notify added observers`() {
        audioClientObserver.notifyAudioClientObserver(testObserverFun)

        verify(exactly = 1) { mockAudioVideoObserver.onAudioSessionStartedConnecting(any()) }
    }

    @Test
    fun `unsubscribeFromAudioClientStateChange should result in no notification`() {
        audioClientObserver.unsubscribeFromAudioClientStateChange(mockAudioVideoObserver)

        audioClientObserver.notifyAudioClientObserver(testObserverFun)

        verifyAudioVideoObserverIsNotNotified()
    }

    @Test
    fun `onAttendeesPresenceChange should NOT notify about attendee presence events when NO attendees updates`() {
        audioClientObserver.onAttendeesPresenceChange(emptyArray())

        verify(exactly = 0) { mockRealtimeObserver.onAttendeesJoined(any()) }
        verify(exactly = 0) { mockRealtimeObserver.onAttendeesLeft(any()) }
        verify(exactly = 0) { mockRealtimeObserver.onAttendeesDropped(any()) }
    }

    @Test
    fun `onAttendeesPresenceChange should notify added observers when a new attendee joined`() {
        audioClientObserver.onAttendeesPresenceChange(arrayOf(
            testAttendeeUpdateJoined
        ))

        verify(exactly = 1) { mockRealtimeObserver.onAttendeesJoined(expectedAttendeeInfos) }
    }

    @Test
    fun `onAttendeesPresenceChange should notify added observers when an attendee left`() {
        audioClientObserver.onAttendeesPresenceChange(arrayOf(
            testAttendeeUpdateLeft
        ))

        verify(exactly = 1) { mockRealtimeObserver.onAttendeesLeft(expectedAttendeeInfos) }
    }

    @Test
    fun `onAttendeesPresenceChange should notify added observers when an attendee got dropped`() {
        audioClientObserver.onAttendeesPresenceChange(arrayOf(
            testAttendeeUpdateDropped
        ))

        verify(exactly = 1) { mockRealtimeObserver.onAttendeesDropped(expectedAttendeeInfos) }
    }

    @Test
    fun `onAttendeesPresenceChange should NOT notify added observers twice when duplicate attendee joined`() {
        audioClientObserver.onAttendeesPresenceChange(arrayOf(
            testAttendeeUpdateJoined
        ))
        audioClientObserver.onAttendeesPresenceChange(arrayOf(
            testAttendeeUpdateJoined
        ))

        verify(exactly = 1) { mockRealtimeObserver.onAttendeesJoined(expectedAttendeeInfos) }
    }

    @Test
    fun `onAttendeesPresenceChange should notify added observers when attendee who had left rejoined`() {
        audioClientObserver.onAttendeesPresenceChange(arrayOf(
            testAttendeeUpdateJoined
        ))
        audioClientObserver.onAttendeesPresenceChange(arrayOf(
            testAttendeeUpdateLeft
        ))
        audioClientObserver.onAttendeesPresenceChange(arrayOf(
            testAttendeeUpdateJoined
        ))

        verify(exactly = 2) { mockRealtimeObserver.onAttendeesJoined(expectedAttendeeInfos) }
    }

    @Test
    fun `onAttendeesPresenceChange should notify added observers with rejoined attendee who has dropped`() {
        audioClientObserver.onAttendeesPresenceChange(arrayOf(
            testAttendeeUpdateJoined
        ))
        audioClientObserver.onAttendeesPresenceChange(arrayOf(
            testAttendeeUpdateDropped
        ))
        audioClientObserver.onAttendeesPresenceChange(arrayOf(
            testAttendeeUpdateJoined
        ))

        verify(exactly = 2) { mockRealtimeObserver.onAttendeesJoined(expectedAttendeeInfos) }
    }

    @Test
    fun `onAttendeesPresenceChange should NOT consider attendee as same only when externalUserId is same`() {
        audioClientObserver.onAttendeesPresenceChange(arrayOf(testAttendeeUpdateJoined))
        audioClientObserver.onAttendeesPresenceChange(
            arrayOf(
                AttendeeUpdate(
                    testId2,
                    testAttendeeUpdateJoined.externalUserId,
                    testAttendeeUpdateJoined.data
                )
            )
        )

        verifyOrder {
            mockRealtimeObserver.onAttendeesJoined(arrayOf(testAttendeeInfo))
            mockRealtimeObserver.onAttendeesJoined(
                arrayOf(
                    AttendeeInfo(
                        testId2,
                        testAttendeeInfo.externalUserId
                    )
                )
            )
        }
    }

    @Test
    fun `onAttendeesPresenceChange should consider attendee as same when attendeeId and externalUserId are same`() {
        audioClientObserver.onAttendeesPresenceChange(arrayOf(testAttendeeUpdateJoined))
        audioClientObserver.onAttendeesPresenceChange(
            arrayOf(
                AttendeeUpdate(
                    testAttendeeUpdateJoined.profileId,
                    testAttendeeUpdateJoined.externalUserId,
                    testAttendeeUpdateJoined.data
                )
            )
        )

        verify(exactly = 1) { mockRealtimeObserver.onAttendeesJoined(arrayOf(testAttendeeInfo)) }
    }

    @Test
    fun `onAttendeesPresenceChange should consider attendee as different when externalUserId is different`() {
        audioClientObserver.onAttendeesPresenceChange(arrayOf(testAttendeeUpdateJoined))
        audioClientObserver.onAttendeesPresenceChange(
            arrayOf(
                AttendeeUpdate(
                    testAttendeeUpdateJoined.profileId,
                    testId2,
                    testAttendeeUpdateJoined.data
                )
            )
        )

        verifyOrder {
            mockRealtimeObserver.onAttendeesJoined(arrayOf(testAttendeeInfo))
            mockRealtimeObserver.onAttendeesJoined(
                arrayOf(
                    AttendeeInfo(
                        testAttendeeInfo.attendeeId,
                        testId2
                    )
                )
            )
        }
    }

    @Test
    fun `onAttendeesPresenceChange should ONLY notify delta on subsequent calls`() {
        audioClientObserver.onAttendeesPresenceChange(arrayOf(testAttendeeUpdateJoined))
        audioClientObserver.onAttendeesPresenceChange(
            arrayOf(
                testAttendeeUpdateJoined,
                AttendeeUpdate(
                    testId2,
                    testId2,
                    testAttendeeUpdateJoined.data
                )
            )
        )

        verifyOrder {
            mockRealtimeObserver.onAttendeesJoined(arrayOf(testAttendeeInfo))
            mockRealtimeObserver.onAttendeesJoined(
                arrayOf(
                    AttendeeInfo(
                        testId2,
                        testId2
                    )
                )
            )
        }
    }

    @Test
    fun `onAttendeesPresenceChange should send local attendee event with non-empty externalUserId`() {
        val testInput = arrayOf(
            AttendeeUpdate(testIdLocal, "", AttendeeStatus.Joined.value)
        )
        val expectedArgs: Array<AttendeeInfo> = arrayOf(
            AttendeeInfo(testIdLocal, testIdLocal)
        )

        audioClientObserver.onAttendeesPresenceChange(testInput)

        verify(exactly = 1) { mockRealtimeObserver.onAttendeesJoined(expectedArgs) }
    }

    @Test
    fun `onVolumeStateChange should notify added observers`() {
        audioClientObserver.onVolumeStateChange(arrayOf(testAttendeeVolumeUpdate))

        verify(exactly = 1) { mockRealtimeObserver.onVolumeChanged(any()) }
    }

    @Test
    fun `onVolumeStateChange should NOT notify when no attendee updates`() {
        audioClientObserver.onVolumeStateChange(null)

        verify(exactly = 0) { mockRealtimeObserver.onVolumeChanged(any()) }
    }

    @Test
    fun `onVolumeStateChange should only notify delta`() {
        val testAttendeeUpdate2 =
            AttendeeUpdate(testId2, testId2, VolumeLevel.Low.value)
        val expectedArgs1 = arrayOf(testVolumeUpdate)
        val expectedArgs2 = arrayOf(
            VolumeUpdate(
                AttendeeInfo(testId2, testId2),
                VolumeLevel.Low
            )
        )

        audioClientObserver.onVolumeStateChange(arrayOf(testAttendeeVolumeUpdate))
        audioClientObserver.onVolumeStateChange(
            arrayOf(
                testAttendeeVolumeUpdate,
                testAttendeeUpdate2
            )
        )

        verifyOrder {
            mockRealtimeObserver.onVolumeChanged(expectedArgs1)
            mockRealtimeObserver.onVolumeChanged(expectedArgs2)
        }
    }

    @Test
    fun `onVolumeChange should send remote attendee event when it has an empty externalUserId`() {
        val testInput = arrayOf(
            testAttendeeVolumeUpdate,
            AttendeeUpdate(testId2, "", VolumeLevel.Low.value)
        )
        val expectedArgs: Array<VolumeUpdate> = arrayOf(
            testVolumeUpdate,
            VolumeUpdate(AttendeeInfo(testId2, ""), VolumeLevel.Low)
        )

        audioClientObserver.onVolumeStateChange(testInput)

        verify(exactly = 1) { mockRealtimeObserver.onVolumeChanged(expectedArgs) }
    }

    @Test
    fun `onVolumeChange should filter out invalid volume levels`() {
        val testInput = arrayOf(
            testAttendeeVolumeUpdate,
            AttendeeUpdate(testId2, testId2, invalidInput)
        )
        val expectedArgs: Array<VolumeUpdate> = arrayOf(testVolumeUpdate)

        audioClientObserver.onVolumeStateChange(testInput)

        verify(exactly = 1) { mockRealtimeObserver.onVolumeChanged(expectedArgs) }
    }

    @Test
    fun `onSignalStrengthChange should notify added observers`() {
        audioClientObserver.onSignalStrengthChange(arrayOf(testAttendeeSignalUpdate))

        verify(exactly = 1) { mockRealtimeObserver.onSignalStrengthChanged(any()) }
    }

    @Test
    fun `onSignalStrengthChange should NOT notify when no attendee updates`() {
        audioClientObserver.onSignalStrengthChange(null)

        verify(exactly = 0) { mockRealtimeObserver.onSignalStrengthChanged(any()) }
    }

    @Test
    fun `onSignalStrengthChange should only notify delta`() {
        val testAttendeeUpdate2 =
            AttendeeUpdate(testId2, testId2, SignalStrength.Low.value)
        val expectedArgs1 = arrayOf(testSignalUpdate)
        val expectedArgs2 =
            arrayOf(
                SignalUpdate(
                    AttendeeInfo(
                        testId2,
                        testId2
                    ), SignalStrength.Low
                )
            )

        audioClientObserver.onSignalStrengthChange(arrayOf(testAttendeeSignalUpdate))
        audioClientObserver.onSignalStrengthChange(
            arrayOf(
                testAttendeeSignalUpdate,
                testAttendeeUpdate2
            )
        )

        verifyOrder {
            mockRealtimeObserver.onSignalStrengthChanged(expectedArgs1)
            mockRealtimeObserver.onSignalStrengthChanged(expectedArgs2)
        }
    }

    @Test
    fun `onSignalStrengthChange should send remote attendee event when it has an empty externalUserId`() {
        val testInput = arrayOf(
            testAttendeeSignalUpdate,
            AttendeeUpdate(testId2, "", SignalStrength.High.value)
        )
        val expectedArgs: Array<SignalUpdate> = arrayOf(
            testSignalUpdate,
            SignalUpdate(AttendeeInfo(testId2, ""), SignalStrength.High)
        )

        audioClientObserver.onSignalStrengthChange(testInput)

        verify(exactly = 1) { mockRealtimeObserver.onSignalStrengthChanged(expectedArgs) }
    }

    @Test
    fun `onSignalStrengthChange should filter out invalid signal strengths`() {
        val testInput = arrayOf(
            testAttendeeSignalUpdate,
            AttendeeUpdate(testId2, testId2, invalidInput)
        )
        val expectedArgs: Array<SignalUpdate> = arrayOf(testSignalUpdate)

        audioClientObserver.onSignalStrengthChange(testInput)

        verify(exactly = 1) { mockRealtimeObserver.onSignalStrengthChanged(expectedArgs) }
    }

    @Test
    fun `onSignalStrengthChange should send local attendee event with non-empty externalUserId`() {
        val testInput = arrayOf(
            AttendeeUpdate(testIdLocal, "", SignalStrength.High.value)
        )
        val expectedArgs: Array<SignalUpdate> = arrayOf(
            SignalUpdate(AttendeeInfo(testIdLocal, testIdLocal), SignalStrength.High)
        )

        audioClientObserver.onSignalStrengthChange(testInput)

        verify(exactly = 1) { mockRealtimeObserver.onSignalStrengthChanged(expectedArgs) }
    }

    @Test
    fun `onVolumeStateChange should notify about attendee mute when newly muted attendees`() {
        audioClientObserver.onVolumeStateChange(arrayOf(testAttendeeVolumeMuted))

        verify(exactly = 1) { mockRealtimeObserver.onAttendeesMuted(any()) }
    }

    @Test
    fun `onVolumeStateChange should NOT notify about attendee mute when NO newly muted attendees`() {
        audioClientObserver.onVolumeStateChange(arrayOf(testAttendeeVolumeUpdate))

        verify(exactly = 0) { mockRealtimeObserver.onAttendeesMuted(any()) }
    }

    @Test
    fun `onVolumeStateChange should notify about attendee unmute newly unmuted attendees`() {
        audioClientObserver.onVolumeStateChange(arrayOf(testAttendeeVolumeMuted))
        audioClientObserver.onVolumeStateChange(arrayOf(testAttendeeVolumeUpdate))

        verify(exactly = 1) { mockRealtimeObserver.onAttendeesUnmuted(any()) }
    }

    @Test
    fun `onVolumeStateChange should NOT notify about attendee unmute when NO newly unmuted attendees`() {
        audioClientObserver.onVolumeStateChange(arrayOf(testAttendeeVolumeMuted))
        audioClientObserver.onVolumeStateChange(arrayOf(testAttendeeVolumeMuted))

        verify(exactly = 0) { mockRealtimeObserver.onAttendeesUnmuted(any()) }
    }

    @Test
    fun `onVolumeStateChange should send local attendee event with non-empty externalUserId`() {
        val testInput = arrayOf(
            AttendeeUpdate(testIdLocal, "", VolumeLevel.High.value)
        )
        val expectedArgs: Array<VolumeUpdate> = arrayOf(
            VolumeUpdate(AttendeeInfo(testIdLocal, testIdLocal), VolumeLevel.High)
        )

        audioClientObserver.onVolumeStateChange(testInput)

        verify(exactly = 1) { mockRealtimeObserver.onVolumeChanged(expectedArgs) }
    }

    @Test
    fun `onTranscriptEventsReceived should do nothing if transcript events is sent as null`() {
        audioClientObserver.subscribeToTranscriptEvent(mockTranscriptEventObserver)

        audioClientObserver.onTranscriptEventsReceived(null)

        verify(exactly = 0) { mockTranscriptEventObserver.onTranscriptEventReceived(any()) }
    }

    @Test
    fun `onTranscriptEventsReceived should send local Transcription Status with TranscriptionStatus events as input`() {
        val successStatusMessage = "success-message"
        val resumedStatusMessage = "resumed-message"

        audioClientObserver.subscribeToTranscriptEvent(mockTranscriptEventObserver)

        val transcriptionStatusStarted = TranscriptionStatusInternal(
            TranscriptionStatusTypeInternal.TranscriptionStatusTypeStarted,
            timestampMs,
            transcriptionRegion,
            transcriptionConfiguration,
            successStatusMessage
        )

        val transcriptionStatusResumed = TranscriptionStatusInternal(
            TranscriptionStatusTypeInternal.TranscriptionStatusTypeResumed,
            timestampMs,
            transcriptionRegion,
            transcriptionConfiguration,
            resumedStatusMessage
        )

        val expectedTranscriptionStatusStarted = TranscriptionStatus(
            TranscriptionStatusType.Started,
            timestampMs,
            transcriptionRegion,
            transcriptionConfiguration,
            successStatusMessage
        )

        val expectedTranscriptionStatusResume = TranscriptionStatus(
            TranscriptionStatusType.Resumed,
            timestampMs,
            transcriptionRegion,
            transcriptionConfiguration,
            resumedStatusMessage
        )

        val events: Array<TranscriptEventInternal> = arrayOf(transcriptionStatusStarted, transcriptionStatusResumed)

        audioClientObserver.onTranscriptEventsReceived(events)

        verify(exactly = 1) { mockTranscriptEventObserver.onTranscriptEventReceived(expectedTranscriptionStatusStarted) }
        verify(exactly = 1) { mockTranscriptEventObserver.onTranscriptEventReceived(expectedTranscriptionStatusResume) }
    }

    @Test
    fun `onTranscriptEventsReceived should send local Transcript with Transcript events as input`() {
        audioClientObserver.subscribeToTranscriptEvent(mockTranscriptEventObserver)

        val testResultId = "testResultId"
        val testChannelId = "testChannelId"
        val isPartial = true

        val transcriptResultOne = TranscriptResultInternal(
            testResultId,
            testChannelId,
            isPartial,
            timestampMs,
            timestampMs + 10L,
            arrayOf(
                TranscriptAlternativeInternal(
                    arrayOf(
                        TranscriptItemInternal(
                            TranscriptItemTypeInternal.TranscriptItemTypePronunciation,
                            timestampMs,
                            timestampMs + 5L,
                            AttendeeInfoInternal(testId1, testId1),
                            "I",
                            true,
                            true,
                            0.0
                        ),
                        TranscriptItemInternal(
                            TranscriptItemTypeInternal.TranscriptItemTypePunctuation,
                            timestampMs + 5L,
                            timestampMs + 10L,
                            AttendeeInfoInternal(testId2, testId2),
                            "am",
                            false,
                            true,
                            0.0
                        )
                    ),
                    arrayOf(),
                    "I am"
                )
            ),
            null,
            null
        )

        val transcriptResultTwo = TranscriptResultInternal(
            testResultId,
            testChannelId,
            isPartial,
            timestampMs + 10L,
            timestampMs + 20L,
            arrayOf(
                TranscriptAlternativeInternal(
                    arrayOf(
                        TranscriptItemInternal(
                            TranscriptItemTypeInternal.TranscriptItemTypePunctuation,
                            timestampMs + 10,
                            timestampMs + 15L,
                            AttendeeInfoInternal(testId2, testId2),
                            "a",
                            true,
                            true,
                            0.0
                        ),
                        TranscriptItemInternal(
                            TranscriptItemTypeInternal.TranscriptItemTypePronunciation,
                            timestampMs + 15L,
                            timestampMs + 20L,
                            AttendeeInfoInternal(testId1, testId1),
                            "guardian",
                            false,
                            true,
                            0.0
                        )
                    ),
                    arrayOf(),
                    "a guardian"
                )
            ),
            null,
            null
        )

        val events: Array<TranscriptEventInternal> = arrayOf(
            TranscriptInternal(arrayOf(transcriptResultOne)),
            TranscriptInternal(arrayOf(transcriptResultTwo))
        )

        val expectedTranscriptOne = Transcript(
            arrayOf(
                TranscriptResult(
                    testResultId,
                    testChannelId,
                    isPartial,
                    timestampMs,
                    timestampMs + 10L,
                    arrayOf(
                        TranscriptAlternative(
                            arrayOf(
                                TranscriptItem(
                                    TranscriptItemType.Pronunciation,
                                    timestampMs,
                                    timestampMs + 5L,
                                    AttendeeInfo(testId1, testId1),
                                    "I",
                                    true,
                                    0.0,
                                    true
                                ),
                                TranscriptItem(
                                    TranscriptItemType.Punctuation,
                                    timestampMs + 5L,
                                    timestampMs + 10L,
                                    AttendeeInfo(testId2, testId2),
                                    "am",
                                    false,
                                    0.0,
                                    true
                                )
                            ),
                            arrayOf(),
                            "I am"
                        )
                    ),
                    null,
                    null
                )
            )
        )

        val expectedTranscriptTwo = Transcript(
            arrayOf(
                TranscriptResult(
                    testResultId,
                    testChannelId,
                    isPartial,
                    timestampMs + 10L,
                    timestampMs + 20L,
                    arrayOf(
                        TranscriptAlternative(
                            arrayOf(
                                TranscriptItem(
                                    TranscriptItemType.Punctuation,
                                    timestampMs + 10L,
                                    timestampMs + 15L,
                                    AttendeeInfo(testId2, testId2),
                                    "a",
                                    true,
                                    0.0,
                                    true
                                ),
                                TranscriptItem(
                                    TranscriptItemType.Pronunciation,
                                    timestampMs + 15L,
                                    timestampMs + 20L,
                                    AttendeeInfo(testId1, testId1),
                                    "guardian",
                                    false,
                                    0.0,
                                    true
                                )
                            ),
                            arrayOf(),
                            "a guardian"
                        )
                    ),
                    null,
                    null
                )
            )
        )

        audioClientObserver.onTranscriptEventsReceived(events)
        verify(exactly = 1) { mockTranscriptEventObserver.onTranscriptEventReceived(expectedTranscriptOne) }
        verify(exactly = 1) { mockTranscriptEventObserver.onTranscriptEventReceived(expectedTranscriptTwo) }
    }

    @Test
    fun `onTranscriptEventsReceived should send local Transcript with Transcript Entity events as input`() {
        audioClientObserver.subscribeToTranscriptEvent(mockTranscriptEventObserver)

        val testResultId = "testResultId"
        val testChannelId = "testChannelId"
        val isPartial = true

        val transcriptResultItem = TranscriptItemInternal(TranscriptItemTypeInternal.TranscriptItemTypePronunciation,
            timestampMs, timestampMs + 5L, AttendeeInfoInternal(testId1, testId1), "I", true, true, 0.0)

        val transcriptResultEntity = TranscriptEntityInternal("PII", "NAME", "John Doe", 1.0, timestampMs + 5L, timestampMs + 10L)

        val transcriptResultAlternative = TranscriptAlternativeInternal(arrayOf(transcriptResultItem), arrayOf(transcriptResultEntity), "I am")

        val transcriptResult = TranscriptResultInternal(testResultId, testChannelId, isPartial, timestampMs, timestampMs + 10L, arrayOf(transcriptResultAlternative), null, arrayOf())

        val events: Array<TranscriptEventInternal> = arrayOf(TranscriptInternal(arrayOf(transcriptResult)))

        val expectedTranscriptItem = TranscriptItem(TranscriptItemType.Pronunciation, timestampMs, timestampMs + 5L,
            AttendeeInfo(testId1, testId1), "I", true, 0.0, true)

        val expectedTranscriptEntity = TranscriptEntity("NAME", 1.0, "John Doe", timestampMs + 5L, timestampMs + 10L, "PII")

        val expectedTranscriptAlternative = TranscriptAlternative(arrayOf(expectedTranscriptItem), arrayOf(expectedTranscriptEntity),
            "I am")

        val expectedTranscript = Transcript(arrayOf(TranscriptResult(testResultId, testChannelId, isPartial,
            timestampMs, timestampMs + 10L, arrayOf(expectedTranscriptAlternative), null, arrayOf())))

        audioClientObserver.onTranscriptEventsReceived(events)
        verify(exactly = 1) { mockTranscriptEventObserver.onTranscriptEventReceived(expectedTranscript) }
    }

    @Test
    fun `onTranscriptEventsReceived should send local Transcript with Transcript LanguageWithScore events as input`() {
        audioClientObserver.subscribeToTranscriptEvent(mockTranscriptEventObserver)

        val testResultId = "testResultId"
        val testChannelId = "testChannelId"
        val isPartial = true
        val languageCode = "en-US"

        val transcriptResultItem = TranscriptItemInternal(TranscriptItemTypeInternal.TranscriptItemTypePronunciation,
            timestampMs, timestampMs + 5L, AttendeeInfoInternal(testId1, testId1), "I", true, true, 0.0)

        val transcriptLanguageWithScoreOne = TranscriptLanguageWithScoreInternal("en-US", 0.78)

        val transcriptLanguageWithScoreTwo = TranscriptLanguageWithScoreInternal("ja-JP", 0.22)

        val transcriptResultAlternative = TranscriptAlternativeInternal(arrayOf(transcriptResultItem), null, "I am")

        val transcriptResult = TranscriptResultInternal(testResultId, testChannelId, isPartial, timestampMs, timestampMs + 10L, arrayOf(transcriptResultAlternative), languageCode, arrayOf(transcriptLanguageWithScoreOne, transcriptLanguageWithScoreTwo))

        val events: Array<TranscriptEventInternal> = arrayOf(TranscriptInternal(arrayOf(transcriptResult)))

        val expectedTranscriptItem = TranscriptItem(TranscriptItemType.Pronunciation, timestampMs, timestampMs + 5L,
            AttendeeInfo(testId1, testId1), "I", true, 0.0, true)

        val expectedTranscriptLanguageWithScoreOne = TranscriptLanguageWithScore("en-US", 0.78)

        val expectedTranscriptLanguageWithScoreTwo = TranscriptLanguageWithScore("ja-JP", 0.22)

        val expectedTranscriptAlternative = TranscriptAlternative(arrayOf(expectedTranscriptItem), null, "I am")

        val expectedTranscript = Transcript(arrayOf(TranscriptResult(testResultId, testChannelId, isPartial,
            timestampMs, timestampMs + 10L, arrayOf(expectedTranscriptAlternative), languageCode, arrayOf(expectedTranscriptLanguageWithScoreOne, expectedTranscriptLanguageWithScoreTwo))))

        audioClientObserver.onTranscriptEventsReceived(events)
        verify(exactly = 1) { mockTranscriptEventObserver.onTranscriptEventReceived(expectedTranscript) }
    }

    @Test
    fun `onTranscriptEventsReceived should return if null transcript events is sent`() {
        audioClientObserver.subscribeToTranscriptEvent(mockTranscriptEventObserver)

        audioClientObserver.onTranscriptEventsReceived(null)

        verify(exactly = 0) { mockTranscriptEventObserver.onTranscriptEventReceived(any()) }
    }

    @Test
    fun `unsubscribeFromRealTimeEvents should result in no notification`() {
        audioClientObserver.unsubscribeFromRealTimeEvents(mockRealtimeObserver)

        audioClientObserver.onVolumeStateChange(arrayOf(testAttendeeVolumeUpdate))
        audioClientObserver.onSignalStrengthChange(arrayOf(testAttendeeSignalUpdate))
        audioClientObserver.onAttendeesPresenceChange(arrayOf(testAttendeeUpdateJoined))

        verify(exactly = 0) { mockRealtimeObserver.onVolumeChanged(any()) }
        verify(exactly = 0) { mockRealtimeObserver.onAttendeesJoined(any()) }
        verify(exactly = 0) { mockRealtimeObserver.onAttendeesLeft(any()) }
        verify(exactly = 0) { mockRealtimeObserver.onAttendeesMuted(any()) }
        verify(exactly = 0) { mockRealtimeObserver.onAttendeesUnmuted(any()) }
        verify(exactly = 0) { mockRealtimeObserver.onSignalStrengthChanged(any()) }
    }

    @Test
    fun `onAudioClientStateChange should NOT notify when new state is UNKNOWN`() {
        runBlockingTest {
            audioClientObserver.onAudioClientStateChange(
                100,
                100
            )
        }

        verifyAudioVideoObserverIsNotNotified()
    }

    @Test
    fun `onAudioClientStateChange should notify of session started event when finished connecting`() {
        runBlockingTest {
            audioClientObserver.onAudioClientStateChange(
                AudioClient.AUDIO_CLIENT_STATE_CONNECTING,
                AudioClient.AUDIO_CLIENT_OK
            )

            audioClientObserver.onAudioClientStateChange(
                AudioClient.AUDIO_CLIENT_STATE_CONNECTED,
                AudioClient.AUDIO_CLIENT_OK
            )
        }

        verify(exactly = 1) { mockAudioVideoObserver.onAudioSessionStarted(false) }
    }

    @Test
    fun `onAudioClientStateChange should publish meeting start event when finished connecting`() {
        runBlockingTest {
            audioClientObserver.onAudioClientStateChange(
                AudioClient.AUDIO_CLIENT_STATE_CONNECTING,
                AudioClient.AUDIO_CLIENT_OK
            )

            audioClientObserver.onAudioClientStateChange(
                AudioClient.AUDIO_CLIENT_STATE_CONNECTED,
                AudioClient.AUDIO_CLIENT_OK
            )
        }

        verify(exactly = 1) { mockMeetingStatsCollector.updateMeetingStartTimeMs() }
        verify(exactly = 1) { mockEventAnalyticsController.publishEvent(EventName.meetingStartSucceeded) }
    }

    @Test
    fun `onAudioClientStateChange should should publish meeting start event when finished reconnecting`() {
        runBlockingTest {
            audioClientObserver.onAudioClientStateChange(
                AudioClient.AUDIO_CLIENT_STATE_RECONNECTING,
                AudioClient.AUDIO_CLIENT_OK
            )

            audioClientObserver.onAudioClientStateChange(
                AudioClient.AUDIO_CLIENT_STATE_CONNECTED,
                AudioClient.AUDIO_CLIENT_OK
            )
        }

        verify(exactly = 1) { mockMeetingStatsCollector.incrementRetryCount() }
        verify(exactly = 1) { mockMeetingStatsCollector.updateMeetingStartTimeMs() }
        verify(exactly = 1) { mockEventAnalyticsController.pushHistory(MeetingHistoryEventName.meetingReconnected) }
        verify(exactly = 1) { mockEventAnalyticsController.publishEvent(EventName.meetingStartSucceeded) }
    }

    @Test
    fun `onAudioClientStateChange should notify of session reconnected event when finished reconnecting`() {
        runBlockingTest {
            audioClientObserver.onAudioClientStateChange(
                AudioClient.AUDIO_CLIENT_STATE_RECONNECTING,
                AudioClient.AUDIO_CLIENT_OK
            )

            audioClientObserver.onAudioClientStateChange(
                AudioClient.AUDIO_CLIENT_STATE_CONNECTED,
                AudioClient.AUDIO_CLIENT_OK
            )
        }

        verify(exactly = 1) { mockAudioVideoObserver.onAudioSessionStarted(true) }
    }

    @Test
    fun `onAudioClientStateChange should notify of session reconnect event when start reconnecting`() {
        runBlockingTest {
            audioClientObserver.onAudioClientStateChange(
                AudioClient.AUDIO_CLIENT_STATE_CONNECTED,
                AudioClient.AUDIO_CLIENT_OK
            )

            audioClientObserver.onAudioClientStateChange(
                AudioClient.AUDIO_CLIENT_STATE_RECONNECTING,
                AudioClient.AUDIO_CLIENT_OK
            )
        }

        verify(exactly = 1) { mockAudioVideoObserver.onAudioSessionDropped() }
    }

    @Test
    fun `onAudioClientStateChange should increment poor connection count`() {
        runBlockingTest {
            audioClientObserver.onAudioClientStateChange(
                AudioClient.AUDIO_CLIENT_STATE_CONNECTED,
                AudioClient.AUDIO_CLIENT_OK
            )

            audioClientObserver.onAudioClientStateChange(
                AudioClient.AUDIO_CLIENT_STATE_CONNECTED,
                AudioClient.AUDIO_CLIENT_STATUS_NETWORK_IS_NOT_GOOD_ENOUGH_FOR_VOIP
            )
        }

        verify(exactly = 1) { mockMeetingStatsCollector.incrementPoorConnectionCount() }
    }

    @Test
    fun `onAudioClientStateChange should notify of poor connection event when status changed to poor network`() {
        runBlockingTest {
            audioClientObserver.onAudioClientStateChange(
                AudioClient.AUDIO_CLIENT_STATE_CONNECTED,
                AudioClient.AUDIO_CLIENT_OK
            )

            audioClientObserver.onAudioClientStateChange(
                AudioClient.AUDIO_CLIENT_STATE_CONNECTED,
                AudioClient.AUDIO_CLIENT_STATUS_NETWORK_IS_NOT_GOOD_ENOUGH_FOR_VOIP
            )
        }

        verify(exactly = 1) { mockAudioVideoObserver.onConnectionBecamePoor() }
    }

    @Test
    fun `onAudioClientStateChange should notify of connection recover event when status changed from poor network`() {
        runBlockingTest {
            audioClientObserver.onAudioClientStateChange(
                AudioClient.AUDIO_CLIENT_STATE_CONNECTED,
                AudioClient.AUDIO_CLIENT_STATUS_NETWORK_IS_NOT_GOOD_ENOUGH_FOR_VOIP
            )

            audioClientObserver.onAudioClientStateChange(
                AudioClient.AUDIO_CLIENT_STATE_CONNECTED,
                AudioClient.AUDIO_CLIENT_OK
            )
        }

        verify(exactly = 1) { mockAudioVideoObserver.onConnectionRecovered() }
    }

    @Test
    fun `onAudioClientStateChange should notify of session reconnect cancel event when cancelling reconnect`() {
        runBlockingTest {
            audioClientObserver.onAudioClientStateChange(
                AudioClient.AUDIO_CLIENT_STATE_RECONNECTING,
                AudioClient.AUDIO_CLIENT_OK
            )

            audioClientObserver.onAudioClientStateChange(
                AudioClient.AUDIO_CLIENT_STATE_DISCONNECTED_NORMAL,
                AudioClient.AUDIO_CLIENT_ERR_JOINED_FROM_ANOTHER_DEVICE
            )
        }

        verify(exactly = 1) { mockAudioVideoObserver.onAudioSessionCancelledReconnect() }
    }

    @Test
    fun `onAudioClientStateChange should not notify of session stop event when disconnected`() {
        runBlockingTest {
            audioClientObserver.onAudioClientStateChange(
                AudioClient.AUDIO_CLIENT_STATE_CONNECTING,
                AudioClient.AUDIO_CLIENT_OK
            )
            audioClientObserver.onAudioClientStateChange(
                AudioClient.AUDIO_CLIENT_STATE_DISCONNECTED_NORMAL,
                AudioClient.AUDIO_CLIENT_ERR_CALL_ENDED
            )
        }

        verify(exactly = 0) { mockAudioVideoObserver.onAudioSessionStopped(any()) }
    }

    @Test
    fun `onAudioClientStateChange should stop session and notify of session stop event when failure while connecting`() {
        every { mockAudioClient.stopSession() } returns 0

        runBlockingTest {
            audioClientObserver.onAudioClientStateChange(
                AudioClient.AUDIO_CLIENT_STATE_CONNECTING,
                AudioClient.AUDIO_CLIENT_OK
            )

            audioClientObserver.onAudioClientStateChange(
                AudioClient.AUDIO_CLIENT_STATE_FAILED_TO_CONNECT,
                AudioClient.AUDIO_CLIENT_ERR_CALL_AT_CAPACITY
            )
        }

        verify(exactly = 1, timeout = TestConstant.globalScopeTimeoutMs) { mockAudioVideoObserver.onAudioSessionStopped(any()) }
        verify(exactly = 1, timeout = TestConstant.globalScopeTimeoutMs) { mockAudioClient.stopSession() }
    }

    @Test
    fun `onAudioClientStateChange should notify of session fail event to EventAnalyticsController when failed to connect`() {
        every { mockAudioClient.stopSession() } returns 0

        runBlockingTest {
            audioClientObserver.onAudioClientStateChange(
                AudioClient.AUDIO_CLIENT_STATE_CONNECTING,
                AudioClient.AUDIO_CLIENT_OK
            )

            audioClientObserver.onAudioClientStateChange(
                AudioClient.AUDIO_CLIENT_STATE_FAILED_TO_CONNECT,
                AudioClient.AUDIO_CLIENT_ERR_CALL_AT_CAPACITY
            )
        }

        verify(exactly = 1, timeout = TestConstant.globalScopeTimeoutMs) {
            mockEventAnalyticsController.publishEvent(EventName.meetingFailed, any())
        }
    }

    @Test
    fun `onAudioClientStateChange should stop session and notify of session stop event when failure while reconnecting`() {
        every { mockAudioClient.stopSession() } returns 0

        runBlockingTest {
            audioClientObserver.onAudioClientStateChange(
                AudioClient.AUDIO_CLIENT_STATE_RECONNECTING,
                AudioClient.AUDIO_CLIENT_OK
            )

            audioClientObserver.onAudioClientStateChange(
                AudioClient.AUDIO_CLIENT_STATE_DISCONNECTED_ABNORMAL,
                AudioClient.AUDIO_CLIENT_ERR_SERVICE_UNAVAILABLE
            )
        }
        coVerify(exactly = 1, timeout = TestConstant.globalScopeTimeoutMs) { mockAudioVideoObserver.onAudioSessionStopped(any()) }
        verify(exactly = 1, timeout = TestConstant.globalScopeTimeoutMs) { mockAudioClient.stopSession() }
    }

    @Test
    fun `onAudioClientStateChange should stop session and notify of session stop event when input device is not responding`() {
        every { mockAudioClient.stopSession() } returns 0

        runBlockingTest {
            audioClientObserver.onAudioClientStateChange(
                AudioClient.AUDIO_CLIENT_STATE_RECONNECTING,
                AudioClient.AUDIO_CLIENT_OK
            )

            audioClientObserver.onAudioClientStateChange(
                AudioClient.AUDIO_CLIENT_STATE_DISCONNECTED_ABNORMAL,
                AudioClient.AUDIO_CLIENT_ERR_INPUT_DEVICE_NOT_RESPONDING
            )
        }
        coVerify(exactly = 1, timeout = TestConstant.globalScopeTimeoutMs) { mockAudioVideoObserver.onAudioSessionStopped(
            MeetingSessionStatus(MeetingSessionStatusCode.AudioInputDeviceNotResponding)
        ) }
        verify(exactly = 1, timeout = TestConstant.globalScopeTimeoutMs) { mockAudioClient.stopSession() }
    }

    @Test
    fun `onAudioClientStateChange should stop session and notify of session stop event when output device is not responding`() {
        every { mockAudioClient.stopSession() } returns 0

        runBlockingTest {
            audioClientObserver.onAudioClientStateChange(
                AudioClient.AUDIO_CLIENT_STATE_RECONNECTING,
                AudioClient.AUDIO_CLIENT_OK
            )

            audioClientObserver.onAudioClientStateChange(
                AudioClient.AUDIO_CLIENT_STATE_DISCONNECTED_ABNORMAL,
                AudioClient.AUDIO_CLIENT_ERR_OUTPUT_DEVICE_NOT_RESPONDING
            )
        }
        coVerify(exactly = 1, timeout = TestConstant.globalScopeTimeoutMs) { mockAudioVideoObserver.onAudioSessionStopped(
            MeetingSessionStatus(MeetingSessionStatusCode.AudioOutputDeviceNotResponding)
        ) }
        verify(exactly = 1, timeout = TestConstant.globalScopeTimeoutMs) { mockAudioClient.stopSession() }
    }

    @Test
    fun `onAudioClientStateChange should NOT notify when state and status is unchanged`() {
        runBlockingTest {
            audioClientObserver.onAudioClientStateChange(
                SessionStateControllerAction.Init.value,
                MeetingSessionStatusCode.OK.value
            )
        }

        verifyAudioVideoObserverIsNotNotified()
    }

    @Test
    fun `onLogMessage should call logger`() {
        audioClientObserver.onLogMessage(AudioClient.L_ERROR, testId1)

        verify(exactly = 1) { mockLogger.error(any(), any()) }
    }

    @Test
    fun `onLogMessage should NOT call logger when message is null`() {
        audioClientObserver.onLogMessage(AudioClient.L_ERROR, null)

        verify(exactly = 0) { mockLogger.error(any(), any()) }
    }

    @Test
    fun `onLogMessage should NOT call logger when log level is non error or non fatal`() {
        audioClientObserver.onLogMessage(AudioClient.L_INFO, testId1)

        verify(exactly = 0) { mockLogger.error(any(), any()) }
    }

    @Test
    fun `onMetrics should call clientMetricsCollector processAudioClientMetrics`() {
        val metrics = mutableMapOf(1 to 2.3, 4 to 5.6)
        audioClientObserver.onMetrics(metrics.keys.toIntArray(), metrics.values.toDoubleArray())

        verify { mockClientMetricsCollector.processAudioClientMetrics(metrics) }
    }

    private fun verifyAudioVideoObserverIsNotNotified() {
        verify(exactly = 0) { mockAudioVideoObserver.onAudioSessionStartedConnecting(any()) }
        verify(exactly = 0) { mockAudioVideoObserver.onAudioSessionStarted(any()) }
        verify(exactly = 0) { mockAudioVideoObserver.onAudioSessionStopped(any()) }
        verify(exactly = 0) { mockAudioVideoObserver.onAudioSessionCancelledReconnect() }
        verify(exactly = 0) { mockAudioVideoObserver.onConnectionRecovered() }
        verify(exactly = 0) { mockAudioVideoObserver.onConnectionBecamePoor() }
        verify(exactly = 0) { mockAudioVideoObserver.onVideoSessionStartedConnecting() }
        verify(exactly = 0) { mockAudioVideoObserver.onVideoSessionStarted(any()) }
        verify(exactly = 0) { mockAudioVideoObserver.onVideoSessionStopped(any()) }
    }
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.internal.contentshare

import android.content.Context
import android.util.Log
import com.amazonaws.services.chime.sdk.meetings.audiovideo.contentshare.ContentShareObserver
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoSource
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.gl.EglCore
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.gl.EglCoreFactory
import com.amazonaws.services.chime.sdk.meetings.internal.utils.AppInfoUtil
import com.amazonaws.services.chime.sdk.meetings.internal.video.DefaultVideoClientFactory
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionConfiguration
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger
import com.xodee.client.video.VideoClient
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.InjectMockKs
import io.mockk.impl.annotations.MockK
import io.mockk.just
import io.mockk.mockkObject
import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.verify
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Before
import org.junit.Test

class DefaultContentShareVideoClientControllerTest {

    @MockK
    private lateinit var mockContext: Context

    @MockK
    private lateinit var mockLogger: Logger

    @MockK
    private lateinit var mockContentShareVideoClientObserver: DefaultContentShareVideoClientObserver

    @MockK(relaxed = true)
    private lateinit var mockConfiguration: MeetingSessionConfiguration

    @MockK
    private lateinit var mockVideoClient: VideoClient

    @MockK
    private lateinit var mockVideoClientFactory: DefaultVideoClientFactory

    @MockK
    private lateinit var mockEglCoreFactory: EglCoreFactory

    @MockK(relaxed = true)
    private lateinit var mockEglCore: EglCore

    @InjectMockKs
    private lateinit var testContentShareVideoClientController: DefaultContentShareVideoClientController

    @MockK
    private lateinit var mockVideoSource: VideoSource

    @MockK
    private lateinit var mockContentShareObserver: ContentShareObserver

    private val testDispatcher = TestCoroutineDispatcher()

    @Before
    fun setUp() {
        mockkStatic(System::class, Log::class, VideoClient::class)
        every { Log.d(any(), any()) } returns 0
        every { System.loadLibrary(any()) } just runs
        every { VideoClient.javaInitializeGlobals(any()) } returns true
        mockkObject(AppInfoUtil)
        every { AppInfoUtil.initializeVideoClientAppDetailedInfo(any()) } just runs
        Dispatchers.setMain(testDispatcher)
        MockKAnnotations.init(this, relaxUnitFun = true)
        every { mockVideoClientFactory.getVideoClient(mockContentShareVideoClientObserver) } returns mockVideoClient
        every { mockEglCoreFactory.createEglCore() } returns mockEglCore
    }

    @After
    fun tearDown() {
        Dispatchers.resetMain()
        testDispatcher.cleanupTestCoroutines()
    }

    @Test
    fun `startVideoShare should call VideoClientStateController setExternalVideoSource`() {
        every { mockVideoClient.start(any()) } returns true
        testContentShareVideoClientController.startVideoShare(mockVideoSource)

        verify(exactly = 1) { mockVideoClient.setExternalVideoSource(any(), any()) }
    }

    @Test
    fun `stopVideoShare should call VideoClientStateController stopService`() {
        testContentShareVideoClientController.stopVideoShare()

        verify(exactly = 1) { mockVideoClient.javaStopService() }
    }

    @Test
    fun `startVideoShare should not call VideoClientStateController startService again when is sharing`() {
        every { mockVideoClient.start(any()) } returns true
        testContentShareVideoClientController.startVideoShare(mockVideoSource)

        testContentShareVideoClientController.startVideoShare(mockVideoSource)

        verify(exactly = 1) { mockVideoClient.start(any()) }
    }

    @Test
    fun `startVideoShare should notify subscribed observer of onContentShareStopped event when failed`() {
        every { mockVideoClient.start(any()) } returns false
        testContentShareVideoClientController.subscribeToVideoClientStateChange(mockContentShareObserver)

        testContentShareVideoClientController.startVideoShare(mockVideoSource)

        verify(exactly = 1) { mockContentShareObserver.onContentShareStopped(any()) }
    }

    @Test
    fun `subscribeToVideoClientStateChange should call contentShareVideoClientObserver subscribeToVideoClientStateChange with given observer`() {
        testContentShareVideoClientController.subscribeToVideoClientStateChange(mockContentShareObserver)

        verify(exactly = 1) { mockContentShareVideoClientObserver.subscribeToVideoClientStateChange(mockContentShareObserver) }
    }

    @Test
    fun `unsubscribeFromVideoClientStateChange should call contentShareVideoClientObserver unsubscribeFromVideoClientStateChange with given observer`() {
        testContentShareVideoClientController.unsubscribeFromVideoClientStateChange(mockContentShareObserver)

        verify(exactly = 1) { mockContentShareVideoClientObserver.unsubscribeFromVideoClientStateChange(mockContentShareObserver) }
    }
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.internal.ingestion

import com.amazonaws.services.chime.sdk.meetings.analytics.EventAttributeName
import com.amazonaws.services.chime.sdk.meetings.analytics.EventName
import com.amazonaws.services.chime.sdk.meetings.internal.ingestion.database.DatabaseManager
import com.amazonaws.services.chime.sdk.meetings.internal.ingestion.database.DirtyEventSQLiteDao
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionStatusCode
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger
import com.google.gson.Gson
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.verify
import java.util.UUID
import org.junit.Assert
import org.junit.Before
import org.junit.Test

class DirtyEventSQLiteDaoTests {
    private val tableName = "DirtyEvents"
    private val gson = Gson()
    private lateinit var dirtyEventDao: DirtyEventSQLiteDao

    @MockK
    private lateinit var databaseManager: DatabaseManager

    @MockK
    private lateinit var logger: Logger

    @MockK
    private lateinit var eventTypeConverter: EventTypeConverters

    private val uuid = "38400000-8cf0-11bd-b23e-10b96e4ef00d"

    private val mockEvent = SDKEvent(
        EventName.meetingFailed, mutableMapOf(
            EventAttributeName.meetingErrorMessage to "test fail",
            EventAttributeName.meetingStatus to MeetingSessionStatusCode.AudioAuthenticationRejected
        )
    )

    private fun buildMockMeetingEventItem(): DirtyMeetingEventItem =
        DirtyMeetingEventItem(UUID.randomUUID().toString(), mockEvent, 200000)

    @Before
    fun setup() {
        MockKAnnotations.init(this, relaxUnitFun = true)

        every { databaseManager.dropTable(any()) } returns true
        every { databaseManager.createTable(any()) } returns true
        every { databaseManager.delete(any(), any(), any()) } returns 0
        every { databaseManager.query(any(), any()) } returns listOf(
            mapOf(
                "id" to "51ed7a0b-906c-4ab6-9b4b-2ba9a012105b",
                "data" to gson.toJson(mockEvent),
                "ttl" to 1242312412424
            )
        )
        every { eventTypeConverter.fromMeetingEvent(any()) } returns ""
        every { eventTypeConverter.toMeetingEvent(any()) } returns mockEvent

        dirtyEventDao = DirtyEventSQLiteDao(databaseManager, logger, eventTypeConverter)
    }

    @Test
    fun `insertDirtyMeetingEventItems should invoke database manager insert`() {
        every { databaseManager.insert(any(), any()) } returns true

        dirtyEventDao.insertDirtyMeetingEventItems(listOf(buildMockMeetingEventItem()))

        verify(exactly = 1) { databaseManager.insert(tableName, any()) }
    }

    @Test
    fun `insertDirtyMeetingEventItems should return false if database manager insert fails`() {
        every { databaseManager.insert(any(), any()) } returns false

        val inserted = dirtyEventDao.insertDirtyMeetingEventItems(listOf(buildMockMeetingEventItem()))

        Assert.assertEquals(false, inserted)
    }

    @Test
    fun `queryDirtyMeetingEventItems should invoke database manager query`() {
        dirtyEventDao.listDirtyMeetingEventItems(5)

        verify(exactly = 1) { databaseManager.query(tableName, 5) }
    }

    @Test
    fun `queryDirtyMeetingEventItems should return a list of DirtyMeetingEventItems`() {
        val dirtyMeetingItems = dirtyEventDao.listDirtyMeetingEventItems(5)

        Assert.assertEquals(1, dirtyMeetingItems.size)
        Assert.assertEquals(mockEvent.name, dirtyMeetingItems[0].data.name)
        Assert.assertEquals(
            mockEvent.eventAttributes[EventAttributeName.meetingErrorMessage.name],
            dirtyMeetingItems[0].data.eventAttributes[EventAttributeName.meetingErrorMessage.name]
        )
    }

    @Test
    fun `deleteDirtyEventsByIds should invoke database manager delete`() {
        val uuids = listOf(uuid)
        dirtyEventDao.deleteDirtyEventsByIds(uuids)

        verify(exactly = 1) { databaseManager.delete(tableName, dirtyEventDao.primaryKey.first, uuids) }
    }

    @Test
    fun `deleteDirtyEventsByIds should return zero if database manager delete returns 0`() {
        val uuids = listOf(uuid)
        val rowsDeleted = dirtyEventDao.deleteDirtyEventsByIds(uuids)

        Assert.assertEquals(0, rowsDeleted)
    }

    @Test
    fun `constructor should invoke database manager createTable`() {
        dirtyEventDao = DirtyEventSQLiteDao(databaseManager, logger, eventTypeConverter)

        verify { databaseManager.createTable(dirtyEventDao) }
    }
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.internal.ingestion

import com.amazonaws.services.chime.sdk.meetings.analytics.EventAttributeName
import com.amazonaws.services.chime.sdk.meetings.analytics.EventName
import com.amazonaws.services.chime.sdk.meetings.internal.ingestion.database.DatabaseManager
import com.amazonaws.services.chime.sdk.meetings.internal.ingestion.database.EventSQLiteDao
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionStatusCode
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger
import com.google.gson.Gson
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.verify
import java.util.UUID
import org.junit.Assert
import org.junit.Before
import org.junit.Test

class EventSQLiteDaoTests {
    private val tableName = "Events"
    private lateinit var eventDao: EventSQLiteDao
    private val gson = Gson()

    @MockK
    private lateinit var databaseManager: DatabaseManager

    @MockK
    private lateinit var logger: Logger

    @MockK
    private lateinit var eventTypeConverter: EventTypeConverters

    private val uuid = "38400000-8cf0-11bd-b23e-10b96e4ef00d"

    private val mockEvent = SDKEvent(
        EventName.meetingFailed, mutableMapOf(
            EventAttributeName.meetingErrorMessage to "test fail",
            EventAttributeName.meetingStatus to MeetingSessionStatusCode.AudioAuthenticationRejected
        )
    )

    private fun buildMockMeetingEventItem(): MeetingEventItem =
        MeetingEventItem(UUID.randomUUID().toString(), mockEvent)

    @Before
    fun setup() {
        MockKAnnotations.init(this, relaxUnitFun = true)

        every { databaseManager.dropTable(any()) } returns true
        every { databaseManager.createTable(any()) } returns true
        every { databaseManager.delete(any(), any(), any()) } returns 0
        every { databaseManager.query(any(), any()) } returns listOf(
            mapOf(
                "id" to "51ed7a0b-906c-4ab6-9b4b-2ba9a012105b",
                "data" to gson.toJson(mockEvent)
            )
        )
        every { eventTypeConverter.fromMeetingEvent(any()) } returns ""
        every { eventTypeConverter.toMeetingEvent(any()) } returns mockEvent

        eventDao =
            EventSQLiteDao(
                databaseManager,
                logger,
                eventTypeConverter
            )
    }

    @Test
    fun `insertMeetingEvent should invoke database manager insert`() {
        every { databaseManager.insert(any(), any()) } returns true

        eventDao.insertMeetingEvent(buildMockMeetingEventItem())

        verify(exactly = 1) { databaseManager.insert(tableName, any()) }
    }

    @Test
    fun `queryMeetingEventItems should invoke database manager query`() {
        eventDao.listMeetingEventItems(5)

        verify(exactly = 1) { databaseManager.query(tableName, 5) }
    }

    @Test
    fun `queryMeetingEventItems should return a list of MeetingEventItem`() {
        val meetingItems = eventDao.listMeetingEventItems(5)

        Assert.assertEquals(1, meetingItems.size)
        Assert.assertEquals(
            mockEvent.eventAttributes[EventAttributeName.meetingErrorMessage.name],
            meetingItems[0].data.eventAttributes[EventAttributeName.meetingErrorMessage.name]
        )
    }

    @Test
    fun `deleteEventsByIds should invoke database manager delete`() {
        val uuids = listOf(uuid)
        eventDao.deleteMeetingEventsByIds(uuids)

        verify(exactly = 1) { databaseManager.delete(tableName, eventDao.primaryKey.first, uuids) }
    }

    @Test
    fun `deleteEventsByIds should return zero if database manager delete returns 0`() {
        val uuids = listOf(uuid)
        val rowsDeleted = eventDao.deleteMeetingEventsByIds(uuids)

        Assert.assertEquals(0, rowsDeleted)
    }

    @Test
    fun `constructor should invoke database manager createTable`() {
        eventDao =
            EventSQLiteDao(
                databaseManager,
                logger,
                eventTypeConverter
            )

        verify { databaseManager.createTable(eventDao) }
    }
}


package com.amazonaws.services.chime.sdk.meetings.internal.ingestion

import com.amazonaws.services.chime.sdk.meetings.analytics.EventAttributeName
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger
import io.mockk.MockKAnnotations
import io.mockk.impl.annotations.MockK
import io.mockk.verify
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test

class EventTypeConvertersTests {

    private lateinit var eventTypeConverters: EventTypeConverters

    @MockK
    private lateinit var logger: Logger

    @Before
    fun setup() {
        MockKAnnotations.init(this, relaxUnitFun = true)

        eventTypeConverters = EventTypeConverters(logger)
    }

    @Test
    fun `toMeetingEvent should convert json string to object`() {

        val nameValue = "test name"
        val deviceNameKey = EventAttributeName.deviceName.name
        val deviceNameValue = "test device"
        val tsKey = EventAttributeName.timestampMs.name
        val tsValue = 1659759236803
        val data = "{\"name\":\"$nameValue\", \"eventAttributes\":{\"$deviceNameKey\":\"$deviceNameValue\", \"$tsKey\":$tsValue}}"
        val event = eventTypeConverters.toMeetingEvent(data)

        assertEquals(nameValue, event.name)
        assertEquals(deviceNameValue, event.eventAttributes[EventAttributeName.deviceName.name] as? String)
        assertEquals(tsValue, (event.eventAttributes[EventAttributeName.timestampMs.name] as? Double ?: 0.0).toLong())
    }

    @Test
    fun `toMeetingEvent should handle null name`() {

        val data = "{\"name\":null, \"eventAttributes\":{}}"
        val event = eventTypeConverters.toMeetingEvent(data)

        assertEquals("", event.name)
    }

    @Test
    fun `toMeetingEvent should handle malformed name`() {

        val data = "{\"name\":{}, \"eventAttributes\":{}}"
        val event = eventTypeConverters.toMeetingEvent(data)

        verify(exactly = 1) { logger.error(any(), any()) }

        assertEquals("", event.name)
    }

    @Test
    fun `toMeetingEvent should handle null event attributes data`() {

        val nameValue = "test name"
        val data = "{\"name\":\"$nameValue\", \"eventAttributes\":null}"
        val event = eventTypeConverters.toMeetingEvent(data)

        assertEquals(0, event.eventAttributes.count())
    }

    @Test
    fun `toMeetingEvent should handle malformed event attributes data`() {

        val nameValue = "test name"
        val data = "{\"name\":\"$nameValue\", \"eventAttributes\":\"invalid data\"}"
        val event = eventTypeConverters.toMeetingEvent(data)

        verify(exactly = 1) { logger.error(any(), any()) }

        assertEquals(0, event.eventAttributes.count())
    }

    @Test
    fun `fromMeetingEvent should convert object to json string`() {

        val nameValue = "test name"
        val deviceNameKey = EventAttributeName.deviceName.toString()
        val deviceNameValue = "test device"
        val tsKey = EventAttributeName.timestampMs.toString()
        val tsValue = 1659759236803
        val mockEvent = SDKEvent(nameValue, mutableMapOf(
            EventAttributeName.deviceName.name to deviceNameValue,
            EventAttributeName.timestampMs.name to tsValue
        ))
        val result = eventTypeConverters.fromMeetingEvent(mockEvent)

        val jsonString = "{\"name\":\"test name\",\"eventAttributes\":{\"$deviceNameKey\":\"$deviceNameValue\",\"$tsKey\":$tsValue}}"
        assertEquals(jsonString, result)
    }
}


package com.amazonaws.services.chime.sdk.meetings.internal.ingestion

import com.amazonaws.services.chime.sdk.meetings.ingestion.EventClientConfiguration
import com.amazonaws.services.chime.sdk.meetings.ingestion.EventClientType
import com.amazonaws.services.chime.sdk.meetings.ingestion.IngestionConfiguration
import com.amazonaws.services.chime.sdk.meetings.internal.utils.EventAttributesUtils
import io.mockk.every
import io.mockk.mockkObject
import io.mockk.unmockkObject
import org.junit.After
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test

class IngestionEventConverterTests {
    private class CustomEventClientConfiguration(
        override val type: EventClientType = EventClientType.Meet,
        override val eventClientJoinToken: String = "token",
        override val tag: String = "custom-tag",
        override val metadataAttributes: Map<String, Any> = mapOf(
            "meetingId" to "meeting1",
            "attendeeId" to "attendee1",
            "additionMetadata" to "value1"
        )
    ) : EventClientConfiguration

    private val ingestionConfiguration = IngestionConfiguration(
        clientConfiguration = CustomEventClientConfiguration(),
        ingestionUrl = "ingestion-url"
    )
    private val eventMeeting1 = SDKEvent(
        name = "some-event-name",
        eventAttributes = mapOf(
            "key-1" to "value1",
            "timestampMs" to 1.00,
            "meetingId" to "meeting1",
            "attendeeId" to "attendee1",
            "additionMetadata" to "value1"
        )
    )
    private val eventMeeting2 = SDKEvent(
        name = "some-event-name",
        eventAttributes = mapOf(
            "key-1" to "value1",
            "timestampMs" to 2.00,
            "meetingId" to "meeting2",
            "attendeeId" to "attendee2",
            "additionMetadata" to "value2"
        )
    )
    private val eventItem1 = MeetingEventItem(id = "event-id-1", data = eventMeeting1)
    private val eventItem2 = MeetingEventItem(id = "event-id-2", data = eventMeeting2)

    @Before
    fun setUp() {
        mockkObject(EventAttributesUtils)
    }

    @After
    fun cleanUp() {
        unmockkObject(EventAttributesUtils)
    }

    @Test
    fun `fromMeetingEventItems should create correct IngestionRecord`() {
        every { EventAttributesUtils.getIngestionMetadata(any()) } returns mapOf(
            "meetingId" to "meeting1", "attendeeId" to "attendee1",
            "additionMetadata" to "value1", "other-metadata" to "value"
        )

        val record = IngestionEventConverter.fromMeetingEventItems(
            listOf(eventItem1, eventItem2), ingestionConfiguration)

        // Event metadata attributes is same as ones in the configuration
        assertTrue(ingestionConfiguration.clientConfiguration.metadataAttributes.keys.none { metadataAttribute ->
            record.events.none { it.payloads.none { payload -> payload.containsKey(metadataAttribute) } }
        })
        // Event payload doesn't contain configuration metadata attributes
        assertTrue(ingestionConfiguration.clientConfiguration.metadataAttributes.keys.none { metadataAttribute ->
            record.events.none { it.payloads.none { payload -> payload.containsKey(metadataAttribute) } }
        })
    }
}


/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package com.amazonaws.services.chime.sdk.meetings.internal.metric

import com.amazonaws.services.chime.sdk.meetings.audiovideo.metric.MetricsObserver
import com.amazonaws.services.chime.sdk.meetings.audiovideo.metric.ObservableMetric
import com.xodee.client.audio.audioclient.AudioClient
import com.xodee.client.video.VideoClient
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.verify
import java.util.Calendar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Before
import org.junit.Test

class DefaultClientMetricsCollectorTest {

    private lateinit var clientMetricsCollector: DefaultClientMetricsCollector

    @MockK
    private lateinit var mockMetricsObserver: MetricsObserver

    // See https://github.com/mockk/mockk/issues/98
    private lateinit var calendar: Calendar
    private val testDispatcher = TestCoroutineDispatcher()

    @Before
    fun setup() {
        Dispatchers.setMain(testDispatcher)
        calendar = mockk()
        every { calendar.timeInMillis } returnsMany listOf<Long>(0, 1100)
        MockKAnnotations.init(this, relaxUnitFun = true)
        mockkStatic(Calendar::class)
        every { Calendar.getInstance() } returns calendar
        clientMetricsCollector =
            DefaultClientMetricsCollector()
    }

    @After
    fun tearDown() {
        Dispatchers.resetMain()
        testDispatcher.cleanupTestCoroutines()
    }

    @Test
    fun `onMetrics should not call observer before interval has passed`() {
        every { calendar.timeInMillis } returns 0
        val rawMetrics =
            mutableMapOf(AudioClient.AUDIO_CLIENT_METRIC_POST_JB_SPK_1S_PACKETS_LOST_PERCENT to 1.0)
        clientMetricsCollector.processAudioClientMetrics(rawMetrics)

        verify(exactly = 0) { mockMetricsObserver.onMetricsReceived(any()) }
    }

    @Test
    fun `onMetrics for audio should call observer after interval has passed and observer should not receive any null metrics`() {
        clientMetricsCollector.subscribeToMetrics(mockMetricsObserver)
        val rawMetrics =
            mutableMapOf(AudioClient.AUDIO_CLIENT_METRIC_POST_JB_SPK_1S_PACKETS_LOST_PERCENT to 1.0)
        val observableMetrics =
            mutableMapOf(ObservableMetric.audioReceivePacketLossPercent to 1.0)

        clientMetricsCollector.processAudioClientMetrics(rawMetrics)

        verify(exactly = 1) { mockMetricsObserver.onMetricsReceived(observableMetrics) }
    }

    @Test
    fun `onMetrics for video should call observer after interval has passed and observer should not receive any null metrics`() {
        clientMetricsCollector.subscribeToMetrics(mockMetricsObserver)
        val rawMetrics = mutableMapOf(VideoClient.VIDEO_AVAILABLE_RECEIVE_BANDWIDTH to 10.0)
        val observableMetrics =
            mutableMapOf(ObservableMetric.videoAvailableReceiveBandwidth to 10.0)

        clientMetricsCollector.processVideoClientMetrics(rawMetrics)

        verify(exactly = 1) { mockMetricsObserver.onMetricsReceived(observableMetrics) }
    }

    @Test
    fun `onMetrics for content share should call observer after interval has passed and observer should not receive any null metrics`() {
        clientMetricsCollector.subscribeToMetrics(mockMetricsObserver)
        val rawMetrics = mutableMapOf(VideoClient.VIDEO_SEND_FPS to 10.0)
        val observableMetrics =
            mutableMapOf(ObservableMetric.contentShareVideoSendFps to 10.0)

        clientMetricsCollector.processContentShareVideoClientMetrics(rawMetrics)

        verify(exactly = 1) { mockMetricsObserver.onMetricsReceived(observableMetrics) }
    }

    @Test
    fun `onMetrics should not emit non-observable metrics`() {
        clientMetricsCollector.subscribeToMetrics(mockMetricsObserver)
        val rawMetrics =
            mutableMapOf(AudioClient.AUDIO_CLIENT_METRIC_MIC_DEVICE_FRAMES_LOST_PERCENT to 1.0)

        clientMetricsCollector.processAudioClientMetrics(rawMetrics)

        val observableMetrics = mutableMapOf<ObservableMetric, Double>()
        verify(exactly = 1) { mockMetricsObserver.onMetricsReceived(observableMetrics) }
    }

    @Test
    fun `onMetrics should not emit invalid metrics`() {
        clientMetricsCollector.subscribeToMetrics(mockMetricsObserver)
        val rawMetrics = mutableMapOf(999 to 1.0)

        clientMetricsCollector.processAudioClientMetrics(rawMetrics)

        val observableMetrics = mutableMapOf<ObservableMetric, Double>()
        verify(exactly = 1) { mockMetricsObserver.onMetricsReceived(observableMetrics) }
    }

    @Test
    fun `onMetrics should not notify observer when unsubscribed`() {
        Thread.sleep(1100)
        clientMetricsCollector.subscribeToMetrics(mockMetricsObserver)
        clientMetricsCollector.unsubscribeFromMetrics(mockMetricsObserver)
        val rawMetrics =
            mutableMapOf(AudioClient.AUDIO_CLIENT_METRIC_POST_JB_SPK_1S_PACKETS_LOST_PERCENT to 1.0)

        clientMetricsCollector.processAudioClientMetrics(rawMetrics)

        verify(exactly = 0) { mockMetricsObserver.onMetricsReceived(any()) }
    }
}