/*
    SPDX-FileCopyrightText: 2019 João Netto <joaonetto901@gmail.com>

    SPDX-License-Identifier: GPL-2.0-or-later
*/

#include <QAction>
#include <QJsonArray>
#include <QJsonDocument>
#include <QMenu>
#include <QTest>
#include <QTimer>

#include "../settings_core.h"
#include "core/action.h"
#include "core/document.h"
#include "core/scripter.h"
#include <QAbstractButton>
#include <QAbstractItemModel>
#include <QMap>
#include <QMessageBox>
#include <QMimeDatabase>
#include <QMimeType>
#include <QThread>
#include <core/form.h>
#include <core/page.h>
#include <core/script/event_p.h>

class MessageBoxHelper : public QObject
{
    Q_OBJECT

public:
    MessageBoxHelper(QMessageBox::StandardButton b, QString message, QMessageBox::Icon icon, QString title, bool hasCheckBox)
        : m_button(b)
        , m_clicked(false)
        , m_message(std::move(message))
        , m_icon(icon)
        , m_title(std::move(title))
        , m_checkBox(hasCheckBox)
    {
        QTimer::singleShot(0, this, &MessageBoxHelper::closeMessageBox);
    }

    ~MessageBoxHelper() override
    {
        QVERIFY(m_clicked);
    }

private Q_SLOTS:
    void closeMessageBox()
    {
        const QWidgetList allToplevelWidgets = QApplication::topLevelWidgets();
        QMessageBox *mb = nullptr;
        for (QWidget *w : allToplevelWidgets) {
            if (w->inherits("QMessageBox")) {
                mb = qobject_cast<QMessageBox *>(w);
                QCOMPARE(mb->text(), m_message);
                QCOMPARE(mb->windowTitle(), m_title);
                QCOMPARE(mb->icon(), m_icon);
                QCheckBox *box = mb->checkBox();
                QCOMPARE(box != nullptr, m_checkBox);
                mb->button(m_button)->click();
            }
        }
        if (!mb) {
            QTimer::singleShot(0, this, &MessageBoxHelper::closeMessageBox);
            return;
        }
        m_clicked = true;
    }

private:
    QMessageBox::StandardButton m_button;
    bool m_clicked;
    QString m_message;
    QMessageBox::Icon m_icon;
    QString m_title;
    bool m_checkBox;
};

class JSFunctionsTest : public QObject
{
    Q_OBJECT

private Q_SLOTS:
    void initTestCase();
    void testNthFieldName();
    void testDisplay();
    void testSetClearInterval();
    void testSetClearTimeOut();
    void testGetOCGs();
    void cleanupTestCase();
    void testAlert();
    void testPopUpMenu();
    void testPopUpMenuEx();
    void testPrintD();
    void testPrintD_data();

private:
    Okular::Document *m_document;
    QMap<QString, Okular::FormField *> m_fields;
};

void JSFunctionsTest::initTestCase()
{
    Okular::SettingsCore::instance(QStringLiteral("jsfunctionstest"));
    m_document = new Okular::Document(nullptr);

    const QString testFile = QStringLiteral(KDESRCDIR "data/kjsfunctionstest.pdf");
    QMimeDatabase db;
    const QMimeType mime = db.mimeTypeForFile(testFile);
    QCOMPARE(m_document->openDocument(testFile, QUrl(), mime), Okular::Document::OpenSuccess);

    const Okular::Page *page = m_document->page(0);
    const QList<Okular::FormField *> pageFormFields = page->formFields();
    for (Okular::FormField *ff : pageFormFields) {
        m_fields.insert(ff->name(), ff);
    }
}

void JSFunctionsTest::testNthFieldName()
{
    for (int i = 0; i < 21; ++i) {
        Okular::ScriptAction *action = new Okular::ScriptAction(Okular::JavaScript,
                                                                QStringLiteral("var field = Doc.getField( Doc.getNthFieldName(%1) );\
                                                                              field.display = display.visible;")
                                                                    .arg(i));
        m_document->processAction(action);
        QVERIFY(m_fields[QStringLiteral("0.%1").arg(i)]->isVisible());
        m_fields[QStringLiteral("0.%1").arg(i)]->setVisible(false);
        delete action;
    }
}

