import XCTest
@testable import Rosetta

// MARK: - Reply Preview Text Tests (ChatDetailView.replyPreviewText parity)

/// Tests for reply preview text logic — cross-platform parity with Android/Desktop.
/// Android: `ChatDetailInput.kt` lines 653-679
/// Desktop: `constructLastMessageTextByAttachments.ts`
///
/// Covers ALL 3 iOS display contexts:
/// 1. Reply bar input — `ChatDetailView.replyPreviewText(for:)`
/// 2. Reply bar render — `ChatDetailView.replyBar(for:)` inline closure
/// 3. Reply quote bubble — `MessageCellView.replyQuoteView(reply:outgoing:)`
/// Plus: chat list last message — `DialogRepository.updateDialogFromMessages()`
final class ReplyPreviewTextTests: XCTestCase {

    // MARK: - Helpers

    /// Standalone replica of `ChatDetailView.replyPreviewText(for:)`.
    /// Must match the real implementation exactly.
    private func replyPreviewText(for message: ChatMessage) -> String {
        let attachmentLabel: String? = {
            for att in message.attachments {
                switch att.type {
                case .image:
                    return "Photo"
                case .file:
                    let parsed = AttachmentPreviewCodec.parseFilePreview(att.preview)
                    if !parsed.fileName.isEmpty { return parsed.fileName }
                    return att.id.isEmpty ? "File" : att.id
                case .avatar:
                    return "Avatar"
                case .messages:
                    return "Forwarded message"
                case .call:
                    return "Call"
                case .voice:
                    return "Voice message"
                }
            }
            return nil
        }()

        let visibleText: String = {
            let stripped = message.text
                .trimmingCharacters(in: .whitespacesAndNewlines)
                .filter { !$0.isASCII || $0.asciiValue! >= 0x20 }
            if MessageCellLayout.isGarbageOrEncrypted(stripped) { return "" }
            return stripped
        }()
        let visibleTextDecoded = EmojiParser.replaceShortcodes(in: visibleText)

        if attachmentLabel != nil, !visibleTextDecoded.isEmpty { return visibleTextDecoded }
        if let label = attachmentLabel { return label }
        if !visibleTextDecoded.isEmpty { return visibleTextDecoded }
        return ""
    }

    /// Standalone replica of SessionManager in-app banner preview logic.
    /// Intentionally mirrors current behavior for regression coverage.
    private func inAppBannerPreviewText(text: String, attachments: [MessageAttachment]) -> String {
        if !text.isEmpty { return EmojiParser.replaceShortcodes(in: text) }
        if let firstAtt = attachments.first {
            switch firstAtt.type {
            case .image: return "Photo"
            case .file: return "File"
            case .voice: return "Voice message"
            case .avatar: return "Avatar"
            case .messages: return "Forwarded message"
            case .call: return "Call"
            default: return "Attachment"
            }
        }
        return "New message"
    }

    /// Standalone replica of NativeMessageList reply text cache logic for UIKit cells.
    /// When outer message is empty, it is treated as forwarded wrapper (no quote text shown).
    private func nativeReplyCacheText(outerMessageText: String, reply: ReplyMessageData) -> String? {
        let displayText = MessageCellLayout.isGarbageOrEncrypted(outerMessageText) ? "" : outerMessageText
        guard !displayText.isEmpty else { return nil }

        let trimmed = reply.message.trimmingCharacters(in: .whitespacesAndNewlines)
        if !trimmed.isEmpty {
            return EmojiParser.replaceShortcodes(in: reply.message)
        }
        if reply.attachments.contains(where: { $0.type == AttachmentType.image.rawValue }) { return "Photo" }
        if reply.attachments.contains(where: { $0.type == AttachmentType.messages.rawValue }) { return "Forwarded message" }
        if reply.attachments.contains(where: { $0.type == AttachmentType.file.rawValue }) { return "File" }
        if reply.attachments.contains(where: { $0.type == AttachmentType.avatar.rawValue }) { return "Avatar" }
        if reply.attachments.contains(where: { $0.type == AttachmentType.voice.rawValue }) { return "Voice message" }
        if reply.attachments.contains(where: { $0.type == AttachmentType.call.rawValue }) { return "Call" }
        return "Attachment"
    }

