package com.yorvana.ui.settings

import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import com.yorvana.TestYorvanaApplication
import com.yorvana.YorvanaApplication
import com.yorvana.data.billing.DebugBillingOverride
import com.yorvana.data.billing.FakeBillingManager
import com.yorvana.data.model.CategoriesFile
import com.yorvana.data.model.OdometerUnit
import com.yorvana.data.model.ServiceRecord
import com.yorvana.data.model.Vehicle
import com.yorvana.data.preferences.AppPreferences
import com.yorvana.data.preferences.AppPreferencesStore
import com.yorvana.data.preferences.CURRENT_CONSENT_VERSION
import com.yorvana.data.preferences.DebugBillingOverrideMode
import com.yorvana.data.storage.DestinationNotEmptyException
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.every
import io.mockk.mockk
import io.mockk.unmockkAll
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import kotlin.time.Duration.Companion.seconds

@OptIn(ExperimentalCoroutinesApi::class)
@Config(application = TestYorvanaApplication::class, sdk = [34])
@RunWith(RobolectricTestRunner::class)
class SettingsViewModelIT {
    private lateinit var preferences: FakeAppPreferencesStore
    private lateinit var vaultStorage: FakeVaultStorage
    private lateinit var app: YorvanaApplication
    private lateinit var appGate: AppGate
    private lateinit var fakeBilling: FakeBillingManager
    private lateinit var debugBillingOverride: DebugBillingOverride
    private lateinit var viewModel: SettingsViewModel
    private val testDispatcher = UnconfinedTestDispatcher()

    @Before
    fun setUp() {
        Dispatchers.setMain(testDispatcher)
        preferences = FakeAppPreferencesStore()
        vaultStorage = FakeVaultStorage()
        app = mockk(relaxed = true)
        fakeBilling = FakeBillingManager()
        appGate = mockk(relaxed = true)
        debugBillingOverride = mockk(relaxed = true)
        every { appGate.isReadOnly } returns MutableStateFlow(false)
        every { debugBillingOverride.mode } returns MutableStateFlow(DebugBillingOverrideMode.NONE)

        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
    }

    @After
    fun tearDown() {
        Dispatchers.resetMain()
        unmockkAll()
    }

    @Test
    fun on_event_move_vault_should_call_storage_and_update_preferences() =
        runTest(testDispatcher) {
            val mockUri = Uri.parse("content://new-vault")
            val newUriString = mockUri.toString()

            vaultStorage.apply {
                isEmpty = true
                moveResult = Result.success(Unit)
            }

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

            viewModel.state.test(timeout = 5.seconds) {
                // Initial
                assertThat(awaitItem().vaultUriString).isEqualTo("content://vault")

                viewModel.onEvent(SettingsEvent.VaultFolderPicked(mockUri))
                // Should see validation or dialog
                var current = awaitItem()
                while (!current.showMoveOrFreshDialog) {
                    current = awaitItem()
                }
                assertThat(current.showMoveOrFreshDialog).isTrue()

                viewModel.onEvent(SettingsEvent.MoveVault)

                // Final state
                while (current.vaultUriString != newUriString) {
                    current = awaitItem()
                }
                assertThat(current.vaultUriString).isEqualTo(newUriString)
                assertThat(current.isMigrating).isFalse()

                assertThat(vaultStorage.lastMoveUri).isEqualTo(mockUri)
            }
        }

    @Test
    fun on_event_start_fresh_should_set_new_uri_and_clear_state() =
        runTest(testDispatcher) {
            val mockUri = Uri.parse("content://fresh-vault")
            val newUriString = mockUri.toString()
            vaultStorage.isEmpty = true

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

            viewModel.state.test(timeout = 5.seconds) {
                assertThat(awaitItem().vaultUriString).isEqualTo("content://vault")

                viewModel.onEvent(SettingsEvent.VaultFolderPicked(mockUri))
                var current = awaitItem()
                while (!current.showMoveOrFreshDialog) {
                    current = awaitItem()
                }
                assertThat(current.showMoveOrFreshDialog).isTrue()

                viewModel.onEvent(SettingsEvent.StartFresh)

                while (current.vaultUriString != newUriString) {
                    current = awaitItem()
                }
                assertThat(current.vaultUriString).isEqualTo(newUriString)
                assertThat(current.showMoveOrFreshDialog).isFalse()
            }
        }

