import XCTest
import CommonCrypto
import P256K
@testable import Rosetta

/// Cross-platform crypto parity tests: iOS ↔ Desktop ↔ Android.
/// Verifies that all crypto operations produce compatible output
/// and that messages encrypted on any platform can be decrypted on iOS.
@MainActor
final class CryptoParityTests: XCTestCase {

    // MARK: - XChaCha20-Poly1305 Round-Trip

    func testXChaCha20Poly1305_encryptDecryptRoundTrip() throws {
        let plaintext = "Привет, мир! Hello, world! 🔐"
        let key = try CryptoPrimitives.randomBytes(count: 32)
        let nonce = try CryptoPrimitives.randomBytes(count: 24)
        let keyAndNonce = key + nonce

        let ciphertextWithTag = try XChaCha20Engine.encrypt(
            plaintext: Data(plaintext.utf8), key: key, nonce: nonce
        )

        let decrypted = try MessageCrypto.decryptIncomingWithPlainKey(
            ciphertext: ciphertextWithTag.hexString,
            plainKeyAndNonce: keyAndNonce
        )

        XCTAssertEqual(decrypted, plaintext, "Round-trip must preserve plaintext")
    }

    func testXChaCha20Poly1305_wrongKeyFails() throws {
        let plaintext = "secret message"
        let key = try CryptoPrimitives.randomBytes(count: 32)
        let nonce = try CryptoPrimitives.randomBytes(count: 24)
        let wrongKey = try CryptoPrimitives.randomBytes(count: 32)

        let ciphertext = try XChaCha20Engine.encrypt(
            plaintext: Data(plaintext.utf8), key: key, nonce: nonce
        )

        XCTAssertThrowsError(
            try XChaCha20Engine.decrypt(
                ciphertextWithTag: ciphertext, key: wrongKey, nonce: nonce
            ),
            "Decryption with wrong key must fail (Poly1305 tag mismatch)"
        )
    }

    func testXChaCha20Poly1305_emptyPlaintext() throws {
        let key = try CryptoPrimitives.randomBytes(count: 32)
        let nonce = try CryptoPrimitives.randomBytes(count: 24)

        let ciphertext = try XChaCha20Engine.encrypt(
            plaintext: Data(), key: key, nonce: nonce
        )
        // Ciphertext should be exactly 16 bytes (Poly1305 tag only)
        XCTAssertEqual(ciphertext.count, 16, "Empty plaintext produces 16-byte tag only")

        let decrypted = try XChaCha20Engine.decrypt(
            ciphertextWithTag: ciphertext, key: key, nonce: nonce
        )
        XCTAssertEqual(decrypted.count, 0, "Decrypted empty plaintext must be empty")
    }

    func testXChaCha20Poly1305_largePlaintext() throws {
        let plaintext = Data(repeating: 0x42, count: 100_000)
        let key = try CryptoPrimitives.randomBytes(count: 32)
        let nonce = try CryptoPrimitives.randomBytes(count: 24)

        let ciphertext = try XChaCha20Engine.encrypt(
            plaintext: plaintext, key: key, nonce: nonce
        )
        XCTAssertEqual(ciphertext.count, plaintext.count + 16)

        let decrypted = try XChaCha20Engine.decrypt(
            ciphertextWithTag: ciphertext, key: key, nonce: nonce
        )
        XCTAssertEqual(decrypted, plaintext)
    }

    // MARK: - ECDH Encrypt → Decrypt Round-Trip

    func testECDH_encryptDecryptRoundTrip() throws {
        let plaintext = "Test ECDH round-trip"

        // Generate recipient key pair
        let recipientPrivKey = try P256K.KeyAgreement.PrivateKey()
        let recipientPubKeyHex = recipientPrivKey.publicKey.dataRepresentation.hexString

        // Encrypt
        let encrypted = try MessageCrypto.encryptOutgoing(
            plaintext: plaintext,
            recipientPublicKeyHex: recipientPubKeyHex
        )

        XCTAssertFalse(encrypted.content.isEmpty, "Content must not be empty")
        XCTAssertFalse(encrypted.chachaKey.isEmpty, "chachaKey must not be empty")
        XCTAssertEqual(encrypted.plainKeyAndNonce.count, 56, "Key+nonce must be 56 bytes")

        // Decrypt
        let decrypted = try MessageCrypto.decryptIncoming(
            ciphertext: encrypted.content,
            encryptedKey: encrypted.chachaKey,
            myPrivateKeyHex: recipientPrivKey.rawRepresentation.hexString
        )

        XCTAssertEqual(decrypted, plaintext, "ECDH round-trip must preserve plaintext")
    }

