package dev.privacyllc.period.domain.prediction

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

/**
 * PRODUCT_PLAN.md §45, for the leak that needs no logging statement.
 *
 * `checkNoHealthLogging` in the root build script stops anyone writing a `Log.d`
 * in a module that can see a cycle date. It cannot see two other routes to the
 * same place, and both were open before this test existed:
 *
 *  - **A data class renders its own contents.** `"bad record: $record"` in an
 *    exception message, a crash payload, or a coroutine's diagnostics prints
 *    every field. Nobody has to type a date for a date to be printed.
 *  - **`require` messages are crash reports.** `Prediction`'s init block
 *    interpolated five predicted dates across three messages — the one string
 *    guaranteed to be collected by any reporter, without anybody choosing to
 *    log it.
 *
 * The dates below are distinctive on purpose: 2026-08-18 renders as `2026-08-18`
 * and its parts as `2026`, `08`, `18`, so a match is a real leak rather than a
 * coincidence with a field name.
 */
class NoDatesInDiagnosticsTest {

    private val date = LocalDate.of(2026, 8, 18)
    private val later = LocalDate.of(2026, 9, 15)

    /** The rendered ISO date, which is what interpolation produces. */
    private fun String.leaksA(d: LocalDate) = contains(d.toString())

    @Test fun `PeriodRecord does not render its dates`() {
        val s = PeriodRecord(id = 7, startDate = date, endDate = later).toString()
        assertFalse("PeriodRecord.toString leaked a start date: $s", s.leaksA(date))
        assertFalse("PeriodRecord.toString leaked an end date: $s", s.leaksA(later))
        // The id survives, because it identifies the row without describing the person.
        assertTrue("the id is what makes a diagnostic useful: $s", s.contains("id=7"))
    }

    @Test fun `SpottingRecord does not render its date`() {
        val s = SpottingRecord(id = 3, date = date).toString()
        assertFalse(s, s.leaksA(date))
    }

    @Test fun `CycleRecord renders lengths but not dates`() {
        val s = CycleRecord(
            previousPeriodStart = date,
            currentPeriodStart = later,
            cycleLengthDays = 28,
        ).toString()
        assertFalse(s, s.leaksA(date))
        assertFalse(s, s.leaksA(later))
        assertTrue("a cycle length says nothing about when: $s", s.contains("28"))
    }

    @Test fun `Prediction does not render its forecast`() {
        val s = Prediction(
            mostLikelyStartDate = date,
            windowStart = date.minusDays(3),
            windowEnd = date.plusDays(3),
            confidenceScore = 0.5,
            confidenceLabel = ConfidenceLabel.MEDIUM,
            modelVersion = "test",
        ).toString()
        assertFalse(s, s.leaksA(date))
        assertFalse(s, s.leaksA(date.minusDays(3)))
        assertFalse(s, s.leaksA(date.plusDays(3)))
    }

    @Test fun `NotYetObservation does not render its date`() {
        val s = NotYetObservation(date = date, predictionId = 4).toString()
        assertFalse(s, s.leaksA(date))
    }

    /**
     * The invariant failures, which are the ones that reach a crash reporter.
     */
    @Test fun `a window in the wrong order fails without naming the dates`() {
        val e = runCatching {
            Prediction(
                mostLikelyStartDate = date,
                windowStart = later,
                windowEnd = date,
                confidenceScore = 0.5,
                confidenceLabel = ConfidenceLabel.LOW,
                modelVersion = "test",
            )
        }.exceptionOrNull()

        val message = e?.message.orEmpty()
        assertTrue("the invariant should still be named: $message", message.isNotBlank())
        assertFalse("require message leaked a date: $message", message.leaksA(date))
        assertFalse("require message leaked a date: $message", message.leaksA(later))
    }

    @Test fun `a most-likely date outside the window fails without naming the dates`() {
        val e = runCatching {
            Prediction(
                mostLikelyStartDate = later,
                windowStart = date.minusDays(1),
                windowEnd = date.plusDays(1),
                confidenceScore = 0.5,
                confidenceLabel = ConfidenceLabel.LOW,
                modelVersion = "test",
            )
        }.exceptionOrNull()

        val message = e?.message.orEmpty()
        assertTrue(message.isNotBlank())
        assertFalse("require message leaked a date: $message", message.leaksA(later))
        assertFalse("require message leaked a date: $message", message.leaksA(date.minusDays(1)))
        assertFalse("require message leaked a date: $message", message.leaksA(date.plusDays(1)))
    }
}
