package app.closer.domain

import app.closer.domain.model.StreakInput
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import java.time.LocalDate

class StreakCalculatorTest {

    private val today = LocalDate.of(2026, 6, 19)

    // -----------------------------------------------------------------
    // Couple streak
    // -----------------------------------------------------------------

    @Test
    fun `couple streak is zero when no shared actions`() {
        val input = StreakInput(
            userDates = emptyList(),
            partnerDates = emptyList(),
            coupleActionDates = emptyList(),
            today = today
        )

        val result = StreakCalculator.calculate(input)

        assertEquals(0, result.coupleStreak.count)
        assertNull(result.coupleStreak.lastActiveDate)
        assertFalse(result.coupleStreak.includesToday)
    }

    @Test
    fun `couple streak counts consecutive shared days including today`() {
        val input = StreakInput(
            userDates = listOf(today, today.minusDays(1), today.minusDays(2)),
            partnerDates = listOf(today, today.minusDays(1), today.minusDays(2)),
            coupleActionDates = listOf(today, today.minusDays(1), today.minusDays(2)),
            today = today
        )

        val result = StreakCalculator.calculate(input)

        assertEquals(3, result.coupleStreak.count)
        assertEquals(today, result.coupleStreak.lastActiveDate)
        assertTrue(result.coupleStreak.includesToday)
    }

    @Test
    fun `couple streak breaks when gap is larger than one day`() {
        val input = StreakInput(
            userDates = listOf(today, today.minusDays(3)),
            partnerDates = listOf(today, today.minusDays(3)),
            coupleActionDates = listOf(today, today.minusDays(3)),
            today = today
        )

        val result = StreakCalculator.calculate(input)

        assertEquals(1, result.coupleStreak.count)
        assertEquals(today, result.coupleStreak.lastActiveDate)
    }

    @Test
    fun `couple streak is counted from yesterday when today has no action`() {
        val input = StreakInput(
            userDates = listOf(today.minusDays(1), today.minusDays(2)),
            partnerDates = listOf(today.minusDays(1), today.minusDays(2)),
            coupleActionDates = listOf(today.minusDays(1), today.minusDays(2)),
            today = today
        )

        val result = StreakCalculator.calculate(input)

        assertEquals(2, result.coupleStreak.count)
        assertEquals(today.minusDays(1), result.coupleStreak.lastActiveDate)
        assertFalse(result.coupleStreak.includesToday)
    }

    // -----------------------------------------------------------------
    // Personal streak
    // -----------------------------------------------------------------

    @Test
    fun `personal streak counts consecutive user-only days including today`() {
        val input = StreakInput(
            userDates = listOf(today, today.minusDays(1), today.minusDays(2)),
            partnerDates = emptyList(),
            coupleActionDates = emptyList(),
            today = today
        )

        val result = StreakCalculator.calculate(input)

        assertEquals(3, result.personalStreak.count)
        assertEquals(today, result.personalStreak.lastActiveDate)
        assertTrue(result.personalStreak.includesToday)
    }

    @Test
    fun `personal streak is independent of couple streak and breaks on missed day`() {
        val input = StreakInput(
            userDates = listOf(today, today.minusDays(1), today.minusDays(3)),
            partnerDates = emptyList(),
            coupleActionDates = emptyList(),
            today = today
        )

        val result = StreakCalculator.calculate(input)

        assertEquals(2, result.personalStreak.count)
        assertEquals(today, result.personalStreak.lastActiveDate)
    }

    @Test
    fun `personal streak resets when latest day is older than yesterday`() {
        val input = StreakInput(
            userDates = listOf(today.minusDays(2), today.minusDays(3)),
            partnerDates = emptyList(),
            coupleActionDates = emptyList(),
            today = today
        )

        val result = StreakCalculator.calculate(input)

        assertEquals(0, result.personalStreak.count)
        assertNull(result.personalStreak.lastActiveDate)
    }

    // -----------------------------------------------------------------
    // Weekly rhythm
    // -----------------------------------------------------------------

