// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL

#include <QObject>
#include <QTest>
#include <QTextDocumentFragment>

#include "texthandler.h"

#include <Quotient/quotient_common.h>
#include <Quotient/syncdata.h>

#include <Kirigami/Platform/PlatformTheme>

#include "block.h"
#include "enums/blocktype.h"
#include "models/customemojimodel.h"
#include "neochatconnection.h"

#include "testutils.h"

using namespace Quotient;

class TextHandlerTest : public QObject
{
    Q_OBJECT

private:
    Connection *connection = nullptr;
    TestUtils::TestRoom *room = nullptr;

private Q_SLOTS:
    void initTestCase();

    void allowedAttributes();
    void stripDisallowedTags();
    void stripDisallowedAttributes();
    void emptyCodeTags();
    void addStyle_data();
    void addStyle();
    void dontAddStyle_data();
    void dontAddStyle();

    void sendSimpleStringCase();
    void sendSingleParaMarkup();
    void sendMultipleSectionMarkup();
    void sendBadLinks();
    void sendEscapeCode();
    void sendCodeClass();
    void sendCustomEmoji();
    void sendCustomEmojiCode_data();
    void sendCustomEmojiCode();
    void sendCustomTags_data();
    void sendCustomTags();
    void sendItalic_data();
    void sendItalic();
    void sendPlainLink_data();
    void sendPlainLink();

    void receiveSpacelessSelfClosingTag();
    void receiveStripReply();
    void receivePlainTextIn();

    void receiveRichInPlainOut_data();
    void receiveRichInPlainOut();
    void receivePlainStripHtml();
    void receivePlainStripMarkup();
    void receiveStripNewlines();

    void receiveRichUserPill();
    void receiveRichStrikethrough();
    void receiveRichtextIn();
    void receiveRichMxcUrl();
    void receiveRichPlainUrl_data();
    void receiveRichPlainUrl();
    void receiveRichEdited_data();
    void receiveRichEdited();
    void receiveLineSeparator();
    void receiveRichCodeUrl();
    void receiveRichColor();
    void receiveRichCustomEmoji();
    void receiveRichCustomEmojiNoTitle();
    void receiveEmote();

    void componentOutput_data();
    void componentOutput();

    void updateSpoiler_data();
    void updateSpoiler();

    void stripMatrixLinks_data();
    void stripMatrixLinks();
};

void TextHandlerTest::initTestCase()
{
    connection = Connection::makeMockConnection(u"@bob:kde.org"_s);
    connection->setAccountData(u"im.ponies.user_emotes"_s,
                               QJsonObject{{"images"_L1,
                                            QJsonObject{{"test"_L1,
                                                         QJsonObject{{"body"_L1, "Test custom emoji"_L1},
                                                                     {"url"_L1, "mxc://example.org/test"_L1},
                                                                     {"usage"_L1, QJsonArray{"emoticon"_L1}}}}}}});
    CustomEmojiModel::instance().setConnection(static_cast<NeoChatConnection *>(connection));

    room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, u"test-texthandler-sync.json"_s);
}

void TextHandlerTest::allowedAttributes()
{
    auto theme = static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
    const QString testInputString1 = u"<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>"_s;
    const QString testOutputString1S = u"<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>"_s;
    const QString testOutputString1R = u"<span data-mx-spoiler style=\"color: transparent; background: %1;\"><font color=#FFFFFF>Test</font><span>"_s.arg(
        theme->alternateBackgroundColor().name());
    // Handle urls where the href has either single (') or double (") quotes.
    const QString testInputString2 = u"<a href=\"https://kde.org\">link</a><a href='https://kde.org'>link</a>"_s;
    const QString testOutputString2S = u"<a href=\"https://kde.org\">link</a><a href='https://kde.org'>link</a>"_s;
    const QString testOutputString2R =
        u"<a href=\"https://kde.org\" style=\"text-decoration: none;\">link</a><a href='https://kde.org' style=\"text-decoration: none;\">link</a>"_s;

    TextHandler testTextHandler;
    testTextHandler.setData(testInputString1);

    QCOMPARE(testTextHandler.handleSendText(), testOutputString1S);
    QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString1R);

    testTextHandler.setData(testInputString2);
    QCOMPARE(testTextHandler.handleSendText(), testOutputString2S);
    QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString2R);
}

void TextHandlerTest::stripDisallowedTags()
{
    const QString testInputString = u"<p>Allowed</p> <span>Allowed</span> <body>Disallowed</body>"_s;
    const QString testOutputString = u"<p>Allowed</p> <span>Allowed</span> Disallowed"_s;

    TextHandler testTextHandler;
    testTextHandler.setData(testInputString);

    QCOMPARE(testTextHandler.handleSendText(), testOutputString);
    QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}

void TextHandlerTest::stripDisallowedAttributes()
{
    const QString testInputString = u"<p style=\"font-size:50px;\" color=#FFFFFF>Test</p>"_s;
    const QString testOutputString = u"Test"_s;

    TextHandler testTextHandler;
    testTextHandler.setData(testInputString);

    QCOMPARE(testTextHandler.handleSendText(), testOutputString);
    QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}

