package com.suno.android.clip import arrow.core.Either import arrow.retrofit.adapter.either.networkhandling.UnexpectedCallError import com.suno.android.common_core_utils.Id import com.suno.android.common_core_utils.SunoLogger import com.suno.android.common_core_utils.constants.ReactionType import com.suno.android.common_data.mappers.clips.SongListData import com.suno.android.common_data.mappers.hooks.LocalHookData import com.suno.android.common_data.metadata.MetadataType import com.suno.android.common_data.metadata.Recommendation import com.suno.android.common_data.metadata.RecommendationMetadata import com.suno.android.common_networking.remote.common.RemoteClipReaction import com.suno.android.common_networking.remote.entities.RemoteUpdateReactionBody import com.suno.android.common_networking.remote.gen.GenService import com.suno.android.media.MediaMetadataManager import com.suno.android.media.MediaReaction import io.mockk.coEvery import io.mockk.coVerify import io.mockk.mockk import io.mockk.slot import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Before import org.junit.Test class UpdateClipReactionUseCaseTest { private val clipId = Id("test-clip-id") private val hookId = Id("test-hook-id") private val recommendationItemId = Id("test-recommendation-id") private val recommendationMetadata = RecommendationMetadata( hookId = hookId, recommendationItemId = recommendationItemId, metadataType = MetadataType.HooksFeed, ) private val genService = mockk() private val mediaMetadataManager = mockk(relaxed = true) private val loggerFactory = mockk(relaxed = true) private lateinit var subject: UpdateClipReactionUseCase @Before fun setUp() { subject = UpdateClipReactionUseCase( loggerFactory = loggerFactory, genService = genService, mediaMetadataManager = mediaMetadataManager, ) } @Test fun `given no current reaction when requesting like then calls service with like reaction`() = runTest { setupSuccessfulServiceCall("L") val result = subject( clipId = clipId, currentReactionType = null, requestedReactionType = ReactionType.LIKE, ) assertEquals(ReactionType.LIKE, result.getOrNull()) verifyServiceCalledWithReaction(RemoteUpdateReactionBody.Reaction.LIKE) } @Test fun `given no current reaction when requesting dislike then calls service with dislike reaction`() = runTest { setupSuccessfulServiceCall("D") val result = subject( clipId = clipId, currentReactionType = null, requestedReactionType = ReactionType.DISLIKE, ) assertEquals(ReactionType.DISLIKE, result.getOrNull()) verifyServiceCalledWithReaction(RemoteUpdateReactionBody.Reaction.DISLIKE) } @Test fun `given no current reaction when requesting nothing then calls service with null reaction`() = runTest { setupSuccessfulServiceCall(null) val result = subject( clipId = clipId, currentReactionType = null, requestedReactionType = ReactionType.NOTHING, ) assertNull(result.getOrNull()) verifyServiceCalledWithNullReaction() } @Test fun `given current reaction when requesting same reaction then toggles off reaction`() = runTest { setupSuccessfulServiceCall(null) val result = subject( clipId = clipId, currentReactionType = ReactionType.LIKE, requestedReactionType = ReactionType.LIKE, ) assertNull(result.getOrNull()) verifyServiceCalledWithNullReaction() } @Test fun `given current like when requesting dislike then switches to dislike`() = runTest { setupSuccessfulServiceCall("D") val result = subject( clipId = clipId, currentReactionType = ReactionType.LIKE, requestedReactionType = ReactionType.DISLIKE, ) assertEquals(ReactionType.DISLIKE, result.getOrNull()) verifyServiceCalledWithReaction(RemoteUpdateReactionBody.Reaction.DISLIKE) } @Test fun `given current dislike when requesting like then switches to like`() = runTest { setupSuccessfulServiceCall("L") val result = subject( clipId = clipId, currentReactionType = ReactionType.DISLIKE, requestedReactionType = ReactionType.LIKE, ) assertEquals(ReactionType.LIKE, result.getOrNull()) verifyServiceCalledWithReaction(RemoteUpdateReactionBody.Reaction.LIKE) } @Test fun `given service returns unknown reaction type then returns null`() = runTest { val remoteResponse = createRemoteClipReaction(reactionType = "Unknown") coEvery { genService.updateReactionType(any(), any()) } returns Either.Right( remoteResponse, ) val result = subject( clipId = clipId, currentReactionType = null, requestedReactionType = ReactionType.LIKE, ) assertNull(result.getOrNull()) } @Test fun `given service returns network error then returns error and logs failure`() = runTest { val networkError = UnexpectedCallError(RuntimeException("Network failure")) coEvery { genService.updateReactionType( any(), any(), ) } returns Either.Left(networkError) val result = subject( clipId = clipId, currentReactionType = null, requestedReactionType = ReactionType.LIKE, ) assertEquals(networkError, result.leftOrNull()) } @Test fun `given successful update reaction then broadcasts media reaction`() = runTest { setupSuccessfulServiceCall("L") subject( clipId = clipId, currentReactionType = null, requestedReactionType = ReactionType.LIKE, ) verifyMediaReactionBroadcast(ReactionType.LIKE) } @Test fun `given service error then does not broadcast media reaction`() = runTest { val networkError = UnexpectedCallError(RuntimeException("Network failure")) coEvery { genService.updateReactionType( any(), any(), ) } returns Either.Left(networkError) subject( clipId = clipId, currentReactionType = null, requestedReactionType = ReactionType.LIKE, ) coVerify(exactly = 0) { mediaMetadataManager.broadcastMediaReaction(any()) } } @Test fun `given recommendation metadata when requesting like then calls service with recommendation metadata`() = runTest { setupSuccessfulServiceCall("L") subject( clipId = clipId, currentReactionType = null, requestedReactionType = ReactionType.LIKE, recommendationMetadata = recommendationMetadata, ) val bodySlot = slot() coVerify { genService.updateReactionType(genId = clipId.value, body = capture(bodySlot)) } assertNotNull(bodySlot.captured.recommendationMetadata) assertEquals(hookId.value, bodySlot.captured.recommendationMetadata?.hookId) assertEquals(recommendationItemId.value, bodySlot.captured.recommendationMetadata?.recommendationItemId) assertEquals("hooks_feed", bodySlot.captured.recommendationMetadata?.contextType) } private fun setupSuccessfulServiceCall( reactionType: String?, ) { val remoteResponse = createRemoteClipReaction(reactionType) coEvery { genService.updateReactionType(any(), any()) } returns Either.Right(remoteResponse) } private fun verifyServiceCalledWithReaction( expectedReaction: RemoteUpdateReactionBody.Reaction?, ) { val bodySlot = slot() coVerify { genService.updateReactionType(clipId.value, capture(bodySlot)) } assertEquals(expectedReaction, bodySlot.captured.reaction) } private fun verifyServiceCalledWithNullReaction() { verifyServiceCalledWithReaction(null) } private fun verifyMediaReactionBroadcast( expectedReaction: ReactionType?, ) { val mediaReactionSlot = slot() coVerify { mediaMetadataManager.broadcastMediaReaction(capture(mediaReactionSlot)) } assertEquals(clipId, mediaReactionSlot.captured.clipId) assertEquals(expectedReaction, mediaReactionSlot.captured.reaction) } private fun createRemoteClipReaction( reactionType: String?, ): RemoteClipReaction = RemoteClipReaction( clip = "test-clip", flagged = false, flaggedReason = null, feedbackReason = null, playCount = 1, skipCount = 0, reactionType = reactionType, updatedAt = "2024-01-01T00:00:00Z", ) }