    @Test
    fun `weekly rhythm is zero when couple actions are below threshold`() {
        val input = StreakInput(
            userDates = listOf(today, today.minusDays(1)),
            partnerDates = listOf(today, today.minusDays(1)),
            coupleActionDates = listOf(today, today.minusDays(1)),
            today = today
        )

        val result = StreakCalculator.calculate(input)

        assertEquals(0, result.weeklyRhythm.count)
    }

    @Test
    fun `weekly rhythm counts one window when threshold met in last seven days`() {
        val actions = listOf(today, today.minusDays(2), today.minusDays(4))
        val input = StreakInput(
            userDates = actions,
            partnerDates = actions,
            coupleActionDates = actions,
            today = today
        )

        val result = StreakCalculator.calculate(input)

        assertEquals(1, result.weeklyRhythm.count)
        assertTrue(result.weeklyRhythm.includesToday)
    }

    @Test
    fun `weekly rhythm counts consecutive windows`() {
        val actions = (
            listOf(today, today.minusDays(1), today.minusDays(2)) +
                listOf(today.minusDays(7), today.minusDays(8), today.minusDays(9))
        )
        val input = StreakInput(
            userDates = actions,
            partnerDates = actions,
            coupleActionDates = actions,
            today = today
        )

        val result = StreakCalculator.calculate(input)

        assertEquals(2, result.weeklyRhythm.count)
    }

    // -----------------------------------------------------------------
    // Milestones
    // -----------------------------------------------------------------

    @Test
    fun `milestone copy returns correct text for day one`() {
        val input = StreakInput(
            userDates = listOf(today),
            partnerDates = listOf(today),
            coupleActionDates = listOf(today),
            today = today
        )

        val result = StreakCalculator.calculate(input)

        assertEquals("You started a rhythm.", result.milestoneCopy)
    }

    @Test
    fun `milestone copy returns correct text for day three`() {
        val input = StreakInput(
            userDates = listOf(today, today.minusDays(1), today.minusDays(2)),
            partnerDates = listOf(today, today.minusDays(1), today.minusDays(2)),
            coupleActionDates = listOf(today, today.minusDays(1), today.minusDays(2)),
            today = today
        )

        val result = StreakCalculator.calculate(input)

        assertEquals("Three nights showing up.", result.milestoneCopy)
    }

    @Test
    fun `milestone copy returns correct text for day seven`() {
        val dates = (0..6).map { today.minusDays(it.toLong()) }
        val input = StreakInput(
            userDates = dates,
            partnerDates = dates,
            coupleActionDates = dates,
            today = today
        )

        val result = StreakCalculator.calculate(input)

        assertEquals("One week of small conversations.", result.milestoneCopy)
    }

    @Test
    fun `milestone copy returns correct text for day thirty`() {
        val dates = (0..29).map { today.minusDays(it.toLong()) }
        val input = StreakInput(
            userDates = dates,
            partnerDates = dates,
            coupleActionDates = dates,
            today = today
        )

        val result = StreakCalculator.calculate(input)

        assertEquals("Thirty days of making space for each other.", result.milestoneCopy)
    }

    @Test
    fun `milestone copy is null for non milestone days`() {
        val input = StreakInput(
            userDates = listOf(today, today.minusDays(1)),
            partnerDates = listOf(today, today.minusDays(1)),
            coupleActionDates = listOf(today, today.minusDays(1)),
            today = today
        )

        val result = StreakCalculator.calculate(input)

        assertNull(result.milestoneCopy)
    }

    @Test
    fun `milestone copy is null when streak does not include today`() {
        val input = StreakInput(
            userDates = listOf(today.minusDays(1), today.minusDays(2), today.minusDays(3)),
            partnerDates = listOf(today.minusDays(1), today.minusDays(2), today.minusDays(3)),
            coupleActionDates = listOf(today.minusDays(1), today.minusDays(2), today.minusDays(3)),
            today = today
        )

        val result = StreakCalculator.calculate(input)

        assertNull(result.milestoneCopy)
    }

    // -----------------------------------------------------------------
    // Repair
    // -----------------------------------------------------------------

