package com.suno.android.common_data.repos import arrow.core.Either import arrow.retrofit.adapter.either.networkhandling.CallError import arrow.retrofit.adapter.either.networkhandling.HttpError 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.SunoMediaType import com.suno.android.common_core_utils.model.Url import com.suno.android.common_core_utils.model.UserHandle import com.suno.android.common_data.mappers.clips.SongListData import com.suno.android.common_data.mappers.hooks.HooksFeed import com.suno.android.common_data.mappers.hooks.LocalHookData import com.suno.android.common_data.metadata.RecommendationMetadata import com.suno.android.common_networking.extensions.ApiResult import com.suno.android.common_networking.extensions.toThrowable import com.suno.android.common_networking.remote.entities.GenMetadataSchema import com.suno.android.common_networking.remote.entities.ProfileStatsSchema import com.suno.android.common_networking.remote.entities.RemoteHookReactionBody.Action import com.suno.android.common_networking.remote.entities.SimpleProfileInfoSchema import com.suno.android.common_networking.remote.entities.hooks.GeneratedClipSchema import com.suno.android.common_networking.remote.entities.hooks.HooksFeedSchema import com.suno.android.common_networking.remote.entities.hooks.RemoteHooksFeedBody import com.suno.android.common_networking.remote.entities.hooks.RemoteShareHookResponse import com.suno.android.common_networking.remote.entities.hooks.VideoHookSchema import com.suno.android.common_networking.remote.entities.hooks.VideoStreamingResolutionSchema import com.suno.android.common_networking.remote.hooks.HooksService import io.mockk.coEvery import io.mockk.coVerify import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Test class DefaultHooksRepositoryTest { private val mockLoggerFactory = mockk(relaxed = true) private val hooksService = mockk() private val subject = DefaultHooksRepository( loggerFactory = mockLoggerFactory, hooksService = hooksService, ) @Test fun `given successful service response when getHooksFeed called then maps response correctly`() = runTest { val mockVideoHook = createMockVideoHookSchema() val mockResponse = createMockHooksFeedSchema(listOf(mockVideoHook)) mockSuccessfulServiceCall(mockResponse) val result = subject.getHooksFeed(startIndex = 0) assertSuccessResult(result) { hooksFeed -> assertEquals(1, hooksFeed.hooks.size) val hook = hooksFeed.hooks[0] assertEquals(Id("hook-123"), hook.hookId) assertEquals("Test caption", hook.caption) assertEquals(Url("https://streaming.example.com/video.mp4"), hook.videoUrl) assertEquals(42, hook.likeCount) assertEquals(5, hook.commentCount) assertEquals(true, hook.currentUserLiked) assertEquals(UserHandle("testuser"), hook.creator.handle) assertEquals(Url("https://example.com/avatar.jpg"), hook.creator.avatarUrl) val clip = hook.clip assertEquals(Id("clip-789"), clip.clipId) assertEquals(SunoMediaType.AUDIO, clip.mediaType) assertEquals("Test Artist", clip.artistName) assertEquals("Test Song Title", clip.nowPlayingTitle) assertEquals(Url("https://example.com/album.jpg"), clip.albumImageUrl) assertEquals("chirp-v3", clip.modelName) assertNull(clip.reaction) assertEquals(1000, clip.playCount) } verifyServiceCalledWithCorrectSpec() } @Test fun `given multiple hooks when getHooksFeed called then maps all hooks correctly`() = runTest { val hooks = listOf( createMockVideoHookSchema(id = "hook-1", title = "Hook 1"), createMockVideoHookSchema(id = "hook-2", title = "Hook 2"), createMockVideoHookSchema(id = "hook-3", title = "Hook 3"), ) val mockResponse = createMockHooksFeedSchema(hooks) mockSuccessfulServiceCall(mockResponse) val result = subject.getHooksFeed(startIndex = 0) assertSuccessResult(result) { hooksFeed -> assertEquals(3, hooksFeed.hooks.size) assertEquals(Id("hook-1"), hooksFeed.hooks[0].hookId) assertEquals(Id("hook-2"), hooksFeed.hooks[1].hookId) assertEquals(Id("hook-3"), hooksFeed.hooks[2].hookId) } verifyServiceCalledWithCorrectSpec() } @Test fun `given video hook with streaming resolution when getHooksFeed called then uses streaming URL as media URL`() = runTest { val videoHook = createMockVideoHookSchema( videoStreamingResolutions = listOf( VideoStreamingResolutionSchema(720, "https://streaming.example.com/video.mp4"), ), renderedVideoUrl = "https://rendered.example.com/video.mp4", renderedVideoPreviewUrl = "https://preview.example.com/video.mp4", ) val mockResponse = createMockHooksFeedSchema(listOf(videoHook)) mockSuccessfulServiceCall(mockResponse) val result = subject.getHooksFeed(startIndex = 0) assertSuccessResult(result) { hooksFeed -> assertEquals(Url("https://streaming.example.com/video.mp4"), hooksFeed.hooks[0].videoUrl) } } @Test fun `given video hook with no streaming resolution when getHooksFeed called then falls back to rendered URL`() = runTest { val videoHook = createMockVideoHookSchema( videoStreamingResolutions = null, renderedVideoUrl = "https://rendered.example.com/video.mp4", renderedVideoPreviewUrl = "https://preview.example.com/video.mp4", ) val mockResponse = createMockHooksFeedSchema(listOf(videoHook)) mockSuccessfulServiceCall(mockResponse) val result = subject.getHooksFeed(startIndex = 0) assertSuccessResult(result) { hooksFeed -> assertEquals(Url("https://rendered.example.com/video.mp4"), hooksFeed.hooks[0].videoUrl) } } @Test fun `given video hook with no streaming or rendered URL when getHooksFeed called then falls back to preview URL`() = runTest { val videoHook = createMockVideoHookSchema( videoStreamingResolutions = null, renderedVideoUrl = null, renderedVideoPreviewUrl = "https://preview.example.com/video.mp4", ) val mockResponse = createMockHooksFeedSchema(listOf(videoHook)) mockSuccessfulServiceCall(mockResponse) val result = subject.getHooksFeed(startIndex = 0) assertSuccessResult(result) { hooksFeed -> assertEquals(Url("https://preview.example.com/video.mp4"), hooksFeed.hooks[0].videoUrl) } } @Test fun `given empty hooks feed when getHooksFeed called then returns empty list`() = runTest { val mockResponse = createMockHooksFeedSchema(emptyList()) mockSuccessfulServiceCall(mockResponse) val result = subject.getHooksFeed(startIndex = 0) assertSuccessResult(result) { hooksFeed -> assertEquals(0, hooksFeed.hooks.size) } } @Test fun `given error when getHooksFeed called then returns error`() = runTest { val error = HttpError(500, "Internal Server Error", "") mockServiceError(error) val result = subject.getHooksFeed(startIndex = 0) assertErrorResult(result, error) verifyServiceCalledWithCorrectSpec() } @Test fun `given action when setHookReaction called then sends same action`() = runTest { val hookId = Id("hook-123") mockSetHookReactionSuccess() val result = subject.setHookReaction( hookId = hookId, action = Action.Unlike, recommendationMetadata = mockk(relaxed = true), ) assertSuccessUnitResult(result) verifySetHookReactionCalledWithCorrectBody(hookId, Action.Unlike) } @Test fun `given error when setHookReaction called then returns error`() = runTest { val hookId = Id("hook-789") val error = HttpError(404, "Not Found", "") mockSetHookReactionError(error) val result = subject.setHookReaction( hookId = hookId, action = Action.Like, recommendationMetadata = mockk(relaxed = true), ) assertErrorResult(result, error) } @Test fun `given valid hook id when reportInappropriate called then creates report body and calls service`() = runTest { val hookId = Id("hook-123") coEvery { hooksService.reportHook(any(), any()) } returns Either.Right(Unit) val result = subject.reportInappropriate( hookId = hookId, recommendationMetadata = mockk(relaxed = true), ) assertSuccessUnitResult(result) coVerify { hooksService.reportHook( hookId.value, match { body -> body.reportReason == null }, ) } } @Test fun `given error when reportInappropriate called then returns error`() = runTest { val hookId = Id("hook-456") val error = HttpError(500, "Internal Server Error", "") coEvery { hooksService.reportHook(any(), any()) } returns Either.Left(error) val result = subject.reportInappropriate( hookId = hookId, recommendationMetadata = mockk(relaxed = true), ) assertErrorResult(result, error) } @Test fun `given successful service response when incrementShareCount called then returns success`() = runTest { val hookId = Id("hook-share-123") val recommendationMetadata = mockk(relaxed = true) val response = RemoteShareHookResponse(hookId = "hook-share-123", success = true) coEvery { hooksService.incrementShareCount(any(), any()) } returns Either.Right(response) val result = subject.incrementShareCount( hookId = hookId, recommendationMetadata = recommendationMetadata, ) assertSuccessUnitResult(result) coVerify(exactly = 1) { hooksService.incrementShareCount(hookId = "hook-share-123", body = any()) } } @Test fun `given service response with success false when incrementShareCount called then returns error`() = runTest { val hookId = Id("hook-share-456") val recommendationMetadata = mockk(relaxed = true) val response = RemoteShareHookResponse(hookId = "hook-share-456", success = false) coEvery { hooksService.incrementShareCount(any(), any()) } returns Either.Right(response) val result = subject.incrementShareCount( hookId = hookId, recommendationMetadata = recommendationMetadata, ) assertErrorResult(result, UnexpectedCallError(Throwable("Error incrementing hook share count"))) coVerify(exactly = 1) { hooksService.incrementShareCount(hookId = "hook-share-456", body = any()) } } @Test fun `given network error when incrementShareCount called then returns error`() = runTest { val hookId = Id("hook-share-789") val recommendationMetadata = mockk(relaxed = true) val error = HttpError(500, "Internal Server Error", "") coEvery { hooksService.incrementShareCount(any(), any()) } returns Either.Left(error) val result = subject.incrementShareCount( hookId = hookId, recommendationMetadata = recommendationMetadata, ) assertErrorResult(result, error) coVerify(exactly = 1) { hooksService.incrementShareCount(hookId = "hook-share-789", body = any()) } } private fun createMockVideoHookSchema( id: String = "hook-123", title: String = "Test Hook", caption: String = "Test caption", user: SimpleProfileInfoSchema? = createMockSimpleProfileInfoSchema(), clip: GeneratedClipSchema = createMockGeneratedClipSchema(), videoStreamingResolutions: List? = listOf( VideoStreamingResolutionSchema(720, "https://streaming.example.com/video.mp4"), ), renderedVideoUrl: String? = "https://rendered.example.com/video.mp4", renderedVideoPreviewUrl: String? = "https://preview.example.com/video.mp4", userId: Int = 456, likeCount: Int = 42, status: String = "complete", currentUserLiked: Boolean = true, commentCount: Int = 5, ): VideoHookSchema = VideoHookSchema( id = id, title = title, caption = caption, user = user, clip = clip, startClipTimestamp = 0.0, endClipTimestamp = 30.0, videoStreamingResolutions = videoStreamingResolutions, renderedVideoUrl = renderedVideoUrl, renderedVideoPreviewUrl = renderedVideoPreviewUrl, userId = userId, originalClipId = "", createdAt = "2024-01-01T00:00:00Z", updatedAt = "2024-01-01T00:00:00Z", thumbnailImageUrl = "https://example.com/thumbnail.jpg", likeCount = likeCount, status = status, allowComments = true, currentUserLiked = currentUserLiked, commentCount = commentCount, currentUserFollowsCreator = false, recommendationItemId = null, renderedVideoS3Id = null, renderedVideoPreviewS3Id = null, thumbnailImageS3Id = null, showLyrics = true, viewCount = 500, videoDuration = 30.0, humanRating = null, lyricDisplay = null, ) private fun createMockSimpleProfileInfoSchema( userId: Int = 456, handle: String? = "testuser", avatarImageUrl: String? = "https://example.com/avatar.jpg", ): SimpleProfileInfoSchema = SimpleProfileInfoSchema( userId = userId, externalUserId = "external-456", stats = ProfileStatsSchema( followersCount = 100, likesCount = 25, clipsCount = 50, ), displayName = "Test User", handle = handle, avatarImageUrl = avatarImageUrl, isFollowing = false, ) private fun createMockGeneratedClipSchema( id: String = "clip-789", title: String = "Test Song Title", imageLargeUrl: String? = "https://example.com/album.jpg", displayName: String? = "Test Artist", playCount: Int = 1000, modelName: String = "chirp-v3", ): GeneratedClipSchema = GeneratedClipSchema( id = id, title = title, imageLargeUrl = imageLargeUrl, audioUrl = "https://example.com/audio.mp3", displayName = displayName, playCount = playCount, majorModelVersion = "v3", modelName = modelName, isLiked = false, isHandleUpdated = false, isTrashed = false, createdAt = "2024-01-01T00:00:00Z", status = "complete", handle = null, avatarImageUrl = null, userId = "456", commentCount = 0, upvoteCount = 0, metadata = GenMetadataSchema(), ) private fun createMockHooksFeedSchema( items: List, ): HooksFeedSchema = HooksFeedSchema(items = items) private fun mockSuccessfulServiceCall( response: HooksFeedSchema, ) { coEvery { hooksService.getHooksFeed(any()) } returns Either.Right(response) } private fun mockServiceError( error: CallError, ) { coEvery { hooksService.getHooksFeed(any()) } returns Either.Left(error) } private fun verifyServiceCalledWithCorrectSpec() { coVerify { hooksService.getHooksFeed(RemoteHooksFeedBody(startIndex = 0, pageSize = 10)) } } private fun assertSuccessResult( result: ApiResult, assertion: (HooksFeed) -> Unit, ) { when (result) { is Either.Right -> assertion(result.value) is Either.Left -> throw AssertionError("Expected success but got error: ${result.value}") } } private fun mockSetHookReactionSuccess() { coEvery { hooksService.setHookReaction(any(), any()) } returns Either.Right(Unit) } private fun mockSetHookReactionError( error: CallError, ) { coEvery { hooksService.setHookReaction(any(), any()) } returns Either.Left(error) } private fun verifySetHookReactionCalledWithCorrectBody( hookId: Id, expectedAction: Action, ) { coVerify { hooksService.setHookReaction( hookId = hookId.value, body = match { it.action == expectedAction }, ) } } private fun assertSuccessUnitResult( result: ApiResult, ) { when (result) { is Either.Right -> assertEquals(Unit, result.value) is Either.Left -> throw AssertionError("Expected success but got error: ${result.value}") } } private fun assertErrorResult( result: ApiResult<*>, expectedError: CallError, ) { when (result) { is Either.Left -> { assertEquals(expectedError::class.java, result.value::class.java) assertEquals(expectedError.toThrowable().message, result.value.toThrowable().message) } is Either.Right -> throw AssertionError("Expected error but got success") } } }