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

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

import com.amazonaws.services.chime.sdk.meetings.realtime.datamessage.DataMessageObserver
import java.security.InvalidParameterException

/**
 * [RealtimeControllerFacade] controls aspects meetings concerning realtime UX
 * that for performance, privacy, or other reasons should be implemented using
 * the most direct path. Callbacks generated by this interface should be
 * consumed synchronously and without business logic dependent on the UI state
 * where possible.
 *
 * Events will be passed through [RealtimeObserver], which in turn provides consumers the
 * volume/mute/signal/attendee callbacks that can be used to render in the UI.
 * Data Messages will be passed through [DataMessageObserver].
 */
interface RealtimeControllerFacade {

    /**
     * Mute the audio input.
     *
     * @return Boolean whether the mute action succeeded
     */
    fun realtimeLocalMute(): Boolean

    /**
     * Unmutes the audio input.
     *
     * @return Boolean whether the unmute action succeeded
     */
    fun realtimeLocalUnmute(): Boolean

    /**
     * Subscribes to real time events with an observer
     *
     * @param observer: [RealtimeObserver] - Observer that handles real time events
     */
    fun addRealtimeObserver(observer: RealtimeObserver)

    /**
     * Unsubscribes from real time events by removing the specified observer
     *
     * @param observer: [RealtimeObserver] - Observer that handles real time events
     */
    fun removeRealtimeObserver(observer: RealtimeObserver)

    /**
     * Send message via data channel. Messages are only expected to be sent after audio video has started,
     * otherwise will be ignored.
     * Even though one can send data messages to any valid topic,
     * in order to receive the messages from the given topic, one need to subscribed to the topic
     * by calling [addRealtimeDataMessageObserver].
     * LifetimeMs specifies milliseconds for the given message can be stored in server side.
     * Up to 1024 messages may be stored for a maximum of 5 minutes.
     *
     * @param topic: String - topic the message is sent to
     * @param data: Any - data payload, it can be ByteArray, String or other serializable object,
     * which will be convert to ByteArray
     * @param lifetimeMs: Int - the milliseconds of lifetime that is available to late subscribers, default as 0
     * @throws [InvalidParameterException] when topic is not match regex `^[a-zA-Z0-9_-]{1,36}$`,
     * or data size is over 2kb, or lifetime ms is negative
     */
    fun realtimeSendDataMessage(topic: String, data: Any, lifetimeMs: Int = 0)

    /**
     *  Subscribes to receive message on a topic, there could be multiple
     *  observers per topic
     *
     * @param topic: String - topic of messages for subscription
     * @param observer: [DataMessageObserver] - observer that handles the data message events
     */
    fun addRealtimeDataMessageObserver(topic: String, observer: DataMessageObserver)

    /**
     * Unsubscribes from a message topic
     *
     * @param topic: String - topic of messages for unsubscription
     */
    fun removeRealtimeDataMessageObserverFromTopic(topic: String)

    /**
     * Enable or disable Amazon Voice Focus (ML-based noise suppression) on the audio input.
     *
     * Amazon Voice Focus is disabled by default when audioClient starts.
     *
     * @param enabled: Boolean - whether to enable Amazon Voice Focus or not
     * @return Boolean whether the enable/disable action succeeded
     */
    fun realtimeSetVoiceFocusEnabled(enabled: Boolean): Boolean

    /**
     * Checks if Amazon Voice Focus is enabled.
     *
     * @return Boolean whether Amazon Voice Focus is enabled or not
     */
    fun realtimeIsVoiceFocusEnabled(): Boolean

    /**
     * Subscribes to transcript event with an observer
     *
     * @param observer: [TranscriptEventObserver] - Observer that handles transcript event
     */
    fun addRealtimeTranscriptEventObserver(observer: TranscriptEventObserver) {}

    /**
     * Unsubscribes from transcript event by removing the specified observer
     *
     * @param observer: [TranscriptEventObserver] - Observer that handles transcript event
     */
    fun removeRealtimeTranscriptEventObserver(observer: TranscriptEventObserver) {}
}


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

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

import com.amazonaws.services.chime.sdk.meetings.audiovideo.AttendeeInfo
import com.amazonaws.services.chime.sdk.meetings.audiovideo.SignalStrength
import com.amazonaws.services.chime.sdk.meetings.audiovideo.SignalUpdate
import com.amazonaws.services.chime.sdk.meetings.audiovideo.VolumeLevel
import com.amazonaws.services.chime.sdk.meetings.audiovideo.VolumeUpdate

/**
 * [RealtimeObserver] lets one listen to real time events such a volume, signal strength, or
 * attendee changes.
 *
 * Note: all callbacks will be called on main thread.
 */
interface RealtimeObserver {
    /**
     * Handles volume changes for attendees whose [VolumeLevel] has changed.
     *
     * Note: this callback will be called on main thread.
     *
     * @param volumeUpdates: Array<[VolumeUpdate]> - Attendees with updated volume levels.
     */
    fun onVolumeChanged(volumeUpdates: Array<VolumeUpdate>)

    /**
     * Handles signal strength changes for attendees whose [SignalStrength] has changed.
     *
     * Note: this callback will be called on main thread.
     *
     * @param signalUpdates: Array<[SignalUpdate]> - Attendees with updated signal strengths.
     */
    fun onSignalStrengthChanged(signalUpdates: Array<SignalUpdate>)

    /**
     * Handles attendee(s) being added.
     *
     * Note: this callback will be called on main thread.
     *
     * @param attendeeInfo: Array<[AttendeeInfo]> - Attendees being added.
     */
    fun onAttendeesJoined(attendeeInfo: Array<AttendeeInfo>)

    /**
     * Handles attendee(s) being removed.
     *
     * Note: this callback will be called on main thread.
     *
     * @param attendeeInfo: Array<[AttendeeInfo]> - Attendees being removed.
     */
    fun onAttendeesLeft(attendeeInfo: Array<AttendeeInfo>)

    /**
     * Handles attendee(s) being dropped due to network.
     *
     * Note: this callback will be called on main thread.
     *
     * @param attendeeInfo: Array<[AttendeeInfo]> - Attendees being dropped.
     */
    fun onAttendeesDropped(attendeeInfo: Array<AttendeeInfo>)

    /**
     * Handles attendee(s) whose [VolumeLevel] has changed to muted.
     *
     * Note: this callback will be called on main thread.
     *
     * @param attendeeInfo: Array<[AttendeeInfo]> - Attendees who are newly muted.
     */
    fun onAttendeesMuted(attendeeInfo: Array<AttendeeInfo>)

    /**
     * Handles attendee(s) whose [VolumeLevel] has changed from muted.
     *
     * Note: this callback will be called on main thread.
     *
     * @param attendeeInfo: Array<[AttendeeInfo]> - Attendees who are newly unmuted.
     */
    fun onAttendeesUnmuted(attendeeInfo: Array<AttendeeInfo>)
}


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

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

import com.amazonaws.services.chime.sdk.meetings.audiovideo.TranscriptEvent

/**
 * [TranscriptEventObserver] lets one listen to [TranscriptEvent] events of current meeting
 *
 * Note: all callbacks will be called on main thread.
 */
interface TranscriptEventObserver {
    /**
     * Handles receive of transcript event - [TranscriptEvent].
     *
     * Note: this callback will be called on main thread.
     *
     * @param transcriptEvent: Array<[TranscriptEvent]> - Transcript events.
     */
    fun onTranscriptEventReceived(transcriptEvent: TranscriptEvent)
}


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

package com.amazonaws.services.chime.sdk.meetings.realtime.datamessage

import com.google.gson.Gson

/**
 * Data message received from server.
 *
 * @property timestampMs: Long - Monotonically increasing server ingest time
 * @property topic: String - Topic this message was sent on
 * @property data: ByteArray - Data payload
 * @property senderAttendeeId - Sender attendee
 * @property senderExternalUserId - Sender attendee external user Id
 * @property throttled - true if server throttled or rejected message,
 * false if server has posted the message to its recipients or it's not a sender receipt
 */
data class DataMessage(
    val timestampMs: Long,
    val topic: String,
    val data: ByteArray,
    val senderAttendeeId: String,
    val senderExternalUserId: String,
    val throttled: Boolean
) {

    /**
     * Helper method to convert ByteArray data to String
     *
     * @return string data
     */
    fun text(): String {
        return String(data)
    }

    /**
     * Helper method to convert ByteArray data to object of given type
     *
     * @param clazz: Object type
     * @return deserialized object
     */
    fun <T> fromJson(clazz: Class<T>): T {
        return Gson().fromJson(text(), clazz)
    }
}


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

package com.amazonaws.services.chime.sdk.meetings.realtime.datamessage

/**
 * [DataMessageObserver] lets one listen to data message receiving event.
 * One can subscribe this observer to multiple data message topic in order
 * to receive and process the message that sent to the topics.
 *
 *  Note: callback will be called on main thread.
 */
interface DataMessageObserver {
    /**
     * Handles data message being received.
     *
     * Note: Data messages sent from local participant will not trigger this callback unless it's throttled.
     *
     * @param dataMessage: [DataMessage] - data message being received
     */
    fun onDataMessageReceived(dataMessage: DataMessage)
}


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

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

// https://docs.aws.amazon.com/chime/latest/APIReference/API_CreateAttendee.html
data class CreateAttendeeResponse(val Attendee: Attendee)

data class Attendee(
    val AttendeeId: String,
    val ExternalUserId: String,
    val JoinToken: String
)


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

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

// https://docs.aws.amazon.com/chime/latest/APIReference/API_CreateMeeting.html
data class CreateMeetingResponse(val Meeting: Meeting)

data class Meeting(
    val ExternalMeetingId: String?,
    val MediaPlacement: MediaPlacement,
    val MediaRegion: String,
    val MeetingId: String
)

data class MediaPlacement @JvmOverloads constructor(
    val AudioFallbackUrl: String,
    val AudioHostUrl: String,
    val SignalingUrl: String,
    val TurnControlUrl: String,
    val EventIngestionUrl: String? = null
)


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

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

