import Testing
import WireProtocol
@testable import SessionCore

/// T-iOS-6 · GateState + GateTracker — permission-gate reducer (plan §3.2).
/// Mirrors the web client's stale-gate guard (public/terminal-session.ts:94-311):
/// epoch increments ONLY on a pending false→true rising edge; a decision carrying
/// a stale epoch is dropped so a slow tap can never approve the NEXT gate.
@Suite("GateState + GateTracker")
struct GateStateTests {
    // MARK: - epoch semantics (rising edge only)

    @Test("pending false→true rising edge creates a gate with epoch +1")
    func risingEdgeCreatesGateWithIncrementedEpoch() {
        // Arrange
        let tracker = GateTracker.initial

        // Act
        let next = tracker.reduce(pending: true, gate: .tool, detail: "Bash")

        // Assert
        #expect(tracker.current == nil)
        #expect(next.current == GateState(kind: .tool, detail: "Bash", epoch: 1))
    }

    @Test("sustained pending frames keep the same epoch")
    func sustainedPendingKeepsSameEpoch() {
        // Arrange
        let held = GateTracker.initial.reduce(pending: true, gate: .tool, detail: "Bash")

        // Act: two continuation frames while the same gate stays held.
        let afterOne = held.reduce(pending: true, gate: .tool, detail: "Bash")
        let afterTwo = afterOne.reduce(pending: true, gate: .tool, detail: "Bash")

        // Assert
        #expect(afterOne.current?.epoch == 1)
        #expect(afterTwo.current?.epoch == 1)
    }

    @Test("continuation frames refresh detail from the latest frame without bumping epoch")
    func sustainedPendingRefreshesDetailFromLatestFrame() {
        // Arrange (mirrors terminal-session.ts:310 — pendingTool follows msg.detail)
        let held = GateTracker.initial.reduce(pending: true, gate: .tool, detail: "Bash")

        // Act
        let refreshed = held.reduce(pending: true, gate: .tool, detail: "Edit")

        // Assert
        #expect(refreshed.current == GateState(kind: .tool, detail: "Edit", epoch: 1))
    }

    @Test("pending true→false falling edge clears the gate")
    func fallingEdgeClearsGate() {
        // Arrange
        let held = GateTracker.initial.reduce(pending: true, gate: .tool, detail: "Bash")

        // Act
        let cleared = held.reduce(pending: false, gate: nil, detail: nil)

        // Assert
        #expect(cleared.current == nil)
    }

    @Test("a second rising edge increments the epoch again")
    func secondRisingEdgeIncrementsEpochAgain() {
        // Arrange: gate 1 held, then resolved.
        let cleared = GateTracker.initial
            .reduce(pending: true, gate: .tool, detail: "Bash")
            .reduce(pending: false, gate: nil, detail: nil)

        // Act: gate 2 arrives.
        let second = cleared.reduce(pending: true, gate: .plan, detail: nil)

        // Assert
        #expect(second.current == GateState(kind: .plan, detail: nil, epoch: 2))
    }

    // MARK: - stale-epoch decisions are dropped (防误批新 gate)

    @Test("a decision carrying a stale epoch is dropped")
    func staleEpochDecisionIsDropped() {
        // Arrange: gate 1 resolved, gate 2 now held (epoch 2).
        let tracker = GateTracker.initial
            .reduce(pending: true, gate: .tool, detail: "Bash")
            .reduce(pending: false, gate: nil, detail: nil)
            .reduce(pending: true, gate: .tool, detail: "Write")

        // Act / Assert: the user's slow tap against gate 1 must NOT approve gate 2.
        #expect(tracker.canDecide(epoch: 1) == false)
    }

    @Test("a decision carrying the current epoch is accepted")
    func currentEpochDecisionIsAccepted() {
        // Arrange
        let tracker = GateTracker.initial
            .reduce(pending: true, gate: .tool, detail: "Bash")
            .reduce(pending: false, gate: nil, detail: nil)
            .reduce(pending: true, gate: .tool, detail: "Write")

        // Act / Assert
        #expect(tracker.canDecide(epoch: 2) == true)
    }

    @Test("a decision when no gate is held is dropped even with the last-issued epoch")
    func decisionWithNoGateHeldIsDropped() {
        // Arrange: gate 1 was resolved server-side before the tap landed.
        let cleared = GateTracker.initial
            .reduce(pending: true, gate: .tool, detail: "Bash")
            .reduce(pending: false, gate: nil, detail: nil)

        // Act / Assert
        #expect(cleared.canDecide(epoch: 1) == false)
    }

    // MARK: - affordances (plan → three-way, tool → two-way)

    @Test("plan gate offers the three-way affordance set")
    func planGateOffersThreeWayAffordances() {
        // Arrange
        let gate = GateState(kind: .plan, detail: nil, epoch: 1)

        // Act / Assert (mirrors public/tabs.ts:343-350)
        #expect(gate.affordances == [.approveAuto, .approveReview, .keepPlanning])
    }

    @Test("tool gate offers the two-way affordance set")
    func toolGateOffersTwoWayAffordances() {
        // Arrange
        let gate = GateState(kind: .tool, detail: "Bash", epoch: 1)

        // Act / Assert (mirrors public/tabs.ts:334-339)
        #expect(gate.affordances == [.approve, .reject])
    }

    @Test("pending frame without a gate kind defaults to a tool gate")
    func pendingWithoutGateKindDefaultsToToolGate() {
        // Arrange: an unrecognized/absent gate decodes as nil (ServerMessage) —
        // the pending signal must never be lost (web: gate ?? null → tool buttons).
        let tracker = GateTracker.initial

        // Act
        let held = tracker.reduce(pending: true, gate: nil, detail: "Bash")

        // Assert
        #expect(held.current?.kind == .tool)
        #expect(held.current?.affordances == [.approve, .reject])
    }

    @Test("affordances map to the exact wire messages the web client sends")
    func affordancesMapToWireMessagesMirroringWebClient() {
        // Arrange / Act / Assert (public/tabs.ts:345-347: acceptEdits / default / reject)
        #expect(GateState.Affordance.approveAuto.clientMessage == .approve(mode: .acceptEdits))
        #expect(GateState.Affordance.approveReview.clientMessage == .approve(mode: .default))
        #expect(GateState.Affordance.keepPlanning.clientMessage == .reject)
        #expect(GateState.Affordance.approve.clientMessage == .approve(mode: nil))
        #expect(GateState.Affordance.reject.clientMessage == .reject)
    }

    // MARK: - purity

    @Test("reduce is pure: same input gives same output and never mutates the receiver")
    func reduceIsPureAndDoesNotMutateReceiver() {
        // Arrange
        let tracker = GateTracker.initial
        let snapshotBefore = tracker

        // Act: reduce twice with the identical input on the SAME value.
        let first = tracker.reduce(pending: true, gate: .plan, detail: "d")
        let second = tracker.reduce(pending: true, gate: .plan, detail: "d")

        // Assert
        #expect(first == second)
        #expect(tracker == snapshotBefore)
    }
}