    func testECDH_decryptReturnsCorrectKeyAndNonce() throws {
        let plaintext = "Key verification"
        let recipientPrivKey = try P256K.KeyAgreement.PrivateKey()
        let recipientPubKeyHex = recipientPrivKey.publicKey.dataRepresentation.hexString

        let encrypted = try MessageCrypto.encryptOutgoing(
            plaintext: plaintext,
            recipientPublicKeyHex: recipientPubKeyHex
        )

        let (decryptedText, recoveredKeyAndNonce) = try MessageCrypto.decryptIncomingFull(
            ciphertext: encrypted.content,
            encryptedKey: encrypted.chachaKey,
            myPrivateKeyHex: recipientPrivKey.rawRepresentation.hexString
        )

        XCTAssertEqual(decryptedText, plaintext)
        XCTAssertEqual(recoveredKeyAndNonce, encrypted.plainKeyAndNonce,
            "Recovered key+nonce must match original")
    }

    func testECDH_wrongPrivateKeyFails() throws {
        let recipientPrivKey = try P256K.KeyAgreement.PrivateKey()
        let wrongPrivKey = try P256K.KeyAgreement.PrivateKey()
        let recipientPubKeyHex = recipientPrivKey.publicKey.dataRepresentation.hexString

        let encrypted = try MessageCrypto.encryptOutgoing(
            plaintext: "secret",
            recipientPublicKeyHex: recipientPubKeyHex
        )

        XCTAssertThrowsError(
            try MessageCrypto.decryptIncoming(
                ciphertext: encrypted.content,
                encryptedKey: encrypted.chachaKey,
                myPrivateKeyHex: wrongPrivKey.rawRepresentation.hexString
            ),
            "Decryption with wrong private key must fail"
        )
    }

    func testECDH_multipleEncryptions_differentCiphertext() throws {
        let plaintext = "same message"
        let recipientPrivKey = try P256K.KeyAgreement.PrivateKey()
        let recipientPubKeyHex = recipientPrivKey.publicKey.dataRepresentation.hexString

        let enc1 = try MessageCrypto.encryptOutgoing(plaintext: plaintext, recipientPublicKeyHex: recipientPubKeyHex)
        let enc2 = try MessageCrypto.encryptOutgoing(plaintext: plaintext, recipientPublicKeyHex: recipientPubKeyHex)

        XCTAssertNotEqual(enc1.content, enc2.content, "Each encryption must use different random key")
        XCTAssertNotEqual(enc1.chachaKey, enc2.chachaKey, "Each encryption must use different ephemeral key")

        // Both must decrypt correctly
        let dec1 = try MessageCrypto.decryptIncoming(ciphertext: enc1.content, encryptedKey: enc1.chachaKey, myPrivateKeyHex: recipientPrivKey.rawRepresentation.hexString)
        let dec2 = try MessageCrypto.decryptIncoming(ciphertext: enc2.content, encryptedKey: enc2.chachaKey, myPrivateKeyHex: recipientPrivKey.rawRepresentation.hexString)
        XCTAssertEqual(dec1, plaintext)
        XCTAssertEqual(dec2, plaintext)
    }

    // MARK: - aesChachaKey (Sync Path) Round-Trip

