package com.yorvana.ui.settings

import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.util.Log
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import com.yorvana.R
import com.yorvana.YorvanaApplication
import com.yorvana.data.billing.BillingManager
import com.yorvana.data.billing.DebugBillingOverride
import com.yorvana.data.billing.FakeBillingManager
import com.yorvana.data.billing.RestoreResult
import com.yorvana.data.model.OdometerUnit
import com.yorvana.data.preferences.AppPreferences
import com.yorvana.data.preferences.AppPreferencesStore
import com.yorvana.data.preferences.DebugBillingOverrideMode
import com.yorvana.data.storage.VaultStorage
import com.yorvana.domain.AppGate
import com.yorvana.ui.util.ViewModelDependencies
import com.yorvana.ui.util.testInstance
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import io.mockk.unmockkStatic
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Before
import org.junit.Test

@OptIn(ExperimentalCoroutinesApi::class)
class SettingsViewModelTest {
    private lateinit var preferences: AppPreferencesStore
    private lateinit var vaultStorage: VaultStorage
    private lateinit var app: YorvanaApplication
    private lateinit var viewModel: SettingsViewModel
    private lateinit var appGate: AppGate
    private lateinit var isReadOnlyFlow: MutableStateFlow<Boolean>
    private lateinit var hasPendingPurchaseFlow: MutableStateFlow<Boolean>
    private lateinit var fakeBilling: FakeBillingManager
    private lateinit var debugBillingOverride: DebugBillingOverride
    private val testDispatcher = UnconfinedTestDispatcher()

    @Before
    fun setUp() {
        mockkStatic(Log::class)
        every { Log.e(any<String>(), any<String>(), any<Throwable>()) } returns 0
        every { Log.w(any<String>(), any<String>()) } returns 0

        Dispatchers.setMain(testDispatcher)
        preferences = mockk(relaxed = true)
        vaultStorage = mockk(relaxed = true)
        app = mockk(relaxed = true)
        appGate = mockk(relaxed = true)
        fakeBilling = FakeBillingManager()
        debugBillingOverride = mockk(relaxed = true)
        isReadOnlyFlow = MutableStateFlow(false)
        hasPendingPurchaseFlow = MutableStateFlow(false)

        // Setup PackageManager mocks because SettingsViewModel reads the app version name on init.
        val pm = mockk<PackageManager>()
        val pi = PackageInfo().apply { versionName = "1.0.0" }
        every { app.applicationContext } returns app
        every { app.packageName } returns "com.yorvana"
        every { app.packageManager } returns pm
        every { pm.getPackageInfo("com.yorvana", 0) } returns pi

        // Mock localized strings for debug events
        every { app.getString(R.string.settings_debug_purchase_success) } returns "Simulated purchase success"
        every { app.getString(R.string.settings_debug_billing_reset) } returns "Billing state reset"
        every { app.getString(R.string.report_bug_support_email) } returns "yorvana.app@gmail.com"
        every { app.getString(R.string.settings_contact_email_subject) } returns "[Yorvana] Contact"

        every { preferences.preferences } returns flowOf(AppPreferences(vaultUriString = "content://vault"))
        every { appGate.isReadOnly } returns isReadOnlyFlow
        every { appGate.hasPendingPurchase } returns hasPendingPurchaseFlow
        every { appGate.isPremium } returns fakeBilling.premiumState
        every { debugBillingOverride.mode } returns MutableStateFlow(DebugBillingOverrideMode.NONE)
    }

    @After
    fun tearDown() {
        Dispatchers.resetMain()
        unmockkStatic(Log::class)
        unmockkAll()
    }

    private fun createViewModel() =
        SettingsViewModel(
            dependencies =
                ViewModelDependencies.testInstance(
                    app = app,
                    appGate = appGate,
                    vaultStorage = vaultStorage,
                    preferences = preferences,
                    billingManager = fakeBilling,
                    debugBillingOverride = debugBillingOverride,
                ),
        )

    @Test
    fun `init should load preferences`() =
        runTest {
            viewModel = createViewModel()

            viewModel.state.test {
                var state = awaitItem()
                while (state.vaultUriString == null) {
                    state = awaitItem()
                }
                assertThat(state.vaultUriString).isEqualTo("content://vault")
                cancelAndIgnoreRemainingEvents()
            }
        }

