/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */
package com.amazonaws.services.chime.sdk.meetings.audiovideo.video

import com.amazonaws.services.chime.sdk.meetings.analytics.MeetingStatsCollector
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.buffer.VideoFrameBuffer
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.gl.EglCoreFactory
import com.amazonaws.services.chime.sdk.meetings.internal.video.VideoClientController
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionConfiguration
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.InjectMockKs
import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import kotlinx.coroutines.Dispatchers
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.Assert
import org.junit.Before
import org.junit.Test

class DefaultVideoTileControllerTest {
    @MockK
    private lateinit var mockMeetingSessionConfiguration: MeetingSessionConfiguration

    @MockK
    private lateinit var mockVideoTileFactory: VideoTileFactory

    @MockK
    private lateinit var mockVideoTileController: VideoClientController

    @MockK
    private lateinit var mockLogger: Logger

    @MockK
    private lateinit var mockEglCoreFactory: EglCoreFactory

    @MockK
    private lateinit var mockVideoTile: VideoTile

    @MockK
    private lateinit var mockVideoRenderView: VideoRenderView

    @MockK
    private lateinit var mockVideoFrame: VideoFrame

    @MockK
    private lateinit var mockVideoFrameBuffer: VideoFrameBuffer

    @MockK
    private lateinit var meetingStatsCollector: MeetingStatsCollector

    @InjectMockKs
    private lateinit var videoTileController: DefaultVideoTileController

    // See https://github.com/Kotlin/kotlinx.coroutines/tree/master/kotlinx-coroutines-test for more examples
    private val testDispatcher = TestCoroutineDispatcher()

    private val tileId = 7 // some random prime number
    private val attendeeId = "chimesarang"
    private val testWidth = 0
    private val testHeight = 0
    private val testTimestamp: Long = 1000
    private val testRotation = VideoRotation.Rotation90
    private val localTile = true
    private val remoteTile = false

    private var onAddObserverCalled = 0
    private var onRemoveObserverCalled = 0
    private var onPauseObserverCalled = 0
    private var onResumeObserverCalled = 0
    private var onSizeChangedObserverCalled = 0

    private val tileObserver = object : VideoTileObserver {
        override fun onVideoTileAdded(tileState: VideoTileState) {
            onAddObserverCalled++
        }

        override fun onVideoTileRemoved(tileState: VideoTileState) {
            onRemoveObserverCalled++
        }

        override fun onVideoTilePaused(tileState: VideoTileState) {
            onPauseObserverCalled++
        }

        override fun onVideoTileResumed(tileState: VideoTileState) {
            onResumeObserverCalled++
        }

        override fun onVideoTileSizeChanged(tileState: VideoTileState) {
            onSizeChangedObserverCalled++
        }
    }

    @Before
    fun setUp() {
        MockKAnnotations.init(this, relaxUnitFun = true)
        every { mockMeetingSessionConfiguration.credentials.attendeeId } returns "attendeeId"
        every { mockVideoTileController.getConfiguration() } returns mockMeetingSessionConfiguration
        every { mockVideoTileFactory.makeTile(tileId, any(), testWidth, testHeight, any()) } returns mockVideoTile
        every { mockVideoTile.state } returns VideoTileState(
            tileId,
            attendeeId,
            testWidth,
            testHeight,
            VideoPauseState.Unpaused,
            remoteTile
        )
        every { mockVideoTile.videoRenderView } returns mockVideoRenderView
        every { mockVideoFrame.width } returns testWidth
        every { mockVideoFrame.height } returns testHeight

        Dispatchers.setMain(testDispatcher)
    }

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

    @Test
    fun `unbindVideoView should call finalize on VideoRenderView`() {
        runBlockingTest {
            videoTileController.onReceiveFrame(
                mockVideoFrame,
                tileId,
                attendeeId,
                VideoPauseState.Unpaused
            )
        }
        videoTileController.bindVideoView(mockVideoRenderView, tileId)
        videoTileController.unbindVideoView(tileId)

        verify { mockVideoTile.unbind() }
    }

    @Test
    fun `onReceiveFrame should call renderFrame on VideoRenderView when bound`() {
        runBlockingTest {
            videoTileController.onReceiveFrame(
                mockVideoFrame,
                tileId,
                attendeeId,
                VideoPauseState.Unpaused
            )
        }
        videoTileController.bindVideoView(mockVideoRenderView, tileId)
        runBlockingTest {
            videoTileController.onReceiveFrame(
                mockVideoFrame,
                tileId,
                attendeeId,
                VideoPauseState.Unpaused
            )
        }

        verify { mockVideoTile.onVideoFrameReceived(any()) }
    }

    @Test
    fun `onReceiveFrame should NOT notify observer about video tile add when observer has been removed`() {
        videoTileController.addVideoTileObserver(tileObserver)
        videoTileController.removeVideoTileObserver(tileObserver)
        runBlockingTest {
            videoTileController.onReceiveFrame(
                mockVideoFrame,
                tileId,
                attendeeId,
                VideoPauseState.Unpaused
            )
        }

        Assert.assertEquals(0, onAddObserverCalled)
    }

    @Test
    fun `onReceiveFrame should NOT notify observer about video tile remove when observer has been removed`() {
        videoTileController.addVideoTileObserver(tileObserver)
        videoTileController.removeVideoTileObserver(tileObserver)
        runBlockingTest {
            videoTileController.onReceiveFrame(
                mockVideoFrame,
                tileId,
                attendeeId,
                VideoPauseState.Unpaused
            )
        }

        Assert.assertEquals(0, onAddObserverCalled)
    }

    @Test
    fun `onReceiveFrame should notify observer about video tile add with non-null frame and new tileId`() {
        val mockObserver = spyk(tileObserver)

        videoTileController.addVideoTileObserver(mockObserver)
        runBlockingTest {
            videoTileController.onReceiveFrame(
                mockVideoFrame,
                tileId,
                attendeeId,
                VideoPauseState.Unpaused
            )
        }

        Assert.assertEquals(1, onAddObserverCalled)
        verify {
            mockObserver.onVideoTileAdded(
                VideoTileState(
                    tileId,
                    attendeeId,
                    testWidth,
                    testHeight,
                    VideoPauseState.Unpaused,
                    remoteTile
                )
            )
        }
    }

    @Test
    fun `onReceiveFrame should notify observer about video tile remove with null frame and old tileId`() {
        val mockObserver = spyk(tileObserver)

        videoTileController.addVideoTileObserver(mockObserver)
        runBlockingTest {
            videoTileController.onReceiveFrame(
                mockVideoFrame,
                tileId,
                attendeeId,
                VideoPauseState.Unpaused
            )
            videoTileController.onReceiveFrame(null, tileId, attendeeId, VideoPauseState.Unpaused)
        }

        Assert.assertEquals(1, onRemoveObserverCalled)
        verify {
            mockObserver.onVideoTileRemoved(
                VideoTileState(
                    tileId,
                    attendeeId,
                    testWidth,
                    testHeight,
                    VideoPauseState.Unpaused,
                    remoteTile
                )
            )
        }
    }

    @Test
    fun `onReceiveFrame should notify observer about video tile pause when pause state is changed to paused`() {
        val mockObserver = spyk(tileObserver)

        videoTileController.addVideoTileObserver(mockObserver)
        runBlockingTest {
            videoTileController.onReceiveFrame(
                mockVideoFrame,
                tileId,
                attendeeId,
                VideoPauseState.Unpaused
            )
            videoTileController.onReceiveFrame(
                mockVideoFrame,
                tileId,
                attendeeId,
                VideoPauseState.PausedForPoorConnection
            )
        }

        verify { mockVideoTile.setPauseState(VideoPauseState.PausedForPoorConnection) }
        // Mock video tile state will be equal to Unpaused
        verify(exactly = 1) {
            mockObserver.onVideoTilePaused(
                VideoTileState(
                    tileId,
                    attendeeId,
                    testWidth,
                    testHeight,
                    VideoPauseState.Unpaused,
                    remoteTile
                )
            )
        }
    }

    @Test
    fun `onReceiveFrame should notify observer about video tile resume when pause state is changed from paused to unpaused`() {
        val mockObserver = spyk(tileObserver)
        every { mockVideoTile.state } returns VideoTileState(
            tileId,
            attendeeId,
            testWidth,
            testHeight,
            VideoPauseState.PausedForPoorConnection,
            remoteTile
        )

        videoTileController.addVideoTileObserver(mockObserver)
        runBlockingTest {
            videoTileController.onReceiveFrame(
                mockVideoFrame,
                tileId,
                attendeeId,
                VideoPauseState.PausedForPoorConnection
            )
            videoTileController.onReceiveFrame(
                mockVideoFrame,
                tileId,
                attendeeId,
                VideoPauseState.Unpaused
            )
        }

        verify { mockVideoTile.setPauseState(VideoPauseState.Unpaused) }
        // Mock video tile state will be equal to PausedForPoorConnection
        verify(exactly = 1) {
            mockObserver.onVideoTileResumed(
                VideoTileState(
                    tileId,
                    attendeeId,
                    testWidth,
                    testHeight,
                    VideoPauseState.PausedForPoorConnection,
                    remoteTile
                )
            )
        }
    }

    @Test
    fun `pauseRemoteVideoTile should call VideoClientController's setRemotePaused when tile exists`() {
        runBlockingTest {
            videoTileController.onReceiveFrame(
                mockVideoFrame,
                tileId,
                attendeeId,
                VideoPauseState.Unpaused
            )
        }
        videoTileController.pauseRemoteVideoTile(tileId)

        verify { mockVideoTileController.setRemotePaused(true, tileId) }
    }

    @Test
    fun `pauseRemoteVideoTile should notify observer about video tile pause when tile exists`() {
        val mockObserver = spyk(tileObserver)
        videoTileController.addVideoTileObserver(mockObserver)

        runBlockingTest {
            videoTileController.onReceiveFrame(
                mockVideoFrame,
                tileId,
                attendeeId,
                VideoPauseState.Unpaused
            )
            videoTileController.pauseRemoteVideoTile(tileId)
        }

        // Mock video tile state will be equal to Unpaused
        verify(exactly = 1) {
            mockObserver.onVideoTilePaused(
                VideoTileState(
                    tileId,
                    attendeeId,
                    testWidth,
                    testHeight,
                    VideoPauseState.Unpaused,
                    remoteTile
                )
            )
        }
    }

    @Test
    fun `resumeRemoteVideoTile should call VideoClientController's setRemotePaused when tile exists`() {
        runBlockingTest {
            videoTileController.onReceiveFrame(
                mockVideoFrame,
                tileId,
                attendeeId,
                VideoPauseState.Unpaused
            )
        }
        videoTileController.resumeRemoteVideoTile(tileId)

        verify { mockVideoTileController.setRemotePaused(false, tileId) }
    }

