package com.suno.android.ui.screens import arrow.core.Either import com.suno.android.common_analytics.listening_source.ListeningSource import com.suno.android.common_analytics.listening_source.ListeningSourceCache import com.suno.android.common_analytics.managers.AnalyticsManager import com.suno.android.common_core_utils.Id import com.suno.android.common_core_utils.constants.SunoMediaType import com.suno.android.common_core_utils.environment.UserPrefsDataStoreManager import com.suno.android.common_core_utils.model.Url import com.suno.android.common_core_utils.model.UserHandle import com.suno.android.common_data.alerts.AlertsRepo import com.suno.android.common_data.alerts.InAppNotificationsState import com.suno.android.common_data.mappers.clips.ClipStatus import com.suno.android.common_data.mappers.clips.LocalClipData import com.suno.android.common_data.mappers.clips.SongListData import com.suno.android.common_data.repos.ClipsRepository import com.suno.android.common_mvi.MviProcessorFactory import com.suno.android.common_networking.remote.profiles.ProfilesService import com.suno.android.gating.statsig.DynamicConfigName import com.suno.android.gating.statsig.StatsigFeatureDataSource import com.suno.android.hooks.HooksFeatureGateManager import com.suno.android.media.MediaManager import com.suno.android.ui.screens.home.notifications.NotificationsScreenEffect import com.suno.android.ui.screens.home.notifications.NotificationsScreenEvent import com.suno.android.ui.screens.home.notifications.NotificationsScreenState import com.suno.android.ui.screens.home.notifications.NotificationsScreenVM import io.mockk.Runs import io.mockk.coEvery import io.mockk.every import io.mockk.just import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Before import org.junit.Test class NotificationsScreenVMTest { private val processorFactory = mockk() private val alertsRepo = mockk(relaxed = true) private val profilesService = mockk() private val mediaManager = mockk(relaxed = true) private val clipsRepository = mockk(relaxed = true) private val analyticsManager = mockk(relaxed = true) private val listeningSourceCache = mockk(relaxed = true) private val statsigManager = mockk() private val hooksFeatureGateManager = mockk() private val prefsDataStoreManager = mockk(relaxed = true) private lateinit var subject: NotificationsScreenVM @Before fun setUp() { Dispatchers.setMain(StandardTestDispatcher()) every { hooksFeatureGateManager.isHooksInAppNotificationsEnabled } returns false coEvery { alertsRepo.syncNotifications() } just Runs every { alertsRepo.inAppNotificationsStateFlow() } returns MutableStateFlow(InAppNotificationsState()) every { statsigManager.checkGate(any()) } returns false coEvery { statsigManager.fetchDynamicConfig(DynamicConfigName.NOTIFICATIONS_BANNER_CONFIG) } returns mockk(relaxed = true) { every { getDictionary(any(), any()) } returns null } every { processorFactory.create( any(), any(), any(), any(), ) } answers { MviProcessorFactory( analyticsManager = analyticsManager, statsigManager = statsigManager, loggerFactory = mockk(relaxed = true), ).create(firstArg(), secondArg(), thirdArg(), arg(3)) } subject = NotificationsScreenVM( processorFactory = processorFactory, alertsRepo = alertsRepo, profilesService = profilesService, mediaManager = mediaManager, clipsRepository = clipsRepository, analyticsManager = analyticsManager, listeningSourceCache = listeningSourceCache, statsigManager = statsigManager, hooksFeatureGateManager = hooksFeatureGateManager, prefsDataStoreManager = prefsDataStoreManager, ) } @After fun tearDown() { Dispatchers.resetMain() } @Test fun `given notification with ID when OnSongClicked then listening source stored in cache`() = runTest { val clipId = Id("clip-123") val notificationId = "notification-456" val mockClip = createMockClipData(clipId.value) coEvery { clipsRepository.getClipById(clipId) } returns Either.Right(mockClip) subject.sendEvent( NotificationsScreenEvent.OnSongClicked( clipId = clipId, notificationId = notificationId, enablePlayback = true, ), ) advanceUntilIdle() verify { listeningSourceCache.put( clipId = any(), listeningSource = match { source -> source is ListeningSource.NotificationSource && source.notificationId.value == notificationId }, ) } } @Test fun `given notification without ID when OnSongClicked then listening source not stored in cache`() = runTest { val clipId = Id("clip-789") val mockClip = createMockClipData(clipId.value) coEvery { clipsRepository.getClipById(clipId) } returns Either.Right(mockClip) subject.sendEvent( NotificationsScreenEvent.OnSongClicked( clipId = clipId, notificationId = null, enablePlayback = true, ), ) advanceUntilIdle() verify(exactly = 0) { listeningSourceCache.put(any(), any()) } } @Test fun `given OnSongClicked when media loads then sets single playing clip data`() = runTest { val clipId = Id("clip-complete-flow") val notificationId = "notification-complete" val mockClip = createMockClipData(clipId.value) coEvery { clipsRepository.getClipById(clipId) } returns Either.Right(mockClip) subject.sendEvent( NotificationsScreenEvent.OnSongClicked( clipId = clipId, notificationId = notificationId, enablePlayback = true, ), ) advanceUntilIdle() verify { mediaManager.setSinglePlayingClipData( localClipData = mockClip, enablePlayback = true, ) } } private fun createMockClipData( clipIdValue: String, ): LocalClipData = LocalClipData( clipId = Id(clipIdValue), mediaType = SunoMediaType.AUDIO, mediaUrl = Url("https://example.com/audio.mp3"), artistName = "Test Artist", artistUserId = Id("user-123"), handle = UserHandle("testuser"), artistAvatarUrl = Url("https://example.com/avatar.jpg"), nowPlayingTitle = "Test Song", albumImageUrl = Url("https://example.com/album.jpg"), videoCoverUrl = null, caption = "Test caption", isPublic = true, status = ClipStatus.Complete, upvoteCount = 10, reaction = null, modelName = "chirp-v3", majorModelVersion = "v3", prompt = null, gptPrompt = null, tags = null, displayTags = null, commentCount = 0, downloadDisabledReason = null, previewUrl = Url("https://example.com/preview.mp3"), playCount = 100, isFollowing = false, ) }