import Foundation import Testing import RemoteCore import TVTransport @testable import PhoneControl private actor FakeTV: PhoneTVControlling { var paired: [TelevisionTarget] = [] var connected: [TelevisionTarget] = [] var commands: [RemoteCommand] = [] var disconnects = 0 var pairHook: (@Sendable () async -> Void)? func setPairHook(_ hook: @escaping @Sendable () async -> Void) { pairHook = hook } var status: CommandReceipt.Status = .sentToTransport func pair(target: TelevisionTarget, sessionID: UUID) async -> TVConnectionState { paired.append(target) if let pairHook { await pairHook(); return .untrusted(televisionID: target.id) } return .ready(televisionID: target.id) } func connect(target: TelevisionTarget, sessionID: UUID) async -> TVConnectionState { connected.append(target); return .ready(televisionID: target.id) } func disconnect() async { disconnects += 1 } func stateUpdates() async -> AsyncStream { AsyncStream { $0.finish() } } func sendPhoneCommand(_ command: RemoteCommand) async -> CommandReceipt { commands.append(command); return .init(id: UUID(), status: status) } func failWrites() { status = .unavailable } } private actor MemorySelection: SelectedTelevisionStoring { var value: SelectedTelevision? var failSave = false init(_ value: SelectedTelevision? = nil, failSave: Bool = false) { self.value = value; self.failSave = failSave } func load() async throws -> SelectedTelevision? { value } func save(_ television: SelectedTelevision) async throws { if failSave { throw PhoneControlError.storageUnavailable }; value = television } func delete() async throws { value = nil } } private actor MemoryAuthorization: PhoneAuthorizationManaging { var forgotten: [UUID] = [] var approvals = 0 let failDelete: Bool init(failDelete: Bool = false) { self.failDelete = failDelete } func approveCertificate(sha256: Data, for televisionID: UUID, approvedAt: Date) async throws { approvals += 1 } func forgetTelevision(_ televisionID: UUID) async throws { if failDelete { throw PhoneControlError.storageUnavailable }; forgotten.append(televisionID) } } @MainActor struct PhoneControlTests { @Test func restorationUsesSavedIdentityAndNeverPairs() async { let selected = SelectedTelevision(host: "tv.local") let tv = FakeTV() let controller = LivePhoneController(transport: tv, authorization: MemoryAuthorization(), selection: MemorySelection(selected)) await controller.start() #expect(controller.television == selected) #expect(controller.isReady) #expect(await tv.paired.isEmpty) #expect(await tv.connected.map(\.id) == [selected.id]) await controller.pair(address: selected.host) #expect(await tv.paired.map(\.id) == [selected.id]) } @Test func storageFailurePreventsPairingAndDifferentTargetRequiresForget() async { let tv = FakeTV() let controller = LivePhoneController(transport: tv, authorization: MemoryAuthorization(), selection: MemorySelection(failSave: true)) await controller.pair(address: "tv.local") #expect(await tv.paired.isEmpty) #expect(controller.error != nil) let existing = LivePhoneController(transport: tv, authorization: MemoryAuthorization(), selection: MemorySelection(.init(host: "old.local"))) await existing.pair(address: "new.local") #expect(await tv.paired.isEmpty) #expect(existing.television?.host == "old.local") } @Test func partialForgetKeepsIdentityForDeletionRetryAndDisconnects() async throws { let selected = SelectedTelevision(host: "tv.local") let store = MemorySelection(selected) let tv = FakeTV() let controller = LivePhoneController(transport: tv, authorization: MemoryAuthorization(failDelete: true), selection: store) await controller.start() await controller.forget() #expect(!controller.isReady) #expect(controller.error != nil) #expect(try await store.load() == selected) #expect(await tv.disconnects == 1) } @Test func forgetDeletesSecurityAndSelection() async throws { let selected = SelectedTelevision(host: "tv.local") let store = MemorySelection(selected) let auth = MemoryAuthorization() let controller = LivePhoneController(transport: FakeTV(), authorization: auth, selection: store) await controller.start(); await controller.forget() #expect(controller.television == nil) #expect(try await store.load() == nil) #expect(await auth.forgotten == [selected.id]) } @Test func ambiguousWriteIsNotReplayedAndDemoDisconnectsLiveRoute() async { let tv = FakeTV() await tv.failWrites() let controller = LivePhoneController(transport: tv, authorization: MemoryAuthorization(), selection: MemorySelection(.init(host: "tv.local"))) await controller.start(); await controller.send(.right) #expect(await tv.commands == [.right]) #expect(controller.error?.contains("uncertain") == true) await controller.setDemo(true); await controller.send(.left) #expect(controller.isDemo && !controller.isReady) #expect(await tv.commands == [.right]) #expect(await tv.disconnects == 1) } @Test func certificateRequiresExplicitApprovalAndDoesNotRetryPairing() async throws { let auth = MemoryAuthorization() let tv = FakeTV() let controller = LivePhoneController(transport: tv, authorization: auth, selection: MemorySelection()) await tv.setPairHook { await controller.receiveUntrustedCertificate(host: "other.local", sha256: Data(repeating: 1, count: 32)) await controller.receiveUntrustedCertificate(host: "tv.local", sha256: Data(repeating: 2, count: 32)) } await controller.pair(address: "tv.local") let candidate = try #require(controller.certificate) #expect(candidate.sha256 == Data(repeating: 2, count: 32)) #expect(await auth.approvals == 0) await controller.approveCertificate(candidate) #expect(await auth.approvals == 1) #expect(await tv.paired.count == 1) #expect(!controller.isReady) #expect(controller.certificate == nil) await controller.approveCertificate(candidate) #expect(await auth.approvals == 1) } @Test func unsolicitedCertificateIsNotOfferedOrApproved() async { let auth = MemoryAuthorization() let controller = LivePhoneController(transport: FakeTV(), authorization: auth, selection: MemorySelection(.init(host: "tv.local"))) await controller.start() controller.receiveUntrustedCertificate(host: "tv.local", sha256: Data(repeating: 1, count: 32)) #expect(controller.certificate == nil) #expect(await auth.approvals == 0) } }