    @Test
    fun `resumeRemoteVideoTile should notify observer about video tile resume when tile exists`() {
        val mockObserver = spyk(tileObserver)
        every { mockVideoTile.state } returns VideoTileState(
            tileId,
            attendeeId,
            testWidth,
            testHeight,
            VideoPauseState.PausedByUserRequest,
            remoteTile
        )
        videoTileController.addVideoTileObserver(mockObserver)

        runBlockingTest {
            videoTileController.onReceiveFrame(
                mockVideoFrame,
                tileId,
                attendeeId,
                VideoPauseState.PausedByUserRequest
            )
            videoTileController.resumeRemoteVideoTile(tileId)
        }

        // Mock video tile state will be equal to PausedByUserRequest
        verify(exactly = 1) {
            mockObserver.onVideoTileResumed(
                VideoTileState(
                    tileId,
                    attendeeId,
                    testWidth,
                    testHeight,
                    VideoPauseState.PausedByUserRequest,
                    remoteTile
                )
            )
        }
    }

    @Test
    fun `pauseRemoteVideoTile should NOT call VideoClientController's setRemotePaused when tile does not exist`() {
        videoTileController.pauseRemoteVideoTile(tileId)

        verify(exactly = 0) { mockVideoTileController.setRemotePaused(true, tileId) }
    }

    @Test
    fun `pauseRemoteVideoTile should NOT notify observer about video tile pause when tile does not exist`() {
        videoTileController.addVideoTileObserver(tileObserver)
        videoTileController.pauseRemoteVideoTile(tileId)

        Assert.assertEquals(0, onPauseObserverCalled)
    }

    @Test
    fun `pauseRemoteVideoTile should NOT call VideoClientController's setRemotePaused when local tile`() {
        every { mockVideoTile.state } returns VideoTileState(tileId, attendeeId, testWidth, testHeight, VideoPauseState.Unpaused, localTile)
        runBlockingTest {
            videoTileController.onReceiveFrame(mockVideoFrame, tileId, null, VideoPauseState.Unpaused)
        }

        videoTileController.pauseRemoteVideoTile(tileId)

        verify(exactly = 0) { mockVideoTileController.setRemotePaused(any(), any()) }
    }

    @Test
    fun `resumeRemoteVideoTile should NOT call VideoClientController's setRemotePaused when tile does not exist`() {
        videoTileController.resumeRemoteVideoTile(tileId)

        verify(exactly = 0) { mockVideoTileController.setRemotePaused(false, tileId) }
    }

    @Test
    fun `resumeRemoteVideoTile should NOT notify observer about video tile resume when tile does not exists`() {
        videoTileController.addVideoTileObserver(tileObserver)
        videoTileController.resumeRemoteVideoTile(tileId)

        Assert.assertEquals(0, onResumeObserverCalled)
    }

    @Test
    fun `resumeRemoteVideoTile should NOT call VideoClientController's setRemotePaused when local tile`() {
        every { mockVideoTile.state } returns VideoTileState(tileId, attendeeId, testWidth, testHeight, VideoPauseState.Unpaused, localTile)
        runBlockingTest {
            videoTileController.onReceiveFrame(mockVideoFrame, tileId, null, VideoPauseState.Unpaused)
        }

        videoTileController.resumeRemoteVideoTile(tileId)

        verify(exactly = 0) { mockVideoTileController.setRemotePaused(any(), any()) }
    }

    @Test
    fun `bind should unbind the view first when already bound`() {
        val mockVideoTile2: VideoTile = mockk(relaxUnitFun = true)
        val tileId2 = 127

        every { mockVideoTileFactory.makeTile(tileId2, any(), testWidth, testHeight, remoteTile) } returns mockVideoTile2
        every { mockVideoTile2.state } returns VideoTileState(
            tileId2,
            attendeeId,
            testWidth,
            testHeight,
            VideoPauseState.Unpaused,
            remoteTile
        )
        every { mockVideoTile2.videoRenderView } returns null
        runBlockingTest {
            videoTileController.onReceiveFrame(
                mockVideoFrame,
                tileId,
                attendeeId,
                VideoPauseState.Unpaused
            )
            videoTileController.onReceiveFrame(
                mockVideoFrame,
                tileId2,
                attendeeId,
                VideoPauseState.Unpaused
            )
        }

        videoTileController.bindVideoView(mockVideoRenderView, tileId)
        videoTileController.bindVideoView(mockVideoRenderView, tileId2)

        verify { mockVideoTile.unbind() }
        verify(exactly = 0) { mockVideoTile2.unbind() }
    }

    @Test
    fun `bind should not rebind the view when already bound with the same tile`() {
        runBlockingTest {
            videoTileController.onReceiveFrame(
                mockVideoFrame,
                tileId,
                attendeeId,
                VideoPauseState.Unpaused
            )
        }

        videoTileController.bindVideoView(mockVideoRenderView, tileId)
        videoTileController.bindVideoView(mockVideoRenderView, tileId)

        verify(exactly = 1) { mockVideoTile.bind(any()) }
        verify(exactly = 0) { mockVideoTile.unbind() }
    }

    @Test
    fun `bind should NOT call unbind on the first view when not bound`() {
        val mockVideoTile2: VideoTile = mockk(relaxUnitFun = true)
        val mockVideoRenderView2: VideoRenderView = mockk(relaxUnitFun = true)
        val tileId2 = 127
        every { mockVideoTileFactory.makeTile(tileId2, any(), testWidth, testHeight, remoteTile) } returns mockVideoTile2
        every { mockVideoTile2.state } returns VideoTileState(
            tileId2,
            attendeeId,
            testWidth,
            testHeight,
            VideoPauseState.Unpaused,
            remoteTile
        )
        every { mockVideoTile2.videoRenderView } returns null
        runBlockingTest {
            videoTileController.onReceiveFrame(
                mockVideoFrame,
                tileId,
                attendeeId,
                VideoPauseState.Unpaused
            )
            videoTileController.onReceiveFrame(
                mockVideoFrame,
                tileId2,
                attendeeId,
                VideoPauseState.Unpaused
            )
        }

        videoTileController.bindVideoView(mockVideoRenderView, tileId)
        videoTileController.bindVideoView(mockVideoRenderView2, tileId2)

        verify(exactly = 0) { mockVideoTile.unbind() }
        verify(exactly = 0) { mockVideoTile2.unbind() }
        verify(exactly = 1) { mockVideoTile.bind(any()) }
        verify(exactly = 1) { mockVideoTile2.bind(any()) }
    }

    @Test
    fun `onReceiveFrame should notify observer about video tile size change with new frame size`() {
        val mockObserver = spyk(tileObserver)

        videoTileController.addVideoTileObserver(mockObserver)
        runBlockingTest {
            videoTileController.onReceiveFrame(
                mockVideoFrame,
                tileId,
                attendeeId,
                VideoPauseState.Unpaused
            )
        }

        every { mockVideoFrameBuffer.width } returns testWidth + 1
        every { mockVideoFrameBuffer.height } returns testHeight + 1

        runBlockingTest {
            videoTileController.onReceiveFrame(
                VideoFrame(
                        testTimestamp,
                        mockVideoFrameBuffer,
                        testRotation
                ),
                tileId,
                attendeeId,
                VideoPauseState.Unpaused
            )
        }

        Assert.assertEquals(1, onSizeChangedObserverCalled)
        verify {
            mockObserver.onVideoTileAdded(
                VideoTileState(
                    tileId,
                    attendeeId,
                    testWidth + 1,
                    testHeight + 1,
                    VideoPauseState.Unpaused,
                    remoteTile
                )
            )
        }
    }

    @Test
    fun `onReceiveFrame should NOT notify observer about video tile size change with the same frame size`() {
        val mockObserver = spyk(tileObserver)

        videoTileController.addVideoTileObserver(mockObserver)
        runBlockingTest {
            videoTileController.onReceiveFrame(
                mockVideoFrame,
                tileId,
                attendeeId,
                VideoPauseState.Unpaused
            )
        }

        every { mockVideoFrameBuffer.width } returns testWidth
        every { mockVideoFrameBuffer.height } returns testHeight

        runBlockingTest {
            videoTileController.onReceiveFrame(
                VideoFrame(
                        testTimestamp,
                        mockVideoFrameBuffer,
                        testRotation
                ),
                tileId,
                attendeeId,
                VideoPauseState.Unpaused
            )
        }

        Assert.assertEquals(0, onSizeChangedObserverCalled)
        verify {
            mockObserver.onVideoTileAdded(
                VideoTileState(
                    tileId,
                    attendeeId,
                    testWidth,
                    testHeight,
                    VideoPauseState.Unpaused,
                    remoteTile
                )
            )
        }
    }

    @Test
    fun `Local video tile should be added with attendeeId`() {
        val mockObserver = spyk(tileObserver)
        every { mockVideoTile.state } returns VideoTileState(
            tileId,
            attendeeId,
            testWidth,
            testHeight,
            VideoPauseState.Unpaused,
            localTile
        )

        videoTileController.addVideoTileObserver(mockObserver)
        runBlockingTest {
            videoTileController.onReceiveFrame(
                mockVideoFrame,
                tileId,
                null,
                VideoPauseState.Unpaused
            )
        }

        verify {
            mockObserver.onVideoTileAdded(
                VideoTileState(
                    tileId,
                    attendeeId,
                    testWidth,
                    testHeight,
                    VideoPauseState.Unpaused,
                    localTile
                )
            )
        }
    }

    @Test
    fun `onReceiveFrame should notify observer about video tile add when the video is unbound and previously removed`() {
        val mockObserver = spyk(tileObserver)

        videoTileController.addVideoTileObserver(mockObserver)
        runBlockingTest {
            // Add video but do not bind it
            videoTileController.onReceiveFrame(
                mockVideoFrame,
                tileId,
                attendeeId,
                VideoPauseState.Unpaused
            )
            // Remove video
            videoTileController.onReceiveFrame(null, tileId, attendeeId, VideoPauseState.Unpaused)
            // Add video again
            videoTileController.onReceiveFrame(
                mockVideoFrame,
                tileId,
                attendeeId,
                VideoPauseState.Unpaused
            )
        }

        verify(exactly = 1) {
            mockObserver.onVideoTileRemoved(
                VideoTileState(
                    tileId,
                    attendeeId,
                    testWidth,
                    testHeight,
                    VideoPauseState.Unpaused,
                    remoteTile
                )
            )
        }
        verify(exactly = 2) {
            mockObserver.onVideoTileAdded(
                VideoTileState(
                    tileId,
                    attendeeId,
                    testWidth,
                    testHeight,
                    VideoPauseState.Unpaused,
                    remoteTile
                )
            )
        }
    }

