package com.github.damontecres.wholphin import android.content.Context import android.content.Intent import android.widget.Toast import androidx.datastore.core.DataStore import com.github.damontecres.wholphin.data.CurrentUser import com.github.damontecres.wholphin.data.RestoredSession import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.update import com.github.damontecres.wholphin.services.AppUpgradeHandler import com.github.damontecres.wholphin.services.BackdropService import com.github.damontecres.wholphin.services.DeviceProfileService import com.github.damontecres.wholphin.services.IntentResult import com.github.damontecres.wholphin.services.IntentService import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.SetupDestination import com.github.damontecres.wholphin.services.SetupNavigationManager import com.github.damontecres.wholphin.test.currentUser import com.github.damontecres.wholphin.test.server import com.github.damontecres.wholphin.test.user import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.util.WholphinDispatchers import com.github.damontecres.wholphin.util.configure import com.github.damontecres.wholphin.util.reset import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.slot import io.mockk.unmockkStatic import io.mockk.verify import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.runTest import org.jellyfin.sdk.model.UUID import org.junit.After import org.junit.Assert import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test @OptIn(ExperimentalCoroutinesApi::class) class TestMainActivityViewModel { private val testDispatcher = StandardTestDispatcher() private val context: Context = mockk() private val preferences: DataStore = mockk() private val serverRepository: ServerRepository = mockk() private val setupNavigationManager: SetupNavigationManager = mockk(relaxed = true) private val navigationManager: NavigationManager = mockk(relaxed = true) private val deviceProfileService: DeviceProfileService = mockk() private val backdropService: BackdropService = mockk(relaxed = true) private val appUpgradeHandler: AppUpgradeHandler = mockk() private val intentService: IntentService = mockk() val viewModel = MainActivityViewModel( context, preferences, serverRepository, setupNavigationManager, navigationManager, deviceProfileService, backdropService, appUpgradeHandler, intentService, ) private val serverId = UUID.randomUUID() private val server = server(serverId) private val userId = UUID.randomUUID() private val currentUser = currentUser(serverId, userId) private val protectedCurrentUser = CurrentUser( server(serverId), user(serverId, userId).copy(pin = "1234"), ) @Before fun setUp() { WholphinDispatchers.configure(testDispatcher) every { appUpgradeHandler.needUpgrade() } returns false every { appUpgradeHandler.copySubfont(any()) } returns Unit mockkStatic(Toast::class) val mockToast = mockk() every { Toast.makeText(any(), any(), any()) } returns mockToast every { mockToast.show() } returns Unit } @After fun tearDown() { WholphinDispatchers.reset() unmockkStatic(Toast::class) } private fun setupPreferences(block: AppPreferences.Builder.() -> Unit) { every { preferences.data } returns flow { emit( AppPreferences.getDefaultInstance().update(block), ) } } @Test fun `Test auto sign in with unprotected profile`() = runTest(testDispatcher) { setupPreferences { signInAutomatically = true } every { serverRepository.current } returns MutableStateFlow(currentUser) viewModel.handleAppStart() val slot = slot() verify { setupNavigationManager.navigateTo(capture(slot)) } slot.captured.let { destination -> Assert.assertTrue( "destination is ${destination::class}", destination is SetupDestination.AppContent, ) destination as SetupDestination.AppContent Assert.assertEquals(currentUser, destination.current) } coVerify(exactly = 0) { serverRepository.restoreSavedSession() } coVerify(exactly = 0) { serverRepository.getMostRecentServer() } } @Test fun `Test auto sign disabled in with current server`() = runTest { setupPreferences { signInAutomatically = false } every { serverRepository.current } returns MutableStateFlow(currentUser) // coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser coEvery { serverRepository.getMostRecentServer() } returns RestoredSession.ServerOnly(server) viewModel.handleAppStart() coVerify(exactly = 0) { serverRepository.tryChangeUser(any(), any()) } coVerify(exactly = 0) { serverRepository.restoreSavedSession() } coVerify(exactly = 1) { serverRepository.getMostRecentServer() } val args = mutableListOf() verify { setupNavigationManager.navigateTo(capture(args)) } Assert.assertEquals(1, args.size) args[0].let { destination -> Assert.assertTrue( "destination is ${destination::class}", destination is SetupDestination.UserList, ) destination as SetupDestination.UserList Assert.assertEquals(currentUser.server, destination.server) } } @Test fun savedColdLaunchNavigatesBeforeSessionRevalidationCompletes() = runTest(testDispatcher) { setupPreferences { signInAutomatically = true } every { serverRepository.current } returns MutableStateFlow(null) coEvery { serverRepository.restoreSavedSession() } returns RestoredSession.Success(currentUser) val release = CompletableDeferred() coEvery { serverRepository.revalidateSession(currentUser) } coAnswers { release.await() } viewModel.handleAppStart() runCurrent() verify { setupNavigationManager.navigateTo(SetupDestination.AppContent(currentUser)) } assertFalse(release.isCompleted) release.complete(Unit) } @Test fun `Test auto sign in, hot load, with protected profile`() = runTest { setupPreferences { signInAutomatically = true } every { serverRepository.current } returns MutableStateFlow( protectedCurrentUser, ) coEvery { serverRepository.tryChangeUser(serverId, userId) } returns null coEvery { serverRepository.restoreSavedSession() } returns RestoredSession.ServerOnly(server) viewModel.handleAppStart() coVerify(exactly = 0) { serverRepository.tryChangeUser(serverId, userId) } val args = mutableListOf() verify { setupNavigationManager.navigateTo(capture(args)) } Assert.assertEquals(1, args.size) args[0].let { destination -> Assert.assertTrue( "destination is ${destination::class}", destination is SetupDestination.UserList, ) destination as SetupDestination.UserList Assert.assertEquals(currentUser.server, destination.server) } } @Test fun `Test auto sign in, cold load, with protected profile`() = runTest { setupPreferences { signInAutomatically = true } every { serverRepository.current } returns MutableStateFlow(null) coEvery { serverRepository.restoreSavedSession() } returns RestoredSession.ServerOnly(server) coEvery { serverRepository.tryChangeUser(serverId, userId) } returns null viewModel.handleAppStart() coVerify(exactly = 1) { serverRepository.restoreSavedSession() } val slot = slot() verify { setupNavigationManager.navigateTo(capture(slot)) } slot.captured.let { destination -> Assert.assertTrue( "destination is ${destination::class}", destination is SetupDestination.UserList, ) destination as SetupDestination.UserList Assert.assertEquals(currentUser.server, destination.server) } } @Test fun `Test auto sign in, no current user`() = runTest { setupPreferences { signInAutomatically = true } every { serverRepository.current } returns MutableStateFlow(null) coEvery { serverRepository.restoreSavedSession() } returns RestoredSession.None viewModel.handleAppStart() coVerify(exactly = 1) { serverRepository.restoreSavedSession() } val slot = slot() verify { setupNavigationManager.navigateTo(capture(slot)) } slot.captured.let { destination -> Assert.assertTrue( "destination is ${destination::class}", destination is SetupDestination.ServerList, ) } } @Test fun `Test intent result is noop, hot reload`() = runTest(testDispatcher) { setupPreferences { signInAutomatically = true } every { serverRepository.current } returns MutableStateFlow(currentUser) coEvery { serverRepository.restoreSavedSession() } returns RestoredSession.Success(currentUser) coEvery { intentService.parseIntent(any()) } returns IntentResult.NoOp val result = viewModel.handleIntent(Intent()) assertFalse(result) // val slot = slot() // verify { setupNavigationManager.navigateTo(capture(slot)) } // slot.captured.let { destination -> // Assert.assertTrue( // "destination is ${destination::class}", // destination is SetupDestination.AppContent, // ) // destination as SetupDestination.AppContent // Assert.assertEquals(currentUser, destination.current) // } coVerify(exactly = 0) { serverRepository.restoreSavedSession() } coVerify(exactly = 0) { serverRepository.getMostRecentServer() } } @Test fun `Test intent result is error, has server`() = runTest(testDispatcher) { setupPreferences { signInAutomatically = true } every { serverRepository.current } returns MutableStateFlow(currentUser) coEvery { intentService.parseIntent(any()) } returns IntentResult.Error("Error") val result = viewModel.handleIntent(Intent()) assertTrue(result) val slot = slot() verify { setupNavigationManager.navigateTo(capture(slot)) } slot.captured.let { destination -> Assert.assertTrue( "destination is ${destination::class}", destination is SetupDestination.UserList, ) destination as SetupDestination.UserList Assert.assertEquals(currentUser.server, destination.server) } } @Test fun `Test intent result is error, does not have server`() = runTest(testDispatcher) { setupPreferences { signInAutomatically = true } every { serverRepository.current } returns MutableStateFlow(null) coEvery { intentService.parseIntent(any()) } returns IntentResult.Error("Error") val result = viewModel.handleIntent(Intent()) assertTrue(result) val slot = slot() verify { setupNavigationManager.navigateTo(capture(slot)) } slot.captured.let { destination -> Assert.assertTrue( "destination is ${destination::class}", destination is SetupDestination.ServerList, ) } } @Test fun `Test intent result is target, has server`() = runTest(testDispatcher) { setupPreferences { signInAutomatically = true } every { serverRepository.current } returns MutableStateFlow(currentUser) // coEvery { serverRepository.tryRestoreSession(serverId, userId) } returns currentUser coEvery { intentService.parseIntent(any()) } returns IntentResult.Target( destinations = listOf(Destination.Favorites), addHomeToBackStack = true, ) val backstack = mutableListOf() every { navigationManager.backStack } returns backstack val result = viewModel.handleIntent(Intent()) assertTrue(result) val slot = slot() verify { setupNavigationManager.navigateTo(capture(slot)) } verify { navigationManager.reloadHome() } slot.captured.let { destination -> Assert.assertTrue( "destination is ${destination::class}", destination is SetupDestination.AppContent, ) destination as SetupDestination.AppContent Assert.assertEquals(currentUser, destination.current) } // Note: navigation manager internally handles Home destination, so it won't be in this list Assert.assertEquals(1, backstack.size) backstack[0].let { destination -> Assert.assertTrue( "destination is ${destination::class}", destination is Destination.Favorites, ) } } @Test fun `Test intent result is target, has server, no home backstack`() = runTest(testDispatcher) { setupPreferences { signInAutomatically = true } every { serverRepository.current } returns MutableStateFlow(currentUser) // coEvery { serverRepository.tryRestoreSession(serverId, userId) } returns currentUser coEvery { intentService.parseIntent(any()) } returns IntentResult.Target( destinations = listOf(Destination.Favorites), addHomeToBackStack = false, ) val backstack = mutableListOf() every { navigationManager.backStack } returns backstack val result = viewModel.handleIntent(Intent()) assertTrue(result) val slot = slot() verify { setupNavigationManager.navigateTo(capture(slot)) } verify(exactly = 0) { navigationManager.reloadHome() } slot.captured.let { destination -> Assert.assertTrue( "destination is ${destination::class}", destination is SetupDestination.AppContent, ) destination as SetupDestination.AppContent Assert.assertEquals(currentUser, destination.current) } Assert.assertEquals(1, backstack.size) backstack[0].let { destination -> Assert.assertTrue( "destination is ${destination::class}", destination is Destination.Favorites, ) } } @Test fun `Test intent result is target, has no server`() = runTest(testDispatcher) { setupPreferences { signInAutomatically = true } every { serverRepository.current } returns MutableStateFlow(null) // coEvery { serverRepository.tryRestoreSession(serverId, userId) } returns currentUser coEvery { intentService.parseIntent(any()) } returns IntentResult.Target( destinations = listOf(Destination.Favorites), addHomeToBackStack = true, ) val backstack = mutableListOf() every { navigationManager.backStack } returns backstack val result = viewModel.handleIntent(Intent()) assertTrue(result) val slot = slot() verify { setupNavigationManager.navigateTo(capture(slot)) } verify(exactly = 0) { navigationManager.reloadHome() } slot.captured.let { destination -> Assert.assertTrue( "destination is ${destination::class}", destination is SetupDestination.ServerList, ) } } }