package dev.privacyllc.period.core.data

import androidx.room.Room
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import dev.privacyllc.period.core.database.PeriodDatabase
import dev.privacyllc.period.domain.prediction.BaselinePredictionEngine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import java.time.Clock
import java.time.LocalDate
import java.time.ZoneOffset

/**
 * The acceptance check for the Batch 01 vertical slice, on a real device,
 * against a real file-backed database.
 *
 * The unit tests use an in-memory database, which proves the logic and proves
 * nothing about persistence: an in-memory database cannot fail to survive a
 * restart because it was never expected to. This one closes the database and
 * reopens it from disk, which is what a force-stop actually does to the app.
 */
@RunWith(AndroidJUnit4::class)
class PeriodCrudTest {

    private val context = InstrumentationRegistry.getInstrumentation().targetContext
    private val dbName = "crud-test.db"
    private val clock = Clock.fixed(
        LocalDate.of(2026, 8, 18).atStartOfDay(ZoneOffset.UTC).toInstant(),
        ZoneOffset.UTC,
    )

    private lateinit var db: PeriodDatabase
    private lateinit var repo: CycleRepository

    private fun open() {
        db = Room.databaseBuilder(context, PeriodDatabase::class.java, dbName).build()
        repo = CycleRepository(db, BaselinePredictionEngine(), clock)
    }

    @Before fun setUp() {
        context.deleteDatabase(dbName)
        open()
    }

    @After fun tearDown() {
        db.close()
        context.deleteDatabase(dbName)
    }

    @Test
    fun addEditDeleteAndSurviveRestart() = runTest {
        // --- add three starts -------------------------------------------------
        repo.confirmPeriodStart(LocalDate.of(2026, 6, 1))
        repo.confirmPeriodStart(LocalDate.of(2026, 6, 30))
        repo.confirmPeriodStart(LocalDate.of(2026, 7, 29))

        // Regression: a repeat of a date already logged must be a value, not a
        // crash. This is the interaction that killed the app on a device.
        assertTrue(
            repo.confirmPeriodStart(LocalDate.of(2026, 7, 29)) is PeriodWriteResult.AlreadyRecorded,
        )

        assertEquals(3, repo.confirmedPeriods.first().size)
        assertEquals(listOf(29, 29), repo.cycles.first().map { it.cycleLengthDays })

        val afterAdd = repo.forecast.first()
        assertNotNull("three confirmed starts must produce a forecast", afterAdd)
        assertEquals(LocalDate.of(2026, 8, 27), afterAdd!!.mostLikelyStartDate)

        // --- editing changes the forecast ------------------------------------
        val latest = repo.confirmedPeriods.first().last()
        repo.editPeriod(latest.id, LocalDate.of(2026, 8, 2), null)

        val afterEdit = repo.forecast.first()!!
        assertNotEquals(
            "editing a record must move the forecast, not leave a stale one",
            afterAdd.mostLikelyStartDate,
            afterEdit.mostLikelyStartDate,
        )
        assertEquals(listOf(29, 33), repo.cycles.first().map { it.cycleLengthDays })

        // --- deleting changes it again ---------------------------------------
        val middle = repo.confirmedPeriods.first()[1]
        repo.deletePeriod(middle.id)

        assertEquals(2, repo.confirmedPeriods.first().size)
        val afterDelete = repo.forecast.first()!!
        assertNotEquals(afterEdit.mostLikelyStartDate, afterDelete.mostLikelyStartDate)

        // --- close and reopen: what a force-stop does -------------------------
        db.close()
        open()

        val reopened = repo.confirmedPeriods.first()
        assertEquals("records must survive the process dying", 2, reopened.size)
        assertEquals(
            listOf(LocalDate.of(2026, 6, 1), LocalDate.of(2026, 8, 2)),
            reopened.map { it.startDate },
        )
        assertEquals(
            "the forecast must be identical after a restart — it is derived, not cached",
            afterDelete.mostLikelyStartDate,
            repo.forecast.first()!!.mostLikelyStartDate,
        )
    }

    @Test
    fun deletingAllHealthDataSurvivesRestart() = runTest {
        repo.confirmPeriodStart(LocalDate.of(2026, 6, 1))
        repo.confirmPeriodStart(LocalDate.of(2026, 6, 30))
        repo.recordSpotting(LocalDate.of(2026, 6, 14))

        repo.deleteAllHealthData()
        db.close()
        open()

        // Delete My Data is irreversible after confirmation — §45. If anything
        // came back after a restart, it was never deleted, only hidden.
        assertEquals(0, repo.confirmedPeriods.first().size)
        assertEquals(0, repo.spotting.first().size)
        assertEquals(null, repo.forecast.first())
    }
}
