import Testing
@testable import SessionCore

/// T-iOS-5 · ReconnectMachine — pure back-off reducer (plan §3.2).
/// Mirrors public/terminal-session.ts:106,248,352-361: retry delays
/// 1s → 2s → 4s → 8s → 16s → 30s (capped), reset to 1s on `connected`.
@Suite("ReconnectMachine")
struct ReconnectMachineTests {
    /// Delay ladder the web client produces (public/terminal-session.ts:356).
    private static let expectedBackoffLadder: [Duration] = [
        .seconds(1), .seconds(2), .seconds(4), .seconds(8),
        .seconds(16), .seconds(30), .seconds(30),
    ]

    /// Drives `count` disconnect→timer-fired cycles from `.initial` and
    /// collects every `.scheduleRetry(after:)` delay along the way.
    private static func scheduledDelays(disconnectCount: Int) -> [Duration] {
        var machine = ReconnectMachine.initial
        var delays: [Duration] = []
        for _ in 0..<disconnectCount {
            let (afterDisconnect, effect) = machine.reduce(.disconnected)
            if case .scheduleRetry(let after) = effect {
                delays.append(after)
            }
            let (afterFire, _) = afterDisconnect.reduce(.retryTimerFired)
            machine = afterFire
        }
        return delays
    }

    /// Returns the machine state after `count` disconnect→timer-fired cycles.
    private static func machineAfterDisconnects(_ count: Int) -> ReconnectMachine {
        var machine = ReconnectMachine.initial
        for _ in 0..<count {
            let (afterDisconnect, _) = machine.reduce(.disconnected)
            let (afterFire, _) = afterDisconnect.reduce(.retryTimerFired)
            machine = afterFire
        }
        return machine
    }

    @Test("disconnect sequence schedules 1s,2s,4s,8s,16s then caps at 30s")
    func disconnectSequenceFollowsDoublingLadderCappedAt30s() {
        // Arrange / Act
        let delays = Self.scheduledDelays(disconnectCount: Self.expectedBackoffLadder.count)

        // Assert
        #expect(delays == Self.expectedBackoffLadder)
    }

    @Test("every disconnected input produces a scheduleRetry effect")
    func disconnectedAlwaysSchedulesARetry() {
        // Arrange
        let disconnectCount = Self.expectedBackoffLadder.count

        // Act
        let delays = Self.scheduledDelays(disconnectCount: disconnectCount)

        // Assert: no cycle dropped its retry (delays collected == cycles run).
        #expect(delays.count == disconnectCount)
    }

    @Test("connected resets backoff so the next disconnect starts at 1s again")
    func connectedResetsBackoffToOneSecond() {
        // Arrange: escalate well into the ladder first.
        let escalated = Self.machineAfterDisconnects(4)

        // Act
        let (afterConnected, connectEffect) = escalated.reduce(.connected)
        let (_, retryEffect) = afterConnected.reduce(.disconnected)

        // Assert
        #expect(connectEffect == .none)
        #expect(afterConnected == .initial)
        #expect(retryEffect == .scheduleRetry(after: .seconds(1)))
    }

    @Test("connected while already at initial state is a no-op")
    func connectedOnInitialStateIsNoOp() {
        // Arrange
        let machine = ReconnectMachine.initial

        // Act
        let (next, effect) = machine.reduce(.connected)

        // Assert
        #expect(next == .initial)
        #expect(effect == .none)
    }

    @Test("retryTimerFired produces connectNow")
    func retryTimerFiredProducesConnectNow() {
        // Arrange
        let (waiting, _) = ReconnectMachine.initial.reduce(.disconnected)

        // Act
        let (_, effect) = waiting.reduce(.retryTimerFired)

        // Assert
        #expect(effect == .connectNow)
    }

    @Test("foregrounded produces immediate connectNow without waiting for the timer")
    func foregroundedProducesImmediateConnectNow() {
        // Arrange: mid-backoff (next disconnect would schedule 4s).
        let waiting = Self.machineAfterDisconnects(2)

        // Act
        let (afterForeground, effect) = waiting.reduce(.foregrounded)

        // Assert: connect right now, and the backoff ladder is NOT reset —
        // only a successful `connected` resets it.
        #expect(effect == .connectNow)
        #expect(afterForeground == waiting)
        let (_, retryEffect) = afterForeground.reduce(.disconnected)
        #expect(retryEffect == .scheduleRetry(after: .seconds(4)))
    }

    @Test("userRetry produces immediate connectNow without waiting for the timer")
    func userRetryProducesImmediateConnectNow() {
        // Arrange
        let waiting = Self.machineAfterDisconnects(3)

        // Act
        let (afterRetry, effect) = waiting.reduce(.userRetry)

        // Assert
        #expect(effect == .connectNow)
        #expect(afterRetry == waiting)
    }

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

        // Act: reduce twice with the identical input on the SAME value.
        let (firstNext, firstEffect) = machine.reduce(.disconnected)
        let (secondNext, secondEffect) = machine.reduce(.disconnected)

        // Assert: deterministic output, untouched original.
        #expect(firstNext == secondNext)
        #expect(firstEffect == secondEffect)
        #expect(machine == snapshotBefore)
        #expect(machine == .initial)
    }
}
