import XCTest @testable import Rosetta @MainActor final class CallSoundAndTimeoutTests: XCTestCase { private let ownKey = "02-own-sound-test" private let peerKey = "02-peer-sound-test" override func setUp() { super.setUp() CallManager.shared.resetForSessionEnd() CallManager.shared.bindAccount(publicKey: ownKey) } override func tearDown() { CallManager.shared.resetForSessionEnd() super.tearDown() } // MARK: - Ring Timeout Tests func testRingTimeoutTaskCreatedOnOutgoingCall() { let result = CallManager.shared.startOutgoingCall( toPublicKey: peerKey, title: "Peer", username: "peer" ) XCTAssertEqual(result, .started) XCTAssertNotNil(CallManager.shared.ringTimeoutTask, "Ring timeout should be set for outgoing call") XCTAssertEqual(CallManager.shared.uiState.phase, .outgoing) } func testRingTimeoutTaskCreatedOnIncomingCall() { let packet = PacketSignalPeer( src: peerKey, dst: ownKey, sharedPublic: "", signalType: .call, roomId: "" ) CallManager.shared.testHandleSignalPacket(packet) XCTAssertNotNil(CallManager.shared.ringTimeoutTask, "Ring timeout should be set for incoming call") XCTAssertEqual(CallManager.shared.uiState.phase, .incoming) } func testRingTimeoutCancelledOnAcceptIncoming() { // Set up incoming call let packet = PacketSignalPeer( src: peerKey, dst: ownKey, sharedPublic: "", signalType: .call, roomId: "" ) CallManager.shared.testHandleSignalPacket(packet) XCTAssertNotNil(CallManager.shared.ringTimeoutTask) // Accept _ = CallManager.shared.acceptIncomingCall() XCTAssertNil(CallManager.shared.ringTimeoutTask, "Ring timeout should be cancelled on accept") } func testRingTimeoutCancelledOnDeclineIncoming() { let packet = PacketSignalPeer( src: peerKey, dst: ownKey, sharedPublic: "", signalType: .call, roomId: "" ) CallManager.shared.testHandleSignalPacket(packet) XCTAssertNotNil(CallManager.shared.ringTimeoutTask) CallManager.shared.declineIncomingCall() XCTAssertNil(CallManager.shared.ringTimeoutTask, "Ring timeout should be cancelled on decline") XCTAssertEqual(CallManager.shared.uiState.phase, .idle) } func testRingTimeoutCancelledOnEndCall() { _ = CallManager.shared.startOutgoingCall( toPublicKey: peerKey, title: "Peer", username: "peer" ) XCTAssertNotNil(CallManager.shared.ringTimeoutTask) CallManager.shared.endCall() XCTAssertNil(CallManager.shared.ringTimeoutTask, "Ring timeout should be cancelled on endCall") } func testRingTimeoutCancelledOnBusySignal() { _ = CallManager.shared.startOutgoingCall( toPublicKey: peerKey, title: "Peer", username: "peer" ) XCTAssertNotNil(CallManager.shared.ringTimeoutTask) let busyPacket = PacketSignalPeer( src: "", dst: "", sharedPublic: "", signalType: .endCallBecauseBusy, roomId: "" ) CallManager.shared.testHandleSignalPacket(busyPacket) XCTAssertNil(CallManager.shared.ringTimeoutTask, "Ring timeout should be cancelled on busy signal") XCTAssertEqual(CallManager.shared.uiState.phase, .idle) } func testRingTimeoutCancelledOnPeerEndCall() { let incomingPacket = PacketSignalPeer( src: peerKey, dst: ownKey, sharedPublic: "", signalType: .call, roomId: "" ) CallManager.shared.testHandleSignalPacket(incomingPacket) XCTAssertNotNil(CallManager.shared.ringTimeoutTask) let endPacket = PacketSignalPeer( src: "", dst: "", sharedPublic: "", signalType: .endCall, roomId: "" ) CallManager.shared.testHandleSignalPacket(endPacket) XCTAssertNil(CallManager.shared.ringTimeoutTask, "Ring timeout should be cancelled when peer ends call") XCTAssertEqual(CallManager.shared.uiState.phase, .idle) } // MARK: - Sound Flow Tests (verify phase transitions trigger correct states) func testOutgoingCallSetsCorrectPhaseForSounds() { let result = CallManager.shared.startOutgoingCall( toPublicKey: peerKey, title: "Peer", username: "peer" ) XCTAssertEqual(result, .started) XCTAssertEqual(CallManager.shared.uiState.phase, .outgoing) // playCalling() should have been called (verified by phase) } func testIncomingCallSetsCorrectPhaseForSounds() { let packet = PacketSignalPeer( src: peerKey, dst: ownKey, sharedPublic: "", signalType: .call, roomId: "" ) CallManager.shared.testHandleSignalPacket(packet) XCTAssertEqual(CallManager.shared.uiState.phase, .incoming) // playRingtone() should have been called (verified by phase) } func testCallActiveTransitionClearsTimeout() { CallManager.shared.testSetUiState( CallUiState( phase: .webRtcExchange, peerPublicKey: peerKey, statusText: "Connecting..." ) ) // Manually set timeout to verify it gets cleared CallManager.shared.startRingTimeout() XCTAssertNotNil(CallManager.shared.ringTimeoutTask) CallManager.shared.setCallActiveIfNeeded() XCTAssertEqual(CallManager.shared.uiState.phase, .active) XCTAssertNil(CallManager.shared.ringTimeoutTask, "Ring timeout should be cancelled when call becomes active") } // MARK: - Duplicate Call Prevention func testCannotStartSecondCallWhileInCall() { _ = CallManager.shared.startOutgoingCall( toPublicKey: peerKey, title: "Peer", username: "peer" ) let result = CallManager.shared.startOutgoingCall( toPublicKey: "02-another-peer", title: "Another", username: "another" ) XCTAssertEqual(result, .alreadyInCall) XCTAssertEqual(CallManager.shared.uiState.peerPublicKey, peerKey, "Original call should not be replaced") } func testCannotAcceptWhenNotIncoming() { _ = CallManager.shared.startOutgoingCall( toPublicKey: peerKey, title: "Peer", username: "peer" ) let result = CallManager.shared.acceptIncomingCall() XCTAssertEqual(result, .notIncoming) } // MARK: - CallsViewModel Filter Tests func testCallLogDirectionMapping() { // Outgoing with duration > 0 = outgoing XCTAssertFalse(CallLogDirection.outgoing.isMissed) XCTAssertTrue(CallLogDirection.outgoing.isOutgoing) XCTAssertFalse(CallLogDirection.outgoing.isIncoming) // Incoming with duration > 0 = incoming XCTAssertFalse(CallLogDirection.incoming.isMissed) XCTAssertFalse(CallLogDirection.incoming.isOutgoing) XCTAssertTrue(CallLogDirection.incoming.isIncoming) // Duration 0 + incoming = missed XCTAssertTrue(CallLogDirection.missed.isMissed) XCTAssertTrue(CallLogDirection.missed.isIncoming) // Duration 0 + outgoing = rejected XCTAssertFalse(CallLogDirection.rejected.isMissed) XCTAssertTrue(CallLogDirection.rejected.isOutgoing) } }