import android.content.Context
import com.amazonaws.services.chime.sdk.meetings.analytics.DefaultEventAnalyticsController
import com.amazonaws.services.chime.sdk.meetings.analytics.DefaultMeetingStatsCollector
import com.amazonaws.services.chime.sdk.meetings.analytics.EventAnalyticsController
import com.amazonaws.services.chime.sdk.meetings.audiovideo.AudioVideoFacade
import com.amazonaws.services.chime.sdk.meetings.audiovideo.DefaultAudioVideoController
import com.amazonaws.services.chime.sdk.meetings.audiovideo.DefaultAudioVideoFacade
import com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.activespeakerdetector.DefaultActiveSpeakerDetector
import com.amazonaws.services.chime.sdk.meetings.audiovideo.contentshare.DefaultContentShareController
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.DefaultVideoTileController
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.DefaultVideoTileFactory
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.gl.DefaultEglCoreFactory
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.gl.EglCoreFactory
import com.amazonaws.services.chime.sdk.meetings.device.DefaultDeviceController
import com.amazonaws.services.chime.sdk.meetings.ingestion.DefaultMeetingEventReporterFactory
import com.amazonaws.services.chime.sdk.meetings.ingestion.EventReporterFactory
import com.amazonaws.services.chime.sdk.meetings.ingestion.IngestionConfiguration
import com.amazonaws.services.chime.sdk.meetings.ingestion.MeetingEventClientConfiguration
import com.amazonaws.services.chime.sdk.meetings.internal.audio.AudioClientFactory
import com.amazonaws.services.chime.sdk.meetings.internal.audio.DefaultAudioClientController
import com.amazonaws.services.chime.sdk.meetings.internal.audio.DefaultAudioClientObserver
import com.amazonaws.services.chime.sdk.meetings.internal.contentshare.DefaultContentShareVideoClientController
import com.amazonaws.services.chime.sdk.meetings.internal.contentshare.DefaultContentShareVideoClientObserver
import com.amazonaws.services.chime.sdk.meetings.internal.metric.DefaultClientMetricsCollector
import com.amazonaws.services.chime.sdk.meetings.internal.video.DefaultVideoClientController
import com.amazonaws.services.chime.sdk.meetings.internal.video.DefaultVideoClientFactory
import com.amazonaws.services.chime.sdk.meetings.internal.video.DefaultVideoClientObserver
import com.amazonaws.services.chime.sdk.meetings.internal.video.DefaultVideoClientStateController
import com.amazonaws.services.chime.sdk.meetings.internal.video.TURNRequestParams
import com.amazonaws.services.chime.sdk.meetings.realtime.DefaultRealtimeController
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger

class DefaultMeetingSession @JvmOverloads constructor(
    override val configuration: MeetingSessionConfiguration,
    override val logger: Logger,
    context: Context,
    eglCoreFactory: EglCoreFactory = DefaultEglCoreFactory(),
    eventReporterFactory: EventReporterFactory = DefaultMeetingEventReporterFactory(
        context,
        IngestionConfiguration(
            MeetingEventClientConfiguration(configuration.credentials.joinToken,
                                            configuration.meetingId,
                                            configuration.credentials.attendeeId),
            configuration.urls.ingestionURL ?: "",
            configuration.urls.ingestionURL.isNullOrEmpty()
        ),
        logger
    )
) : MeetingSession {

    override val audioVideo: AudioVideoFacade

    override val eventAnalyticsController: EventAnalyticsController

    init {
        val meetingStatsCollector = DefaultMeetingStatsCollector(logger)

        val eventReporter = eventReporterFactory.createEventReporter()

        eventAnalyticsController = DefaultEventAnalyticsController(
            logger,
            configuration,
            meetingStatsCollector,
            eventReporter
        )

        val metricsCollector = DefaultClientMetricsCollector()
        val audioClientObserver =
            DefaultAudioClientObserver(
                logger,
                metricsCollector,
                configuration,
                meetingStatsCollector,
                eventAnalyticsController
            )

        val audioClient =
            AudioClientFactory.getAudioClient(context, audioClientObserver)

        audioClientObserver.audioClient = audioClient

        val audioClientController =
            DefaultAudioClientController(
                context,
                logger,
                audioClientObserver,
                audioClient,
                meetingStatsCollector,
                eventAnalyticsController
            )

        val turnRequestParams =
            TURNRequestParams(
                configuration.meetingId,
                configuration.urls.signalingURL,
                configuration.urls.turnControlURL,
                configuration.credentials.joinToken
            )

        val videoClientStateController =
            DefaultVideoClientStateController(
                logger
            )

        val videoClientObserver =
            DefaultVideoClientObserver(
                context,
                logger,
                turnRequestParams,
                metricsCollector,
                videoClientStateController,
                configuration.urls.urlRewriter
            )

        val videoClientFactory = DefaultVideoClientFactory()

        val videoClientController =
            DefaultVideoClientController(
                context,
                logger,
                videoClientStateController,
                videoClientObserver,
                configuration,
                videoClientFactory,
                eglCoreFactory,
                eventAnalyticsController
            )

        val videoTileFactory = DefaultVideoTileFactory(logger)

        val videoTileController =
            DefaultVideoTileController(
                logger,
                videoClientController,
                videoTileFactory,
                eglCoreFactory,
                meetingStatsCollector
            )
        videoClientObserver.subscribeToVideoTileChange(videoTileController)

        val deviceController =
            DefaultDeviceController(
                context,
                audioClientController,
                videoClientController,
                eventAnalyticsController
            )

        val realtimeController =
            DefaultRealtimeController(
                audioClientController,
                audioClientObserver,
                videoClientController,
                videoClientObserver
            )

        val activeSpeakerDetector = DefaultActiveSpeakerDetector(audioClientObserver)

        val audioVideoController =
            DefaultAudioVideoController(
                audioClientController,
                audioClientObserver,
                metricsCollector,
                configuration,
                videoClientController,
                videoClientObserver
            )

        val contentShareConfiguration =
            configuration.createContentShareMeetingSessionConfiguration()

        val contentShareTurnRequestParams =
            TURNRequestParams(
                contentShareConfiguration.meetingId,
                contentShareConfiguration.urls.signalingURL,
                contentShareConfiguration.urls.turnControlURL,
                contentShareConfiguration.credentials.joinToken
            )

        val contentShareObserver =
            DefaultContentShareVideoClientObserver(
                context,
                logger,
                contentShareTurnRequestParams,
                metricsCollector,
                contentShareConfiguration.urls.urlRewriter
            )

        val contentShareVideoClientController =
            DefaultContentShareVideoClientController(
                context,
                logger,
                contentShareObserver,
                contentShareConfiguration,
                videoClientFactory,
                eglCoreFactory
            )

        val contentShareController =
            DefaultContentShareController(
                logger,
                contentShareVideoClientController
            )

        audioVideo = DefaultAudioVideoFacade(
            context,
            audioVideoController,
            realtimeController,
            deviceController,
            videoTileController,
            activeSpeakerDetector,
            contentShareController,
            eventAnalyticsController
        )
    }
}


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

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

import com.amazonaws.services.chime.sdk.meetings.analytics.EventAnalyticsController
import com.amazonaws.services.chime.sdk.meetings.audiovideo.AudioVideoFacade
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger

/**
 * [MeetingSession] contains everything needed for the attendee to authenticate,
 * reach the meeting service, start audio, and start video
 */
interface MeetingSession {
    val configuration: MeetingSessionConfiguration
    val logger: Logger
    val audioVideo: AudioVideoFacade
    val eventAnalyticsController: EventAnalyticsController
}


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

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

import com.amazonaws.services.chime.sdk.meetings.utils.DefaultModality
import com.amazonaws.services.chime.sdk.meetings.utils.ModalityType

/**
 * [MeetingSessionConfiguration] includes information needed to start the meeting session such as
 * attendee credentials and URLs for audio and video
 *
 * Constructs a MeetingSessionConfiguration with a chime:[CreateMeetingResponse] and
 * chime:[CreateAttendeeResponse] response and optional custom [URLRewriter] that will
 * rewrite urls given to new urls.
 */
data class MeetingSessionConfiguration(
    val meetingId: String,
    val externalMeetingId: String?,
    val credentials: MeetingSessionCredentials,
    val urls: MeetingSessionURLs
) {
    @JvmOverloads
    constructor(
        createMeetingResponse: CreateMeetingResponse,
        createAttendeeResponse: CreateAttendeeResponse,
        urlRewriter: URLRewriter = ::defaultUrlRewriter
    ) : this(
        createMeetingResponse.Meeting.MeetingId,
        createMeetingResponse.Meeting.ExternalMeetingId,
        MeetingSessionCredentials(
            createAttendeeResponse.Attendee.AttendeeId,
            createAttendeeResponse.Attendee.ExternalUserId,
            createAttendeeResponse.Attendee.JoinToken
        ),
        MeetingSessionURLs(
            createMeetingResponse.Meeting.MediaPlacement.AudioFallbackUrl,
            createMeetingResponse.Meeting.MediaPlacement.AudioHostUrl,
            createMeetingResponse.Meeting.MediaPlacement.TurnControlUrl,
            createMeetingResponse.Meeting.MediaPlacement.SignalingUrl,
            urlRewriter,
            createMeetingResponse.Meeting.MediaPlacement.EventIngestionUrl
        )
    )

    constructor(
        meetingId: String,
        credentials: MeetingSessionCredentials,
        urls: MeetingSessionURLs
    ) : this(meetingId, null, credentials, urls)

    fun createContentShareMeetingSessionConfiguration(): MeetingSessionConfiguration {
        val contentModality: String =
            DefaultModality.MODALITY_SEPARATOR + ModalityType.Content.value
        return MeetingSessionConfiguration(
            meetingId,
            externalMeetingId,
            MeetingSessionCredentials(
                credentials.attendeeId + contentModality,
                credentials.externalUserId,
                credentials.joinToken + contentModality
            ),
            urls
        )
    }
}


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

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

/**
 * [MeetingSessionCredentials] includes the credentials used to authenticate
 * the attendee on the meeting
 */
data class MeetingSessionCredentials(
    val attendeeId: String,
    val externalUserId: String,
    val joinToken: String
)


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

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

/**
 * [MeetingSessionStatus] indicates a status received regarding the session.
 *
 * @param statusCode: [MeetingSessionStatusCode] - Additional details for the status
 */
data class MeetingSessionStatus(val statusCode: MeetingSessionStatusCode?) {
    constructor(statusCodeValue: Int) : this(MeetingSessionStatusCode.from(statusCodeValue))
}


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

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

/**
 * [MeetingSessionStatusCode] provides additional details for the [MeetingSessionStatus]
 * received for a session.
 */
enum class MeetingSessionStatusCode(val value: Int) {
    /**
     * Everything is OK so far.
     */
    OK(0),

    /**
     * The attendee left the meeting normally.
     */
    Left(1),

    /**
     * The attendee joined from another device.
     */
    AudioJoinedFromAnotherDevice(2),

    /**
     * The attendee should explicitly switch itself from joined with audio to
     * checked-in.
     */
    AudioDisconnectAudio(3),

    /**
     * Authentication was rejected. The client is not allowed on this call.
     */
    AudioAuthenticationRejected(4),

    /**
     * The client can not join because the call is at capacity.
     */
    AudioCallAtCapacity(5),

    /**
     * The call was ended.
     */
    AudioCallEnded(6),

    /**
     * There was an internal server error with the audio leg.
     */
    AudioInternalServerError(7),

    /**
     * Could not connect the audio leg due to the service being unavailable.
     */
    AudioServiceUnavailable(8),

    /**
     * The audio leg failed.
     */
    AudioDisconnected(9),

