import XCTest
@testable import Rosetta

/// Tests for race condition fixes in CallKit + custom UI interaction.
/// Covers: finishCall re-entrancy, double-accept prevention, phase guards,
/// CallKit foreground suppression, and UUID lifecycle.
@MainActor
final class CallRaceConditionTests: XCTestCase {
    private let ownKey = "02-own-race-test"
    private let peerKey = "02-peer-race-test"

    override func setUp() {
        super.setUp()
        CallManager.shared.resetForSessionEnd()
        CallManager.shared.bindAccount(publicKey: ownKey)
    }

    override func tearDown() {
        CallManager.shared.resetForSessionEnd()
        super.tearDown()
    }

    // MARK: - Helper

    private func simulateIncomingCall() {
        let packet = PacketSignalPeer(
            src: peerKey,
            dst: ownKey,
            sharedPublic: "",
            signalType: .call,
            roomId: ""
        )
        CallManager.shared.testHandleSignalPacket(packet)
    }

    // MARK: - finishCall Re-Entrancy Guard

    func testFinishCallReEntrancyGuardResetsAfterCompletion() {
        simulateIncomingCall()
        XCTAssertEqual(CallManager.shared.uiState.phase, .incoming)
        XCTAssertFalse(CallManager.shared.isFinishingCall)

        CallManager.shared.endCall()

        XCTAssertEqual(CallManager.shared.uiState.phase, .idle)
        XCTAssertFalse(CallManager.shared.isFinishingCall, "isFinishingCall must reset after finishCall completes (defer block)")
    }

    func testFinishCallNoOpWhenAlreadyIdle() {
        XCTAssertEqual(CallManager.shared.uiState.phase, .idle)

        // Should be a no-op — no crash, no state change
        CallManager.shared.finishCall(reason: "test", notifyPeer: false)

        XCTAssertEqual(CallManager.shared.uiState.phase, .idle)
        XCTAssertFalse(CallManager.shared.isFinishingCall)
    }

    func testFinishCallCleansUpPhaseToIdle() {
        _ = CallManager.shared.startOutgoingCall(
            toPublicKey: peerKey,
            title: "Peer",
            username: "peer"
        )
        XCTAssertEqual(CallManager.shared.uiState.phase, .outgoing)

        CallManager.shared.finishCall(reason: nil, notifyPeer: false)

        XCTAssertEqual(CallManager.shared.uiState.phase, .idle)
        XCTAssertFalse(CallManager.shared.isFinishingCall)
    }

    // MARK: - Double-Accept Prevention

    func testDoubleAcceptReturnsNotIncomingOnSecondCall() {
        simulateIncomingCall()
        XCTAssertEqual(CallManager.shared.uiState.phase, .incoming)

        // First accept succeeds
        let result1 = CallManager.shared.acceptIncomingCall()
        XCTAssertEqual(result1, .started)
        XCTAssertEqual(CallManager.shared.uiState.phase, .keyExchange)

        // Second accept fails — phase is no longer .incoming
        let result2 = CallManager.shared.acceptIncomingCall()
        XCTAssertEqual(result2, .notIncoming, "Second accept must fail — phase already changed to keyExchange")
    }

    func testAcceptAfterDeclineReturnsNotIncoming() {
        simulateIncomingCall()
        XCTAssertEqual(CallManager.shared.uiState.phase, .incoming)

        CallManager.shared.declineIncomingCall()
        XCTAssertEqual(CallManager.shared.uiState.phase, .idle)

        let result = CallManager.shared.acceptIncomingCall()
        XCTAssertEqual(result, .notIncoming, "Accept after decline must fail")
    }

    func testAcceptAfterEndCallReturnsNotIncoming() {
        simulateIncomingCall()
        XCTAssertEqual(CallManager.shared.uiState.phase, .incoming)

        CallManager.shared.endCall()
        XCTAssertEqual(CallManager.shared.uiState.phase, .idle)

        let result = CallManager.shared.acceptIncomingCall()
        XCTAssertEqual(result, .notIncoming, "Accept after endCall must fail")
    }