void JSFunctionsTest::testDisplay()
{
    Okular::ScriptAction *action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("field = Doc.getField(\"0.0\");field.display=display.hidden;\
        field = Doc.getField(\"0.10\");field.display=display.visible;"));
    m_document->processAction(action);
    QVERIFY(!m_fields[QStringLiteral("0.0")]->isVisible());
    QVERIFY(!m_fields[QStringLiteral("0.0")]->isPrintable());
    QVERIFY(m_fields[QStringLiteral("0.10")]->isVisible());
    QVERIFY(m_fields[QStringLiteral("0.10")]->isPrintable());
    delete action;

    action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("field = Doc.getField(\"0.10\");field.display=display.noView;\
        field = Doc.getField(\"0.15\");field.display=display.noPrint;"));
    m_document->processAction(action);
    QVERIFY(!m_fields[QStringLiteral("0.10")]->isVisible());
    QVERIFY(m_fields[QStringLiteral("0.10")]->isPrintable());
    QVERIFY(m_fields[QStringLiteral("0.15")]->isVisible());
    QVERIFY(!m_fields[QStringLiteral("0.15")]->isPrintable());
    delete action;

    action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("field = Doc.getField(\"0.15\");field.display=display.hidden;\
        field = Doc.getField(\"0.20\");field.display=display.visible;"));
    m_document->processAction(action);
    QVERIFY(!m_fields[QStringLiteral("0.15")]->isVisible());
    QVERIFY(!m_fields[QStringLiteral("0.15")]->isPrintable());
    QVERIFY(m_fields[QStringLiteral("0.20")]->isVisible());
    QVERIFY(m_fields[QStringLiteral("0.20")]->isPrintable());
    delete action;

    action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("field = Doc.getField(\"0.20\");field.display=display.hidden;\
        field = Doc.getField(\"0.0\");field.display=display.visible;"));
    m_document->processAction(action);
    QVERIFY(!m_fields[QStringLiteral("0.20")]->isVisible());
    QVERIFY(!m_fields[QStringLiteral("0.20")]->isPrintable());
    QVERIFY(m_fields[QStringLiteral("0.0")]->isVisible());
    QVERIFY(m_fields[QStringLiteral("0.0")]->isPrintable());
    delete action;
}

void delay()
{
    QTime dieTime = QTime::currentTime().addSecs(2);
    while (QTime::currentTime() < dieTime) {
        QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
    }
}

void JSFunctionsTest::testSetClearInterval()
{
    Okular::ScriptAction *action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("obj = new Object();obj.idx=0;\
        obj.inc=function(){field = Doc.getField(Doc.getNthFieldName(obj.idx));\
        field.display = display.visible;\
        obj.idx = obj.idx + 1;};\
        intv = app.setInterval('obj.inc()', 450);obj.idx;"));
    m_document->processAction(action);
    QVERIFY(m_fields[QStringLiteral("0.0")]->isVisible());
    QVERIFY(!m_fields[QStringLiteral("0.3")]->isVisible());
    delete action;
    delay();

    action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("app.clearInterval(intv);obj.idx;"));
    m_document->processAction(action);
    QVERIFY(m_fields[QStringLiteral("0.3")]->isVisible());
    delete action;
}

void JSFunctionsTest::testSetClearTimeOut()
{
    Okular::ScriptAction *action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("intv = app.setTimeOut('obj.inc()', 1);obj.idx;"));
    m_document->processAction(action);
    QVERIFY(m_fields[QStringLiteral("0.3")]->isVisible());
    QVERIFY(!m_fields[QStringLiteral("0.4")]->isVisible());
    delay();
    delete action;

    QVERIFY(m_fields[QStringLiteral("0.4")]->isVisible());

    action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("intv = app.setTimeOut('obj.inc()', 2000);obj.idx;"));
    m_document->processAction(action);
    QVERIFY(m_fields[QStringLiteral("0.4")]->isVisible());
    delete action;

    action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("app.clearTimeOut(intv);obj.idx;"));
    m_document->processAction(action);
    QVERIFY(m_fields[QStringLiteral("0.4")]->isVisible());
    delay();
    QVERIFY(m_fields[QStringLiteral("0.4")]->isVisible());
    delete action;
}