    /**
     * Due to connection health, a reconnect has been triggered.
     */
    ConnectionHealthReconnect(10),

    /**
     * The network has become poor and is no longer good enough for VoIP.
     */
    NetworkBecamePoor(11),

    /**
     * Video Client Failed.
     */
    VideoServiceFailed(12),

    /**
     * The video client has tried to send video but was unable to do so due to capacity reached.
     * However, the video client can still receive remote video streams.
     */
    VideoAtCapacityViewOnly(13),

    /**
     * Designated output device is not responding and timed out.
     */
    AudioOutputDeviceNotResponding(14),

    /**
     * Designated input device is not responding and timed out.
     */
    AudioInputDeviceNotResponding(15);

    companion object {
        fun from(intValue: Int): MeetingSessionStatusCode? = values().find { it.value == intValue }
    }
}


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

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

/**
 * [MeetingSessionURLs] contains the URLs that will be used to reach the
 * meeting service.
 */
data class MeetingSessionURLs @JvmOverloads constructor(
    private val _audioFallbackURL: String,
    private val _audioHostURL: String,
    private val _turnControlURL: String,
    private val _signalingURL: String,
    val urlRewriter: URLRewriter,
    private val _ingestionURL: String? = null
) {
    val audioHostURL = urlRewriter(_audioHostURL)
    val audioFallbackURL = urlRewriter(_audioFallbackURL)
    val turnControlURL = urlRewriter(_turnControlURL)
    val signalingURL = urlRewriter(_signalingURL)
    val ingestionURL = _ingestionURL?.let { urlRewriter(it) }
}


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

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

/**
 * [URLRewriter] Function to transform URLs.
 * Use this to rewrite URLs to traverse proxies.
 * @param url: Url string
 * @return A new url string manipulated
 */
typealias URLRewriter = (url: String) -> String

/**
 * The default implementation returns the original URL unchanged.
 */
fun defaultUrlRewriter(url: String) = url


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

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

/**
 * [DefaultModality] is a backwards compatible extension of the
 * attendee id (UUID string) and session token schemas (base 64 string).
 * It appends #<modality> to either strings, which indicates the modality
 * of the participant.
 *
 * For example,
 * attendeeId: "abcdefg"
 * contentAttendeeId: "abcdefg#content"
 * base(contentAttendeeId): "abcdefg"
 * modality(contentAttendeeId): "content"
 * hasModality(contentAttendeeId): true
 */
class DefaultModality(private val id: String) {

    companion object {
        const val MODALITY_SEPARATOR = "#"
    }

    /**
     * The Id
     */
    fun id(): String {
        return id
    }

    /**
     * The base of the Id
     */
    fun base(): String {
        if (id.isEmpty()) {
            return ""
        }
        return id.split(MODALITY_SEPARATOR)[0]
    }

    /**
     * The modality of the Id
     */
    fun modality(): ModalityType? {
        if (id.isEmpty()) {
            return null
        }
        val components = id.split(MODALITY_SEPARATOR)
        return if (components.size == 2) ModalityType.fromValue(components[1]) else null
    }

    /**
     * Check whether the current Id contains the input modality
     */
    fun hasModality(modality: ModalityType): Boolean {
        return modality() == modality
    }
}


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

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

import android.opengl.GLES20
import com.amazonaws.services.chime.sdk.meetings.internal.video.gl.GlUtil

/**
 * [GlTextureFrameBufferHelper] is a helper class for handling OpenGL framebuffer with only color
 * attachment and no depth or stencil buffer.
 */
class GlTextureFrameBufferHelper(
    private val pixelFormat: Int = 0
) {
    // Gets the OpenGL frame buffer id. This value is only valid after setSize() has been called.
    var frameBufferId = 0

    // Gets the OpenGL texture id. This value is only valid after setSize() has been called.
    var textureId = 0

    // Current size, this object can be reallocated.
    var width: Int = 0
    var height: Int = 0

    init {
        when (pixelFormat) {
            GLES20.GL_LUMINANCE, GLES20.GL_RGB, GLES20.GL_RGBA -> {
            }
            else -> throw IllegalArgumentException("Invalid pixel format: $pixelFormat")
        }
    }

    // (Re)allocate texture. Will do nothing if the requested size equals the current size.
    fun setSize(width: Int, height: Int) {
        require(!(width <= 0 || height <= 0)) { "Invalid size: " + width + "x" + height }
        if (width == this.width && height == this.height) {
            return
        }
        this.width = width
        this.height = height
        // Lazy allocation the first time setSize() is called.
        if (textureId == 0) {
            textureId = GlUtil.generateTexture(GLES20.GL_TEXTURE_2D)
        }
        if (frameBufferId == 0) {
            val frameBuffers = IntArray(1)
            GLES20.glGenFramebuffers(1, frameBuffers, 0)
            frameBufferId = frameBuffers[0]
        }

        GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId)
        GLES20.glTexImage2D(
            GLES20.GL_TEXTURE_2D, 0, pixelFormat, width, height, 0, pixelFormat,
            GLES20.GL_UNSIGNED_BYTE, null
        )
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
        GlUtil.checkGlError("Failed to bind texture")

        // Attach the texture to the framebuffer as color attachment.
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBufferId)
        GLES20.glFramebufferTexture2D(
            GLES20.GL_FRAMEBUFFER,
            GLES20.GL_COLOR_ATTACHMENT0,
            GLES20.GL_TEXTURE_2D,
            textureId,
            0
        )
        GlUtil.checkGlError("Failed to bind and create framebuffer")

        // Check that the framebuffer is in a good state.
        val status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER)
        check(status == GLES20.GL_FRAMEBUFFER_COMPLETE) { "Framebuffer not complete, status: $status" }
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0)
    }

    fun release() {
        GLES20.glDeleteTextures(1, intArrayOf(textureId), 0)
        textureId = 0
        GLES20.glDeleteFramebuffers(1, intArrayOf(frameBufferId), 0)
        frameBufferId = 0
        width = 0
        height = 0
    }
}


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

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

enum class ModalityType(val value: String) {
    Content("content");

    companion object {
        fun fromValue(value: String): ModalityType? = values().find { it.value == value }
    }
}


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

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

import java.util.concurrent.atomic.AtomicInteger

class RefCountDelegate(private val releaseCallback: Runnable) {
    private val refCount: AtomicInteger = AtomicInteger(1)

    fun retain() {
        val updatedCount: Int = refCount.incrementAndGet()
        check(updatedCount >= 2) { "retain() called on an object with refcount < 1" }
    }

    fun release() {
        val updatedCount: Int = refCount.decrementAndGet()
        check(updatedCount >= 0) { "release() called on an object with refcount < 1" }
        if (updatedCount == 0) {
            releaseCallback.run()
        }
    }
}


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

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

import com.amazonaws.services.chime.sdk.BuildConfig

/**
 * [Versioning] provides API to retrieve SDK version
 */
class Versioning {
    companion object {
        /**
         * Return current version of Amazon Chime SDK for Android.
         */
        fun sdkVersion(): String {
            return BuildConfig.VERSION_NAME
        }
    }
}


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

package com.amazonaws.services.chime.sdk.meetings.utils.logger

import android.util.Log

/**
 * [ConsoleLogger] writes logs with console
 *
 * ```
 * // Working with the ConsoleLogger
 * val logger = new ConsoleLogger("demo"); // defaults to WARN
 * logger.info("info");
 * logger.debug("debug");
 * logger.warn("warn");
 * logger.error("error");
 *
 * // Setting logging levels
 * val logger = new ConsoleLogger("demo", LogLevel.WARN);
 * logger.debug("debug"); // does not print
 * logger.setLogLevel(LogLevel.DEBUG)
 * logger.debug("debug"); // print
 * ```
 */
class ConsoleLogger(private var level: LogLevel = LogLevel.INFO) : Logger {

    override fun verbose(tag: String, msg: String) {
        this.log(LogLevel.VERBOSE, tag, msg)
    }

    override fun debug(tag: String, msg: String) {
        this.log(LogLevel.DEBUG, tag, msg)
    }

    override fun info(tag: String, msg: String) {
        this.log(LogLevel.INFO, tag, msg)
    }

    override fun warn(tag: String, msg: String) {
        this.log(LogLevel.WARN, tag, msg)
    }

    override fun error(tag: String, msg: String) {
        this.log(LogLevel.ERROR, tag, msg)
    }

    override fun setLogLevel(level: LogLevel) {
        this.level = level
    }

    override fun getLogLevel(): LogLevel {
        return this.level
    }

    private fun log(type: LogLevel, tag: String, msg: String) {
        if (type.priority < this.level.priority) return

        when (type) {
            LogLevel.VERBOSE -> Log.v(tag, msg)
            LogLevel.DEBUG -> Log.d(tag, msg)
            LogLevel.INFO -> Log.i(tag, msg)
            LogLevel.WARN -> Log.w(tag, msg)
            LogLevel.ERROR -> Log.e(tag, msg)
        }
    }
}


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

package com.amazonaws.services.chime.sdk.meetings.utils.logger

/**
 * [Logger] defines how to write logs for different logging level.
 */
interface Logger {

    /**
     * Emits an verbose message if the log level is equal to or lower than verbose level.
     */
    fun verbose(tag: String, msg: String)

    /**
     * Emits an debug message if the log level is equal to or lower than debug level.
     */
    fun debug(tag: String, msg: String)

    /**
     * Emits an info message if the log level is equal to or lower than info level.
     */
    fun info(tag: String, msg: String)

    /**
     * Emits a warning message if the log level is equal to or lower than warn level.
     */
    fun warn(tag: String, msg: String)

    /**
     * Emits an error message if the log level is equal to or lower than error level.
     */
    fun error(tag: String, msg: String)

    /**
     * Sets the log level.
     */
    fun setLogLevel(level: LogLevel)

    /**
     * Gets the current log level.
     */
    fun getLogLevel(): LogLevel
}


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

package com.amazonaws.services.chime.sdk.meetings.utils.logger

enum class LogLevel(val priority: Int) {
    VERBOSE(0),
    DEBUG(1),
    INFO(2),
    WARN(3),
    ERROR(4),
    OFF(5)
}


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

class TestConstant {
    companion object {
        const val globalScopeTimeoutMs = 5000L
    }
}


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

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

import com.amazonaws.services.chime.sdk.meetings.ingestion.EventReporter
import com.amazonaws.services.chime.sdk.meetings.internal.utils.DeviceUtils
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionConfiguration
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionCredentials
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.mockkObject
import io.mockk.mockkStatic
import io.mockk.spyk
import io.mockk.unmockkObject
import io.mockk.verify
import java.util.Calendar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runBlockingTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test

@ExperimentalCoroutinesApi
class DefaultEventAnalyticsControllerTest {
    private val testDispatcher = TestCoroutineDispatcher()

    private lateinit var testEventAnalyticsController: EventAnalyticsController

