package wang.yaojia.webterm.components

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import wang.yaojia.webterm.session.Connection
import wang.yaojia.webterm.session.ConnectionState
import wang.yaojia.webterm.session.Exited
import wang.yaojia.webterm.session.FailureReason
import wang.yaojia.webterm.session.Output
import wang.yaojia.webterm.wire.WireConstants
import kotlin.time.Duration.Companion.seconds

/**
 * The [bannerModel] reducer contract (plan §1, §5 A21 Verify). Pure JVM — no device:
 *  1. **Precedence** — terminal phases (`exited`/`failed`) OUTRANK transient ones (`connecting`/
 *     `reconnecting`): once terminal, a later transient connection event never overwrites the banner.
 *  2. **Spawn failure** — `exit(-1)` is a spawn failure (reason required), no spinner, offers new session.
 *  3. **replayTooLarge** — non-retryable: NO spinner + actionable + a "new session" affordance.
 *  4. **Normal exit** — `exit(code)` (code ≥ 0) is not a spawn failure but still offers a new session.
 */
class ReconnectBannerTest {

    // ── 1. Precedence: terminal OUTRANKS transient ───────────────────────────────

    @Test
    fun `exited outranks a later connecting event`() {
        val exited = bannerModel(BannerModel.Hidden, Exited(code = 0, reason = null))
        assertTrue(exited is BannerModel.Exited)

        // A stray transient connect arriving after the shell exited must NOT clear the exit banner.
        val after = bannerModel(exited, Connection(ConnectionState.Connecting))
        assertEquals(exited, after)
    }

    @Test
    fun `failed outranks a later reconnecting event`() {
        val failed = bannerModel(
            BannerModel.Hidden,
            Connection(ConnectionState.Failed(FailureReason.REPLAY_TOO_LARGE)),
        )
        assertTrue(failed is BannerModel.Failed)

        val after = bannerModel(failed, Connection(ConnectionState.Reconnecting(attempt = 3, next = 8.seconds)))
        assertEquals(failed, after)
    }

    @Test
    fun `a fresh exit replaces an earlier failure (terminal replaces terminal)`() {
        val failed = bannerModel(BannerModel.Hidden, Connection(ConnectionState.Failed(FailureReason.REPLAY_TOO_LARGE)))
        val exited = bannerModel(failed, Exited(code = 137, reason = "killed"))
        assertEquals(BannerModel.Exited(137, "killed"), exited)
    }

    // ── 2. Spawn-failure copy (code == -1) ───────────────────────────────────────

    @Test
    fun `exit code -1 is a spawn failure and carries the reason`() {
        val model = bannerModel(BannerModel.Hidden, Exited(code = WireConstants.SPAWN_FAILED_EXIT_CODE, reason = "no shell"))
        val exited = model as BannerModel.Exited
        assertEquals(-1, exited.code)
        assertTrue(exited.isSpawnFailure)
        assertEquals("no shell", exited.reason)
        assertFalse(exited.showsSpinner)
        assertTrue(exited.offersNewSession)
    }

    // ── 3. replayTooLarge: no spinner, actionable, new-session action ─────────────

    @Test
    fun `replayTooLarge shows no spinner and offers a new session`() {
        val model = bannerModel(BannerModel.Hidden, Connection(ConnectionState.Failed(FailureReason.REPLAY_TOO_LARGE)))
        val failed = model as BannerModel.Failed
        assertEquals(FailureReason.REPLAY_TOO_LARGE, failed.reason)
        assertFalse(failed.showsSpinner) // never a spinner — reconnecting would hit the same wall forever
        assertTrue(failed.offersNewSession)
    }

    // ── 4. Normal exit ───────────────────────────────────────────────────────────

    @Test
    fun `a normal exit is not a spawn failure but still offers a new session`() {
        val exited = bannerModel(BannerModel.Hidden, Exited(code = 0, reason = null)) as BannerModel.Exited
        assertFalse(exited.isSpawnFailure)
        assertFalse(exited.showsSpinner)
        assertTrue(exited.offersNewSession)
    }

    // ── Transient reductions (no terminal state in play) ─────────────────────────

    @Test
    fun `connecting then reconnecting then connected walks the transient states`() {
        var model: BannerModel = BannerModel.Hidden
        model = bannerModel(model, Connection(ConnectionState.Connecting))
        assertEquals(BannerModel.Connecting, model)
        assertTrue(model.showsSpinner)

        model = bannerModel(model, Connection(ConnectionState.Reconnecting(attempt = 1, next = 2.seconds)))
        assertEquals(BannerModel.Reconnecting(1, 2.seconds), model)
        assertTrue(model.showsSpinner)

        model = bannerModel(model, Connection(ConnectionState.Connected))
        assertEquals(BannerModel.Hidden, model) // connected → banner hidden
    }

    @Test
    fun `non-connection events leave the banner unchanged`() {
        val connecting = bannerModel(BannerModel.Hidden, Connection(ConnectionState.Connecting))
        assertEquals(connecting, bannerModel(connecting, Output("some bytes")))
    }
}
