/* This file is part of the KDE project
   Copyright (C) 2014 Alex Richardson <arichardson.kde@gmail.com>

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Library General Public
   License as published by the Free Software Foundation; either
   version 2 of the License, or (at your option) any later version.

   This library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Library General Public License for more details.

   You should have received a copy of the GNU Library General Public License
   along with this library; see the file COPYING.LIB.  If not, write to
   the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
   Boston, MA 02110-1301, USA.
*/

#include <QObject>
#include <QProcess>
#include <QTemporaryFile>
#include <QTest>
#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>

namespace QTest
{

template<> inline char *toString(const QJsonValue &val)
{
    // simply reuse the QDebug representation
    QString result;
    QDebug(&result) << val;
    return QTest::toString(result);
}

}

class DesktopToJsonTest : public QObject
{
    Q_OBJECT

private:
    void compareJson(const QJsonObject& actual, const QJsonObject& expected) {
        for (auto it = actual.constBegin(); it != actual.constEnd(); ++it) {
            if (expected.constFind(it.key()) == expected.constEnd()) {
                qCritical() << "Result has key" << it.key() << "which is not expected!";
                QFAIL("Invalid output");
            }
            if (it.value().isObject() && expected.value(it.key()).isObject()) {
                compareJson(it.value().toObject(), expected.value(it.key()).toObject());
            } else {
                QCOMPARE(it.value(), expected.value(it.key()));
            }
        }
        for (auto it = expected.constBegin(); it != expected.constEnd(); ++it) {
            if (actual.constFind(it.key()) == actual.constEnd()) {
                qCritical() << "Result is missing key" << it.key();
                QFAIL("Invalid output");
            }
            if (it.value().isObject() && actual.value(it.key()).isObject()) {
                compareJson(it.value().toObject(), actual.value(it.key()).toObject());
            } else {
                QCOMPARE(it.value(), actual.value(it.key()));
            }
        }
    }

private Q_SLOTS:

    void testDesktopToJson_data()
    {
        QTest::addColumn<QByteArray>("input");
        QTest::addColumn<QJsonObject>("expectedResult");
        QTest::addColumn<bool>("compatibilityMode");
        QTest::addColumn<QStringList>("serviceTypes");

        QJsonObject expectedResult;
        QJsonObject kpluginObj;
        QByteArray input =
                // include an insignificant group
                "[Some Group]\n"
                "Foo=Bar\n"
                "\n"
                "[Desktop Entry]\n"
                // only data inside [Desktop Entry] should be included
                "Name=Example\n"
                //empty lines
                "\n"
                " \n"
                // make sure translations are included:
                "Name[de_DE]=Beispiel\n"
                // ignore comments:
                "#Comment=Comment\n"
                "  #Comment=Comment\n"
                "Categories=foo;bar;a\\;b\n"
                // As the case is significant, the keys Name and NAME are not equivalent:
                "CaseSensitive=ABC\n"
                "CASESENSITIVE=abc\n"
                // Space before and after the equals sign should be ignored:
                "SpacesBeforeEq   =foo\n"
                "SpacesAfterEq=   foo\n"
                //  Space before and after the equals sign should be ignored; the = sign is the actual delimiter.
                // TODO: error in spec (spaces before and after the key??)
                "   SpacesBeforeKey=foo\n"
                "SpacesAfterKey   =foo\n"
                // ignore trailing spaces
                "TrailingSpaces=foo   \n"
                // However spaces in the value are significant:
                "SpacesInValue=Hello, World!\n"
                //  The escape sequences \s, \n, \t, \r, and \\ are supported for values of
                // type string and localestring, meaning ASCII space, newline, tab,
                // carriage return, and backslash, respectively:
                "EscapeSequences=So\\sme esc\\nap\\te se\\\\qu\\re\\\\nces\n" // make sure that the last n is a literal n not a newline!
                // the standard keys that are used by plugins, make sure correct types are used:
                "X-KDE-PluginInfo-Category=Examples\n" // string key
                "X-KDE-PluginInfo-Version=1.0\n"
                // The multiple values should be separated by a semicolon and the value of the key
                // may be optionally terminated by a semicolon. Trailing empty strings must always
                // be terminated with a semicolon. Semicolons in these values need to be escaped using \;.
                "X-KDE-PluginInfo-Depends=foo,bar,esc\\,aped\n" // string list key
                "X-KDE-ServiceTypes=\n" // empty string list
                "X-KDE-PluginInfo-EnabledByDefault=true\n" // bool key
                // now start a new group
                "[New Group]\n"
                "InWrongGroup=true\n";

        expectedResult[QStringLiteral("Categories")] = QStringLiteral("foo;bar;a\\;b");
        expectedResult[QStringLiteral("CaseSensitive")] = QStringLiteral("ABC");
        expectedResult[QStringLiteral("CASESENSITIVE")] = QStringLiteral("abc");
        expectedResult[QStringLiteral("SpacesBeforeEq")] = QStringLiteral("foo");
        expectedResult[QStringLiteral("SpacesAfterEq")] = QStringLiteral("foo");
        expectedResult[QStringLiteral("SpacesBeforeKey")] = QStringLiteral("foo");
        expectedResult[QStringLiteral("SpacesAfterKey")] = QStringLiteral("foo");
        expectedResult[QStringLiteral("TrailingSpaces")] = QStringLiteral("foo");
        expectedResult[QStringLiteral("SpacesInValue")] = QStringLiteral("Hello, World!");
        expectedResult[QStringLiteral("EscapeSequences")] = QStringLiteral("So me esc\nap\te se\\qu\re\\nces");
        kpluginObj[QStringLiteral("Name")] = QStringLiteral("Example");
        kpluginObj[QStringLiteral("Name[de_DE]")] = QStringLiteral("Beispiel");
        kpluginObj[QStringLiteral("Category")] = QStringLiteral("Examples");
        kpluginObj[QStringLiteral("Dependencies")] = QJsonArray::fromStringList
                (QStringList() << QStringLiteral("foo") << QStringLiteral("bar") << QStringLiteral("esc,aped"));
        kpluginObj[QStringLiteral("ServiceTypes")] = QJsonArray::fromStringList(QStringList());
        kpluginObj[QStringLiteral("EnabledByDefault")] = true;
        kpluginObj[QStringLiteral("Version")] = QStringLiteral("1.0");
        QJsonObject compatResult = expectedResult;
        compatResult[QStringLiteral("Name")] = QStringLiteral("Example");
        compatResult[QStringLiteral("Name[de_DE]")] = QStringLiteral("Beispiel");
        compatResult[QStringLiteral("X-KDE-PluginInfo-Category")] = QStringLiteral("Examples");
        compatResult[QStringLiteral("X-KDE-PluginInfo-Version")] = QStringLiteral("1.0");
        compatResult[QStringLiteral("X-KDE-PluginInfo-Depends")] = QJsonArray::fromStringList
                (QStringList() << QStringLiteral("foo") << QStringLiteral("bar") << QStringLiteral("esc,aped"));
        compatResult[QStringLiteral("X-KDE-ServiceTypes")] = QJsonArray::fromStringList(QStringList());
        compatResult[QStringLiteral("X-KDE-PluginInfo-EnabledByDefault")] = true;

        expectedResult[QStringLiteral("KPlugin")] = kpluginObj;

        QTest::newRow("newFormat") << input << expectedResult << false << QStringList();
        QTest::newRow("compatFormat") << input << compatResult << true << QStringList();


        // test conversion of a currently existing .desktop file (excluding most of the translations):
        QByteArray kdevInput =
                "[Desktop Entry]\n"
                "Type = Service\n"
                "Icon=text-x-c++src\n"
                "Exec=blubb\n"
                "Comment=C/C++ Language Support\n"
                "Comment[fr]=Prise en charge du langage C/C++\n"
                "Comment[it]=Supporto al linguaggio C/C++\n"
                "Name=C++ Support\n"
                "Name[fi]=C++-tuki\n"
                "Name[fr]=Prise en charge du C++\n"
                "GenericName=Language Support\n"
                "GenericName[sl]=Podpora jeziku\n"
                "ServiceTypes=KDevelop/NonExistentPlugin\n"
                "X-KDE-Library=kdevcpplanguagesupport\n"
                "X-KDE-PluginInfo-Name=kdevcppsupport\n"
                "X-KDE-PluginInfo-Category=Language Support\n"
                "X-KDevelop-Version=1\n"
                "X-KDevelop-Language=C++\n"
                "X-KDevelop-Args=CPP\n"
                "X-KDevelop-Interfaces=ILanguageSupport\n"
                "X-KDevelop-SupportedMimeTypes=text/x-chdr,text/x-c++hdr,text/x-csrc,text/x-c++src\n"
                "X-KDevelop-Mode=NoGUI\n"
                "X-KDevelop-LoadMode=AlwaysOn";

        QJsonParseError e;
        QJsonObject kdevExpected = QJsonDocument::fromJson("{\n"
                                                           " \"GenericName\": \"Language Support\",\n"
                                                           " \"GenericName[sl]\": \"Podpora jeziku\",\n"
                                                           " \"KPlugin\": {\n"
                                                           "     \"Category\": \"Language Support\",\n"
                                                           "     \"Description\": \"C/C++ Language Support\",\n"
                                                           "     \"Description[fr]\": \"Prise en charge du langage C/C++\",\n"
                                                           "     \"Description[it]\": \"Supporto al linguaggio C/C++\",\n"
                                                           "     \"Icon\": \"text-x-c++src\",\n"
                                                           "     \"Id\": \"kdevcppsupport\",\n"
                                                           "     \"Name\": \"C++ Support\",\n"
                                                           "     \"Name[fi]\": \"C++-tuki\",\n"
                                                           "     \"Name[fr]\": \"Prise en charge du C++\",\n"
                                                           "     \"ServiceTypes\": [ \"KDevelop/NonExistentPlugin\" ]\n"
                                                           " },\n"
                                                           " \"X-KDevelop-Args\": \"CPP\",\n"
                                                           " \"X-KDevelop-Interfaces\": \"ILanguageSupport\",\n"
                                                           " \"X-KDevelop-Language\": \"C++\",\n"
                                                           " \"X-KDevelop-LoadMode\": \"AlwaysOn\",\n"
                                                           " \"X-KDevelop-Mode\": \"NoGUI\",\n"
                                                           " \"X-KDevelop-SupportedMimeTypes\": \"text/x-chdr,text/x-c++hdr,text/x-csrc,text/x-c++src\",\n"
                                                           " \"X-KDevelop-Version\": \"1\"\n"
                                                           "}\n", &e).object();
        QCOMPARE(e.error, QJsonParseError::NoError);
        QTest::newRow("kdevcpplanguagesupport no servicetype") << kdevInput << kdevExpected << false << QStringList();

        QJsonObject kdevExpectedWithServiceType = QJsonDocument::fromJson("{\n"
                                                                          " \"GenericName\": \"Language Support\",\n"
                                                                          " \"GenericName[sl]\": \"Podpora jeziku\",\n"
                                                                          " \"KPlugin\": {\n"
                                                                          "     \"Category\": \"Language Support\",\n"
                                                                          "     \"Description\": \"C/C++ Language Support\",\n"
                                                                          "     \"Description[fr]\": \"Prise en charge du langage C/C++\",\n"
                                                                          "     \"Description[it]\": \"Supporto al linguaggio C/C++\",\n"
                                                                          "     \"Icon\": \"text-x-c++src\",\n"
                                                                          "     \"Id\": \"kdevcppsupport\",\n"
                                                                          "     \"Name\": \"C++ Support\",\n"
                                                                          "     \"Name[fi]\": \"C++-tuki\",\n"
                                                                          "     \"Name[fr]\": \"Prise en charge du C++\",\n"
                                                                          "     \"ServiceTypes\": [ \"KDevelop/NonExistentPlugin\" ]\n"
                                                                          " },\n"
                                                                          " \"X-KDevelop-Args\": \"CPP\",\n"
                                                                          " \"X-KDevelop-Interfaces\": [\"ILanguageSupport\"],\n"
                                                                          " \"X-KDevelop-Language\": \"C++\",\n"
                                                                          " \"X-KDevelop-LoadMode\": \"AlwaysOn\",\n"
                                                                          " \"X-KDevelop-Mode\": \"NoGUI\",\n"
                                                                          " \"X-KDevelop-SupportedMimeTypes\": [\"text/x-chdr\", \"text/x-c++hdr\", \"text/x-csrc\", \"text/x-c++src\"],\n"
                                                                          " \"X-KDevelop-Version\": 1\n"
                                                                          "}\n", &e).object();
        QCOMPARE(e.error, QJsonParseError::NoError);
        const QString kdevServiceTypePath = QFINDTESTDATA("data/servicetypes/fake-kdevelopplugin.desktop");
        QVERIFY(!kdevServiceTypePath.isEmpty());
        QTest::newRow("kdevcpplanguagesupport with servicetype") << kdevInput << kdevExpectedWithServiceType
                                                                 << false << QStringList(kdevServiceTypePath);
        // test conversion of the X-KDE-PluginInfo-Author + X-KDE-PluginInfo-Email key:
        QByteArray authorInput =
                "[Desktop Entry]\n"
                "Type=Service\n"
                "X-KDE-PluginInfo-Author=Foo Bar\n"
                "X-KDE-PluginInfo-Email=foo.bar@baz.com\n";

        QJsonObject authorsExpected = QJsonDocument::fromJson("{\n"
                                                              " \"KPlugin\": {\n"
                                                              "     \"Authors\": [ { \"Name\": \"Foo Bar\", \"Email\": \"foo.bar@baz.com\" } ]\n"
                                                              " }\n }\n", &e).object();
        QCOMPARE(e.error, QJsonParseError::NoError);
        QTest::newRow("authors") << authorInput << authorsExpected << false << QStringList();

        // test case-insensitive conversion of boolean keys
        const QString boolServiceType = QFINDTESTDATA("data/servicetypes/bool-servicetype.desktop");
        QVERIFY(!boolServiceType.isEmpty());

        QByteArray boolInput1 = "[Desktop Entry]\nType=Service\nX-Test-Bool=true\n";
        QByteArray boolInput2 = "[Desktop Entry]\nType=Service\nX-Test-Bool=TRue\n";
        QByteArray boolInput3 = "[Desktop Entry]\nType=Service\nX-Test-Bool=false\n";
        QByteArray boolInput4 = "[Desktop Entry]\nType=Service\nX-Test-Bool=FALse\n";

        auto boolResultTrue = QJsonDocument::fromJson("{\"KPlugin\":{},\"X-Test-Bool\": true}", &e).object();
        QCOMPARE(e.error, QJsonParseError::NoError);
        auto boolResultFalse = QJsonDocument::fromJson("{\"KPlugin\":{},\"X-Test-Bool\": false}", &e).object();
        QCOMPARE(e.error, QJsonParseError::NoError);
        QTest::newRow("bool true") << boolInput1 << boolResultTrue << false << QStringList(boolServiceType);
        QTest::newRow("bool TRue") << boolInput2 << boolResultTrue << false << QStringList(boolServiceType);
        QTest::newRow("bool false") << boolInput3 << boolResultFalse << false << QStringList(boolServiceType);
        QTest::newRow("bool FALse") << boolInput4 << boolResultFalse << false << QStringList(boolServiceType);

        // test conversion of kcookiejar.desktop (for some reason the wrong boolean values were committed)
        QByteArray kcookiejarInput =
                "[Desktop Entry]\n"
                "Type= Service\n"
                "Name=Cookie Jar\n"
                "Comment=Stores network cookies\n"
                "X-KDE-ServiceTypes=KDEDModule\n"
                "X-KDE-Library=kf5/kded/kcookiejar\n"
                "X-KDE-Kded-autoload=false\n"
                "X-KDE-Kded-load-on-demand=true\n";
        auto kcookiejarResult = QJsonDocument::fromJson(
                    "{\n"
                    "  \"KPlugin\": {\n"
                    "    \"Description\": \"Stores network cookies\",\n"
                    "    \"Name\": \"Cookie Jar\",\n"
                    "    \"ServiceTypes\": [\n"
                    "      \"KDEDModule\"\n"
                    "    ]\n"
                    "  },\n"
                    "\"X-KDE-Kded-autoload\": false,\n"
                    "\"X-KDE-Kded-load-on-demand\": true\n"
                    "}\n", &e).object();
        const QString kdedmoduleServiceType = QFINDTESTDATA("data/servicetypes/fake-kdedmodule.desktop");
        QVERIFY(!kdedmoduleServiceType.isEmpty());
        QTest::newRow("kcookiejar") << kcookiejarInput << kcookiejarResult << false << QStringList(kdedmoduleServiceType);


    }