    @Test
    fun `onEvent OdometerUnitChanged should call preferences`() =
        runTest {
            viewModel = createViewModel()

            viewModel.onEvent(SettingsEvent.OdometerUnitChanged(OdometerUnit.MI))

            coVerify { preferences.setDefaultOdometerUnit(OdometerUnit.MI) }
        }

    @Test
    fun `onEvent OpenCategories should emit NavigateToCategories effect`() =
        runTest {
            viewModel = createViewModel()

            viewModel.effects.test {
                viewModel.onEvent(SettingsEvent.OpenCategories)
                assertThat(awaitItem()).isEqualTo(SettingsEffect.NavigateToCategories)
                cancelAndIgnoreRemainingEvents()
            }
        }

    @Test
    fun `onEvent OpenContact should emit LaunchContactEmail effect`() =
        runTest {
            viewModel = createViewModel()

            viewModel.effects.test {
                viewModel.onEvent(SettingsEvent.OpenContact)
                assertThat(awaitItem())
                    .isEqualTo(
                        SettingsEffect.LaunchContactEmail(
                            recipient = "yorvana.app@gmail.com",
                            subject = "[Yorvana] Contact",
                        ),
                    )
                cancelAndIgnoreRemainingEvents()
            }
        }

    @Test
    fun `onEvent RequestUpgrade should emit LaunchPurchaseFlow when not premium`() =
        runTest {
            viewModel = createViewModel()
            // premiumState is false by default in setUp

            viewModel.effects.test {
                viewModel.onEvent(SettingsEvent.RequestUpgrade)
                assertThat(awaitItem()).isEqualTo(SettingsEffect.LaunchPurchaseFlow)
                cancelAndIgnoreRemainingEvents()
            }
        }

    @Test
    fun `onEvent RequestUpgrade should be ignored when already premium`() =
        runTest {
            viewModel = createViewModel()
            fakeBilling.premiumState.value = true
            runCurrent() // let the init block collect the premium state

            viewModel.effects.test {
                viewModel.onEvent(SettingsEvent.RequestUpgrade)
                expectNoEvents()
                cancelAndIgnoreRemainingEvents()
            }
        }

    @Test
    fun `onEvent CurrencyChanged should update preferences after a delay`() =
        runTest {
            viewModel = createViewModel()

            viewModel.onEvent(SettingsEvent.CurrencyChanged("EUR"))

            coVerify(exactly = 0) { preferences.setDefaultCurrency("EUR") }

            // Wait for delay (400ms)
            testScheduler.advanceTimeBy(500)

            coVerify { preferences.setDefaultCurrency("EUR") }
        }

    @Test
    fun `onEvent OpenFileManager should emit LaunchFileManager effect`() =
        runTest {
            // Bypass state combining by setting it up correctly from start
            every { preferences.preferences } returns MutableStateFlow(AppPreferences(vaultUriString = "content://vault"))
            viewModel = createViewModel()

            // Minimal test to avoid coroutine leaks in complex flows
            viewModel.effects.test {
                // Ensure state is loaded
                if (viewModel.state.value.vaultUriString == "content://vault") {
                    viewModel.onEvent(SettingsEvent.OpenFileManager)
                    val effect = awaitItem()
                    assertThat(effect).isInstanceOf(SettingsEffect.LaunchFileManager::class.java)
                }
                cancelAndIgnoreRemainingEvents()
            }
        }

    @Test
    fun `onEvent RequestUpgrade should emit LaunchPurchaseFlow`() =
        runTest {
            viewModel = createViewModel()

            viewModel.effects.test {
                viewModel.onEvent(SettingsEvent.RequestUpgrade)
                assertThat(awaitItem()).isEqualTo(SettingsEffect.LaunchPurchaseFlow)
                cancelAndIgnoreRemainingEvents()
            }
        }

    @Test
    fun `onEvent RestorePurchases with Success hadPurchase=true should show restore_success`() =
        runTest {
            fakeBilling.restoreResult = RestoreResult.Success(hadPurchase = true)
            every { app.getString(R.string.restore_success) } returns "Purchase restored successfully"
            viewModel = createViewModel()

            viewModel.effects.test {
                viewModel.onEvent(SettingsEvent.RestorePurchases)
                val effect = awaitItem()
                assertThat(effect).isInstanceOf(SettingsEffect.ShowSnackbar::class.java)
                assertThat((effect as SettingsEffect.ShowSnackbar).message).isEqualTo("Purchase restored successfully")
                cancelAndIgnoreRemainingEvents()
            }
        }

