package com.ponzischeme89.memby.ui.player

import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test

/**
 * The credits speed ramp and its climbdown.
 *
 * The fallback is the half worth pinning. Doubling the speed doubles the bitrate pulled from
 * Emby over HTTP, so the ceiling has to be able to fall — and it must never climb back inside
 * one credit roll, or a marginal stream would oscillate between stuttering and recovering for
 * the length of the roll.
 */
class CreditsSpeedTest {

    @Test
    fun `the ramp starts at normal speed and ends at the ceiling`() {
        assertEquals(CREDITS_NORMAL_SPEED, creditsSpeedAt(0L, CREDITS_TARGET_SPEED), TOLERANCE)
        assertEquals(
            CREDITS_TARGET_SPEED,
            creditsSpeedAt(CREDITS_RAMP_MS, CREDITS_TARGET_SPEED),
            TOLERANCE,
        )
        // And stays there rather than overshooting once the ramp is past.
        assertEquals(
            CREDITS_TARGET_SPEED,
            creditsSpeedAt(CREDITS_RAMP_MS * 10, CREDITS_TARGET_SPEED),
            TOLERANCE,
        )
    }

    @Test
    fun `the ramp only ever moves forwards`() {
        var previous = CREDITS_NORMAL_SPEED
        var elapsed = 0L
        while (elapsed <= CREDITS_RAMP_MS) {
            val speed = creditsSpeedAt(elapsed, CREDITS_TARGET_SPEED)
            assertTrue(
                "speed went backwards at ${elapsed}ms: $previous then $speed",
                speed >= previous - TOLERANCE,
            )
            previous = speed
            elapsed += 60L
        }
    }

    /**
     * Eased out, the same curve every other transition in this package uses: most of the
     * change happens immediately, so the effect reads as the picture being let go rather than
     * as a slow drift nobody attributes to anything.
     */
    @Test
    fun `the ramp is past halfway by the time it is half over`() {
        val halfway = creditsSpeedAt(CREDITS_RAMP_MS / 2, CREDITS_TARGET_SPEED)
        val midpoint = (CREDITS_NORMAL_SPEED + CREDITS_TARGET_SPEED) / 2f
        assertTrue("halfway speed was $halfway, wanted past $midpoint", halfway > midpoint)
    }

    /** A ramp to nowhere must not produce a curve. */
    @Test
    fun `a ceiling of normal speed produces normal speed throughout`() {
        listOf(0L, 100L, CREDITS_RAMP_MS, CREDITS_RAMP_MS * 4).forEach { elapsed ->
            assertEquals(
                CREDITS_NORMAL_SPEED,
                creditsSpeedAt(elapsed, CREDITS_NORMAL_SPEED),
                TOLERANCE,
            )
        }
    }

    @Test
    fun `the ceiling falls one step at a time and stops at normal speed`() {
        val first = creditsCeilingAfterStall(CREDITS_TARGET_SPEED)
        assertEquals(1.5f, first, TOLERANCE)
        val second = creditsCeilingAfterStall(first)
        assertEquals(CREDITS_NORMAL_SPEED, second, TOLERANCE)
        // The floor is terminal: there is nowhere below it and nothing left to give up.
        assertEquals(CREDITS_NORMAL_SPEED, creditsCeilingAfterStall(second), TOLERANCE)
    }

    /**
     * The one property that must hold for a marginal stream: a stall never leaves the ceiling
     * where it was, so a title that cannot hold 2× cannot be asked to hold it again.
     */
    @Test
    fun `a stall always gives up speed`() {
        var ceiling = CREDITS_TARGET_SPEED
        repeat(6) {
            val next = creditsCeilingAfterStall(ceiling)
            assertTrue("ceiling rose from $ceiling to $next", next <= ceiling + TOLERANCE)
            ceiling = next
        }
        assertEquals(CREDITS_NORMAL_SPEED, ceiling, TOLERANCE)
    }

    @Test
    fun `only a ceiling above normal speed counts as active`() {
        assertTrue(creditsSpeedIsActive(CREDITS_TARGET_SPEED))
        assertTrue(creditsSpeedIsActive(1.5f))
        assertFalse(creditsSpeedIsActive(CREDITS_NORMAL_SPEED))
        assertFalse(creditsSpeedIsActive(0.5f))
    }

    /**
     * The chip is built from whole tenths rather than by formatting the float: a quantised
     * 1.5 is not exactly 1.5 in binary, so `toString` on it prints "1.5000001", and
     * `String.format` would print a comma for the point on a set configured in half of Europe.
     */
    @Test
    fun `the label reads as a person would write it`() {
        assertEquals("2×", creditsSpeedLabel(CREDITS_TARGET_SPEED))
        assertEquals("1.5×", creditsSpeedLabel(1.5f))
        assertEquals("1×", creditsSpeedLabel(CREDITS_NORMAL_SPEED))
        // And off the ramp, where the speed is whatever the curve produced.
        assertEquals("1.8×", creditsSpeedLabel(1.7996f))
    }

    private companion object {
        const val TOLERANCE = 0.03f
    }
}