    void testDesktopToJson()
    {
        QTemporaryFile output;
        QTemporaryFile inputFile;
        QVERIFY(inputFile.open());
        QVERIFY(output.open()); // create the file
        QFETCH(QByteArray, input);
        QFETCH(QJsonObject, expectedResult);
        QFETCH(bool, compatibilityMode);
        QFETCH(QStringList, serviceTypes);
        output.close();
        inputFile.write(input);
        inputFile.flush();
        inputFile.close();
        qDebug() << expectedResult;


        QProcess proc;
        proc.setProgram(QStringLiteral(DESKTOP_TO_JSON_EXE));
        QStringList arguments = QStringList() << QStringLiteral("-i") << inputFile.fileName() << QStringLiteral("-o") << output.fileName();
        if (compatibilityMode) {
            arguments << QStringLiteral("-c");
        }
        for(const QString &s : qAsConst(serviceTypes)) {
            arguments << QStringLiteral("-s") << s;
        }
        proc.setArguments(arguments);
        proc.start();
        QVERIFY(proc.waitForFinished(10000));
        qDebug() << "desktoptojson STDOUT: " <<  proc.readAllStandardOutput().data();
        QByteArray errorOut = proc.readAllStandardError();
        if (!errorOut.isEmpty()) {
            qWarning().nospace() << "desktoptojson STDERR:\n\n" <<  errorOut.constData() << "\n";
        }
        QCOMPARE(proc.exitCode(), 0);
        QVERIFY(output.open());
        QByteArray jsonString = output.readAll();
        qDebug() << "result: " << jsonString;
        QJsonParseError e;
        QJsonDocument doc = QJsonDocument::fromJson(jsonString, &e);
        QCOMPARE(e.error, QJsonParseError::NoError);
        QJsonObject result = doc.object();
        compareJson(result, expectedResult);
        QVERIFY(!QTest::currentTestFailed());
    }
};

QTEST_MAIN(DesktopToJsonTest)

#include "desktoptojsontest.moc"
