package dev.privacyllc.period.domain.cycle

import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import java.time.LocalDate

/**
 * §27's rule, which is easier to state than to keep: **do not overstate what the
 * app has learned.** Most of these tests are about staying quiet.
 */
class CycleInsightsTest {

    private fun records(vararg gaps: Long, duration: Long? = null): List<PeriodRecord> {
        var cursor = LocalDate.of(2026, 1, 1)
        val out = mutableListOf(cursor)
        gaps.forEach { cursor = cursor.plusDays(it); out += cursor }
        return out.mapIndexed { i, d ->
            PeriodRecord(id = i.toLong(), startDate = d, endDate = duration?.let { d.plusDays(it - 1) })
        }
    }

    @Test fun `no history says so rather than showing zeroes`() {
        val i = CycleInsights.from(emptyList())
        assertEquals(LearningStage.NO_DATA, i.stage)
        assertNull(i.averageCycleDays)
        assertNull(i.typicalCycleRange)
        assertTrue(i.recentCycleLengths.isEmpty())
    }

    @Test fun `one interval is an anecdote, not an average`() {
        val i = CycleInsights.from(records(29))
        assertEquals(LearningStage.LEARNING, i.stage)
        assertEquals(1, i.cycleCount)
        // "Average cycle: 29.0 days" from a single observation is the app
        // claiming a pattern it has seen once.
        assertNull(i.averageCycleDays)
        assertNull(i.typicalCycleRange)
    }

    @Test fun `averages appear from two intervals`() {
        val i = CycleInsights.from(records(28, 30))
        assertEquals(29.0, i.averageCycleDays!!, 0.001)
        assertEquals(LearningStage.GETTING_TO_KNOW, i.stage)
    }

    @Test fun `the learning stage tracks the amount of history`() {
        assertEquals(LearningStage.GETTING_TO_KNOW, CycleInsights.from(records(29, 28, 30)).stage)
        assertEquals(
            LearningStage.PERSONALIZED,
            CycleInsights.from(records(29, 28, 30, 29, 28)).stage,
        )
    }

    @Test fun `recent cycles are newest first and capped`() {
        val i = CycleInsights.from(records(21, 22, 23, 24, 25, 26, 27, 28))
        assertEquals(CycleInsights.RECENT_COUNT, i.recentCycleLengths.size)
        assertEquals(28, i.recentCycleLengths.first())
        assertEquals(23, i.recentCycleLengths.last())
    }

    @Test fun `one long cycle does not become the typical range`() {
        // Reporting 28..61 as "typical" would describe her as far more erratic
        // than she is, on the screen whose entire job is to say what has been
        // learned about her.
        val i = CycleInsights.from(records(29, 28, 30, 29, 61, 29))
        val range = i.typicalCycleRange!!
        assertTrue("range was $range", range.last <= 32)
    }

    @Test fun `period duration needs more than one closed period`() {
        assertNull(CycleInsights.from(records(29, duration = null)).averagePeriodDays)
        assertNull(CycleInsights.from(records(29)).averagePeriodDays)

        val i = CycleInsights.from(records(29, 28, duration = 5))
        assertEquals(5.0, i.averagePeriodDays!!, 0.001)
    }

    @Test fun `implausible intervals are excluded from what is reported`() {
        // A 200-day gap is a user returning after a year, not a cycle, and
        // averaging it in would make every figure on the screen wrong.
        val i = CycleInsights.from(records(29, 28, 200, 29))
        assertTrue("200 must not appear", i.recentCycleLengths.none { it > 90 })
        assertTrue(i.averageCycleDays!! < 40)
    }
}