    @MockK
    private lateinit var mockLogger: Logger

    @MockK
    private lateinit var mockMeetingSessionConfiguration: MeetingSessionConfiguration

    @MockK
    private lateinit var eventReporter: EventReporter

    @MockK
    private lateinit var mockMeetingStatsCollector: MeetingStatsCollector

    private val meetingDurationAttribute: EventAttributes =
        mutableMapOf(EventAttributeName.meetingDurationMs to 1000L)

    @Before
    fun setup() {
        mockkStatic(Calendar::class)

        MockKAnnotations.init(this, relaxUnitFun = true)
        testEventAnalyticsController =
            DefaultEventAnalyticsController(
                mockLogger,
                mockMeetingSessionConfiguration,
                mockMeetingStatsCollector,
                eventReporter
            )
        every { mockMeetingStatsCollector.getMeetingStatsEventAttributes() } returns meetingDurationAttribute
        every { mockMeetingStatsCollector.getMeetingHistory() } returns emptyList()
        every { mockMeetingSessionConfiguration.credentials } returns MeetingSessionCredentials(
            "attendeeId",
            "externalUserId",
            "joinToken"
        )
        every { mockMeetingSessionConfiguration.externalMeetingId } returns "externalMeetingId"
        every { mockMeetingSessionConfiguration.meetingId } returns "meetingId"
        mockkObject(DeviceUtils)
        every { DeviceUtils.deviceManufacturer } returns "deviceManufacturer"
        every { DeviceUtils.deviceModel } returns "deviceModel"
        every { DeviceUtils.deviceName } returns "deviceName"
        every { DeviceUtils.sdkVersion } returns "sdkVersion"
        every { DeviceUtils.sdkName } returns "sdkName"
        every { DeviceUtils.osName } returns "osName"
        every { DeviceUtils.osVersion } returns "osVersion"

        Dispatchers.setMain(testDispatcher)
    }

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

    @Test
    fun `onEventReceived should be invoked when publishEvent is called`() {
        val observer = spyk<EventAnalyticsObserver>()
        testEventAnalyticsController.addEventAnalyticsObserver(observer)
        runBlockingTest {
            testEventAnalyticsController.publishEvent(EventName.meetingFailed)
        }

        verify(exactly = 1) { observer.onEventReceived(withArg {
            assertEquals(EventName.meetingFailed, it)
        }, any()) }
    }

    @Test
    fun `onEventReceived should not be invoked when publishEvent is called when no observer is available `() {
        val observer = spyk<EventAnalyticsObserver>()
        testEventAnalyticsController.addEventAnalyticsObserver(observer)
        testEventAnalyticsController.removeEventAnalyticsObserver(observer)

        runBlockingTest {
            testEventAnalyticsController.publishEvent(EventName.meetingFailed)
        }

        verify(exactly = 0) { observer.onEventReceived(any(), any()) }
    }

    @Test
    fun `publishEvent should invoked EventReporter's report`() {
        testEventAnalyticsController.publishEvent(EventName.meetingFailed)

        verify(exactly = 1) { eventReporter.report(any()) }
    }

    @Test
    fun `pushHistory should invoked EventReporter's report`() {
        testEventAnalyticsController.pushHistory(MeetingHistoryEventName.meetingReconnected)

        verify(exactly = 1) { eventReporter.report(any()) }
    }

    @Test
    fun `onEventReceived should add event when publishEvent is called when no observer is available `() {
        val observer = spyk<EventAnalyticsObserver>()
        testEventAnalyticsController.addEventAnalyticsObserver(observer)
        testEventAnalyticsController.removeEventAnalyticsObserver(observer)

        runBlockingTest {
            testEventAnalyticsController.publishEvent(EventName.meetingFailed)
        }

        verify(exactly = 0) { observer.onEventReceived(any(), any()) }
    }

    @Test
    fun `onEventReceived should be invoked with meeting stats when event is meeting life cycle related`() {
        val observer = spyk<EventAnalyticsObserver>()
        testEventAnalyticsController.addEventAnalyticsObserver(observer)

        runBlockingTest {
            testEventAnalyticsController.publishEvent(EventName.meetingFailed)
        }

        verify(exactly = 1) {
            observer.onEventReceived(
                EventName.meetingFailed,
                withArg { assertTrue(it.containsKey(EventAttributeName.meetingDurationMs)) })
        }
    }

    @Test
    fun `onEventReceived should be invoked without meeting stats when event is not meeting life cycle related`() {
        val observer = spyk<EventAnalyticsObserver>()
        testEventAnalyticsController.addEventAnalyticsObserver(observer)

        runBlockingTest {
            testEventAnalyticsController.publishEvent(EventName.videoInputFailed)
        }

        verify(exactly = 1) { observer.onEventReceived(
            EventName.videoInputFailed,
            withArg { assertFalse(it.containsKey(EventAttributeName.meetingDurationMs)) })
        }
    }

    @Test
    fun `getCommonEventAttributes should return common attributes`() {
        val attributes = testEventAnalyticsController.getCommonEventAttributes()

        assertTrue(attributes.containsKey(EventAttributeName.attendeeId))
        assertTrue(attributes.containsKey(EventAttributeName.deviceName))
        assertTrue(attributes.containsKey(EventAttributeName.deviceManufacturer))
        assertTrue(attributes.containsKey(EventAttributeName.deviceModel))
        assertTrue(attributes.containsKey(EventAttributeName.sdkVersion))
        assertTrue(attributes.containsKey(EventAttributeName.mediaSdkVersion))
        assertTrue(attributes.containsKey(EventAttributeName.meetingId))
    }

    @Test
    fun `getMeetingHistory should call meetingStatsCollector to get history events`() {
        testEventAnalyticsController.getMeetingHistory()

        verify { mockMeetingStatsCollector.getMeetingHistory() }
    }
}


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

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

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

class DefaultMeetingStatsCollectorTest {
    @MockK
    private lateinit var mockLogger: Logger

    @InjectMockKs
    private lateinit var testMeetingStatsCollector: DefaultMeetingStatsCollector

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

    @Test
    fun `maxVideoTileCount should take the higher value between the current maxVideoTileCount and the value passed in`() {
        testMeetingStatsCollector.updateMaxVideoTile(5)

        val maxVideoTileCount = testMeetingStatsCollector.getMeetingStatsEventAttributes()[EventAttributeName.maxVideoTileCount]
        assertEquals(5, maxVideoTileCount)

        testMeetingStatsCollector.updateMaxVideoTile(6)
        val maxVideoTileCountAfter = testMeetingStatsCollector.getMeetingStatsEventAttributes()[EventAttributeName.maxVideoTileCount]
        assertEquals(6, maxVideoTileCountAfter)

        testMeetingStatsCollector.updateMaxVideoTile(5)
        val maxVideoTileCountLast = testMeetingStatsCollector.getMeetingStatsEventAttributes()[EventAttributeName.maxVideoTileCount]
        assertEquals(6, maxVideoTileCountLast)
    }

    @Test
    fun `resetMeetingStats should reset meeting stats`() {
        testMeetingStatsCollector.updateMeetingStartTimeMs()
        testMeetingStatsCollector.incrementPoorConnectionCount()
        testMeetingStatsCollector.incrementRetryCount()
        testMeetingStatsCollector.updateMaxVideoTile(5)

        testMeetingStatsCollector.resetMeetingStats()

        val meetingDurationMs = testMeetingStatsCollector.getMeetingStatsEventAttributes()[EventAttributeName.meetingDurationMs]
        val retryCount = testMeetingStatsCollector.getMeetingStatsEventAttributes()[EventAttributeName.retryCount]
        val poorConnectionCount = testMeetingStatsCollector.getMeetingStatsEventAttributes()[EventAttributeName.poorConnectionCount]
        val maxVideoTileCount = testMeetingStatsCollector.getMeetingStatsEventAttributes()[EventAttributeName.maxVideoTileCount]
        assertEquals(0L, meetingDurationMs)
        assertEquals(0, retryCount)
        assertEquals(0, poorConnectionCount)
        assertEquals(0, maxVideoTileCount)
    }

    @Test
    fun `getMeetingHistory should return current meeting history`() {
        assertEquals(0, testMeetingStatsCollector.getMeetingHistory().size)

        testMeetingStatsCollector.addMeetingHistoryEvent(MeetingHistoryEventName.meetingStartSucceeded, 100)

        assertEquals(1, testMeetingStatsCollector.getMeetingHistory().size)
    }
}


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

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

import com.amazonaws.services.chime.sdk.meetings.internal.utils.JsonUtils
import org.junit.Assert
import org.junit.Test

class EventAttributesTest {
    @Test
    fun `toJsonString should call JSONUtils marshal function`() {
        val attributes = mutableMapOf<EventAttributeName, Any>()
        val text = attributes.toJsonString()

        Assert.assertEquals(JsonUtils.marshal(attributes), text)
    }
}


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

import com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.AudioMode
import org.junit.Assert
import org.junit.Test

class AudioVideoConfigurationTest {
    @Test
    fun `default audio mode should be stereo`() {
        Assert.assertEquals(AudioVideoConfiguration().audioMode, AudioMode.Stereo48K)
    }
}


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

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

import com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.AudioMode
import com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.AudioRecordingPresetOverride
import com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.AudioStreamType
import com.amazonaws.services.chime.sdk.meetings.audiovideo.metric.MetricsObserver
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.LocalVideoConfiguration
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.RemoteVideoSource
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoSource
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoSubscriptionConfiguration
import com.amazonaws.services.chime.sdk.meetings.internal.audio.AudioClientController
import com.amazonaws.services.chime.sdk.meetings.internal.audio.AudioClientObserver
import com.amazonaws.services.chime.sdk.meetings.internal.metric.ClientMetricsCollector
import com.amazonaws.services.chime.sdk.meetings.internal.video.VideoClientController
import com.amazonaws.services.chime.sdk.meetings.internal.video.VideoClientObserver
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionConfiguration
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionCredentials
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionStatus
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionStatusCode
import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionURLs
import com.amazonaws.services.chime.sdk.meetings.session.defaultUrlRewriter
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.mockkConstructor
import io.mockk.slot
import io.mockk.verify
import java.util.Timer
import java.util.TimerTask
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.setMain
import org.junit.Before
import org.junit.Test

class DefaultAudioVideoControllerTest {
    @MockK
    private lateinit var audioVideo: AudioVideoObserver
    @MockK
    private lateinit var metricsObserver: MetricsObserver

    private val meetingId = "meetingId"
    private val externalMeetingId = "externalMeetingId"
    private val attendeeId = "attendeeId"
    private val externalUserId = "externalUserId"
    private val joinToken = "joinToken"
    private val audioFallbackURL = "audioFallbackURL"
    private val audioHostURL = "audioHostURL"
    private val turnControlURL = "turnControlURL"
    private val signalingURL = "signalingURL"

