package com.suno.android.common_data.alerts import arrow.core.Either import arrow.retrofit.adapter.either.networkhandling.UnexpectedCallError import com.suno.android.common_core_utils.SunoLogger import com.suno.android.common_networking.remote.entities.NotificationsInner import com.suno.android.common_networking.remote.entities.ReadNotificationsSpec import com.suno.android.common_networking.remote.entities.UserNotificationSchema import com.suno.android.common_networking.remote.notification.NotificationService import io.mockk.coEvery import io.mockk.coVerify import io.mockk.mockk import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Test class AlertsRepoTest { private val loggerFactory = mockk(relaxed = true) private val notificationService = mockk() private val subject = AlertsRepoImpl( loggerFactory = loggerFactory, notificationService = notificationService, ) @Test fun `given initial state when accessing inAppNotificationsStateFlow then returns empty state`() = runTest { val initialState = getCurrentState() assertEmptyState(initialState) } @Test fun `given new notifications when syncNotifications called then updates state with notifications`() = runTest { val mockNotifications = listOf( createMockNotification(id = "1", isRead = false), createMockNotification(id = "2", isRead = true), ) val mockResponse = createMockResponse(notifications = mockNotifications) mockSuccessfulSync(mockResponse) subject.syncNotifications() val state = getCurrentState() assertStateHasNotifications(state, expectedSize = 2, hasUnread = true) assertEquals("1", state.notifications?.get(0)?.id) assertEquals("2", state.notifications?.get(1)?.id) } @Test fun `given existing notifications when syncNotifications called then appends new notifications`() = runTest { val firstNotifications = listOf( createMockNotification(id = "1", isRead = true), ) val firstResponse = createMockResponse(notifications = firstNotifications) mockSuccessfulSync(firstResponse) subject.syncNotifications() val secondNotifications = listOf( createMockNotification(id = "2", isRead = false), ) val secondResponse = createMockResponse( notifiedAt = "2024-01-01T01:00:00Z", notifications = secondNotifications, ) mockSuccessfulSync(secondResponse) subject.syncNotifications() val state = getCurrentState() assertStateHasNotifications(state, expectedSize = 2, hasUnread = true) assertEquals("1", state.notifications?.get(0)?.id) assertEquals("2", state.notifications?.get(1)?.id) } @Test fun `given all read notifications when syncNotifications called then sets hasUnreadNotifications to false`() = runTest { val mockNotifications = listOf( createMockNotification(id = "1", isRead = true), createMockNotification(id = "2", isRead = true), ) val mockResponse = createMockResponse(notifications = mockNotifications) mockSuccessfulSync(mockResponse) subject.syncNotifications() val state = getCurrentState() assertStateHasNotifications(state, expectedSize = 2, hasUnread = false) } @Test fun `given API error when syncNotifications called then handles error gracefully`() = runTest { mockFailedSync() subject.syncNotifications() val state = getCurrentState() assertEmptyState(state) } @Test fun `given empty notifications list when syncNotifications called then handles empty list correctly`() = runTest { val mockResponse = createMockResponse(notifications = emptyList()) mockSuccessfulSync(mockResponse) subject.syncNotifications() val state = getCurrentState() assertStateHasNotifications(state, expectedSize = 0, hasUnread = false) } @Test fun `given notification ids when markNotificationsAsRead called then calls service with correct parameters`() = runTest { val idsList = listOf("1", "2", "3") mockSuccessfulMarkAsRead() subject.markNotificationsAsRead(idsList) coVerify { notificationService.markNotificationsAsRead( ReadNotificationsSpec(ids = idsList), ) } } @Test fun `given API error when markNotificationsAsRead called then handles error gracefully`() = runTest { val idsList = listOf("1", "2", "3") mockFailedMarkAsRead() subject.markNotificationsAsRead(idsList) coVerify { notificationService.markNotificationsAsRead( ReadNotificationsSpec(ids = idsList), ) } } @Test fun `given notifications exist when markNotificationsAsRead succeeds then updates local state to read`() = runTest { val initialNotifications = listOf( createMockNotification(id = "1", isRead = false), createMockNotification(id = "2", isRead = false), createMockNotification(id = "3", isRead = true), ) val mockResponse = createMockResponse(notifications = initialNotifications) mockSuccessfulSync(mockResponse) subject.syncNotifications() val initialState = getCurrentState() assertStateHasNotifications(initialState, expectedSize = 3, hasUnread = true) mockSuccessfulMarkAsRead() subject.markNotificationsAsRead(listOf("1", "2")) val updatedState = getCurrentState() val notification1 = updatedState.notifications?.find { it.id == "1" } val notification2 = updatedState.notifications?.find { it.id == "2" } val notification3 = updatedState.notifications?.find { it.id == "3" } assertTrue(notification1?.isRead == true) assertEquals(true, notification2?.isRead) assertEquals(true, notification3?.isRead) assertFalse(updatedState.hasUnreadNotifications) } @Test fun `given initial state when syncNotifications called then uses correct afterDatetimeUtc parameter`() = runTest { val mockResponse = createMockResponse() mockSuccessfulSync(mockResponse) subject.syncNotifications() coVerify { notificationService.getNotifications("1970-01-01T00:00:00.000Z") } } @Test fun `given successful sync when syncNotifications called then updates lastSyncTime for next call`() = runTest { val newTimestamp = "2024-01-01T12:00:00Z" val mockResponse = createMockResponse(notifiedAt = newTimestamp) mockSuccessfulSync(mockResponse) subject.syncNotifications() mockSuccessfulSync(mockResponse) subject.syncNotifications() coVerify { notificationService.getNotifications(newTimestamp) } } private fun createMockNotification( id: String, isRead: Boolean = false, notificationType: String = "test_type", ): NotificationsInner = NotificationsInner( id = id, priority = 1, updatedAt = "2024-01-01T00:00:00Z", isRead = isRead, notificationType = notificationType, userProfiles = null, totalUsers = null, contentId = null, contentTitle = null, contentImageUrl = null, contentMessage = null, ) private fun createMockResponse( notifiedAt: String = "2024-01-01T00:00:00Z", notifications: List = emptyList(), ): UserNotificationSchema = UserNotificationSchema( notifiedAt = notifiedAt, notifications = notifications, ) private fun mockSuccessfulSync( response: UserNotificationSchema, ) { coEvery { notificationService.getNotifications(any()) } returns Either.Right(response) } private fun mockFailedSync( error: UnexpectedCallError = UnexpectedCallError(Throwable()), ) { coEvery { notificationService.getNotifications(any()) } returns Either.Left(error) } private fun mockSuccessfulMarkAsRead() { coEvery { notificationService.markNotificationsAsRead(any()) } returns Either.Right(Unit) } private fun mockFailedMarkAsRead( error: UnexpectedCallError = UnexpectedCallError(Throwable()), ) { coEvery { notificationService.markNotificationsAsRead(any()) } returns Either.Left(error) } private suspend fun getCurrentState(): InAppNotificationsState = subject.inAppNotificationsStateFlow().first() private fun assertEmptyState( state: InAppNotificationsState, ) { assertNull(state.notifications) assertFalse(state.hasUnreadNotifications) } private fun assertStateHasNotifications( state: InAppNotificationsState, expectedSize: Int, hasUnread: Boolean, ) { assertEquals(expectedSize, state.notifications?.size) assertEquals(hasUnread, state.hasUnreadNotifications) } }