import Foundation
import Testing

@testable import NucleicProtocol

/// The reliable-UDP direct transport (Covalence direct, SYNC §3.3): wire codec vectors plus a
/// live two-socket loopback proving reliable, in-order, framed delivery both ways — including
/// a maximum-size frame (the 16 MiB contract every transport carries), which forces the
/// window/ACK/reassembly path through thousands of datagrams.
@Suite struct RUDPTests {
    private let tokenA = Data(repeating: 0xA1, count: 16)
    private let tokenB = Data(repeating: 0xB2, count: 16)

    @Test func wireCodecRoundTrips() {
        let payload = Data("hello direct".utf8)
        let dgram = RUDPFrameChannel.encode(.data, token: tokenA, seq: 42, payload: payload)
        let decoded = RUDPFrameChannel.decode(dgram)
        #expect(decoded?.kind == .data)
        #expect(decoded?.token == tokenA)
        #expect(decoded?.seq == 42)
        #expect(decoded?.payload == payload)

        // Junk / short / wrong-magic → nil.
        #expect(RUDPFrameChannel.decode(Data([0x00, 0x01])) == nil)
        var wrongMagic = dgram
        wrongMagic[0] = 0x00
        #expect(RUDPFrameChannel.decode(wrongMagic) == nil)
    }

    @Test func ackAndPingCodec() {
        let ack = RUDPFrameChannel.encode(.ack, token: tokenB, seq: 7, payload: Data())
        #expect(RUDPFrameChannel.decode(ack)?.kind == .ack)
        #expect(RUDPFrameChannel.decode(ack)?.seq == 7)
        let ping = RUDPFrameChannel.encode(.ping, token: tokenB, seq: 0, payload: Data())
        #expect(RUDPFrameChannel.decode(ping)?.kind == .ping)
    }

    /// Two RUDP channels over two real loopback UDP sockets: frames flow reliably and in order
    /// both directions, and a 16 MiB frame reassembles exactly.
    @Test func liveLoopbackReliableInOrderDelivery() async throws {
        let socketA = try UDPSocket(port: 0, ipv6: false)
        let socketB = try UDPSocket(port: 0, ipv6: false)
        let remoteA = UDPEndpoint.v4(ip: "127.0.0.1", port: socketB.localPort)
        let remoteB = UDPEndpoint.v4(ip: "127.0.0.1", port: socketA.localPort)

        // A stamps tokenB on its datagrams and expects tokenA back; B mirrors.
        let a = RUDPFrameChannel(socket: socketA, remote: remoteA, sendToken: tokenB, recvToken: tokenA)
        let b = RUDPFrameChannel(socket: socketB, remote: remoteB, sendToken: tokenA, recvToken: tokenB)
        defer {
            a.close()
            b.close()
        }

        let bSink = FrameSink()
        let aSink = FrameSink()
        let bTask = Task { for await f in b.frames() { await bSink.add(f) } }
        let aTask = Task { for await f in a.frames() { await aSink.add(f) } }
        defer {
            bTask.cancel()
            aTask.cancel()
        }

        // A → B: a burst of small frames must arrive in order.
        for i in 0..<20 { a.send(Data("a\(i)".utf8)) }
        // B → A: one frame back.
        b.send(Data("hi-from-b".utf8))
        // A → B: a maximum-size frame after the burst.
        var big = Data(count: WireFraming.maxFrameSize - 64)
        big[0] = 0x7E
        big[big.count - 1] = 0x5A
        a.send(big)

        // A full-size frame is ~15k datagrams. Windows loopback's go-back-N path needs more than
        // 30 seconds under load (measured on Titan); this is a correctness gate, not a throughput
        // benchmark, so leave enough time for its retransmit window to converge.
        let bFrames = await bSink.wait(count: 21, timeout: .seconds(60))
        #expect(bFrames.count == 21)
        #expect(Array(bFrames.prefix(20)) == (0..<20).map { Data("a\($0)".utf8) })
        #expect(bFrames.last == big)

        let aFrames = await aSink.wait(count: 1, timeout: .seconds(5))
        #expect(aFrames.first == Data("hi-from-b".utf8))
    }
}

/// A minimal async frame collector with a bounded wait — shared by the direct-path tests.
actor FrameSink {
    private var frames: [Data] = []
    func add(_ f: Data) { frames.append(f) }
    var all: [Data] { frames }

    func wait(count: Int, timeout: Duration) async -> [Data] {
        let clock = ContinuousClock()
        let deadline = clock.now + timeout
        while clock.now < deadline {
            if frames.count >= count { return frames }
            try? await clock.sleep(for: .milliseconds(20))
        }
        return frames
    }
}
