import BitFoundation
import Combine
import CoreBluetooth
import Foundation
import Testing
@testable import bitchat

/// Wire-level coverage for finalized DM media. The sender encrypts one typed
/// private-file payload, relays see only the outer Noise packet/fragments, and
/// the receiver reassembles, decrypts, validates, persists, and delivers it.
@Suite("Private media end to end", .serialized)
struct PrivateMediaEndToEndTests {
    @Test
    func privateMediaCancellationTombstonesAreCountBounded() async {
        let root = FileManager.default.temporaryDirectory
            .appendingPathComponent("private-media-tombstone-bound-\(UUID().uuidString)", isDirectory: true)
        defer { try? FileManager.default.removeItem(at: root) }
        let service = makeService(baseDirectory: root)

        for index in 0..<600 {
            service.cancelTransfer("cancelled-before-admission-\(index)")
        }

        #expect(service._test_privateMediaAdmissionEntryCount() <= 512)
        await service._test_drainPrivateMediaSendPipeline()
    }

    @Test
    func privateMediaAdmissionCapacityRejectsNewcomerWithoutEvictingActiveTransfer() async {
        let root = FileManager.default.temporaryDirectory
            .appendingPathComponent("private-media-admission-capacity-\(UUID().uuidString)", isDirectory: true)
        defer { try? FileManager.default.removeItem(at: root) }
        let service = makeService(baseDirectory: root)
        let now = Date()
        let activeIDs = (0..<512).map { "capacity-active-\($0)" }
        for transferId in activeIDs {
            #expect(service._test_beginPrivateMediaAdmission(transferId, now: now))
        }
        defer {
            for transferId in activeIDs {
                service._test_finishPrivateMediaAdmission(transferId)
            }
        }

        let overflowID = "capacity-overflow-\(UUID().uuidString)"
        let rejections = TransferCancellationRecorder()
        let cancellable = TransferProgressManager.shared.publisher.sink { rejections.record($0) }
        let content = Data("%PDF-1.7\ncapacity".utf8)
        service.sendFilePrivate(
            BitchatFilePacket(
                fileName: "capacity.pdf",
                fileSize: UInt64(content.count),
                mimeType: "application/pdf",
                content: content
            ),
            to: PeerID(str: "1122334455667788"),
            transferId: overflowID,
            allowLegacyFallback: true
        )

        #expect(await TestHelpers.waitUntil(
            { rejections.contains(overflowID) },
            timeout: TestConstants.longTimeout
        ))
        #expect(rejections.reason(for: overflowID) != nil)
        #expect(service._test_isPrivateMediaAdmissionActive(activeIDs[0], now: now))
        #expect(service._test_privateMediaAdmissionEntryCount() == 512)
        _ = cancellable
    }

    @Test
    func expiredActivePrivateMediaAdmissionEmitsVisibleFailure() async {
        let root = FileManager.default.temporaryDirectory
            .appendingPathComponent("private-media-admission-expiry-\(UUID().uuidString)", isDirectory: true)
        defer { try? FileManager.default.removeItem(at: root) }
        let service = makeService(baseDirectory: root)
        let transferId = "expired-active-\(UUID().uuidString)"
        let admittedAt = Date(timeIntervalSince1970: 1_000)
        let rejections = TransferCancellationRecorder()
        let cancellable = TransferProgressManager.shared.publisher.sink { rejections.record($0) }

        #expect(service._test_beginPrivateMediaAdmission(transferId, now: admittedAt))
        #expect(!service._test_isPrivateMediaAdmissionActive(
            transferId,
            now: admittedAt.addingTimeInterval(60 * 60 + 1)
        ))
        #expect(await TestHelpers.waitUntil(
            { rejections.contains(transferId) },
            timeout: TestConstants.longTimeout
        ))
        #expect(rejections.reason(for: transferId) != nil)
        #expect(service._test_privateMediaAdmissionEntryCount() == 0)
        _ = cancellable
    }

    @Test
    func approvedLegacySendCancelledBeforeDeferredAdmissionDoesNotTransmit() async throws {
        try await assertApprovedLegacySendCancelledBeforeAdmission(label: "cancel")
    }

    @Test
    func approvedLegacySendDeletedBeforeDeferredAdmissionDoesNotTransmit() async throws {
        // ChatMediaTransferCoordinator.deleteMediaMessage now invokes this same
        // synchronous transport cancellation before removing its mapping; its
        // coordinator-level call is covered separately in the context tests.
        try await assertApprovedLegacySendCancelledBeforeAdmission(label: "delete")
    }

    @Test
    func panicSuspensionFinishesAdmissionAtInitialDeferredSendBoundary() async {
        let root = FileManager.default.temporaryDirectory
            .appendingPathComponent("private-media-panic-deferred-\(UUID().uuidString)", isDirectory: true)
        defer { try? FileManager.default.removeItem(at: root) }
        let service = makeService(baseDirectory: root)
        let tap = PacketTap()
        service._test_onOutboundPacket = tap.record
        service.suspendForPanicReset()
        defer { service.completePanicReset(restartServices: false) }

        let transferId = "panic-deferred-\(UUID().uuidString)"
        let content = Data("%PDF-1.7\npanic-deferred".utf8)
        service.sendFilePrivate(
            BitchatFilePacket(
                fileName: "panic-deferred.pdf",
                fileSize: UInt64(content.count),
                mimeType: "application/pdf",
                content: content
            ),
            to: PeerID(str: "1122334455667788"),
            transferId: transferId,
            allowLegacyFallback: true
        )
        await service._test_drainPrivateMediaSendPipeline()

        let state = service._test_privateMediaTransferState(transferId: transferId)
        #expect(!state.admissionActive)
        #expect(!state.pendingNoise)
        #expect(state.activeScheduler == 0)
        #expect(state.pendingScheduler == 0)
        #expect(service._test_privateMediaAdmissionEntryCount() == 0)
        #expect(tap.snapshot().isEmpty)
    }

    @Test
    func panicSuspensionFinishesAdmissionAtBroadcastBoundary() {
        let root = FileManager.default.temporaryDirectory
            .appendingPathComponent("private-media-panic-broadcast-\(UUID().uuidString)", isDirectory: true)
        defer { try? FileManager.default.removeItem(at: root) }
        let service = makeService(baseDirectory: root)
        let tap = PacketTap()
        service._test_onOutboundPacket = tap.record
        service.suspendForPanicReset()
        defer { service.completePanicReset(restartServices: false) }

        let transferId = "panic-broadcast-\(UUID().uuidString)"
        #expect(service._test_beginPrivateMediaAdmission(transferId, now: Date()))
        let packet = BitchatPacket(
            type: MessageType.noiseEncrypted.rawValue,
            senderID: Data(hexString: service.myPeerID.id) ?? Data(),
            recipientID: Data(hexString: "1122334455667788"),
            timestamp: UInt64(Date().timeIntervalSince1970 * 1_000),
            payload: Data([NoisePayloadType.privateFile.rawValue]),
            signature: nil,
            ttl: TransportConfig.messageTTLDefault
        )

        service._test_broadcastPrivateMediaPacket(packet, transferId: transferId)

        #expect(!service._test_isPrivateMediaAdmissionActive(transferId, now: Date()))
        #expect(service._test_privateMediaAdmissionEntryCount() == 0)
        #expect(tap.snapshot().isEmpty)
    }

    @Test
    func legacyFallbackRequiresPerSendConsentAndConsumesItOnce() async throws {
        let root = FileManager.default.temporaryDirectory
            .appendingPathComponent("private-media-capability-\(UUID().uuidString)", isDirectory: true)
        defer { try? FileManager.default.removeItem(at: root) }
        let aliceRoot = root.appendingPathComponent("alice", isDirectory: true)
        let bobRoot = root.appendingPathComponent("bob", isDirectory: true)
        let alice = makeService(baseDirectory: aliceRoot)
        let bob = makeService(baseDirectory: bobRoot)
        alice._test_seedConnectedPeer(
            bob.myPeerID,
            nickname: "Old Bob",
            noisePublicKey: bob.noiseStaticPublicKeyData()
        )

        let tap = PacketTap()
        let delegate = MessageCaptureDelegate()
        alice._test_onOutboundPacket = tap.record
        bob.delegate = delegate
        let content = Data("%PDF-1.7\nprivate".utf8)
        let file = BitchatFilePacket(
            fileName: "private.pdf",
            fileSize: UInt64(content.count),
            mimeType: "application/pdf",
            content: content
        )
        let cancellations = TransferCancellationRecorder()
        let cancellable = TransferProgressManager.shared.publisher.sink { cancellations.record($0) }

        let deniedID = "legacy-without-consent-\(UUID().uuidString)"
        alice.sendFilePrivate(
            file,
            to: bob.myPeerID,
            transferId: deniedID,
            allowLegacyFallback: false
        )
        let denied = await TestHelpers.waitUntil(
            { cancellations.contains(deniedID) },
            timeout: TestConstants.longTimeout
        )
        #expect(denied)
        #expect(tap.snapshot().allSatisfy { $0.type != MessageType.fileTransfer.rawValue })

        let allowedID = "legacy-with-consent-\(UUID().uuidString)"
        alice.sendFilePrivate(
            file,
            to: bob.myPeerID,
            transferId: allowedID,
            allowLegacyFallback: true
        )

        let sent = await TestHelpers.waitUntil(
            { tap.snapshot().contains { $0.type == MessageType.fileTransfer.rawValue } },
            timeout: TestConstants.longTimeout
        )
        #expect(sent)

        let outbound = tap.snapshot()
        let rawTransfers = outbound.filter { $0.type == MessageType.fileTransfer.rawValue }
        let raw = try #require(rawTransfers.first)
        #expect(rawTransfers.count == 1, "Migration fallback must never dual-send")
        #expect(outbound.allSatisfy { $0.type != MessageType.noiseEncrypted.rawValue })
        #expect(raw.recipientID == Data(hexString: bob.myPeerID.toShort().id))
        #expect(raw.signature?.count == 64)
        #expect(BitchatFilePacket.decode(raw.payload)?.content == content)

        // Exercise the normal raw receive path with Alice's actual signing
        // key. The migration fallback is accepted because it is directed and
        // signed; the handler still rejects unsigned/forged raw transfers.
        bob._test_handlePacket(
            raw,
            fromPeerID: alice.myPeerID,
            signingPublicKey: alice.noiseSigningPublicKeyData()
        )
        let delivered = await TestHelpers.waitUntil(
            { delegate.snapshot().count == 1 },
            timeout: TestConstants.longTimeout
        )
        #expect(delivered)
        #expect(delegate.snapshot().first?.isPrivate == true)
        #expect(recursivelyStoredFiles(under: bobRoot).count == 1)

        // Consent is invocation-scoped, not a sticky peer preference.
        let retryID = "legacy-retry-without-consent-\(UUID().uuidString)"
        alice.sendFilePrivate(file, to: bob.myPeerID, transferId: retryID, allowLegacyFallback: false)
        let retryDenied = await TestHelpers.waitUntil(
            { cancellations.contains(retryID) },
            timeout: TestConstants.longTimeout
        )
        #expect(retryDenied)
        #expect(tap.snapshot().filter { $0.type == MessageType.fileTransfer.rawValue }.count == 1)
        _ = cancellable
    }

    @Test
    func authenticatedPrivateMediaCapabilityPinsAgainstRawDowngrade() async throws {
        let root = FileManager.default.temporaryDirectory
            .appendingPathComponent("private-media-pin-\(UUID().uuidString)", isDirectory: true)
        defer { try? FileManager.default.removeItem(at: root) }
        let identity = MockIdentityManager(MockKeychain())
        let alice = makeService(
            baseDirectory: root.appendingPathComponent("alice", isDirectory: true),
            identityManager: identity
        )
        let bob = makeService(baseDirectory: root.appendingPathComponent("bob", isDirectory: true))
        let bobKey = bob.noiseStaticPublicKeyData()

        alice._test_seedConnectedPeer(
            bob.myPeerID,
            nickname: "Bob",
            capabilities: .privateMedia,
            noisePublicKey: bobKey
        )
        #expect(alice.privateMediaSendPolicy(to: bob.myPeerID) == .awaitingCapabilityProof)
        let bobFingerprint = bobKey.sha256Fingerprint()
        #expect(!identity.hasObservedPrivateMediaCapability(fingerprint: bobFingerprint))

        try await establishSession(alice: alice, bob: bob)
        let capabilityPinned = await TestHelpers.waitUntil(
            { identity.hasObservedPrivateMediaCapability(fingerprint: bobFingerprint) },
            timeout: TestConstants.longTimeout
        )
        #expect(capabilityPinned)

        #expect(alice.privateMediaSendPolicy(to: bob.myPeerID) == .encrypted)

        // A public no-bit announce cannot override state authenticated by the
        // current session. A later authenticated no-bit state is a real
        // downgrade and must block despite a caller offering legacy consent.
        alice._test_seedConnectedPeer(
            bob.myPeerID,
            nickname: "Bob",
            capabilities: [],
            noisePublicKey: bobKey
        )
        #expect(alice.privateMediaSendPolicy(to: bob.myPeerID) == .encrypted)
        let authenticatedNoBit = try authenticatedPeerStatePacket(
            from: bob,
            to: alice,
            capabilities: []
        )
        alice._test_handlePacket(authenticatedNoBit, fromPeerID: bob.myPeerID)
        let downgradeObserved = await TestHelpers.waitUntil(
            { alice.privateMediaSendPolicy(to: bob.myPeerID) == .blockedDowngrade },
            timeout: TestConstants.longTimeout
        )
        #expect(downgradeObserved)
        #expect(alice.privateMediaSendPolicy(to: bob.myPeerID) == .blockedDowngrade)

        let tap = PacketTap()
        alice._test_onOutboundPacket = tap.record
        let transferID = "pinned-downgrade-\(UUID().uuidString)"
        let cancellations = TransferCancellationRecorder()
        let cancellable = TransferProgressManager.shared.publisher.sink { cancellations.record($0) }
        let content = Data("%PDF-1.7\nblocked".utf8)
        alice.sendFilePrivate(
            BitchatFilePacket(
                fileName: "blocked.pdf",
                fileSize: UInt64(content.count),
                mimeType: "application/pdf",
                content: content
            ),
            to: bob.myPeerID,
            transferId: transferID,
            allowLegacyFallback: true
        )

        let blocked = await TestHelpers.waitUntil(
            { cancellations.contains(transferID) },
            timeout: TestConstants.longTimeout
        )
        #expect(blocked)
        #expect(tap.snapshot().allSatisfy { $0.type != MessageType.fileTransfer.rawValue })
        _ = cancellable
    }

    @Test
    func unpinnedExplicitCapabilitiesWithoutPrivateMediaRequireConsent() {
        let root = FileManager.default.temporaryDirectory
            .appendingPathComponent("private-media-explicit-capabilities-\(UUID().uuidString)", isDirectory: true)
        defer { try? FileManager.default.removeItem(at: root) }
        let alice = makeService(baseDirectory: root.appendingPathComponent("alice", isDirectory: true))
        let bob = makeService(baseDirectory: root.appendingPathComponent("bob", isDirectory: true))

        alice._test_seedConnectedPeer(
            bob.myPeerID,
            nickname: "Modern Bob",
            capabilities: [],
            noisePublicKey: bob.noiseStaticPublicKeyData()
        )

        #expect(alice.privateMediaSendPolicy(to: bob.myPeerID) == .legacyRequiresConsent)
    }

    @Test
    func privateMediaRetryRequiresExactAuthenticatedBit9Proof() async throws {
        let root = FileManager.default.temporaryDirectory
            .appendingPathComponent(
                "private-media-receipt-proof-\(UUID().uuidString)",
                isDirectory: true
            )
        defer { try? FileManager.default.removeItem(at: root) }
        let alice = makeService(
            baseDirectory: root.appendingPathComponent(
                "alice",
                isDirectory: true
            )
        )
        let bob = makeService(
            baseDirectory: root.appendingPathComponent(
                "bob",
                isDirectory: true
            )
        )
        let bothCapabilities: PeerCapabilities = [
            .privateMedia,
            .privateMediaReceipts
        ]

        // A public bit-9 announce is discovery only.
        alice._test_seedConnectedPeer(
            bob.myPeerID,
            nickname: "Bob",
            capabilities: bothCapabilities,
            noisePublicKey: bob.noiseStaticPublicKeyData()
        )
        #expect(
            alice.authenticatedPrivateMediaReceiptSessionGeneration(
                to: bob.myPeerID
            ) == nil
        )

        let proofs = try await establishSessionCapturingPeerState(
            alice: alice,
            bob: bob
        )
        #expect(
            alice.authenticatedPrivateMediaReceiptSessionGeneration(
                to: bob.myPeerID
            ) == nil
        )

        // Bit 8 alone preserves encrypted transfer compatibility but cannot
        // authorize automatic resend.
        let privateMediaOnly = try authenticatedPeerStatePacket(
            from: bob,
            to: alice,
            capabilities: .privateMedia
        )
        alice._test_handlePacket(
            privateMediaOnly,
            fromPeerID: bob.myPeerID
        )
        #expect(await TestHelpers.waitUntil(
            {
                alice.privateMediaSendPolicy(to: bob.myPeerID)
                    == .encrypted
            },
            timeout: TestConstants.longTimeout
        ))
        #expect(
            alice.authenticatedPrivateMediaReceiptSessionGeneration(
                to: bob.myPeerID
            ) == nil
        )

        let receiptCapable = try authenticatedPeerStatePacket(
            from: bob,
            to: alice,
            capabilities: bothCapabilities
        )
        alice._test_handlePacket(
            receiptCapable,
            fromPeerID: bob.myPeerID
        )
        #expect(await TestHelpers.waitUntil(
            {
                alice.authenticatedPrivateMediaReceiptSessionGeneration(
                    to: bob.myPeerID
                ) != nil
            },
            timeout: TestConstants.longTimeout
        ))

        bob._test_handlePacket(
            proofs.alice,
            fromPeerID: alice.myPeerID
        )
        alice._test_onOutboundPacket = nil
        bob._test_onOutboundPacket = nil
    }

    @Test
    func receiptRetryRechecksBit9AtDeferredTransportBoundary() async throws {
        let root = FileManager.default.temporaryDirectory
            .appendingPathComponent(
                "private-media-retry-proof-race-\(UUID().uuidString)",
                isDirectory: true
            )
        defer { try? FileManager.default.removeItem(at: root) }
        let alice = makeService(
            baseDirectory: root.appendingPathComponent(
                "alice",
                isDirectory: true
            )
        )
        let bob = makeService(
            baseDirectory: root.appendingPathComponent(
                "bob",
                isDirectory: true
            )
        )
        let receiptCapabilities: PeerCapabilities = [
            .privateMedia,
            .privateMediaReceipts
        ]
        alice._test_seedConnectedPeer(
            bob.myPeerID,
            nickname: "Bob",
            capabilities: receiptCapabilities,
            noisePublicKey: bob.noiseStaticPublicKeyData()
        )
        bob._test_seedConnectedPeer(
            alice.myPeerID,
            nickname: "Alice",
            capabilities: receiptCapabilities,
            noisePublicKey: alice.noiseStaticPublicKeyData()
        )
        try await establishSession(alice: alice, bob: bob)
        #expect(
            alice.authenticatedPrivateMediaReceiptSessionGeneration(
                to: bob.myPeerID
            ) != nil
        )

        let privateMediaOnly = try authenticatedPeerStatePacket(
            from: bob,
            to: alice,
            capabilities: .privateMedia
        )
        let transferID =
            "receipt-proof-race-\(UUID().uuidString)"
        let tap = PacketTap()
        let boundaryProofs = ReceiptCapabilityRecorder()
        let rejections = TransferCancellationRecorder()
        let cancellable = TransferProgressManager.shared.publisher.sink {
            rejections.record($0)
        }
        alice._test_onOutboundPacket = tap.record
        alice._test_beforePrivateMediaDeferredSend = { id in
            guard id == transferID else { return }
            boundaryProofs.record(
                alice
                    .authenticatedPrivateMediaReceiptSessionGeneration(
                        to: bob.myPeerID
                    ) != nil
            )
        }
        defer {
            alice._test_beforePrivateMediaDeferredSend = nil
            alice._test_onOutboundPacket = nil
        }

        // Rotate authenticated state before the deferred retry reaches its
        // admission boundary.
        alice._test_handlePacket(
            privateMediaOnly,
            fromPeerID: bob.myPeerID
        )
        let content = Data("%PDF-1.7\nreceipt-proof-race".utf8)
        alice.sendFilePrivateReceiptRetry(
            BitchatFilePacket(
                fileName: "receipt-proof-race.pdf",
                fileSize: UInt64(content.count),
                mimeType: "application/pdf",
                content: content
            ),
            to: bob.myPeerID,
            transferId: transferID
        )
        #expect(await TestHelpers.waitUntil(
            { boundaryProofs.snapshot() == [false] },
            timeout: TestConstants.longTimeout
        ))
        await alice._test_drainPrivateMediaSendPipeline()

        #expect(await TestHelpers.waitUntil(
            { rejections.contains(transferID) },
            timeout: TestConstants.longTimeout
        ))
        #expect(tap.snapshot().allSatisfy {
            $0.type != MessageType.fileTransfer.rawValue
                && !(
                    $0.type == MessageType.noiseEncrypted.rawValue
                        && $0.version == 2
                )
        })
        let state = alice._test_privateMediaTransferState(
            transferId: transferID
        )
        #expect(!state.admissionActive)
        #expect(!state.pendingNoise)
        _ = cancellable
    }

    @Test
    func capabilityAnnounceCannotPoisonPinWithoutMatchingNoiseAuthentication() async throws {
        let root = FileManager.default.temporaryDirectory
            .appendingPathComponent("private-media-poisoning-\(UUID().uuidString)", isDirectory: true)
        defer { try? FileManager.default.removeItem(at: root) }
        let identity = MockIdentityManager(MockKeychain())
        let alice = makeService(
            baseDirectory: root.appendingPathComponent("alice", isDirectory: true),
            identityManager: identity
        )
        let bob = makeService(baseDirectory: root.appendingPathComponent("bob", isDirectory: true))
        let bobFingerprint = bob.noiseStaticPublicKeyData().sha256Fingerprint()
        let capableAnnounce = try signedAnnounce(
            from: bob,
            capabilities: .privateMedia
        )
        alice._test_handlePacket(
            capableAnnounce,
            fromPeerID: bob.myPeerID,
            preseedPeer: false
        )
        let advertised = await TestHelpers.waitUntil(
            { alice.privateMediaSendPolicy(to: bob.myPeerID) == .awaitingCapabilityProof },
            timeout: TestConstants.longTimeout
        )
        #expect(advertised)
        // The production signed-announce path ran, but with no authenticated
        // session it must remain a no-op. Querying policy is side-effect free.
        #expect(!identity.hasObservedPrivateMediaCapability(fingerprint: bobFingerprint))

        let noBitAnnounce = try signedAnnounce(
            from: bob,
            capabilities: []
        )
        alice._test_handlePacket(
            noBitAnnounce,
            fromPeerID: bob.myPeerID,
            preseedPeer: false
        )
        let remainedLegacyEligible = await TestHelpers.waitUntil(
            { alice.privateMediaSendPolicy(to: bob.myPeerID) == .legacyRequiresConsent },
            timeout: TestConstants.longTimeout
        )
        #expect(remainedLegacyEligible)
    }

    @Test
    func copiedNoiseKeyPreannounceCannotPinWhenRealOwnerAuthenticates() async throws {
        let root = FileManager.default.temporaryDirectory
            .appendingPathComponent("private-media-copied-static-\(UUID().uuidString)", isDirectory: true)
        defer { try? FileManager.default.removeItem(at: root) }
        let identity = MockIdentityManager(MockKeychain())
        let alice = makeService(
            baseDirectory: root.appendingPathComponent("alice", isDirectory: true),
            identityManager: identity
        )
        let bob = makeService(baseDirectory: root.appendingPathComponent("bob", isDirectory: true))
        let attacker = makeService(baseDirectory: root.appendingPathComponent("attacker", isDirectory: true))
        let bobKey = bob.noiseStaticPublicKeyData()
        let bobFingerprint = bobKey.sha256Fingerprint()

        // Mallory copies Bob's public Noise key, advertises bit 8, supplies
        // Mallory's Ed25519 key, and self-signs. This is internally consistent
        // but does not prove possession of Bob's Noise private key.
        let forged = try copiedStaticAnnounce(
            claimedOwner: bob,
            signedBy: attacker,
            capabilities: .privateMedia
        )
        alice._test_handlePacket(forged, fromPeerID: bob.myPeerID, preseedPeer: false)
        let hintAccepted = await TestHelpers.waitUntil(
            { alice.privateMediaSendPolicy(to: bob.myPeerID) == .awaitingCapabilityProof },
            timeout: TestConstants.longTimeout
        )
        #expect(hintAccepted)
        #expect(!identity.hasObservedPrivateMediaCapability(fingerprint: bobFingerprint))

        let proofs = try await establishSessionCapturingPeerState(alice: alice, bob: bob)
        #expect(!identity.hasObservedPrivateMediaCapability(fingerprint: bobFingerprint))
        #expect(alice.privateMediaSendPolicy(to: bob.myPeerID) == .awaitingCapabilityProof)

        // Only Bob's encrypted state authorizes bit 8 and replaces the forged
        // announcement signing key with Bob's Noise-authenticated Ed key.
        alice._test_handlePacket(proofs.bob, fromPeerID: bob.myPeerID)
        let pinned = await TestHelpers.waitUntil(
            { identity.hasObservedPrivateMediaCapability(fingerprint: bobFingerprint) },
            timeout: TestConstants.longTimeout
        )
        #expect(pinned)
        #expect(identity.authenticatedSigningPublicKey(forFingerprint: bobFingerprint)
            == bob.noiseSigningPublicKeyData())
        #expect(alice.privateMediaSendPolicy(to: bob.myPeerID) == .encrypted)

        bob._test_handlePacket(proofs.alice, fromPeerID: alice.myPeerID)
        alice._test_onOutboundPacket = nil
        bob._test_onOutboundPacket = nil
    }

    @Test
    func droppedInitiatorProofConvergesViaSingleAuthenticatedEcho() async throws {
        let root = FileManager.default.temporaryDirectory
            .appendingPathComponent("private-media-proof-echo-\(UUID().uuidString)", isDirectory: true)
        defer { try? FileManager.default.removeItem(at: root) }
        let alice = makeService(baseDirectory: root.appendingPathComponent("alice", isDirectory: true))
        let bob = makeService(baseDirectory: root.appendingPathComponent("bob", isDirectory: true))
        alice._test_seedConnectedPeer(
            bob.myPeerID,
            nickname: "Bob",
            capabilities: .privateMedia,
            noisePublicKey: bob.noiseStaticPublicKeyData()
        )
        bob._test_seedConnectedPeer(
            alice.myPeerID,
            nickname: "Alice",
            capabilities: .privateMedia,
            noisePublicKey: alice.noiseStaticPublicKeyData()
        )

        let initial = try await establishSessionCapturingPeerState(alice: alice, bob: bob)
        // Model Alice's first proof racing ahead of Bob's message-3 handling
        // and being dropped. Bob's proof reaches Alice; Alice must emit one
        // idempotent echo that lets Bob converge without a new handshake.
        _ = initial.alice
        let echoTap = PacketTap()
        alice._test_onOutboundPacket = echoTap.record
        alice._test_handlePacket(initial.bob, fromPeerID: bob.myPeerID)
        let echoed = await TestHelpers.waitUntil(
            { echoTap.snapshot().contains { $0.type == MessageType.noiseEncrypted.rawValue } },
            timeout: TestConstants.longTimeout
        )
        #expect(echoed)
        let echo = try #require(
            echoTap.snapshot().first { $0.type == MessageType.noiseEncrypted.rawValue }
        )
        bob._test_handlePacket(echo, fromPeerID: alice.myPeerID)
        let converged = await TestHelpers.waitUntil(
            {
                alice.privateMediaSendPolicy(to: bob.myPeerID) == .encrypted
                    && bob.privateMediaSendPolicy(to: alice.myPeerID) == .encrypted
            },
            timeout: TestConstants.longTimeout
        )
        #expect(converged)
        alice._test_onOutboundPacket = nil
        bob._test_onOutboundPacket = nil
    }

    @Test
    func noProofTimeoutResolvesToConsentWithoutSendingRawMedia() async throws {
        let root = FileManager.default.temporaryDirectory
            .appendingPathComponent("private-media-proof-timeout-\(UUID().uuidString)", isDirectory: true)
        defer { try? FileManager.default.removeItem(at: root) }
        let alice = makeService(baseDirectory: root.appendingPathComponent("alice", isDirectory: true))
        let bob = makeService(baseDirectory: root.appendingPathComponent("bob", isDirectory: true))
        alice._test_seedConnectedPeer(
            bob.myPeerID,
            nickname: "Prerelease Bob",
            capabilities: .privateMedia,
            noisePublicKey: bob.noiseStaticPublicKeyData()
        )

        _ = try await establishSessionCapturingPeerState(alice: alice, bob: bob)
        #expect(alice.privateMediaSendPolicy(to: bob.myPeerID) == .awaitingCapabilityProof)
        let recorder = PrivateMediaPolicyRecorder()
        alice.resolvePrivateMediaSendPolicy(to: bob.myPeerID) { recorder.record($0) }
        let registered = await TestHelpers.waitUntil(
            { alice._test_hasPendingPrivateMediaPolicyResolution(for: bob.myPeerID) },
            timeout: TestConstants.longTimeout
        )
        #expect(registered)
        alice._test_forcePrivateMediaProofTimeout(for: bob.myPeerID)
        let resolved = await TestHelpers.waitUntil(
            { recorder.snapshot() == .legacyRequiresConsent },
            timeout: TestConstants.longTimeout
        )
        #expect(resolved)
        #expect(alice.privateMediaSendPolicy(to: bob.myPeerID) == .legacyRequiresConsent)
        #expect(recorder.snapshot() != .encrypted)
        alice._test_onOutboundPacket = nil
        bob._test_onOutboundPacket = nil
    }

    @Test
    func panicDropsPendingPrivateMediaPolicyCompletion() async throws {
        let root = FileManager.default.temporaryDirectory
            .appendingPathComponent(
                "private-media-policy-panic-\(UUID().uuidString)",
                isDirectory: true
            )
        defer { try? FileManager.default.removeItem(at: root) }
        let alice = makeService(
            baseDirectory: root.appendingPathComponent(
                "alice",
                isDirectory: true
            )
        )
        let bob = makeService(
            baseDirectory: root.appendingPathComponent(
                "bob",
                isDirectory: true
            )
        )
        alice._test_seedConnectedPeer(
            bob.myPeerID,
            nickname: "Prerelease Bob",
            capabilities: .privateMedia,
            noisePublicKey: bob.noiseStaticPublicKeyData()
        )

        _ = try await establishSessionCapturingPeerState(
            alice: alice,
            bob: bob
        )
        #expect(
            alice.privateMediaSendPolicy(to: bob.myPeerID)
                == .awaitingCapabilityProof
        )
        let recorder = PrivateMediaPolicyRecorder()
        alice.resolvePrivateMediaSendPolicy(to: bob.myPeerID) {
            recorder.record($0)
        }
        let registered = await TestHelpers.waitUntil(
            {
                alice._test_hasPendingPrivateMediaPolicyResolution(
                    for: bob.myPeerID
                )
            },
            timeout: TestConstants.longTimeout
        )
        #expect(registered)

        alice.suspendForPanicReset()
        alice.resetIdentityForPanic(
            currentNickname: "anon",
            restartServices: false
        )
        alice._test_forcePrivateMediaProofTimeout(for: bob.myPeerID)
        await Task.yield()

        #expect(recorder.snapshot() == nil)
        #expect(
            !alice._test_hasPendingPrivateMediaPolicyResolution(
                for: bob.myPeerID
            )
        )
    }

    @Test
    func queuedPrivatePayloadWaitsForProofNotHandshakeCompletion() async throws {
        let root = FileManager.default.temporaryDirectory
            .appendingPathComponent("private-media-proof-drain-\(UUID().uuidString)", isDirectory: true)
        defer { try? FileManager.default.removeItem(at: root) }
        let alice = makeService(baseDirectory: root.appendingPathComponent("alice", isDirectory: true))
        let bob = makeService(baseDirectory: root.appendingPathComponent("bob", isDirectory: true))
        alice._test_seedConnectedPeer(
            bob.myPeerID,
            nickname: "Bob",
            capabilities: .privateMedia,
            noisePublicKey: bob.noiseStaticPublicKeyData()
        )
        let proofs = try await establishSessionCapturingPeerState(alice: alice, bob: bob)

        let content = Data("proof-gated-private-file".utf8)
        let file = BitchatFilePacket(
            fileName: "proof.txt",
            fileSize: UInt64(content.count),
            mimeType: "text/plain",
            content: content
        )
        let payload = try #require(BLENoisePayloadFactory.privateFile(file))
        let transferID = "proof-gated-\(UUID().uuidString)"
        alice._test_enqueuePendingNoisePayload(payload, transferId: transferID, for: bob.myPeerID)
        alice._test_sendPendingNoisePayloadsAfterHandshake(for: bob.myPeerID)

        #expect(alice._test_privateMediaTransferState(transferId: transferID).pendingNoise)
        alice._test_handlePacket(proofs.bob, fromPeerID: bob.myPeerID)
        let drained = await TestHelpers.waitUntil(
            { !alice._test_privateMediaTransferState(transferId: transferID).pendingNoise },
            timeout: TestConstants.longTimeout
        )
        #expect(drained)
        bob._test_handlePacket(proofs.alice, fromPeerID: alice.myPeerID)
        alice._test_onOutboundPacket = nil
        bob._test_onOutboundPacket = nil
    }

    @Test
    func authenticatedFingerprintMismatchCannotPoisonCapabilityPin() async throws {
        let root = FileManager.default.temporaryDirectory
            .appendingPathComponent("private-media-key-mismatch-\(UUID().uuidString)", isDirectory: true)
        defer { try? FileManager.default.removeItem(at: root) }
        let identity = MockIdentityManager(MockKeychain())
        let alice = makeService(
            baseDirectory: root.appendingPathComponent("alice", isDirectory: true),
            identityManager: identity
        )
        let bob = makeService(baseDirectory: root.appendingPathComponent("bob", isDirectory: true))
        let impostor = makeService(baseDirectory: root.appendingPathComponent("impostor", isDirectory: true))
        let impostorKey = impostor.noiseStaticPublicKeyData()
        let reconciliations = PeerIDRecorder()
        alice._test_onPrivateMediaSessionReconciled = reconciliations.record

        alice._test_seedConnectedPeer(
            bob.myPeerID,
            nickname: "Bob",
            capabilities: .privateMedia,
            noisePublicKey: impostorKey
        )
        try await establishSession(alice: alice, bob: bob)

        let sessionReconciled = await TestHelpers.waitUntil(
            { reconciliations.contains(bob.myPeerID) },
            timeout: TestConstants.longTimeout
        )
        #expect(sessionReconciled)
        #expect(!identity.hasObservedPrivateMediaCapability(
            fingerprint: impostorKey.sha256Fingerprint()
        ))
        #expect(identity.hasObservedPrivateMediaCapability(
            fingerprint: bob.noiseStaticPublicKeyData().sha256Fingerprint()
        ))
        alice._test_seedConnectedPeer(
            bob.myPeerID,
            nickname: "Bob",
            capabilities: [],
            noisePublicKey: impostorKey
        )
        // The exact live Noise identity remains authoritative over a later
        // impostor registry rewrite.
        #expect(alice.privateMediaSendPolicy(to: bob.myPeerID) == .encrypted)
    }

    @Test
    func capabilityAnnounceAfterNoiseSessionStillRequiresEncryptedProof() async throws {
        let root = FileManager.default.temporaryDirectory
            .appendingPathComponent("private-media-race-\(UUID().uuidString)", isDirectory: true)
        defer { try? FileManager.default.removeItem(at: root) }
        let identity = MockIdentityManager(MockKeychain())
        let alice = makeService(
            baseDirectory: root.appendingPathComponent("alice", isDirectory: true),
            identityManager: identity
        )
        let bob = makeService(baseDirectory: root.appendingPathComponent("bob", isDirectory: true))

        let proofs = try await establishSessionCapturingPeerState(alice: alice, bob: bob)
        let bobKey = bob.noiseStaticPublicKeyData()
        let capableAnnounce = try signedAnnounce(
            from: bob,
            capabilities: .privateMedia
        )
        alice._test_handlePacket(
            capableAnnounce,
            fromPeerID: bob.myPeerID,
            preseedPeer: false
        )

        let announceDidNotPin = await TestHelpers.waitUntil(
            { alice.privateMediaSendPolicy(to: bob.myPeerID) == .awaitingCapabilityProof },
            timeout: TestConstants.longTimeout
        )
        #expect(announceDidNotPin)
        #expect(!identity.hasObservedPrivateMediaCapability(
            fingerprint: bobKey.sha256Fingerprint()
        ))

        alice._test_handlePacket(proofs.bob, fromPeerID: bob.myPeerID)

        let pinned = await TestHelpers.waitUntil(
            {
                identity.hasObservedPrivateMediaCapability(
                    fingerprint: bobKey.sha256Fingerprint()
                )
            },
            timeout: TestConstants.longTimeout
        )
        #expect(pinned)
        bob._test_handlePacket(proofs.alice, fromPeerID: alice.myPeerID)
        alice._test_onOutboundPacket = nil
        bob._test_onOutboundPacket = nil
    }

    @Test
    func consentedLegacySendRejectsAboveAndroidFragmentCapButEncryptedDoesNot() async throws {
        let root = FileManager.default.temporaryDirectory
            .appendingPathComponent("private-media-fragment-cap-\(UUID().uuidString)", isDirectory: true)
        defer { try? FileManager.default.removeItem(at: root) }
        let alice = makeService(baseDirectory: root.appendingPathComponent("alice", isDirectory: true))
        let bob = makeService(baseDirectory: root.appendingPathComponent("bob", isDirectory: true))
        let oldCarol = makeService(baseDirectory: root.appendingPathComponent("carol", isDirectory: true))

        alice._test_seedConnectedPeer(
            bob.myPeerID,
            nickname: "Bob",
            capabilities: .privateMedia,
            noisePublicKey: bob.noiseStaticPublicKeyData()
        )
        alice._test_seedConnectedPeer(
            oldCarol.myPeerID,
            nickname: "Old Carol",
            noisePublicKey: oldCarol.noiseStaticPublicKeyData()
        )
        try await establishSession(alice: alice, bob: bob)

        var state: UInt64 = 0x1234_5678_9ABC_DEF0
        let body = Data((0..<(130 * 1024)).map { _ in
            state = state &* 6364136223846793005 &+ 1442695040888963407
            return UInt8(truncatingIfNeeded: state >> 32)
        })
        let content = Data("%PDF-1.7\n".utf8) + body
        let file = BitchatFilePacket(
            fileName: "too-many-fragments.pdf",
            fileSize: UInt64(content.count),
            mimeType: "application/pdf",
            content: content
        )

        let tap = PacketTap()
        alice._test_onOutboundPacket = tap.record
        let rejections = TransferCancellationRecorder()
        let cancellable = TransferProgressManager.shared.publisher.sink { rejections.record($0) }
        let encryptedID = "encrypted-over-256-\(UUID().uuidString)"
        let legacyID = "legacy-over-256-\(UUID().uuidString)"

        alice.sendFilePrivate(file, to: bob.myPeerID, transferId: encryptedID, allowLegacyFallback: false)
        alice.sendFilePrivate(
            file,
            to: oldCarol.myPeerID,
            transferId: legacyID,
            allowLegacyFallback: true
        )

        // The directed raw-file migration fallback (Android-style peer without
        // the .privateMedia capability) still honors the 256-fragment ceiling.
        let legacyRejected = await TestHelpers.waitUntil(
            { rejections.contains(legacyID) },
            timeout: TestConstants.longTimeout
        )
        #expect(legacyRejected)
        #expect(rejections.reason(for: legacyID)?.contains("256") == true)

        // Encrypted private media to a .privateMedia-capable peer is NOT forced
        // down to Android's 256 cap: it uses the full receiver ceiling and
        // proceeds to fragment/emit (a 130 KiB file exceeds 256 fragments).
        let encryptedEmitted = await TestHelpers.waitUntil(
            { !tap.snapshot().isEmpty },
            timeout: TestConstants.longTimeout
        )
        #expect(encryptedEmitted, "Encrypted send to a capable peer must not be blocked by the Android cap")
        #expect(!rejections.contains(encryptedID))
        _ = cancellable
    }

    @Test
    func queuedPrivateEncryptionFailureRejectsBoundTransfer() async throws {
        let root = FileManager.default.temporaryDirectory
            .appendingPathComponent("private-media-queued-failure-\(UUID().uuidString)", isDirectory: true)
        defer { try? FileManager.default.removeItem(at: root) }
        let alice = makeService(baseDirectory: root.appendingPathComponent("alice", isDirectory: true))
        let bob = makeService(baseDirectory: root.appendingPathComponent("bob", isDirectory: true))
        try await establishSession(alice: alice, bob: bob)

        let transferID = "queued-encryption-failure-\(UUID().uuidString)"
        let rejections = TransferCancellationRecorder()
        let cancellable = TransferProgressManager.shared.publisher.sink { rejections.record($0) }
        var oversizedTypedPayload = Data([NoisePayloadType.privateFile.rawValue])
        oversizedTypedPayload.append(Data(
            repeating: 0x42,
            count: NoiseSecurityConstants.maxPrivateFilePlaintextSize
        ))

        alice._test_enqueuePendingNoisePayload(
            oversizedTypedPayload,
            transferId: transferID,
            for: bob.myPeerID
        )
        alice._test_sendPendingNoisePayloadsAfterHandshake(for: bob.myPeerID)

        let rejected = await TestHelpers.waitUntil(
            { rejections.contains(transferID) },
            timeout: TestConstants.longTimeout
        )
        #expect(rejected)
        #expect(rejections.reason(for: transferID)?.isEmpty == false)
        _ = cancellable
    }

    @Test
    func canonical0x20EncryptedFileIsAcceptedAcrossV1OuterPacket() async throws {
        let content = Data("%PDF-1.7\nandroid-private".utf8)
        try await assertInboundEncryptedPrivateMedia(
            typeByte: 0x20,
            content: content,
            outerVersion: 1,
            directoryLabel: "android-0x20"
        )
    }

    @Test
    func prerelease0x09LargeEncryptedFileIsAcceptedDuringMigration() async throws {
        let content = Data("%PDF-1.7\nprerelease-private".utf8)
            + Data(repeating: 0x39, count: 70 * 1024)
        #expect(content.count > NoiseSecurityConstants.maxMessageSize)
        try await assertInboundEncryptedPrivateMedia(
            typeByte: NoisePayloadType.prereleasePrivateFileRawValue,
            content: content,
            outerVersion: 2,
            directoryLabel: "prerelease-0x09"
        )
    }

    @Test
    func privateJPEGIsOpaqueBeforeFragmentationAndDelivers() async throws {
        let marker = Data("JPEG_PRIVATE_MARKER_7f5e5eacb86f4b9a".utf8)
        let content = Data([0xFF, 0xD8, 0xFF, 0xE0])
            + marker
            + Data(repeating: 0x4A, count: 6 * 1024)
        try await assertPrivateMediaRoundTrip(
            fileName: "img_20260725_120000_11111111-1111-1111-1111-111111111111.jpg",
            mimeType: "image/jpeg",
            content: content,
            marker: marker,
            expectedMessagePrefix: "[image]"
        )
    }

    @Test
    func finalizedPrivateM4AIsOpaqueBeforeFragmentationAndDelivers() async throws {
        let marker = Data("M4A_PRIVATE_MARKER_e0cd431b61fb4a6c".utf8)
        let content = Data([0x00, 0x00, 0x00, 0x18])
            + Data("ftypM4A ".utf8)
            + marker
            + Data(repeating: 0x4D, count: 6 * 1024)
        try await assertPrivateMediaRoundTrip(
            fileName: "voice_0011223344556677.m4a",
            mimeType: "audio/mp4",
            content: content,
            marker: marker,
            expectedMessagePrefix: "[voice]"
        )
    }

    @Test
    func capablePeerUsesCanonicalAndroid0x20EncryptedSend() async throws {
        let marker = Data("PDF_PRIVATE_MARKER_b333f84b8fc7478d".utf8)
        let content = Data("%PDF-1.7\n".utf8)
            + marker
            + Data(repeating: 0x50, count: 6 * 1024)
        let file = BitchatFilePacket(
            fileName: "private.pdf",
            fileSize: UInt64(content.count),
            mimeType: "application/pdf",
            content: content
        )
        #expect(BLENoisePayloadFactory.privateFile(file)?.first == 0x20)
        try await assertPrivateMediaRoundTrip(
            fileName: "private.pdf",
            mimeType: "application/pdf",
            content: content,
            marker: marker,
            expectedMessagePrefix: "[file]"
        )
    }

    @Test
    func privateMediaAboveOrdinaryNoiseLimitUsesV2OuterPacketAndDelivers() async throws {
        let marker = Data("LARGE_PRIVATE_MARKER_1ec63f261a7041ee".utf8)
        let content = Data("%PDF-1.7\n".utf8)
            + marker
            + Data(repeating: 0x4C, count: 70 * 1024)
        try await assertPrivateMediaRoundTrip(
            fileName: "large-private.pdf",
            mimeType: "application/pdf",
            content: content,
            marker: marker,
            expectedMessagePrefix: "[file]",
            expectedOuterVersion: 2
        )
    }

    /// Models an already-established remote sender independently of the local
    /// send policy. Exact Android b7f0b33d plaintext bytes are frozen in
    /// `BLENoisePayloadFactoryTests`; this helper exercises the encrypted
    /// inbound transport around that shared wire encoding.
    private func assertInboundEncryptedPrivateMedia(
        typeByte: UInt8,
        content: Data,
        outerVersion: UInt8,
        directoryLabel: String
    ) async throws {
        let root = FileManager.default.temporaryDirectory
            .appendingPathComponent("private-media-\(directoryLabel)-\(UUID().uuidString)", isDirectory: true)
        let aliceRoot = root.appendingPathComponent("alice", isDirectory: true)
        let bobRoot = root.appendingPathComponent("bob", isDirectory: true)
        defer { try? FileManager.default.removeItem(at: root) }

        let alice = makeService(baseDirectory: aliceRoot)
        let bob = makeService(baseDirectory: bobRoot)
        let delegate = MessageCaptureDelegate()
        bob.delegate = delegate
        try await establishSession(alice: alice, bob: bob)

        let file = BitchatFilePacket(
            fileName: "\(directoryLabel).pdf",
            fileSize: UInt64(content.count),
            mimeType: "application/pdf",
            content: content
        )
        let encodedFile = try #require(file.encode())
        var typedPayload = Data([typeByte])
        typedPayload.append(encodedFile)

        let encrypted = try alice._test_makeEncryptedNoisePacket(typedPayload, to: bob.myPeerID)
        let remoteShapedPacket = BitchatPacket(
            type: encrypted.type,
            senderID: encrypted.senderID,
            recipientID: encrypted.recipientID,
            timestamp: encrypted.timestamp,
            payload: encrypted.payload,
            signature: nil,
            ttl: encrypted.ttl,
            version: outerVersion
        )
        bob._test_handlePacket(remoteShapedPacket, fromPeerID: alice.myPeerID)

        let delivered = await TestHelpers.waitUntil(
            { delegate.snapshot().count == 1 },
            timeout: TestConstants.longTimeout
        )
        #expect(delivered)
        #expect(delegate.snapshot().first?.isPrivate == true)
        let stored = recursivelyStoredFiles(under: bobRoot)
        #expect(stored.count == 1)
        if let storedURL = stored.first {
            #expect(try Data(contentsOf: storedURL) == content)
        }
    }

    private func assertApprovedLegacySendCancelledBeforeAdmission(label: String) async throws {
        let root = FileManager.default.temporaryDirectory
            .appendingPathComponent("private-media-admission-\(label)-\(UUID().uuidString)", isDirectory: true)
        defer { try? FileManager.default.removeItem(at: root) }
        let alice = makeService(baseDirectory: root.appendingPathComponent("alice", isDirectory: true))
        let bob = makeService(baseDirectory: root.appendingPathComponent("bob", isDirectory: true))
        alice._test_seedConnectedPeer(
            bob.myPeerID,
            nickname: "Legacy Bob",
            noisePublicKey: bob.noiseStaticPublicKeyData()
        )

        let transferId = "approved-\(label)-\(UUID().uuidString)"
        let gate = PrivateMediaDeferredSendGate()
        let tap = PacketTap()
        alice._test_onOutboundPacket = tap.record
        alice._test_beforePrivateMediaDeferredSend = { id in
            guard id == transferId else { return }
            gate.pause()
        }
        defer {
            gate.release()
            alice._test_beforePrivateMediaDeferredSend = nil
        }

        let content = Data("%PDF-1.7\ncancelled-before-admission".utf8)
        alice.sendFilePrivate(
            BitchatFilePacket(
                fileName: "cancelled.pdf",
                fileSize: UInt64(content.count),
                mimeType: "application/pdf",
                content: content
            ),
            to: bob.myPeerID,
            transferId: transferId,
            allowLegacyFallback: true
        )

        let paused = await TestHelpers.waitUntil(
            { gate.hasPaused },
            timeout: TestConstants.longTimeout
        )
        #expect(paused)

        // This is the transport action used by both cancel and delete. It must
        // invalidate synchronously while messageQueue is still held above.
        alice.cancelTransfer(transferId)
        gate.release()
        await alice._test_drainPrivateMediaSendPipeline()

        let state = alice._test_privateMediaTransferState(transferId: transferId)
        #expect(!state.admissionActive)
        #expect(!state.pendingNoise)
        #expect(state.activeScheduler == 0)
        #expect(state.pendingScheduler == 0)
        #expect(await TestHelpers.waitUntil(
            { alice._test_privateMediaAdmissionEntryCount() == 0 },
            timeout: TestConstants.longTimeout
        ))
        #expect(tap.snapshot().allSatisfy {
            $0.type != MessageType.fileTransfer.rawValue
                && $0.type != MessageType.noiseEncrypted.rawValue
        })
    }

    private func assertPrivateMediaRoundTrip(
        fileName: String,
        mimeType: String,
        content: Data,
        marker: Data,
        expectedMessagePrefix: String,
        expectedOuterVersion: UInt8 = 2
    ) async throws {
        let root = FileManager.default.temporaryDirectory
            .appendingPathComponent("private-media-e2e-\(UUID().uuidString)", isDirectory: true)
        let aliceRoot = root.appendingPathComponent("alice", isDirectory: true)
        let bobRoot = root.appendingPathComponent("bob", isDirectory: true)
        defer { try? FileManager.default.removeItem(at: root) }

        let alice = makeService(baseDirectory: aliceRoot)
        let bob = makeService(baseDirectory: bobRoot)
        let tap = PacketTap()
        let delegate = MessageCaptureDelegate()
        bob.delegate = delegate

        alice._test_seedConnectedPeer(
            bob.myPeerID,
            nickname: "Bob",
            capabilities: .privateMedia,
            noisePublicKey: bob.noiseStaticPublicKeyData()
        )
        bob._test_seedConnectedPeer(
            alice.myPeerID,
            nickname: "Alice",
            capabilities: .privateMedia,
            noisePublicKey: alice.noiseStaticPublicKeyData()
        )
        try await establishSession(alice: alice, bob: bob)
        alice._test_onOutboundPacket = tap.record

        let file = BitchatFilePacket(
            fileName: fileName,
            fileSize: UInt64(content.count),
            mimeType: mimeType,
            content: content
        )
        alice.sendFilePrivate(file, to: bob.myPeerID, transferId: "wire-\(UUID().uuidString)", allowLegacyFallback: false)

        let fragmented = await TestHelpers.waitUntil(
            { tap.hasCompleteFragmentTrain },
            timeout: 10
        )
        #expect(fragmented)

        let outbound = tap.snapshot()
        let encryptedPackets = outbound.filter { $0.type == MessageType.noiseEncrypted.rawValue }
        let fragments = outbound
            .filter { $0.type == MessageType.fragment.rawValue }
            .sorted { fragmentIndex($0) < fragmentIndex($1) }

        #expect(encryptedPackets.count == 1)
        #expect(encryptedPackets.first?.version == expectedOuterVersion)
        #expect(!fragments.isEmpty)
        #expect(outbound.allSatisfy { $0.type != MessageType.fileTransfer.rawValue })
        for packet in encryptedPackets + fragments {
            #expect(packet.payload.range(of: marker) == nil)
            #expect(packet.payload.range(of: content) == nil)
        }

        // Real BLE delivers the train at the scheduler's paced interval. Feed
        // bounded batches here instead of enqueuing hundreds of synthetic
        // callbacks at once, which can exhaust libdispatch worker threads as
        // they wait on the fragment-assembly barrier.
        for batchStart in stride(from: 0, to: fragments.count, by: 16) {
            let batchEnd = min(batchStart + 16, fragments.count)
            for fragment in fragments[batchStart..<batchEnd] {
                bob._test_handlePacket(fragment, fromPeerID: alice.myPeerID)
            }
            await bob._test_drainFragmentPipeline()
        }

        let delivered = await TestHelpers.waitUntil(
            { delegate.snapshot().count == 1 },
            timeout: TestConstants.longTimeout
        )
        #expect(delivered)

        let message = try #require(delegate.snapshot().first)
        #expect(message.isPrivate)
        #expect(message.senderPeerID == alice.myPeerID)
        #expect(message.content.hasPrefix(expectedMessagePrefix))
        if let stableMessageID = PrivateMediaMessageIdentity.stableID(
            for: file,
            senderPeerID: alice.myPeerID,
            recipientPeerID: bob.myPeerID
        ) {
            #expect(message.id == stableMessageID)
        } else {
            // Generic/legacy filenames retain random per-arrival IDs so two
            // unrelated "photo.jpg" transfers are never deduplicated.
            #expect(!message.id.hasPrefix("media-"))
        }

        let stored = recursivelyStoredFiles(under: bobRoot)
        #expect(stored.count == 1)
        let storedURL = try #require(stored.first)
        #expect(try Data(contentsOf: storedURL) == content)
    }

    private func makeService(
        baseDirectory: URL,
        identityManager: SecureIdentityStateManagerProtocol? = nil
    ) -> BLEService {
        let keychain = MockKeychain()
        return BLEService(
            keychain: keychain,
            idBridge: NostrIdentityBridge(keychain: MockKeychainHelper()),
            identityManager: identityManager ?? MockIdentityManager(keychain),
            initializeBluetoothManagers: false,
            incomingFileStore: BLEIncomingFileStore(baseDirectory: baseDirectory)
        )
    }

    private func establishSession(alice: BLEService, bob: BLEService) async throws {
        let proofs = try await establishSessionCapturingPeerState(alice: alice, bob: bob)
        bob._test_handlePacket(proofs.alice, fromPeerID: alice.myPeerID)
        alice._test_handlePacket(proofs.bob, fromPeerID: bob.myPeerID)
        // Fence the message/identity mutations without assuming either test
        // seeded a registry entry (inbound-only tests intentionally do not).
        await alice._test_drainNoiseMessagePipeline()
        await bob._test_drainNoiseMessagePipeline()
        alice._test_onOutboundPacket = nil
        bob._test_onOutboundPacket = nil
    }

    private func establishSessionCapturingPeerState(
        alice: BLEService,
        bob: BLEService
    ) async throws -> (alice: BitchatPacket, bob: BitchatPacket) {
        let aliceTap = PacketTap()
        let bobTap = PacketTap()
        alice._test_onOutboundPacket = aliceTap.record
        bob._test_onOutboundPacket = bobTap.record

        let first = try alice._test_noiseInitiateHandshake(with: bob.myPeerID)
        let second = try #require(
            try bob._test_noiseProcessHandshakeMessage(from: alice.myPeerID, message: first)
        )
        let third = try #require(
            try alice._test_noiseProcessHandshakeMessage(from: bob.myPeerID, message: second)
        )
        _ = try bob._test_noiseProcessHandshakeMessage(from: alice.myPeerID, message: third)
        #expect(alice.canDeliverSecurely(to: bob.myPeerID))
        #expect(bob.canDeliverSecurely(to: alice.myPeerID))

        let emitted = await TestHelpers.waitUntil(
            {
                aliceTap.snapshot().contains { $0.type == MessageType.noiseEncrypted.rawValue }
                    && bobTap.snapshot().contains { $0.type == MessageType.noiseEncrypted.rawValue }
            },
            timeout: TestConstants.longTimeout
        )
        #expect(emitted)
        let aliceProof = try #require(
            aliceTap.snapshot().first { $0.type == MessageType.noiseEncrypted.rawValue }
        )
        let bobProof = try #require(
            bobTap.snapshot().first { $0.type == MessageType.noiseEncrypted.rawValue }
        )
        return (aliceProof, bobProof)
    }

    private func authenticatedPeerStatePacket(
        from sender: BLEService,
        to recipient: BLEService,
        capabilities: PeerCapabilities
    ) throws -> BitchatPacket {
        let state = AuthenticatedPeerStatePacket(
            capabilities: capabilities,
            signingPublicKey: sender.noiseSigningPublicKeyData()
        )
        let typed = try #require(BLENoisePayloadFactory.authenticatedPeerState(state))
        return try sender._test_makeEncryptedNoisePacket(typed, to: recipient.myPeerID)
    }

    private func signedAnnounce(
        from service: BLEService,
        capabilities: PeerCapabilities?
    ) throws -> BitchatPacket {
        let announcement = AnnouncementPacket(
            nickname: "Bob",
            noisePublicKey: service.noiseStaticPublicKeyData(),
            signingPublicKey: service.noiseSigningPublicKeyData(),
            directNeighbors: nil,
            capabilities: capabilities
        )
        let payload = try #require(announcement.encode())
        let unsigned = BitchatPacket(
            type: MessageType.announce.rawValue,
            senderID: Data(hexString: service.myPeerID.id) ?? Data(),
            recipientID: nil,
            timestamp: UInt64(Date().timeIntervalSince1970 * 1_000),
            payload: payload,
            signature: nil,
            ttl: TransportConfig.messageTTLDefault
        )
        return service.signPacketForBroadcast(unsigned)
    }

    private func copiedStaticAnnounce(
        claimedOwner: BLEService,
        signedBy signer: BLEService,
        capabilities: PeerCapabilities
    ) throws -> BitchatPacket {
        let announcement = AnnouncementPacket(
            nickname: "Mallory-as-Bob",
            noisePublicKey: claimedOwner.noiseStaticPublicKeyData(),
            signingPublicKey: signer.noiseSigningPublicKeyData(),
            directNeighbors: nil,
            capabilities: capabilities
        )
        let payload = try #require(announcement.encode())
        let unsigned = BitchatPacket(
            type: MessageType.announce.rawValue,
            senderID: Data(hexString: claimedOwner.myPeerID.id) ?? Data(),
            recipientID: nil,
            timestamp: UInt64(Date().timeIntervalSince1970 * 1_000),
            payload: payload,
            signature: nil,
            // Relayed shape avoids the proactive direct-hint handshake in
            // this deterministic test; it does not change signature validity.
            ttl: TransportConfig.messageTTLDefault - 1
        )
        return signer.signPacketForBroadcast(unsigned)
    }

    private func recursivelyStoredFiles(under root: URL) -> [URL] {
        guard let enumerator = FileManager.default.enumerator(
            at: root,
            includingPropertiesForKeys: [.isRegularFileKey]
        ) else { return [] }

        return enumerator.compactMap { item in
            guard let url = item as? URL,
                  !url.pathComponents.contains(".private-media-receipts"),
                  url.lastPathComponent != ".private-media-receipts.json",
                  (try? url.resourceValues(forKeys: [.isRegularFileKey]).isRegularFile) == true else {
                return nil
            }
            return url
        }
    }
}

