package dev.privacyllc.period.domain.prediction

import dev.privacyllc.period.domain.cycle.PeriodRecord
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import java.time.LocalDate

/**
 * The six states of §22, and specifically their boundaries.
 *
 * Every boundary here is somebody's actual Tuesday: the day a period is
 * predicted, the day after one ends, the day a forecast slips. Those are the
 * days the screen is most read and the ones a hand-check never covers.
 */
class CycleStatusTest {

    private val today = LocalDate.of(2026, 8, 18)

    private fun period(start: LocalDate, end: LocalDate? = null) =
        PeriodRecord(id = 1, startDate = start, endDate = end)

    private fun forecast(on: LocalDate) = Prediction(
        mostLikelyStartDate = on,
        windowStart = on.minusDays(2),
        windowEnd = on.plusDays(2),
        confidenceScore = 0.6,
        confidenceLabel = ConfidenceLabel.MEDIUM,
        modelVersion = "test",
    )

    private fun status(
        periods: List<PeriodRecord>,
        forecastOn: LocalDate? = null,
        at: LocalDate = today,
        fertile: ClosedRange<LocalDate>? = null,
        ovulation: LocalDate? = null,
        original: LocalDate? = null,
    ) = CycleStatusRules.statusFor(
        periods = periods,
        forecast = forecastOn?.let(::forecast),
        today = at,
        fertileWindow = fertile,
        ovulation = ovulation,
        originalForecastDate = original,
    )

    @Test fun `no history is the empty state rather than a forecast of nothing`() {
        assertEquals(CycleStatus.NoData, status(emptyList()))
    }

    // -----------------------------------------------------------------------
    // During a period
    // -----------------------------------------------------------------------

    @Test fun `the day a period starts is day one, not day zero`() {
        val s = status(listOf(period(today))) as CycleStatus.DuringPeriod
        assertEquals(1, s.dayOfPeriod)
    }

    @Test fun `an ended period stops being current the day after it ends`() {
        val start = today.minusDays(5)
        assertTrue(status(listOf(period(start, end = today)), forecastOn = today.plusDays(20))
            is CycleStatus.DuringPeriod)
        assertTrue(
            "the day after the end is no longer during the period",
            status(listOf(period(start, end = today.minusDays(1))), forecastOn = today.plusDays(20))
                !is CycleStatus.DuringPeriod,
        )
    }

    @Test fun `an unclosed period does not hold the screen forever`() {
        // Somebody who never tapped "Period Ended" three weeks ago is not still
        // bleeding; they forgot. Keeping the during-period screen would hide the
        // next forecast, which is the thing they opened the app for.
        val old = today.minusDays(CycleStatusRules.MAX_ASSUMED_PERIOD_DAYS.toLong())
        assertTrue(status(listOf(period(old)), forecastOn = today.plusDays(18)) !is CycleStatus.DuringPeriod)
    }

    // -----------------------------------------------------------------------
    // Approaching, the day, and past it
    // -----------------------------------------------------------------------

    @Test fun `the countdown appears within a week and not before`() {
        val periods = listOf(period(today.minusDays(20), end = today.minusDays(16)))

        val soon = status(periods, forecastOn = today.plusDays(CycleStatusRules.APPROACHING_DAYS.toLong()))
        assertTrue(soon is CycleStatus.PeriodApproaching)
        assertEquals(CycleStatusRules.APPROACHING_DAYS, (soon as CycleStatus.PeriodApproaching).daysUntil)

        val later = status(periods, forecastOn = today.plusDays(CycleStatusRules.APPROACHING_DAYS + 1L))
        assertTrue(later is CycleStatus.BetweenPeriodAndFertile)
    }

    @Test fun `the predicted day is its own state`() {
        val periods = listOf(period(today.minusDays(28), end = today.minusDays(24)))
        assertTrue(status(periods, forecastOn = today) is CycleStatus.PredictedDay)
    }

    @Test fun `past the forecast is beyond, never late`() {
        val periods = listOf(period(today.minusDays(32), end = today.minusDays(28)))
        val s = status(periods, forecastOn = today.plusDays(1), original = today.minusDays(2))

        // §22: no "your period is late". The state carries what was originally
        // said so the screen can be accountable about it.
        assertTrue(s is CycleStatus.BeyondForecast)
        s as CycleStatus.BeyondForecast
        assertEquals(today.minusDays(2), s.originalForecast)
        assertEquals(2, s.daysPast)
    }

    @Test fun `a re-conditioned forecast does not keep saying may start today`() {
        // The engine moves the forecast forward on each "Not yet". Without the
        // original date, every one of those days would look like a fresh
        // predicted day and the app would sound like it never learns.
        val periods = listOf(period(today.minusDays(33), end = today.minusDays(29)))
        val s = status(periods, forecastOn = today, original = today.minusDays(3))
        assertTrue(s is CycleStatus.BeyondForecast)
    }

    // -----------------------------------------------------------------------
    // Fertility — Batch 04 makes these reachable
    // -----------------------------------------------------------------------

    @Test fun `fertility states do not appear before fertility is estimated`() {
        val periods = listOf(period(today.minusDays(10), end = today.minusDays(6)))
        val s = status(periods, forecastOn = today.plusDays(18))

        assertTrue(s is CycleStatus.BetweenPeriodAndFertile)
        // Null rather than a number: showing a placeholder here would be
        // inventing a fertility estimate, which is the one thing this screen
        // must never do.
        assertEquals(null, (s as CycleStatus.BetweenPeriodAndFertile).daysToFertileWindow)
    }

    @Test fun `the fertile window state appears once fertility is known`() {
        val periods = listOf(period(today.minusDays(12), end = today.minusDays(8)))
        val s = status(
            periods,
            forecastOn = today.plusDays(16),
            fertile = today.minusDays(1)..today.plusDays(3),
            ovulation = today.plusDays(2),
        )
        assertTrue(s is CycleStatus.DuringFertileWindow)
        assertEquals(2, (s as CycleStatus.DuringFertileWindow).daysToOvulation)
    }

    @Test fun `an imminent period outranks the fertile window`() {
        // Both can be true at once for a short cycle. The period is what the
        // user opened the app to find out.
        val periods = listOf(period(today.minusDays(24), end = today.minusDays(20)))
        val s = status(periods, forecastOn = today, fertile = today.minusDays(1)..today.plusDays(3))
        assertTrue(s is CycleStatus.PredictedDay)
    }

    @Test fun `a period that is on outranks every estimate`() {
        val s = status(
            listOf(period(today.minusDays(1))),
            forecastOn = today,
            fertile = today..today.plusDays(3),
        )
        assertTrue("a fact beats an estimate", s is CycleStatus.DuringPeriod)
    }
}