    @Test
    fun `repair is available when yesterday was missed and today is a couple day`() {
        val input = StreakInput(
            userDates = listOf(today, today.minusDays(2)),
            partnerDates = listOf(today, today.minusDays(2)),
            coupleActionDates = listOf(today, today.minusDays(2)),
            today = today
        )

        val result = StreakCalculator.calculate(input)

        assertTrue(result.canRepair)
        assertEquals(today.minusDays(1), result.repairDueDate)
    }

    @Test
    fun `repair is not available when no day was missed`() {
        val input = StreakInput(
            userDates = listOf(today, today.minusDays(1), today.minusDays(2)),
            partnerDates = listOf(today, today.minusDays(1), today.minusDays(2)),
            coupleActionDates = listOf(today, today.minusDays(1), today.minusDays(2)),
            today = today
        )

        val result = StreakCalculator.calculate(input)

        assertFalse(result.canRepair)
        assertNull(result.repairDueDate)
    }

    @Test
    fun `repair is not available when today is not a couple day`() {
        val input = StreakInput(
            userDates = listOf(today.minusDays(2)),
            partnerDates = listOf(today.minusDays(2)),
            coupleActionDates = listOf(today.minusDays(2)),
            today = today
        )

        val result = StreakCalculator.calculate(input)

        assertFalse(result.canRepair)
        assertNull(result.repairDueDate)
    }

    @Test
    fun `repair is not available more than once per seven day window`() {
        val input = StreakInput(
            userDates = listOf(today, today.minusDays(2), today.minusDays(5)),
            partnerDates = listOf(today, today.minusDays(2), today.minusDays(5)),
            coupleActionDates = listOf(today, today.minusDays(2), today.minusDays(5)),
            today = today,
            lastRepairDate = today.minusDays(3)
        )

        val result = StreakCalculator.calculate(input)

        assertFalse(result.canRepair)
        assertNull(result.repairDueDate)
    }

    @Test
    fun `repair is available again after seven days since last repair`() {
        val input = StreakInput(
            userDates = listOf(today, today.minusDays(2)),
            partnerDates = listOf(today, today.minusDays(2)),
            coupleActionDates = listOf(today, today.minusDays(2)),
            today = today,
            lastRepairDate = today.minusDays(8)
        )

        val result = StreakCalculator.calculate(input)

        assertTrue(result.canRepair)
        assertEquals(today.minusDays(1), result.repairDueDate)
    }

    // -----------------------------------------------------------------
    // Edge cases and determinism
    // -----------------------------------------------------------------

    @Test
    fun `result is deterministic for unsorted input lists`() {
        val input = StreakInput(
            userDates = listOf(today.minusDays(2), today, today.minusDays(1)),
            partnerDates = listOf(today, today.minusDays(2), today.minusDays(1)),
            coupleActionDates = listOf(today.minusDays(1), today.minusDays(2), today),
            today = today
        )

        val first = StreakCalculator.calculate(input)
        val second = StreakCalculator.calculate(input)

        assertEquals(first, second)
        assertEquals(3, first.coupleStreak.count)
    }

    @Test
    fun `couple streak and personal streak do not conflict`() {
        val input = StreakInput(
            userDates = listOf(today, today.minusDays(1), today.minusDays(2), today.minusDays(3)),
            partnerDates = listOf(today, today.minusDays(1)),
            coupleActionDates = listOf(today, today.minusDays(1)),
            today = today
        )

        val result = StreakCalculator.calculate(input)

        assertEquals(2, result.coupleStreak.count)
        assertEquals(4, result.personalStreak.count)
    }

    @Test
    fun `timezone behavior uses local date only`() {
        // The calculator itself only sees LocalDate; callers must convert.
        // This test documents that behavior.
        val localToday = LocalDate.of(2026, 6, 19)
        val input = StreakInput(
            userDates = listOf(localToday),
            partnerDates = listOf(localToday),
            coupleActionDates = listOf(localToday),
            today = localToday
        )

        val result = StreakCalculator.calculate(input)

        assertTrue(result.coupleStreak.includesToday)
        assertEquals(localToday, result.coupleStreak.lastActiveDate)
    }
}
