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,
    ) = SurfaceTickPolicy.Input(
        isEnabled = enabled,
        isAppForeground = foreground,
        placementOk = placementOk,
        surfacePresentation = presentation,
        errorDegrees = error,
        nowMillis = now,
    )

    // --- Proximity ticking (outside the center zone) ---

    @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)
        assertEquals(1.5, p.tickRatePerSecond(12.0), 1e-9)
    }

    @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[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 `ticks fire on the current interval and react immediately to changing tilt`() {
        val p = SurfaceTickPolicy()
        assertEquals(SurfaceTickPolicy.Cue.TICK, p.update(input(error = 5.0, now = 0))) // first tick, anchor 0
        // Drop toward center (still outside the 0.2 zone): next tick uses the FAST interval.
        assertNull(p.update(input(error = 0.4, now = 124)))
        assertEquals(SurfaceTickPolicy.Cue.TICK, p.update(input(error = 0.4, now = 125)))
    }

    @Test
    fun `a long gap yields one tick, not a catch-up burst`() {
        val p = SurfaceTickPolicy()
        p.update(input(error = 2.0, now = 0))
        assertEquals(SurfaceTickPolicy.Cue.TICK, p.update(input(error = 2.0, now = 10_000)))
        assertNull(p.update(input(error = 2.0, now = 10_001)))
    }

    // --- Bullseye alignment (inside the center zone) ---

    @Test
    fun `entering the zone announces immediately - even on a fast pass, no dwell`() {
        val p = SurfaceTickPolicy()
        p.update(input(error = 3.0, now = 0)) // ticking, outside
        // One frame later it's already inside (a fast sweep): aligned right away.
        assertEquals(SurfaceTickPolicy.Cue.ALIGNED, p.update(input(error = 0.1, now = 20)))
    }

    @Test
    fun `stays aligned every frame while inside, ticks suppressed`() {
        val p = SurfaceTickPolicy()
        assertEquals(SurfaceTickPolicy.Cue.ALIGNED, p.update(input(error = 0.15, now = 0)))
        for (t in 20..2_000 step 20) {
            assertEquals(SurfaceTickPolicy.Cue.ALIGNED, p.update(input(error = 0.15, now = t.toLong())))
        }
    }

    @Test
    fun `spatial hysteresis - holds aligned to 0_35, resumes ticks past it`() {
        val p = SurfaceTickPolicy()
        assertEquals(SurfaceTickPolicy.Cue.ALIGNED, p.update(input(error = 0.1, now = 0)))
        // 0.3 is past the 0.2 enter but below the 0.35 exit: still aligned.
        assertEquals(SurfaceTickPolicy.Cue.ALIGNED, p.update(input(error = 0.3, now = 20)))
        // Beyond 0.35: exit, and ticks resume immediately (anchor cleared on exit).
        assertEquals(SurfaceTickPolicy.Cue.TICK, p.update(input(error = 0.4, now = 40)))
    }

    @Test
    fun `re-crossing rearms only after leaving the exit band, then announces again`() {
        val p = SurfaceTickPolicy()
        p.update(input(error = 0.1, now = 0)) // aligned
        p.update(input(error = 0.25, now = 20)) // 0.2 < 0.25 < 0.35: still aligned (not rearmed)
        assertEquals(SurfaceTickPolicy.Cue.ALIGNED, p.update(input(error = 0.25, now = 40)))
        // Swing out past 0.35, then back to center: announces again.
        p.update(input(error = 0.5, now = 60)) // exits
        assertEquals(SurfaceTickPolicy.Cue.ALIGNED, p.update(input(error = 0.1, now = 80)))
    }
}
