//
// Copyright 2025 Element Creations Ltd.
// Copyright 2022-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//

@testable import ElementX
import SwiftUI
import Testing

struct AttributedStringBuilderTests {
    private let attributedStringBuilder: AttributedStringBuilder
    private let maxHeaderPointSize = ceil(UIFont.preferredFont(forTextStyle: .body).pointSize * 1.2)
    
    init() async throws {
        attributedStringBuilder = AttributedStringBuilder(mentionBuilder: MentionBuilder())
    }
    
    @Test
    func renderHTMLStringWithHeaders() throws {
        let attributedString = try #require(attributedStringBuilder.fromHTML(HTMLFixtures.headers.rawValue), "Could not build the attributed string")
        
        #expect(String(attributedString.characters) == "H1 Header\nH2 Header\nH3 Header\nH4 Header\nH5 Header\nH6 Header")
        
        #expect(attributedString.runs.count == 4) // newlines hold no attributes
        
        let pointSizes = attributedString.runs.compactMap(\.uiKit.font?.pointSize)
        #expect(pointSizes == [23, 21, 19, 17])
    }
    
    @Test
    func renderHTMLStringWithPreCode() throws {
        let attributedString = try #require(attributedStringBuilder.fromHTML(HTMLFixtures.code.rawValue), "Could not build the attributed string")
        
        #expect(attributedString.runs.first?.uiKit.font?.fontName == ".AppleSystemUIFontMonospaced-Regular")
        
        let string = String(attributedString.characters)
        
        let regex = try #require(try? NSRegularExpression(pattern: "\\R", options: []), "Could not build the regex for the test.")
        
        #expect(regex.numberOfMatches(in: string, options: [], range: .init(location: 0, length: string.count)) == 23)
    }
    
    @Test
    func renderHTMLStringWithLink() throws {
        let attributedString = try #require(attributedStringBuilder.fromHTML(HTMLFixtures.links.rawValue), "Could not build the attributed string")
        
        #expect(String(attributedString.characters) == "Links too:\nMatrix rules! 🤘, beta.org, www.gamma.org, http://delta.org")
        
        let link = attributedString.runs.first { $0.link != nil }?.link
        
        #expect(link?.host == "www.alpha.org")
    }
    
    @Test
    func renderPlainStringWithLink() throws {
        let plainString = "This text contains a https://www.matrix.org link."
        
        let attributedString = try #require(attributedStringBuilder.fromPlain(plainString), "Could not build the attributed string")
        
        #expect(String(attributedString.characters) == plainString)
        
        #expect(attributedString.runs.count == 3)
        
        let link = attributedString.runs.first { $0.link != nil }?.link
        
        #expect(link?.host == "www.matrix.org")
    }
    
    @Test
    func punctuationAtTheEndOfPlainStringLinks() throws {
        let plainString = "Most punctuation marks are removed https://www.matrix.org:;., but closing brackets are kept https://example.com/(test)"
        
        let attributedString = try #require(attributedStringBuilder.fromPlain(plainString), "Could not build the attributed string")
        
        #expect(String(attributedString.characters) == plainString)
        
        #expect(attributedString.runs.count == 4)
        
        let firstLink = attributedString.runs.first { $0.link != nil }?.link
        #expect(firstLink == "https://www.matrix.org")
        let secondLink = attributedString.runs.last { $0.link != nil }?.link
        #expect(secondLink == "https://example.com/(test)")
    }
    
    @Test
    func linkDefaultScheme() throws {
        let plainString = "This text contains a matrix.org link."
        
        let attributedString = try #require(attributedStringBuilder.fromPlain(plainString), "Could not build the attributed string")
        
        #expect(String(attributedString.characters) == plainString)
        
        #expect(attributedString.runs.count == 3)
        
        let link = attributedString.runs.first { $0.link != nil }?.link
        
        #expect(link == "https://matrix.org")
    }
    
    @Test
    func mailToLinks() throws {
        let plainString = "Linking to email addresses like stefan@matrix.org should work as well"
        
        let attributedString = try #require(attributedStringBuilder.fromPlain(plainString), "Could not build the attributed string")
        
        let link = attributedString.runs.first { $0.link != nil }?.link
        #expect(link == "mailto:stefan@matrix.org")
    }
    
    @Test
    func renderHTMLStringWithLinkInHeader() throws {
        let h1HTMLString = "<h1><a href=\"https://matrix.org/\">Matrix.org</a></h1>"
        let h2HTMLString = "<h2><a href=\"https://matrix.org/\">Matrix.org</a></h2>"
        let h3HTMLString = "<h3><a href=\"https://matrix.org/\">Matrix.org</a></h3>"
        
        let h1AttributedString = try #require(attributedStringBuilder.fromHTML(h1HTMLString), "Could not build the attributed string")
        let h2AttributedString = try #require(attributedStringBuilder.fromHTML(h2HTMLString), "Could not build the attributed string")
        let h3AttributedString = try #require(attributedStringBuilder.fromHTML(h3HTMLString), "Could not build the attributed string")
        
        let h1Font = try #require(h1AttributedString.runs.first?.uiKit.font, "Could not extract a font from the strings.")
        let h2Font = try #require(h2AttributedString.runs.first?.uiKit.font, "Could not extract a font from the strings.")
        let h3Font = try #require(h3AttributedString.runs.first?.uiKit.font, "Could not extract a font from the strings.")
        
        #expect(String(h1AttributedString.characters) == "Matrix.org")
        #expect(String(h2AttributedString.characters) == "Matrix.org")
        #expect(String(h3AttributedString.characters) == "Matrix.org")
        
        #expect(h1AttributedString.runs.count == 1)
        #expect(h2AttributedString.runs.count == 1)
        #expect(h3AttributedString.runs.count == 1)
        
        #expect(h1Font == h2Font)
        #expect(h2Font == h3Font)
        
        #expect(h1Font.pointSize > UIFont.preferredFont(forTextStyle: .body).pointSize)
        #expect(h1Font.pointSize <= 23)
        
        #expect(h1AttributedString.runs.first?.link?.host == "matrix.org")
        #expect(h2AttributedString.runs.first?.link?.host == "matrix.org")
        #expect(h3AttributedString.runs.first?.link?.host == "matrix.org")
    }
    
