package dev.privacyllc.period.feature.today

import dev.privacyllc.period.domain.cycle.PeriodRecord
import dev.privacyllc.period.domain.prediction.ConfidenceLabel
import dev.privacyllc.period.domain.prediction.Prediction
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import java.time.LocalDate

/**
 * The two pieces of arithmetic the Today screen shows as its largest text.
 *
 * Derived on [TodayUiState] rather than inside a composable precisely so they
 * can be tested here — a countdown that is off by one is the kind of defect a
 * user notices immediately and a screenshot review never does.
 */
class TodayUiStateTest {

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

    private fun forecastOn(date: LocalDate) = Prediction(
        mostLikelyStartDate = date,
        windowStart = date.minusDays(1),
        windowEnd = date.plusDays(1),
        confidenceScore = 0.7,
        confidenceLabel = ConfidenceLabel.HIGH,
        modelVersion = "test",
    )

    private fun state(
        forecast: Prediction? = null,
        periodStarts: List<LocalDate> = emptyList(),
    ) = TodayUiState(
        loading = false,
        periods = periodStarts.mapIndexed { i, d -> PeriodRecord(id = i.toLong(), startDate = d) },
        forecast = forecast,
        today = today,
    )

    @Test fun `no forecast means no countdown rather than zero`() {
        // Zero would render as "your period may start today", which is a claim.
        assertNull(state().daysUntil)
    }

    @Test fun `the countdown is whole days from today`() {
        assertEquals(4L, state(forecastOn(LocalDate.of(2026, 8, 22))).daysUntil)
        assertEquals(1L, state(forecastOn(LocalDate.of(2026, 8, 19))).daysUntil)
        assertEquals(0L, state(forecastOn(today)).daysUntil)
    }

    @Test fun `a forecast already passed counts negative rather than clamping to zero`() {
        // The screen says "Not yet?" here. Clamping to 0 would keep saying "may
        // start today" for days, which §22 explicitly calls out as the wrong
        // tone — and would hide that the forecast needs re-conditioning.
        assertEquals(-3L, state(forecastOn(LocalDate.of(2026, 8, 15))).daysUntil)
    }

    @Test fun `cycle day counts from the most recent start, inclusive`() {
        // The day a period starts is cycle day 1, not day 0.
        assertEquals(1L, state(periodStarts = listOf(today)).cycleDay)
        assertEquals(18L, state(periodStarts = listOf(LocalDate.of(2026, 8, 1))).cycleDay)
    }

    @Test fun `cycle day uses the latest start regardless of list order`() {
        assertEquals(
            18L,
            state(
                periodStarts = listOf(
                    LocalDate.of(2026, 6, 1),
                    LocalDate.of(2026, 8, 1),
                    LocalDate.of(2026, 7, 1),
                ),
            ).cycleDay,
        )
    }

    @Test fun `no history means no cycle day`() {
        assertNull(state().cycleDay)
    }

    @Test fun `a start date in the future does not produce a zero or negative cycle day`() {
        // Reachable by editing a record forward. "Cycle day 0" or "-2" is
        // nonsense on screen; showing nothing is honest.
        assertNull(state(periodStarts = listOf(today.plusDays(2))).cycleDay)
    }

    @Test fun `a countdown spans month and year boundaries correctly`() {
        val newYear = TodayUiState(
            loading = false,
            forecast = forecastOn(LocalDate.of(2027, 1, 2)),
            today = LocalDate.of(2026, 12, 28),
        )
        assertEquals(5L, newYear.daysUntil)

        val leap = TodayUiState(
            loading = false,
            forecast = forecastOn(LocalDate.of(2028, 3, 1)),
            today = LocalDate.of(2028, 2, 27),
        )
        assertEquals(3L, leap.daysUntil)  // 2028 is a leap year: 27th, 28th, 29th, 1st
    }
}