/**
 * Make sure that empty code tags are handled.
 * (this was a bug during development hence the test)
 */
void TextHandlerTest::emptyCodeTags()
{
    const QString testInputString = u"<pre><code></code></pre>"_s;
    const QString testOutputString = u"<pre><code></code></pre>"_s;

    TextHandler testTextHandler;
    testTextHandler.setData(testInputString);

    QCOMPARE(testTextHandler.handleSendText(), testOutputString);
    QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}

void TextHandlerTest::addStyle_data()
{
    QTest::addColumn<QString>("testInputString");
    QTest::addColumn<QString>("testOutputString");

    QTest::newRow("link") << u"<a href=\"https://kde.org\">link</a>"_s << u"<a href=\"https://kde.org\" style=\"text-decoration: none;\">link</a>"_s;
    QTest::newRow("table")
        << u"<table><tr><th>Company</th><th>Contact</th><th>Country</th></tr><tr><td>Alfreds Futterkiste</td><td>Maria Anders</td><td>Germany</td></tr><tr><td>Centro comercial Moctezuma</td><td>Francisco Chang</td><td>Mexico</td></tr></table>"_s
        << u"<table style=\"width: 100%; border-collapse: collapse; border: 1px; border-style: solid;\"><tr><th style=\"border: 1px solid black; padding: 3px;\">Company</th><th style=\"border: 1px solid black; padding: 3px;\">Contact</th><th style=\"border: 1px solid black; padding: 3px;\">Country</th></tr><tr><td style=\"border: 1px solid black; padding: 3px;\">Alfreds Futterkiste</td><td style=\"border: 1px solid black; padding: 3px;\">Maria Anders</td><td style=\"border: 1px solid black; padding: 3px;\">Germany</td></tr><tr><td style=\"border: 1px solid black; padding: 3px;\">Centro comercial Moctezuma</td><td style=\"border: 1px solid black; padding: 3px;\">Francisco Chang</td><td style=\"border: 1px solid black; padding: 3px;\">Mexico</td></tr></table>"_s;
    auto theme = static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
    QTest::newRow("spoiler") << u"<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>"_s
                             << u"<span data-mx-spoiler style=\"color: transparent; background: %1;\"><font color=#FFFFFF>Test</font><span>"_s.arg(
                                    theme->alternateBackgroundColor().name());
}

void TextHandlerTest::addStyle()
{
    QFETCH(QString, testInputString);
    QFETCH(QString, testOutputString);

    TextHandler testTextHandler;
    testTextHandler.setData(testInputString);

    QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}

void TextHandlerTest::dontAddStyle_data()
{
    QTest::addColumn<QString>("testInputString");
    QTest::addColumn<QString>("testOutputString");

    QTest::newRow("link") << u"<a href=\"https://kde.org\">link</a>"_s << u"<a href=\"https://kde.org\">link</a>"_s;
    QTest::newRow("table")
        << u"<table><tr><th>Company</th><th>Contact</th><th>Country</th></tr><tr><td>Alfreds Futterkiste</td><td>Maria Anders</td><td>Germany</td></tr><tr><td>Centro comercial Moctezuma</td><td>Francisco Chang</td><td>Mexico</td></tr></table>"_s
        << u"<table><tr><th>Company</th><th>Contact</th><th>Country</th></tr><tr><td>Alfreds Futterkiste</td><td>Maria Anders</td><td>Germany</td></tr><tr><td>Centro comercial Moctezuma</td><td>Francisco Chang</td><td>Mexico</td></tr></table>"_s;
    QTest::newRow("spoiler") << u"<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>"_s
                             << u"<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>"_s;
}

void TextHandlerTest::dontAddStyle()
{
    QFETCH(QString, testInputString);
    QFETCH(QString, testOutputString);

    TextHandler testTextHandler;
    testTextHandler.setData(testInputString);

    QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}

void TextHandlerTest::sendSimpleStringCase()
{
    const QString testInputString = u"This data should just be left alone."_s;
    const QString testOutputString = u"This data should just be left alone."_s;

    TextHandler testTextHandler;
    testTextHandler.setData(testInputString);

    QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}

void TextHandlerTest::sendSingleParaMarkup()
{
    const QString testInputString =
        u"Text para with **bold**, *italic*, [link](https://kde.org), ![image](mxc://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e), `inline code`."_s;
    const QString testOutputString =
        u"Text para with <strong>bold</strong>, <em>italic</em>, <a href=\"https://kde.org\">link</a>, <img src=\"mxc://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e\" alt=\"image\">, <code>inline code</code>."_s;

    TextHandler testTextHandler;
    testTextHandler.setData(testInputString);

    QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}

void TextHandlerTest::sendMultipleSectionMarkup()
{
    const QString testInputString =
        u"Text para\n> blockquote\n* List 1\n* List 2\n1. one\n2. two\n# Heading 1\n## Heading 2\nhorizontal rule\n\n---\n```\ncodeblock\n```"_s;
    const QString testOutputString =
        u"<p>Text para</p>\n<blockquote>\n<p>blockquote</p>\n</blockquote>\n<ul>\n<li>List 1</li>\n<li>List 2</li>\n</ul>\n<ol>\n<li>one</li>\n<li>two</li>\n</ol>\n<h1>Heading 1</h1>\n<h2>Heading 2</h2>\n<p>horizontal rule</p>\n<hr>\n<pre><code>codeblock\n</code></pre>"_s;

    TextHandler testTextHandler;
    testTextHandler.setData(testInputString);

    QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}