    private val meetingSessionConfiguration = MeetingSessionConfiguration(
        meetingId,
        externalMeetingId,
        MeetingSessionCredentials(attendeeId, externalUserId, joinToken),
        MeetingSessionURLs(audioFallbackURL, audioHostURL, turnControlURL, signalingURL, ::defaultUrlRewriter)
    )
    private val localVideoConfig = LocalVideoConfiguration()

    @MockK
    private lateinit var audioClientObserver: AudioClientObserver

    @MockK
    private lateinit var audioClientController: AudioClientController

    @MockK
    private lateinit var clientMetricsCollector: ClientMetricsCollector

    @MockK
    private lateinit var videoClientController: VideoClientController

    @MockK
    private lateinit var videoClientObserver: VideoClientObserver

    private lateinit var audioVideoController: DefaultAudioVideoController

    @MockK
    private lateinit var mockPrimaryMeetingSessionCredentials: MeetingSessionCredentials

    @MockK
    private lateinit var mockPrimaryMeetingPromotionObserver: PrimaryMeetingPromotionObserver

    @MockK
    private lateinit var mockTimer: Timer

    @MockK
    private lateinit var mockVideoSource: VideoSource

    @ExperimentalCoroutinesApi
    private val testDispatcher = TestCoroutineDispatcher()

    @ExperimentalCoroutinesApi
    @Before
    fun setup() {
        Dispatchers.setMain(testDispatcher)
        MockKAnnotations.init(this, relaxUnitFun = true)
        audioVideoController =
            DefaultAudioVideoController(
                audioClientController,
                audioClientObserver,
                clientMetricsCollector,
                meetingSessionConfiguration,
                videoClientController,
                videoClientObserver
            )
    }

    @Test
    fun `start should call audioClientController start with the parameters in configuration`() {
        audioVideoController.start()
        verify {
            audioClientController.start(
                audioFallbackURL,
                audioHostURL,
                meetingId,
                attendeeId,
                joinToken,
                AudioMode.Stereo48K,
                AudioStreamType.VoiceCall,
                AudioRecordingPresetOverride.None
            )
        }
    }

    @Test
    fun `start with mono 16KHz should call audioClientController start with the parameters in configuration and mono 16KHz`() {
        val testAudioVideoConfiguration = AudioVideoConfiguration(audioMode = AudioMode.Mono16K)
        audioVideoController.start(testAudioVideoConfiguration)
        verify {
            audioClientController.start(
                    audioFallbackURL,
                    audioHostURL,
                    meetingId,
                    attendeeId,
                    joinToken,
                    AudioMode.Mono16K,
                    AudioStreamType.VoiceCall,
                    AudioRecordingPresetOverride.None
            )
        }
    }

    @Test
    fun `start with mono 48KHz should call audioClientController start with the parameters in configuration and mono 48KHz`() {
        val testAudioVideoConfiguration = AudioVideoConfiguration(audioMode = AudioMode.Mono48K)
        audioVideoController.start(testAudioVideoConfiguration)
        verify {
            audioClientController.start(
                    audioFallbackURL,
                    audioHostURL,
                    meetingId,
                    attendeeId,
                    joinToken,
                    AudioMode.Mono48K,
                    AudioStreamType.VoiceCall,
                    AudioRecordingPresetOverride.None
            )
        }
    }

    @Test
    fun `start with stereo 48KHz should call audioClientController start with the parameters in configuration and stereo 48KHz`() {
        val testAudioVideoConfiguration = AudioVideoConfiguration(audioMode = AudioMode.Stereo48K)
        audioVideoController.start(testAudioVideoConfiguration)
        verify {
            audioClientController.start(
                    audioFallbackURL,
                    audioHostURL,
                    meetingId,
                    attendeeId,
                    joinToken,
                    AudioMode.Stereo48K,
                    AudioStreamType.VoiceCall,
                    AudioRecordingPresetOverride.None
            )
        }
    }

    @Test
    fun `start() with voice call stream should call audioClientController start() with AudioStreamType VoiceCall`() {
        val testAudioVideoConfiguration = AudioVideoConfiguration(audioStreamType = AudioStreamType.VoiceCall)
        audioVideoController.start(testAudioVideoConfiguration)
        verify {
            audioClientController.start(
                audioFallbackURL,
                audioHostURL,
                meetingId,
                attendeeId,
                joinToken,
                AudioMode.Stereo48K,
                AudioStreamType.VoiceCall,
                AudioRecordingPresetOverride.None
            )
        }
    }

    @Test
    fun `start() with music stream should call audioClientController start() with AudioStreamType Music`() {
        val testAudioVideoConfiguration = AudioVideoConfiguration(audioStreamType = AudioStreamType.Music)
        audioVideoController.start(testAudioVideoConfiguration)
        verify {
            audioClientController.start(
                audioFallbackURL,
                audioHostURL,
                meetingId,
                attendeeId,
                joinToken,
                AudioMode.Stereo48K,
                AudioStreamType.Music,
                AudioRecordingPresetOverride.None
            )
        }
    }

    @Test
    fun `start with no_device should call audioClientController start with the parameters in configuration and no device`() {
        val testAudioVideoConfiguration = AudioVideoConfiguration(audioMode = AudioMode.NoDevice)
        audioVideoController.start(testAudioVideoConfiguration)
        verify {
            audioClientController.start(
                audioFallbackURL,
                audioHostURL,
                meetingId,
                attendeeId,
                joinToken,
                AudioMode.NoDevice,
                AudioStreamType.VoiceCall,
                AudioRecordingPresetOverride.None
            )
        }
    }

    @Test
    fun `start should call videoClientController start with the parameters in configuration`() {
        audioVideoController.start()
        verify {
            videoClientController.start()
        }
    }

    @Test
    fun `stop should call audioClientController stop`() {
        audioVideoController.stop()

        verify { audioClientController.stop() }
    }

    @Test
    fun `stop should call videoClientController stopAndDestroy`() {
        audioVideoController.stop()

        verify { videoClientController.stopAndDestroy() }
    }

    @Test
    fun `addAudioVideoObserver should call audioClientObserver subscribeToAudioClientStateChange with given observer`() {
        audioVideoController.addAudioVideoObserver(audioVideo)

        verify { audioClientObserver.subscribeToAudioClientStateChange(audioVideo) }
    }

    @Test
    fun `removeAudioVideoObserver should call audioClientObserver unsubscribeFromAudioClientStateChange with given observer`() {
        audioVideoController.removeAudioVideoObserver(audioVideo)

        verify { audioClientObserver.unsubscribeFromAudioClientStateChange(audioVideo) }
    }

    @Test
    fun `addAudioVideoObserver should call videoClientObserver subscribeToVideoClientStateChange with given observer`() {
        audioVideoController.addAudioVideoObserver(audioVideo)

        verify { videoClientObserver.subscribeToVideoClientStateChange(audioVideo) }
    }

    @Test
    fun `removeAudioVideoObserver should call videoClientObserver unsubscribeFromAVideoClientStateChange with given observer`() {
        audioVideoController.removeAudioVideoObserver(audioVideo)

        verify { videoClientObserver.unsubscribeFromVideoClientStateChange(audioVideo) }
    }

    @Test
    fun `addMetricsObserver should call clientMetricsCollector addObserver with given observer`() {
        audioVideoController.addMetricsObserver(metricsObserver)

        verify { clientMetricsCollector.subscribeToMetrics(metricsObserver) }
    }

    @Test
    fun `removeMetricsObserver should call clientMetricsCollector removeObserver with given observer`() {
        audioVideoController.removeMetricsObserver(metricsObserver)

        verify { clientMetricsCollector.unsubscribeFromMetrics(metricsObserver) }
    }

    @Test
    fun `startLocalVideo should call videoClientController startLocalVideo`() {
        audioVideoController.startLocalVideo()

        verify { videoClientController.startLocalVideo() }
    }

    @Test
    fun `startLocalVideo should call videoClientController startLocalVideo with given video config)`() {
        audioVideoController.startLocalVideo(localVideoConfig)

        verify { videoClientController.startLocalVideo(localVideoConfig) }
    }

    @Test
    fun `startLocalVideo should call videoClientController startLocalVideo with given video source`() {
        audioVideoController.startLocalVideo(mockVideoSource)

        verify { videoClientController.startLocalVideo(mockVideoSource) }
    }

    @Test
    fun `startLocalVideo should call videoClientController startLocalVideo with given video source and config)`() {
        audioVideoController.startLocalVideo(mockVideoSource, localVideoConfig)

        verify { videoClientController.startLocalVideo(mockVideoSource, localVideoConfig) }
    }

    @Test
    fun `stopLocalVideo should call videoClientController stopLocalVideo`() {
        audioVideoController.stopLocalVideo()

        verify { videoClientController.stopLocalVideo() }
    }

    @Test
    fun `startRemoteVideo should call videoClientController startRemoteVideo`() {
        audioVideoController.startRemoteVideo()

        verify { videoClientController.startRemoteVideo() }
    }

    @Test
    fun `stopRemoteVideo should call videoClientController stopRemoteVideo`() {
        audioVideoController.stopRemoteVideo()

        verify { videoClientController.stopRemoteVideo() }
    }

    @Test
    fun `updateVideoSourceSubscriptions should call videoClientController updateVideoSourceSubscriptions`() {
        val addedOrUpdated = emptyMap<RemoteVideoSource, VideoSubscriptionConfiguration>()
        val removed = emptyArray<RemoteVideoSource>()

        audioVideoController.updateVideoSourceSubscriptions(addedOrUpdated, removed)

        verify { videoClientController.updateVideoSourceSubscriptions(addedOrUpdated, removed) }
    }

    @Test
    fun `promoteToPrimaryMeeting returns success when both audio and video client callback for success`() {
        val audioObserver = slot<PrimaryMeetingPromotionObserver>()
        val videoObserver = slot<PrimaryMeetingPromotionObserver>()
        every { audioClientController.promoteToPrimaryMeeting(any(), observer = capture(audioObserver)) } returns Unit
        every { videoClientController.promoteToPrimaryMeeting(any(), observer = capture(videoObserver)) } returns Unit

        audioVideoController.promoteToPrimaryMeeting(mockPrimaryMeetingSessionCredentials, mockPrimaryMeetingPromotionObserver)
        audioObserver.captured.onPrimaryMeetingPromotion(MeetingSessionStatus(MeetingSessionStatusCode.OK))
        videoObserver.captured.onPrimaryMeetingPromotion(MeetingSessionStatus(MeetingSessionStatusCode.OK))

        verify { mockPrimaryMeetingPromotionObserver.onPrimaryMeetingPromotion(MeetingSessionStatus(MeetingSessionStatusCode.OK)) }
    }

