package dev.privacyllc.period.domain.prediction

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

/**
 * §17 and §18.
 *
 * Most of these are about **not** claiming things: not a crisp ovulation date
 * off a vague forecast, not a window when there is no forecast at all, and never
 * a word that reads as permission.
 */
class FertilityEstimateTest {

    private fun forecast(on: String, halfWidth: Long) = LocalDate.parse(on).let { d ->
        Prediction(
            mostLikelyStartDate = d,
            windowStart = d.minusDays(halfWidth),
            windowEnd = d.plusDays(halfWidth),
            confidenceScore = 0.6,
            confidenceLabel = ConfidenceLabel.MEDIUM,
            modelVersion = "test",
        )
    }

    @Test fun `no forecast means no fertility estimate at all`() {
        // §17 hangs the estimate off the predicted next period. Without one, a
        // day-14 guess would be exactly the fixed-rule implication §17 rules out.
        assertNull(FertilityEstimate.from(null))
    }

    @Test fun `ovulation sits a luteal phase before the predicted start`() {
        val e = FertilityEstimate.from(forecast("2026-09-01", 1))!!
        assertEquals(LocalDate.of(2026, 8, 18), e.ovulationDate)
        assertEquals(FertilityEstimate.DEFAULT_LUTEAL_PHASE_DAYS, e.lutealPhaseDays)
    }

    @Test fun `the luteal phase is an assumption that can be changed`() {
        val e = FertilityEstimate.from(forecast("2026-09-01", 1), lutealPhaseDays = 12)!!
        assertEquals(LocalDate.of(2026, 8, 20), e.ovulationDate)
    }

    @Test fun `the window opens before ovulation and closes just after it`() {
        val e = FertilityEstimate.from(forecast("2026-09-01", 0))!!
        // Sperm survive several days, the egg about one. An estimate centred on
        // ovulation would be biologically wrong in the direction that matters.
        assertEquals(e.ovulationDate.minusDays(5), e.fertileWindowStart)
        assertEquals(e.ovulationDate.plusDays(1), e.fertileWindowEnd)
        assertTrue(e.ovulationDate in e.fertileWindow)
    }

    @Test fun `a vaguer forecast produces a wider window`() {
        // Ovulation is derived from a predicted date, so it can never be more
        // certain than that prediction.
        val tight = FertilityEstimate.from(forecast("2026-09-01", 1))!!
        val looser = FertilityEstimate.from(forecast("2026-09-01", 3))!!

        fun width(e: FertilityEstimate) =
            e.fertileWindowEnd.toEpochDay() - e.fertileWindowStart.toEpochDay()

        assertTrue("looser ${width(looser)} must exceed tight ${width(tight)}", width(looser) > width(tight))
        assertEquals(1, tight.uncertaintyDays)
        assertEquals(3, looser.uncertaintyDays)
    }

    @Test fun `a forecast too vague to locate ovulation produces no estimate at all`() {
        // The window is seven days wide before any uncertainty is added, so a
        // forecast carrying ±5 gives a seventeen-day "fertile window" — over
        // half a cycle. It was on screen before anybody noticed: a user one
        // cycle in was shown 8 Aug – 24 Aug. Declining is the better answer.
        assertNull(FertilityEstimate.from(forecast("2026-09-01", 5)))
        assertNull(FertilityEstimate.from(forecast("2026-09-01", 8)))
        assertNotNull(FertilityEstimate.from(forecast("2026-09-01", 3)))
    }

    @Test fun `an estimate is never wider than about a fortnight`() {
        (0L..FertilityEstimate.MAX_USEFUL_UNCERTAINTY_DAYS.toLong()).forEach { half ->
            val e = FertilityEstimate.from(forecast("2026-09-01", half))!!
            val width = e.fertileWindowEnd.toEpochDay() - e.fertileWindowStart.toEpochDay()
            assertTrue("half=$half gave a $width-day window", width <= 13)
        }
    }

    @Test fun `the estimate moves with a re-conditioned forecast`() {
        // A "Not yet" moves the period forecast. Pinning ovulation to the old
        // one would have the same screen contradict itself.
        val before = FertilityEstimate.from(forecast("2026-09-01", 1))!!
        val after = FertilityEstimate.from(forecast("2026-09-04", 1))!!
        assertEquals(3, after.ovulationDate.toEpochDay() - before.ovulationDate.toEpochDay())
    }

    // -----------------------------------------------------------------------
    // §18's vocabulary
    // -----------------------------------------------------------------------

    @Test fun `days inside the window are higher likelihood and days outside are lower`() {
        val e = FertilityEstimate.from(forecast("2026-09-01", 1))!!
        assertEquals(FertilityLikelihood.HIGHER, e.likelihoodOn(e.ovulationDate))
        assertEquals(FertilityLikelihood.HIGHER, e.likelihoodOn(e.fertileWindowStart))
        assertEquals(FertilityLikelihood.HIGHER, e.likelihoodOn(e.fertileWindowEnd))
        assertEquals(FertilityLikelihood.LOWER, e.likelihoodOn(e.fertileWindowStart.minusDays(1)))
        assertEquals(FertilityLikelihood.LOWER, e.likelihoodOn(e.fertileWindowEnd.plusDays(1)))
    }

    @Test fun `no label anywhere reads as permission`() {
        // §18: avoid "safe" or "unsafe". The type has no such value, and this
        // test exists so adding one is a deliberate act with a failing test.
        val words = FertilityLikelihood.entries.map { it.label.lowercase() } +
            FertilityLikelihood.entries.map { it.name.lowercase() }
        assertTrue(
            "a fertility label contained a permission word: $words",
            words.none { it.contains("safe") || it.contains("unsafe") || it.contains("protect") },
        )
    }

    @Test fun `an estimate always contains its own ovulation day`() {
        // The constructor enforces it; this covers the constants drifting apart.
        (0L..FertilityEstimate.MAX_USEFUL_UNCERTAINTY_DAYS.toLong()).forEach { half ->
            val e = FertilityEstimate.from(forecast("2026-09-01", half))!!
            assertTrue("half=$half", e.ovulationDate in e.fertileWindow)
        }
    }
}