    func testAesChachaKey_encryptDecryptRoundTrip() throws {
        let plaintext = "Sync path message"
        let privateKeyHex = try P256K.KeyAgreement.PrivateKey().rawRepresentation.hexString

        // Encrypt with XChaCha20
        let recipientPrivKey = try P256K.KeyAgreement.PrivateKey()
        let encrypted = try MessageCrypto.encryptOutgoing(
            plaintext: plaintext,
            recipientPublicKeyHex: recipientPrivKey.publicKey.dataRepresentation.hexString
        )

        // Build aesChachaKey (same logic as makeOutgoingPacket)
        guard let latin1String = String(data: encrypted.plainKeyAndNonce, encoding: .isoLatin1) else {
            XCTFail("Latin-1 encoding must succeed for any 56-byte sequence")
            return
        }
        let aesChachaPayload = Data(latin1String.utf8)
        let aesChachaKey = try CryptoManager.shared.encryptWithPasswordDesktopCompat(
            aesChachaPayload,
            password: privateKeyHex
        )

        // Decrypt aesChachaKey (same logic as decryptIncomingMessage sync path)
        let decryptedPayload = try CryptoManager.shared.decryptWithPassword(
            aesChachaKey,
            password: privateKeyHex
        )
        let recoveredKeyAndNonce = MessageCrypto.androidUtf8BytesToLatin1Bytes(decryptedPayload)

        XCTAssertEqual(recoveredKeyAndNonce.count, 56, "Recovered key+nonce must be 56 bytes")

        // Decrypt message content with recovered key
        let decryptedText = try MessageCrypto.decryptIncomingWithPlainKey(
            ciphertext: encrypted.content,
            plainKeyAndNonce: recoveredKeyAndNonce
        )
        XCTAssertEqual(decryptedText, plaintext, "aesChachaKey round-trip must recover original text")
    }

    func testAesChachaKey_latin1Utf8RoundTrip_allByteValues() throws {
        // Verify that Latin-1 → UTF-8 → Latin-1 round-trip preserves ALL byte values 0-255.
        // This is critical: key bytes can be ANY value.
        var allBytes = Data(count: 256)
        for i in 0..<256 { allBytes[i] = UInt8(i) }

        guard let latin1String = String(data: allBytes, encoding: .isoLatin1) else {
            XCTFail("Latin-1 must encode all 256 byte values")
            return
        }
        let utf8Bytes = Data(latin1String.utf8)
        let recovered = MessageCrypto.androidUtf8BytesToLatin1Bytes(utf8Bytes)

        XCTAssertEqual(recovered, allBytes,
            "Latin-1 → UTF-8 → Latin-1 must be lossless for all byte values 0-255")
    }

    func testAesChachaKey_wrongPasswordFails() throws {
        let privateKeyHex = try P256K.KeyAgreement.PrivateKey().rawRepresentation.hexString
        let wrongKeyHex = try P256K.KeyAgreement.PrivateKey().rawRepresentation.hexString

        let data = try CryptoPrimitives.randomBytes(count: 56)
        guard let latin1 = String(data: data, encoding: .isoLatin1) else { return }
        let plaintext = Data(latin1.utf8)
        let encrypted = try CryptoManager.shared.encryptWithPasswordDesktopCompat(
            plaintext, password: privateKeyHex
        )

        do {
            let decrypted = try CryptoManager.shared.decryptWithPassword(
                encrypted, password: wrongKeyHex, requireCompression: true
            )
            XCTAssertNotEqual(
                decrypted,
                plaintext,
                "Wrong password must never recover original plaintext"
            )
        } catch {
            // Expected path for the majority of wrong-password attempts.
        }
    }

    // MARK: - Attachment Password Candidates

