package com.fabledsword.minstrel.connectivity

import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class ReachabilityMachineTest {

    private fun machine() = ReachabilityMachine()

    @Test
    fun `starts healthy`() {
        val m = machine()
        m.onLinkChange(up = true)
        assertEquals(ServerHealth.Healthy, m.health())
    }

    @Test
    fun `no link is offline regardless of probes`() {
        val m = machine()
        m.onLinkChange(up = false)
        m.onProbeFailure(nowMs = 1)
        assertEquals(ServerHealth.Offline, m.health())
    }

    @Test
    fun `single probe failure is unstable not down`() {
        val m = machine()
        m.onLinkChange(up = true)
        m.onProbeFailure(nowMs = 1_000)
        assertEquals(ServerHealth.Unstable, m.health())
    }

    @Test
    fun `probe success from unstable recovers to healthy`() {
        val m = machine()
        m.onLinkChange(up = true)
        m.onProbeFailure(nowMs = 1_000)
        m.onSuccess()
        assertEquals(ServerHealth.Healthy, m.health())
    }

    @Test
    fun `op success from unstable recovers to healthy`() {
        val m = machine()
        m.onLinkChange(up = true)
        m.onProbeFailure(nowMs = 1_000)
        m.onSuccess() // a successful stream read / API 2xx is self-proving
        assertEquals(ServerHealth.Healthy, m.health())
    }

    @Test
    fun `two op failures plus a failed probe escalate immediately`() {
        val m = machine()
        m.onLinkChange(up = true)
        m.onOpFailure(nowMs = 1_000)
        m.onOpFailure(nowMs = 1_500) // corroboration reached
        m.onProbeFailure(nowMs = 2_000) // probe agrees → fast ServerDown
        assertEquals(ServerHealth.ServerDown, m.health())
    }

    @Test
    fun `op failures with a successful probe stay healthy (track-specific)`() {
        val m = machine()
        m.onLinkChange(up = true)
        m.onOpFailure(nowMs = 1_000)
        m.onOpFailure(nowMs = 1_500)
        m.onSuccess() // arbiter says server is fine
        assertEquals(ServerHealth.Healthy, m.health())
    }

    @Test
    fun `stale op failures do not corroborate`() {
        val m = machine()
        m.onLinkChange(up = true)
        m.onOpFailure(nowMs = 0)
        m.onOpFailure(nowMs = 1_000)
        // both op failures are now older than the corroboration window:
        m.onProbeFailure(nowMs = 1_000 + CORROBORATION_WINDOW_MS + 1)
        assertEquals(ServerHealth.Unstable, m.health()) // not enough fresh corroboration
    }

    @Test
    fun `sustained failure backstop escalates after the window`() {
        val m = machine()
        m.onLinkChange(up = true)
        m.onProbeFailure(nowMs = 1_000) // unstable, streak starts
        m.onProbeFailure(nowMs = 1_000 + ESCALATE_AFTER_MS) // sustained ≥ backstop
        assertEquals(ServerHealth.ServerDown, m.health())
    }

    @Test
    fun `link restored keeps last-known down until a fresh result`() {
        val m = machine()
        m.onLinkChange(up = true)
        m.onProbeFailure(nowMs = 1_000)
        m.onProbeFailure(nowMs = 1_000 + ESCALATE_AFTER_MS) // ServerDown
        m.onLinkChange(up = false)
        assertEquals(ServerHealth.Offline, m.health())
        m.onLinkChange(up = true)
        // link back but no fresh probe result yet — last known reachability was down:
        assertEquals(ServerHealth.ServerDown, m.health())
        m.onSuccess()
        assertEquals(ServerHealth.Healthy, m.health())
    }

    @Test
    fun `unstable does not gate — it is not offline or serverdown`() {
        val m = machine()
        m.onLinkChange(up = true)
        m.onProbeFailure(nowMs = 1_000)
        val health = m.health()
        assertTrue(health != ServerHealth.Offline && health != ServerHealth.ServerDown)
        assertEquals(ServerHealth.Unstable, health)
    }
}