    @Test
    fun `promoteToPrimaryMeeting returns failure when video fails`() {
        val audioObserver = slot<PrimaryMeetingPromotionObserver>()
        val videoObserver = slot<PrimaryMeetingPromotionObserver>()
        every { audioClientController.promoteToPrimaryMeeting(any(), observer = capture(audioObserver)) } returns Unit
        every { videoClientController.promoteToPrimaryMeeting(any(), observer = capture(videoObserver)) } returns Unit

        audioVideoController.promoteToPrimaryMeeting(mockPrimaryMeetingSessionCredentials, mockPrimaryMeetingPromotionObserver)
        audioObserver.captured.onPrimaryMeetingPromotion(MeetingSessionStatus(MeetingSessionStatusCode.OK))

        videoObserver.captured.onPrimaryMeetingPromotion(MeetingSessionStatus(MeetingSessionStatusCode.AudioServiceUnavailable))
        verify { mockPrimaryMeetingPromotionObserver.onPrimaryMeetingPromotion(MeetingSessionStatus(MeetingSessionStatusCode.AudioServiceUnavailable)) }
    }

    @Test
    fun `promoteToPrimaryMeeting returns failure when audio fails`() {
        val audioObserver = slot<PrimaryMeetingPromotionObserver>()
        val videoObserver = slot<PrimaryMeetingPromotionObserver>()
        every { audioClientController.promoteToPrimaryMeeting(any(), observer = capture(audioObserver)) } returns Unit
        every { videoClientController.promoteToPrimaryMeeting(any(), observer = capture(videoObserver)) } returns Unit

        audioVideoController.promoteToPrimaryMeeting(mockPrimaryMeetingSessionCredentials, mockPrimaryMeetingPromotionObserver)
        audioObserver.captured.onPrimaryMeetingPromotion(MeetingSessionStatus(MeetingSessionStatusCode.AudioServiceUnavailable))
        videoObserver.captured.onPrimaryMeetingPromotion(MeetingSessionStatus(MeetingSessionStatusCode.OK))

        verify { mockPrimaryMeetingPromotionObserver.onPrimaryMeetingPromotion(MeetingSessionStatus(MeetingSessionStatusCode.AudioServiceUnavailable)) }
    }

    @Test
    fun `promoteToPrimaryMeeting returns failure if either times out`() {
        mockkConstructor(Timer::class)
        val timerTaskSlot = slot<TimerTask>()
        every { anyConstructed<Timer>().schedule(capture(timerTaskSlot), 5000L) } returns Unit
        val audioObserver = slot<PrimaryMeetingPromotionObserver>()
        val videoObserver = slot<PrimaryMeetingPromotionObserver>()
        every { audioClientController.promoteToPrimaryMeeting(any(), observer = capture(audioObserver)) } returns Unit
        every { videoClientController.promoteToPrimaryMeeting(any(), observer = capture(videoObserver)) } returns Unit

        audioVideoController.promoteToPrimaryMeeting(mockPrimaryMeetingSessionCredentials, mockPrimaryMeetingPromotionObserver)
        videoObserver.captured.onPrimaryMeetingPromotion(MeetingSessionStatus(MeetingSessionStatusCode.OK))
        timerTaskSlot.captured.run()

        verify { mockPrimaryMeetingPromotionObserver.onPrimaryMeetingPromotion(MeetingSessionStatus(MeetingSessionStatusCode.AudioInternalServerError)) }
    }

    @Test
    fun `demoteFromPrimaryMeeting calls demoteFromPrimaryMeeting on clients and also calls observer function`() {
        every { audioClientController.promoteToPrimaryMeeting(any(), any()) } returns Unit
        every { videoClientController.promoteToPrimaryMeeting(any(), any()) } returns Unit

        audioVideoController.promoteToPrimaryMeeting(mockPrimaryMeetingSessionCredentials, mockPrimaryMeetingPromotionObserver)
        audioVideoController.demoteFromPrimaryMeeting()

        verify { mockPrimaryMeetingPromotionObserver.onPrimaryMeetingDemotion(MeetingSessionStatus(MeetingSessionStatusCode.OK)) }
    }

    @Test
    fun `promoteToPrimaryMeeting and a demotion from client will call original observer`() {
        val audioObserver = slot<PrimaryMeetingPromotionObserver>()
        val videoObserver = slot<PrimaryMeetingPromotionObserver>()
        every { audioClientController.promoteToPrimaryMeeting(any(), observer = capture(audioObserver)) } returns Unit
        every { videoClientController.promoteToPrimaryMeeting(any(), observer = capture(videoObserver)) } returns Unit

        audioVideoController.promoteToPrimaryMeeting(mockPrimaryMeetingSessionCredentials, mockPrimaryMeetingPromotionObserver)
        audioObserver.captured.onPrimaryMeetingDemotion(MeetingSessionStatus(MeetingSessionStatusCode.AudioServiceUnavailable))

        verify { mockPrimaryMeetingPromotionObserver.onPrimaryMeetingDemotion(MeetingSessionStatus(MeetingSessionStatusCode.AudioServiceUnavailable)) }
    }
}


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

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

import android.content.Context
import androidx.core.content.ContextCompat
import com.amazonaws.services.chime.sdk.meetings.analytics.EventAnalyticsController
import com.amazonaws.services.chime.sdk.meetings.analytics.EventAnalyticsObserver
import com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.AudioMode
import com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.activespeakerdetector.ActiveSpeakerDetectorFacade
import com.amazonaws.services.chime.sdk.meetings.audiovideo.contentshare.ContentShareController
import com.amazonaws.services.chime.sdk.meetings.audiovideo.contentshare.ContentShareObserver
import com.amazonaws.services.chime.sdk.meetings.audiovideo.contentshare.ContentShareSource
import com.amazonaws.services.chime.sdk.meetings.audiovideo.metric.MetricsObserver
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.LocalVideoConfiguration
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoSource
import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoTileController
import com.amazonaws.services.chime.sdk.meetings.device.DeviceChangeObserver
import com.amazonaws.services.chime.sdk.meetings.device.DeviceController
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.realtime.RealtimeControllerFacade
import com.amazonaws.services.chime.sdk.meetings.realtime.RealtimeObserver
import com.amazonaws.services.chime.sdk.meetings.realtime.TranscriptEventObserver
import com.amazonaws.services.chime.sdk.meetings.realtime.datamessage.DataMessageObserver
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.InjectMockKs
import io.mockk.impl.annotations.MockK
import io.mockk.mockkClass
import io.mockk.mockkStatic
import io.mockk.verify
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test

class DefaultAudioVideoFacadeTest {
    private val devices = emptyList<MediaDevice>()

    private val mediaDevice = MediaDevice(
        "label",
        MediaDeviceType.OTHER
    )

    private val messageTopic = "topic"
    private val messageData = "data"
    private val messageLifetimeMs = 3000
    private val localVideoConfig = LocalVideoConfiguration()

    @MockK
    private lateinit var mockAudioVideoObserver: AudioVideoObserver

    @MockK
    private lateinit var mockRealtimeObserver: RealtimeObserver

    @MockK
    private lateinit var mockEventAnalyticsObserver: EventAnalyticsObserver

    @MockK
    private lateinit var mockDataMessageObserver: DataMessageObserver

    @MockK
    private lateinit var mockTranscriptEventObserver: TranscriptEventObserver

    @MockK
    private lateinit var mockDeviceChangeObserver: DeviceChangeObserver

    @MockK
    private lateinit var mockMetricsObserver: MetricsObserver

    @MockK
    private lateinit var mockContentShareObserver: ContentShareObserver

    @MockK
    private lateinit var context: Context

    @MockK
    private lateinit var audioVideoController: AudioVideoControllerFacade

    @MockK
    private lateinit var eventAnalyticsController: EventAnalyticsController

    @MockK
    private lateinit var realtimeController: RealtimeControllerFacade

    @MockK
    private lateinit var deviceController: DeviceController

    @MockK
    private lateinit var videoTileController: VideoTileController

    @MockK
    private lateinit var activeSpeakerDetector: ActiveSpeakerDetectorFacade

    @MockK
    private lateinit var contentShareController: ContentShareController

    @MockK
    private lateinit var mockVideoSource: VideoSource

    @InjectMockKs
    private lateinit var audioVideoFacade: DefaultAudioVideoFacade

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

    @Test(expected = SecurityException::class)
    fun `start should throw exception when the required permissions are not granted with default AudioMode`() {
        mockkStatic(ContextCompat::class)
        every { ContextCompat.checkSelfPermission(any(), any()) } returns 1
        audioVideoFacade.start()
    }

    @Test
    fun `start should not check permissions if AudioMode is NoDevice`() {
        mockkStatic(ContextCompat::class)
        audioVideoFacade.start(AudioVideoConfiguration(audioMode = AudioMode.NoDevice))
        verify(exactly = 0) { ContextCompat.checkSelfPermission(any(), any()) }
    }

    @Test
    fun `addEventAnalyticsObserver should call EvenController addEventAnalyticsObserver with given observer`() {
        audioVideoFacade.addEventAnalyticsObserver(mockEventAnalyticsObserver)

        verify { eventAnalyticsController.addEventAnalyticsObserver(mockEventAnalyticsObserver) }
    }

    @Test
    fun `addAudioVideoObserver should call audioVideoController addObserver with given observer`() {
        audioVideoFacade.addAudioVideoObserver(mockAudioVideoObserver)
        verify { audioVideoController.addAudioVideoObserver(mockAudioVideoObserver) }
    }

    @Test
    fun `removeAudioVideoObserver should call audioVideoController removeObserve with given observer`() {
        audioVideoFacade.removeAudioVideoObserver(mockAudioVideoObserver)
        verify { audioVideoController.removeAudioVideoObserver(mockAudioVideoObserver) }
    }

    @Test
    fun `addMetricsObserver should call audioVideoController addMetricsObserver with given observer`() {
        audioVideoFacade.addMetricsObserver(mockMetricsObserver)
        verify { audioVideoController.addMetricsObserver(mockMetricsObserver) }
    }

    @Test
    fun `removeEventAnalyticsObserver should call EvenAnalyticsController removeEventAnalyticsObserver with given observer`() {
        audioVideoFacade.removeEventAnalyticsObserver(mockEventAnalyticsObserver)

        verify { eventAnalyticsController.removeEventAnalyticsObserver(mockEventAnalyticsObserver) }
    }

    @Test
    fun `removeMetricsObserver should call audioVideoController removeMetricsObserver with given observer`() {
        audioVideoFacade.removeMetricsObserver(mockMetricsObserver)
        verify { audioVideoController.removeMetricsObserver(mockMetricsObserver) }
    }

    @Test
    fun `start should call audioVideoController start when the required permissions are granted`() {
        mockkStatic(ContextCompat::class)
        every { ContextCompat.checkSelfPermission(any(), any()) } returns 0
        audioVideoFacade.start()
        verify { audioVideoController.start(AudioVideoConfiguration()) }
    }

