// SPDX-License-Identifier: GPL-3.0-or-later

#include "test_utils.h"
#include "tests.h"
#include "tests_common.h"

#include "common/mimetypes.h"
#include "common/commandstatus.h"

#include <QKeySequence>
#include <QRegularExpression>

void CoreTests::configMaxitems()
{
    RUN("config" << "maxitems" << "3", "3\n");
    RUN("add" << "A" << "B" << "C", "");
    RUN("add" << "D", "");
    RUN("separator" << " " << "read" << "0" << "1" << "2", "D C B");
    RUN("size", "3\n");

    RUN("add" << "E" << "F", "");
    RUN("separator" << " " << "read" << "0" << "1" << "2", "F E D");
    RUN("size", "3\n");

    RUN("config" << "maxitems" << "2", "2\n");
    RUN("separator" << " " << "read" << "0" << "1", "F E");
    RUN("size", "2\n");

    // Adding too many items fails.
    m_test->ignoreErrors(
        QRegularExpression(R"(Cannot add new items\. Tab "CLIPBOARD" reached the maximum number of items\.)") );
    RUN_EXPECT_ERROR("add" << "1" << "2" << "3", CommandException);
    RUN("separator" << " " << "read" << "0" << "1", "F E");
    RUN("size", "2\n");

    // Single item in tabs.
    RUN("config" << "maxitems" << "1", "1\n");
    RUN("separator" << " " << "read" << "0", "F");
    RUN("size", "1\n");

    RUN("add" << "G", "");
    RUN("separator" << " " << "read" << "0", "G");
    RUN("size", "1\n");

    RUN("write" << "1" << "text/plain" << "H", "");
    RUN("separator" << " " << "read" << "0", "H");
    RUN("size", "1\n");

    // No items in tabs.
    RUN("config" << "maxitems" << "0", "0\n");
    RUN("size", "0\n");

    RUN_EXPECT_ERROR("add" << "1", CommandException);
    RUN_EXPECT_ERROR("write" << "1", CommandException);
    RUN("size", "0\n");

    // Invalid value.
    RUN("config" << "maxitems" << "-99", "0\n");
    RUN("size", "0\n");
}

void CoreTests::keysAndFocusing()
{
    RUN("disable", "");
    KEYS(clipboardBrowserId << "CTRL+T");
    WAIT_ON_OUTPUT("currentWindowTitle", appWindowTitle("New Tab"));

    KEYS(tabDialogLineEditId << "ESC");
    WAIT_ON_OUTPUT("currentWindowTitle", appWindowTitle("*Clipboard Storing Disabled*"));

    RUN("enable", "");
}

void CoreTests::selectItems()
{
    const auto tab = QString(clipboardTabName);
    RUN("add" << "C" << "B" << "A", "");

    KEYS("RIGHT" << "SHIFT+DOWN" << "SHIFT+DOWN");
    TEST_SELECTED(tab + " 2 0 1 2\n");

    KEYS("SHIFT+UP");
    TEST_SELECTED(tab + " 1 0 1\n");

    KEYS("END");
    TEST_SELECTED(tab + " 2 2\n");

    KEYS("SHIFT+UP");
    TEST_SELECTED(tab + " 1 2 1\n");

    KEYS("CTRL+A");
    TEST_SELECTED(tab + " 1 0 1 2\n");

    // CTRL+SPACE toggles current item selection
    RUN("add" << "D", "");
    KEYS("PGUP" << "CTRL+SHIFT+DOWN" << "CTRL+SHIFT+DOWN");
    TEST_SELECTED(tab + " 2 0\n");
    KEYS("CTRL+SPACE");
    TEST_SELECTED(tab + " 2 0 2\n");
    KEYS("SHIFT+DOWN");
    TEST_SELECTED(tab + " 3 0 2 3\n");
    KEYS("CTRL+SPACE");
    TEST_SELECTED(tab + " 3 0 2\n");
}

void CoreTests::moveItems()
{
    const auto tab = QString(clipboardTabName);
    const auto args = Args() << "separator" << " ";
    RUN(args << "add" << "C" << "B" << "A", "");

    // move item one down
    KEYS("RIGHT" << "CTRL+DOWN");
    RUN(args << "read" << "0" << "1" << "2", "B A C");
    TEST_SELECTED(tab + " 1 1\n");

    // move items to top
    KEYS("SHIFT+DOWN" << "CTRL+HOME");
    RUN(args << "read" << "0" << "1" << "2", "A C B");
    TEST_SELECTED(tab + " 1 0 1\n");
}

