package com.onthelevel.core.audio

import com.onthelevel.core.audio.VoiceGuidancePolicy.Direction
import com.onthelevel.core.audio.VoiceGuidancePolicy.Phrase
import com.onthelevel.core.sensors.SurfacePresentation
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test

class VoiceGuidancePolicyTest {

    private fun input(
        pitch: Double = 0.0,
        roll: Double = 0.0,
        locked: Boolean = false,
        settling: Boolean = false,
        enabled: Boolean = true,
        foreground: Boolean = true,
        placementOk: Boolean = true,
        presentation: SurfacePresentation? = SurfacePresentation.FACE_UP,
    ) = VoiceGuidancePolicy.Input(
        isEnabled = enabled,
        isAppForeground = foreground,
        placementOk = placementOk,
        presentation = presentation,
        isSettling = settling,
        pitchDegrees = pitch,
        rollDegrees = roll,
        isLocked = locked,
        nowMillis = 0,
    )

    /** Move (silent), then come to rest — the frame the board settles on. */
    private fun VoiceGuidancePolicy.moveThenSettle(pitch: Double, roll: Double): Phrase? {
        update(input(pitch = pitch, roll = roll, settling = true))
        return update(input(pitch = pitch, roll = roll, settling = false))
    }

    @Test
    fun `first settled reading gives one instruction, low side of the dominant axis`() {
        val p = VoiceGuidancePolicy()
        assertEquals(Phrase.Raise(Direction.RAISE_LEFT, fine = false), p.update(input(roll = 3.0)))
    }

    @Test
    fun `same direction after another settle stays silent`() {
        val p = VoiceGuidancePolicy()
        assertEquals(Phrase.Raise(Direction.RAISE_LEFT, fine = false), p.update(input(roll = 3.0)))
        // Adjusted, still needs the same coarse "raise the left": say nothing.
        assertNull(p.moveThenSettle(pitch = 0.0, roll = 2.8))
    }

    @Test
    fun `direction change speaks once`() {
        val p = VoiceGuidancePolicy()
        p.update(input(roll = 3.0)) // raise left
        // Overcorrected past level (left now high): speak the new direction, once.
        assertEquals(Phrase.Raise(Direction.RAISE_RIGHT, fine = false), p.moveThenSettle(pitch = 0.0, roll = -3.0))
        // Settling again with the same direction: silent.
        assertNull(p.moveThenSettle(pitch = 0.0, roll = -2.9))
    }

    @Test
    fun `crossing coarse to fine speaks once`() {
        val p = VoiceGuidancePolicy()
        p.update(input(roll = 3.0)) // coarse "raise the left"
        // Now close, same direction: announce the finer correction, once.
        assertEquals(Phrase.Raise(Direction.RAISE_LEFT, fine = true), p.moveThenSettle(pitch = 0.0, roll = 0.6))
        // Still fine, same direction: silent.
        assertNull(p.moveThenSettle(pitch = 0.0, roll = 0.5))
    }

    @Test
    fun `lock speaks once`() {
        val p = VoiceGuidancePolicy()
        p.update(input(roll = 3.0))
        assertEquals(Phrase.Level, p.update(input(locked = true)))
        assertNull(p.update(input(locked = true)))
    }

    @Test
    fun `no periodic repeat while sitting settled and off-level`() {
        val p = VoiceGuidancePolicy()
        assertEquals(Phrase.Raise(Direction.RAISE_LEFT, fine = false), p.update(input(roll = 3.0)))
        // Many more settled frames, unchanged: silence, no timer-driven repeat.
        repeat(50) { assertNull(p.update(input(roll = 3.0))) }
    }

    @Test
    fun `silent when disabled, backgrounded, or off-surface`() {
        assertNull(VoiceGuidancePolicy().update(input(roll = 3.0, enabled = false)))
        assertNull(VoiceGuidancePolicy().update(input(roll = 3.0, foreground = false)))
        assertNull(VoiceGuidancePolicy().update(input(roll = 3.0, placementOk = false)))
        assertNull(VoiceGuidancePolicy().update(input(roll = 3.0, presentation = SurfacePresentation.NEAR_VERTICAL)))
    }
}