void TextHandlerTest::sendBadLinks()
{
    const QString testInputString = u"[link](kde.org), ![image](https://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e)"_s;
    const QString testOutputString = u"<a>link</a>, <img alt=\"image\">"_s;

    TextHandler testTextHandler;
    testTextHandler.setData(testInputString);

    QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}

/**
 * All text between code tags is treated as plain so it should get escaped.
 */
void TextHandlerTest::sendEscapeCode()
{
    const QString testInputString = u"```\n<p>Test <span style=\"font-size:50px;\">some</span> code</p>\n```"_s;
    const QString testOutputString =
        u"<pre><code>&lt;p&gt;Test &lt;span style=&quot;font-size:50px;&quot;&gt;some&lt;/span&gt; code&lt;/p&gt;\n</code></pre>"_s;

    TextHandler testTextHandler;
    testTextHandler.setData(testInputString);

    QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}

void TextHandlerTest::sendCodeClass()
{
    const QString testInputString = u"```html\nsome code\n```\n<pre><code class=\"code-underline\">some more code</code></pre>"_s;
    const QString testOutputString = u"<pre><code class=\"language-html\">some code\n</code></pre>\n<pre><code>some more code</code></pre>"_s;

    TextHandler testTextHandler;
    testTextHandler.setData(testInputString);

    QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}

void TextHandlerTest::sendCustomEmoji()
{
    const QString testInputString = u":test:"_s;
    const QString testOutputString =
        u"<img data-mx-emoticon=\"\" src=\"mxc://example.org/test\" alt=\":test:\" title=\":test:\" height=\"32\" vertical-align=\"middle\" />"_s;

    TextHandler testTextHandler;
    testTextHandler.setData(testInputString);

    QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}

void TextHandlerTest::sendCustomEmojiCode_data()
{
    QTest::addColumn<QString>("testInputString");
    QTest::addColumn<QString>("testOutputString");

    QTest::newRow("inline") << u"`:test:`"_s << u"<code>:test:</code>"_s;
    QTest::newRow("block") << u"```\n:test:\n```"_s << u"<pre><code>:test:\n</code></pre>"_s;
}

// Custom emojis in code blocks should be left alone.
void TextHandlerTest::sendCustomEmojiCode()
{
    QFETCH(QString, testInputString);
    QFETCH(QString, testOutputString);

    TextHandler testTextHandler;
    testTextHandler.setData(testInputString);

    QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}

void TextHandlerTest::sendCustomTags_data()
{
    QTest::addColumn<QString>("testInputString");
    QTest::addColumn<QString>("testOutputString");

    // spoiler
    QTest::newRow("incomplete spoiler") << u"||test"_s << u"||test"_s;
    QTest::newRow("complete spoiler") << u"||test||"_s << u"<span data-mx-spoiler>test</span>"_s;
    QTest::newRow("multiple spoiler") << u"||apple||banana||pear||"_s << u"<span data-mx-spoiler>apple</span>banana<span data-mx-spoiler>pear</span>"_s;
    QTest::newRow("inside code block spoiler") << u"```||apple||```"_s << u"<code>||apple||</code>"_s;
    QTest::newRow("outside code block spoiler") << u"||apple|| ```||banana||``` ||pear||"_s
                                                << u"<span data-mx-spoiler>apple</span> <code>||banana||</code> <span data-mx-spoiler>pear</span>"_s;
    QTest::newRow("complex spoiler") << u"Between `formFactor == Horizontal||Vertical` and `location == top||left||bottom||right`"_s
                                     << u"Between <code>formFactor == Horizontal||Vertical</code> and <code>location == top||left||bottom||right</code>"_s;

    // strikethrough
    QTest::newRow("incomplete strikethrough") << u"~~test"_s << u"~~test"_s;
    QTest::newRow("complete strikethrough") << u"~~test~~"_s << u"<del>test</del>"_s;
    QTest::newRow("inside code block strikethrough") << u"```~~apple~~```"_s << u"<code>~~apple~~</code>"_s;
    QTest::newRow("outside code block strikethrough") << u"~~apple~~ ```~~banana~~``` ~~pear~~"_s
                                                      << u"<del>apple</del> <code>~~banana~~</code> <del>pear</del>"_s;
}

void TextHandlerTest::sendCustomTags()
{
    QFETCH(QString, testInputString);
    QFETCH(QString, testOutputString);

    TextHandler testTextHandler;
    testTextHandler.setData(testInputString);

    QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}

