package dev.privacyllc.period.feature.today

import androidx.test.core.app.ApplicationProvider
import dev.privacyllc.period.core.data.CycleData
import dev.privacyllc.period.core.data.CycleRepository
import dev.privacyllc.period.core.data.PeriodWriteResult
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.assertNull
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

/**
 * §23, §24 and §25 as they behave, rather than as they look.
 *
 * The spotting question is the one worth pinning: §25 permits it and, in the
 * same paragraph, says not to over-question. Both halves are tests, because
 * only asking is easy and only-asking-when-warranted is the requirement.
 */
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
class TodayViewModelTest {

    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: TodayViewModel

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

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

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

    @Test fun `logging today confirms it in words the user can read`() {
        vm.confirmStart(today)
        await { repo.confirmedPeriods.first().isNotEmpty() }
        await { vm.message.value != null }
        assertTrue(vm.message.value!!.startsWith("Logged"))
    }

    @Test fun `a very short entry asks whether it was spotting`() = runBlocking {
        val id = (repo.confirmPeriodStart(today) as PeriodWriteResult.Added).id

        vm.setEnd(id, today)  // one day
        await { vm.prompt.value != null }

        val prompt = vm.prompt.value
        assertTrue(prompt is TodayPrompt.PeriodOrSpotting)
        assertEquals(id, (prompt as TodayPrompt.PeriodOrSpotting).recordId)
    }

    @Test fun `an ordinary period is not questioned`() = runBlocking {
        val id = (repo.confirmPeriodStart(today.minusDays(4)) as PeriodWriteResult.Added).id

        vm.setEnd(id, today)  // five days
        await { vm.message.value != null }

        // §25: do not over-question. Five days is a period, and asking would
        // teach the user that the app second-guesses everything they enter.
        assertNull(vm.prompt.value)
    }

    @Test fun `the spotting question is asked once per record`() = runBlocking {
        val id = (repo.confirmPeriodStart(today) as PeriodWriteResult.Added).id

        vm.setEnd(id, today)
        await { vm.prompt.value != null }
        vm.dismissPrompt()

        // Answering "Period" — that is, dismissing — must settle it. Asking
        // again on the next edit is how one gentle question becomes nagging.
        vm.setEnd(id, today)
        await { vm.message.value != null }
        assertNull(vm.prompt.value)
    }

    @Test fun `reclassifying as spotting removes the period and leaves the cycle alone`() = runBlocking {
        // A real cycle, so there is a forecast that must survive.
        repo.confirmPeriodStart(today.minusDays(56))
        repo.confirmPeriodStart(today.minusDays(28))
        val before = repo.forecast.first()!!.mostLikelyStartDate

        val id = (repo.confirmPeriodStart(today) as PeriodWriteResult.Added).id
        vm.reclassifyAsSpotting(id, today)
        await { repo.spotting.first().isNotEmpty() }

        assertEquals("the period record is gone", 2, repo.confirmedPeriods.first().size)
        assertEquals("and the day is kept as spotting", 1, repo.spotting.first().size)
        // §25: spotting must not reset the cycle. The forecast is the proof.
        assertEquals(before, repo.forecast.first()!!.mostLikelyStartDate)
    }

    @Test fun `logging spotting directly never touches the cycle`() = runBlocking {
        repo.confirmPeriodStart(today.minusDays(56))
        repo.confirmPeriodStart(today.minusDays(28))
        val before = repo.forecast.first()!!.mostLikelyStartDate

        vm.logSpotting(today)
        await { repo.spotting.first().isNotEmpty() }

        assertEquals(2, repo.confirmedPeriods.first().size)
        assertEquals(before, repo.forecast.first()!!.mostLikelyStartDate)
    }
}
