/*
    SPDX-FileCopyrightText: 2006 David Faure <faure@kde.org>

    SPDX-License-Identifier: LGPL-2.0-only
*/
#include "kdesktopfiletest.h"
#include "helper.h"
#include <QTemporaryFile>
#include <kconfiggroup.h>
#include <kdesktopfileaction.h>
#include <ksharedconfig.h>

#include "kdesktopfile.h"

#include <QTest>

#if defined(Q_OS_UNIX)
#include <unistd.h>
#endif

QTEST_MAIN(KDesktopFileTest)

void KDesktopFileTest::initTestCase()
{
    QStandardPaths::setTestModeEnabled(true);

    KConfigGroup actionRestrictions(KSharedConfig::openConfig(), QStringLiteral("KDE Action Restrictions"));
    actionRestrictions.writeEntry("someBlockedAction", false);
    actionRestrictions.writeEntry("user/restrictedadmin", false);
    actionRestrictions.writeEntry("user/root", false);
}

void KDesktopFileTest::testRead()
{
    qputenv("TESTVAR", "value");
    QTemporaryFile file(QDir::tempPath() + QStringLiteral("/testReadXXXXXX.desktop"));
    QVERIFY(file.open());
    const QString fileName = file.fileName();
    QTextStream ts(&file);
    ts << "[Desktop Entry]\n"
          "Type= Application\n"
          "Name=My Application\n"
          "Icon = foo\n"
          "URL[$e]=file:///tmp/$TESTVAR\n"
          "MimeType =text/plain;image/png;\n"
          "\n";
    file.close();
    QVERIFY(QFile::exists(fileName));
    QVERIFY(KDesktopFile::isDesktopFile(fileName));
    KDesktopFile df(fileName);
    QCOMPARE(df.readType(), QString::fromLatin1("Application"));
    QVERIFY(df.hasApplicationType());
    QCOMPARE(df.readName(), QString::fromLatin1("My Application"));
    QCOMPARE(df.readIcon(), QString::fromLatin1("foo"));
    QCOMPARE(df.readMimeTypes(), QStringList() << QString::fromLatin1("text/plain") << QString::fromLatin1("image/png"));
    QVERIFY(!df.hasLinkType());
    QCOMPARE(df.fileName(), QFileInfo(fileName).canonicalFilePath());
    QCOMPARE(df.readUrl(), QStringLiteral("file:///tmp/value"));
}

void KDesktopFileTest::testReadLocalized_data()
{
    QTest::addColumn<QLocale>("locale");
    QTest::addColumn<QString>("translation");

    const QString german = QStringLiteral("Meine Anwendung");
    const QString swiss = QStringLiteral("Mein Anwendungsli");

    QTest::newRow("de") << QLocale(QLocale::German) << german;
    QTest::newRow("de_DE") << QLocale(QStringLiteral("de_DE")) << german;
    QTest::newRow("de_DE@bayern") << QLocale(QStringLiteral("de_DE@bayern")) << german;
    QTest::newRow("de@bayern") << QLocale(QStringLiteral("de@bayern")) << german;
    QTest::newRow("de@freiburg") << QLocale(QStringLiteral("de@freiburg")) << QStringLiteral("Mein Anwendungsle");
    // For CH we have a special translation
    QTest::newRow("de_CH") << QLocale(QLocale::German, QLocale::Switzerland) << swiss;
    QTest::newRow("de_CH@bern") << QLocale(QStringLiteral("de_CH@bern")) << swiss;
    // Austria should fall back to "de"
    QTest::newRow("de_AT") << QLocale(QLocale::German, QLocale::Austria) << german;
    QTest::newRow("de_AT@tirol") << QLocale(QStringLiteral("de_AT@tirol")) << german;
    // no translation for French
    QTest::newRow("fr") << QLocale(QLocale::French) << QStringLiteral("My Application");
}