    @Test
    fun `onReceiveFrame should notify observer about video tile add when the video is bound and previously removed`() {
        val mockObserver = spyk(tileObserver)

        videoTileController.addVideoTileObserver(mockObserver)
        runBlockingTest {
            // Add video and bind it
            videoTileController.onReceiveFrame(
                mockVideoFrame,
                tileId,
                attendeeId,
                VideoPauseState.Unpaused
            )
            videoTileController.bindVideoView(mockVideoRenderView, tileId)
            // Remove video
            videoTileController.onReceiveFrame(null, tileId, attendeeId, VideoPauseState.Unpaused)
            // Add video again
            videoTileController.onReceiveFrame(
                mockVideoFrame,
                tileId,
                attendeeId,
                VideoPauseState.Unpaused
            )
        }

        verify(exactly = 1) {
            mockObserver.onVideoTileRemoved(
                VideoTileState(
                    tileId,
                    attendeeId,
                    testWidth,
                    testHeight,
                    VideoPauseState.Unpaused,
                    remoteTile
                )
            )
        }
        verify(exactly = 2) {
            mockObserver.onVideoTileAdded(
                VideoTileState(
                    tileId,
                    attendeeId,
                    testWidth,
                    testHeight,
                    VideoPauseState.Unpaused,
                    remoteTile
                )
            )
        }
    }

    @Test
    fun `onReceiveFrame should notify observer about video tile add when the paused video was previously removed then added and resumed`() {
        val mockObserver = spyk(tileObserver)

        videoTileController.addVideoTileObserver(mockObserver)
        runBlockingTest {
            // Add video but do not bind it
            videoTileController.onReceiveFrame(
                mockVideoFrame,
                tileId,
                attendeeId,
                VideoPauseState.Unpaused
            )
            // Pause video
            videoTileController.onReceiveFrame(
                mockVideoFrame,
                tileId,
                attendeeId,
                VideoPauseState.PausedByUserRequest
            )
            // Remove video
            videoTileController.onReceiveFrame(null, tileId, attendeeId, VideoPauseState.Unpaused)
            // Add video again
            videoTileController.onReceiveFrame(
                null,
                tileId,
                attendeeId,
                VideoPauseState.PausedByUserRequest
            )
            // Resume video
            videoTileController.onReceiveFrame(
                mockVideoFrame,
                tileId,
                attendeeId,
                VideoPauseState.Unpaused
            )
        }

        verify(exactly = 1) {
            mockObserver.onVideoTileRemoved(
                VideoTileState(
                    tileId,
                    attendeeId,
                    testWidth,
                    testHeight,
                    VideoPauseState.Unpaused,
                    remoteTile
                )
            )
        }
        verify(exactly = 2) {
            mockObserver.onVideoTileAdded(
                VideoTileState(
                    tileId,
                    attendeeId,
                    testWidth,
                    testHeight,
                    VideoPauseState.Unpaused,
                    remoteTile
                )
            )
        }
    }
}


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

package com.amazonaws.services.chime.sdk.meetings.audiovideo.video

import com.amazonaws.services.chime.sdk.meetings.utils.logger.ConsoleLogger
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Test

class DefaultVideoTileFactoryTest {
    @Test
    fun `makeTile should return object with data from parameters`() {
        val testTileId = 1
        val testAttendeeId = "attendeeId"
        val testWidth = 720
        val testHeight = 1280
        val testLogger = ConsoleLogger()
        val videoTileFactory = DefaultVideoTileFactory(testLogger)

        val testOutput: VideoTile = videoTileFactory.makeTile(testTileId, testAttendeeId, testWidth, testHeight, false)

        assertNotNull(testOutput)
        assertEquals(testTileId, testOutput.state.tileId)
        assertEquals(testAttendeeId, testOutput.state.attendeeId)
        assertEquals(testWidth, testOutput.state.videoStreamContentWidth)
        assertEquals(testHeight, testOutput.state.videoStreamContentHeight)
    }
}


package com.amazonaws.services.chime.sdk.meetings.audiovideo.video

import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test

class VideoTileStateTest {

    private val tileId = 117 // some random id
    private val testWidth = 720
    private val testHeight = 1280
    private val attendeeIdVideo = "chimesarang"
    private val attendeeIdScreenShare = "chimesarang#content"

    // local tile
    val vtsLocal = VideoTileState(tileId, attendeeIdVideo, testWidth, testHeight, VideoPauseState.Unpaused, true)

    // regular video sharing
    val vtsVideo = VideoTileState(tileId, attendeeIdVideo, testWidth, testHeight, VideoPauseState.Unpaused, false)

    // screen sharing
    val vtsScreenShare = VideoTileState(tileId, attendeeIdScreenShare, testWidth, testHeight, VideoPauseState.Unpaused, false)

    @Test
    fun `isLocalTile should be true when tile is local`() {
        assertTrue(vtsLocal.isLocalTile)
        assertFalse(vtsVideo.isLocalTile)
        assertFalse(vtsScreenShare.isLocalTile)
    }

    @Test
    fun `isContent should be true when sharing screen`() {
        assertFalse(vtsLocal.isContent)
        assertFalse(vtsVideo.isContent)
        assertTrue(vtsScreenShare.isContent)
    }
}


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

package com.amazonaws.services.chime.sdk.meetings.audiovideo.video.capture

import android.content.Context
import android.graphics.Matrix
import android.hardware.camera2.CameraCaptureSession
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CameraManager
import android.hardware.camera2.CameraMetadata
import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
import android.util.Range
import android.view.WindowManager
import androidx.core.content.ContextCompat
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoContentHint
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoFrame
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoSink
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.buffer.VideoFrameTextureBuffer
import com.amazonaws.services.chime.sdk.meetings.device.MediaDevice
import com.amazonaws.services.chime.sdk.meetings.device.MediaDeviceType
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger
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.mockk
import io.mockk.mockkConstructor
import io.mockk.mockkObject
import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.slot
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.Assert
import org.junit.Before
import org.junit.Ignore

class DefaultCameraCaptureSourceTest {

    @MockK
    private lateinit var mockContext: Context

    @MockK
    private lateinit var mockLogger: Logger

    @MockK
    private lateinit var mockSurfaceTextureCaptureSourceFactory: DefaultSurfaceTextureCaptureSourceFactory

    @MockK
    private lateinit var mockCameraManager: CameraManager

    @InjectMockKs
    private lateinit var testCameraCaptureSource: DefaultCameraCaptureSource

    private lateinit var mockLooper: Looper

    @MockK
    private lateinit var mockFrontCameraCharacteristics: CameraCharacteristics

    @MockK
    private lateinit var mockBackCameraCharacteristics: CameraCharacteristics

    @MockK(relaxed = true)
    private lateinit var mockSurfaceTextureCaptureSource: SurfaceTextureCaptureSource

    @MockK(relaxed = true)
    private lateinit var mockMatrix: Matrix

    @MockK
    private lateinit var mockVideoSink: VideoSink

    @MockK(relaxed = true)
    private lateinit var mockWindowManager: WindowManager

    @MockK(relaxed = true)
    private lateinit var mockCameraDevice: CameraDevice

    @MockK(relaxed = true)
    private lateinit var mockCameraSession: CameraCaptureSession

    @MockK
    private lateinit var mockObserver: CaptureSourceObserver

    private val testDispatcher = TestCoroutineDispatcher()