    func testAttachmentPasswordCandidates_rawkeyFormat() {
        // 56 random bytes as hex
        let keyData = Data([
            0x00, 0x01, 0x7F, 0x80, 0xFF, 0xFE, 0xAB, 0xCD,
            0x00, 0x01, 0x7F, 0x80, 0xFF, 0xFE, 0xAB, 0xCD,
            0x00, 0x01, 0x7F, 0x80, 0xFF, 0xFE, 0xAB, 0xCD,
            0x00, 0x01, 0x7F, 0x80, 0xFF, 0xFE, 0xAB, 0xCD,
            0x00, 0x01, 0x7F, 0x80, 0xFF, 0xFE, 0xAB, 0xCD,
            0x00, 0x01, 0x7F, 0x80, 0xFF, 0xFE, 0xAB, 0xCD,
            0x00, 0x01, 0x7F, 0x80, 0xFF, 0xFE, 0xAB, 0xCD,
        ])
        let stored = "rawkey:" + keyData.hexString

        let candidates = MessageCrypto.attachmentPasswordCandidates(from: stored)

        // Must have at least HEX + Android + WHATWG
        XCTAssertGreaterThanOrEqual(candidates.count, 3, "Must generate ≥3 candidates")

        // HEX must be first (Desktop commit 61e83bd parity)
        XCTAssertEqual(candidates[0], keyData.hexString,
            "First candidate must be HEX (Desktop parity)")

        // All candidates must be unique
        XCTAssertEqual(candidates.count, Set(candidates).count,
            "All candidates must be unique (deduplicated)")
    }

    func testAttachmentPasswordCandidates_legacyFormat() {
        let stored = "some_legacy_password_string"
        let candidates = MessageCrypto.attachmentPasswordCandidates(from: stored)

        XCTAssertEqual(candidates.count, 2, "Legacy format returns hex+plain candidates")
        XCTAssertEqual(candidates[0], Data(stored.utf8).map { String(format: "%02x", $0) }.joined())
        XCTAssertEqual(candidates[1], stored, "Legacy plain candidate must be preserved")
    }

    func testAttachmentPasswordCandidates_hexMatchesDesktop() {
        // Desktop: key.toString('hex') = lowercase hex of raw 56 bytes
        let keyBytes = Data([
            0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE,
            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
            0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10,
            0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
            0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20,
            0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
            0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30,
        ])
        let stored = "rawkey:" + keyBytes.hexString
        let candidates = MessageCrypto.attachmentPasswordCandidates(from: stored)

        // Desktop: Buffer.from(keyBytes).toString('hex')
        let expectedDesktopPassword = "deadbeefcafebabe0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30"

        // Verify hex format matches (lowercase, no separators)
        XCTAssertTrue(candidates[0].allSatisfy { "0123456789abcdef".contains($0) },
            "HEX candidate must be lowercase hex")

        // Verify exact match with expected Desktop output
        // Note: the hex is based on keyBytes.hexString which should be lowercase
        XCTAssertEqual(candidates[0], keyBytes.hexString)
        XCTAssertEqual(candidates[0], expectedDesktopPassword)
    }

    // MARK: - PBKDF2 Parity