private func fragmentIndex(_ packet: BitchatPacket) -> Int {
    guard packet.payload.count >= 10 else { return .max }
    return (Int(packet.payload[8]) << 8) | Int(packet.payload[9])
}

private final class PacketTap: @unchecked Sendable {
    private let lock = NSLock()
    private var packets: [BitchatPacket] = []

    func record(_ packet: BitchatPacket) {
        lock.lock()
        packets.append(packet)
        lock.unlock()
    }

    func snapshot() -> [BitchatPacket] {
        lock.lock()
        defer { lock.unlock() }
        return packets
    }

    var hasCompleteFragmentTrain: Bool {
        let fragments = snapshot().filter { $0.type == MessageType.fragment.rawValue }
        guard let first = fragments.first, first.payload.count >= 12 else { return false }
        let total = (Int(first.payload[10]) << 8) | Int(first.payload[11])
        return total > 0 && fragments.count >= total
    }
}

private final class PrivateMediaDeferredSendGate: @unchecked Sendable {
    private let condition = NSCondition()
    private var paused = false
    private var released = false

    var hasPaused: Bool {
        condition.lock()
        defer { condition.unlock() }
        return paused
    }

    func pause() {
        condition.lock()
        paused = true
        condition.broadcast()
        while !released {
            condition.wait()
        }
        condition.unlock()
    }

