package dev.privacyllc.period.domain.prediction

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

class CalendarMarksTest {

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

    private fun forecast(from: LocalDate, to: LocalDate) = Prediction(
        mostLikelyStartDate = from.plusDays((to.toEpochDay() - from.toEpochDay()) / 2),
        windowStart = from,
        windowEnd = to,
        confidenceScore = 0.6,
        confidenceLabel = ConfidenceLabel.MEDIUM,
        modelVersion = "test",
    )

    private fun marks(
        periods: List<PeriodRecord> = emptyList(),
        spotting: List<SpottingRecord> = emptyList(),
        prediction: Prediction? = null,
        fertile: ClosedRange<LocalDate>? = null,
        ovulation: LocalDate? = null,
    ) = CalendarMarks.forMonth(month, periods, spotting, prediction, today, fertile, ovulation)
        .associateBy { it.date.dayOfMonth }

    @Test fun `every day of the month is present exactly once`() {
        val days = marks()
        assertEquals(31, days.size)
        assertTrue(days.values.all { it.mark == DayMark.NONE })
    }

    @Test fun `a closed period marks every day it covers, inclusive`() {
        val days = marks(
            listOf(PeriodRecord(1, LocalDate.of(2026, 8, 3), LocalDate.of(2026, 8, 6))),
        )
        assertEquals(DayMark.CONFIRMED_PERIOD, days.getValue(3).mark)
        assertEquals(DayMark.CONFIRMED_PERIOD, days.getValue(6).mark)
        assertEquals(DayMark.NONE, days.getValue(2).mark)
        assertEquals(DayMark.NONE, days.getValue(7).mark)
    }

    @Test fun `an unclosed period marks only its start`() {
        // Filling forward to today would draw days the user never said were
        // period days. The calendar must not assert that.
        val days = marks(listOf(PeriodRecord(1, LocalDate.of(2026, 8, 15), null)))
        assertEquals(DayMark.CONFIRMED_PERIOD, days.getValue(15).mark)
        assertEquals(DayMark.NONE, days.getValue(16).mark)
    }

    @Test fun `the whole forecast window is marked as predicted`() {
        val days = marks(prediction = forecast(LocalDate.of(2026, 8, 20), LocalDate.of(2026, 8, 24)))
        (20..24).forEach { assertEquals("day $it", DayMark.PREDICTED_PERIOD, days.getValue(it).mark) }
        assertEquals(DayMark.NONE, days.getValue(19).mark)
        assertEquals(DayMark.NONE, days.getValue(25).mark)
    }

    @Test fun `a confirmed period beats a prediction over the same day`() {
        val days = marks(
            periods = listOf(PeriodRecord(1, LocalDate.of(2026, 8, 21), LocalDate.of(2026, 8, 23))),
            prediction = forecast(LocalDate.of(2026, 8, 20), LocalDate.of(2026, 8, 24)),
        )
        // §26: predicted and confirmed must never look identical, and where both
        // apply the fact is what the user needs to see.
        assertEquals(DayMark.CONFIRMED_PERIOD, days.getValue(21).mark)
        assertEquals(DayMark.PREDICTED_PERIOD, days.getValue(20).mark)
    }

    @Test fun `spotting is its own mark and never a period`() {
        val days = marks(spotting = listOf(SpottingRecord(1, LocalDate.of(2026, 8, 10))))
        assertEquals(DayMark.SPOTTING, days.getValue(10).mark)
        assertTrue(days.values.none { it.mark == DayMark.CONFIRMED_PERIOD })
    }

    @Test fun `ovulation outranks the fertile window it sits inside`() {
        val days = marks(
            fertile = LocalDate.of(2026, 8, 8)..LocalDate.of(2026, 8, 13),
            ovulation = LocalDate.of(2026, 8, 12),
        )
        assertEquals(DayMark.OVULATION, days.getValue(12).mark)
        assertEquals(DayMark.FERTILE_WINDOW, days.getValue(11).mark)
    }

    @Test fun `fertility is absent until it is estimated`() {
        assertTrue(marks().values.none { it.mark == DayMark.FERTILE_WINDOW || it.mark == DayMark.OVULATION })
    }

    @Test fun `today is flagged and says so to a screen reader`() {
        val days = marks(periods = listOf(PeriodRecord(1, today, today)))
        val d = days.getValue(18)
        assertTrue(d.isToday)
        assertEquals("18, period, today", d.accessibilityLabel)
    }

    @Test fun `every marked day describes itself in words`() {
        // §43: a grid of bare numbers carries none of the calendar's information.
        val days = marks(
            periods = listOf(PeriodRecord(1, LocalDate.of(2026, 8, 3), LocalDate.of(2026, 8, 5))),
            spotting = listOf(SpottingRecord(1, LocalDate.of(2026, 8, 10))),
            prediction = forecast(LocalDate.of(2026, 8, 28), LocalDate.of(2026, 8, 30)),
            fertile = LocalDate.of(2026, 8, 14)..LocalDate.of(2026, 8, 16),
            ovulation = LocalDate.of(2026, 8, 15),
        )
        assertEquals("3, period", days.getValue(3).accessibilityLabel)
        assertEquals("10, spotting", days.getValue(10).accessibilityLabel)
        assertEquals("28, period predicted", days.getValue(28).accessibilityLabel)
        assertEquals("15, estimated ovulation", days.getValue(15).accessibilityLabel)
        assertEquals("14, estimated fertile window", days.getValue(14).accessibilityLabel)
        assertEquals("1", days.getValue(1).accessibilityLabel)
    }
}
