package com.suno.android.common_data.billing import arrow.core.Either import arrow.retrofit.adapter.either.networkhandling.UnexpectedCallError import com.suno.android.common_core_utils.SunoLogger import com.suno.android.common_core_utils.environment.UserPrefsDataStoreManager import com.suno.android.common_networking.cms.CMSService import com.suno.android.common_networking.remote.billing.BillingService import com.suno.android.common_networking.remote.entities.SubscriptionInfoResponse import com.suno.android.common_networking.remote.session.Model import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test @OptIn(ExperimentalCoroutinesApi::class) class SunoBillingRepoTest { private val mockSubscriptionResponse = SubscriptionInfoResponse( subscriptionPlatform = "android", isActive = true, isPastDue = false, credits = 100, subscriptionType = true, renewsOn = "2024-01-01", cancelOn = null, period = "monthly", changingTo = null, monthlyUsage = 50, monthlyLimit = 500, creditPacks = emptyList(), models = emptyList(), plan = null, plans = emptyList(), totalCreditsLeft = 100, freePersonaClipsRemaining = 5, freeCoverClipsRemaining = 3, freeMobileRemastersRemaining = 2, freeMobileV4GensRemaining = 10, revcatSubscriptionsOfferingId = "offering_id", ) private val mockLoggerFactory = mockk(relaxed = true) private val billingService = mockk() private val cmsService = mockk() private val userPrefsDataStoreManager = mockk(relaxed = true) private val subject = SunoBillingRepoImpl( loggerFactory = mockLoggerFactory, billingService = billingService, cmsService = cmsService, userPrefsDataStoreManager = userPrefsDataStoreManager, ) @Before fun setUp() { Dispatchers.setMain(StandardTestDispatcher()) } @After fun tearDown() { Dispatchers.resetMain() } @Test fun `given new repository when created then billing state is null`() = runTest { val initialState = subject.billingStateFlow().value assertNull(initialState) } @Test fun `given successful refresh when accessing billing state flow then returns current state`() = runTest { coEvery { billingService.getSubscriptionInfo() } returns Either.Right(mockSubscriptionResponse) subject.refreshBillingState() val stateFlow = subject.billingStateFlow() assertEquals(mockSubscriptionResponse, stateFlow.value) } @Test fun `given successful billing service when refreshing billing state then returns true`() = runTest { coEvery { billingService.getSubscriptionInfo() } returns Either.Right(mockSubscriptionResponse) val result = subject.refreshBillingState() assertTrue(result) } @Test fun `given failed billing service when refreshing billing state then returns false`() = runTest { val error = UnexpectedCallError(RuntimeException("Network error")) coEvery { billingService.getSubscriptionInfo() } returns Either.Left(error) val result = subject.refreshBillingState() assertFalse(result) } @Test fun `given failed billing service when refreshing billing state then does not update state flow`() = runTest { val error = UnexpectedCallError(RuntimeException("Network error")) coEvery { billingService.getSubscriptionInfo() } returns Either.Left(error) subject.refreshBillingState() assertNull(subject.billingStateFlow().value) } @Test fun `given successful refresh when using refresh flow then emits true`() = runTest { coEvery { billingService.getSubscriptionInfo() } returns Either.Right(mockSubscriptionResponse) val result = subject.refreshBillingStateFlow().first() assertTrue(result) } @Test fun `given failed refresh when using refresh flow then emits false`() = runTest { val error = UnexpectedCallError(RuntimeException("Network error")) coEvery { billingService.getSubscriptionInfo() } returns Either.Left(error) val result = subject.refreshBillingStateFlow().first() assertFalse(result) } @Test fun `given refresh flow when not collected then does not execute until collection`() = runTest { coEvery { billingService.getSubscriptionInfo() } returns Either.Right(mockSubscriptionResponse) val flow = subject.refreshBillingStateFlow() coVerify(exactly = 0) { billingService.getSubscriptionInfo() } flow.first() coVerify(exactly = 1) { billingService.getSubscriptionInfo() } } @Test fun `given multiple refresh calls when executed sequentially then maintains state consistency`() = runTest { val firstResponse = mockSubscriptionResponse.copy(credits = 100) val secondResponse = mockSubscriptionResponse.copy(credits = 200) coEvery { billingService.getSubscriptionInfo() } returns Either.Right(firstResponse) subject.refreshBillingState() assertEquals(firstResponse, subject.billingStateFlow().value) coEvery { billingService.getSubscriptionInfo() } returns Either.Right(secondResponse) subject.refreshBillingState() assertEquals(secondResponse, subject.billingStateFlow().value) } @Test fun `given concurrent refresh operations when executed then handles correctly`() = runTest { coEvery { billingService.getSubscriptionInfo() } returns Either.Right(mockSubscriptionResponse) val result1 = subject.refreshBillingState() val result2 = subject.refreshBillingState() assertTrue(result1) assertTrue(result2) assertEquals(mockSubscriptionResponse, subject.billingStateFlow().value) } @Test fun `given subscription response with null values when processed then handles correctly`() = runTest { val responseWithNulls = mockSubscriptionResponse.copy( subscriptionPlatform = null, renewsOn = null, cancelOn = null, changingTo = null, plan = null, freePersonaClipsRemaining = null, freeCoverClipsRemaining = null, freeMobileRemastersRemaining = null, freeMobileV4GensRemaining = null, revcatSubscriptionsOfferingId = null, audioUploadLimits = null, ) coEvery { billingService.getSubscriptionInfo() } returns Either.Right(responseWithNulls) val result = subject.refreshBillingState() assertTrue(result) assertEquals(responseWithNulls, subject.billingStateFlow().value) } @Test fun `given inactive subscription response when processed then handles correctly`() = runTest { val inactiveResponse = mockSubscriptionResponse.copy( isActive = false, isPastDue = true, credits = 0, totalCreditsLeft = 0, ) coEvery { billingService.getSubscriptionInfo() } returns Either.Right(inactiveResponse) val result = subject.refreshBillingState() assertTrue(result) assertEquals(inactiveResponse, subject.billingStateFlow().value) } @Test fun `given successful refresh then calls setSelectedModelIfEmpty with default model`() = runTest { val defaultModelName = "v4.5-all" val model = mockk(relaxed = true) { every { name } returns defaultModelName every { isDefaultModel } returns true every { externalKey } returns "chirp-auk-turbo" } val response = mockSubscriptionResponse.copy(models = listOf(model)) coEvery { billingService.getSubscriptionInfo() } returns Either.Right(response) coEvery { userPrefsDataStoreManager.setSelectedModelIfEmpty(any()) } returns Unit subject.refreshBillingState() coVerify { userPrefsDataStoreManager.setSelectedModelIfEmpty(selectedModel = defaultModelName) } } @Test fun `given successful refresh with no default model then calls setSelectedModelIfEmpty with fallback`() = runTest { val model = mockk(relaxed = true) { every { name } returns "chirp-v3-5" every { isDefaultModel } returns false } val response = mockSubscriptionResponse.copy(models = listOf(model)) coEvery { billingService.getSubscriptionInfo() } returns Either.Right(response) coEvery { userPrefsDataStoreManager.setSelectedModelIfEmpty(any()) } returns Unit subject.refreshBillingState() coVerify { userPrefsDataStoreManager.setSelectedModelIfEmpty(selectedModel = "v4.5-all") } } @Test fun `given failed refresh then calls setSelectedModelIfEmpty with fallback model`() = runTest { val error = UnexpectedCallError(RuntimeException("Network error")) coEvery { billingService.getSubscriptionInfo() } returns Either.Left(error) coEvery { userPrefsDataStoreManager.setSelectedModelIfEmpty(any()) } returns Unit subject.refreshBillingState() coVerify { userPrefsDataStoreManager.setSelectedModelIfEmpty(selectedModel = "v4.5-all") } } }