    @Test
    fun `start should call audioVideoController start with configuration when the required permissions are granted`() {
        mockkStatic(ContextCompat::class)
        every { ContextCompat.checkSelfPermission(any(), any()) } returns 0
        val testConfiguration = AudioVideoConfiguration()
        audioVideoFacade.start(testConfiguration)
        verify { audioVideoController.start(testConfiguration) }
    }

    @Test
    fun `stop should call audioVideoController stop`() {
        audioVideoFacade.stop()
        verify { audioVideoController.stop() }
    }

    @Test
    fun `realtimeLocalMute should call realtimeController realtimeLocalMute and return the status`() {
        every { realtimeController.realtimeLocalMute() } returns true
        assertTrue(audioVideoFacade.realtimeLocalMute())
    }

    @Test
    fun `realtimeLocalUnmute should call realtimeController realtimeLocalUnmute and return the status`() {
        every { realtimeController.realtimeLocalUnmute() } returns true
        assertTrue(audioVideoFacade.realtimeLocalUnmute())
    }

    @Test
    fun `addRealtimeObserver should call realtimeController addRealtimeObserver with given observer`() {
        audioVideoFacade.addRealtimeObserver(mockRealtimeObserver)
        verify { realtimeController.addRealtimeObserver(mockRealtimeObserver) }
    }

    @Test
    fun `removeRealtimeObserver should call realtimeController removeRealtimeObserver with given observer`() {
        audioVideoFacade.removeRealtimeObserver(mockRealtimeObserver)
        verify { realtimeController.removeRealtimeObserver(mockRealtimeObserver) }
    }

    @Test
    fun `realtimeSendDataMessage should call realtimeController realtimeSendDataMessage with given data`() {
        audioVideoFacade.realtimeSendDataMessage(messageTopic, messageData, messageLifetimeMs)
        verify { realtimeController.realtimeSendDataMessage(messageTopic, messageData, messageLifetimeMs) }
    }

    @Test
    fun `realtimeSendDataMessage should call realtimeController realtimeSendDataMessage with 0 lifetime ms as default`() {
        audioVideoFacade.realtimeSendDataMessage(messageTopic, messageData)
        verify { realtimeController.realtimeSendDataMessage(messageTopic, messageData, 0) }
    }

    @Test
    fun `addRealtimeDataMessageObserver should call realtimeController addRealtimeDataMessageObserver with given observer`() {
        audioVideoFacade.addRealtimeDataMessageObserver(messageTopic, mockDataMessageObserver)
        verify { realtimeController.addRealtimeDataMessageObserver(messageTopic, mockDataMessageObserver) }
    }

    @Test
    fun `removeRealtimeDataMessageObserverFromTopic should call realtimeController removeRealtimeDataMessageObserverFromTopic with given topic`() {
        audioVideoFacade.removeRealtimeDataMessageObserverFromTopic(messageTopic)
        verify { realtimeController.removeRealtimeDataMessageObserverFromTopic(messageTopic) }
    }

    @Test
    fun `realtimeSetVoiceFocusEnabled(true) should call realtimeController realtimeSetVoiceFocusEnabled(true) and return the status`() {
        every { realtimeController.realtimeSetVoiceFocusEnabled(true) } returns true
        assertTrue(audioVideoFacade.realtimeSetVoiceFocusEnabled(true))
        verify { realtimeController.realtimeSetVoiceFocusEnabled(true) }
    }

    @Test
    fun `realtimeIsVoiceFocusEnabled should call realtimeController realtimeIsVoiceFocusEnabled and return the status true`() {
        every { realtimeController.realtimeIsVoiceFocusEnabled() } returns true
        assertTrue(audioVideoFacade.realtimeIsVoiceFocusEnabled())
        verify { realtimeController.realtimeIsVoiceFocusEnabled() }
    }

    @Test
    fun `realtimeSetVoiceFocusEnabled(false) should call realtimeController realtimeSetVoiceFocusEnabled(false) and return the status`() {
        every { realtimeController.realtimeSetVoiceFocusEnabled(false) } returns true
        assertTrue(audioVideoFacade.realtimeSetVoiceFocusEnabled(false))
        verify { realtimeController.realtimeSetVoiceFocusEnabled(false) }
    }

    @Test
    fun `realtimeIsVoiceFocusEnabled should call realtimeController realtimeIsVoiceFocusEnabled and return the status false`() {
        every { realtimeController.realtimeIsVoiceFocusEnabled() } returns false
        assertFalse(audioVideoFacade.realtimeIsVoiceFocusEnabled())
        verify { realtimeController.realtimeIsVoiceFocusEnabled() }
    }

    @Test
    fun `addRealtimeTranscriptEventObserver should call realtimeController addRealtimeTranscriptEventObserver with given observer`() {
        audioVideoFacade.addRealtimeTranscriptEventObserver(mockTranscriptEventObserver)
        verify { realtimeController.addRealtimeTranscriptEventObserver(mockTranscriptEventObserver) }
    }

    @Test
    fun `removeRealtimeTranscriptEventObserver should call realtimeController removeRealtimeTranscriptEventObserver with given observer`() {
        audioVideoFacade.removeRealtimeTranscriptEventObserver(mockTranscriptEventObserver)
        verify { realtimeController.removeRealtimeTranscriptEventObserver(mockTranscriptEventObserver) }
    }

    @Test
    fun `listAudioDevices should call devices deviceController listAudioDevices and return the list of devices`() {
        every { deviceController.listAudioDevices() } returns devices
        assertEquals(devices, audioVideoFacade.listAudioDevices())
    }

    @Test
    fun `chooseAudioDevice should call deviceController chooseAudioDevice`() {
        audioVideoFacade.chooseAudioDevice(mediaDevice)
        verify { deviceController.chooseAudioDevice(mediaDevice) }
    }

    @Test
    fun `addDeviceChangeObserver should call deviceController addDeviceChangeObserver with given observer`() {
        audioVideoFacade.addDeviceChangeObserver(mockDeviceChangeObserver)
        verify { deviceController.addDeviceChangeObserver(mockDeviceChangeObserver) }
    }

    @Test
    fun `removeDeviceChangeObserver should call deviceController removeDeviceChangeObserver with given observer`() {
        audioVideoFacade.removeDeviceChangeObserver(mockDeviceChangeObserver)
        verify { deviceController.removeDeviceChangeObserver(mockDeviceChangeObserver) }
    }

    @Test
    fun `startLocalVideo should call audioVideoController startLocalVideo`() {
        audioVideoFacade.startLocalVideo()
        verify { audioVideoController.startLocalVideo() }
    }

    @Test
    fun `startLocalVideo should call audioVideoController startLocalVideo with given video config`() {
        audioVideoFacade.startLocalVideo(localVideoConfig)
        verify { audioVideoController.startLocalVideo(localVideoConfig) }
    }

    @Test
    fun `startLocalVideo should call audioVideoController startLocalVideo with given video source`() {
        audioVideoFacade.startLocalVideo(mockVideoSource)
        verify { audioVideoController.startLocalVideo(mockVideoSource) }
    }

    @Test
    fun `startLocalVideo should call audioVideoController startLocalVideo with given video source and config)`() {
        audioVideoFacade.startLocalVideo(mockVideoSource, localVideoConfig)
        verify { audioVideoController.startLocalVideo(mockVideoSource, localVideoConfig) }
    }

    @Test
    fun `stopLocalVideo should call audioVideoController stopLocalVideo`() {
        audioVideoFacade.stopLocalVideo()
        verify { audioVideoController.stopLocalVideo() }
    }

    @Test
    fun `getActiveCamera should call deviceController getActiveCamera`() {
        every { deviceController.getActiveCamera() } returns mediaDevice

        assertEquals(mediaDevice, audioVideoFacade.getActiveCamera())
        verify { deviceController.getActiveCamera() }
    }

    @Test
    fun `switchCamera should call deviceController switchCamera`() {
        audioVideoFacade.switchCamera()

        verify { deviceController.switchCamera() }
    }

    @Test
    fun `startContentShare should call contentShareController startContentShare`() {
        val source = mockkClass(ContentShareSource::class)

        audioVideoFacade.startContentShare(source)

        verify { contentShareController.startContentShare(source) }
    }

    @Test
    fun `stopContentShare should call contentShareController stopContentShare`() {
        audioVideoFacade.stopContentShare()

        verify { contentShareController.stopContentShare() }
    }

    @Test
    fun `addContentShareObserver should call deviceController addContentShareObserver with given observer`() {
        audioVideoFacade.addContentShareObserver(mockContentShareObserver)

        verify { contentShareController.addContentShareObserver(mockContentShareObserver) }
    }

    @Test
    fun `removeContentShareObserver should call deviceController removeContentShareObserver with given observer`() {
        audioVideoFacade.removeContentShareObserver(mockContentShareObserver)

        verify { contentShareController.removeContentShareObserver(mockContentShareObserver) }
    }
}


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

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

class AudioModeTest {
    @Test
    fun `get enum value from int`() {
        Assert.assertEquals(AudioMode.from(1), AudioMode.Mono16K)
        Assert.assertEquals(AudioMode.from(2), AudioMode.Mono48K)
        Assert.assertEquals(AudioMode.from(3), AudioMode.Stereo48K)
        Assert.assertEquals(AudioMode.from(4), AudioMode.NoDevice)
    }

    @Test
    fun `get enum value from invalid int returns null`() {
        Assert.assertNull(AudioMode.from(-1))
    }

    @Test
    fun `get enum value from int with fallback to default value`() {
        Assert.assertEquals(AudioMode.from(-1, AudioMode.Stereo48K), AudioMode.Stereo48K)
    }
}


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

package com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.activespeakerdetector

import com.amazonaws.services.chime.sdk.meetings.audiovideo.AttendeeInfo
import com.amazonaws.services.chime.sdk.meetings.audiovideo.VolumeLevel
import com.amazonaws.services.chime.sdk.meetings.audiovideo.VolumeUpdate
import com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.activespeakerpolicy.DefaultActiveSpeakerPolicy
import com.amazonaws.services.chime.sdk.meetings.internal.audio.AudioClientObserver
import io.mockk.MockKAnnotations
import io.mockk.impl.annotations.MockK
import io.mockk.spyk
import io.mockk.verify
import junit.framework.Assert.assertNotNull
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.junit.Before
import org.junit.Test

@ExperimentalCoroutinesApi
class DefaultActiveSpeakerDetectorTest {

    @MockK
    private lateinit var audioClientObserver: AudioClientObserver

    private lateinit var activeSpeakerObserverWithScore1: ActiveSpeakerObserver

    private lateinit var activeSpeakerObserverWithScore2: ActiveSpeakerObserver

    private lateinit var activeSpeakerObserverWithoutScore: ActiveSpeakerObserver

    private lateinit var activeSpeakerDetector: DefaultActiveSpeakerDetector

    private lateinit var activeSpeakerPolicy: DefaultActiveSpeakerPolicy