    @Test
    func renderHTMLStringWithIFrame() throws {
        let htmlString = "<iframe src=\"https://www.matrix.org/\"></iframe>"
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        #expect(attributedString.uiKit.attachment == nil,
                "iFrame attachments should be removed as they're not included in the allowedHTMLTags array.")
    }
    
    @Test
    func linkWithFragment() throws {
        var string = "https://example.com/#/"
        try checkLinkIn(attributedString: attributedStringBuilder.fromHTML(string), expectedLink: "https://example.com", expectedRuns: 1)
        try checkLinkIn(attributedString: attributedStringBuilder.fromPlain(string), expectedLink: "https://example.com", expectedRuns: 1)
        
        string = "https://example.com/#/some_fragment/"
        try checkLinkIn(attributedString: attributedStringBuilder.fromHTML(string), expectedLink: "https://example.com/#/some_fragment", expectedRuns: 1)
        try checkLinkIn(attributedString: attributedStringBuilder.fromPlain(string), expectedLink: "https://example.com/#/some_fragment", expectedRuns: 1)
    }
    
    @Test
    func permalink() throws {
        let string = "https://matrix.to/#/!hello:matrix.org/$world?via=matrix.org"
        try checkLinkIn(attributedString: attributedStringBuilder.fromHTML(string), expectedLink: string, expectedRuns: 1)
        try checkLinkIn(attributedString: attributedStringBuilder.fromPlain(string), expectedLink: string, expectedRuns: 1)
    }
    
    @Test
    func matrixURI() throws {
        let string = "matrix:roomid/hello:matrix.org/e/world?via=matrix.org"
        try checkLinkIn(attributedString: attributedStringBuilder.fromHTML(string), expectedLink: string, expectedRuns: 1)
        try checkLinkIn(attributedString: attributedStringBuilder.fromPlain(string), expectedLink: string, expectedRuns: 1)
    }
    
    @Test
    func userIDLink() throws {
        let userID = "@user:matrix.org"
        let string = "The user is \(userID)."
        let expectedLink = "https://matrix.to/#/\(userID)"
        try checkLinkIn(attributedString: attributedStringBuilder.fromHTML(string), expectedLink: expectedLink, expectedRuns: 3)
        try checkLinkIn(attributedString: attributedStringBuilder.fromPlain(string), expectedLink: expectedLink, expectedRuns: 3)
    }
    
    @Test
    func roomAliasLink() throws {
        let roomAlias = "#room:matrix.org"
        let string = "The room is \(roomAlias)."
        let expectedLink = try #require(URL(string: "https://matrix.to/#/\(roomAlias)"), "The expected link should be valid.")
        try checkLinkIn(attributedString: attributedStringBuilder.fromHTML(string), expectedLink: expectedLink.absoluteString, expectedRuns: 3)
        try checkLinkIn(attributedString: attributedStringBuilder.fromPlain(string), expectedLink: expectedLink.absoluteString, expectedRuns: 3)
    }
    
    @Test
    func defaultFont() throws {
        let htmlString = "<b>Test</b> <i>string</i> "
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        #expect(attributedString.runs.count == 3)
    }
    
    @Test
    func defaultForegroundColor() throws {
        let htmlString = "<b>Test</b> <i>string</i> <a href=\"https://www.matrix.org/\">link</a> <code><a href=\"https://www.matrix.org/\">link</a></code>"
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        #expect(attributedString.runs.count == 7)
        
        for run in attributedString.runs {
            #expect(run.uiKit.foregroundColor == nil)
        }
    }
    
    @Test
    func customForegroundColor() throws {
        // swiftlint:disable:next line_length
        let htmlString = "<font color=\"#ff00be\">R</font><font color=\"#ff0082\">a</font><font color=\"#ff0047\">i</font><font color=\"#ff5800\">n </font><font color=\"#ffa300\">w</font><font color=\"#d2ba00\">w</font><font color=\"#97ca00\">w</font><font color=\"#3ed500\">.</font><font color=\"#00dd00\">m</font><font color=\"#00e251\">a</font><font color=\"#00e595\">t</font><font color=\"#00e7d6\">r</font><font color=\"#00e7ff\">i</font><font color=\"#00e6ff\">x</font><font color=\"#00e3ff\">.</font><font color=\"#00dbff\">o</font><font color=\"#00ceff\">r</font><font color=\"#00baff\">g</font><font color=\"#f477ff\"> b</font><font color=\"#ff3aff\">o</font><font color=\"#ff00fb\">w</font>"
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        #expect(attributedString.runs.count == 3)
        
        var foundLink = false
        // Foreground colors should be completely stripped from the attributed string
        // letting UI components chose the defaults (e.g. tintColor)
        for run in attributedString.runs {
            if run.link != nil {
                #expect(run.link?.host == "www.matrix.org")
                #expect(run.uiKit.foregroundColor == nil)
                foundLink = true
            } else {
                #expect(run.uiKit.foregroundColor == nil)
            }
        }
        
        #expect(foundLink)
    }
    
    @Test
    func singleBlockquote() throws {
        let htmlString = "<blockquote>Blockquote</blockquote><p>Another paragraph</p>"
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        #expect(attributedString.runs.count == 2)
        
        #expect(attributedString.formattedComponents.count == 2)
        
        for run in attributedString.runs where run.elementX.blockquote ?? false {
            return
        }
        
        Issue.record("Couldn't find blockquote")
        
        #expect(String(attributedString.characters) == "Blockquote\nAnother paragraph")
    }
    
    // swiftlint:disable line_length
    @Test
    func blockquoteWithinText() throws {
        let htmlString = """
        The text before the blockquote
        <blockquote> For 50 years, WWF has been protecting the future of nature. The world's leading conservation organization, WWF works in 100 countries and is supported by 1.2 million members in the United States and close to 5 million globally.</blockquote>
        The text after the blockquote
        """
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        #expect(attributedString.runs.count == 3)
        
        #expect(attributedString.formattedComponents.count == 3)
        
        for run in attributedString.runs where run.elementX.blockquote ?? false {
            return
        }
        
        Issue.record("Couldn't find blockquote")
    }
    
    // swiftlint:enable line_length
    
    @Test
    func blockquoteWithLink() throws {
        let htmlString = "<blockquote>Blockquote with a <a href=\"https://www.matrix.org/\">link</a> in it</blockquote>"
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        #expect(attributedString.runs.count == 3)
        
        let coalescedComponents = attributedString.formattedComponents
        
        #expect(coalescedComponents.count == 1)
        
        #expect(coalescedComponents.first?.attributedString.runs.count == 3, "Link not present in the component")
        
        var foundBlockquoteAndLink = false
        for run in attributedString.runs where run.elementX.blockquote ?? false && run.link != nil {
            foundBlockquoteAndLink = true
        }
        
        #expect(foundBlockquoteAndLink != nil, "Couldn't find blockquote or link")
    }
    
    @Test
    func replyBlockquote() throws {
        let htmlString = "<blockquote><a href=\"https://matrix.to/#/someroom/someevent\">In reply to</a> <a href=\"https://matrix.to/#/@user:matrix.org\">@user:matrix.org</a><br>The future is <code>swift run tools</code> 😎</blockquote>"
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        let coalescedComponents = attributedString.formattedComponents
        #expect(coalescedComponents.count == 1)
        
        let component = try #require(coalescedComponents.first, "Could not get the first component")
        
        #expect(component.type == .blockquote, "The reply quote should be a blockquote.")
    }
    
    @Test
    func multipleGroupedBlockquotes() throws {
        let attributedString = try #require(attributedStringBuilder.fromHTML(HTMLFixtures.groupedBlockQuotes.rawValue), "Could not build the attributed string")
        
        #expect(attributedString.runs.count == 11)
        #expect(attributedString.formattedComponents.count == 5)
        
        var numberOfBlockquotes = 0
        for run in attributedString.runs where run.elementX.blockquote ?? false && run.link != nil {
            numberOfBlockquotes += 1
        }
        
        #expect(numberOfBlockquotes == 3, "Couldn't find all the blockquotes")
    }
    
    @Test
    func multipleSeparatedBlockquotes() throws {
        let attributedString = try #require(attributedStringBuilder.fromHTML(HTMLFixtures.separatedBlockQuotes.rawValue), "Could not build the attributed string")
        
        let coalescedComponents = attributedString.formattedComponents
        
        #expect(attributedString.runs.count == 5)
        #expect(coalescedComponents.count == 5)
        
        var numberOfBlockquotes = 0
        for run in attributedString.runs where run.elementX.blockquote ?? false {
            numberOfBlockquotes += 1
        }
        
        #expect(numberOfBlockquotes == 2, "Couldn't find all the blockquotes")
    }
    
    @Test
    func userPermalinkMentionAtachment() {
        let string = "https://matrix.to/#/@test:matrix.org"
        let attributedStringFromHTML = attributedStringBuilder.fromHTML(string)
        #expect(attributedStringFromHTML?.attachment != nil)
        #expect(attributedStringFromHTML?.userID == "@test:matrix.org")
        #expect(attributedStringFromHTML?.link?.absoluteString == string)
        let attributedStringFromPlain = attributedStringBuilder.fromPlain(string)
        #expect(attributedStringFromPlain?.attachment != nil)
        #expect(attributedStringFromPlain?.userID == "@test:matrix.org")
        #expect(attributedStringFromPlain?.link?.absoluteString == string)
    }
    
    @Test
    func userIDMentionAtachment() {
        let string = "@test:matrix.org"
        let attributedStringFromHTML = attributedStringBuilder.fromHTML(string)
        #expect(attributedStringFromHTML?.attachment != nil)
        #expect(attributedStringFromHTML?.userID == "@test:matrix.org")
        #expect(attributedStringFromHTML?.link?.absoluteString == "https://matrix.to/#/@test:matrix.org")
        let attributedStringFromPlain = attributedStringBuilder.fromPlain(string)
        #expect(attributedStringFromPlain?.attachment != nil)
        #expect(attributedStringFromPlain?.userID == "@test:matrix.org")
        #expect(attributedStringFromPlain?.link?.absoluteString == "https://matrix.to/#/@test:matrix.org")
    }
    
    @Test
    func roomIDPermalinkMentionAttachment() {
        let string = "https://matrix.to/#/!test:matrix.org"
        let attributedStringFromHTML = attributedStringBuilder.fromHTML(string)
        #expect(attributedStringFromHTML?.attachment != nil)
        #expect(attributedStringFromHTML?.roomID == "!test:matrix.org")
        #expect(attributedStringFromHTML?.link?.absoluteString == string)
        let attributedStringFromPlain = attributedStringBuilder.fromPlain(string)
        #expect(attributedStringFromPlain?.attachment != nil)
        #expect(attributedStringFromHTML?.roomID == "!test:matrix.org")
        #expect(attributedStringFromPlain?.link?.absoluteString == string)
    }
    
    @Test
    func roomAliasPermalinkMentionAttachment() {
        let string = "https://matrix.to/#/#test:matrix.org"
        let attributedStringFromHTML = attributedStringBuilder.fromHTML(string)
        #expect(attributedStringFromHTML?.attachment != nil)
        #expect(attributedStringFromHTML?.roomAlias == "#test:matrix.org")
        #expect(attributedStringFromHTML?.link?.absoluteString == "https://matrix.to/#/%23test:matrix.org")
        let attributedStringFromPlain = attributedStringBuilder.fromPlain(string)
        #expect(attributedStringFromPlain?.attachment != nil)
        #expect(attributedStringFromHTML?.roomAlias == "#test:matrix.org")
        #expect(attributedStringFromPlain?.link?.absoluteString == "https://matrix.to/#/%23test:matrix.org")
    }
    
    @Test
    func roomAliasMentionAttachment() {
        let string = "#test:matrix.org"
        let attributedStringFromHTML = attributedStringBuilder.fromHTML(string)
        #expect(attributedStringFromHTML?.attachment != nil)
        #expect(attributedStringFromHTML?.roomAlias == "#test:matrix.org")
        #expect(attributedStringFromHTML?.link?.absoluteString == "https://matrix.to/#/%23test:matrix.org")
        let attributedStringFromPlain = attributedStringBuilder.fromPlain(string)
        #expect(attributedStringFromPlain?.attachment != nil)
        #expect(attributedStringFromHTML?.roomAlias == "#test:matrix.org")
        #expect(attributedStringFromPlain?.link?.absoluteString == "https://matrix.to/#/%23test:matrix.org")
    }
    
    @Test
    func eventRoomIDPermalinkMentionAttachment() {
        let string = "https://matrix.to/#/!test:matrix.org/$test"
        let attributedStringFromHTML = attributedStringBuilder.fromHTML(string)
        #expect(attributedStringFromHTML?.attachment != nil)
        #expect(attributedStringFromHTML?.eventOnRoomID == .some(.init(roomID: "!test:matrix.org", eventID: "$test")))
        #expect(attributedStringFromHTML?.link?.absoluteString == string)
        let attributedStringFromPlain = attributedStringBuilder.fromPlain(string)
        #expect(attributedStringFromPlain?.attachment != nil)
        #expect(attributedStringFromPlain?.eventOnRoomID == .some(.init(roomID: "!test:matrix.org", eventID: "$test")))
        #expect(attributedStringFromPlain?.link?.absoluteString == string)
    }
    
    @Test
    func eventRoomAliasPermalinkMentionAttachment() {
        let string = "https://matrix.to/#/#test:matrix.org/$test"
        let attributedStringFromHTML = attributedStringBuilder.fromHTML(string)
        #expect(attributedStringFromHTML?.attachment != nil)
        #expect(attributedStringFromHTML?.eventOnRoomAlias == .some(.init(alias: "#test:matrix.org", eventID: "$test")))
        #expect(attributedStringFromHTML?.link?.absoluteString == "https://matrix.to/#/%23test:matrix.org/$test")
        let attributedStringFromPlain = attributedStringBuilder.fromPlain(string)
        #expect(attributedStringFromPlain?.attachment != nil)
        #expect(attributedStringFromPlain?.eventOnRoomAlias == .some(.init(alias: "#test:matrix.org", eventID: "$test")))
        #expect(attributedStringFromPlain?.link?.absoluteString == "https://matrix.to/#/%23test:matrix.org/$test")
    }
    
    @Test
    func userMentionAtachmentInBlockQuotes() throws {
        let link = "https://matrix.to/#/@test:matrix.org"
        let string = "<blockquote>hello \(link) how are you?</blockquote>"
        let attributedStringFromHTML = try #require(attributedStringBuilder.fromHTML(string), "Attributed string is nil")
        
        for run in attributedStringFromHTML.runs {
            #expect(run.blockquote != nil)
        }
        
        try checkAttachment(attributedString: attributedStringFromHTML, expectedRuns: 3)
        try checkLinkIn(attributedString: attributedStringFromHTML, expectedLink: link, expectedRuns: 3)
    }
    
    @Test
    func allUsersMentionAtachmentInBlockQuotes() throws {
        let string = "<blockquote>hello @room how are you?</blockquote>"
        let attributedStringFromHTML = try #require(attributedStringBuilder.fromHTML(string), "Attributed string is nil")
        
        for run in attributedStringFromHTML.runs {
            #expect(run.blockquote != nil)
        }
        
        try checkAttachment(attributedString: attributedStringFromHTML, expectedRuns: 3)
    }
    
    @Test
    func allUsersMentionAttachment() throws {
        let string = "@room"
        let attributedStringFromHTML = attributedStringBuilder.fromHTML(string)
        try checkAttachment(attributedString: attributedStringFromHTML, expectedRuns: 1)
        let attributedStringFromPlain = attributedStringBuilder.fromPlain(string)
        try checkAttachment(attributedString: attributedStringFromPlain, expectedRuns: 1)
        
        let string2 = "Hello @room"
        let attributedStringFromHTML2 = attributedStringBuilder.fromHTML(string2)
        try checkAttachment(attributedString: attributedStringFromHTML2, expectedRuns: 2)
        let attributedStringFromPlain2 = attributedStringBuilder.fromPlain(string2)
        try checkAttachment(attributedString: attributedStringFromPlain2, expectedRuns: 2)
        
        let string3 = "Hello @room how are you doing?"
        let attributedStringFromHTML3 = attributedStringBuilder.fromHTML(string3)
        try checkAttachment(attributedString: attributedStringFromHTML3, expectedRuns: 3)
        let attributedStringFromPlain3 = attributedStringBuilder.fromPlain(string3)
        try checkAttachment(attributedString: attributedStringFromPlain3, expectedRuns: 3)
    }
    
    @Test
    func linksHavePriorityOverAllUserMention() throws {
        let string = "https://test@room.org"
        let attributedStringFromHTML = attributedStringBuilder.fromHTML(string)
        try checkLinkIn(attributedString: attributedStringFromHTML, expectedLink: string, expectedRuns: 1)
        let attributedStringFromPlain = attributedStringBuilder.fromPlain(string)
        try checkLinkIn(attributedString: attributedStringFromPlain, expectedLink: string, expectedRuns: 1)
        
        let string2 = "https://matrix.to/#/@roomusername:matrix.org"
        let attributedStringFromHTML2 = attributedStringBuilder.fromHTML(string2)
        try checkLinkIn(attributedString: attributedStringFromHTML2, expectedLink: string2, expectedRuns: 1)
        try checkAttachment(attributedString: attributedStringFromHTML2, expectedRuns: 1)
        let attributedStringFromPlain2 = attributedStringBuilder.fromPlain(string2)
        try checkLinkIn(attributedString: attributedStringFromPlain2, expectedLink: string2, expectedRuns: 1)
        try checkAttachment(attributedString: attributedStringFromPlain2, expectedRuns: 1)
    }
    
    @Test
    func uRLsAreIgnoredInCode() {
        var htmlString = "<pre><code>test https://matrix.org test</code></pre>"
        var attributedStringFromHTML = attributedStringBuilder.fromHTML(htmlString)
        #expect(attributedStringFromHTML?.runs.count == 1)
        #expect(attributedStringFromHTML?.link == nil)
        
        htmlString = "<pre><code>matrix.org</code></pre>"
        attributedStringFromHTML = attributedStringBuilder.fromHTML(htmlString)
        #expect(attributedStringFromHTML?.runs.count == 1)
        #expect(attributedStringFromHTML?.link == nil)
    }
    
    @Test
    func hyperlinksAreIgnoredInCode() {
        let htmlString = "<pre><code>test <a href=\"https://matrix.org\">matrix</a> test</code></pre>"
        let attributedStringFromHTML = attributedStringBuilder.fromHTML(htmlString)
        #expect(attributedStringFromHTML?.link == nil)
    }
    
    @Test
    func userMentionIsIgnoredInCode() {
        let htmlString = "<pre><code>test https://matrix.org/#/@test:matrix.org test</code></pre>"
        let attributedString = attributedStringBuilder.fromHTML(htmlString)
        
        #expect(attributedString?.runs.count == 1)
        
        #expect(attributedString?.attachment == nil)
    }
    
    @Test
    func plainTextUserMentionIsIgnoredInCode() {
        let htmlString = "<pre><code>Hey @some.user.ceriu:matrix.org</code></pre>"
        let attributedString = attributedStringBuilder.fromHTML(htmlString)
        
        #expect(attributedString?.runs.count == 1)
        
        #expect(attributedString?.attachment == nil)
    }
    
    @Test
    func allUsersIsIgnoredInCode() {
        let htmlString = "<pre><code>test @room test</code></pre>"
        let attributedString = attributedStringBuilder.fromHTML(htmlString)
        
        #expect(attributedString?.runs.count == 1)
        
        #expect(attributedString?.attachment == nil)
    }
    
    @Test
    func multipleMentions() throws {
        let url = try #require(URL(string: "https://matrix.to/#/@test:matrix.org"), "Invalid url")
        
        let string = "Hello @room, but especially hello to you \(url)"
        let attributedStringFromHTML = try #require(attributedStringBuilder.fromHTML(string), "Attributed string is nil")
        
        var foundAttachments = 0
        var foundLink: URL?
        for run in attributedStringFromHTML.runs {
            if run.attachment != nil {
                foundAttachments += 1
            }
            
            if let link = run.link {
                foundLink = link
            }
        }
        #expect(foundLink == url)
        #expect(foundAttachments == 2)
        
        let attributedStringFromPlain = try #require(attributedStringBuilder.fromPlain(string), "Attributed string is nil")
        
        foundAttachments = 0
        foundLink = nil
        for run in attributedStringFromPlain.runs {
            if run.attachment != nil {
                foundAttachments += 1
            }
            
            if let link = run.link {
                foundLink = link
            }
        }
        #expect(foundLink == url)
        #expect(foundAttachments == 2)
    }
    
    @Test
    func multipleMentions2() throws {
        let url = try #require(URL(string: "https://matrix.to/#/@test:matrix.org"), "Invalid url")
        
        let string = "\(url) @room"
        let attributedStringFromHTML = try #require(attributedStringBuilder.fromHTML(string), "Attributed string is nil")
        
        var foundAttachments = 0
        var foundLink: URL?
        for run in attributedStringFromHTML.runs {
            if run.attachment != nil {
                foundAttachments += 1
            }
            
            if let link = run.link {
                foundLink = link
            }
        }
        #expect(foundLink == url)
        #expect(foundAttachments == 2)
        
        let attributedStringFromPlain = try #require(attributedStringBuilder.fromPlain(string), "Attributed string is nil")
        
        foundAttachments = 0
        foundLink = nil
        for run in attributedStringFromPlain.runs {
            if run.attachment != nil {
                foundAttachments += 1
            }
            
            if let link = run.link {
                foundLink = link
            }
        }
        #expect(foundLink == url)
        #expect(foundAttachments == 2)
    }
    
    @Test
    func imageTags() throws {
        let htmlString = "Hey <img src=\"smiley.gif\" alt=\"Smiley face\">! How's work<img src=\"workplace.jpg\">?"
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        #expect(String(attributedString.characters) == "Hey [img: Smiley face]! How's work[img]?")
    }
    
    @Test
    func listTags() throws {
        let htmlString = "<p>like</p>\n<ul>\n<li>this<br />\ntest</li>\n</ul>\n"
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        #expect(String(attributedString.characters) == "like\n    • this\ntest")
    }
    
    @Test
    func unorderedList() throws {
        let htmlString = "<ul><li>1</li><li>2</li><li>3</li></ul>"
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        #expect(String(attributedString.characters) == "  • 1\n  • 2\n  • 3")
    }
    
    @Test
    func nestedUnorderedList() throws {
        let htmlString = "<ul><li>A<ul><li>A1</li><li>A2</li><li>A3</li></ul></li><li>B</li><li>C</li></ul>"
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        #expect(String(attributedString.characters) == "  • A\n      • A1\n      • A2\n      • A3\n  • B\n  • C")
    }
    
    @Test
    func orderedList() throws {
        let htmlString = "<ol><li>1</li><li>2</li><li>3</li></ol>"
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        #expect(String(attributedString.characters) == "  1. 1\n  2. 2\n  3. 3")
    }
    
    @Test
    func nestedOrderedList() throws {
        let htmlString = "<ol><li>A<ol><li>A1</li><li>A2</li><li>A3</li></ol></li><li>B</li><li>C</li></ol>"
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        #expect(String(attributedString.characters) == "  1. A\n      1. A1\n      2. A2\n      3. A3\n  2. B\n  3. C")
    }
    
    @Test
    func outOfOrderListNubmering() throws {
        let htmlString = "<ol start=\"2\">\n<li>this is a two</li>\n</ol>"
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        #expect(String(attributedString.characters) == "   2. this is a two")
    }
    
    @Test
    func nestedHeterogeneousLists() throws {
        let htmlString = "<ol><li>A<ul><li>A1</li><li>A2</li><li>A3</li></ul></li><li>B</li><li>C</li></ol>"
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        #expect(String(attributedString.characters) == "  1. A\n      • A1\n      • A2\n      • A3\n  2. B\n  3. C")
    }
    
    /// https://github.com/element-hq/element-x-ios/issues/4856
    @Test
    func normalisedWhitespaces() throws {
        let html = """
        <a href="https://github.com/stefan">Stefan</a>      pushed
                <a href="https://github.com">2 commits</a>
            to
         main:<ul>         <li>
                    <a href="https://github.com"><code>Some update</code></a>
                    
                </li>
                <li>
                    <a href="https://github.com"><code>Some other update</code></a>
                    
                </li>
         </ul>
        """
        let attributedString = try #require(attributedStringBuilder.fromHTML(html), "Could not build the attributed string")
        
        #expect(String(attributedString.characters) == "Stefan pushed 2 commits to main:\n   •  Some update \n   •  Some other update")
    }
    
    // MARK: - Phishing prevention
    
    @Test
    func phishingLink() throws {
        let htmlString = "Hey check the following link <a href=\"https://matrix.org\">https://element.io</a>"
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        #expect(String(attributedString.characters) == "Hey check the following link https://element.io")
        
        #expect(attributedString.runs.count == 2)
        
        let link = try #require(attributedString.runs.first { $0.link != nil }?.link, "Couldn't find the link")
        #expect(link.requiresConfirmation)
        #expect(link.confirmationParameters?.internalURL.absoluteString == "https://matrix.org")
        #expect(link.confirmationParameters?.displayString == "https://element.io")
    }
    
    @Test
    func validLink() throws {
        let htmlString = "Hey check the following <a href=\"https://matrix.org\">link</a>"
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        let link = try #require(attributedString.runs.first { $0.link != nil }?.link, "Couldn't find the link")
        #expect(!link.requiresConfirmation)
        #expect(link.absoluteString == "https://matrix.org")
    }
    
    @Test
    func validLinkWithRTLOverride() throws {
        let htmlString = "<a href=\"https://matrix.org\">\u{202E}https://matrix.org</a>"
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        let link = try #require(attributedString.runs.first { $0.link != nil }?.link, "Couldn't find the link")
        #expect(!link.requiresConfirmation)
        #expect(link.absoluteString == "https://matrix.org")
    }
    
    @Test
    func phishingUserID() throws {
        let htmlString = "Hey check the following user <a href=\"https://matrix.org\">@alice:matrix.org</a>"
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        #expect(String(attributedString.characters) == "Hey check the following user @alice:matrix.org")
        
        #expect(attributedString.runs.count == 2)
        
        let link = try #require(attributedString.runs.first { $0.link != nil }?.link, "Couldn't find the link")
        #expect(link.requiresConfirmation)
        #expect(link.confirmationParameters?.internalURL.absoluteString == "https://matrix.org")
        #expect(link.confirmationParameters?.displayString == "@alice:matrix.org")
    }
    
    @Test
    func validUserIDLink() throws {
        let htmlString = "Hey check the following user <a href=\"https://matrix.to/#/@alice:matrix.org\">@alice:matrix.org</a>"
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        try checkAttachment(attributedString: attributedString, expectedRuns: 2)
        
        let link = try #require(attributedString.runs.first { $0.link != nil }?.link, "Couldn't find the link")
        #expect(!link.requiresConfirmation)
        #expect(link.absoluteString == "https://matrix.to/#/@alice:matrix.org")
    }
    
    @Test
    func phishingUserIDWithAnotherUserIDPermalink() throws {
        let htmlString = "Hey check the following user <a href=\"https://matrix.to/#/@bob:matrix.org\">@alice:matrix.org</a>"
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        #expect(String(attributedString.characters) == "Hey check the following user @alice:matrix.org")
        
        #expect(attributedString.runs.count == 2)
        
        let link = try #require(attributedString.runs.first { $0.link != nil }?.link, "Couldn't find the link")
        #expect(link.requiresConfirmation)
        #expect(link.confirmationParameters?.internalURL.absoluteString == "https://matrix.to/#/@bob:matrix.org")
        #expect(link.confirmationParameters?.displayString == "@alice:matrix.org")
    }
    
    @Test
    func phishingUserIDWithDistractingCharacters() throws {
        let htmlString = "Hey check the following user <a href=\"https://matrix.org\">👉️ @alice:matrix.org</a>"
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        #expect(String(attributedString.characters) == "Hey check the following user 👉️ @alice:matrix.org")
        
        #expect(attributedString.runs.count == 2)
        
        let link = try #require(attributedString.runs.first { $0.link != nil }?.link, "Couldn't find the link")
        #expect(link.requiresConfirmation)
        #expect(link.confirmationParameters?.internalURL.absoluteString == "https://matrix.org")
        #expect(link.confirmationParameters?.displayString == "👉️ @alice:matrix.org")
    }
    
    @Test
    func phishingLinkWithDistractingCharacters() throws {
        let htmlString = "Hey check the following link <a href=\"https://matrix.org\">👉️ https://element.io</a>"
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        #expect(String(attributedString.characters) == "Hey check the following link 👉️ https://element.io")
        
        #expect(attributedString.runs.count == 2)
        
        let link = try #require(attributedString.runs.first { $0.link != nil }?.link, "Couldn't find the link")
        #expect(link.requiresConfirmation)
        #expect(link.confirmationParameters?.internalURL.absoluteString == "https://matrix.org")
        #expect(link.confirmationParameters?.displayString == "👉️ https://element.io")
    }
    
    @Test
    func validLinkWithDistractingCharacters() throws {
        let htmlString = "Hey check the following link <a href=\"https://element.io\">👉️ https://element.io</a>"
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        #expect(String(attributedString.characters) == "Hey check the following link 👉️ https://element.io")
        
        let link = try #require(attributedString.runs.first { $0.link != nil }?.link, "Couldn't find the link")
        
        #expect(!link.requiresConfirmation)
        #expect(link.absoluteString == "https://element.io")
    }
    
    @Test
    func phishingLinkWithFakeDotCharacter() throws {
        let htmlString = "Hey check the following link <a href=\"https://matrix.org\">https://element﹒io</a>"
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        #expect(String(attributedString.characters) == "Hey check the following link https://element﹒io")
        
        #expect(attributedString.runs.count == 2)
        
        let link = try #require(attributedString.runs.first { $0.link != nil }?.link, "Couldn't find the link")
        #expect(link.requiresConfirmation)
        #expect(link.confirmationParameters?.internalURL.absoluteString == "https://matrix.org")
        #expect(link.confirmationParameters?.displayString == "https://element﹒io")
    }
    
    @Test
    func phishingMatrixPermalinks() throws {
        let htmlString = "Hey check the following room <a href=\"https://matrix.to/#/#offensive-room:matrix.org\">https://matrix.to/#/#beautiful-room:matrix.org</a>"
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        #expect(attributedString.runs.count == 2)
        
        let link = try #require(attributedString.runs.first { $0.link != nil }?.link, "Couldn't find the link")
        
        #expect(link.requiresConfirmation)
        #expect(link.confirmationParameters?.internalURL.absoluteString == "https://matrix.to/#/%23offensive-room:matrix.org")
        #expect(link.confirmationParameters?.displayString == "https://matrix.to/#/#beautiful-room:matrix.org")
    }
    
    @Test
    func validMatrixPermalinks() throws {
        let htmlString = "Hey check the following room <a href=\"https://matrix.to/#/#beautiful-room:matrix.org\">https://matrix.to/#/#beautiful-room:matrix.org</a>"
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        try checkAttachment(attributedString: attributedString, expectedRuns: 2)
        let link = try #require(attributedString.runs.first { $0.link != nil }?.link, "Couldn't find the link")
        
        #expect(!link.requiresConfirmation)
        #expect(link.absoluteString == "https://matrix.to/#/%23beautiful-room:matrix.org")
    }
    
    @Test
    func phishingRoomAlias() throws {
        let htmlString = "Hey check the following room <a href=\"https://matrix.org\">#room:matrix.org</a>"
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        #expect(String(attributedString.characters) == "Hey check the following room #room:matrix.org")
        
        #expect(attributedString.runs.count == 2)
        
        let link = try #require(attributedString.runs.first { $0.link != nil }?.link, "Couldn't find the link")
        #expect(link.requiresConfirmation)
        #expect(link.confirmationParameters?.internalURL.absoluteString == "https://matrix.org")
        #expect(link.confirmationParameters?.displayString == "#room:matrix.org")
    }
    
    @Test
    func validRoomAliasLink() throws {
        let htmlString = "Hey check the following user <a href=\"https://matrix.to/#/#room:matrix.org\">#room:matrix.org</a>"
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        try checkAttachment(attributedString: attributedString, expectedRuns: 2)
        
        let link = try #require(attributedString.runs.first { $0.link != nil }?.link, "Couldn't find the link")
        #expect(!link.requiresConfirmation)
        #expect(link.absoluteString == "https://matrix.to/#/%23room:matrix.org")
    }
    
    @Test
    func phishingRoomAliasWithAnotherRoomAliasPermalink() throws {
        let htmlString = "Hey check the following room <a href=\"https://matrix.to/#/#another-room:matrix.org\">#room:matrix.org</a>"
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        #expect(String(attributedString.characters) == "Hey check the following room #room:matrix.org")
        
        #expect(attributedString.runs.count == 2)
        
        let link = try #require(attributedString.runs.first { $0.link != nil }?.link, "Couldn't find the link")
        #expect(link.requiresConfirmation)
        #expect(link.confirmationParameters?.internalURL.absoluteString == "https://matrix.to/#/%23another-room:matrix.org")
        #expect(link.confirmationParameters?.displayString == "#room:matrix.org")
    }
    
    @Test
    func roomAliasWithDistractingCharacters() throws {
        let htmlString = "Hey check the following user <a href=\"https://matrix.org\">👉️ #room:matrix.org</a>"
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        #expect(String(attributedString.characters) == "Hey check the following user 👉️ #room:matrix.org")
        
        #expect(attributedString.runs.count == 2)
        
        let link = try #require(attributedString.runs.first { $0.link != nil }?.link, "Couldn't find the link")
        #expect(link.requiresConfirmation)
        #expect(link.confirmationParameters?.internalURL.absoluteString == "https://matrix.org")
        #expect(link.confirmationParameters?.displayString == "👉️ #room:matrix.org")
    }
    
    @Test
    func mxExternalPaymentDetailsRemoved() throws {
        var htmlString = "This is visible.<span data-msc4286-external-payment-details> But this is hidden <a href=\"https://matrix.org\">and this link too</a></span>"
        
        let attributedString = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        #expect(String(attributedString.characters) == "This is visible.")
        
        for run in attributedString.runs where run.link != nil {
            Issue.record("No link expected, but found one")
            return
        }
        
        htmlString = "This is visible.<span> And this text <a href=\"https://matrix.org\">and link</a> are visible too.</span>"
        
        let attributedString2 = try #require(attributedStringBuilder.fromHTML(htmlString), "Could not build the attributed string")
        
        #expect(String(attributedString2.characters) == "This is visible. And this text and link are visible too.")
        
        try #require(attributedString2.runs.first { $0.link != nil }?.link, "Couldn't find the link")
    }
    
    // MARK: - Private
    
    private func checkLinkIn(attributedString: AttributedString?, expectedLink: String, expectedRuns: Int) throws {
        let attributedString = try #require(attributedString, "Could not build the attributed string")
        
        #expect(attributedString.runs.count == expectedRuns)
        
        for run in attributedString.runs where run.link != nil {
            #expect(run.link?.absoluteString == expectedLink)
            return
        }
        
        Issue.record("Couldn't find expected value.")
    }
    
    private func checkAttachment(attributedString: AttributedString?, expectedRuns: Int) throws {
        let attributedString = try #require(attributedString, "Could not build the attributed string")
        
        #expect(attributedString.runs.count == expectedRuns)
        
        for run in attributedString.runs where run.attachment != nil {
            return
        }
        
        Issue.record("Couldn't find expected value.")
    }
}