    func testPBKDF2_consistentOutput() throws {
        let password = "test_password_hex_string"
        let key1 = CryptoPrimitives.pbkdf2(
            password: password, salt: "rosetta", iterations: 1000,
            keyLength: 32, prf: CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256)
        )
        let key2 = CryptoPrimitives.pbkdf2(
            password: password, salt: "rosetta", iterations: 1000,
            keyLength: 32, prf: CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256)
        )

        XCTAssertNotNil(key1)
        XCTAssertNotNil(key2)
        XCTAssertEqual(key1, key2, "PBKDF2 must be deterministic")
        XCTAssertEqual(key1.count, 32, "PBKDF2 key must be 32 bytes")
    }

    func testPBKDF2_differentPasswordsDifferentKeys() throws {
        let key1 = CryptoPrimitives.pbkdf2(
            password: "password_a", salt: "rosetta", iterations: 1000,
            keyLength: 32, prf: CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256)
        )
        let key2 = CryptoPrimitives.pbkdf2(
            password: "password_b", salt: "rosetta", iterations: 1000,
            keyLength: 32, prf: CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256)
        )

        XCTAssertNotEqual(key1, key2, "Different passwords must produce different keys")
    }

    // MARK: - encryptWithPassword / decryptWithPassword Parity

    func testEncryptWithPasswordDesktopCompat_roundTrip() throws {
        let plaintext = "Cross-platform encrypted message content"
        let password = "my_private_key_hex"

        let encrypted = try CryptoManager.shared.encryptWithPasswordDesktopCompat(
            Data(plaintext.utf8), password: password
        )

        // Must be ivBase64:ctBase64 format
        let parts = encrypted.components(separatedBy: ":")
        XCTAssertEqual(parts.count, 2, "Format must be ivBase64:ctBase64")

        let decrypted = try CryptoManager.shared.decryptWithPassword(
            encrypted, password: password, requireCompression: true
        )
        let decryptedText = String(data: decrypted, encoding: .utf8)

        XCTAssertEqual(decryptedText, plaintext, "Desktop-compat round-trip must preserve plaintext")
    }

    func testEncryptWithPassword_iOSOnly_roundTrip() throws {
        let plaintext = "iOS-only storage encryption"
        let password = "local_private_key"

        let encrypted = try CryptoManager.shared.encryptWithPassword(
            Data(plaintext.utf8), password: password
        )

        let decrypted = try CryptoManager.shared.decryptWithPassword(
            encrypted, password: password, requireCompression: true
        )
        let decryptedText = String(data: decrypted, encoding: .utf8)

        XCTAssertEqual(decryptedText, plaintext, "iOS-only round-trip must preserve plaintext")
    }

    func testDecryptWithPassword_wrongPassword_withCompression_fails() throws {
        let encrypted = try CryptoManager.shared.encryptWithPasswordDesktopCompat(
            Data("secret".utf8), password: "correct_password"
        )

        do {
            let decrypted = try CryptoManager.shared.decryptWithPassword(
                encrypted,
                password: "wrong_password",
                requireCompression: true
            )
            let decryptedText = String(data: decrypted, encoding: .utf8)
            XCTAssertNotEqual(
                decryptedText,
                "secret",
                "Wrong password must never recover original plaintext"
            )
        } catch {
            // Expected path for most wrong-password attempts.
        }
    }

    // MARK: - UTF-8 Decoder Parity (Android ↔ iOS)

    func testAndroidUtf8Decoder_validAscii() {
        let bytes = Data("Hello".utf8)
        let result = MessageCrypto.bytesToAndroidUtf8String(bytes)
        XCTAssertEqual(result, "Hello", "ASCII must decode identically")
    }

    func testAndroidUtf8Decoder_validMultibyte() {
        // Use BMP-only multibyte text here; four-byte emoji sequences are
        // covered by malformed/compatibility behavior in separate tests.
        let bytes = Data("Привет мир".utf8)
        let result = MessageCrypto.bytesToAndroidUtf8String(bytes)
        XCTAssertEqual(result, "Привет мир", "Valid UTF-8 must decode identically")
    }

    func testAndroidUtf8Decoder_matchesWhatWG_onValidUtf8() {
        // For valid UTF-8, both decoders must produce identical results
        for _ in 0..<100 {
            let randomBytes = (0..<56).map { _ in UInt8.random(in: 0...127) }
            let data = Data(randomBytes)

            let android = MessageCrypto.bytesToAndroidUtf8String(data)
            let whatwg = String(decoding: data, as: UTF8.self)

            XCTAssertEqual(android, whatwg,
                "For ASCII bytes, Android and WHATWG decoders must match")
        }
    }

    func testLatin1RoundTrip_withBothDecoders_onValidUtf8() {
        // When input is valid UTF-8 (from CryptoJS Latin-1→UTF-8), both decoders match
        // This is the ACTUAL scenario for Desktop→iOS messages
        var allLatin1 = Data(count: 256)
        for i in 0..<256 { allLatin1[i] = UInt8(i) }

        // Simulate Desktop: Latin-1 string → UTF-8 bytes
        guard let latin1String = String(data: allLatin1, encoding: .isoLatin1) else {
            XCTFail("Latin-1 encoding must work")
            return
        }
        let utf8Bytes = Data(latin1String.utf8)

        // iOS recovery path 1: WHATWG
        let recoveredWhatWG = MessageCrypto.androidUtf8BytesToLatin1Bytes(utf8Bytes)
        // iOS recovery path 2: Android polyfill
        let recoveredAndroid = MessageCrypto.androidUtf8BytesToLatin1BytesAlt(utf8Bytes)

        XCTAssertEqual(recoveredWhatWG, allLatin1,
            "WHATWG decoder must recover all 256 Latin-1 bytes from valid UTF-8")
        XCTAssertEqual(recoveredAndroid, allLatin1,
            "Android decoder must recover all 256 Latin-1 bytes from valid UTF-8")
        XCTAssertEqual(recoveredWhatWG, recoveredAndroid,
            "Both decoders must produce identical results for valid UTF-8 input")
    }

    // MARK: - Defense Layers

    func testIsProbablyEncryptedPayload_base64Colon() {
        // ivBase64:ctBase64 (both ≥16 chars)
        let encrypted = "YWJjZGVmZ2hpamtsbQ==:eHl6MTIzNDU2Nzg5MA=="
        XCTAssertTrue(MessageRepository.testIsProbablyEncrypted(encrypted),
            "ivBase64:ctBase64 must be detected")
    }

    func testIsProbablyEncryptedPayload_hexString() {
        // Pure hex ≥40 chars
        let hex = String(repeating: "ab", count: 30) // 60 chars
        XCTAssertTrue(MessageRepository.testIsProbablyEncrypted(hex),
            "Pure hex ≥40 chars must be detected")
    }

    func testIsProbablyEncryptedPayload_chunked() {
        XCTAssertTrue(MessageRepository.testIsProbablyEncrypted("CHNK:data:here"),
            "CHNK: format must be detected")
    }

    func testIsProbablyEncryptedPayload_normalText() {
        XCTAssertFalse(MessageRepository.testIsProbablyEncrypted("Hello, world!"),
            "Normal text must NOT be flagged")
        XCTAssertFalse(MessageRepository.testIsProbablyEncrypted("Привет"),
            "Cyrillic text must NOT be flagged")
    }

    func testIsProbablyEncryptedPayload_shortHex() {
        // Short hex (<40 chars) must NOT be flagged (could be normal text)
        XCTAssertFalse(MessageRepository.testIsProbablyEncrypted("abcdef1234"),
            "Short hex must NOT be flagged")
    }

    func testIsProbablyEncryptedPayload_shortBase64Parts() {
        // Both parts <16 chars must NOT be flagged
        XCTAssertFalse(MessageRepository.testIsProbablyEncrypted("abc:def"),
            "Short base64:base64 must NOT be flagged")
    }

    func testIsProbablyEncryptedPayload_emptyString() {
        // Empty string is not encrypted
        XCTAssertFalse(MessageRepository.testIsProbablyEncrypted(""),
            "Empty string must NOT be flagged as encrypted (it's just empty)")
    }

    // MARK: - Compression Parity (zlib)

    func testZlibDeflate_inflate_roundTrip() throws {
        let original = Data("Test compression for cross-platform parity".utf8)

        let compressed = try CryptoPrimitives.zlibDeflate(original)
        XCTAssertTrue(compressed.count > 0, "Compressed data must not be empty")
        XCTAssertTrue(compressed[0] == 0x78, "zlib deflate must start with 0x78 header")

        let decompressed = try CryptoPrimitives.rawInflate(compressed)
        XCTAssertEqual(decompressed, original, "Inflate must recover original data")
    }

    func testRawDeflate_inflate_roundTrip() throws {
        let original = Data("Raw deflate for iOS-only storage".utf8)

        let compressed = try CryptoPrimitives.rawDeflate(original)
        XCTAssertTrue(compressed.count > 0)

        let decompressed = try CryptoPrimitives.rawInflate(compressed)
        XCTAssertEqual(decompressed, original, "Raw inflate must recover original data")
    }

    // MARK: - Full Message Flow (Simulate Desktop → iOS)

    func testFullMessageFlow_desktopToiOS() throws {
        // Simulate: Desktop sends message to iOS user
        let senderPrivKey = try P256K.KeyAgreement.PrivateKey()
        let recipientPrivKey = try P256K.KeyAgreement.PrivateKey()
        let recipientPubKeyHex = recipientPrivKey.publicKey.dataRepresentation.hexString

        let originalMessage = "Message from Desktop to iOS 🚀"

        // Step 1: Desktop encrypts (simulated on iOS since crypto is identical)
        let encrypted = try MessageCrypto.encryptOutgoing(
            plaintext: originalMessage,
            recipientPublicKeyHex: recipientPubKeyHex
        )

        // Step 2: Build aesChachaKey for sync (Desktop logic)
        let senderPrivKeyHex = senderPrivKey.rawRepresentation.hexString
        guard let latin1 = String(data: encrypted.plainKeyAndNonce, encoding: .isoLatin1) else {
            XCTFail("Latin-1 encoding failed")
            return
        }
        let aesChachaKey = try CryptoManager.shared.encryptWithPasswordDesktopCompat(
            Data(latin1.utf8), password: senderPrivKeyHex
        )

        // Step 3: iOS receives and decrypts via ECDH path
        let (ecdhText, ecdhKeyAndNonce) = try MessageCrypto.decryptIncomingFull(
            ciphertext: encrypted.content,
            encryptedKey: encrypted.chachaKey,
            myPrivateKeyHex: recipientPrivKey.rawRepresentation.hexString
        )
        XCTAssertEqual(ecdhText, originalMessage, "ECDH path must decrypt correctly")

        // Step 4: iOS receives own message via aesChachaKey sync path
        let syncDecrypted = try CryptoManager.shared.decryptWithPassword(
            aesChachaKey, password: senderPrivKeyHex
        )
        let syncKeyAndNonce = MessageCrypto.androidUtf8BytesToLatin1Bytes(syncDecrypted)
        let syncText = try MessageCrypto.decryptIncomingWithPlainKey(
            ciphertext: encrypted.content,
            plainKeyAndNonce: syncKeyAndNonce
        )
        XCTAssertEqual(syncText, originalMessage, "aesChachaKey sync path must decrypt correctly")

        // Step 5: Verify key+nonce matches across both paths
        XCTAssertEqual(ecdhKeyAndNonce, syncKeyAndNonce,
            "ECDH and sync paths must recover the same key+nonce")

        // Step 6: Verify attachment password derivation matches
        let ecdhPassword = "rawkey:" + ecdhKeyAndNonce.hexString
        let syncPassword = "rawkey:" + syncKeyAndNonce.hexString
        let ecdhCandidates = MessageCrypto.attachmentPasswordCandidates(from: ecdhPassword)
        let syncCandidates = MessageCrypto.attachmentPasswordCandidates(from: syncPassword)
        XCTAssertEqual(ecdhCandidates, syncCandidates,
            "Attachment password candidates must be identical across both paths")
    }

    func testDecryptIncomingMessage_allowsAttachmentOnlyEmptyContent() throws {
        let privateKeyHex = try P256K.KeyAgreement.PrivateKey().rawRepresentation.hexString
        var packet = PacketMessage()
        packet.fromPublicKey = "02peer_attachment_only"
        packet.toPublicKey = "02my_attachment_only"
        packet.content = ""
        packet.chachaKey = ""
        packet.attachments = [
            MessageAttachment(
                id: "att-1",
                preview: "preview",
                blob: "",
                type: .image,
                transportTag: "tag-1",
                transportServer: "cdn.rosetta.im"
            ),
        ]

        let result = SessionManager.testDecryptIncomingMessage(
            packet: packet,
            myPublicKey: "02my_attachment_only",
            privateKeyHex: privateKeyHex,
            groupKey: nil
        )

        XCTAssertNotNil(result)
        XCTAssertEqual(result?.text, "")
    }

    func testDecryptIncomingMessage_rejectsEmptyContentWithoutAttachments() throws {
        let privateKeyHex = try P256K.KeyAgreement.PrivateKey().rawRepresentation.hexString
        var packet = PacketMessage()
        packet.fromPublicKey = "02peer_invalid"
        packet.toPublicKey = "02my_invalid"
        packet.content = ""
        packet.chachaKey = ""
        packet.attachments = []

        let result = SessionManager.testDecryptIncomingMessage(
            packet: packet,
            myPublicKey: "02my_invalid",
            privateKeyHex: privateKeyHex,
            groupKey: nil
        )

        XCTAssertNil(result)
    }

    func testRecoverRetryPlaintext_rejectsCiphertextFallback() throws {
        let privateKeyHex = try P256K.KeyAgreement.PrivateKey().rawRepresentation.hexString
        let wrongPrivateKeyHex = try P256K.KeyAgreement.PrivateKey().rawRepresentation.hexString

        let encrypted = try CryptoManager.shared.encryptWithPassword(
            Data("retry-text".utf8),
            password: privateKeyHex
        )

        let recoveredWithWrongKey = SessionManager.testRecoverRetryPlaintext(
            storedText: encrypted,
            privateKeyHex: wrongPrivateKeyHex
        )
        XCTAssertNotEqual(
            recoveredWithWrongKey,
            encrypted,
            "Retry recovery must never return encrypted wire payload as plaintext"
        )
        XCTAssertNotEqual(
            recoveredWithWrongKey,
            "retry-text",
            "Wrong key must never recover original plaintext"
        )

        let recoveredPlainLegacy = SessionManager.testRecoverRetryPlaintext(
            storedText: "legacy plain text",
            privateKeyHex: privateKeyHex
        )
        XCTAssertEqual(recoveredPlainLegacy, "legacy plain text")
    }

    func testRawKeyAndNonceParser_requiresStrictRawKeyFormat() throws {
        let raw = try CryptoPrimitives.randomBytes(count: 56)
        let validStored = "rawkey:" + raw.hexString

        let decodedValid = SessionManager.testRawKeyAndNonceFromStoredAttachmentPassword(validStored)
        XCTAssertEqual(decodedValid, raw)

        XCTAssertNil(
            SessionManager.testRawKeyAndNonceFromStoredAttachmentPassword(raw.hexString),
            "Missing rawkey prefix must be rejected"
        )
        XCTAssertNil(
            SessionManager.testRawKeyAndNonceFromStoredAttachmentPassword("rawkey:abc"),
            "Odd-length hex must be rejected"
        )
        XCTAssertNil(
            SessionManager.testRawKeyAndNonceFromStoredAttachmentPassword("rawkey:zz"),
            "Non-hex symbols must be rejected"
        )
    }

    func testDataStrictHexString_rejectsInvalidInput() {
        XCTAssertNil(Data(strictHexString: "abc"))
        XCTAssertNil(Data(strictHexString: "0g"))
        XCTAssertEqual(Data(strictHexString: "0A0b"), Data([0x0A, 0x0B]))
    }

    // MARK: - Stress Test: Random Key Bytes

    func testECDH_100RandomKeys_allDecryptSuccessfully() throws {
        for i in 0..<100 {
            let recipientPrivKey = try P256K.KeyAgreement.PrivateKey()
            let recipientPubKeyHex = recipientPrivKey.publicKey.dataRepresentation.hexString

            let encrypted = try MessageCrypto.encryptOutgoing(
                plaintext: "Message #\(i)",
                recipientPublicKeyHex: recipientPubKeyHex
            )

            let decrypted = try MessageCrypto.decryptIncoming(
                ciphertext: encrypted.content,
                encryptedKey: encrypted.chachaKey,
                myPrivateKeyHex: recipientPrivKey.rawRepresentation.hexString
            )

            XCTAssertEqual(decrypted, "Message #\(i)", "Message #\(i) must decrypt correctly")
        }
    }

    func testAesChachaKey_100RandomKeys_allRoundTrip() throws {
        for i in 0..<100 {
            let keyAndNonce = try CryptoPrimitives.randomBytes(count: 56)
            let password = try P256K.KeyAgreement.PrivateKey().rawRepresentation.hexString

            guard let latin1 = String(data: keyAndNonce, encoding: .isoLatin1) else {
                XCTFail("Latin-1 must work for key #\(i)")
                continue
            }

            let encrypted = try CryptoManager.shared.encryptWithPasswordDesktopCompat(
                Data(latin1.utf8), password: password
            )

            let decrypted = try CryptoManager.shared.decryptWithPassword(
                encrypted, password: password
            )
            let recovered = MessageCrypto.androidUtf8BytesToLatin1Bytes(decrypted)

            XCTAssertEqual(recovered, keyAndNonce,
                "aesChachaKey round-trip #\(i) must recover original 56 bytes")
        }
    }
}