void KDesktopFileTest::testReadLocalized()
{
    QTemporaryFile file(QDir::tempPath() + QStringLiteral("/testReadLocalizedXXXXXX.desktop"));
    QVERIFY(file.open());
    const QString fileName = file.fileName();
    QTextStream ts(&file);
    ts << "[Desktop Entry]\n"
          "Type=Application\n"
          "Name=My Application\n"
          "Name[de]=Meine Anwendung\n"
          "Name[de@freiburg]=Mein Anwendungsle\n"
          "Name[de_CH]=Mein Anwendungsli\n"
          "Icon=foo\n"
          "\n";
    file.close();
    QVERIFY(QFile::exists(fileName));
    QVERIFY(KDesktopFile::isDesktopFile(fileName));

    DefaultLocale defaultLocale;

    QFETCH(QLocale, locale);
    QLocale::setDefault(locale);
    KDesktopFile df(fileName);

    QEXPECT_FAIL("de@freiburg", "QLocale doesn't support modifiers", Continue);
    QTEST(df.readName(), "translation");
}

void KDesktopFileTest::testSuccessfulTryExec()
{
    QTemporaryFile file(QDir::tempPath() + QStringLiteral("/testSuccessfulTryExecXXXXXX.desktop"));
    QVERIFY(file.open());
    const QString fileName = file.fileName();
    QTextStream ts(&file);
    ts << "[Desktop Entry]\n"
          "TryExec=whoami\n"
          "\n";
    file.close();
    QVERIFY(QFile::exists(fileName));
    KDesktopFile df(fileName);
    QCOMPARE(df.tryExec(), true);
}

void KDesktopFileTest::testUnsuccessfulTryExec()
{
    QTemporaryFile file(QDir::tempPath() + QStringLiteral("/testUnsuccessfulTryExecXXXXXX.desktop"));
    QVERIFY(file.open());
    const QString fileName = file.fileName();
    QTextStream ts(&file);
    ts << "[Desktop Entry]\n"
          "TryExec=/does/not/exist\n"
          "\n";
    file.close();
    QVERIFY(QFile::exists(fileName));
    KDesktopFile df(fileName);
    QCOMPARE(df.tryExec(), false);
}

void KDesktopFileTest::testActionGroup()
{
    QTemporaryFile file(QDir::tempPath() + QStringLiteral("/testActionGroupXXXXXX.desktop"));
    QVERIFY(file.open());
    const QString fileName = file.fileName();
    QTextStream ts(&file);
    ts << "[Desktop Entry]\n"
          // make sure escaping of ';' using "\;" works
          "Actions=encrypt;_SEPARATOR_;semi\\;colon;decrypt;noname;missing;\n"
          "[Desktop Action encrypt]\n"
          "Name=Encrypt file\n"
          "[Desktop Action decrypt]\n"
          "Name=Decrypt file\n"
          // no escaping needed in group header
          "[Desktop Action semi;colon]\n"
          "Name=With semicolon\n"
          // has no name, so ignore it
          "[Desktop Action noname]\n"
          "Exec=ls\n"
          // missing has no entry at all
          "\n";
    file.close();
    QVERIFY(QFile::exists(fileName));
    KDesktopFile df(fileName);
    QCOMPARE(df.readType(), QString());
    QCOMPARE(df.fileName(), QFileInfo(fileName).canonicalFilePath());
    QCOMPARE(df.readActions(),
             (QStringList{QStringLiteral("encrypt"),
                          QStringLiteral("_SEPARATOR_"),
                          QStringLiteral("semi;colon"),
                          QStringLiteral("decrypt"),
                          QStringLiteral("noname"),
                          QStringLiteral("missing")}));

    QCOMPARE(df.hasActionGroup(QStringLiteral("encrypt")), true);
    QCOMPARE(df.hasActionGroup(QStringLiteral("semi;colon")), true);
    QCOMPARE(df.hasActionGroup(QStringLiteral("decrypt")), true);
    QCOMPARE(df.hasActionGroup(QStringLiteral("noname")), true);
    QCOMPARE(df.hasActionGroup(QStringLiteral("missing")), false);
    QCOMPARE(df.hasActionGroup(QStringLiteral("doesnotexist")), false);

    // "noname" and "missing" should not be on this list
    const auto actions = df.actions();
    QCOMPARE(actions.size(), 4);
    QVERIFY(std::all_of(actions.begin(), actions.end(), [&fileName](const KDesktopFileAction &action) {
        return action.desktopFilePath() == fileName;
    }));
    QCOMPARE(actions.at(0).actionsKey(), QStringLiteral("encrypt"));
    QCOMPARE(actions.at(0).name(), QStringLiteral("Encrypt file"));
    QCOMPARE(actions.at(1).isSeparator(), true);
    QCOMPARE(actions.at(2).actionsKey(), QStringLiteral("semi;colon"));
    QCOMPARE(actions.at(2).name(), QStringLiteral("With semicolon"));
    QCOMPARE(actions.at(3).name(), QStringLiteral("Decrypt file"));
    QCOMPARE(actions.at(3).actionsKey(), QStringLiteral("decrypt"));
    QCOMPARE(actions.at(3).name(), QStringLiteral("Decrypt file"));
}

