package dev.privacyllc.period.feature.settings

import androidx.test.core.app.ApplicationProvider
import dev.privacyllc.period.core.data.CycleData
import dev.privacyllc.period.core.data.CycleRepository
import dev.privacyllc.period.domain.prediction.PersonalPredictionEngine
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import kotlinx.coroutines.withTimeout
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import java.time.Clock
import java.time.LocalDate
import java.time.ZoneOffset

/**
 * §45's Delete My Data, tested for what it destroys **and what it leaves**.
 *
 * The second half is the one worth having. "It deleted everything" is easy to
 * assert and is not the requirement: the requirement is that health history goes
 * and the user's own settings do not, because `core/datastore` was made a
 * separate store precisely so a privacy action could not quietly reset a choice
 * somebody made.
 */
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
class PrivacyViewModelTest {

    private val dispatcher = UnconfinedTestDispatcher()
    private val today = LocalDate.of(2026, 8, 18)
    private val clock = Clock.fixed(today.atStartOfDay(ZoneOffset.UTC).toInstant(), ZoneOffset.UTC)

    private lateinit var repo: CycleRepository
    private lateinit var vm: PrivacyViewModel

    @Before fun setUp() {
        Dispatchers.setMain(dispatcher)
        repo = CycleData.repository(
            ApplicationProvider.getApplicationContext(),
            PersonalPredictionEngine(),
            clock,
        )
        runBlocking { repo.deleteAllHealthData() }
        vm = PrivacyViewModel(repo)
    }

    @After fun tearDown() = Dispatchers.resetMain()

    private fun await(predicate: suspend () -> Boolean) = runBlocking {
        withTimeout(5_000) { while (!predicate()) delay(10) }
    }

    private fun seedThreeCycles() = runBlocking {
        repo.confirmPeriodStart(today.minusDays(84))
        repo.confirmPeriodStart(today.minusDays(56))
        repo.confirmPeriodStart(today.minusDays(28))
    }

    @Test fun `it starts idle`() {
        assertEquals(DeletionState.IDLE, vm.deletion.value)
    }

    @Test fun `it erases every period record`() {
        seedThreeCycles()
        await { repo.confirmedPeriods.first().size == 3 }

        vm.deleteEverything()

        await { vm.deletion.value == DeletionState.DONE }
        assertTrue(runBlocking { repo.confirmedPeriods.first().isEmpty() })
    }

    @Test fun `the forecast goes with the history that produced it`() {
        seedThreeCycles()
        await { repo.forecast.first() != null }

        vm.deleteEverything()
        await { vm.deletion.value == DeletionState.DONE }

        assertEquals(null, runBlocking { repo.forecast.first() })
    }

    /**
     * The app must survive its own empty state afterwards.
     *
     * Deleting everything puts the user back on a screen that has never been
     * reached from this direction before — the first-run empty state on a
     * process that has already seen data. Reading the flows is what Today does
     * on recomposition, so this is the crash that would greet them.
     */
    @Test fun `reading the flows after deletion does not throw`() {
        seedThreeCycles()
        await { repo.confirmedPeriods.first().size == 3 }

        vm.deleteEverything()
        await { vm.deletion.value == DeletionState.DONE }

        runBlocking {
            assertTrue(repo.confirmedPeriods.first().isEmpty())
            assertEquals(null, repo.forecast.first())
            assertTrue(repo.notYetObservations.first().isEmpty())
        }
    }

    @Test fun `acknowledging returns it to idle so the dialog does not reappear`() {
        seedThreeCycles()
        vm.deleteEverything()
        await { vm.deletion.value == DeletionState.DONE }

        vm.acknowledge()

        assertEquals(DeletionState.IDLE, vm.deletion.value)
    }

    /**
     * Tapping twice was a real crash in Batch 01, on a different screen.
     * Here the second tap must be ignored rather than starting a second
     * transaction against a database the first one is emptying.
     */
    @Test fun `a second delete while one is running is ignored`() {
        seedThreeCycles()

        vm.deleteEverything()
        vm.deleteEverything()

        await { vm.deletion.value == DeletionState.DONE }
        assertTrue(runBlocking { repo.confirmedPeriods.first().isEmpty() })
    }
}