void CoreTests::deleteItems()
{
    const auto tab = QString(clipboardTabName);
    const auto args = Args() << "separator" << ",";
    RUN(args << "add" << "C" << "B" << "A", "");

    // delete first item
    KEYS("RIGHT" << m_test->shortcutToRemove());
    RUN(args << "read" << "0" << "1" << "2", "B,C,");
    TEST_SELECTED(tab + " 0 0\n");

    // select all and delete
    KEYS("CTRL+A" << m_test->shortcutToRemove());
    RUN(args << "size", "0\n");
}

void CoreTests::searchItems()
{
    RUN("add" << "a" << "b" << "c", "");
    KEYS(":b" << "TAB");
    TEST_SELECTED(QString(clipboardTabName) + " 1 1\n");
}

void CoreTests::searchItemsAndSelect()
{
    RUN("add" << "xx1" << "a" << "xx2" << "c" << "xx3" << "d", "");
    KEYS(":xx" << filterEditId);
    TEST_SELECTED(QString(clipboardTabName) + " 1 1\n");

    KEYS(filterEditId << "DOWN" << filterEditId);
    TEST_SELECTED(QString(clipboardTabName) + " 3 3\n");

    KEYS(filterEditId << "DOWN" << filterEditId);
    TEST_SELECTED(QString(clipboardTabName) + " 5 5\n");

    KEYS(filterEditId << "TAB" << clipboardBrowserId);
}

void CoreTests::searchItemsAndCopy()
{
    RUN("add" << "TEST_ITEM", "");
    KEYS(":test" << "CTRL+C" << filterEditId);
    WAIT_FOR_CLIPBOARD("TEST_ITEM");
}

void CoreTests::searchRowNumber()
{
    RUN("add" << "d2" << "c" << "b2" << "a", "");

    KEYS(":2");
    KEYS(filterEditId << "TAB" << clipboardBrowserId);
    TEST_SELECTED(QString(clipboardTabName) + " 1 1\n");
    KEYS("CTRL+A");
    TEST_SELECTED(QString(clipboardTabName) + " 1 1 3\n");

    KEYS(":0");
    KEYS(filterEditId << "TAB" << clipboardBrowserId << "CTRL+A");
    TEST_SELECTED(QString(clipboardTabName) + " _\n");

    RUN("config" << "row_index_from_one" << "false", "false\n");
    KEYS(":2");
    KEYS(filterEditId << "TAB" << clipboardBrowserId);
    TEST_SELECTED(QString(clipboardTabName) + " 2 2\n");
    KEYS("CTRL+A");
    TEST_SELECTED(QString(clipboardTabName) + " 2 1 2 3\n");

    KEYS(":0");
    KEYS(filterEditId << "TAB" << clipboardBrowserId << "CTRL+A");
    TEST_SELECTED(QString(clipboardTabName) + " 0 0\n");

    KEYS(":5");
    KEYS(filterEditId << "TAB" << clipboardBrowserId << "CTRL+A");
    TEST_SELECTED(QString(clipboardTabName) + " _\n");

    RUN("filter" << "-1", "");
    RUN("filter", "-1\n");
    KEYS(clipboardBrowserId << "CTRL+A");
    TEST_SELECTED(QString(clipboardTabName) + " _\n");
}

void CoreTests::searchAccented()
{
    RUN("add" << "a" << "väčšina" << "a", "");
    RUN("filter" << "vacsina", "");
    WAIT_ON_OUTPUT("testSelected", QByteArray(clipboardTabName) + " 1 1\n");
}

void CoreTests::searchManyItems()
{
    RUN("config"
        << "maxitems" << "100000"
        << "row_index_from_one" << "false",
        "maxitems=100000\n"
        "row_index_from_one=false\n"
    );
    RUN("add.apply(this, [...Array(100000).keys()].reverse())", "");

    RUN("filter" << "90001", "");
    TEST_SELECTED(QString(clipboardTabName) + " 90001 90001\n");

    RUN("filter" << "9 8 7 6 5", "");
    WAIT_ON_OUTPUT("testSelected", QString(clipboardTabName) + " 56789 56789\n");

    RUN("config" << "filter_regular_expression" << "true", "true\n");
    RUN("filter" << ".*99999", "");
    WAIT_ON_OUTPUT("testSelected", QString(clipboardTabName) + " 99999 99999\n");

    KEYS(":9" << ":0002" << filterEditId);
    RUN("filter", "90002\n");
    KEYS(filterEditId << "TAB" << clipboardBrowserId);
    TEST_SELECTED(QString(clipboardTabName) + " 90002 90002\n");

    KEYS(":9" << ":999" << filterEditId);
    KEYS(filterEditId << "TAB" << clipboardBrowserId);
    WAIT_ON_OUTPUT(
        QStringLiteral(
            "plugins.itemtests.keys('%1', 'CTRL+A', '%1');"
            "testSelected()")
        .arg(clipboardBrowserId),
        QString(clipboardTabName) + " 9999"
        " 9999 19999 29999 39999 49999 59999 69999 79999 89999"
        " 99990 99991 99992 99993 99994 99995 99996 99997 99998 99999\n"
    );
}