    func release() {
        condition.lock()
        released = true
        condition.broadcast()
        condition.unlock()
    }
}

private final class PeerIDRecorder: @unchecked Sendable {
    private let lock = NSLock()
    private var peerIDs: [PeerID] = []

    func record(_ peerID: PeerID) {
        lock.lock()
        peerIDs.append(peerID)
        lock.unlock()
    }

    func contains(_ peerID: PeerID) -> Bool {
        lock.lock()
        defer { lock.unlock() }
        return peerIDs.contains(peerID)
    }
}

private final class PrivateMediaPolicyRecorder: @unchecked Sendable {
    private let lock = NSLock()
    private var policy: PrivateMediaSendPolicy?

    func record(_ policy: PrivateMediaSendPolicy) {
        lock.lock()
        self.policy = policy
        lock.unlock()
    }

    func snapshot() -> PrivateMediaSendPolicy? {
        lock.lock()
        defer { lock.unlock() }
        return policy
    }
}

private final class ReceiptCapabilityRecorder: @unchecked Sendable {
    private let lock = NSLock()
    private var values: [Bool] = []

    func record(_ value: Bool) {
        lock.lock()
        values.append(value)
        lock.unlock()
    }

    func snapshot() -> [Bool] {
        lock.lock()
        defer { lock.unlock() }
        return values
    }
}