    func testAcceptWithNoAccountReturnsAccountNotBound() {
        CallManager.shared.bindAccount(publicKey: "")
        simulateIncomingCall()

        // Incoming signal was rejected because ownPublicKey is empty,
        // so phase stays idle.
        let result = CallManager.shared.acceptIncomingCall()
        XCTAssertEqual(result, .notIncoming)
    }

    // MARK: - Phase State Machine Guards

    func testIncomingCallWhileBusySendsBusySignal() {
        // Start outgoing call first
        _ = CallManager.shared.startOutgoingCall(
            toPublicKey: peerKey,
            title: "Peer",
            username: "peer"
        )
        XCTAssertEqual(CallManager.shared.uiState.phase, .outgoing)

        // Second incoming call from different peer
        let packet = PacketSignalPeer(
            src: "02-another-peer",
            dst: ownKey,
            sharedPublic: "",
            signalType: .call,
            roomId: ""
        )
        CallManager.shared.testHandleSignalPacket(packet)

        // Phase should still be outgoing (not replaced by incoming)
        XCTAssertEqual(CallManager.shared.uiState.phase, .outgoing, "Existing call must not be replaced by new incoming")
        XCTAssertEqual(CallManager.shared.uiState.peerPublicKey, peerKey, "Peer key must remain from original call")
    }

    func testEndCallBecauseBusySkipsAttachment() {
        _ = CallManager.shared.startOutgoingCall(
            toPublicKey: peerKey,
            title: "Peer",
            username: "peer"
        )
        XCTAssertEqual(CallManager.shared.uiState.phase, .outgoing)

        let busyPacket = PacketSignalPeer(
            src: "",
            dst: "",
            sharedPublic: "",
            signalType: .endCallBecauseBusy,
            roomId: ""
        )
        CallManager.shared.testHandleSignalPacket(busyPacket)

        XCTAssertEqual(CallManager.shared.uiState.phase, .idle)
        // skipAttachment=true prevents flooding chat with "Cancelled Call" bubbles
    }

    func testEndCallFromPeerTransitionsToIdle() {
        simulateIncomingCall()
        XCTAssertEqual(CallManager.shared.uiState.phase, .incoming)

        let endPacket = PacketSignalPeer(
            src: "",
            dst: "",
            sharedPublic: "",
            signalType: .endCall,
            roomId: ""
        )
        CallManager.shared.testHandleSignalPacket(endPacket)

        XCTAssertEqual(CallManager.shared.uiState.phase, .idle)
    }

    func testPeerDisconnectedTransitionsToIdle() {
        simulateIncomingCall()
        _ = CallManager.shared.acceptIncomingCall()
        XCTAssertEqual(CallManager.shared.uiState.phase, .keyExchange)

        let disconnectPacket = PacketSignalPeer(
            src: "",
            dst: "",
            sharedPublic: "",
            signalType: .endCallBecausePeerDisconnected,
            roomId: ""
        )
        CallManager.shared.testHandleSignalPacket(disconnectPacket)

        XCTAssertEqual(CallManager.shared.uiState.phase, .idle)
    }

    // MARK: - WebRTC Phase Guard

    func testSetCallActiveOnlyFromWebRtcExchange() {
        // Phase is idle — setCallActiveIfNeeded should be no-op
        CallManager.shared.setCallActiveIfNeeded()
        XCTAssertEqual(CallManager.shared.uiState.phase, .idle, "setCallActive must not activate from idle")

        // Phase is incoming — should also be no-op
        simulateIncomingCall()
        CallManager.shared.setCallActiveIfNeeded()
        XCTAssertEqual(CallManager.shared.uiState.phase, .incoming, "setCallActive must not activate from incoming")
    }

    func testSetCallActiveFromWebRtcExchangeSucceeds() {
        CallManager.shared.testSetUiState(
            CallUiState(
                phase: .webRtcExchange,
                peerPublicKey: peerKey,
                statusText: "Connecting..."
            )
        )

        CallManager.shared.setCallActiveIfNeeded()

        XCTAssertEqual(CallManager.shared.uiState.phase, .active)
    }

    // MARK: - CallKit UUID Lifecycle