    @Before
    fun setUp() {
        // Setup handler/thread/looper mocking
        mockLooper = mockk()
        mockkConstructor(HandlerThread::class)
        every { anyConstructed<HandlerThread>().looper } returns mockLooper
        mockkConstructor(Handler::class)
        every { anyConstructed<Handler>().looper } returns mockLooper
        val slot = slot<Runnable>()
        every { anyConstructed<Handler>().post(capture(slot)) } answers {
            slot.captured.run()
            true
        }
        mockkStatic(Looper::class)
        every { Looper.myLooper() } returns mockLooper

        mockkObject(MediaDevice.Companion)
        every { MediaDevice.listVideoDevices(any()) } returns listOf(
            MediaDevice("front", MediaDeviceType.VIDEO_FRONT_CAMERA, "0"),
            MediaDevice("back", MediaDeviceType.VIDEO_BACK_CAMERA, "1")
        )
        every { MediaDevice.listSupportedVideoCaptureFormats(any(), any()) } returns listOf(
            VideoCaptureFormat(1280, 720, 15)
        )

        mockkStatic(ContextCompat::class)
        every { ContextCompat.checkSelfPermission(any(), any()) } returns 0

        mockkConstructor(Matrix::class)
        every { anyConstructed<Matrix>().preTranslate(any(), any()) } returns true
        every { anyConstructed<Matrix>().preScale(any(), any()) } returns true
        every { anyConstructed<Matrix>().preRotate(any()) } returns true
        every { anyConstructed<Matrix>().preConcat(any()) } returns true

        Dispatchers.setMain(testDispatcher)

        // Most of the previous mocks need to be done before constructor call
        MockKAnnotations.init(this, relaxUnitFun = true)

        every { mockCameraManager.getCameraCharacteristics("0") } returns mockFrontCameraCharacteristics
        every { mockCameraManager.getCameraCharacteristics("1") } returns mockBackCameraCharacteristics
        every { mockCameraManager.openCamera(any(), any<CameraDevice.StateCallback>(), any()) } just runs
        every { mockSurfaceTextureCaptureSourceFactory.createSurfaceTextureCaptureSource(any(), any(), any()) } returns mockSurfaceTextureCaptureSource
        every { mockFrontCameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES) } returns arrayOf(Range(15, 15))
        every { mockFrontCameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) } returns 270
        every { mockFrontCameraCharacteristics.get(CameraCharacteristics.LENS_FACING) } returns CameraMetadata.LENS_FACING_FRONT
        every { mockFrontCameraCharacteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION) } returns null
        every { mockBackCameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES) } returns arrayOf(Range(15, 15))
        every { mockBackCameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) } returns 180
        every { mockBackCameraCharacteristics.get(CameraCharacteristics.LENS_FACING) } returns CameraMetadata.LENS_FACING_BACK
        every { mockContext.getSystemService(Context.WINDOW_SERVICE) } returns mockWindowManager
    }

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

    @Ignore("Broken on build server, possible Mockk issue")
    fun `start creates and starts surface source, and calls CameraManager openCamera`() {
        testCameraCaptureSource.start()

        verify { mockSurfaceTextureCaptureSourceFactory.createSurfaceTextureCaptureSource(1280, 720, VideoContentHint.Motion) }
        verify { mockSurfaceTextureCaptureSource.start() }
        verify { mockSurfaceTextureCaptureSource.addVideoSink(any()) }
        verify { mockCameraManager.openCamera("0", any<CameraDevice.StateCallback>(), any()) }
    }

    @Ignore("Broken on build server, possible Mockk issue")
    fun `switch should default to back if front camera is missing`() {
        testCameraCaptureSource.start()
        testCameraCaptureSource.switchCamera()
        every { MediaDevice.listVideoDevices(any()) } returns listOf(
            MediaDevice("back", MediaDeviceType.VIDEO_BACK_CAMERA, "1")
        ) andThen listOf(
            MediaDevice("back", MediaDeviceType.VIDEO_BACK_CAMERA, "1")
        )

        Assert.assertEquals(MediaDeviceType.VIDEO_BACK_CAMERA, testCameraCaptureSource.device?.type)

        testCameraCaptureSource.switchCamera()

        Assert.assertEquals(MediaDeviceType.VIDEO_BACK_CAMERA, testCameraCaptureSource.device?.type)
}

    @Ignore("Broken on build server, possible Mockk issue")
    fun `stop stops and releases surface texture capture source`() {
        testCameraCaptureSource.start()
        testCameraCaptureSource.stop()

        verify { mockSurfaceTextureCaptureSource.removeVideoSink(any()) }
        verify { mockSurfaceTextureCaptureSource.stop() }
        verify { mockSurfaceTextureCaptureSource.release() }
    }

    @Ignore("Broken on build server, possible Mockk issue")
    fun `setting device will restart source`() {
        testCameraCaptureSource.start()
        testCameraCaptureSource.device = MediaDevice("back", MediaDeviceType.VIDEO_BACK_CAMERA, "1")

        verify(exactly = 1) { mockCameraManager.openCamera("0", any<CameraDevice.StateCallback>(), any()) }
        verify(exactly = 1) { mockCameraManager.openCamera("1", any<CameraDevice.StateCallback>(), any()) }
    }

    @Ignore("Broken on build server, possible Mockk issue")
    fun `switchCamera will switch to back camera`() {
        testCameraCaptureSource.start()
        testCameraCaptureSource.switchCamera()

        verify(exactly = 1) { mockCameraManager.openCamera("0", any<CameraDevice.StateCallback>(), any()) }
        verify(exactly = 1) { mockCameraManager.openCamera("1", any<CameraDevice.StateCallback>(), any()) }
    }

    @Ignore("Broken on build server, possible Mockk issue")
    fun `capturer will pass through frames`() {
        testCameraCaptureSource.addVideoSink(mockVideoSink)
        testCameraCaptureSource.start()
        val testFrame = VideoFrame(0,
            VideoFrameTextureBuffer(
                1280,
                720,
                1,
                mockMatrix,
                VideoFrameTextureBuffer.Type.TEXTURE_2D,
                Runnable {})
        )
        testCameraCaptureSource.onVideoFrameReceived(testFrame)

        verify(exactly = 1) { mockVideoSink.onVideoFrameReceived(any()) }
    }

    @Ignore("Broken on build server, possible Mockk issue")
    fun `onOpened triggers camera device createCaptureSession`() {
        val slot = slot<CameraDevice.StateCallback>()
        every { mockCameraManager.openCamera(any(), capture(slot), any()) } just runs

        testCameraCaptureSource.start()

        slot.captured.onOpened(mockCameraDevice)

        verify { mockCameraDevice.createCaptureSession(any(), any(), any()) }
    }

    @Ignore("Broken on build server, possible Mockk issue")
    fun `onConfigured triggers setRepeatingRequest`() {
        val stateCallbackSlot = slot<CameraDevice.StateCallback>()
        every { mockCameraManager.openCamera(any(), capture(stateCallbackSlot), any()) } just runs
        val sessionCallbackSlot = slot<CameraCaptureSession.StateCallback>()
        every { mockCameraDevice.createCaptureSession(any(), capture(sessionCallbackSlot), any()) } just runs

        testCameraCaptureSource.start()

        stateCallbackSlot.captured.onOpened(mockCameraDevice)
        sessionCallbackSlot.captured.onConfigured(mockCameraSession)

        verify { mockCameraSession.setRepeatingRequest(any(), any(), any()) }
    }

    @Ignore("Broken on build server, possible Mockk issue")
    fun `No FPS ranges trigger configuration error`() {
        val stateCallbackSlot = slot<CameraDevice.StateCallback>()
        every { mockCameraManager.openCamera(any(), capture(stateCallbackSlot), any()) } just runs
        val sessionCallbackSlot = slot<CameraCaptureSession.StateCallback>()
        every { mockCameraDevice.createCaptureSession(any(), capture(sessionCallbackSlot), any()) } just runs

        every { mockFrontCameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES) } returns null

        testCameraCaptureSource.addCaptureSourceObserver(mockObserver)
        testCameraCaptureSource.start()

        stateCallbackSlot.captured.onOpened(mockCameraDevice)
        sessionCallbackSlot.captured.onConfigured(mockCameraSession)

        verify { mockObserver.onCaptureFailed(CaptureSourceError.ConfigurationFailure) }
    }

    @Ignore("Broken on build server, possible Mockk issue")
    fun `FPS range above max value still is selected`() {
        val stateCallbackSlot = slot<CameraDevice.StateCallback>()
        every { mockCameraManager.openCamera(any(), capture(stateCallbackSlot), any()) } just runs
        val sessionCallbackSlot = slot<CameraCaptureSession.StateCallback>()
        every { mockCameraDevice.createCaptureSession(any(), capture(sessionCallbackSlot), any()) } just runs

        every { mockFrontCameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES) } returns arrayOf(Range(30, 30))

        testCameraCaptureSource.addCaptureSourceObserver(mockObserver)
        testCameraCaptureSource.start()

        stateCallbackSlot.captured.onOpened(mockCameraDevice)
        sessionCallbackSlot.captured.onConfigured(mockCameraSession)

        verify { mockObserver.onCaptureFailed(CaptureSourceError.ConfigurationFailure) }
    }
}


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

package com.amazonaws.services.chime.sdk.meetings.audiovideo.video.capture

import android.content.Context
import android.content.Intent
import android.hardware.display.DisplayManager
import android.hardware.display.VirtualDisplay
import android.media.projection.MediaProjection
import android.media.projection.MediaProjectionManager
import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
import android.view.Display
import android.view.Surface
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoFrame
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoSink
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.InjectMockKs
import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import io.mockk.mockkConstructor
import io.mockk.mockkStatic
import io.mockk.slot
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 DefaultScreenCaptureSourceTest {

    @MockK(relaxed = true)
    private lateinit var mockContext: Context

    @MockK
    private lateinit var mockLogger: Logger

    @MockK
    private lateinit var mockSurfaceTextureCaptureSourceFactory: SurfaceTextureCaptureSourceFactory

    @MockK(relaxed = true)
    private lateinit var mockDisplayManager: DisplayManager

    @MockK
    private lateinit var mockMediaProjectionManager: MediaProjectionManager

    private var activityResult = 1

    @MockK
    private lateinit var mockActivityData: Intent

    @InjectMockKs
    private lateinit var testScreenCaptureSource: DefaultScreenCaptureSource

    private lateinit var mockLooper: Looper

    @MockK(relaxed = true)
    private lateinit var mockSurfaceTextureCaptureSource: SurfaceTextureCaptureSource

    @MockK
    private lateinit var mockVideoSink: VideoSink

    @MockK
    private lateinit var mockMediaProjection: MediaProjection

    @MockK
    private lateinit var mockVirtualDisplay: VirtualDisplay

    @MockK
    private lateinit var mockCaptureSourceObserver: CaptureSourceObserver

    @MockK
    private lateinit var mockFrame: VideoFrame

    @MockK
    private lateinit var mockDisplay: Display

    private val testDispatcher = TestCoroutineDispatcher()

    @Before
    fun setUp() {
        mockLooper = mockk()
        mockkConstructor(HandlerThread::class)
        every { anyConstructed<HandlerThread>().looper } returns mockLooper
        mockkConstructor(Handler::class)
        every { anyConstructed<Handler>().looper } returns mockLooper
        val slot = slot<Runnable>()
        every { anyConstructed<Handler>().post(capture(slot)) } answers {
            slot.captured.run()
            true
        }
        mockkStatic(Looper::class)
        every { Looper.myLooper() } returns mockLooper

        Dispatchers.setMain(testDispatcher)

        MockKAnnotations.init(this, relaxUnitFun = true)

        every { mockSurfaceTextureCaptureSourceFactory.createSurfaceTextureCaptureSource(any(), any(), any()) } returns mockSurfaceTextureCaptureSource
        every { mockContext.getSystemService(Context.DISPLAY_SERVICE) } returns mockDisplayManager
        every { mockContext.getSystemService(Context.MEDIA_PROJECTION_SERVICE) } returns mockMediaProjectionManager
        every { mockMediaProjectionManager.getMediaProjection(1, mockActivityData) } returns mockMediaProjection
        every { mockMediaProjection.createVirtualDisplay(any(), any(), any(), any(), any(), any(), any(), any()) } returns mockVirtualDisplay
        every { mockDisplayManager.getDisplay(Display.DEFAULT_DISPLAY) } returns mockDisplay
    }

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

    @Test
    fun `start will notify subscribed observers of onCaptureStarted event`() {
        every { mockDisplay.rotation } returns Surface.ROTATION_0
        testScreenCaptureSource.addCaptureSourceObserver(mockCaptureSourceObserver)

        testScreenCaptureSource.start()

        verify(exactly = 1) { mockCaptureSourceObserver.onCaptureStarted() }
    }

    @Test
    fun `stop will notify subscribed observers of onCaptureStopped event`() {
        testScreenCaptureSource.addCaptureSourceObserver(mockCaptureSourceObserver)

        testScreenCaptureSource.stop()

        verify(exactly = 1) { mockCaptureSourceObserver.onCaptureStopped() }
    }

    @Test
    fun `restarting for rotation will not notify subscribed observers of events`() {
        every { mockDisplay.rotation } returns Surface.ROTATION_90

        testScreenCaptureSource.onVideoFrameReceived(mockFrame)

        verify(exactly = 1) { mockMediaProjection.stop() }
        verify(exactly = 1) { mockMediaProjectionManager.getMediaProjection(1, mockActivityData) }
        verify(exactly = 0) { mockCaptureSourceObserver.onCaptureStopped() }
        verify(exactly = 0) { mockCaptureSourceObserver.onCaptureStarted() }
    }
}


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

package com.amazonaws.services.chime.sdk.meetings.audiovideo.video.capture