    /// Standalone replica of `MessageCellView.replyQuoteView` preview logic.
    /// Uses `ReplyMessageData` + `ReplyAttachmentData` (Int type), not `MessageAttachment`.
    private func replyQuotePreviewText(for reply: ReplyMessageData) -> String {
        let trimmed = reply.message.trimmingCharacters(in: .whitespaces)
        if !trimmed.isEmpty { return EmojiParser.replaceShortcodes(in: reply.message) }
        if reply.attachments.contains(where: { $0.type == AttachmentType.image.rawValue }) { return "Photo" }
        if reply.attachments.contains(where: { $0.type == AttachmentType.messages.rawValue }) { return "Forwarded message" }
        if reply.attachments.contains(where: { $0.type == AttachmentType.file.rawValue }) { return "File" }
        if reply.attachments.contains(where: { $0.type == AttachmentType.avatar.rawValue }) { return "Avatar" }
        if reply.attachments.contains(where: { $0.type == AttachmentType.voice.rawValue }) { return "Voice message" }
        if reply.attachments.contains(where: { $0.type == AttachmentType.call.rawValue }) { return "Call" }
        return "Attachment"
    }

    /// Standalone replica of `DialogRepository.updateDialogFromMessages` preview logic.
    private func chatListPreviewText(text: String, attachments: [MessageAttachment]) -> String {
        let textIsEmpty = text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
            || Self.isGarbageText(text)
        if textIsEmpty, let firstAtt = attachments.first {
            switch firstAtt.type {
            case .image: return "Photo"
            case .file: return "File"
            case .avatar: return "Avatar"
            case .messages: return "Forwarded message"
            case .call: return "Call"
            case .voice: return "Voice message"
            }
        } else if textIsEmpty {
            return ""
        }
        return text
    }