    func testCallKitUUIDClearedAfterFinishCall() {
        simulateIncomingCall()

        CallManager.shared.endCall()

        XCTAssertEqual(CallManager.shared.uiState.phase, .idle)
        // CallKit UUID should be cleared after finishCall
        XCTAssertNil(CallKitManager.shared.currentCallUUID, "CallKit UUID must be cleared after call ends")
    }

    func testCallKitUUIDSetForIncoming() {
        // CallKit is always reported for incoming calls (foreground and background).
        // Telegram parity: CallKit compact banner in foreground, full screen on lock screen.
        simulateIncomingCall()

        XCTAssertEqual(CallManager.shared.uiState.phase, .incoming)
        XCTAssertNotNil(
            CallKitManager.shared.currentCallUUID,
            "CallKit must be reported for all incoming calls"
        )
    }

    // MARK: - UI State Visibility

    func testOverlayNotVisibleDuringIncoming() {
        // Telegram parity: CallKit handles incoming UI.
        // Custom overlay only appears after accept (phase != .incoming).
        simulateIncomingCall()

        XCTAssertTrue(CallManager.shared.uiState.isVisible)
        // isFullScreenVisible is true but MainTabView guards with phase != .incoming
        XCTAssertEqual(CallManager.shared.uiState.phase, .incoming)
    }

    func testOverlayVisibleAfterAccept() {
        simulateIncomingCall()
        _ = CallManager.shared.acceptIncomingCall()

        XCTAssertEqual(CallManager.shared.uiState.phase, .keyExchange)
        XCTAssertTrue(CallManager.shared.uiState.isFullScreenVisible)
        XCTAssertFalse(CallManager.shared.uiState.isMinimized)
    }

    func testOverlayHiddenAfterDecline() {
        simulateIncomingCall()
        CallManager.shared.declineIncomingCall()

        XCTAssertFalse(CallManager.shared.uiState.isVisible)
        XCTAssertFalse(CallManager.shared.uiState.isFullScreenVisible)
    }

    func testMinimizedBarVisibleAfterMinimize() {
        simulateIncomingCall()
        CallManager.shared.minimizeCall()

        XCTAssertTrue(CallManager.shared.uiState.isVisible)
        XCTAssertTrue(CallManager.shared.uiState.isMinimized)
        XCTAssertFalse(CallManager.shared.uiState.isFullScreenVisible)
    }

    func testExpandAfterMinimize() {
        simulateIncomingCall()
        CallManager.shared.minimizeCall()
        XCTAssertTrue(CallManager.shared.uiState.isMinimized)

        CallManager.shared.expandCall()

        XCTAssertFalse(CallManager.shared.uiState.isMinimized)
        XCTAssertTrue(CallManager.shared.uiState.isFullScreenVisible)
    }

    // MARK: - Rapid State Transitions

    func testRapidStartEndDoesNotCrash() {
        for _ in 0..<10 {
            _ = CallManager.shared.startOutgoingCall(
                toPublicKey: peerKey,
                title: "Peer",
                username: "peer"
            )
            CallManager.shared.endCall()
        }

        XCTAssertEqual(CallManager.shared.uiState.phase, .idle)
        XCTAssertFalse(CallManager.shared.isFinishingCall)
    }

    func testRapidIncomingDeclineDoesNotCrash() {
        for _ in 0..<10 {
            simulateIncomingCall()
            CallManager.shared.declineIncomingCall()
        }

        XCTAssertEqual(CallManager.shared.uiState.phase, .idle)
        XCTAssertFalse(CallManager.shared.isFinishingCall)
    }

    func testRapidAcceptDeclineCycle() {
        // Incoming → Accept → End → Incoming → Decline → repeat
        for i in 0..<5 {
            simulateIncomingCall()
            if i % 2 == 0 {
                _ = CallManager.shared.acceptIncomingCall()
                CallManager.shared.endCall()
            } else {
                CallManager.shared.declineIncomingCall()
            }
        }

        XCTAssertEqual(CallManager.shared.uiState.phase, .idle)
        XCTAssertFalse(CallManager.shared.isFinishingCall)
    }
}