void KDesktopFileTest::testIsAuthorizedDesktopFile()
{
    QTemporaryFile file(QDir::tempPath() + QStringLiteral("/testAuthXXXXXX.desktop"));
    QVERIFY(file.open());
    const QString fileName = file.fileName();
    QTextStream ts(&file);
    ts << "[Desktop Entry]\n"
          "Type=Application\n"
          "Name=My Application\n"
          "Exec=kfoo\n"
          "\n";
    file.close();
    QVERIFY(QFile::exists(fileName));

#if defined(Q_OS_UNIX)
    // Try to fix test failing in docker running as root
    const QFileInfo entryInfo(fileName);
    if (entryInfo.ownerId() == 0) {
        // try to find a valid user/group combination
        for (int i = 1; i < 100; ++i) {
            if (chown(fileName.toLocal8Bit().constData(), i, i) == 0) {
                break;
            }
        }
        const QFileInfo entryInfo1(fileName);
        if (entryInfo1.ownerId() == 0) {
            QEXPECT_FAIL("", "Running test as root and could not find a non root user to change the ownership of the file too", Continue);
        }
    }
#endif

    QVERIFY(!KDesktopFile::isAuthorizedDesktopFile(fileName));

    const QString autostartFile = QStandardPaths::locate(QStandardPaths::GenericConfigLocation, QStringLiteral("autostart/plasma-desktop.desktop"));
    if (!autostartFile.isEmpty()) {
        QVERIFY(KDesktopFile::isAuthorizedDesktopFile(autostartFile));
    } else {
        qWarning("Skipping test for plasma-desktop.desktop, not found. kde-workspace not installed?");
    }
}

void KDesktopFileTest::testTryExecWithAuthorizeAction()
{
    {
        QTemporaryFile file(QDir::tempPath() + QStringLiteral("/testAuthActionXXXXXX.desktop"));
        QVERIFY(file.open());
        const QString fileName = file.fileName();
        QTextStream ts(&file);
        ts << "[Desktop Entry]\n"
              "Type=Application\n"
              "Name=My Application\n"
              "Exec=kfoo\n"
              "TryExec=";
#ifdef Q_OS_WIN
        ts << "cmd\n";
#else
        ts << "bash\n";
#endif
        ts << "X-KDE-AuthorizeAction=someAction"
              "\n";
        file.close();

        KDesktopFile desktopFile(fileName);
        QVERIFY(desktopFile.tryExec());
    }
    {
        QTemporaryFile file(QDir::tempPath() + QStringLiteral("/testAuthActionXXXXXX.desktop"));
        QVERIFY(file.open());
        const QString fileName = file.fileName();
        QTextStream ts(&file);
        ts << "[Desktop Entry]\n"
              "Type=Application\n"
              "Name=My Application\n"
              "Exec=kfoo\n"
              "TryExec=bash\n"
              "X-KDE-AuthorizeAction=someBlockedAction"
              "\n";
        file.close();

        KDesktopFile desktopFile(fileName);
        QVERIFY(!desktopFile.tryExec());
    }
}

