package me.khodak.claudeusage

import me.khodak.claudeusage.data.UsageData
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Test

/**
 * Pure-JVM tests for the UsageData data class: derived metrics and the mergedWith fallback logic.
 * UsageData has no Android dependencies, so these run on the plain JVM.
 */
class UsageDataTest {

    @Test
    fun progressPercentMessagesPath() {
        // messagesLimit > 0 and effectiveUsed derived from messagesUsed → 25/100 = 25%.
        val d = UsageData(messagesUsed = 25, messagesLimit = 100)
        assertEquals(25, d.progressPercent)
    }

    @Test
    fun progressPercentMessagesPathCoercedTo100() {
        // Used above limit must clamp to 100, never exceed.
        val d = UsageData(messagesUsed = 150, messagesLimit = 100)
        assertEquals(100, d.progressPercent)
    }

    @Test
    fun progressPercentUtilizationPathWhenNoMessages() {
        // No messages limit → fall through to utilization, truncated to Int and coerced.
        val d = UsageData(fiveHourUtilization = 42.9f)
        assertEquals(42, d.progressPercent)
    }

    @Test
    fun progressPercentUtilizationCoercedTo100() {
        val d = UsageData(fiveHourUtilization = 130f)
        assertEquals(100, d.progressPercent)
    }

    @Test
    fun progressPercentZeroWhenNoReadings() {
        assertEquals(0, UsageData().progressPercent)
    }

    @Test
    fun effectiveUsedPrefersMessagesUsed() {
        assertEquals(30, UsageData(messagesUsed = 30, messagesLimit = 100).effectiveUsed)
    }

    @Test
    fun effectiveUsedDerivedFromRemaining() {
        // No messagesUsed, but remaining + limit present → limit - remaining.
        val d = UsageData(messagesRemaining = 40, messagesLimit = 100)
        assertEquals(60, d.effectiveUsed)
    }

    @Test
    fun effectiveUsedUnknownIsNegativeOne() {
        assertEquals(-1, UsageData().effectiveUsed)
    }

    @Test
    fun effectiveRemainingPrefersRemaining() {
        assertEquals(40, UsageData(messagesRemaining = 40, messagesLimit = 100).effectiveRemaining)
    }

    @Test
    fun effectiveRemainingDerivedFromUsed() {
        val d = UsageData(messagesUsed = 70, messagesLimit = 100)
        assertEquals(30, d.effectiveRemaining)
    }

    @Test
    fun effectiveRemainingUnknownIsNegativeOne() {
        assertEquals(-1, UsageData().effectiveRemaining)
    }

    @Test
    fun hasAnyReadingTrueWithUtilization() {
        assertTrue(UsageData(fiveHourUtilization = 10f).hasAnyReading)
        assertTrue(UsageData(weeklyUtilization = 5f).hasAnyReading)
    }

    @Test
    fun hasAnyReadingTrueWithRateLimitData() {
        // hasRateLimitData true when messagesLimit > 0.
        assertTrue(UsageData(messagesLimit = 100).hasAnyReading)
    }

    @Test
    fun hasAnyReadingFalseWhenEmpty() {
        assertFalse(UsageData().hasAnyReading)
    }

    @Test
    fun sessionReadingPctPrefersUtilization() {
        // Utilization present → used verbatim (kept as Float, not truncated), even alongside messages.
        val d = UsageData(fiveHourUtilization = 42.5f, messagesUsed = 10, messagesLimit = 100)
        assertEquals(42.5f, d.sessionReadingPct, 0f)
    }

    @Test
    fun sessionReadingPctFallsBackToMessages() {
        // No utilization → derive from message counts so message-only accounts still get history.
        val d = UsageData(messagesUsed = 30, messagesLimit = 120)
        assertEquals(25f, d.sessionReadingPct, 0f)
    }

    @Test
    fun sessionReadingPctMessagesCoercedTo100() {
        val d = UsageData(messagesUsed = 150, messagesLimit = 100)
        assertEquals(100f, d.sessionReadingPct, 0f)
    }

    @Test
    fun sessionReadingPctNegativeWhenNoReading() {
        // No utilization and no message data → -1f, so recordHistory skips the point.
        assertEquals(-1f, UsageData().sessionReadingPct, 0f)
    }

    @Test
    fun mergedWithEmptyFetchKeepsPreviousMetrics() {
        // (a) previous has a reading; this fetch is empty (all defaults) → previous metrics kept.
        val previous = UsageData(
            fiveHourUtilization = 55f,
            utilizationResetAtEpoch = 1_000L,
            weeklyUtilization = 20f,
            weeklyResetAtEpoch = 2_000L,
            messagesLimit = 100,
            messagesUsed = 40
        )
        val emptyFetch = UsageData(isLoggedIn = true)
        val merged = emptyFetch.mergedWith(previous)

        assertEquals(55f, merged.fiveHourUtilization, 0f)
        assertEquals(20f, merged.weeklyUtilization, 0f)
        assertEquals(100, merged.messagesLimit)
        assertEquals(40, merged.messagesUsed)
        assertEquals(1_000L, merged.utilizationResetAtEpoch)
        assertEquals(2_000L, merged.weeklyResetAtEpoch)
        // Login context from the fresh (empty) attempt is carried forward.
        assertTrue(merged.isLoggedIn)
    }

    @Test
    fun mergedWithPartialFetchUnionsBothWindows() {
        // (b) previous only had five-hour; this fetch only has weekly → result has both.
        val previous = UsageData(
            fiveHourUtilization = 33f,
            utilizationResetAtEpoch = 500L
        )
        val partial = UsageData(
            weeklyUtilization = 77f,
            weeklyResetAtEpoch = 900L
        )
        val merged = partial.mergedWith(previous)

        assertEquals(33f, merged.fiveHourUtilization, 0f)
        assertEquals(500L, merged.utilizationResetAtEpoch)
        assertEquals(77f, merged.weeklyUtilization, 0f)
        assertEquals(900L, merged.weeklyResetAtEpoch)
    }

    @Test
    fun mergedWithNullPreviousReturnsThis() {
        // (c) previous == null → returns this unchanged.
        val fetch = UsageData(fiveHourUtilization = 12f)
        assertSame(fetch, fetch.mergedWith(null))
    }

    @Test
    fun mergedWithPreviousWithoutReadingReturnsThis() {
        // previous has no usable reading → this is returned unchanged.
        val fetch = UsageData(fiveHourUtilization = 12f)
        assertSame(fetch, fetch.mergedWith(UsageData()))
    }
}
