package com.craigvg.lichun_android.viewmodel import com.craigvg.lichun_android.domain.models.Person import com.craigvg.lichun_android.domain.models.Player import com.craigvg.lichun_android.managers.AnalyticsManager import com.craigvg.lichun_android.managers.SoundManager import com.craigvg.lichun_android.managers.ToastManager import com.craigvg.lichun_android.managers.TooltipManager import com.craigvg.lichun_android.network.WebSocketManager import io.mockk.every import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow 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.assertTrue import org.junit.Before import org.junit.Test @OptIn(ExperimentalCoroutinesApi::class) class GameStateViewModelTest { private lateinit var webSocketManager: WebSocketManager private lateinit var soundManager: SoundManager private lateinit var analyticsManager: AnalyticsManager private lateinit var toastManager: ToastManager private lateinit var tooltipManager: TooltipManager private lateinit var viewModel: GameStateViewModel private val testDispatcher = StandardTestDispatcher() private val playerFlow = MutableStateFlow(Player()) private val personFlow = MutableStateFlow(Person()) private val isConnectedFlow = MutableStateFlow(false) private val appLoadedFlow = MutableStateFlow(false) @Before fun setup() { Dispatchers.setMain(testDispatcher) webSocketManager = mockk(relaxed = true) soundManager = mockk(relaxed = true) analyticsManager = mockk(relaxed = true) toastManager = mockk(relaxed = true) tooltipManager = mockk(relaxed = true) every { webSocketManager.player } returns playerFlow every { webSocketManager.person } returns personFlow every { webSocketManager.isConnected } returns isConnectedFlow every { webSocketManager.appLoaded } returns appLoadedFlow every { webSocketManager.lifeEvents } returns MutableStateFlow(emptyList()) every { webSocketManager.unclaimedEventCount } returns MutableStateFlow(0) every { webSocketManager.currentQuestion } returns MutableStateFlow(null) every { webSocketManager.currentMessageEvent } returns MutableStateFlow(null) every { webSocketManager.currentRelationshipEvent } returns MutableStateFlow(null) every { webSocketManager.currentError } returns MutableStateFlow(null) every { webSocketManager.showEnergyRefillModal } returns MutableStateFlow(false) every { webSocketManager.showTimeSkipModal } returns MutableStateFlow(false) every { webSocketManager.showTimeSkipSummary } returns MutableStateFlow(false) every { webSocketManager.energyRefillTiers } returns MutableStateFlow(emptyList()) every { webSocketManager.timeSkipTiers } returns MutableStateFlow(emptyList()) every { webSocketManager.lastTimeSkipSummary } returns MutableStateFlow(null) every { webSocketManager.achievements } returns MutableStateFlow(emptyList()) every { webSocketManager.showAchievementUnlock } returns MutableStateFlow(false) every { webSocketManager.unacknowledgedAchievements } returns MutableStateFlow(emptyList()) every { webSocketManager.dailyRewardState } returns MutableStateFlow(null) every { webSocketManager.showDailyRewards } returns MutableStateFlow(false) every { webSocketManager.dailyQuestsState } returns MutableStateFlow(null) every { webSocketManager.showDailyQuests } returns MutableStateFlow(false) every { webSocketManager.unlimitedEnergyUntil } returns MutableStateFlow(null) every { webSocketManager.tutorialStep } returns MutableStateFlow(0) every { webSocketManager.tutorialComplete } returns MutableStateFlow(false) every { webSocketManager.tutorialMessage } returns MutableStateFlow(null) every { toastManager.currentToast } returns MutableStateFlow(null) every { tooltipManager.currentTooltip } returns MutableStateFlow(null) every { tooltipManager.isVisible } returns MutableStateFlow(false) viewModel = GameStateViewModel( webSocketManager = webSocketManager, soundManager = soundManager, analyticsManager = analyticsManager, toastManager = toastManager, tooltipManager = tooltipManager ) } @After fun tearDown() { Dispatchers.resetMain() } @Test fun `canAfford returns true when resources sufficient`() { personFlow.value = Person(calcEnergy = 100, money = 500.0, diamonds = 50) testDispatcher.scheduler.advanceUntilIdle() assertTrue(viewModel.canAfford(energyCost = 50, moneyCost = 200.0, diamondCost = 10)) } @Test fun `canAfford returns false when energy insufficient`() { personFlow.value = Person(calcEnergy = 10, money = 500.0, diamonds = 50) testDispatcher.scheduler.advanceUntilIdle() assertFalse(viewModel.canAfford(energyCost = 50)) } @Test fun `canAfford returns false when money insufficient`() { personFlow.value = Person(calcEnergy = 100, money = 50.0, diamonds = 50) testDispatcher.scheduler.advanceUntilIdle() assertFalse(viewModel.canAfford(moneyCost = 200.0)) } @Test fun `canAfford returns false when diamonds insufficient`() { personFlow.value = Person(calcEnergy = 100, money = 500.0, diamonds = 5) testDispatcher.scheduler.advanceUntilIdle() assertFalse(viewModel.canAfford(diamondCost = 10)) } @Test fun `setSpeed delegates to webSocketManager`() { viewModel.setSpeed(3) verify { webSocketManager.setSpeed(3) } } @Test fun `formattedTime formats AM correctly`() = runTest { playerFlow.value = Player(hourOfDay = 9, minuteOfHour = 5) testDispatcher.scheduler.advanceUntilIdle() assertEquals("9:05 AM", viewModel.formattedTime.value) } @Test fun `formattedTime formats PM correctly`() = runTest { playerFlow.value = Player(hourOfDay = 14, minuteOfHour = 30) testDispatcher.scheduler.advanceUntilIdle() assertEquals("2:30 PM", viewModel.formattedTime.value) } @Test fun `formattedTime formats midnight correctly`() = runTest { playerFlow.value = Player(hourOfDay = 0, minuteOfHour = 0) testDispatcher.scheduler.advanceUntilIdle() assertEquals("12:00 AM", viewModel.formattedTime.value) } @Test fun `formattedTime formats noon correctly`() = runTest { playerFlow.value = Player(hourOfDay = 12, minuteOfHour = 0) testDispatcher.scheduler.advanceUntilIdle() assertEquals("12:00 PM", viewModel.formattedTime.value) } @Test fun `seasonEmoji returns correct emoji for spring`() = runTest { playerFlow.value = Player(season = "Spring") testDispatcher.scheduler.advanceUntilIdle() assertEquals("\uD83C\uDF38", viewModel.seasonEmoji.value) } @Test fun `seasonEmoji returns correct emoji for winter`() = runTest { playerFlow.value = Player(season = "Winter") testDispatcher.scheduler.advanceUntilIdle() assertEquals("❄️", viewModel.seasonEmoji.value) } }