void TextHandlerTest::sendItalic_data()
{
    QTest::addColumn<QString>("testInputString");
    QTest::addColumn<QString>("testOutputString");

    QTest::newRow("usual case") << u"_going fast_"_s << u"<u>going fast</u>"_s;
    QTest::newRow("unusual underscores that aren't underlines") << u"XDG_DATA_DIRS"_s << u"XDG_DATA_DIRS"_s;
    QTest::newRow("one underscore") << u"a_a"_s << u"a_a"_s;
    QTest::newRow("surrounding underscores") << u"_XDG_DATA_DIRS_"_s << u"<u>XDG_DATA_DIRS</u>"_s;
    QTest::newRow("surrounding underline and underscores") << u"~~_XDG_DATA_DIRSXDG_DATA_DIRS_~~"_s << u"<del><u>XDG_DATA_DIRSXDG_DATA_DIRS</u></del>"_s;
    QTest::newRow("inline code")
        << u"my `m_dataBuffer` is really good because in `XDG_DATA_DIRS` it's not considered _unless_ it's a _XDG_DATA_DIRS_"_s
        << u"my <code>m_dataBuffer</code> is really good because in <code>XDG_DATA_DIRS</code> it's not considered <u>unless</u> it's a <u>XDG_DATA_DIRS</u>"_s;
    QTest::newRow("codeblock") << u"```m_dataBuffer XDG_DATA_DIR```"_s << u"<code>m_dataBuffer XDG_DATA_DIR</code>"_s;
}

void TextHandlerTest::sendItalic()
{
    QFETCH(QString, testInputString);
    QFETCH(QString, testOutputString);

    TextHandler testTextHandler;
    testTextHandler.setData(testInputString);

    QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}

void TextHandlerTest::sendPlainLink_data()
{
    QTest::addColumn<QString>("testInputString");
    QTest::addColumn<QString>("testOutputString");

    QTest::newRow("simple") << u"https://kde.org/"_s << u"https://kde.org/"_s;
    QTest::newRow("double underscore")
        << u"https://bugs.kde.org/buglist.cgi?bug_status=__open__&amp;component=general&amp;list_id=3150863&amp;product=plasma-systemmonitor"_s
        << u"https://bugs.kde.org/buglist.cgi?bug_status=__open__&amp;component=general&amp;list_id=3150863&amp;product=plasma-systemmonitor"_s;
}

void TextHandlerTest::sendPlainLink()
{
    QFETCH(QString, testInputString);
    QFETCH(QString, testOutputString);

    TextHandler testTextHandler;
    testTextHandler.setData(testInputString);

    QCOMPARE(testTextHandler.handleSendText(), testOutputString);
}

void TextHandlerTest::receiveSpacelessSelfClosingTag()
{
    const QString testInputString = u"Test...<br/>...ing"_s;
    const QString testRichOutputString = u"Test...<br/>...ing"_s;
    const QString testPlainOutputString = u"Test...\n...ing"_s;

    TextHandler testTextHandler;
    testTextHandler.setData(testInputString);

    QCOMPARE(testTextHandler.handleRecieveRichText(), testRichOutputString);
    QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText), testPlainOutputString);
}

void TextHandlerTest::receiveStripReply()
{
    const QString testInputString =
        u"<mx-reply><blockquote><a href=\"https://matrix.to/#/!somewhere:example.org/$event:example.org\">In reply to</a><a href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a><br />Message replied to.</blockquote></mx-reply>Reply message."_s;
    const QString testOutputString = u"Reply message."_s;

    TextHandler testTextHandler;
    testTextHandler.setData(testInputString);

    QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
    QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputString);
}

void TextHandlerTest::receiveRichInPlainOut_data()
{
    QTest::addColumn<QString>("testInputString");
    QTest::addColumn<QString>("testOutputString");

    QTest::newRow("ampersand") << u"a &amp; b"_s << u"a & b"_s;
    QTest::newRow("quote") << u"&quot;a and b&quot;"_s << u"\"a and b\""_s;
    QTest::newRow("new line") << u"new<br>line"_s << u"new\nline"_s;
    QTest::newRow("unescape") << u"can&#x27;t"_s << u"can't"_s;
}

void TextHandlerTest::receiveRichInPlainOut()
{
    QFETCH(QString, testInputString);
    QFETCH(QString, testOutputString);

    TextHandler testTextHandler;
    testTextHandler.setData(testInputString);

    QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText), testOutputString);
}

void TextHandlerTest::receivePlainTextIn()
{
    const QString testInputString = u"<plain text in tag bracket>\nTest link https://kde.org."_s;
    const QString testOutputStringRich =
        u"&lt;plain text in tag bracket&gt;<br>Test link <a href=\"https://kde.org\" style=\"text-decoration: none;\">https://kde.org</a>."_s;
    QString testOutputStringPlain = u"<plain text in tag bracket>\nTest link https://kde.org."_s;

    // Make sure quotes are maintained in a plain string.
    const QString testInputString2 = u"last line is \"Time to switch to a new topic.\""_s;
    const QString testOutputString2 = u"last line is \"Time to switch to a new topic.\""_s;

    TextHandler testTextHandler;
    testTextHandler.setData(testInputString);

    QCOMPARE(testTextHandler.handleRecieveRichText(Qt::PlainText), testOutputStringRich);
    QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputStringPlain);

    testTextHandler.setData(testInputString2);
    QCOMPARE(testTextHandler.handleRecieveRichText(Qt::PlainText), testOutputString2);
    QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputString2);
}