    @Test
    fun `onEvent RestorePurchases with Success hadPurchase=false should show restore_none_found`() =
        runTest {
            fakeBilling.restoreResult = RestoreResult.Success(hadPurchase = false)
            every { app.getString(R.string.restore_none_found) } returns "No previous purchases found"
            viewModel = createViewModel()

            viewModel.effects.test {
                viewModel.onEvent(SettingsEvent.RestorePurchases)
                val effect = awaitItem()
                assertThat(effect).isInstanceOf(SettingsEffect.ShowSnackbar::class.java)
                assertThat((effect as SettingsEffect.ShowSnackbar).message).isEqualTo("No previous purchases found")
                cancelAndIgnoreRemainingEvents()
            }
        }

    @Test
    fun `onEvent RestorePurchases with BillingUnavailable should show restore_error`() =
        runTest {
            fakeBilling.restoreResult = RestoreResult.BillingUnavailable
            every { app.getString(R.string.restore_error_billing_unavailable) } returns "Restore failed: billing unavailable"
            viewModel = createViewModel()

            viewModel.effects.test {
                viewModel.onEvent(SettingsEvent.RestorePurchases)
                val effect = awaitItem()
                assertThat(effect).isInstanceOf(SettingsEffect.ShowSnackbar::class.java)
                assertThat((effect as SettingsEffect.ShowSnackbar).message).isEqualTo("Restore failed: billing unavailable")
                cancelAndIgnoreRemainingEvents()
            }
        }

    @Test
    fun `onEvent RestorePurchases with Error should show restore_error`() =
        runTest {
            fakeBilling.restoreResult = RestoreResult.Error(responseCode = 3)
            every { app.getString(R.string.restore_error_code, 3) } returns "Restore failed (code 3)"
            viewModel = createViewModel()

            viewModel.effects.test {
                viewModel.onEvent(SettingsEvent.RestorePurchases)
                val effect = awaitItem()
                assertThat(effect).isInstanceOf(SettingsEffect.ShowSnackbar::class.java)
                assertThat((effect as SettingsEffect.ShowSnackbar).message).isEqualTo("Restore failed (code 3)")
                cancelAndIgnoreRemainingEvents()
            }
        }

    @Test
    fun `onEvent RestorePurchases does not leave isRestoringPurchases set on success`() =
        runTest {
            fakeBilling.restoreResult = RestoreResult.Success(hadPurchase = false)
            every { app.getString(any<Int>()) } returns ""
            viewModel = createViewModel()

            assertThat(viewModel.state.value.isRestoringPurchases).isFalse()
            viewModel.onEvent(SettingsEvent.RestorePurchases)
            assertThat(viewModel.state.value.isRestoringPurchases).isFalse()
        }

    @Test
    fun `onEvent RestorePurchases is re-entrancy-guarded`() =
        runTest {
            val deferred = CompletableDeferred<RestoreResult>()
            fakeBilling.restoreDeferred = deferred
            every { app.getString(any<Int>()) } returns ""
            viewModel = createViewModel()

            // Keep a subscriber active so WhileSubscribed activates the combine and state.value is live.
            val job = launch(UnconfinedTestDispatcher(testScheduler)) { viewModel.state.collect {} }

            // First call: coroutine suspends at deferred.await()
            viewModel.onEvent(SettingsEvent.RestorePurchases)
            assertThat(viewModel.state.value.isRestoringPurchases).isTrue()

            // Second call: guard returns early because isRestoringPurchases=true
            viewModel.onEvent(SettingsEvent.RestorePurchases)
            assertThat(viewModel.state.value.isRestoringPurchases).isTrue()

            // Complete the first call
            deferred.complete(RestoreResult.Success(hadPurchase = false))
            assertThat(viewModel.state.value.isRestoringPurchases).isFalse()

            job.cancel()
        }

    @Test
    fun `state reflects isPremium from appGate`() =
        runTest {
            fakeBilling.premiumState.value = true
            viewModel = createViewModel()

            viewModel.state.test {
                var state = awaitItem()
                while (!state.isPremium) {
                    state = awaitItem()
                }
                assertThat(state.isPremium).isTrue()
                cancelAndIgnoreRemainingEvents()
            }
        }