void CoreTests::copyItems()
{
    const auto tab = QString(clipboardTabName);
    RUN("add" << "C" << "B" << "A", "");

    // Select and copy all items.
    KEYS("CTRL+A" << keyNameFor(QKeySequence::Copy));

    // This seems to be required on Windows.
    WAIT_ON_OUTPUT("clipboard", "A\nB\nC");

    // Paste all items.
    KEYS(keyNameFor(QKeySequence::Paste));
    RUN("separator" << " " << "read" << "0" << "1" << "2" << "3" << "4" << "5", "A B C A B C");
    RUN("size", "6\n");
}

void CoreTests::copyItemsNullCharacter()
{
    // Items with embedded null characters should have nulls stripped
    // when concatenated for the clipboard (GitHub issue #3515).
    const auto tab = QString(clipboardTabName);
    RUN("write(0,"
        "{'text/plain': 'A' + String.fromCharCode(0) + 'B'},"
        "{'text/plain': 'CD'})", "");

    // Select and copy all items.
    KEYS("CTRL+A" << keyNameFor(QKeySequence::Copy));

    // Clipboard should contain both items without null characters.
    WAIT_ON_OUTPUT("clipboard", "CD\nAB");
}


void CoreTests::selectAndCopyOrder()
{
    const auto tab = testTab(1);
    const Args args = Args("tab") << tab << "separator" << " ";
    RUN(args << "add" << "D" << "C" << "B" << "A", "");
    RUN("setCurrentTab" << tab, "");

    KEYS("END" << "SHIFT+UP" << "SHIFT+UP" << "SHIFT+UP");
    TEST_SELECTED(tab + " 0 3 2 1 0\n");

    KEYS(keyNameFor(QKeySequence::Copy));
    WAIT_ON_OUTPUT("clipboard", "D\nC\nB\nA");
}

void CoreTests::sortAndReverse()
{
    const auto tab = testTab(1);
    const Args args = Args("tab") << tab << "separator" << " ";
    RUN(args << "add" << "D" << "A" << "C" << "B", "");
    RUN("setCurrentTab" << tab, "");

    KEYS("CTRL+A");
    TEST_SELECTED(tab + " 0 0 1 2 3\n");

    KEYS("CTRL+SHIFT+S");
    RUN(args << "read" << "0" << "1" << "2" << "3" << "4", "A B C D ");
    TEST_SELECTED(tab + " 1 0 1 2 3\n");
    KEYS(keyNameFor(QKeySequence::Copy));
    WAIT_ON_OUTPUT("clipboard", "A\nB\nC\nD");

    KEYS("CTRL+SHIFT+R");
    RUN(args << "read" << "0" << "1" << "2" << "3" << "4", "D C B A ");
    TEST_SELECTED(tab + " 2 0 1 2 3\n");
    KEYS(keyNameFor(QKeySequence::Copy));
    WAIT_ON_OUTPUT("clipboard", "D\nC\nB\nA");

    KEYS("CTRL+SHIFT+R");
    RUN(args << "read" << "0" << "1" << "2" << "3" << "4", "A B C D ");
    TEST_SELECTED(tab + " 1 0 1 2 3\n");
}

void CoreTests::editItems()
{
    RUN("config" << "edit_ctrl_return" << "true", "true\n");

    RUN("add" << "Line 4" << "Line 1", "");

    KEYS(clipboardBrowserId << "F2"
        << editorId << "END" << "ENTER" << ":Line 2" << "F2");
    RUN("read" << "0", "Line 1\nLine 2");

    KEYS(clipboardBrowserId << "DOWN" << "F2"
        << editorId << "HOME" << ":Line 3" << "ENTER" << "F2");
    RUN("read" << "1", "Line 3\nLine 4");
    RUN("read" << "0", "Line 1\nLine 2");

    // Edit multiple items
    KEYS(clipboardBrowserId << "SHIFT+UP" << "F2"
        << editorId << "END" << "ENTER" << ":Line 5" << "F2");
    RUN("read" << "0", "Line 3\nLine 4\nLine 1\nLine 2\nLine 5");
    RUN("read" << "1", "Line 1\nLine 2");
    RUN("read" << "2", "Line 3\nLine 4");
}

