package com.onthelevel.core.audio

import com.onthelevel.core.sensors.SurfacePresentation
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test

class SurfaceTickPolicyTest {

    private fun input(
        error: Double = 2.0,
        now: Long = 0,
        enabled: Boolean = true,
        foreground: Boolean = true,
        placementOk: Boolean = true,
        presentation: SurfacePresentation? = SurfacePresentation.FACE_UP,
        locked: Boolean = false,
    ) = SurfaceTickPolicy.Input(
        isEnabled = enabled,
        isAppForeground = foreground,
        placementOk = placementOk,
        surfacePresentation = presentation,
        errorDegrees = error,
        isLocked = locked,
        nowMillis = now,
    )

    @Test
    fun `rate hits both anchors and clamps beyond them`() {
        val p = SurfaceTickPolicy()
        assertEquals(8.0, p.tickRatePerSecond(0.4), 1e-9)
        assertEquals(1.5, p.tickRatePerSecond(5.0), 1e-9)
        assertEquals(8.0, p.tickRatePerSecond(0.1), 1e-9) // clamp near
        assertEquals(1.5, p.tickRatePerSecond(12.0), 1e-9) // clamp far
    }

    @Test
    fun `rate is monotonic - closer is always faster`() {
        val p = SurfaceTickPolicy()
        val rates = listOf(0.4, 1.0, 2.0, 3.0, 4.0, 5.0).map { p.tickRatePerSecond(it) }
        for (i in 1 until rates.size) assertTrue("rates must decrease with error", rates[i] < rates[i - 1])
    }

    @Test
    fun `silent and reset while disabled, backgrounded, or off-surface`() {
        val p = SurfaceTickPolicy()
        assertNull(p.update(input(enabled = false)))
        assertNull(p.update(input(foreground = false)))
        assertNull(p.update(input(placementOk = false)))
        assertNull(p.update(input(presentation = SurfacePresentation.NEAR_VERTICAL)))
    }

    @Test
    fun `re-entry ticks immediately`() {
        val p = SurfaceTickPolicy()
        assertEquals(SurfaceTickPolicy.Cue.TICK, p.update(input(error = 2.0, now = 0)))
        // Gate off, then back on: immediate tick again.
        assertNull(p.update(input(enabled = false, now = 10)))
        assertEquals(SurfaceTickPolicy.Cue.TICK, p.update(input(error = 2.0, now = 20)))
    }

    @Test
    fun `ticks on the interval and does not double-fire before it`() {
        val p = SurfaceTickPolicy()
        val interval = p.intervalMillis(2.0)
        assertEquals(SurfaceTickPolicy.Cue.TICK, p.update(input(error = 2.0, now = 0)))
        assertNull(p.update(input(error = 2.0, now = interval - 1)))
        assertEquals(SurfaceTickPolicy.Cue.TICK, p.update(input(error = 2.0, now = interval)))
    }

    @Test
    fun `a long gap yields one tick, not a catch-up burst`() {
        val p = SurfaceTickPolicy()
        val interval = p.intervalMillis(2.0)
        p.update(input(error = 2.0, now = 0)) // first tick, deadline = interval
        // Jump far past many intervals: exactly one tick fires...
        assertEquals(SurfaceTickPolicy.Cue.TICK, p.update(input(error = 2.0, now = 10_000)))
        // ...and the next deadline is in the future, so the very next update is silent.
        assertNull(p.update(input(error = 2.0, now = 10_001)))
    }

    @Test
    fun `deadline advances without drifting slow`() {
        val p = SurfaceTickPolicy()
        val interval = p.intervalMillis(2.0)
        var ticks = 0
        // Poll every 20 ms (like the sensor); count ticks over 10 nominal intervals.
        val end = interval * 10
        var now = 0L
        while (now <= end) {
            if (p.update(input(error = 2.0, now = now)) == SurfaceTickPolicy.Cue.TICK) ticks++
            now += 20
        }
        // Should be ~10 ticks (first is immediate), not fewer from accumulated lateness.
        assertTrue("expected ~10-11 ticks, got $ticks", ticks in 10..11)
    }

    @Test
    fun `level fires once per lock, then silence, then ticks resume on unlock`() {
        val p = SurfaceTickPolicy()
        p.update(input(error = 2.0, now = 0)) // ticking
        assertEquals(SurfaceTickPolicy.Cue.LEVEL, p.update(input(locked = true, now = 100)))
        assertNull(p.update(input(locked = true, now = 200)))
        assertNull(p.update(input(locked = true, now = 5_000)))
        // Unlock: immediate tick again.
        assertEquals(SurfaceTickPolicy.Cue.TICK, p.update(input(error = 2.0, locked = false, now = 5_100)))
    }
}