    @Test
    fun `state reflects isReadOnly from appGate`() =
        runTest {
            isReadOnlyFlow.value = true
            viewModel = createViewModel()

            viewModel.state.test {
                var state = awaitItem()
                while (!state.isReadOnly) {
                    state = awaitItem()
                }
                assertThat(state.isReadOnly).isTrue()
                cancelAndIgnoreRemainingEvents()
            }
        }

    @Test
    fun `onEvent RestorePurchases with Exception should show generic billing_error`() =
        runTest {
            // Mocking the BillingManager interface instead of using FakeBillingManager
            // to easily throw an exception.
            val mockBilling = mockk<BillingManager>()
            coEvery { mockBilling.restorePurchases() } throws RuntimeException("SDK error")
            every { app.getString(R.string.billing_error, "SDK error") } returns "Purchase failed: SDK error"

            val vm =
                SettingsViewModel(
                    dependencies =
                        ViewModelDependencies.testInstance(
                            app = app,
                            appGate = appGate,
                            vaultStorage = vaultStorage,
                            preferences = preferences,
                            billingManager = mockBilling,
                            debugBillingOverride = debugBillingOverride,
                        ),
                )

            vm.effects.test {
                vm.onEvent(SettingsEvent.RestorePurchases)
                val effect = awaitItem()
                assertThat(effect).isEqualTo(SettingsEffect.ShowSnackbar("Purchase failed: SDK error"))
                cancelAndIgnoreRemainingEvents()
            }
            assertThat(vm.state.value.isRestoringPurchases).isFalse()
        }

    @Test
    fun `onEvent SetDebugOverride should call debugBillingOverride setMode`() =
        runTest {
            viewModel = createViewModel()

            viewModel.onEvent(SettingsEvent.SetDebugOverride(DebugBillingOverrideMode.FORCE_PREMIUM))

            coVerify { debugBillingOverride.setMode(DebugBillingOverrideMode.FORCE_PREMIUM) }
        }

    @Test
    fun `onEvent CrashReportingToggled should update preferences`() =
        runTest {
            viewModel = createViewModel()

            viewModel.onEvent(SettingsEvent.CrashReportingToggled(true))

            coVerify { preferences.setCrashReportingEnabled(true) }
        }

    @Test
    fun `onEvent CrashReportingToggled false should call disableSentryNow`() =
        runTest {
            viewModel = createViewModel()

            viewModel.onEvent(SettingsEvent.CrashReportingToggled(false))

            coVerify { app.disableSentryNow() }
        }

    @Test
    fun `onEvent SimulatePurchaseSuccess should call simulatePurchaseSuccess and show snackbar`() =
        runTest {
            viewModel = createViewModel()

            viewModel.effects.test {
                viewModel.onEvent(SettingsEvent.SimulatePurchaseSuccess)
                assertThat(fakeBilling.premiumState.value).isTrue()
                val effect = awaitItem()
                assertThat(effect).isInstanceOf(SettingsEffect.ShowSnackbar::class.java)
                assertThat((effect as SettingsEffect.ShowSnackbar).message).isEqualTo("Simulated purchase success")
                cancelAndIgnoreRemainingEvents()
            }
        }

    @Test
    fun `isPremiumPending should reflect appGate hasPendingPurchase`() =
        runTest {
            viewModel = createViewModel()

            viewModel.state.test {
                awaitItem() // Skip initial

                hasPendingPurchaseFlow.value = true
                assertThat(awaitItem().isPremiumPending).isTrue()

                hasPendingPurchaseFlow.value = false
                assertThat(awaitItem().isPremiumPending).isFalse()
            }
        }

    @Test
    fun `onEvent ResetBillingState should call resetBillingState and show snackbar`() =
        runTest {
            fakeBilling.premiumState.value = true
            viewModel = createViewModel()

            viewModel.effects.test {
                viewModel.onEvent(SettingsEvent.ResetBillingState)
                assertThat(fakeBilling.premiumState.value).isFalse()
                val effect = awaitItem()
                assertThat(effect).isInstanceOf(SettingsEffect.ShowSnackbar::class.java)
                assertThat((effect as SettingsEffect.ShowSnackbar).message).isEqualTo("Billing state reset")
                cancelAndIgnoreRemainingEvents()
            }
        }
}