    /// Mirror of `DialogRepository.isGarbageText`.
    private static func isGarbageText(_ text: String) -> Bool {
        let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
        if trimmed.isEmpty { return true }

        // Detect encrypted payload: ivBase64:ctBase64 format
        let parts = trimmed.components(separatedBy: ":")
        if parts.count == 2 {
            let base64Chars = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "+/="))
            let bothBase64 = parts.allSatisfy { part in
                part.count >= 16 && part.unicodeScalars.allSatisfy { base64Chars.contains($0) }
            }
            if bothBase64 { return true }
        }
        if trimmed.hasPrefix("CHNK:") { return true }

        let validCharacters = trimmed.unicodeScalars.filter { scalar in
            scalar.value != 0xFFFD &&
            scalar.value > 0x1F &&
            scalar.value != 0x7F &&
            !CharacterSet.controlCharacters.contains(scalar)
        }
        return validCharacters.isEmpty
    }

    private func makeMessage(
        text: String = "",
        attachments: [MessageAttachment] = []
    ) -> ChatMessage {
        ChatMessage(
            id: UUID().uuidString,
            fromPublicKey: "sender",
            toPublicKey: "recipient",
            text: text,
            timestamp: Int64(Date().timeIntervalSince1970 * 1000),
            deliveryStatus: .delivered,
            isRead: true,
            attachments: attachments
        )
    }

    private func makeAttachment(type: AttachmentType, preview: String = "", id: String = "") -> MessageAttachment {
        MessageAttachment(id: id.isEmpty ? UUID().uuidString : id, preview: preview, blob: "", type: type)
    }

    private func makeReply(
        message: String = "",
        attachments: [ReplyAttachmentData] = []
    ) -> ReplyMessageData {
        ReplyMessageData(
            message_id: UUID().uuidString,
            publicKey: "sender",
            message: message,
            timestamp: Int64(Date().timeIntervalSince1970 * 1000),
            attachments: attachments
        )
    }

    private func makeReplyAttachment(type: Int, preview: String = "") -> ReplyAttachmentData {
        ReplyAttachmentData(id: UUID().uuidString, type: type, preview: preview, blob: "")
    }

    // =========================================================================
    // MARK: - Reply Bar Preview (ChatDetailView.replyPreviewText)
    // =========================================================================

    // MARK: Photo

    func testReplyBar_PhotoNoCaption() {
        let msg = makeMessage(attachments: [makeAttachment(type: .image)])
        XCTAssertEqual(replyPreviewText(for: msg), "Photo")
    }

    func testReplyBar_PhotoWithCaption() {
        let msg = makeMessage(text: "Nice sunset", attachments: [makeAttachment(type: .image)])
        XCTAssertEqual(replyPreviewText(for: msg), "Nice sunset")
    }

    func testReplyBar_PhotoWithWhitespaceCaption() {
        let msg = makeMessage(text: "   \n  ", attachments: [makeAttachment(type: .image)])
        XCTAssertEqual(replyPreviewText(for: msg), "Photo")
    }

    func testReplyBar_MultiplePhotos() {
        let msg = makeMessage(attachments: [
            makeAttachment(type: .image),
            makeAttachment(type: .image),
            makeAttachment(type: .image)
        ])
        XCTAssertEqual(replyPreviewText(for: msg), "Photo")
    }

    // MARK: File

    func testReplyBar_FileWithFilename() {
        let att = makeAttachment(type: .file, preview: "tag::12345::document.pdf")
        let msg = makeMessage(attachments: [att])
        XCTAssertEqual(replyPreviewText(for: msg), "document.pdf")
    }

    func testReplyBar_FileNoPreview() {
        let att = makeAttachment(type: .file, id: "file_123")
        let msg = makeMessage(attachments: [att])
        XCTAssertEqual(replyPreviewText(for: msg), "file_123")
    }

    func testReplyBar_FileEmptyIdAndPreview() {
        let att = MessageAttachment(id: "", preview: "", blob: "", type: .file)
        let msg = makeMessage(attachments: [att])
        XCTAssertEqual(replyPreviewText(for: msg), "File")
    }

    func testReplyBar_FileWithCaption() {
        let att = makeAttachment(type: .file, preview: "tag::500::notes.txt")
        let msg = makeMessage(text: "Here are the notes", attachments: [att])
        XCTAssertEqual(replyPreviewText(for: msg), "Here are the notes")
    }

    // MARK: Avatar

    func testReplyBar_Avatar() {
        let msg = makeMessage(attachments: [makeAttachment(type: .avatar)])
        XCTAssertEqual(replyPreviewText(for: msg), "Avatar")
    }

    func testReplyBar_AvatarWithCaptionUsesCaption() {
        let msg = makeMessage(text: "Profile pic", attachments: [makeAttachment(type: .avatar)])
        XCTAssertEqual(replyPreviewText(for: msg), "Profile pic")
    }

    // MARK: Forward

    func testReplyBar_Forward() {
        let msg = makeMessage(attachments: [makeAttachment(type: .messages)])
        XCTAssertEqual(replyPreviewText(for: msg), "Forwarded message")
    }

    func testReplyBar_ForwardWithCaptionUsesCaption() {
        let msg = makeMessage(text: "See this", attachments: [makeAttachment(type: .messages)])
        XCTAssertEqual(replyPreviewText(for: msg), "See this")
    }

    // MARK: Call (Android/Desktop parity)

    func testReplyBar_Call() {
        let msg = makeMessage(attachments: [makeAttachment(type: .call)])
        XCTAssertEqual(replyPreviewText(for: msg), "Call")
    }

    func testReplyBar_CallWithCaptionUsesCaption() {
        let msg = makeMessage(text: "Missed you", attachments: [makeAttachment(type: .call)])
        XCTAssertEqual(replyPreviewText(for: msg), "Missed you")
    }

    func testReplyBar_VoiceMessage() {
        let msg = makeMessage(attachments: [makeAttachment(type: .voice)])
        XCTAssertEqual(replyPreviewText(for: msg), "Voice message")
    }

    func testReplyBar_VoiceWithCaptionUsesCaption() {
        let msg = makeMessage(text: "Listen", attachments: [makeAttachment(type: .voice)])
        XCTAssertEqual(replyPreviewText(for: msg), "Listen")
    }

    // MARK: Text

    func testReplyBar_TextOnly() {
        let msg = makeMessage(text: "Hello world")
        XCTAssertEqual(replyPreviewText(for: msg), "Hello world")
    }

    func testReplyBar_TextShortcodesAreDecodedToEmoji() {
        let msg = makeMessage(text: "ok :emoji_1f44d:")
        let preview = replyPreviewText(for: msg)
        XCTAssertFalse(preview.contains(":emoji_"))
        XCTAssertTrue(preview.contains(EmojiParser.unifiedToEmoji("1f44d")))
    }

    func testReplyBar_Empty() {
        let msg = makeMessage()
        XCTAssertEqual(replyPreviewText(for: msg), "")
    }

    // MARK: Priority

    func testReplyBar_ImagePriorityOverCall() {
        let msg = makeMessage(attachments: [
            makeAttachment(type: .image),
            makeAttachment(type: .call)
        ])
        XCTAssertEqual(replyPreviewText(for: msg), "Photo")
    }

    func testReplyBar_ImagePriorityOverFile() {
        let msg = makeMessage(attachments: [
            makeAttachment(type: .image),
            makeAttachment(type: .file, preview: "tag::100::doc.pdf")
        ])
        XCTAssertEqual(replyPreviewText(for: msg), "Photo")
    }

    func testReplyBar_FilePriorityOverAvatar() {
        let msg = makeMessage(attachments: [
            makeAttachment(type: .file, preview: "tag::100::doc.pdf"),
            makeAttachment(type: .avatar)
        ])
        XCTAssertEqual(replyPreviewText(for: msg), "doc.pdf")
    }

    // =========================================================================
    // MARK: - Reply Quote Bubble (MessageCellView.replyQuoteView)
    // =========================================================================

    func testQuote_PhotoNoText() {
        let reply = makeReply(attachments: [makeReplyAttachment(type: 0)])
        XCTAssertEqual(replyQuotePreviewText(for: reply), "Photo")
    }

    func testQuote_PhotoWithText() {
        let reply = makeReply(message: "Check this out", attachments: [makeReplyAttachment(type: 0)])
        XCTAssertEqual(replyQuotePreviewText(for: reply), "Check this out")
    }

    func testQuote_File() {
        let reply = makeReply(attachments: [makeReplyAttachment(type: 2)])
        XCTAssertEqual(replyQuotePreviewText(for: reply), "File")
    }

    func testQuote_Avatar() {
        let reply = makeReply(attachments: [makeReplyAttachment(type: 3)])
        XCTAssertEqual(replyQuotePreviewText(for: reply), "Avatar")
    }

    func testQuote_Forward() {
        let reply = makeReply(attachments: [makeReplyAttachment(type: 1)])
        XCTAssertEqual(replyQuotePreviewText(for: reply), "Forwarded message")
    }

    func testQuote_Call() {
        let reply = makeReply(attachments: [makeReplyAttachment(type: 4)])
        XCTAssertEqual(replyQuotePreviewText(for: reply), "Call")
    }

    func testQuote_VoiceMessage() {
        let reply = makeReply(attachments: [makeReplyAttachment(type: AttachmentType.voice.rawValue)])
        XCTAssertEqual(replyQuotePreviewText(for: reply), "Voice message")
    }

    func testQuote_UnknownType() {
        let reply = makeReply(attachments: [makeReplyAttachment(type: 99)])
        XCTAssertEqual(replyQuotePreviewText(for: reply), "Attachment")
    }

    func testQuote_TextPriority() {
        let reply = makeReply(message: "Some text", attachments: [makeReplyAttachment(type: 4)])
        XCTAssertEqual(replyQuotePreviewText(for: reply), "Some text")
    }

    func testQuote_TextShortcodesAreDecodedToEmoji() {
        let reply = makeReply(message: "ok :emoji_1f44d:", attachments: [])
        let preview = replyQuotePreviewText(for: reply)
        XCTAssertFalse(preview.contains(":emoji_"))
        XCTAssertTrue(preview.contains(EmojiParser.unifiedToEmoji("1f44d")))
    }

    func testQuote_WhitespaceText() {
        let reply = makeReply(message: "   ", attachments: [makeReplyAttachment(type: 0)])
        XCTAssertEqual(replyQuotePreviewText(for: reply), "Photo")
    }

    // =========================================================================
    // MARK: - Chat List Preview (DialogRepository)
    // =========================================================================

    func testChatList_Photo() {
        XCTAssertEqual(chatListPreviewText(text: "", attachments: [makeAttachment(type: .image)]), "Photo")
    }

    func testChatList_File() {
        XCTAssertEqual(chatListPreviewText(text: "", attachments: [makeAttachment(type: .file)]), "File")
    }

    func testChatList_Avatar() {
        XCTAssertEqual(chatListPreviewText(text: "", attachments: [makeAttachment(type: .avatar)]), "Avatar")
    }

    func testChatList_Forward() {
        XCTAssertEqual(chatListPreviewText(text: "", attachments: [makeAttachment(type: .messages)]), "Forwarded message")
    }

    func testChatList_Call() {
        XCTAssertEqual(chatListPreviewText(text: "", attachments: [makeAttachment(type: .call)]), "Call")
    }

    func testChatList_Voice() {
        XCTAssertEqual(chatListPreviewText(text: "", attachments: [makeAttachment(type: .voice)]), "Voice message")
    }

    func testChatList_TextWins() {
        XCTAssertEqual(chatListPreviewText(text: "Hello", attachments: [makeAttachment(type: .image)]), "Hello")
    }

    func testChatList_EmptyNoAttachment() {
        XCTAssertEqual(chatListPreviewText(text: "", attachments: []), "")
    }

    func testChatList_GarbageTextShowsAttachment() {
        // U+FFFD replacement chars = garbage text → should show attachment label
        let garbage = "\u{FFFD}\u{FFFD}\u{FFFD}"
        XCTAssertEqual(chatListPreviewText(text: garbage, attachments: [makeAttachment(type: .image)]), "Photo")
    }

    func testChatList_ControlCharsShowsAttachment() {
        let control = "\u{01}\u{02}\u{03}"
        XCTAssertEqual(chatListPreviewText(text: control, attachments: [makeAttachment(type: .call)]), "Call")
    }

    // =========================================================================
    // MARK: - In-App Banner Preview (SessionManager)
    // =========================================================================

    func testInAppBanner_TextShortcodesAreDecodedToEmoji() {
        let input = "ok :emoji_1f44d:"
        let preview = inAppBannerPreviewText(text: input, attachments: [])
        XCTAssertFalse(preview.contains(":emoji_"))
        XCTAssertTrue(preview.contains(EmojiParser.unifiedToEmoji("1f44d")))
    }

    func testInAppBanner_VoiceAttachmentLabel() {
        let preview = inAppBannerPreviewText(text: "", attachments: [makeAttachment(type: .voice)])
        XCTAssertEqual(preview, "Voice message")
    }

    func testInAppBanner_AvatarAttachmentLabel() {
        let preview = inAppBannerPreviewText(text: "", attachments: [makeAttachment(type: .avatar)])
        XCTAssertEqual(preview, "Avatar")
    }

    // =========================================================================
    // MARK: - Native Reply Cache (NativeMessageList UIKit path)
    // =========================================================================

    func testNativeReplyCache_VoiceAttachmentLabel() {
        let reply = makeReply(message: "", attachments: [makeReplyAttachment(type: AttachmentType.voice.rawValue)])
        XCTAssertEqual(nativeReplyCacheText(outerMessageText: "fix", reply: reply), "Voice message")
    }

    func testNativeReplyCache_DecodesEmojiShortcodes() {
        let reply = makeReply(message: "ok :emoji_1f44d:", attachments: [])
        XCTAssertEqual(nativeReplyCacheText(outerMessageText: "text", reply: reply), "ok 👍")
    }

    // =========================================================================
    // MARK: - AttachmentType JSON Decode Fault Tolerance
    // =========================================================================

    func testDecode_UnknownTypeFallsBackToImage() throws {
        let json = #"[{"id":"test","preview":"","blob":"","type":99}]"#
        let decoded = try JSONDecoder().decode([MessageAttachment].self, from: Data(json.utf8))
        XCTAssertEqual(decoded.count, 1)
        XCTAssertEqual(decoded[0].type, .image)
    }

    func testDecode_NegativeTypeFallsBackToImage() throws {
        let json = #"[{"id":"test","preview":"","blob":"","type":-1}]"#
        let decoded = try JSONDecoder().decode([MessageAttachment].self, from: Data(json.utf8))
        XCTAssertEqual(decoded.count, 1)
        XCTAssertEqual(decoded[0].type, .image)
    }

    func testDecode_MixedKnownAndUnknown() throws {
        let json = #"[{"id":"a","preview":"","blob":"","type":0},{"id":"b","preview":"","blob":"","type":99}]"#
        let decoded = try JSONDecoder().decode([MessageAttachment].self, from: Data(json.utf8))
        XCTAssertEqual(decoded.count, 2)
        XCTAssertEqual(decoded[0].type, .image)
        XCTAssertEqual(decoded[1].type, .image)
    }

    func testDecode_AllKnownTypes() throws {
        let json = """
        [{"id":"a","preview":"","blob":"","type":0},\
        {"id":"b","preview":"","blob":"","type":1},\
        {"id":"c","preview":"","blob":"","type":2},\
        {"id":"d","preview":"","blob":"","type":3},\
        {"id":"e","preview":"","blob":"","type":4}]
        """
        let decoded = try JSONDecoder().decode([MessageAttachment].self, from: Data(json.utf8))
        XCTAssertEqual(decoded.count, 5)
        XCTAssertEqual(decoded[0].type, .image)
        XCTAssertEqual(decoded[1].type, .messages)
        XCTAssertEqual(decoded[2].type, .file)
        XCTAssertEqual(decoded[3].type, .avatar)
        XCTAssertEqual(decoded[4].type, .call)
    }

    func testDecode_EmptyArrayIsValid() throws {
        let json = "[]"
        let decoded = try JSONDecoder().decode([MessageAttachment].self, from: Data(json.utf8))
        XCTAssertEqual(decoded.count, 0)
    }

    // MARK: - Encode → Decode Round-Trip

    func testRoundTrip_AllTypes() throws {
        let original: [MessageAttachment] = [
            MessageAttachment(id: "1", preview: "tag::hash", blob: "data", type: .image),
            MessageAttachment(id: "2", preview: "", blob: "", type: .messages),
            MessageAttachment(id: "3", preview: "tag::500::doc.pdf", blob: "", type: .file),
            MessageAttachment(id: "4", preview: "", blob: "", type: .avatar),
            MessageAttachment(id: "5", preview: "60", blob: "", type: .call),
        ]
        let data = try JSONEncoder().encode(original)
        let decoded = try JSONDecoder().decode([MessageAttachment].self, from: data)
        XCTAssertEqual(decoded, original)
    }

    // =========================================================================
    // MARK: - Cross-Context Consistency
    // =========================================================================

    /// Verify all 3 contexts produce the same label for each type when text is empty.
    func testAllContextsAgree_Photo() {
        let msg = makeMessage(attachments: [makeAttachment(type: .image)])
        let reply = makeReply(attachments: [makeReplyAttachment(type: 0)])
        XCTAssertEqual(replyPreviewText(for: msg), "Photo")
        XCTAssertEqual(replyQuotePreviewText(for: reply), "Photo")
        XCTAssertEqual(chatListPreviewText(text: "", attachments: msg.attachments), "Photo")
    }

    func testAllContextsAgree_File() {
        // Note: reply bar shows filename, quote/chatlist show "File"
        let reply = makeReply(attachments: [makeReplyAttachment(type: 2)])
        XCTAssertEqual(replyQuotePreviewText(for: reply), "File")
        XCTAssertEqual(chatListPreviewText(text: "", attachments: [makeAttachment(type: .file)]), "File")
    }

    func testAllContextsAgree_Avatar() {
        let msg = makeMessage(attachments: [makeAttachment(type: .avatar)])
        let reply = makeReply(attachments: [makeReplyAttachment(type: 3)])
        XCTAssertEqual(replyPreviewText(for: msg), "Avatar")
        XCTAssertEqual(replyQuotePreviewText(for: reply), "Avatar")
        XCTAssertEqual(chatListPreviewText(text: "", attachments: msg.attachments), "Avatar")
    }

    func testAllContextsAgree_Forward() {
        let msg = makeMessage(attachments: [makeAttachment(type: .messages)])
        let reply = makeReply(attachments: [makeReplyAttachment(type: 1)])
        XCTAssertEqual(replyPreviewText(for: msg), "Forwarded message")
        XCTAssertEqual(replyQuotePreviewText(for: reply), "Forwarded message")
        XCTAssertEqual(chatListPreviewText(text: "", attachments: msg.attachments), "Forwarded message")
    }

    func testAllContextsAgree_Call() {
        let msg = makeMessage(attachments: [makeAttachment(type: .call)])
        let reply = makeReply(attachments: [makeReplyAttachment(type: 4)])
        XCTAssertEqual(replyPreviewText(for: msg), "Call")
        XCTAssertEqual(replyQuotePreviewText(for: reply), "Call")
        XCTAssertEqual(chatListPreviewText(text: "", attachments: msg.attachments), "Call")
    }
}
