package wang.yaojia.webterm.session

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import wang.yaojia.webterm.wire.ApproveMode
import wang.yaojia.webterm.wire.ClientMessage
import wang.yaojia.webterm.wire.GateKind

/** Ports iOS `GateStateTests` — the two-line epoch stale-guard (security-load-bearing). */
class GateTrackerTest {

    @Test
    fun initialHoldsNoGateAndDecidesNothing() {
        val t = GateTracker.INITIAL
        assertNull(t.current)
        assertFalse(t.canDecide(epoch = 0))
        assertFalse(t.canDecide(epoch = 1))
    }

    @Test
    fun risingEdgeMintsEpochOne() {
        val t = GateTracker.INITIAL.reduce(pending = true, gate = GateKind.TOOL, detail = "Bash")
        val gate = t.current!!
        assertEquals(GateKind.TOOL, gate.kind)
        assertEquals("Bash", gate.detail)
        assertEquals(1, gate.epoch)
        assertTrue(t.canDecide(epoch = 1))
    }

    @Test
    fun nullGateWhilePendingIsTreatedAsTool() {
        val t = GateTracker.INITIAL.reduce(pending = true, gate = null, detail = null)
        assertEquals(GateKind.TOOL, t.current!!.kind)
    }

    @Test
    fun sustainedPendingKeepsEpochAndRefreshesKindAndDetail() {
        val first = GateTracker.INITIAL.reduce(pending = true, gate = GateKind.TOOL, detail = "Bash")
        val second = first.reduce(pending = true, gate = GateKind.PLAN, detail = "ExitPlanMode")
        val gate = second.current!!
        assertEquals(1, gate.epoch, "epoch must not advance while a gate stays held")
        assertEquals(GateKind.PLAN, gate.kind)
        assertEquals("ExitPlanMode", gate.detail)
    }

    @Test
    fun fallingEdgeClearsGateButRetainsEpochCounter() {
        val held = GateTracker.INITIAL.reduce(pending = true, gate = GateKind.TOOL, detail = null)
        val lifted = held.reduce(pending = false, gate = null, detail = null)
        assertNull(lifted.current)
        assertFalse(lifted.canDecide(epoch = 1))

        // Next rising edge must NOT reuse epoch 1 — the counter is retained.
        val next = lifted.reduce(pending = true, gate = GateKind.TOOL, detail = null)
        assertEquals(2, next.current!!.epoch)
    }

    @Test
    fun staleDecisionAgainstAResolvedGateIsDropped() {
        // gate #1 held at epoch 1, then resolved, then gate #2 rises at epoch 2.
        val gate1 = GateTracker.INITIAL.reduce(pending = true, gate = GateKind.TOOL, detail = null)
        val resolved = gate1.reduce(pending = false, gate = null, detail = null)
        val gate2 = resolved.reduce(pending = true, gate = GateKind.TOOL, detail = null)

        assertEquals(2, gate2.current!!.epoch)
        assertFalse(gate2.canDecide(epoch = 1), "a slow tap for gate #1 must never approve gate #2")
        assertTrue(gate2.canDecide(epoch = 2))
    }

    @Test
    fun canDecideIsFalseWhenNoGateIsHeld() {
        val lifted = GateTracker.INITIAL
            .reduce(pending = true, gate = GateKind.TOOL, detail = null)
            .reduce(pending = false, gate = null, detail = null)
        assertFalse(lifted.canDecide(epoch = 1))
    }

    @Test
    fun equalityFoldsCurrentAndEpoch() {
        val a = GateTracker.INITIAL.reduce(pending = true, gate = GateKind.TOOL, detail = "x")
        val b = GateTracker.INITIAL.reduce(pending = true, gate = GateKind.TOOL, detail = "x")
        assertEquals(a, b)
        assertEquals(a.hashCode(), b.hashCode())
    }

    @Test
    fun planGateOffersThreeWayAffordances() {
        val gate = GateState(kind = GateKind.PLAN, detail = null, epoch = 1)
        assertEquals(
            listOf(
                GateState.Affordance.APPROVE_AUTO,
                GateState.Affordance.APPROVE_REVIEW,
                GateState.Affordance.KEEP_PLANNING,
            ),
            gate.affordances,
        )
    }

    @Test
    fun toolGateOffersTwoWayAffordances() {
        val gate = GateState(kind = GateKind.TOOL, detail = null, epoch = 1)
        assertEquals(
            listOf(GateState.Affordance.APPROVE, GateState.Affordance.REJECT),
            gate.affordances,
        )
    }

    @Test
    fun affordanceClientMessagesMatchTheWebThreeWayContract() {
        assertEquals(
            ClientMessage.Approve(ApproveMode.ACCEPT_EDITS),
            GateState.Affordance.APPROVE_AUTO.clientMessage,
        )
        assertEquals(
            ClientMessage.Approve(ApproveMode.DEFAULT),
            GateState.Affordance.APPROVE_REVIEW.clientMessage,
        )
        assertEquals(ClientMessage.Reject, GateState.Affordance.KEEP_PLANNING.clientMessage)
        assertEquals(ClientMessage.Approve(null), GateState.Affordance.APPROVE.clientMessage)
        assertEquals(ClientMessage.Reject, GateState.Affordance.REJECT.clientMessage)
    }
}
