import Testing
import TestSupport
import WireProtocol
@testable import SessionCore

/// T-iOS-5 · PingScheduler — keep-alive pacer (plan §3.2 / §3.2.1).
/// 25s ping cadence (`Tunables.pingInterval`); 1 missed pong tolerated,
/// `Tunables.pongMissLimit` (2) CONSECUTIVE misses → connection declared lost.
/// All timing on FakeClock — zero real waits.
@Suite("PingScheduler")
struct PingSchedulerTests {
    /// Scripted pong ledger: pops one result per ping, records the call count.
    /// Runs out of script → answers `true` (pong received).
    private actor PongScript {
        private var results: [Bool]
        private(set) var pingCount = 0

        init(_ results: [Bool]) {
            self.results = results
        }

        func nextResult() -> Bool {
            pingCount += 1
            guard !results.isEmpty else { return true }
            return results.removeFirst()
        }
    }

    /// Starts `run` on a FakeClock and parks it at its first sleep.
    private static func startScheduler(
        clock: FakeClock,
        script: PongScript,
        interval: Duration = Tunables.pingInterval
    ) async -> Task<PingScheduler.Outcome, Never> {
        let scheduler = PingScheduler(interval: interval)
        let task = Task {
            await scheduler.run(clock: clock) { await script.nextResult() }
        }
        await clock.waitForSleepers(count: 1)
        return task
    }

    /// Fires one ping tick: advance past the interval, then wait until the
    /// scheduler is parked in its NEXT sleep (proof the tick fully processed).
    private static func fireTickAndAwaitNextSleep(clock: FakeClock) async {
        clock.advance(by: Tunables.pingInterval)
        await clock.waitForSleepers(count: 1)
    }

    @Test("default interval is Tunables.pingInterval (25s)")
    func defaultIntervalIsTunablesPingInterval() {
        // Arrange / Act
        let scheduler = PingScheduler()

        // Assert
        #expect(scheduler.interval == Tunables.pingInterval)
        #expect(scheduler.interval == .seconds(25))
    }

    @Test("sends one ping per interval tick while pongs keep arriving")
    func sendsOnePingPerIntervalTickWhenPongsArrive() async {
        // Arrange
        let clock = FakeClock()
        let script = PongScript([true, true, true])
        let task = await Self.startScheduler(clock: clock, script: script)

        // Act: three full interval ticks.
        await Self.fireTickAndAwaitNextSleep(clock: clock)
        await Self.fireTickAndAwaitNextSleep(clock: clock)
        await Self.fireTickAndAwaitNextSleep(clock: clock)

        // Assert: exactly one ping per tick, none before the first interval.
        #expect(await script.pingCount == 3)
        task.cancel()
        #expect(await task.value == .cancelled)
    }

    @Test("does not ping before the first interval has elapsed")
    func doesNotPingBeforeFirstIntervalElapses() async {
        // Arrange
        let clock = FakeClock()
        let script = PongScript([])
        let task = await Self.startScheduler(clock: clock, script: script)

        // Act: advance just short of the interval.
        clock.advance(by: Tunables.pingInterval - .seconds(1))

        // Assert: still parked, zero pings sent.
        #expect(clock.pendingSleeperCount == 1)
        #expect(await script.pingCount == 0)
        task.cancel()
        #expect(await task.value == .cancelled)
    }

    @Test("tolerates a single missed pong and keeps the connection alive")
    func toleratesSingleMissedPongAndKeepsPinging() async {
        // Arrange: miss one pong, then recover.
        let clock = FakeClock()
        let script = PongScript([false, true])
        let task = await Self.startScheduler(clock: clock, script: script)

        // Act: tick #1 misses — scheduler must survive and park again;
        // tick #2 gets its pong.
        await Self.fireTickAndAwaitNextSleep(clock: clock)
        await Self.fireTickAndAwaitNextSleep(clock: clock)

        // Assert: both pings sent, run still alive (it re-parked after each).
        #expect(await script.pingCount == 2)
        task.cancel()
        #expect(await task.value == .cancelled)
    }

    @Test("two consecutive missed pongs signal connectionLost")
    func twoConsecutiveMissedPongsSignalConnectionLost() async {
        // Arrange
        let clock = FakeClock()
        let script = PongScript([false, false])
        let task = await Self.startScheduler(clock: clock, script: script)

        // Act: miss #1 (tolerated) …
        await Self.fireTickAndAwaitNextSleep(clock: clock)
        // … miss #2 == Tunables.pongMissLimit → run must finish by itself.
        clock.advance(by: Tunables.pingInterval)

        // Assert
        #expect(await task.value == .connectionLost)
        #expect(await script.pingCount == Tunables.pongMissLimit)

        // Assert: a dead scheduler never pings again.
        clock.advance(by: Tunables.pingInterval)
        #expect(await script.pingCount == Tunables.pongMissLimit)
    }

    @Test("a pong between misses resets the counter so non-consecutive misses never disconnect")
    func pongBetweenMissesResetsConsecutiveMissCounter() async {
        // Arrange: miss, pong, miss, pong — never two misses in a row.
        let clock = FakeClock()
        let script = PongScript([false, true, false, true])
        let task = await Self.startScheduler(clock: clock, script: script)

        // Act
        for _ in 0..<4 {
            await Self.fireTickAndAwaitNextSleep(clock: clock)
        }

        // Assert: still alive after 4 ticks (it re-parked after the last one).
        #expect(await script.pingCount == 4)
        task.cancel()
        #expect(await task.value == .cancelled)
    }

    @Test("cancellation while sleeping ends run as cancelled, not connectionLost")
    func cancellationWhileSleepingEndsRunAsCancelled() async {
        // Arrange
        let clock = FakeClock()
        let script = PongScript([])
        let task = await Self.startScheduler(clock: clock, script: script)

        // Act: tear down mid-sleep (normal close path).
        task.cancel()

        // Assert
        #expect(await task.value == .cancelled)
        #expect(await script.pingCount == 0)
    }

    @Test("honors a custom interval instead of the default")
    func honorsCustomIntervalInsteadOfDefault() async {
        // Arrange
        let customInterval: Duration = .seconds(5)
        let clock = FakeClock()
        let script = PongScript([true])
        let task = await Self.startScheduler(
            clock: clock, script: script, interval: customInterval
        )

        // Act: one custom-interval tick.
        clock.advance(by: customInterval)
        await clock.waitForSleepers(count: 1)

        // Assert
        #expect(await script.pingCount == 1)
        task.cancel()
        #expect(await task.value == .cancelled)
    }
}