void JSFunctionsTest::testGetOCGs()
{
    QAbstractItemModel *model = m_document->layersModel();

    Okular::ScriptAction *action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("var ocg = this.getOCGs(this.pageNum);\
        ocg[0].state = false;"));
    m_document->processAction(action);
    QVERIFY(!model->data(model->index(0, 0), Qt::CheckStateRole).toBool());
    delete action;

    action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ocg[0].state = true;"));
    m_document->processAction(action);
    QVERIFY(model->data(model->index(0, 0), Qt::CheckStateRole).toBool());
    delete action;

    action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ocg[1].state = false;"));
    m_document->processAction(action);
    QVERIFY(!model->data(model->index(1, 0), Qt::CheckStateRole).toBool());
    delete action;

    action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ocg[1].state = true;"));
    m_document->processAction(action);
    QVERIFY(model->data(model->index(1, 0), Qt::CheckStateRole).toBool());
    delete action;

    action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ocg[2].state = false;"));
    m_document->processAction(action);
    QVERIFY(!model->data(model->index(2, 0), Qt::CheckStateRole).toBool());
    delete action;

    action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ocg[2].state = true;"));
    m_document->processAction(action);
    QVERIFY(model->data(model->index(2, 0), Qt::CheckStateRole).toBool());
    delete action;

    action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ocg[3].state = false;"));
    m_document->processAction(action);
    QVERIFY(!model->data(model->index(3, 0), Qt::CheckStateRole).toBool());
    delete action;

    action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ocg[3].state = true;"));
    m_document->processAction(action);
    QVERIFY(model->data(model->index(3, 0), Qt::CheckStateRole).toBool());
    delete action;
}

void JSFunctionsTest::testAlert()
{
    Okular::ScriptAction *action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ret = app.alert( \"Random Message\" );"));
    QScopedPointer<MessageBoxHelper> messageBoxHelper;
    messageBoxHelper.reset(new MessageBoxHelper(QMessageBox::Ok, QStringLiteral("Random Message"), QMessageBox::Critical, QStringLiteral("Okular"), false));
    m_document->processAction(action);
    delete action;

    action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ret = app.alert( \"Empty Message\", 1 );"));
    messageBoxHelper.reset(new MessageBoxHelper(QMessageBox::Ok, QStringLiteral("Empty Message"), QMessageBox::Warning, QStringLiteral("Okular"), false));
    m_document->processAction(action);
    delete action;

    action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ret = app.alert( \"No Message\", 2, 2 );"));
    messageBoxHelper.reset(new MessageBoxHelper(QMessageBox::Yes, QStringLiteral("No Message"), QMessageBox::Question, QStringLiteral("Okular"), false));
    m_document->processAction(action);
    delete action;

    action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ret = app.alert( \"No\", 3, 2, \"Test Dialog\" );"));
    messageBoxHelper.reset(new MessageBoxHelper(QMessageBox::No, QStringLiteral("No"), QMessageBox::Information, QStringLiteral("Test Dialog"), false));
    m_document->processAction(action);
    delete action;

    action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("var oCheckBox = new Object();\
                                                                            ret = app.alert( \"Cancel\", 3, 3, \"Test Dialog\", 0, oCheckBox );"));
    messageBoxHelper.reset(new MessageBoxHelper(QMessageBox::Cancel, QStringLiteral("Cancel"), QMessageBox::Information, QStringLiteral("Test Dialog"), true));
    m_document->processAction(action);
    delete action;
}