    @Test
    fun dialog_visibility_events_should_update_state() =
        runTest(testDispatcher) {
            val mockUri = Uri.parse("content://vault")
            vaultStorage.apply {
                isEmpty = true
                isVault = true
            }

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

            viewModel.state.test(timeout = 5.seconds) {
                awaitItem() // Initial

                // 1. DismissMoveDialog
                viewModel.onEvent(SettingsEvent.VaultFolderPicked(mockUri))
                var current = awaitItem()
                while (!current.showMoveOrFreshDialog) {
                    current = awaitItem()
                }
                viewModel.onEvent(SettingsEvent.DismissMoveDialog)
                while (current.showMoveOrFreshDialog) {
                    current = awaitItem()
                }
                assertThat(current.showMoveOrFreshDialog).isFalse()

                // 2. DismissConflictDialog
                vaultStorage.apply {
                    isEmpty = false
                    isVault = false
                }
                viewModel.onEvent(SettingsEvent.VaultFolderPicked(mockUri))
                while (!current.showConflictDialog) {
                    current = awaitItem()
                }
                viewModel.onEvent(SettingsEvent.DismissConflictDialog)
                while (current.showConflictDialog) {
                    current = awaitItem()
                }
                assertThat(current.showConflictDialog).isFalse()

                // 3. DismissUseExistingVaultDialog
                vaultStorage.isVault = true
                viewModel.onEvent(SettingsEvent.VaultFolderPicked(mockUri))
                while (!current.showUseExistingVaultDialog) {
                    current = awaitItem()
                }
                viewModel.onEvent(SettingsEvent.DismissUseExistingVaultDialog)
                while (current.showUseExistingVaultDialog) {
                    current = awaitItem()
                }
                assertThat(current.showUseExistingVaultDialog).isFalse()
            }
        }

    @Test
    fun move_vault_should_show_conflict_dialog_on_destination_not_empty_exception() =
        runTest(testDispatcher) {
            val newUri = Uri.parse("content://conflict")
            vaultStorage.apply {
                isEmpty = false
                isVault = false
                moveResult = Result.failure(DestinationNotEmptyException())
            }

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

            viewModel.state.test(timeout = 5.seconds) {
                assertThat(awaitItem().isValidating).isFalse()

                viewModel.onEvent(SettingsEvent.VaultFolderPicked(newUri))
                var current = awaitItem()
                while (!current.showConflictDialog) {
                    current = awaitItem()
                }
                assertThat(current.showConflictDialog).isTrue()
                assertThat(current.isValidating).isFalse()
            }
        }

    @Test
    fun handle_vault_picked_should_show_existing_vault_dialog_when_is_vault_folder_is_true() =
        runTest(testDispatcher) {
            val newUri = Uri.parse("content://existing")
            vaultStorage.apply {
                isEmpty = false
                isVault = true
            }

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

            viewModel.state.test(timeout = 5.seconds) {
                assertThat(awaitItem().isValidating).isFalse()

                viewModel.onEvent(SettingsEvent.VaultFolderPicked(newUri))
                var current = awaitItem()
                while (!current.showUseExistingVaultDialog) {
                    current = awaitItem()
                }
                assertThat(current.showUseExistingVaultDialog).isTrue()
                assertThat(current.isValidating).isFalse()
            }
        }

    // --- Fakes ---

    class FakeAppPreferencesStore : AppPreferencesStore {
        private val prefsFlow = MutableStateFlow(AppPreferences(vaultUriString = "content://vault"))
        override val preferences: Flow<AppPreferences> = prefsFlow

