package dev.privacyllc.period.core.notifications

import dev.privacyllc.period.domain.prediction.ConfidenceLabel
import dev.privacyllc.period.domain.prediction.CycleStatus
import dev.privacyllc.period.domain.prediction.FertilityEstimate
import dev.privacyllc.period.domain.prediction.Prediction
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import java.time.LocalDate

/**
 * §29 and §30. The test that matters most is the one about stopping.
 */
class ReminderRulesTest {

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

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

    private fun decide(
        status: CycleStatus,
        prefs: ReminderPreferences = ReminderPreferences(),
        checkIns: Int = 0,
        fertility: FertilityEstimate? = null,
    ) = ReminderRules.decide(status, prefs, checkIns, fertility, today)

    private fun approaching(days: Int) =
        CycleStatus.PeriodApproaching(20, days, forecast(today.plusDays(days.toLong())))

    private fun beyond() = CycleStatus.BeyondForecast(
        cycleDay = 32,
        daysPast = 2,
        originalForecast = today.minusDays(2),
        updated = forecast(today.plusDays(1)),
    )

    // -----------------------------------------------------------------------
    // §30 — do not nag forever
    // -----------------------------------------------------------------------

    @Test fun `the app asks a bounded number of times and then says it will stop`() {
        // The whole reason this rule exists: asking a woman every morning
        // whether her period has started is the app being anxious at her.
        repeat(ReminderRules.MAX_CHECK_INS) { n ->
            val d = decide(beyond(), checkIns = n)
            assertTrue("check-in $n should still ask, got $d", d is ReminderDecision.Send)
            assertEquals(ReminderKind.DID_IT_START, (d as ReminderDecision.Send).kind)
        }

        assertEquals(
            ReminderDecision.StopAsking,
            decide(beyond(), checkIns = ReminderRules.MAX_CHECK_INS),
        )
    }

    @Test fun `after the stopping message nothing further is sent`() {
        // Exactly once. A "we'll stop checking" that arrives three days running
        // is worse than the nagging it replaced.
        (ReminderRules.MAX_CHECK_INS + 1..ReminderRules.MAX_CHECK_INS + 5).forEach { n ->
            assertEquals("at $n", ReminderDecision.Nothing, decide(beyond(), checkIns = n))
        }
    }

    // -----------------------------------------------------------------------
    // §30's flow
    // -----------------------------------------------------------------------

    @Test fun `the heads-up goes out a few days before and not every day`() {
        assertTrue(decide(approaching(ReminderRules.APPROACHING_LEAD_DAYS)) is ReminderDecision.Send)
        // Not on the days either side, or the user gets four notifications for
        // one period.
        assertEquals(ReminderDecision.Nothing, decide(approaching(ReminderRules.APPROACHING_LEAD_DAYS + 1)))
        assertEquals(ReminderDecision.Nothing, decide(approaching(ReminderRules.APPROACHING_LEAD_DAYS - 1)))
    }

    @Test fun `the predicted day asks whether it started`() {
        val d = decide(CycleStatus.PredictedDay(28, forecast(today)))
        assertEquals(ReminderKind.PERIOD_EXPECTED_TODAY, (d as ReminderDecision.Send).kind)
    }

    @Test fun `a period that has run a few days asks whether it is over`() {
        val early = CycleStatus.DuringPeriod(2, today.minusDays(1), null, null)
        assertEquals(ReminderDecision.Nothing, decide(early))

        val later = CycleStatus.DuringPeriod(
            ReminderRules.END_CHECK_IN_AFTER_DAYS, today.minusDays(4), null, null,
        )
        assertEquals(ReminderKind.PERIOD_END_CHECK_IN, (decide(later) as ReminderDecision.Send).kind)
    }

    @Test fun `a period already marked ended is not asked about`() {
        val ended = CycleStatus.DuringPeriod(6, today.minusDays(5), today, null)
        assertEquals(ReminderDecision.Nothing, decide(ended))
    }

    // -----------------------------------------------------------------------
    // §29 — the toggles are independent
    // -----------------------------------------------------------------------

    @Test fun `every type can be switched off on its own`() {
        assertEquals(
            ReminderDecision.Nothing,
            decide(approaching(3), ReminderPreferences(periodApproaching = false)),
        )
        assertEquals(
            ReminderDecision.Nothing,
            decide(CycleStatus.PredictedDay(28, forecast(today)), ReminderPreferences(periodExpectedToday = false)),
        )
        assertEquals(
            ReminderDecision.Nothing,
            decide(beyond(), ReminderPreferences(didItStart = false), checkIns = 0),
        )
    }

    @Test fun `fertility reminders are off unless asked for`() {
        val fertility = FertilityEstimate.from(forecast(today.plusDays(14)))!!
        val onOvulation = fertility.ovulationDate

        // Default preferences: fertility off. Somebody not tracking fertility
        // should never be told about ovulation.
        assertEquals(
            ReminderDecision.Nothing,
            ReminderRules.decide(
                CycleStatus.BetweenPeriodAndFertile(14, 14, null),
                ReminderPreferences(),
                0,
                fertility,
                onOvulation,
            ),
        )

        val d = ReminderRules.decide(
            CycleStatus.BetweenPeriodAndFertile(14, 14, null),
            ReminderPreferences(ovulationEstimated = true),
            0,
            fertility,
            onOvulation,
        )
        assertEquals(ReminderKind.OVULATION_ESTIMATED, (d as ReminderDecision.Send).kind)
    }

    @Test fun `a period notification outranks a fertility one on the same day`() {
        // Two notifications in one morning is how both get muted.
        val fertility = FertilityEstimate.from(forecast(today.plusDays(14)))!!
        val d = ReminderRules.decide(
            CycleStatus.PredictedDay(28, forecast(today)),
            ReminderPreferences(ovulationEstimated = true),
            0,
            fertility,
            fertility.ovulationDate,
        )
        assertEquals(ReminderKind.PERIOD_EXPECTED_TODAY, (d as ReminderDecision.Send).kind)
    }

    @Test fun `no history means no reminders at all`() {
        assertEquals(ReminderDecision.Nothing, decide(CycleStatus.NoData))
    }
}