void TextHandlerTest::receiveStripNewlines()
{
    const QString testInputStringPlain = u"Test\nmany\nnew\nlines."_s;
    const QString testInputStringRich = u"Test<br>many<br />new<br>lines."_s;
    const QString testOutputString = u"Test many new lines."_s;

    const QString testInputStringPlain2 = u"* List\n* Items"_s;
    const QString testOutputString2 = u"List Items"_s;

    TextHandler testTextHandler;
    testTextHandler.setData(testInputStringPlain);

    QCOMPARE(testTextHandler.handleRecievePlainText(Qt::PlainText, true), testOutputString);
    QCOMPARE(testTextHandler.handleRecieveRichText(Qt::PlainText, nullptr, nullptr, true), testOutputString);

    testTextHandler.setData(testInputStringRich);
    QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText, true), testOutputString);
    QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, nullptr, nullptr, true), testOutputString);

    testTextHandler.setData(testInputStringPlain2);
    QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText, true), testOutputString2);
}

/**
 * For a plain text output of a received string all html is stripped except for
 * code which is unescaped if it's html.
 */
void TextHandlerTest::receivePlainStripHtml()
{
    const QString testInputString = u"<p>Test</p> <pre><code>Some code <strong>with tags</strong></code></pre>"_s;
    const QString testOutputString = u"Test Some code <strong>with tags</strong>"_s;

    TextHandler testTextHandler;
    testTextHandler.setData(testInputString);

    QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText), testOutputString);
}

void TextHandlerTest::receivePlainStripMarkup()
{
    const QString testInputString = u"**bold** `<p>inline code</p>` *italic*"_s;
    const QString testOutputString = u"bold <p>inline code</p> italic"_s;

    TextHandler testTextHandler;
    testTextHandler.setData(testInputString);

    QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputString);
}

void TextHandlerTest::receiveRichUserPill()
{
    const QString testInputString = u"<p><a href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a></p>"_s;
    const QString testOutputString = u"<b><a href=\"https://matrix.to/#/@alice:example.org\" style=\"text-decoration: none;\">@alice:example.org</a></b>"_s;

    TextHandler testTextHandler;
    testTextHandler.setData(testInputString);

    QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}

void TextHandlerTest::receiveRichStrikethrough()
{
    const QString testInputString = u"<p><del>Test</del></p>"_s;
    const QString testOutputString = u"<s>Test</s>"_s;

    TextHandler testTextHandler;
    testTextHandler.setData(testInputString);

    QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}

void TextHandlerTest::receiveRichtextIn()
{
    const QString testInputString = u"<p>Test</p> <pre><code>Some code <strong>with tags</strong></code></pre>"_s;
    const QString testOutputString = u"<p>Test</p> <pre><code>Some code &lt;strong&gt;with tags&lt;/strong&gt;</code></pre>"_s;

    TextHandler testTextHandler;
    testTextHandler.setData(testInputString);

    QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}

void TextHandlerTest::receiveRichMxcUrl()
{
    const QString testInputString =
        u"<img src=\"mxc://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e\" alt=\"image\"><img src=\"mxc://kde.org/34c3464b3a1bd7f55af2d559e07d2c773c430e73\" alt=\"image\">"_s;
    const QString testOutputString =
        u"<img  src=\"mxc://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e?user_id=@bob:kde.org&room_id=%23myroom:kde.org&event_id=$143273582443PhrSn:example.org\" alt=\"image\"><img  src=\"mxc://kde.org/34c3464b3a1bd7f55af2d559e07d2c773c430e73?user_id=@bob:kde.org&room_id=%23myroom:kde.org&event_id=$143273582443PhrSn:example.org\" alt=\"image\">"_s;

    TextHandler testTextHandler;
    testTextHandler.setData(testInputString);

    QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, room->messageEvents().at(0).get()), testOutputString);
}

void TextHandlerTest::receiveRichPlainUrl_data()
{
    QTest::addColumn<QString>("input");
    QTest::addColumn<QString>("output");

    // This is an actual link that caused trouble which is why it's so long. Keeping
    // so we can confirm consistent behaviour for complex urls.
    QTest::addRow("link 1")
        << u"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im <a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im\">Link already rich</a>"_s
        << u"<a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im\" style=\"text-decoration: none;\">https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im</a> <a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im\" style=\"text-decoration: none;\">Link already rich</a>"_s;

    // Another real case. The linkification wasn't handling it when a single link
    // contains what looks like and email. It was broken into 3 but needs to
    // be just single link.
    QTest::addRow("link 2")
        << u"https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/"_s
        << u"<a href=\"https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/\" style=\"text-decoration: none;\">https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/</a>"_s;

    QTest::addRow("email")
        << uR"(email@example.com <a href="mailto:email@example.com">Link already rich</a>)"_s
        << uR"(<a href="mailto:email@example.com" style="text-decoration: none;">email@example.com</a> <a href="mailto:email@example.com" style="text-decoration: none;">Link already rich</a>)"_s;
    QTest::addRow("mxid")
        << u"@user:kde.org <a href=\"https://matrix.to/#/@user:kde.org\">Link already rich</a>"_s
        << u"<b><a href=\"https://matrix.to/#/@user:kde.org\" style=\"text-decoration: none;\">@user:kde.org</a></b> <b><a href=\"https://matrix.to/#/@user:kde.org\" style=\"text-decoration: none;\">Link already rich</a></b>"_s;
    QTest::addRow("mxid with prefix") << u"a @user:kde.org b"_s
                                      << u"a <b><a href=\"https://matrix.to/#/@user:kde.org\" style=\"text-decoration: none;\">@user:kde.org</a></b> b"_s;

    // Another real case where our old URL regex failed to consider a valid and common symbol.
    QTest::addRow("link 3")
        << u"https://community.kde.org/Infrastructure/GitLab#Testing_someone_else's_merge_request"_s
        << u"<a href=\"https://community.kde.org/Infrastructure/GitLab#Testing_someone_else's_merge_request\" style=\"text-decoration: none;\">https://community.kde.org/Infrastructure/GitLab#Testing_someone_else's_merge_request</a>"_s;
}

