import Foundation import Testing import RemoteCore import WatchBridgeCommon import WatchBridgeWatch @MainActor private final class FakeWatchSession: RelaySessionSending { var activationState: RelayActivationState = .notActivated var isReachable = true var sent: [[String: Any]] = [] var reply: (([String: Any]) -> Void)? var error: ((Error) -> Void)? var replyTwice = false func activate() { activationState = .activated } func sendMessage(_ message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void, errorHandler: @escaping (Error) -> Void) { sent.append(message) reply = replyHandler error = errorHandler } func updateApplicationContext(_ context: [String: Any]) throws {} func answer(_ value: [String: Any]) { reply?(value) if replyTwice { reply?(value); error?(RelayTestError.failed) } } } private enum RelayTestError: Error { case failed } @MainActor @Test func watchNegotiatesBeforeSendingCommandAndUsesMilliseconds() async throws { let fake = FakeWatchSession() let bridge = WatchRelayBridge(session: fake) bridge.activate() bridge.activationDidComplete(error: nil) bridge.beginSession() #expect(fake.sent.count == 1) let request = try RelayContainer(dictionary: fake.sent[0]) #expect(request.kind == .beginSession) let sessionID = UUID(), tvID = UUID() let grant = RemoteSessionGrant(sessionID: sessionID, televisionID: tvID, issuedAt: Date(timeIntervalSince1970: 1_700_000_000), maximumCommandLifetimeMilliseconds: 500) fake.answer(try RelayContainer(kind: .session, value: grant).dictionary()) await Task.yield() bridge.sendCommand(.right, now: Date(timeIntervalSince1970: 1_700_000_000)) #expect(fake.sent.count == 2) let command = try RelayContainer(dictionary: fake.sent[1]) let envelope = try command.decode(CommandEnvelope.self, expectedKind: .command) #expect(envelope.sessionID == sessionID) #expect(envelope.televisionID == tvID) #expect(envelope.sequence == 0) #expect(envelope.expiresAt.timeIntervalSince1970 == 1_700_000_000.5) } @MainActor @Test func watchDropsCommandsWhenUnreachableAndDuplicateRepliesCompleteOnce() async throws { let fake = FakeWatchSession() let bridge = WatchRelayBridge(session: fake) bridge.activate() bridge.activationDidComplete(error: nil) fake.isReachable = false bridge.beginSession() #expect(fake.sent.isEmpty) fake.isReachable = true bridge.beginSession() fake.replyTwice = true let grant = RemoteSessionGrant(sessionID: UUID(), televisionID: UUID(), issuedAt: Date()) fake.answer(try RelayContainer(kind: .session, value: grant).dictionary()) await Task.yield() #expect(bridge.state == .armed(sessionID: grant.sessionID, televisionID: grant.televisionID)) } @MainActor @Test func watchDeactivationDisarmsAndClearsGrant() async throws { let fake = FakeWatchSession() let bridge = WatchRelayBridge(session: fake) bridge.activate() bridge.activationDidComplete(error: nil) bridge.beginSession() let grant = RemoteSessionGrant(sessionID: UUID(), televisionID: UUID(), issuedAt: Date()) fake.answer(try RelayContainer(kind: .session, value: grant).dictionary()) await Task.yield() bridge.didDeactivate() #expect(bridge.state == .inactive) #expect(fake.sent.count == 1) } @MainActor @Test func watchReachabilityLossRevokesArmedGrantAndNeverReplays() async throws { let fake = FakeWatchSession() let bridge = WatchRelayBridge(session: fake) bridge.activate(); bridge.activationDidComplete(error: nil); bridge.beginSession() let grant = RemoteSessionGrant(sessionID: UUID(), televisionID: UUID(), issuedAt: Date()) fake.answer(try RelayContainer(kind: .session, value: grant).dictionary()) await Task.yield() #expect(bridge.grant != nil) fake.isReachable = false; bridge.reachabilityDidChange() #expect(bridge.grant == nil) #expect(bridge.state == .ready(reachable: false)) fake.isReachable = true; bridge.reachabilityDidChange() bridge.sendCommand(.right) #expect(fake.sent.count == 1) } @MainActor @Test func lateGrantAfterStopDoesNotRearmAndRepeatedConnectDoesNotDuplicate() async throws { let fake = FakeWatchSession() let bridge = WatchRelayBridge(session: fake) bridge.activate(); bridge.activationDidComplete(error: nil) bridge.beginSession(); bridge.beginSession() #expect(fake.sent.count == 1) let lateReply = fake.reply bridge.endSession() let grant = RemoteSessionGrant(sessionID: UUID(), televisionID: UUID(), issuedAt: Date()) lateReply?(try RelayContainer(kind: .session, value: grant).dictionary()) await Task.yield() #expect(bridge.grant == nil) #expect(bridge.state == .ready(reachable: true)) } @MainActor @Test func invalidationCompletesCommandOnceAndDiscardsLateSuccess() async throws { let fake = FakeWatchSession() let bridge = WatchRelayBridge(session: fake) bridge.activate(); bridge.activationDidComplete(error: nil); bridge.beginSession() let grant = RemoteSessionGrant(sessionID: UUID(), televisionID: UUID(), issuedAt: Date()) fake.answer(try RelayContainer(kind: .session, value: grant).dictionary()) await Task.yield() var completions = 0 var succeeded = false bridge.sendCommand(.right) { result in completions += 1 if case .success = result { succeeded = true } } let command = try RelayContainer(dictionary: fake.sent.last!).decode(CommandEnvelope.self, expectedKind: .command) let lateReply = fake.reply bridge.didDeactivate() lateReply?(try RelayContainer(kind: .receipt, value: CommandReceipt(id: command.id, status: .sentToTransport)).dictionary()) for _ in 0..<10 { await Task.yield() } #expect(completions == 1) #expect(!succeeded) #expect(bridge.grant == nil) }