class PopupMenuHelper : public QObject
{
    Q_OBJECT

public:
    PopupMenuHelper()
        : m_menuFound(false)
    {
        QTimer::singleShot(0, this, &PopupMenuHelper::closeMenu);
    }

    ~PopupMenuHelper() override
    {
    }

    bool menuFound()
    {
        return m_menuFound;
    }

    const QJsonArray &menuTree()
    {
        return m_menuTree;
    }

private Q_SLOTS:
    void closeMenu()
    {
        const QWidgetList allToplevelWidgets = QApplication::topLevelWidgets();
        QMenu *menu = nullptr;
        for (QWidget *w : allToplevelWidgets) {
            if (w->objectName() == QStringLiteral("popUpMenuEx") && w->inherits("QMenu")) {
                menu = qobject_cast<QMenu *>(w);

                // Generate an tree of string with all the menu trees
                processQMenuToJS(menu, m_menuTree);

                menu->close();
                m_menuFound = true;
                break;
            }
        }
        if (!menu) {
            QTimer::singleShot(0, this, &PopupMenuHelper::closeMenu);
        }
    }

private:
    static void processQMenuToJS(QMenu *menu, QJsonArray &array)
    {
        const QList<QAction *> actions = menu->actions();

        for (QAction *action : actions) {
            QMenu *itMenu = action->menu();

            if (itMenu != nullptr) {
                QJsonArray subMenus;
                subMenus.append(action->text());
                processQMenuToJS(itMenu, subMenus);
                array.append(subMenus);
            } else {
                array.append(action->text());
            }
        }
    }

private:
    bool m_menuFound;
    QJsonArray m_menuTree;
};

void JSFunctionsTest::testPopUpMenu()
{
    Okular::ScriptAction *action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("ret = app.popUpMenu( [\"Fruits\",\"Apples\",\"Oranges\"], \"-\",\"Beans\",\"Corn\" );"));

    QScopedPointer<PopupMenuHelper> popupMenuHelper;
    popupMenuHelper.reset(new PopupMenuHelper());
    m_document->processAction(action);
    QJsonArray expected = {
        QJsonArray {
            QJsonValue {QStringLiteral("Fruits")},
            QJsonValue {QStringLiteral("Apples")},
            QJsonValue {QStringLiteral("Oranges")},
        },
        QJsonValue {QStringLiteral("-")},
        QJsonValue {QStringLiteral("Beans")},
        QJsonValue {QStringLiteral("Corn")},
    };

    QString expectedString = QString::fromUtf8(QJsonDocument(expected).toJson());
    QString resultString = QString::fromUtf8(QJsonDocument(popupMenuHelper->menuTree()).toJson());
    QString description = QStringLiteral("Expected:\n") + expectedString + QStringLiteral("But got:\n") + resultString;
    QVERIFY2(expected == popupMenuHelper->menuTree(), description.toUtf8().constData());

    delete action;
}

void JSFunctionsTest::testPopUpMenuEx()
{
    QScopedPointer<PopupMenuHelper> popupMenuHelper;

    Okular::ScriptAction *action = new Okular::ScriptAction(
        Okular::JavaScript,
        QStringLiteral("ret = app.popUpMenuEx( {cName:\"Fruits\", oSubMenu:[{cName:\"Apples\", bMarked:false},{cName:\"Oranges\", bMarked:true}]}, {cName:\"-\"},{cName:\"Beans\", bEnabled:false},{cName:\"Corn\", bEnabled:true} )"));

    popupMenuHelper.reset(new PopupMenuHelper());
    m_document->processAction(action);
    QVERIFY(popupMenuHelper->menuFound());

    QJsonArray expected = {
        QJsonArray {
            QJsonValue {QStringLiteral("Fruits")},
            QJsonValue {QStringLiteral("Apples")},
            QJsonValue {QStringLiteral("Oranges")},
        },
        QJsonValue {QStringLiteral("-")},
        QJsonValue {QStringLiteral("Beans")},
        QJsonValue {QStringLiteral("Corn")},
    };

    QString expectedString = QString::fromUtf8(QJsonDocument(expected).toJson());
    QString resultString = QString::fromUtf8(QJsonDocument(popupMenuHelper->menuTree()).toJson());
    QString description = QStringLiteral("Expected:\n") + expectedString + QStringLiteral("But got:\n") + resultString;
    QVERIFY2(expected == popupMenuHelper->menuTree(), description.toUtf8().constData());

    delete action;

    // Test infinite recursion
    action = new Okular::ScriptAction(Okular::JavaScript, QStringLiteral("\
        var recursiveMenu = {\"cName\": \"Devil menu\"};\n\
        recursiveMenu.oSubMenu = [ recursiveMenu ];\n\
        ret = app.popUpMenuEx( recursiveMenu );"));

    popupMenuHelper.reset(new PopupMenuHelper());
    m_document->processAction(action);
    QVERIFY(popupMenuHelper->menuFound());
    // Must not crash
    delete action;
}