/**
 * For when your rich input string has a plain text url left in.
 *
 * This test is to show that a url that is already rich will be left alone but a
 * plain one will be linkified.
 */
void TextHandlerTest::receiveRichPlainUrl()
{
    QFETCH(QString, input);
    QFETCH(QString, output);

    TextHandler testTextHandler;
    testTextHandler.setData(input);

    QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), output);
}

void TextHandlerTest::receiveRichEdited_data()
{
    QTest::addColumn<QString>("testInputString");
    QTest::addColumn<QString>("testOutputString");

    auto theme = static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));

    QTest::newRow("basic") << u"Edited"_s << u"Edited <span style=\"color:%1\">(edited)</span>"_s.arg(theme ? theme->disabledTextColor().name() : u"#000000"_s);
    QTest::newRow("multiple paragraphs") << u"<p>Edited</p>\n<p>Edited</p>"_s
                                         << u"<p>Edited</p>\n<p>Edited <span style=\"color:%1\">(edited)</span></p>"_s.arg(
                                                theme ? theme->disabledTextColor().name() : u"#000000"_s);
}

void TextHandlerTest::receiveRichEdited()
{
    QFETCH(QString, testInputString);
    QFETCH(QString, testOutputString);

    TextHandler testTextHandler;
    testTextHandler.setData(testInputString);

    const auto event = eventCast<const Quotient::RoomMessageEvent>(room->messageEvents().at(2).get());
    QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, event, false, event->isReplaced()), testOutputString);
}

void TextHandlerTest::receiveLineSeparator()
{
    auto text = u"foo\u2028bar"_s;
    TextHandler textHandler;
    textHandler.setData(text);
    QCOMPARE(textHandler.handleRecievePlainText(Qt::PlainText, true), u"foo bar"_s);
}

void TextHandlerTest::receiveRichCodeUrl()
{
    auto input = u"<code>https://kde.org</code>"_s;
    TextHandler testTextHandler;
    testTextHandler.setData(input);
    QCOMPARE(testTextHandler.handleRecieveRichText(), input);
}

void TextHandlerTest::receiveRichColor()
{
    const QString testInputString =
        u"<span data-mx-color=\"#ff00be\">¯</span><span data-mx-color=\"#ff3b1d\">\\</span><span data-mx-color=\"#ffa600\">_</span><span data-mx-color=\"#64d200\">(</span><span data-mx-color=\"#00e261\">ツ</span><span data-mx-color=\"#00e7ff\">)</span><span data-mx-color=\"#00e1ff\">_</span><span data-mx-color=\"#00bdff\">/</span><span data-mx-color=\"#ff60ff\">¯</span>"_s;
    const QString testOutputString =
        u"<span style=\"color: #ff00be;\">¯</span><span style=\"color: #ff3b1d;\">\\</span><span style=\"color: #ffa600;\">_</span><span style=\"color: #64d200;\">(</span><span style=\"color: #00e261;\">ツ</span><span style=\"color: #00e7ff;\">)</span><span style=\"color: #00e1ff;\">_</span><span style=\"color: #00bdff;\">/</span><span style=\"color: #ff60ff;\">¯</span>"_s;

    TextHandler testTextHandler;
    testTextHandler.setData(testInputString);

    QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
}

void TextHandlerTest::receiveRichCustomEmoji()
{
    const QString testInputString =
        u"<img data-mx-emoticon=\"\" src=\"mxc://test.com/examplemxc\" alt=\":test:\" title=\":test:\" height=\"32\" vertical-align=\"middle\" />"_s;
    const QString testOutputString =
        u"<a href=\"title://:test:\"><img  src=\"mxc://test.com/examplemxc?user_id=@bob:kde.org&room_id=%23myroom:kde.org&event_id=$zrCiBxBnqqTn0Z5FY78qSZAszno_w8nJJXzfBULG-3E\" alt=\":test:\" title=\":test:\" height=\"32\"></a>"_s;

    TextHandler testTextHandler;
    testTextHandler.setData(testInputString);

    // Event is meaningless, we just need it for the title wrapping code to run
    const auto event = eventCast<const Quotient::RoomMessageEvent>(room->messageEvents().at(2).get());

    // Ensure that imgs (such as emoticons) with the title tag expose it via the custom link tag
    QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, event), testOutputString);
}