private final class MessageCaptureDelegate: BitchatDelegate, @unchecked Sendable {
    private let lock = NSLock()
    private var messages: [BitchatMessage] = []

    func didReceiveMessage(_ message: BitchatMessage) {
        lock.lock()
        messages.append(message)
        lock.unlock()
    }

    func snapshot() -> [BitchatMessage] {
        lock.lock()
        defer { lock.unlock() }
        return messages
    }

    func didConnectToPeer(_ peerID: PeerID) {}
    func didDisconnectFromPeer(_ peerID: PeerID) {}
    func didUpdatePeerList(_ peers: [PeerID]) {}
    func didUpdateBluetoothState(_ state: CBManagerState) {}
}

private final class TransferCancellationRecorder: @unchecked Sendable {
    private let lock = NSLock()
    private var transferIDs: Set<String> = []
    private var rejectionReasons: [String: String] = [:]

    func record(_ event: TransferProgressManager.Event) {
        let id: String
        switch event {
        case .cancelled(let cancelledID, _, _):
            id = cancelledID
        case .rejected(let rejectedID, _):
            id = rejectedID
        case .started, .updated, .completed:
            return
        }
        lock.lock()
        transferIDs.insert(id)
        if case .rejected(_, let reason) = event {
            rejectionReasons[id] = reason
        }
        lock.unlock()
    }

    func contains(_ transferID: String) -> Bool {
        lock.lock()
        defer { lock.unlock() }
        return transferIDs.contains(transferID)
    }


    func reason(for transferID: String) -> String? {
        lock.lock()
        defer { lock.unlock() }
        return rejectionReasons[transferID]
    }
}