void KDesktopFileTest::testLocateLocal_data()
{
    const QString systemConfigLocation = QStandardPaths::standardLocations(QStandardPaths::GenericConfigLocation).constLast();
    const QString writableConfigLocation = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation);
    const QString systemDataLocation = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation).constLast();
    const QString writableDataLocation = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);

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

    QTest::newRow("configLocation, system-wide") << systemConfigLocation + QLatin1String{"/test.desktop"}
                                                 << writableConfigLocation + QLatin1String{"/test.desktop"};
    QTest::newRow("autostart, system-wide") << systemConfigLocation + QLatin1String{"/autostart/test.desktop"}
                                            << writableConfigLocation + QLatin1String{"/autostart/test.desktop"};
    QTest::newRow("dataLocation, system-wide") << systemDataLocation + QLatin1String{"/test.desktop"} << writableDataLocation + QLatin1String{"/test.desktop"};
    QTest::newRow("applications, system-wide") << systemDataLocation + QLatin1String{"/applications/test.desktop"}
                                               << writableDataLocation + QLatin1String{"/applications/test.desktop"};
    QTest::newRow("desktop-directories, system-wide") << systemDataLocation + QLatin1String{"/desktop-directories/test.directory"}
                                                      << writableDataLocation + QLatin1String{"/desktop-directories/test.directory"};
    QTest::newRow("configLocation, writable") << writableConfigLocation + QLatin1String{"/test.desktop"}
                                              << writableConfigLocation + QLatin1String{"/test.desktop"};
    QTest::newRow("autostart, writable") << writableConfigLocation + QLatin1String{"/autostart/test.desktop"}
                                         << writableConfigLocation + QLatin1String{"/autostart/test.desktop"};
    QTest::newRow("dataLocation, writable") << writableDataLocation + QLatin1String{"/test.desktop"} << writableDataLocation + QLatin1String{"/test.desktop"};
    QTest::newRow("applications, writable") << writableDataLocation + QLatin1String{"/applications/test.desktop"}
                                            << writableDataLocation + QLatin1String{"/applications/test.desktop"};
    QTest::newRow("desktop-directories, writable") << writableDataLocation + QLatin1String{"/desktop-directories/test.directory"}
                                                   << writableDataLocation + QLatin1String{"/desktop-directories/test.directory"};
    QTest::newRow("unknown location") << QStringLiteral("/test.desktop") << writableDataLocation + QLatin1String{"/test.desktop"};
}

void KDesktopFileTest::testLocateLocal()
{
    QFETCH(QString, path);
    QFETCH(QString, result);

    QCOMPARE(KDesktopFile::locateLocal(path), result);
}

void KDesktopFileTest::testWritePrimaryGroupFirst()
{
    QTemporaryFile tmpFile(QDir::tempPath() + QStringLiteral("/testWritePrimaryGroupFirstXXXXXX.desktop"));
    QVERIFY(tmpFile.open());
    KDesktopFile df(tmpFile.fileName());

    df.actionGroup(QStringLiteral("AnAction")).writeEntry("Exec", "testapp --an-action");
    df.desktopGroup().writeEntry("Name", "Test App");

    df.sync();

    auto lines = readLinesFrom(tmpFile.fileName());
    QCOMPARE(lines.at(0), "[Desktop Entry]\n");
    QVERIFY(lines.contains("[Desktop Action AnAction]\n"));
}

void KDesktopFileTest::testSubstituteUidAdminAccountFallback()
{
    // When X-KDE-SubstituteUID is true and X-KDE-Username is absent, the
    // ADMIN_ACCOUNT environment variable controls which username is passed to
    // KAuthorized::authorize().

    QTemporaryFile file(QDir::tempPath() + QStringLiteral("/testSubstituteUidXXXXXX.desktop"));
    QVERIFY(file.open());
    QTextStream ts(&file);
    ts << "[Desktop Entry]\n"
          "Type=Application\n"
          "Name=Privileged App\n"
          "Exec=ls\n"
          "X-KDE-SubstituteUID=true\n"
          "\n";
    file.close();

    // "user/restrictedadmin" is blocked in initTestCase via KDE Action Restrictions.

    // When ADMIN_ACCOUNT names a blocked user, tryExec() must return false.
    qputenv("ADMIN_ACCOUNT", "restrictedadmin");
    {
        KDesktopFile df(file.fileName());
        QCOMPARE(df.tryExec(), false);
    }

    // When ADMIN_ACCOUNT names a user that is not restricted, tryExec() must
    // return true.
    qputenv("ADMIN_ACCOUNT", "anotheradmin");
    {
        KDesktopFile df(file.fileName());
        QCOMPARE(df.tryExec(), true);
    }

    // When ADMIN_ACCOUNT is unset, "root" is the default.  "user/root" is
    // blocked in initTestCase, so tryExec() must return false.
    qunsetenv("ADMIN_ACCOUNT");
    {
        KDesktopFile df(file.fileName());
        QCOMPARE(df.tryExec(), false);
    }
}

#include "moc_kdesktopfiletest.cpp"