/** @brief Checks a single JS action against an expected result
 *
 * Runs an action with the given @p script and checks that it
 * does pop-up a messagebox with the given @p result text.
 */
class PrintDHelper
{
public:
    PrintDHelper(Okular::Document *document, const QString &script, const QString &result)
        : action(new Okular::ScriptAction(Okular::JavaScript, script))
        , box(new MessageBoxHelper(QMessageBox::Ok, result, QMessageBox::Critical, QStringLiteral("Okular"), false))
    {
        document->processAction(action.data());
    }

private:
    QScopedPointer<Okular::ScriptAction> action;
    QScopedPointer<MessageBoxHelper> box;
};

void JSFunctionsTest::testPrintD_data()
{
    // Force consistent locale
    QLocale locale(QStringLiteral("en_US"));
    QLocale::setDefault(locale);

    QTest::addColumn<QString>("script");
    QTest::addColumn<QString>("result");

    QTest::newRow("mmyyy") << QStringLiteral(
                                  "var date = new Date( 2010, 0, 5, 11, 10, 32, 1 );\
                            ret = app.alert( util.printd( \"mm\\\\yyyy\", date ) );")
                           << QStringLiteral("01\\2010");
    QTest::newRow("myy") << QStringLiteral("ret = app.alert( util.printd( \"m\\\\yy\", date ) );") << QStringLiteral("1\\10");
    QTest::newRow("ddmmHHMM") << QStringLiteral("ret = app.alert( util.printd( \"dd\\\\mm HH:MM\", date ) );") << QStringLiteral("05\\01 11:10");
    QTest::newRow("ddmmHHMMss") << QStringLiteral("ret = app.alert( util.printd( \"dd\\\\mm HH:MM:ss\", date ) );") << QStringLiteral("05\\01 11:10:32");
    QTest::newRow("yyyymmHHMMss") << QStringLiteral("ret = app.alert( util.printd( \"yyyy\\\\mm HH:MM:ss\", date ) );") << QStringLiteral("2010\\01 11:10:32");
    QTest::newRow("0") << QStringLiteral("ret = app.alert( util.printd( 0, date ) );") << QStringLiteral("D:20100105111032");
    QTest::newRow("1") << QStringLiteral("ret = app.alert( util.printd( 1, date ) );") << QStringLiteral("2010.01.05 11:10:32");

    QDate date(2010, 1, 5);
    QTest::newRow("2") << QStringLiteral("ret = app.alert( util.printd( 2, date ) );") << QString(date.toString(locale.dateFormat(QLocale::ShortFormat)) + QStringLiteral(" 11:10:32\u202FAM"));
}

void JSFunctionsTest::testPrintD()
{
    QFETCH(QString, script);
    QFETCH(QString, result);

    QVERIFY(script.contains(QLatin1String("printd")));
    PrintDHelper test(m_document, script, result);
}

void JSFunctionsTest::cleanupTestCase()
{
    m_document->closeDocument();
    delete m_document;
}

QTEST_MAIN(JSFunctionsTest)
#include "jsfunctionstest.moc"