        override suspend fun setVaultUri(uri: String?) {
            prefsFlow.value = prefsFlow.value.copy(vaultUriString = uri)
        }

        override suspend fun setDefaultOdometerUnit(unit: OdometerUnit) {
            prefsFlow.value = prefsFlow.value.copy(defaultOdometerUnit = unit)
        }

        override suspend fun setDefaultCurrency(currency: String) {
            prefsFlow.value = prefsFlow.value.copy(defaultCurrency = currency)
        }

        override suspend fun setIsPremiumCached(value: Boolean) {
            prefsFlow.value = prefsFlow.value.copy(isPremiumCached = value)
        }

        override suspend fun setDebugBillingOverrideMode(mode: DebugBillingOverrideMode) {
            prefsFlow.value = prefsFlow.value.copy(debugBillingOverrideMode = mode)
        }

        override suspend fun setCrashReportingEnabled(enabled: Boolean) {
            prefsFlow.value = prefsFlow.value.copy(crashReportingEnabled = enabled)
        }

        override suspend fun setCrashReportingConsentPromptShownVersion(version: Int) {
            prefsFlow.value = prefsFlow.value.copy(crashReportingConsentPromptShownVersion = version)
        }

        override suspend fun recordConsentDecision(enabled: Boolean) {
            setCrashReportingEnabled(enabled)
            setCrashReportingConsentPromptShownVersion(CURRENT_CONSENT_VERSION)
        }

        override fun isCrashReportingEnabledSync(): Boolean = prefsFlow.value.crashReportingEnabled

        override fun getVaultUriSync(): String? = prefsFlow.value.vaultUriString
    }

    class FakeVaultStorage : VaultStorage {
        var isEmpty = true
        var isVault = false
        var moveResult: Result<Unit> = Result.success(Unit)
        var lastMoveUri: Uri? = null

        override fun isConfigured(): Boolean = true

        override suspend fun listVehicleIds(): Result<List<String>> = Result.success(emptyList())

        override suspend fun readVehicle(vehicleId: String): Result<Vehicle> = Result.failure(Exception())

        override suspend fun writeVehicle(vehicle: Vehicle): Result<Unit> = Result.success(Unit)

        override suspend fun deleteVehicle(vehicleId: String): Result<Unit> = Result.success(Unit)

        override suspend fun listRecordIds(vehicleId: String): Result<List<String>> = Result.success(emptyList())

        override suspend fun readRecord(
            vId: String,
            rId: String,
        ): Result<ServiceRecord> = Result.failure(Exception())

        override suspend fun writeRecord(record: ServiceRecord): Result<Unit> = Result.success(Unit)

        override suspend fun deleteRecord(
            vId: String,
            rId: String,
        ): Result<Unit> = Result.success(Unit)

        override suspend fun copyAttachmentIntoVault(
            vId: String,
            rId: String,
            s: Uri,
            f: String,
        ) = Result.success(Unit)

        override suspend fun deleteAttachment(
            vId: String,
            rId: String,
            f: String,
        ) = Result.success(Unit)

        override fun resolveAttachmentDocumentFile(
            vId: String,
            rId: String,
            f: String,
        ): DocumentFile? = null

        override suspend fun readCategories(): Result<CategoriesFile> = Result.success(CategoriesFile())

        override suspend fun writeCategories(categories: CategoriesFile) = Result.success(Unit)

        override suspend fun moveVaultTo(newRootUri: Uri): Result<Unit> {
            lastMoveUri = newRootUri
            return moveResult
        }

        override suspend fun isDestinationEmpty(uri: Uri): Boolean = isEmpty

        override suspend fun isVaultFolder(uri: Uri): Boolean = isVault

        override fun setVaultUri(uri: Uri?) = Unit

        override fun getVaultUri(): Uri? = null

        override suspend fun clearVault() = Unit
    }
}