import android.graphics.SurfaceTexture
import android.opengl.EGL14
import android.opengl.EGLSurface
import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoContentHint
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoSink
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.utils.logger.Logger
import com.xodee.client.video.TimestampAligner
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.just
import io.mockk.mockkConstructor
import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.slot
import io.mockk.verify
import java.lang.Runnable
import org.junit.Assert.assertNotEquals
import org.junit.Before
import org.junit.Test

class DefaultSurfaceTextureCaptureSourceTest {
    @MockK
    private lateinit var mockLogger: Logger

    @MockK
    private lateinit var mockEglCoreFactory: EglCoreFactory

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

    private lateinit var testSurfaceTextureCaptureSource: DefaultSurfaceTextureCaptureSource

    @MockK
    private lateinit var mockLooper: Looper

    @MockK(relaxed = true)
    private lateinit var mockEglSurface: EGLSurface

    @MockK
    private lateinit var mockVideoSink: VideoSink

    @MockK
    private lateinit var mockSurfaceTexture: SurfaceTexture

    private val testWidth = 1
    private val testHeight = 2
    private val testContentHint = VideoContentHint.Motion

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

        // Setup handler/thread mocks
        mockkConstructor(HandlerThread::class)
        every { anyConstructed<HandlerThread>().looper } returns mockLooper
        mockkConstructor(Handler::class)
        every { anyConstructed<Handler>().looper } returns mockLooper
        val slot = slot<Runnable>()
        every { anyConstructed<Handler>().post(capture(slot)) } answers {
            slot.captured.run()
            true
        }
        every { mockLooper.quit() } just runs

        every { mockEglCoreFactory.createEglCore() } returns mockEglCore

        mockkStatic(EGL14::class)
        every { EGL14.eglCreatePbufferSurface(any(), any(), any(), any()) } returns mockEglSurface

        mockkConstructor(SurfaceTexture::class)

        mockkConstructor(TimestampAligner::class)
        every { anyConstructed<TimestampAligner>().translateTimestamp(any()) } returns 0

        testSurfaceTextureCaptureSource = DefaultSurfaceTextureCaptureSource(
                mockLogger,
                testWidth,
                testHeight,
                testContentHint,
                mockEglCoreFactory
        )
    }

    @Test
    fun `constructor sets expected dimensions of surface`() {
        DefaultSurfaceTextureCaptureSource(
                mockLogger,
                testWidth,
                testHeight,
                testContentHint,
                mockEglCoreFactory
        )

        verify { anyConstructed<SurfaceTexture>().setDefaultBufferSize(testWidth, testHeight) }
    }

    @Test
    fun `start calls setOnFrameAvailableListener with non-null listener`() {
        val slot = slot<SurfaceTexture.OnFrameAvailableListener>()
        every { anyConstructed<SurfaceTexture>().setOnFrameAvailableListener(capture(slot), any()) } just runs

        testSurfaceTextureCaptureSource.start()

        assertNotEquals(slot.captured, null)
    }

    @Test
    fun `stop calls setOnFrameAvailableListener with null listener`() {
        val slot = slot<SurfaceTexture.OnFrameAvailableListener>()
        every { anyConstructed<SurfaceTexture>().setOnFrameAvailableListener(capture(slot)) } just runs

        testSurfaceTextureCaptureSource.stop()

        verify { anyConstructed<SurfaceTexture>().setOnFrameAvailableListener(null) }
    }

    @Test
    fun `surface texture frame is passed along to video sink`() {
        val slot = slot<SurfaceTexture.OnFrameAvailableListener>()
        every { anyConstructed<SurfaceTexture>().setOnFrameAvailableListener(capture(slot), any()) } just runs

        testSurfaceTextureCaptureSource.start()
        testSurfaceTextureCaptureSource.addVideoSink(mockVideoSink)

        slot.captured.onFrameAvailable(mockSurfaceTexture)

        verify { anyConstructed<SurfaceTexture>().updateTexImage() }
        verify { mockVideoSink.onVideoFrameReceived(any()) }
    }

    @Test
    fun `release will release resources if there is no frame in flight`() {
        testSurfaceTextureCaptureSource.release()

        verify { anyConstructed<SurfaceTexture>().release() }
        verify { mockEglCore.release() }
        verify { anyConstructed<TimestampAligner>().dispose() }
        verify { mockLooper.quit() }
    }
}


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

package com.amazonaws.services.chime.sdk.meetings.audiovideo.video.gl

import android.opengl.EGL14
import android.opengl.EGLConfig
import android.opengl.EGLContext
import android.opengl.EGLDisplay
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.mockkStatic
import io.mockk.slot
import io.mockk.verify
import org.junit.Before
import org.junit.Test

class DefaultEglCoreTest {

    @MockK
    private lateinit var mockDisplay: EGLDisplay

    @MockK
    private lateinit var mockContext: EGLContext

    @MockK
    private lateinit var mockConfig: EGLConfig

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

        // Set up some static functions which need to return valid values
        mockkStatic(EGL14::class)
        every { EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY) } returns mockDisplay
        every { EGL14.eglInitialize(any(), any(), any(), any(), any()) } returns true
        every { EGL14.eglQueryContext(any(), any(), any(), any(), any()) } returns true

        val slot = slot<Array<EGLConfig>>()
        every { EGL14.eglChooseConfig(any(), any(), any(), capture(slot), any(), any(), any(), any()) } answers {
            slot.captured[0] = mockConfig
            true
        }
    }

    @Test
    fun `constructor calls eglInitialize and eglCreateContext`() {
        DefaultEglCore(Runnable {}, mockContext)

        verify { EGL14.eglInitialize(any(), any(), any(), any(), any()) }
        verify { EGL14.eglCreateContext(any(), any(), any(), any(), any()) }
    }

    @Test
    fun `release calls eglDestroyContext and eglTerminate`() {
        val testEglCore = DefaultEglCore(Runnable {}, mockContext)
        testEglCore.release()

        verify { EGL14.eglDestroyContext(any(), any()) }
        verify { EGL14.eglTerminate(any()) }
    }
}


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

package com.amazonaws.services.chime.sdk.meetings.audiovideo.video.gl

import android.content.Context
import android.graphics.Matrix
import android.opengl.EGL14
import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
import android.view.SurfaceHolder
import android.view.SurfaceView
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoFrame
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.buffer.VideoFrameTextureBuffer
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkConstructor
import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.slot
import io.mockk.verify
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.android.HandlerDispatcher
import kotlinx.coroutines.test.setMain
import org.junit.Before
import org.junit.Test

class TextureRenderViewTest {
    @MockK
    private lateinit var mockContext: Context

    private lateinit var testEglVideoRenderView: TextureRenderView

    @MockK
    private lateinit var mockEglCoreFactory: EglCoreFactory

    private lateinit var mockHandlerDispatcher: HandlerDispatcher

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

    private lateinit var mockLooper: Looper

    @MockK
    private lateinit var mockMatrix: Matrix

    @MockK
    private lateinit var mockHolder: SurfaceHolder

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

        // Setup handler/thread/looper mocking
        mockLooper = mockk()
        mockkConstructor(HandlerThread::class)
        every { anyConstructed<HandlerThread>().looper } returns mockLooper
        mockHandlerDispatcher = mockk()
        mockkConstructor(Handler::class)
        every { anyConstructed<Handler>().looper } returns mockLooper
        val slot = slot<Runnable>()
        every { anyConstructed<Handler>().post(capture(slot)) } answers {
            slot.captured.run()
            true
        }
        mockkStatic(Looper::class)
        every { Looper.myLooper() } returns mockLooper
        every { mockLooper.quitSafely() } just runs
        Dispatchers.setMain(Dispatchers.Unconfined)

        mockkConstructor(SurfaceView::class)
        // For some reason the following has no effect
        every { anyConstructed<SurfaceView>().holder } returns mockHolder
        every { mockHolder.addCallback(any()) } just runs

        every { mockEglCoreFactory.createEglCore() } returns mockEglCore

        // Create actual test object
        testEglVideoRenderView = TextureRenderView(mockContext)
    }

    @Test
    fun `init creates and starts thread, creates eglCore`() {
        testEglVideoRenderView.init(mockEglCoreFactory)

        verify { anyConstructed<HandlerThread>().start() }
        verify { mockEglCoreFactory.createEglCore() }
    }

    @Test
    fun `release releases eglCore and stops thread`() {
        testEglVideoRenderView.init(mockEglCoreFactory)
        testEglVideoRenderView.release()

        verify { mockEglCore.release() }
        verify { mockLooper.quitSafely() }
    }

    @Test
    fun `onReceiveVideoFrame eventually calls eglSwapBuffers`() {
        mockkStatic(EGL14::class)

        testEglVideoRenderView.init(mockEglCoreFactory)
        val testFrame = VideoFrame(0,
                VideoFrameTextureBuffer(
                        1280,
                        720,
                        1,
                        mockMatrix,
                        VideoFrameTextureBuffer.Type.TEXTURE_OES,
                        Runnable {})
        )

        testEglVideoRenderView.onVideoFrameReceived(testFrame)

        verify { EGL14.eglSwapBuffers(any(), any()) }
    }
}


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

package com.amazonaws.services.chime.sdk.meetings.device

import android.content.Context
import android.content.Intent
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.media.AudioRecordingConfiguration
import com.amazonaws.services.chime.sdk.meetings.analytics.EventAnalyticsController
import com.amazonaws.services.chime.sdk.meetings.analytics.MeetingHistoryEventName
import com.amazonaws.services.chime.sdk.meetings.internal.audio.AudioClientController
import com.amazonaws.services.chime.sdk.meetings.internal.audio.AudioClientState
import com.amazonaws.services.chime.sdk.meetings.internal.audio.DefaultAudioClientController
import com.amazonaws.services.chime.sdk.meetings.internal.video.VideoClientController
import com.xodee.client.audio.audioclient.AudioClient
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.mockkClass
import io.mockk.mockkStatic
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.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test

@ExperimentalCoroutinesApi
class DefaultDeviceControllerTest {
    @MockK
    private lateinit var activeConfiguration: AudioRecordingConfiguration

    @MockK
    private lateinit var audioDevice: AudioDeviceInfo

    @MockK
    private lateinit var speakerInfo: AudioDeviceInfo

    @MockK
    private lateinit var earpieceInfo: AudioDeviceInfo

    @MockK
    private lateinit var telephonyInfo: AudioDeviceInfo

    @MockK
    private lateinit var wiredHeadsetInfo: AudioDeviceInfo

    @MockK
    private lateinit var bluetoothInfo: AudioDeviceInfo

    @MockK
    private lateinit var context: Context

    @MockK
    private lateinit var audioClientController: AudioClientController

    @MockK
    private lateinit var videoClientController: VideoClientController

    @MockK
    private lateinit var eventAnalyticsController: EventAnalyticsController