    private val timeout = 1000L
    private val testId1 = "aliceId"
    private val testAttendeeInfo1 =
        AttendeeInfo(testId1, testId1)
    private val testVolumeUpdate1 =
        VolumeUpdate(
            testAttendeeInfo1,
            VolumeLevel.High
        )

    private val testId2 = "bobId"
    private val testAttendeeInfo2 =
        AttendeeInfo(testId2, testId2)
    private val testVolumeUpdate2 =
        VolumeUpdate(
            testAttendeeInfo2,
            VolumeLevel.High
        )

    @Before
    fun setup() {
        MockKAnnotations.init(this, relaxUnitFun = true)
        activeSpeakerDetector = spyk(DefaultActiveSpeakerDetector(audioClientObserver))
        activeSpeakerObserverWithScore1 = spyk(object : ActiveSpeakerObserver {
            override val scoreCallbackIntervalMs: Int?
                get() = 200

            override fun onActiveSpeakerDetected(attendeeInfo: Array<AttendeeInfo>) {
            }

            override fun onActiveSpeakerScoreChanged(scores: Map<AttendeeInfo, Double>) {
            }
        })
        activeSpeakerObserverWithScore2 = spyk(object : ActiveSpeakerObserver {
            override val scoreCallbackIntervalMs: Int?
                get() = 400

            override fun onActiveSpeakerDetected(attendeeInfo: Array<AttendeeInfo>) {
            }

            override fun onActiveSpeakerScoreChanged(scores: Map<AttendeeInfo, Double>) {
            }
        })
        activeSpeakerObserverWithoutScore = spyk(object : ActiveSpeakerObserver {
            override val scoreCallbackIntervalMs: Int?
                get() = null

            override fun onActiveSpeakerDetected(attendeeInfo: Array<AttendeeInfo>) {
            }

            override fun onActiveSpeakerScoreChanged(scores: Map<AttendeeInfo, Double>) {
            }
        })
        activeSpeakerPolicy = DefaultActiveSpeakerPolicy()
    }

    @Test
    fun `DefaultActiveSpeakerDetector should not be null`() {
        assertNotNull(activeSpeakerDetector)
    }

    @Test
    fun `DefaultActiveSpeakerDetector should show no active speakers when no volume update`() {
        activeSpeakerDetector.addActiveSpeakerObserver(
            activeSpeakerPolicy,
            activeSpeakerObserverWithoutScore
        )
        activeSpeakerDetector.onAttendeesJoined(arrayOf(testAttendeeInfo1))
        activeSpeakerDetector.removeActiveSpeakerObserver(activeSpeakerObserverWithoutScore)

        verify(exactly = 0) {
            activeSpeakerObserverWithoutScore.onActiveSpeakerDetected(
                arrayOf(
                    testAttendeeInfo1
                )
            )
        }
    }

    @Test
    fun `DefaultActiveSpeakerDetector should show active speaker on volume update`() {
        activeSpeakerDetector.addActiveSpeakerObserver(
            activeSpeakerPolicy,
            activeSpeakerObserverWithoutScore
        )
        activeSpeakerDetector.onAttendeesJoined(arrayOf(testAttendeeInfo1))
        activeSpeakerDetector.onVolumeChanged(arrayOf(testVolumeUpdate1))
        Thread.sleep(timeout)
        activeSpeakerDetector.removeActiveSpeakerObserver(activeSpeakerObserverWithoutScore)

        verify(exactly = 1) {
            activeSpeakerObserverWithoutScore.onActiveSpeakerDetected(
                arrayOf(
                    testAttendeeInfo1
                )
            )
        }
    }

    @Test
    fun `DefaultActiveSpeakerDetector should show active speakers scores`() {
        activeSpeakerDetector.addActiveSpeakerObserver(
            activeSpeakerPolicy,
            activeSpeakerObserverWithScore1
        )
        activeSpeakerDetector.onAttendeesJoined(arrayOf(testAttendeeInfo1))
        Thread.sleep(500)
        activeSpeakerDetector.removeActiveSpeakerObserver(activeSpeakerObserverWithScore1)

        verify(exactly = 2) {
            activeSpeakerObserverWithScore1.onActiveSpeakerScoreChanged(
                mutableMapOf(testAttendeeInfo1 to 0.0)
            )
        }
    }

    @Test
    fun `DefaultActiveSpeakerDetector should send active speakers scores to each observer`() {
        activeSpeakerDetector.addActiveSpeakerObserver(
            activeSpeakerPolicy,
            activeSpeakerObserverWithScore1
        )
        activeSpeakerDetector.addActiveSpeakerObserver(
            activeSpeakerPolicy,
            activeSpeakerObserverWithScore2
        )
        activeSpeakerDetector.onAttendeesJoined(arrayOf(testAttendeeInfo1))
        Thread.sleep(500)
        activeSpeakerDetector.removeActiveSpeakerObserver(activeSpeakerObserverWithScore1)
        activeSpeakerDetector.removeActiveSpeakerObserver(activeSpeakerObserverWithScore2)

        verify(exactly = 2) {
            activeSpeakerObserverWithScore1.onActiveSpeakerScoreChanged(
                mutableMapOf(testAttendeeInfo1 to 0.0)
            )
        }
        verify(exactly = 1) {
            activeSpeakerObserverWithScore2.onActiveSpeakerScoreChanged(
                mutableMapOf(testAttendeeInfo1 to 0.0)
            )
        }
        verify(exactly = 0) { activeSpeakerObserverWithScore1.onActiveSpeakerDetected(any()) }
        verify(exactly = 0) { activeSpeakerObserverWithScore2.onActiveSpeakerDetected(any()) }
    }

    @Test
    fun `DefaultActiveSpeakerDetector should show multiple active speaker on volume update`() {
        activeSpeakerDetector.addActiveSpeakerObserver(
            activeSpeakerPolicy,
            activeSpeakerObserverWithoutScore
        )
        activeSpeakerDetector.onAttendeesJoined(arrayOf(testAttendeeInfo1, testAttendeeInfo2))
        activeSpeakerDetector.onVolumeChanged(arrayOf(testVolumeUpdate1, testVolumeUpdate2))
        Thread.sleep(timeout)
        activeSpeakerDetector.removeActiveSpeakerObserver(activeSpeakerObserverWithoutScore)

        verify(exactly = 1) {
            activeSpeakerObserverWithoutScore.onActiveSpeakerDetected(
                attendeeInfo = or(
                    arrayOf(testAttendeeInfo1, testAttendeeInfo2),
                    arrayOf(testAttendeeInfo2, testAttendeeInfo1)
                )
            )
        }
    }
}


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

package com.amazonaws.services.chime.sdk.meetings.audiovideo.audio.activespeakerpolicy

import com.amazonaws.services.chime.sdk.meetings.audiovideo.AttendeeInfo
import com.amazonaws.services.chime.sdk.meetings.audiovideo.VolumeLevel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.junit.Before
import org.junit.Test

@ExperimentalCoroutinesApi
class DefaultActiveSpeakerPolicyTest {

    private lateinit var activeSpeakerPolicy: DefaultActiveSpeakerPolicy

    private val attendeeInfo = AttendeeInfo(
        "attendeeId",
        "externalUserId"
    )

    @Before
    fun setup() {
        activeSpeakerPolicy = DefaultActiveSpeakerPolicy()
    }

    @Test
    fun `DefaultActiveSpeakerPolicy should not be null`() {
        assert(activeSpeakerPolicy != null)
    }

    @Test
    fun `speaker score should be 0 when volume is muted`() {
        val score = activeSpeakerPolicy.calculateScore(attendeeInfo, VolumeLevel.Muted)

        assert(score == 0.0)
    }

    @Test
    fun `speaker score should not be 0 when volume is greater than NotSpeaking`() {
        val score = activeSpeakerPolicy.calculateScore(attendeeInfo, VolumeLevel.Low)

        assert(score > 0.0)
    }
}


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

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

import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoSource
import com.amazonaws.services.chime.sdk.meetings.internal.contentshare.ContentShareVideoClientController
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger
import io.mockk.MockKAnnotations
import io.mockk.impl.annotations.InjectMockKs
import io.mockk.impl.annotations.MockK
import io.mockk.verify
import org.junit.Before
import org.junit.Test

class DefaultContentShareControllerTest {
    @MockK
    private lateinit var videoSource: VideoSource

    @MockK
    private lateinit var contentShareObserver: ContentShareObserver

    @MockK
    private lateinit var logger: Logger

    @MockK
    private lateinit var contentShareVideoClientController: ContentShareVideoClientController

    @InjectMockKs
    private lateinit var contentShareController: DefaultContentShareController

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

    @Test
    fun `startContentShare should call contentShareVideoClientController startVideoShare when video source is available`() {
        val contentShareSource = ContentShareSource()
        contentShareSource.videoSource = videoSource

        contentShareController.startContentShare(contentShareSource)

        verify(exactly = 1) { contentShareVideoClientController.startVideoShare(videoSource) }
    }

    @Test
    fun `startContentShare should not call contentShareVideoClientController startVideoShare when video source is null`() {
        val contentShareSource = ContentShareSource()

        contentShareController.startContentShare(contentShareSource)

        verify(exactly = 0) { contentShareVideoClientController.startVideoShare(any()) }
    }

    @Test
    fun `stopContentShare should call contentShareVideoClientController stopVideoShare`() {
        contentShareController.stopContentShare()

        verify(exactly = 1) { contentShareVideoClientController.stopVideoShare() }
    }

    @Test
    fun `addContentShareObserver should call contentShareVideoClientController subscribeToVideoClientStateChange with given observer`() {
        contentShareController.addContentShareObserver(contentShareObserver)

        verify(exactly = 1) { contentShareVideoClientController.subscribeToVideoClientStateChange(contentShareObserver) }
    }

    @Test
    fun `removeContentShareObserver should call contentShareVideoClientController unsubscribeFromVideoClientStateChange with given observer`() {
        contentShareController.removeContentShareObserver(contentShareObserver)

        verify(exactly = 1) { contentShareVideoClientController.unsubscribeFromVideoClientStateChange(contentShareObserver) }
    }
}


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

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

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

class ObservableMetricTest {
    @Test
    fun `isContentShareMetric should return expected values`() {
        assertTrue(ObservableMetric.contentShareVideoSendBitrate.isContentShareMetric())
        assertFalse(ObservableMetric.videoSendBitrate.isContentShareMetric())
        assertTrue(ObservableMetric.contentShareVideoSendFps.isContentShareMetric())
        assertFalse(ObservableMetric.videoSendFps.isContentShareMetric())
        assertTrue(ObservableMetric.contentShareVideoSendPacketLossPercent.isContentShareMetric())
        assertFalse(ObservableMetric.videoSendPacketLossPercent.isContentShareMetric())
        assertTrue(ObservableMetric.contentShareVideoSendRttMs.isContentShareMetric())
        assertFalse(ObservableMetric.videoSendRttMs.isContentShareMetric())
    }
}