package wang.yaojia.webterm.session

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import kotlin.time.Duration.Companion.seconds

class ReconnectMachineTest {

    private fun scheduledDelay(effect: ReconnectMachine.Effect): kotlin.time.Duration {
        assertTrue(effect is ReconnectMachine.Effect.ScheduleRetry)
        return (effect as ReconnectMachine.Effect.ScheduleRetry).after
    }

    @Test
    fun `disconnect ladder climbs 1 2 4 8 16 30 and then caps at 30`() {
        var machine = ReconnectMachine.initial
        val scheduled = buildList {
            repeat(7) {
                val (next, effect) = machine.reduce(ReconnectMachine.Input.Disconnected)
                add(scheduledDelay(effect))
                machine = next
            }
        }

        assertEquals(
            listOf(1, 2, 4, 8, 16, 30, 30).map { it.seconds },
            scheduled,
        )
    }

    @Test
    fun `a successful connect resets the backoff ladder to the initial rung`() {
        var machine = ReconnectMachine.initial
        repeat(3) { machine = machine.reduce(ReconnectMachine.Input.Disconnected).first } // climb the ladder

        val (afterConnect, connectEffect) = machine.reduce(ReconnectMachine.Input.Connected)
        assertEquals(ReconnectMachine.Effect.None, connectEffect)
        assertEquals(ReconnectMachine.INITIAL_RETRY_DELAY, afterConnect.nextRetryDelay)

        // The next disconnect schedules the initial 1 s rung again.
        val (_, effect) = afterConnect.reduce(ReconnectMachine.Input.Disconnected)
        assertEquals(1.seconds, scheduledDelay(effect))
    }

    @Test
    fun `foregrounding connects now WITHOUT resetting the backoff ladder`() {
        // Two disconnects: last scheduled 2 s, stored next rung is 4 s.
        var machine = ReconnectMachine.initial
        machine = machine.reduce(ReconnectMachine.Input.Disconnected).first
        machine = machine.reduce(ReconnectMachine.Input.Disconnected).first

        val (afterForeground, effect) = machine.reduce(ReconnectMachine.Input.Foregrounded)
        assertEquals(ReconnectMachine.Effect.ConnectNow, effect)

        // Ladder NOT reset: the next disconnect still schedules 4 s (not 1 s).
        val (_, disconnectEffect) = afterForeground.reduce(ReconnectMachine.Input.Disconnected)
        assertEquals(4.seconds, scheduledDelay(disconnectEffect))
    }

    @Test
    fun `retry timer fired and user retry both connect now and preserve the snapshot`() {
        val machine = ReconnectMachine.initial.reduce(ReconnectMachine.Input.Disconnected).first

        val (afterTimer, timerEffect) = machine.reduce(ReconnectMachine.Input.RetryTimerFired)
        assertEquals(ReconnectMachine.Effect.ConnectNow, timerEffect)
        assertEquals(machine, afterTimer)

        val (afterUser, userEffect) = machine.reduce(ReconnectMachine.Input.UserRetry)
        assertEquals(ReconnectMachine.Effect.ConnectNow, userEffect)
        assertEquals(machine, afterUser)
    }

    @Test
    fun `the session id is carried through disconnect connect and foreground`() {
        val machine = ReconnectMachine.initial.withSessionId("abc-123")
        assertEquals("abc-123", machine.sessionId)

        // Survives a disconnect (entering back-off) ...
        val afterDisconnect = machine.reduce(ReconnectMachine.Input.Disconnected).first
        assertEquals("abc-123", afterDisconnect.sessionId)

        // ... a successful reconnect that resets ONLY the delay ladder ...
        val afterConnect = afterDisconnect.reduce(ReconnectMachine.Input.Connected).first
        assertEquals("abc-123", afterConnect.sessionId)
        assertEquals(ReconnectMachine.INITIAL_RETRY_DELAY, afterConnect.nextRetryDelay)

        // ... and a foreground connect-now.
        val afterForeground = afterConnect.reduce(ReconnectMachine.Input.Foregrounded).first
        assertEquals("abc-123", afterForeground.sessionId)
    }

    @Test
    fun `initial seeds the ladder with no id and withSessionId carries one`() {
        assertEquals(null, ReconnectMachine.initial.sessionId)
        assertEquals(ReconnectMachine.INITIAL_RETRY_DELAY, ReconnectMachine.initial.nextRetryDelay)

        val seeded = ReconnectMachine.initial.withSessionId("sess-1")
        assertEquals("sess-1", seeded.sessionId)
        assertEquals(ReconnectMachine.INITIAL_RETRY_DELAY, seeded.nextRetryDelay)
    }
}