    @MockK
    private lateinit var audioManager: AudioManager

    @MockK
    private lateinit var deviceChangeObserver: DeviceChangeObserver

    private lateinit var deviceController: DefaultDeviceController

    private val testDispatcher = TestCoroutineDispatcher()

    private fun setupForNewAPILevel() {
        MockKAnnotations.init(this, relaxUnitFun = true)
        deviceController = DefaultDeviceController(
            context,
            audioClientController,
            videoClientController,
            eventAnalyticsController,
            audioManager,
            24
        )
        commonSetup()
    }

    private fun setupForOldAPILevel() {
        MockKAnnotations.init(this, relaxUnitFun = true)
        every { context.registerReceiver(any(), any()) } returns Intent()
        deviceController = DefaultDeviceController(
            context,
            audioClientController,
            videoClientController,
            eventAnalyticsController,
            audioManager,
            21
        )
        commonSetup()
    }

    private fun commonSetup() {
        every { speakerInfo.type } returns AudioDeviceInfo.TYPE_BUILTIN_SPEAKER
        every { speakerInfo.productName } returns "default speaker"
        every { earpieceInfo.type } returns AudioDeviceInfo.TYPE_BUILTIN_EARPIECE
        every { earpieceInfo.productName } returns "default receiver"
        every { telephonyInfo.type } returns AudioDeviceInfo.TYPE_TELEPHONY
        every { telephonyInfo.productName } returns "telephony receiver"
        every { wiredHeadsetInfo.type } returns AudioDeviceInfo.TYPE_WIRED_HEADSET
        every { wiredHeadsetInfo.productName } returns "my wired headset"
        every { bluetoothInfo.type } returns AudioDeviceInfo.TYPE_BLUETOOTH_SCO
        every { bluetoothInfo.productName } returns "my bluetooth headphone"
        every { audioDevice.productName } returns "my product name"
        every { audioDevice.type } returns AudioDeviceInfo.TYPE_BLUETOOTH_SCO
        every { activeConfiguration.audioDevice } returns audioDevice
        every { audioManager.activeRecordingConfigurations } returns listOf(activeConfiguration)
    }

    @Before
    fun setup() {
        Dispatchers.setMain(testDispatcher)
    }

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

    @Test
    fun `deviceController should call addMeetingHistoryEvent when audio device is selected`() {
        setupForNewAPILevel()
        every { audioClientController.setRoute(any()) } returns true
        mockkStatic(DefaultAudioClientController::class)
        DefaultAudioClientController.audioClientState = AudioClientState.STARTED
        deviceController.chooseAudioDevice(MediaDevice(
            "speaker",
            MediaDeviceType.AUDIO_BUILTIN_SPEAKER
        ))

        verify(exactly = 1) {
            eventAnalyticsController.pushHistory(MeetingHistoryEventName.audioInputSelected)
        }
    }

    @Test
    fun `deviceController should register device change event when build version is high`() {
        setupForNewAPILevel()
        verify { audioManager.registerAudioDeviceCallback(any(), null) }
    }

    @Test
    fun `deviceController should call BluetoothDeviceController startListening`() {
        setupForNewAPILevel()
    }

    @Test
    fun `deviceController stopListening should call BluetoothDeviceController stopListening`() {
        setupForNewAPILevel()
    }

    @Test
    fun `deviceController getActiveAudioDevice should return device from audioManager active recording`() {
        setupForNewAPILevel()
        every { audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS) } returns arrayOf(
            speakerInfo, earpieceInfo, audioDevice
        )
        val expected = MediaDevice("my product name (Bluetooth)", MediaDeviceType.AUDIO_BLUETOOTH)
        val mediaDevice = deviceController.getActiveAudioDevice()
        assertEquals(expected, mediaDevice)
    }

    @Test
    fun `deviceController should register device change event when build version is low`() {
        setupForOldAPILevel()
        verify(exactly = 3) { context.registerReceiver(any(), any()) }
    }

    @Test
    fun `listAudioDevices should return a list of connected devices with product name when build version is high`() {
        setupForNewAPILevel()
        every { audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS) } returns arrayOf(
            speakerInfo, earpieceInfo, bluetoothInfo
        )

        val devices = deviceController.listAudioDevices()

        assertEquals(3, devices.size)
        devices.forEach {
            assertTrue(
                it.type == MediaDeviceType.AUDIO_BUILTIN_SPEAKER &&
                        it.label == "default speaker (Speaker)" ||
                        it.type == MediaDeviceType.AUDIO_HANDSET &&
                        it.label == "default receiver (Handset)" ||
                        it.type == MediaDeviceType.AUDIO_BLUETOOTH &&
                        it.label == "my bluetooth headphone (Bluetooth)"
            )
        }
    }

    @Test
    fun `listAudioDevices should return a list of connected devices when build version is low`() {
        setupForOldAPILevel()
        every { audioManager.isBluetoothScoOn } returns true
        every { audioManager.isBluetoothA2dpOn } returns true
        every { audioManager.isWiredHeadsetOn } returns false

        val devices = deviceController.listAudioDevices()
        assertEquals(3, devices.size)
        devices.forEach {
            assertTrue(
                it.type == MediaDeviceType.AUDIO_HANDSET &&
                        it.label == "Handset" ||
                        it.type == MediaDeviceType.AUDIO_BUILTIN_SPEAKER &&
                        it.label == "Speaker" ||
                        it.type == MediaDeviceType.AUDIO_BLUETOOTH &&
                        it.label == "Bluetooth"
            )
        }
    }

    @Test
    fun `listAudioDevices should not return both wired headset and receiver when build version is high`() {
        setupForNewAPILevel()
        every { audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS) } returns arrayOf(
            speakerInfo, earpieceInfo, wiredHeadsetInfo
        )

        val devices = deviceController.listAudioDevices()
        assertEquals(2, devices.size)
        devices.forEach {
            assertTrue(
                it.type == MediaDeviceType.AUDIO_WIRED_HEADSET ||
                        it.type == MediaDeviceType.AUDIO_BUILTIN_SPEAKER
            )
        }
    }

    @Test
    fun `listAudioDevices should return one handset when multiple handset available (only apply to higher API level)`() {
        setupForNewAPILevel()
        every { audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS) } returns arrayOf(
            earpieceInfo, telephonyInfo
        )

        val devices = deviceController.listAudioDevices()
        assertEquals(1, devices.size)
        assertEquals(MediaDeviceType.AUDIO_HANDSET, devices[0].type)
    }

    @Test
    fun `listAudioDevices should not return both wired headset and receiver when build version is low`() {
        setupForOldAPILevel()
        every { audioManager.isBluetoothScoOn } returns false
        every { audioManager.isBluetoothA2dpOn } returns false
        every { audioManager.isWiredHeadsetOn } returns true

        val devices = deviceController.listAudioDevices()
        assertEquals(2, devices.size)
        devices.forEach {
            assertTrue(
                it.type == MediaDeviceType.AUDIO_WIRED_HEADSET ||
                        it.type == MediaDeviceType.AUDIO_BUILTIN_SPEAKER
            )
        }
    }

    @Test
    fun `chooseAudioDevice should call AudioClientController setRoute`() {
        setupForOldAPILevel()
        every { audioClientController.setRoute(any()) } returns true
        mockkStatic(DefaultAudioClientController::class)
        DefaultAudioClientController.audioClientState = AudioClientState.STARTED
        deviceController.chooseAudioDevice(
            MediaDevice(
                "speaker",
                MediaDeviceType.AUDIO_BUILTIN_SPEAKER
            )
        )

        verify { audioClientController.setRoute(AudioClient.SPK_STREAM_ROUTE_SPEAKER) }
    }

    @Test
    fun `chooseAudioDevice should call AudioClientController setRoute with headset`() {
        setupForOldAPILevel()
        every { audioClientController.setRoute(any()) } returns true
        mockkStatic(DefaultAudioClientController::class)
        DefaultAudioClientController.audioClientState = AudioClientState.STARTED
        deviceController.chooseAudioDevice(
            MediaDevice(
                "usb headset",
                MediaDeviceType.AUDIO_USB_HEADSET
            )
        )

        verify { audioClientController.setRoute(AudioClient.SPK_STREAM_ROUTE_HEADSET) }
    }

    @Test
    fun `chooseAudioDevice should call audioManager startBluetoothSco when choosing bluetooth device`() {
        setupForOldAPILevel()
        every { audioClientController.setRoute(any()) } returns true
        mockkStatic(DefaultAudioClientController::class)
        DefaultAudioClientController.audioClientState = AudioClientState.STARTED
        deviceController.chooseAudioDevice(
            MediaDevice(
                "bluetooth",
                MediaDeviceType.AUDIO_BLUETOOTH
            )
        )

        verify { audioManager.startBluetoothSco() }
    }

    @Test
    fun `chooseAudioDevice should disable speaker and bluetooth when choosing other devices`() {
        setupForOldAPILevel()
        every { audioClientController.setRoute(any()) } returns true
        mockkStatic(DefaultAudioClientController::class)
        DefaultAudioClientController.audioClientState = AudioClientState.STARTED
        deviceController.chooseAudioDevice(
            MediaDevice(
                "wired headset",
                MediaDeviceType.AUDIO_WIRED_HEADSET
            )
        )

        verify { audioManager.setSpeakerphoneOn(false) }
        verify { audioManager.setBluetoothScoOn(false) }
    }

    @Test
    fun `chooseAudioDevice should default to handset when not bluetooth, wired headset, or speaker`() {
        setupForOldAPILevel()
        every { audioClientController.setRoute(any()) } returns true
        mockkStatic(DefaultAudioClientController::class)
        DefaultAudioClientController.audioClientState = AudioClientState.STARTED
        deviceController.chooseAudioDevice(
            MediaDevice(
                "handset",
                MediaDeviceType.AUDIO_HANDSET
            )
        )

        verify { audioClientController.setRoute(AudioClient.SPK_STREAM_ROUTE_RECEIVER) }
    }

    @Test
    fun `getActiveCamera should return null when no active camera`() {
        setupForOldAPILevel()
        every { videoClientController.getActiveCamera() } returns null

        assertNull(deviceController.getActiveCamera())
    }

    @Test
    fun `getActiveCamera should return a media device when active camera existing`() {
        setupForOldAPILevel()
        val videoDevice = mockkClass(MediaDevice::class)
        every { videoDevice.id } returns "0"
        every { videoDevice.type } returns MediaDeviceType.VIDEO_FRONT_CAMERA
        every { videoClientController.getActiveCamera() } returns videoDevice

        val mediaDevice = deviceController.getActiveCamera()!!

        assertEquals("0", mediaDevice.id)
        assertEquals(MediaDeviceType.VIDEO_FRONT_CAMERA, mediaDevice.type)
    }

    @Test
    fun `switchCamera should call videoClientController switchCamera`() {
        setupForOldAPILevel()

        deviceController.switchCamera()

        verify { videoClientController.switchCamera() }
    }

    @Test
    fun `notifyAudioDeviceChange should notify added observers`() {
        setupForOldAPILevel()
        deviceController.addDeviceChangeObserver(deviceChangeObserver)
        every { audioManager.isBluetoothScoOn } returns false
        every { audioManager.isBluetoothA2dpOn } returns false
        every { audioManager.isWiredHeadsetOn } returns false

        deviceController.notifyAudioDeviceChange()

        verify { deviceChangeObserver.onAudioDeviceChanged(any()) }
    }

    @Test
    fun `notifyAudioDeviceChange should NOT notify removed observer`() {
        setupForOldAPILevel()
        deviceController.addDeviceChangeObserver(deviceChangeObserver)
        deviceController.removeDeviceChangeObserver(deviceChangeObserver)

        deviceController.notifyAudioDeviceChange()

        verify(exactly = 0) { deviceChangeObserver.onAudioDeviceChanged(any()) }
    }

    @Test
    fun `getActiveAudioDevice should return null for old API Level`() {
        setupForOldAPILevel()
        val mediaDevice = deviceController.getActiveAudioDevice()
        assertNull(mediaDevice)
    }

    @Test
    fun `deviceController should call BluetoothDeviceController startListening for old API level`() {
        setupForOldAPILevel()
    }

    @Test
    fun `deviceController stopListening should call BluetoothDeviceController stopListening for old API level`() {
        setupForOldAPILevel()
    }
}


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

