import Foundation import Combine import Testing @testable import bitchat @Suite("TransferProgressManager Tests") struct TransferProgressManagerTests { @Test("Start publishes started event and stores snapshot") @MainActor func startPublishesAndStoresSnapshot() async throws { let manager = TransferProgressManager() let transferID = "transfer-start" var cancellable: AnyCancellable? let recorder = EventRecorder() cancellable = manager.publisher.sink { event in if case .started(let id, let total) = event { recorder.append("started:\(id):\(total)") } } manager.start(id: transferID, totalFragments: 3) let didReceive = await TestHelpers.waitUntil({ recorder.values == ["started:\(transferID):3"] }, timeout: 5.0) #expect(didReceive) #expect(recorder.values == ["started:\(transferID):3"]) #expect(manager.snapshot(id: transferID)?.sent == 0) #expect(manager.snapshot(id: transferID)?.total == 3) _ = cancellable } @Test("Sending final fragment publishes update and completion then clears snapshot") @MainActor func recordFragmentSentPublishesProgressAndCompletion() async throws { let manager = TransferProgressManager() let transferID = "transfer-complete" var cancellable: AnyCancellable? let recorder = EventRecorder() cancellable = manager.publisher.sink { event in switch event { case .started(let id, let total): recorder.append("started:\(id):\(total)") case .updated(let id, let sent, let total): recorder.append("updated:\(id):\(sent):\(total)") case .completed(let id, let total): recorder.append("completed:\(id):\(total)") case .cancelled: break } } manager.start(id: transferID, totalFragments: 1) manager.recordFragmentSent(id: transferID) let didReceive = await TestHelpers.waitUntil({ recorder.values.count == 3 }, timeout: 5.0) #expect(didReceive) #expect(recorder.values == [ "started:\(transferID):1", "updated:\(transferID):1:1", "completed:\(transferID):1" ]) #expect(manager.snapshot(id: transferID) == nil) _ = cancellable } @Test("Cancel publishes cancelled event and clears state") @MainActor func cancelPublishesAndClearsState() async throws { let manager = TransferProgressManager() let transferID = "transfer-cancel" var cancellable: AnyCancellable? let recorder = EventRecorder() cancellable = manager.publisher.sink { event in switch event { case .started(let id, let total): recorder.append("started:\(id):\(total)") case .cancelled(let id, let sent, let total): recorder.append("cancelled:\(id):\(sent):\(total)") case .updated, .completed: break } } manager.start(id: transferID, totalFragments: 4) manager.recordFragmentSent(id: transferID) manager.cancel(id: transferID) let didReceive = await TestHelpers.waitUntil({ recorder.values.contains("started:\(transferID):4") && recorder.values.contains("cancelled:\(transferID):1:4") }, timeout: 5.0) #expect(didReceive) #expect(recorder.values.contains("started:\(transferID):4")) #expect(recorder.values.contains("cancelled:\(transferID):1:4")) #expect(manager.snapshot(id: transferID) == nil) _ = cancellable } @Test("Preflight rejection publishes cancellation without a started state") @MainActor func rejectBeforeStartPublishesCancellation() async { let manager = TransferProgressManager() let transferID = "transfer-preflight-reject" var cancellable: AnyCancellable? let recorder = EventRecorder() cancellable = manager.publisher.sink { event in if case .cancelled(let id, let sent, let total) = event { recorder.append("cancelled:\(id):\(sent):\(total)") } } manager.rejectBeforeStart(id: transferID) let didReceive = await TestHelpers.waitUntil({ recorder.values == ["cancelled:\(transferID):0:0"] }, timeout: 5.0) #expect(didReceive) #expect(manager.snapshot(id: transferID) == nil) _ = cancellable } } private final class EventRecorder: @unchecked Sendable { private let lock = NSLock() private var storage: [String] = [] var values: [String] { lock.lock() defer { lock.unlock() } return storage } func append(_ value: String) { lock.lock() storage.append(value) lock.unlock() } }