import Foundation
import Testing
@testable import IrdP2P
@testable import IrdP2PSignaling
import IrdP2PCrypto
@Suite struct NegotiationTests {
@Test func lowerPeerOffersAndBothGoDirect() async throws {
let h = Harness()
let (a, ca) = try await h.join(peer: highPeer)
let (b, cb) = try await h.join(peer: lowPeer)
#expect(await ca.waitFor(.peerReady(lowPeer, .direct)))
#expect(await cb.waitFor(.peerReady(highPeer, .direct)))
// the offer came from the lower id: find the first signal frame carrying an offer, sent by lowPeer
let offers = h.relay.frames.filter { $0.from == lowPeer }
#expect(!offers.isEmpty)
await a.leave(); await b.leave()
}
@Test func dataChannelLossFallsBackThenRestartsViaTheLadder() async throws {
let h = Harness()
let (a, ca) = try await h.join(peer: lowPeer)
let (b, cb) = try await h.join(peer: highPeer)
#expect(await ca.waitFor(.peerReady(highPeer, .direct)))
#expect(await cb.waitFor(.peerReady(lowPeer, .direct)))
#expect(await h.webrtc.connectionAttempts == 1)
await h.webrtc.failAll()
#expect(await ca.waitFor(timeout: .seconds(5)) { $0 == .peerReady(highPeer, .relayed) && ca.count(.peerReady(highPeer, .relayed)) >= 2 } != nil)
#expect(await cb.waitFor(timeout: .seconds(5)) { $0 == .peerReady(lowPeer, .relayed) && cb.count(.peerReady(lowPeer, .relayed)) >= 2 } != nil)
// relayed messaging works meanwhile
try await a.send(to: highPeer, .json(.string("via relay")))
#expect(await cb.waitFor(.message(from: lowPeer, .json(.string("via relay")))))
// the offerer's ladder: its 2 s restart sleeper joins the two keepalive sleepers
#expect(await eventuallyTrue { h.clock.sleeperCount >= 3 })
h.clock.advance(by: .seconds(2))
#expect(await eventuallyTrue { await h.webrtc.connectionAttempts == 2 })
#expect(await ca.waitFor(timeout: .seconds(5)) { $0 == .peerReady(highPeer, .direct) && ca.count(.peerReady(highPeer, .direct)) >= 2 } != nil)
#expect(await cb.waitFor(timeout: .seconds(5)) { $0 == .peerReady(lowPeer, .direct) && cb.count(.peerReady(lowPeer, .direct)) >= 2 } != nil)
try await b.send(to: lowPeer, .json(.string("direct again")))
#expect(await ca.waitFor(.message(from: highPeer, .json(.string("direct again")))))
await a.leave(); await b.leave()
}
@Test func failedNegotiationsWalkTheLadderThenWaitForWake() async throws {
let h = Harness()
await h.webrtc.failNextConnections(10)
let (a, ca) = try await h.join(peer: lowPeer)
let (b, cb) = try await h.join(peer: highPeer)
#expect(await ca.waitFor(.peerReady(highPeer, .relayed)))
#expect(await cb.waitFor(.peerReady(lowPeer, .relayed)))
#expect(await eventuallyTrue { await h.webrtc.connectionAttempts == 1 }) // the first negotiation failed
var expected = 1
for delay in Backoff.iceRestartLadder {
// the offerer's restart sleeper is registered once the failure was processed
#expect(await eventuallyTrue { h.clock.sleeperCount >= 3 }) // two keepalives + the restart
h.clock.advance(by: delay)
expected += 1
let want = expected
#expect(await eventuallyTrue { await h.webrtc.connectionAttempts == want }, "ladder step \(delay)")
}
#expect(!ca.contains(.peerReady(highPeer, .direct)))
// out of ladder: nothing more is scheduled until wake()
h.clock.advance(by: .seconds(60))
try? await Task.sleep(for: .milliseconds(50))
let finalAttempts = expected
#expect(await h.webrtc.connectionAttempts == finalAttempts)
await h.webrtc.failNextConnections(-10)
await a.wake()
let direct = await eventuallyTrue(timeout: .seconds(5)) {
let ta = await a.transport(of: highPeer)
let tb = await b.transport(of: lowPeer)
return ta == .direct && tb == .direct
}
if !direct { Collector.dump("wake did not reconnect", h, [("a", ca), ("b", cb)]) }
#expect(direct)
await a.leave(); await b.leave()
}
@Test func relayOnlyRoomsNeverNegotiate() async throws {
let h = Harness()
let (a, ca) = try await h.join(peer: lowPeer, preferDataChannel: false)
let (b, cb) = try await h.join(peer: highPeer, preferDataChannel: false)
#expect(await ca.waitFor(.peerReady(highPeer, .relayed)))
#expect(await cb.waitFor(.peerReady(lowPeer, .relayed)))
try? await Task.sleep(for: .milliseconds(50))
#expect(!ca.contains(.peerReady(highPeer, .direct)))
#expect(await a.transport(of: highPeer) == .relayed)
await a.leave(); await b.leave()
}
}