void TextHandlerTest::receiveRichCustomEmojiNoTitle()
{
    const QString testInputString = u"<img data-mx-emoticon=\"\" src=\"mxc://test.com/examplemxc\" height=\"32\" vertical-align=\"middle\" />"_s;
    const QString testOutputString =
        u"<img  src=\"mxc://test.com/examplemxc?user_id=@bob:kde.org&room_id=%23myroom:kde.org&event_id=$zrCiBxBnqqTn0Z5FY78qSZAszno_w8nJJXzfBULG-3E\" height=\"32\">"_s;

    TextHandler testTextHandler;
    testTextHandler.setData(testInputString);

    // Event is meaningless, we just need it for the title wrapping code to run
    const auto event = eventCast<const Quotient::RoomMessageEvent>(room->messageEvents().at(2).get());

    // Ensure that imgs with no title set don't get wrapped.
    QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, event), testOutputString);
}

void TextHandlerTest::componentOutput_data()
{
    QTest::addColumn<QString>("testInputString");
    QTest::addColumn<std::vector<Blocks::Block *>>("testOutputComponents");

    QTest::newRow("multiple paragraphs") << u"<p>Text</p>\n<p>Text</p>"_s
                                         << std::vector<Blocks::Block *>{
                                                new Blocks::TextBlock(Blocks::Text, QTextDocumentFragment::fromPlainText(u"Text"_s), {}, this),
                                                new Blocks::TextBlock(Blocks::Text, QTextDocumentFragment::fromPlainText(u"Text"_s), {}, this)};
    QTest::newRow("code") << u"<p>Text</p>\n<pre><code class=\"language-html\">Some code\n</code></pre>"_s
                          << std::vector<Blocks::Block *>{new Blocks::TextBlock(Blocks::Text, QTextDocumentFragment::fromPlainText(u"Text"_s), {}, this),
                                                    new Blocks::CodeBlock(Blocks::Code, QTextDocumentFragment::fromPlainText(u"Some code"_s), u"html"_s, this)};
    QTest::newRow("quote") << u"<p>Text</p>\n<blockquote>\n<p>blockquote</p>\n</blockquote>"_s
                           << std::vector<Blocks::Block *>{new Blocks::TextBlock(Blocks::Text, QTextDocumentFragment::fromPlainText(u"Text"_s), {}, this),
                                                     new Blocks::TextBlock(Blocks::Quote, QTextDocumentFragment::fromPlainText(u"“blockquote”"_s), {}, this)};
    QTest::newRow("multiple paragraph quote")
        << u"<blockquote>\n<p>blockquote</p>\n<p>next paragraph</p>\n</blockquote>"_s
        << std::vector<Blocks::Block *>{
               new Blocks::TextBlock(Blocks::Quote, QTextDocumentFragment::fromPlainText(u"<p>“blockquote</p>\n<p>next paragraph”</p>"_s), {}, this)};
    QTest::newRow("no tag first paragraph") << u"Text\n<p>Text</p>"_s
                                            << std::vector<Blocks::Block *>{
                                                   new Blocks::TextBlock(Blocks::Text, QTextDocumentFragment::fromPlainText(u"Text"_s), {}, this),
                                                   new Blocks::TextBlock(Blocks::Text, QTextDocumentFragment::fromPlainText(u"Text"_s), {}, this)};
    QTest::newRow("no tag last paragraph") << u"<p>Text</p>\nText"_s
                                           << std::vector<Blocks::Block *>{
                                                  new Blocks::TextBlock(Blocks::Text, QTextDocumentFragment::fromPlainText(u"Text"_s), {}, this),
                                                  new Blocks::TextBlock(Blocks::Text, QTextDocumentFragment::fromPlainText(u"Text"_s), {}, this)};
    QTest::newRow("inline code") << u"<p><code>https://kde.org</code></p>\n<p>Text</p>"_s
                                 << std::vector<Blocks::Block *>{
                                        new Blocks::TextBlock(Blocks::Text, QTextDocumentFragment::fromHtml(u"<code>https://kde.org</code>"_s), {}, this),
                                        new Blocks::TextBlock(Blocks::Text, QTextDocumentFragment::fromPlainText(u"Text"_s), {}, this)};
    QTest::newRow("inline code single block")
        << u"<code>https://kde.org</code>"_s
        << std::vector<Blocks::Block *>{new Blocks::TextBlock(Blocks::Text, QTextDocumentFragment::fromHtml(u"<code>https://kde.org</code>"_s), {}, this)};
    QTest::newRow("long start tag")
        << u"Ah, you mean something like<br/><pre data-md=\"```\"><code class=\"language-qml\"># main.qml\nimport CustomQml\n...\nControls.TextField { id: someField }\nCustomQml {\n    someTextProperty: someField.text\n}\n</code></pre>Sure you can, it's still local to the same file where you defined the id"_s
        << std::vector<Blocks::Block *>{
               new Blocks::TextBlock(Blocks::Text, QTextDocumentFragment::fromPlainText(u"Ah, you mean something like<br/>"_s), {}, this),
               new Blocks::CodeBlock(
                   Blocks::Code,
                   QTextDocumentFragment::fromPlainText(
                       u"# main.qml\nimport CustomQml\n...\nControls.TextField { id: someField }\nCustomQml {\n    someTextProperty: someField.text\n}"_s),
                   u"qml"_s,
                   this),
               new Blocks::TextBlock(Blocks::Text,
                                     QTextDocumentFragment::fromPlainText(u"Sure you can, it's still local to the same file where you defined the id"_s),
                                     {},
                                     this)};
    QTest::newRow("code with non-breaking space") << u"<pre><code class=\"language-html\">&nbsp;&nbsp;&nbsp;&nbsp;Ideally Indented Code\n</code></pre>"_s
        << std::vector<Blocks::Block *>{
            new Blocks::CodeBlock(
                Blocks::Code,
                QTextDocumentFragment::fromPlainText(u"    Ideally Indented Code"_s),
                u"html"_s,
                this)};
}