package com.amazonaws.services.chime.sdk.meetings.device

import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import android.hardware.camera2.CameraMetadata
import android.media.AudioDeviceInfo
import io.mockk.MockKAnnotations
import io.mockk.clearAllMocks
import io.mockk.every
import io.mockk.impl.annotations.MockK
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test

class MediaDeviceTest {
    @MockK
    private lateinit var mockCameraManager: CameraManager

    @MockK
    private lateinit var mockFrontCameraCharacteristics: CameraCharacteristics

    @MockK
    private lateinit var mockBackCameraCharacteristics: CameraCharacteristics

    @MockK
    private lateinit var mockOtherCameraCharacteristics: CameraCharacteristics

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

        clearAllMocks()
    }

    @Test
    fun `MediaDevice should be able to sort by order`() {
        val speaker = MediaDevice(
            "speaker",
            MediaDeviceType.AUDIO_BUILTIN_SPEAKER
        )
        val receiver = MediaDevice(
            "handset",
            MediaDeviceType.AUDIO_HANDSET
        )
        val wiredHeadset = MediaDevice(
            "wired headset",
            MediaDeviceType.AUDIO_WIRED_HEADSET
        )
        val bluetoothAudio = MediaDevice(
            "bluetooth",
            MediaDeviceType.AUDIO_BLUETOOTH
        )
        val sortedDevices =
            listOf(speaker, receiver, wiredHeadset, bluetoothAudio).sortedBy { it.order }
        assertTrue(
            sortedDevices[0] == bluetoothAudio &&
                    sortedDevices[1] == wiredHeadset &&
                    sortedDevices[2] == speaker &&
                    sortedDevices[3] == receiver
        )
    }

    @Test
    fun `MediaDeviceType fromAudioDeviceInfo should return known type when defined value`() {
        val type = MediaDeviceType.fromAudioDeviceInfo(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER)

        assertEquals(MediaDeviceType.AUDIO_BUILTIN_SPEAKER, type)
    }

    @Test
    fun `MediaDeviceType fromAudioDeviceInfo should return OTHER type when undefined value`() {
        val type = MediaDeviceType.fromAudioDeviceInfo(-99)

        assertEquals(MediaDeviceType.OTHER, type)
    }

    @Test
    fun `listVideoDevices converts from cameras provided from CameraManager`() {
        every { mockCameraManager.cameraIdList } returns arrayOf("0", "1", "2")
        every { mockCameraManager.getCameraCharacteristics("0") } returns mockFrontCameraCharacteristics
        every { mockCameraManager.getCameraCharacteristics("1") } returns mockBackCameraCharacteristics
        every { mockCameraManager.getCameraCharacteristics("2") } returns mockOtherCameraCharacteristics
        every { mockFrontCameraCharacteristics.get(CameraCharacteristics.LENS_FACING) } returns CameraMetadata.LENS_FACING_FRONT
        every { mockBackCameraCharacteristics.get(CameraCharacteristics.LENS_FACING) } returns CameraMetadata.LENS_FACING_BACK
        every { mockOtherCameraCharacteristics.get(CameraCharacteristics.LENS_FACING) } returns null

        val devices = MediaDevice.listVideoDevices(mockCameraManager)

        assertEquals(devices.size, 3)
        assertEquals(devices[0].id, "0")
        assertEquals(devices[1].id, "1")
        assertEquals(devices[2].id, "2")
        assertEquals(devices[0].type, MediaDeviceType.VIDEO_FRONT_CAMERA)
        assertEquals(devices[1].type, MediaDeviceType.VIDEO_BACK_CAMERA)
        assertEquals(devices[2].type, MediaDeviceType.OTHER)
    }
}


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

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

import com.amazonaws.services.chime.sdk.meetings.analytics.EventName
import com.amazonaws.services.chime.sdk.meetings.internal.ingestion.SDKEvent
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.verify
import java.util.Timer
import org.junit.Before
import org.junit.Test

class DefaultEventReporterTests {
    @MockK
    private lateinit var ingestionConfiguration: IngestionConfiguration

    @MockK
    private lateinit var eventClientConfiguration: EventClientConfiguration

    @MockK
    private lateinit var eventBuffer: EventBuffer

    @MockK
    private lateinit var timer: Timer

    @MockK
    private lateinit var logger: Logger

    private val flushIntervalMs = 200L
    private val metadataAttributeKey1 = "metadataAttributeKey1"
    private val metadataAttributeValue1 = 1
    private val metadataAttributeKey2 = "metadataAttributeKey2"
    private val metadataAttributeValue2 = "text"
    private val metadataAttributes = mapOf(
        metadataAttributeKey1 to metadataAttributeValue1,
        metadataAttributeKey2 to metadataAttributeValue2
    )

    @Before
    fun setup() {
        MockKAnnotations.init(this, relaxed = true)
        every { ingestionConfiguration.disabled } returns false
        every { ingestionConfiguration.flushIntervalMs } returns flushIntervalMs
        every { ingestionConfiguration.clientConfiguration } returns eventClientConfiguration
        every { eventClientConfiguration.metadataAttributes } returns metadataAttributes
    }

    @Test
    fun `constructor should not invoke timer schedule if disabled`() {
        every { ingestionConfiguration.disabled } returns true

        DefaultEventReporter(
            ingestionConfiguration,
            eventBuffer,
            logger
        )

        verify(exactly = 0) { timer.scheduleAtFixedRate(any(), any<Long>(), any()) }
        verify(exactly = 0) { eventBuffer.process() }
    }

    @Test
    fun `constructor should invoke timer schedule if enabled`() {
        DefaultEventReporter(
            ingestionConfiguration,
            eventBuffer,
            logger,
            timer
        )

        verify(exactly = 1) { timer.scheduleAtFixedRate(any(), flushIntervalMs, flushIntervalMs) }
    }

    @Test
    fun `report should add configuration common attributes`() {
        val defaultEventReporter = DefaultEventReporter(
            ingestionConfiguration,
            eventBuffer,
            logger
        )

        defaultEventReporter.report(SDKEvent(EventName.meetingFailed, mutableMapOf()))

        verify(exactly = 1) { eventBuffer.add(match {
            it.eventAttributes[metadataAttributeKey1] == metadataAttributeValue1 &&
                    it.eventAttributes[metadataAttributeKey2] == metadataAttributeValue2 }
        ) }
    }
}


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

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

import com.amazonaws.services.chime.sdk.meetings.internal.utils.HttpException
import com.amazonaws.services.chime.sdk.meetings.internal.utils.HttpResponse
import com.amazonaws.services.chime.sdk.meetings.internal.utils.HttpUtils
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger
import io.mockk.MockKAnnotations
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.mockkObject
import io.mockk.unmockkObject
import kotlin.Exception
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test

@ExperimentalCoroutinesApi
class DefaultEventSenderTests {
    @MockK
    private lateinit var ingestionConfiguration: IngestionConfiguration
    @MockK
    private lateinit var logger: Logger

    private lateinit var eventSender: EventSender

    @MockK
    private lateinit var ingestionRecord: IngestionRecord

    @Before
    fun setup() {
        MockKAnnotations.init(this, relaxed = true)
        mockkObject(HttpUtils)
        every { ingestionConfiguration.ingestionUrl } returns "https://example.com"
        every { ingestionConfiguration.retryCountLimit } returns 5
        eventSender = DefaultEventSender(ingestionConfiguration, logger)
    }

    @After
    fun cleanup() {
        unmockkObject(HttpUtils)
    }

    @Test
    fun `send should invoke HttpUtils send and return true if succeeded`() {
        coEvery { HttpUtils.post(any(), any(), any(), any(), any()) }.returns(HttpResponse("", null))

        runBlockingTest {
            val result = eventSender.sendRecord(ingestionRecord)

            coVerify { HttpUtils.post(any(), any(), any(), any(), any()) }
            Assert.assertEquals(true, result)
        }
    }

    @Test
    fun `send should return false if failed`() {
        coEvery { HttpUtils.post(any(), any(), any(), any(), any()) }.returns(
            HttpResponse(
                null,
                HttpException(errorCode = 420)
            )
        )

        runBlockingTest {
            val result = eventSender.sendRecord(ingestionRecord)

            coVerify { HttpUtils.post(any(), any(), any(), any(), any()) }
            Assert.assertEquals(false, result)
        }
    }

    @Test
    fun `send should return false if thrown exception`() {
        coEvery { HttpUtils.post(any(), any(), any(), any(), any()) }.throws(
            Exception()
        )

        runBlockingTest {
            val result = eventSender.sendRecord(ingestionRecord)

            coVerify { HttpUtils.post(any(), any(), any(), any(), any()) }
            Assert.assertEquals(false, result)
        }
    }
}


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

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