void CoreTests::createNewItem()
{
    RUN("config" << "edit_ctrl_return" << "true", "true\n");

    KEYS("CTRL+N" << editorId << ":Line 1" << "ENTER" << ":Line 2" << "F2");
    RUN("read" << "0", "Line 1\nLine 2");

    KEYS("CTRL+N" << editorId << ":Line 3" << "ENTER" << ":Line 4" << "F2");
    RUN("read" << "0", "Line 3\nLine 4");
}

void CoreTests::editNotes()
{
    const auto script = R"(
        add(
          {
            [mimeText]: 'B',
            // https://stackoverflow.com/a/13139830
            [mimeIcon]: frombase64("R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"),
          },
          {
            [mimeText]: 'A',
            [mimeIcon]: String.fromCharCode(0xf188),
          }
        )
    )";
    RUN(script, "");

    RUN("config" << "editor" << "", "\n");
    KEYS("SHIFT+F2" << ":A Note" << "F2");
    RUN("read" << mimeText << "0" << mimeItemNotes << "0" << "F2", "A\nA Note");
    RUN("read" << mimeText << "1" << mimeItemNotes << "1" << "F2", "B\n");

    KEYS("DOWN");

    KEYS("SHIFT+F2" << ":B Note" << "F2");
    RUN("read" << mimeText << "1" << mimeItemNotes << "1" << "F2", "B\nB Note");
    RUN("read" << mimeText << "0" << mimeItemNotes << "0" << "F2", "A\nA Note");
}

void CoreTests::editHtml()
{
    // Create new item with bold text
    KEYS("CTRL+N" << editorId << ":BOLD"
        << QKeySequence(QKeySequence::SelectAll).toString()
        << QKeySequence(QKeySequence::Bold).toString()
        << "F2" << clipboardBrowserId);

    const auto readHtmlItem = [this](){
        QByteArray out;
        run(Args() << "read(mimeHtml, 0)", &out);
        return QString::fromUtf8(out);
    };

    QRegularExpression re("font-weight: *[67]00[^>]*>BOLD</[^>]*>");
    QString html = readHtmlItem();
    QVERIFY2( html.contains(re), html.toUtf8() );

    // Append normal text
    KEYS("F2" << editorId << "END"
        << QKeySequence(QKeySequence::Bold).toString()
        << ":,NORMAL" << "F2" << clipboardBrowserId);
    html = readHtmlItem();
    QVERIFY2( html.contains(re), html.toUtf8() );
    re.setPattern( re.pattern() + ",NORMAL" );
    QVERIFY2( html.contains(re), html.toUtf8() );

    // Append italic text
    KEYS("F2" << editorId << "END"
        << QKeySequence(QKeySequence::Italic).toString()
        << ":,ITALIC" << "F2" << clipboardBrowserId);
    html = readHtmlItem();
    QVERIFY2( html.contains(re), html.toUtf8() );
    re.setPattern( re.pattern() + "<[^>]*font-style: *italic[^>]*>,ITALIC</[^>]*>" );
    QVERIFY2( html.contains(re), html.toUtf8() );

    // Append underline text
    KEYS("F2" << editorId << "END"
        << QKeySequence(QKeySequence::Underline).toString()
        << ":,ULINE" << "F2" << clipboardBrowserId);
    html = readHtmlItem();
    QVERIFY2( html.contains(re), html.toUtf8() );
    re.setPattern( re.pattern() + "<[^>]*text-decoration: *underline[^>]*>,ULINE</[^>]*>" );
    QVERIFY2( html.contains(re), html.toUtf8() );

    // Append strike-through text
    KEYS("F2" << editorId << "END"
        << "mouse|CLICK|text=Strikethrough"
        << ":,XXX" << "F2" << clipboardBrowserId);
    html = readHtmlItem();
    QVERIFY2( html.contains(re), html.toUtf8() );
    re.setPattern( re.pattern() + "<[^>]*text-decoration: *underline line-through[^>]*>,XXX</[^>]*>" );
    QVERIFY2( html.contains(re), html.toUtf8() );

    // Erase style
    KEYS("F2" << editorId << "END"
        << QKeySequence(QKeySequence::SelectAll).toString()
        << "mouse|CLICK|text=Erase Style"
        << "F2" << clipboardBrowserId);
    RUN("read(0)", "BOLD,NORMAL,ITALIC,ULINE,XXX");
}
