package dev.privacyllc.period.core.datastore

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
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.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.time.LocalTime

/**
 * Runs on the JVM against a real DataStore backed by a temporary file — no
 * emulator, no Robolectric. That is possible because the repository takes a
 * `DataStore` rather than a `Context`, which is the reason it is shaped that
 * way.
 */
class UserPreferencesRepositoryTest {

    @get:Rule val tmp = TemporaryFolder()

    private lateinit var scope: TestScope
    private lateinit var store: DataStore<Preferences>
    private lateinit var repo: UserPreferencesRepository

    @Before fun setUp() {
        scope = TestScope(StandardTestDispatcher())
        store = PreferenceDataStoreFactory.create(
            scope = CoroutineScope(scope.coroutineContext),
            produceFile = { tmp.newFile("prefs.preferences_pb") },
        )
        repo = UserPreferencesRepository(store)
    }

    @After fun tearDown() = scope.cancel()

    // -----------------------------------------------------------------------
    // Defaults. These are decisions, and each of these tests is what stops one
    // being changed by accident.
    // -----------------------------------------------------------------------

    @Test
    fun `a fresh install defaults to discreet notifications`() = scope.runTest {
        // PRODUCT_PLAN.md §28. A default of DIRECT would put menstrual detail on
        // a lock screen before the user has been asked a single question, and it
        // is the most likely real privacy breach in this product.
        assertEquals(NotificationPrivacy.DISCREET, repo.preferences.first().notificationPrivacy)
    }

    @Test
    fun `a fresh install has fertility reminders off`() = scope.runTest {
        val p = repo.preferences.first()
        assertFalse("an unrequested fertile-window reminder is an unpleasant surprise", p.fertileWindowReminderEnabled)
        assertFalse("likewise ovulation", p.ovulationReminderEnabled)
        assertTrue("the period reminder is the one the user came for", p.periodApproachingEnabled)
        assertTrue(p.periodExpectedTodayEnabled)
        assertTrue(p.didItStartEnabled)
    }

    @Test
    fun `a fresh install is not entitled and has not onboarded`() = scope.runTest {
        val p = repo.preferences.first()
        assertFalse(p.adsRemoved)
        assertFalse(p.onboardingCompleted)
        assertFalse(p.biometricLockEnabled)
        assertEquals(AppTheme.SYSTEM, p.theme)
        assertEquals(LocalTime.of(10, 0), p.reminderTime)
    }

    // -----------------------------------------------------------------------
    // Round trips
    // -----------------------------------------------------------------------

    @Test
    fun `every setting survives being written and read back`() = scope.runTest {
        repo.setNotificationPrivacy(NotificationPrivacy.MAXIMUM_PRIVACY)
        repo.setReminderTime(LocalTime.of(21, 45))
        repo.setPeriodApproachingEnabled(false)
        repo.setFertileWindowReminderEnabled(true)
        repo.setOvulationReminderEnabled(true)
        repo.setBiometricLockEnabled(true)
        repo.setTheme(AppTheme.DARK)
        repo.setAdsRemoved(true)
        repo.setOnboardingCompleted(true)

        assertEquals(
            UserPreferences(
                notificationPrivacy = NotificationPrivacy.MAXIMUM_PRIVACY,
                reminderTime = LocalTime.of(21, 45),
                periodApproachingEnabled = false,
                fertileWindowReminderEnabled = true,
                ovulationReminderEnabled = true,
                biometricLockEnabled = true,
                theme = AppTheme.DARK,
                adsRemoved = true,
                onboardingCompleted = true,
            ),
            repo.preferences.first(),
        )
    }

    @Test
    fun `the reminder time keeps its minutes rather than rounding to the hour`() = scope.runTest {
        repo.setReminderTime(LocalTime.of(7, 5))
        assertEquals(LocalTime.of(7, 5), repo.preferences.first().reminderTime)
        repo.setReminderTime(LocalTime.of(0, 0))
        assertEquals(LocalTime.of(0, 0), repo.preferences.first().reminderTime)
        repo.setReminderTime(LocalTime.of(23, 59))
        assertEquals(LocalTime.of(23, 59), repo.preferences.first().reminderTime)
    }

    // -----------------------------------------------------------------------
    // What happens when the stored value is nonsense
    // -----------------------------------------------------------------------

    @Test
    fun `an unrecognised privacy mode falls back to discreet rather than to direct`() = scope.runTest {
        // A downgrade, a rollback, or a hand-edited file can leave a value this
        // build does not know. The fallback must be the SAFE mode — falling
        // back to DIRECT would leak, and falling back to whatever enum entry
        // happens to be first would be luck rather than a decision.
        store.edit { it[stringPreferencesKey("notification_privacy")] = "TELEPATHY" }
        assertEquals(NotificationPrivacy.DISCREET, repo.preferences.first().notificationPrivacy)
    }

    @Test
    fun `an unrecognised theme falls back to system`() = scope.runTest {
        store.edit { it[stringPreferencesKey("theme")] = "SEPIA" }
        assertEquals(AppTheme.SYSTEM, repo.preferences.first().theme)
    }

    @Test
    fun `an out-of-range reminder time falls back to the default`() = scope.runTest {
        store.edit { it[androidx.datastore.preferences.core.intPreferencesKey("reminder_minute_of_day")] = 99_999 }
        assertEquals(UserPreferences.DEFAULT_REMINDER_TIME, repo.preferences.first().reminderTime)

        store.edit { it[androidx.datastore.preferences.core.intPreferencesKey("reminder_minute_of_day")] = -1 }
        assertEquals(UserPreferences.DEFAULT_REMINDER_TIME, repo.preferences.first().reminderTime)
    }

    // -----------------------------------------------------------------------
    // Reset
    // -----------------------------------------------------------------------

    @Test
    fun `resetting returns every setting to its default`() = scope.runTest {
        repo.setNotificationPrivacy(NotificationPrivacy.DIRECT)
        repo.setBiometricLockEnabled(true)

        repo.resetToDefaults()

        assertEquals(UserPreferences.Defaults, repo.preferences.first())
    }

    @Test
    fun `settings are independent of each other`() = scope.runTest {
        // Writing one must not disturb another. Obvious, and the kind of thing
        // that stops being true the day somebody writes a whole-object setter.
        repo.setNotificationPrivacy(NotificationPrivacy.DIRECT)
        repo.setTheme(AppTheme.LIGHT)
        repo.setBiometricLockEnabled(true)

        assertEquals(NotificationPrivacy.DIRECT, repo.preferences.first().notificationPrivacy)

        repo.setAdsRemoved(true)

        val p = repo.preferences.first()
        assertEquals(NotificationPrivacy.DIRECT, p.notificationPrivacy)
        assertEquals(AppTheme.LIGHT, p.theme)
        assertTrue(p.biometricLockEnabled)
        assertTrue(p.adsRemoved)
    }
}