import com.amazonaws.services.chime.sdk.meetings.internal.ingestion.DirtyEventDao
import com.amazonaws.services.chime.sdk.meetings.internal.ingestion.DirtyMeetingEventItem
import com.amazonaws.services.chime.sdk.meetings.internal.ingestion.EventDao
import com.amazonaws.services.chime.sdk.meetings.internal.ingestion.IngestionEventConverter
import com.amazonaws.services.chime.sdk.meetings.internal.ingestion.MeetingEventItem
import com.amazonaws.services.chime.sdk.meetings.internal.ingestion.SDKEvent
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionStatusCode
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger
import io.mockk.MockKAnnotations
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.mockkObject
import io.mockk.mockkStatic
import io.mockk.verify
import java.util.Calendar
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Before
import org.junit.Test

@ExperimentalCoroutinesApi
class DefaultMeetingEventBufferTests {
    @MockK
    private lateinit var ingestionConfiguration: IngestionConfiguration

    @MockK
    private lateinit var eventDao: EventDao

    @MockK
    private lateinit var dirtyEventDao: DirtyEventDao

    @MockK
    private lateinit var eventSender: EventSender

    @MockK
    private lateinit var logger: Logger

    @MockK
    private lateinit var calendar: Calendar

    @MockK
    private lateinit var ingestionRecord: IngestionRecord

    @ExperimentalCoroutinesApi
    private val eventScope: CoroutineScope = TestCoroutineScope()

    private lateinit var defaultMeetingEventBuffer: EventBuffer

    private val currentTimestampMs = 100L
    private val futureTtl = 101L
    private val expiredTtl = 99L
    private val expiredDirtyEventId = "expired-event-id"
    private val malformedDirtyEventId = "malformed-event-id"
    private val validSDKEvent = SDKEvent(name = "event-name", eventAttributes = mapOf("timestampMs" to 1000))
    private val malformedSDKEvent = SDKEvent(name = "event-name", eventAttributes = mapOf("blah" to 1000))
    private val expiredDirtyEventItem = DirtyMeetingEventItem(id = expiredDirtyEventId, data = validSDKEvent, ttl = expiredTtl)
    private val malformedDirtyEventItem = DirtyMeetingEventItem(id = malformedDirtyEventId, data = malformedSDKEvent, ttl = futureTtl)
    private val event = SDKEvent(name = "non-urgent-event", eventAttributes = emptyMap())
    private val urgentEvent = SDKEvent(name = "meetingFailed", eventAttributes = mapOf("meetingStatus" to MeetingSessionStatusCode.AudioAuthenticationRejected))
    private val eventItem = MeetingEventItem(id = "event-id", data = event)
    private val flushSize = 5

    @Before
    fun setup() {
        MockKAnnotations.init(this, relaxed = true)
        mockkObject(IngestionEventConverter)
        every { ingestionConfiguration.flushSize } returns flushSize
        every { calendar.timeInMillis } returns currentTimestampMs
        mockkStatic(Calendar::class)
        every { Calendar.getInstance() } returns calendar
        every { IngestionEventConverter.fromDirtyMeetingEventItems(any(), any()) } returns ingestionRecord
        every { IngestionEventConverter.fromMeetingEventItems(any(), any()) } returns ingestionRecord

        coEvery { eventSender.sendRecord(any()) } returns true
    }

    private fun construct() {
        defaultMeetingEventBuffer = DefaultMeetingEventBuffer(
            ingestionConfiguration,
            eventDao,
            dirtyEventDao,
            eventSender,
            logger,
            eventScope
        )
    }

    @Test
    fun `should delete expired events when process the dirty events`() {
        every { dirtyEventDao.listDirtyMeetingEventItems(any()) } returns
                listOf(expiredDirtyEventItem)

        construct()

        runBlockingTest {
            verify(exactly = 1) { dirtyEventDao.deleteDirtyEventsByIds(listOf(expiredDirtyEventId)) }
            coVerify(exactly = 0) { eventSender.sendRecord(any()) }
        }
    }

    @Test
    fun `should delete malformed events when process the dirty events`() {
        every { dirtyEventDao.listDirtyMeetingEventItems(any()) } returns
                listOf(malformedDirtyEventItem)

        construct()

        runBlockingTest {
            verify(exactly = 1) { dirtyEventDao.deleteDirtyEventsByIds(listOf(malformedDirtyEventId)) }
            coVerify(exactly = 0) { eventSender.sendRecord(any()) }
        }
    }

    @Test
    fun `add should insert event to EventDao`() {
        construct()

        defaultMeetingEventBuffer.add(event)

        verify(exactly = 1) { eventDao.insertMeetingEvent(any()) }
    }

    @Test
    fun `add should send immediately if it is urgent event`() {
        construct()

        defaultMeetingEventBuffer.add(urgentEvent)

        runBlockingTest {
            verify(exactly = 1) { eventDao.insertMeetingEvent(any()) }
            coVerify(exactly = 1) { eventSender.sendRecord(any()) }
        }
    }

    @Test
    fun `add should not send immediately if it is not urgent event`() {
        construct()

        defaultMeetingEventBuffer.add(event)

        runBlockingTest {
            verify(exactly = 1) { eventDao.insertMeetingEvent(any()) }
            coVerify(exactly = 0) { eventSender.sendRecord(any()) }
        }
    }

    @Test
    fun `process should not send events when there is no stored events`() {
        construct()
        every { eventDao.listMeetingEventItems(any()) } returns emptyList()

        runBlockingTest {
            defaultMeetingEventBuffer.process()

            coVerify(exactly = 0) { eventSender.sendRecord(any()) }
        }
    }

    @Test
    fun `process should send events when there is stored events`() {
        construct()
        every { eventDao.listMeetingEventItems(any()) } returns listOf(eventItem)

        runBlockingTest {
            defaultMeetingEventBuffer.process()

            coVerify(exactly = 1) { eventSender.sendRecord(any()) }
        }
    }

    @Test
    fun `process should remove processed event items`() {
        construct()
        every { eventDao.listMeetingEventItems(any()) } returns listOf(eventItem)

        runBlockingTest {
            // send successfully
            coEvery { eventSender.sendRecord(any()) } returns true
            defaultMeetingEventBuffer.process()
            // send failed
            coEvery { eventSender.sendRecord(any()) } returns false
            defaultMeetingEventBuffer.process()

            verify(exactly = 2) { eventDao.deleteMeetingEventsByIds(listOf(eventItem.id)) }
        }
    }

    @Test
    fun `process should insert events as dirtyEvents when failed to send`() {
        construct()
        every { eventDao.listMeetingEventItems(any()) } returns listOf(eventItem)
        coEvery { eventSender.sendRecord(any()) } returns false

        runBlockingTest {
            defaultMeetingEventBuffer.process()

            coVerify(exactly = 1) { eventSender.sendRecord(any()) }
            verify(exactly = 1) { eventDao.deleteMeetingEventsByIds(any()) }
            verify(exactly = 1) { dirtyEventDao.insertDirtyMeetingEventItems(any()) }
        }
    }
}


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

import android.content.Context
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import org.junit.Assert.assertNotNull
import org.junit.Before
import org.junit.Test

class DefaultMeetingEventReporterFactoryTest {

    @MockK
    private lateinit var context: Context

    @MockK
    private lateinit var ingestionConfiguration: IngestionConfiguration

    @MockK
    private lateinit var logger: Logger

    private lateinit var factory: DefaultMeetingEventReporterFactory

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

        every { ingestionConfiguration.ingestionUrl } returns "http://test.com/"
        every { ingestionConfiguration.flushIntervalMs } returns 1L

        factory = DefaultMeetingEventReporterFactory(context, ingestionConfiguration, logger)
    }

    @Test
    fun `createEventReporter should return EventReporter`() {

        every { ingestionConfiguration.disabled } returns false

        val reporter = factory.createEventReporter()

        assertNotNull(reporter)
    }
}


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

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

import io.mockk.MockKAnnotations
import io.mockk.impl.annotations.MockK
import org.junit.Assert
import org.junit.Before
import org.junit.Test

class IngestionConfigurationTests {
    @MockK
    private lateinit var clientConfiguration: MeetingEventClientConfiguration
    private val ingestionUrl = "https://ingesitonurl.com"
    private val disabled = false
    private val flushSize = 70
    private val flushIntervalMs = 3000L
    private val retryCountLimit = 3

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

    @Test
    fun `constructor should return values passed in`() {
        val ingestionConfiguration = IngestionConfiguration(
            clientConfiguration,
            ingestionUrl,
            disabled,
            flushSize,
            flushIntervalMs,
            retryCountLimit
        )

        Assert.assertEquals(clientConfiguration, ingestionConfiguration.clientConfiguration)
        Assert.assertEquals(ingestionUrl, ingestionConfiguration.ingestionUrl)
        Assert.assertEquals(disabled, ingestionConfiguration.disabled)
        Assert.assertEquals(flushSize, ingestionConfiguration.flushSize)
        Assert.assertEquals(flushIntervalMs, ingestionConfiguration.flushIntervalMs)
        Assert.assertEquals(retryCountLimit, ingestionConfiguration.retryCountLimit)
    }

    @Test
    fun `constructor should restrict numerical values`() {
        val ingestionConfiguration = IngestionConfiguration(
            clientConfiguration,
            ingestionUrl,
            disabled,
            100000000,
            -5,
            100000000
        )

        Assert.assertEquals(FLUSH_SIZE_LIMIT_MAXIMUM, ingestionConfiguration.flushSize)
        Assert.assertEquals(FLUSH_INTERVAL_LIMIT_MINIMUM, ingestionConfiguration.flushIntervalMs)
        Assert.assertEquals(RETRY_COUNT_LIMIT_MAXIMUM, ingestionConfiguration.retryCountLimit)
    }
}


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

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

import com.amazonaws.services.chime.sdk.meetings.analytics.EventAttributeName
import com.google.gson.Gson
import org.junit.Assert
import org.junit.Test

class IngestionEventTests {
    private val metadata = mapOf(
        EventAttributeName.meetingId.name to "eeeeeeieei"
    )
    private val payloads = listOf(mapOf(
        "1" to "2",
        "hello" to 5
    ))

    @Test
    fun `IngestionEvent version should be encoded as v`() {
        val ingestionEvent = IngestionEvent(EventClientType.Meet.name, metadata, payloads)
        val gson = Gson()

        val ingestionEventJson = gson.toJson(ingestionEvent)

        Assert.assertTrue(ingestionEventJson.contains("\"v\":"))
    }
}


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

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

import org.junit.Assert
import org.junit.Test

class AttendeeStatusTest {
    @Test
    fun `from should return object with value from param`() {
        Assert.assertEquals(AttendeeStatus.Joined, AttendeeStatus.from(1))
        Assert.assertEquals(AttendeeStatus.Left, AttendeeStatus.from(2))
        Assert.assertEquals(AttendeeStatus.Dropped, AttendeeStatus.from(3))
    }

    @Test
    fun `from should return null with invalid value from param`() {
        Assert.assertNull(AttendeeStatus.from(4))
    }
}