void TextHandlerTest::componentOutput()
{
    QFETCH(QString, testInputString);
    QFETCH(std::vector<Blocks::Block *>, testOutputComponents);

    TextHandler testTextHandler;
    bool same = true;
    const auto components = testTextHandler.textComponents(testInputString, Qt::RichText, nullptr, nullptr, false, false, this);
    if (components.size() != testOutputComponents.size()) {
        same = false;
    }
    if (same) {
        for (int i = 0; i < (int)components.size(); ++i) {
            if (*components[i] != *testOutputComponents[i]) {
                same = false;
            }
        }
    }
    QCOMPARE(same, true);
}

void TextHandlerTest::updateSpoiler_data()
{
    QTest::addColumn<QString>("testInputString");
    QTest::addColumn<QString>("testOutputString");
    QTest::addColumn<bool>("spoilerRevealed");

    auto theme = static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
    QTest::newRow("same length") << u"<span data-mx-spoiler style=\"color: #123456; background-color: #123456;\">Test<span>"_s
                                 << u"<span data-mx-spoiler style=\"color: transparent; background-color: %1;\">Test<span>"_s.arg(
                                        theme->alternateBackgroundColor().name())
                                 << false;
    QTest::newRow("different length") << u"<span data-mx-spoiler style=\"color: short; background-color: looooooooooong;\">Test<span>"_s
                                      << u"<span data-mx-spoiler style=\"color: transparent; background-color: %1;\">Test<span>"_s.arg(
                                             theme->alternateBackgroundColor().name())
                                      << false;
    QTest::newRow("spoiler revealed")
        << u"<span data-mx-spoiler style=\"color: transparent; background-color: %1;\">Test<span>"_s.arg(theme->alternateBackgroundColor().name())
        << u"<span data-mx-spoiler style=\"color: %1; background-color: %2;\">Test<span>"_s.arg(theme->textColor().name(), theme->alternateBackgroundColor().name())
        << true;
}

void TextHandlerTest::updateSpoiler()
{
    QFETCH(QString, testInputString);
    QFETCH(QString, testOutputString);
    QFETCH(bool, spoilerRevealed);

    QCOMPARE(TextHandler::updateSpoilerText(this, testInputString, spoilerRevealed), testOutputString);
}

void TextHandlerTest::stripMatrixLinks_data()
{
    QTest::addColumn<QString>("testInputString");
    QTest::addColumn<QString>("testOutputString");

    QTest::newRow("user link") << u"[Bot](https://matrix.to/#/@bot:bot.com) foobar"_s << u"Bot foobar"_s;
    QTest::newRow("room link") << u"[\\#tokodon:kde.org](https://matrix.to/#/#tokodon:kde.org) room link"_s << u"#tokodon:kde.org room link"_s;
    QTest::newRow("multiple links")
        << u"[Bot](https://matrix.to/#/@bot:bot.com) check out this client: [\\#tokodon:kde.org](https://matrix.to/#/#tokodon:kde.org) it's super cool!"_s
        << u"Bot check out this client: #tokodon:kde.org it's super cool!"_s;
}

void TextHandlerTest::stripMatrixLinks()
{
    QFETCH(QString, testInputString);
    QFETCH(QString, testOutputString);

    QCOMPARE(TextHandler::stripMatrixLinks(testInputString), testOutputString);
}

void TextHandlerTest::receiveEmote()
{
    auto components = TextHandler().textComponents(u"does something"_s, Qt::RichText, room, room->messageEvents().at(1).get(), false, false, this);
    QCOMPARE(components.size(), 1);
    const auto block = dynamic_cast<Blocks::TextBlock *>(components.at(0));
    const auto html = block->item()->initialFragment().toHtml();
    QRegularExpression regex(QStringLiteral(
        R"(<!--StartFragment-->\* <a href="[^"]+"><span style="[^"]*color:#[0-9A-Fa-f]{6};?[^"]*">.*?</span></a> does something<!--EndFragment-->)"));
    QVERIFY(html.contains(regex));
}

QTEST_MAIN(TextHandlerTest)
#include "texthandlertest.moc"
