/*
    This file is part of the KDE libraries
    SPDX-FileCopyrightText: 2021 Slava Aseev <nullptrnine@basealt.ru>

    SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "fdo_secrets_test.h"
#include "../kwalletfreedesktopsession.h"
#include "mockkwalletd.cpp"
// cannot be in mockkwalletd.cpp, as CMake's automoc does not look there
#include "moc_ksecretd.cpp"
#include "moc_ktimeout.cpp"
#include "static_mock.hpp"

#include <QJsonDocument>

void FdoSecretsTest::initTestCase()
{
    if (qEnvironmentVariableIntValue("KDECI_CANNOT_CREATE_WINDOWS")) {
        QSKIP("These tests are unstable on Windows CI, skipping.");
    }

    QStandardPaths::setTestModeEnabled(true);
    static QCA::Initializer init{};
}

void FdoSecretsTest::serviceStaticFunctions()
{
    auto labels = Testset<QString, FdoUniqueLabel>{
        {"label", {"label", -1}},
        {"label__0_", {"label", 0}},
        {"label___1_", {"label_", 1}},
        {"__0_label___200_", {"__0_label_", 200}},
    };

    runTestset(FdoUniqueLabel::fromName, labels);
    runRevTestset(
        [](const FdoUniqueLabel &l) {
            return l.toName();
        },
        labels);

    runTestset(KWalletFreedesktopService::wrapToCollectionPath,
               Testset<QString, QString>{
                   {"/org/freedesktop/secrets/collection/abcd", "/org/freedesktop/secrets/collection/abcd"},
                   {"/org/freedesktop/secrets/collection/abcd/2", "/org/freedesktop/secrets/collection/abcd"},
                   {"/org/freedesktop/secrets/collection/abcd/2/2/3/4", "/org/freedesktop/secrets/collection/abcd"},
               });

    QCOMPARE(KWalletFreedesktopService::nextPromptPath().path(), "/org/freedesktop/secrets/prompt/p0");
    QCOMPARE(KWalletFreedesktopService::nextPromptPath().path(), "/org/freedesktop/secrets/prompt/p1");
}

void FdoSecretsTest::collectionStaticFunctions()
{
    auto dirNameTestset = Testset<EntryLocation, FdoUniqueLabel>{
        {{FDO_SECRETS_DEFAULT_DIR, "entry1"}, {"entry1", -1}},
        {{FDO_SECRETS_DEFAULT_DIR, "entry__3_"}, {"entry", 3}},
        {{"Passwords", "password__"}, {"Passwords/password__", -1}},
        {{"Passwords__3_", "password__200_"}, {"Passwords__3_/password", 200}},
        {{"", "password"}, {"/password", -1}},
        {{"", "/"}, {"//", -1}},
        {{FDO_SECRETS_DEFAULT_DIR, "/"}, {"/", -1}},
        {{FDO_SECRETS_DEFAULT_DIR, "/__2_"}, {"/", 2}},
        {{"https:", "/foobar.org/"}, {"https://foobar.org/", -1}},
        {{"https:", "/foobar.org/__80_"}, {"https://foobar.org/", 80}},
    };

    runTestset(
        [](const EntryLocation &l) {
            return l.toUniqueLabel();
        },
        dirNameTestset);
    runRevTestset(
        [](const FdoUniqueLabel &l) {
            return l.toEntryLocation();
        },
        dirNameTestset);
}

void FdoSecretsTest::cleanup()
{
    SET_FUNCTION_RESULT(KSecretD::wallets, QStringList());
}

void FdoSecretsTest::precreatedWallets()
{
    const QStringList wallets = {"wallet1", "wallet2", "wallet2__0_", "wallet2__1_"};
    SET_FUNCTION_RESULT(KSecretD::wallets, wallets);
    SET_FUNCTION_RESULT_OVERLOADED(KSecretD::isOpen, true, bool(KSecretD::*)(int));

    std::unique_ptr<KSecretD> kwalletd{new KSecretD};
    std::unique_ptr<KWalletFreedesktopService> service{new KWalletFreedesktopService(kwalletd.get())};

    QCOMPARE(wallets.size(), service->collections().size());
    for (const auto &walletName : wallets) {
        auto collection = service->getCollectionByWalletName(walletName);
        QVERIFY(collection);
        QVERIFY(collection->label() == "wallet1" || collection->label() == "wallet2");
    }

    auto firstCollection = service->getCollectionByWalletName(wallets.front());
    auto &item1 = firstCollection->pushNewItem(FdoUniqueLabel{"item1", -1}, QDBusObjectPath(firstCollection->fdoObjectPath().path() + "/0"));
    QCOMPARE(item1.fdoObjectPath().path(), (firstCollection->fdoObjectPath().path() + "/0"));
    QCOMPARE(&item1, service->getItemByObjectPath(item1.fdoObjectPath()));
}

void FdoSecretsTest::aliases()
{
    std::unique_ptr<KSecretD> kwalletd{new KSecretD};
    std::unique_ptr<KWalletFreedesktopService> service{new KWalletFreedesktopService(kwalletd.get())};

    service->createCollectionAlias("alias", "walletName");
    service->createCollectionAlias("alias2", "walletName");
    service->createCollectionAlias("alias3", "walletName300");
    service->updateCollectionAlias("alias3", "walletName");
    QSet<QString> checkAliases = {"alias", "alias2", "alias3"};
    const QStringList aliases = service->readAliasesFor("walletName");
    for (const auto &alias : aliases)
        checkAliases.remove(alias);
    QVERIFY(checkAliases.isEmpty());

    service->removeAlias("alias");
    service->removeAlias("alias2");
    service->removeAlias("alias3");
    QVERIFY(service->readAliasesFor("walletName").isEmpty());
}

struct SetupSessionT {
    QDBusObjectPath sessionPath;
    QCA::SymmetricKey symmetricKey;
    int commonSecretSize = 0;
    QByteArray error;
};

#define SETUP_SESSION_VERIFY(cond)                                                                                                                             \
    do {                                                                                                                                                       \
        if (!(cond))                                                                                                                                           \
            return SetupSessionT{QDBusObjectPath(), QCA::SymmetricKey(), 0, #cond};                                                                            \
    } while (false)

static QCA::SymmetricKey deriveLibsecretStyleSymmetricKey(const QCA::SecureArray &commonSecret)
{
    auto paddedSecret = commonSecret.toByteArray();
    if (paddedSecret.size() < FDO_DH_PUBLIC_KEY_SIZE) {
        paddedSecret.prepend(FDO_DH_PUBLIC_KEY_SIZE - paddedSecret.size(), '\0');
    }

    return QCA::HKDF().makeKey(QCA::SecureArray(paddedSecret), {}, {}, FDO_SECRETS_CIPHER_KEY_SIZE);
}

SetupSessionT setupSession(KWalletFreedesktopService *service, bool requireShortDhSecret = false, bool requireMsbSetClientPublicKey = false)
{
    QCA::KeyGenerator keygen;
    auto dlGroup = QCA::DLGroup(keygen.createDLGroup(QCA::IETF_1024));
    if (dlGroup.isNull()) {
        return SetupSessionT{QDBusObjectPath(), QCA::SymmetricKey(), 0, "createDLGroup failed, maybe libqca-ossl is missing"};
    }

    for (int attempt = 0; attempt < 4096; ++attempt) {
        SetupSessionT result;
        auto privateKey = QCA::PrivateKey(keygen.createDH(dlGroup));
        auto publicKey = QCA::PublicKey(privateKey);

        auto pubKeyBytes = KWalletFreedesktopSessionAlgorithmDhAes::normalizeUnsignedDhValue(publicKey.toDH().y().toArray().toByteArray());
        if (requireMsbSetClientPublicKey && (static_cast<unsigned char>(pubKeyBytes.front()) & 0x80U) == 0) {
            continue;
        }
        auto sessionPubKeyVariant = service->OpenSession("dh-ietf1024-sha256-aes128-cbc-pkcs7", QDBusVariant(pubKeyBytes), result.sessionPath);
        SETUP_SESSION_VERIFY(result.sessionPath.path() != "/");
        SETUP_SESSION_VERIFY(sessionPubKeyVariant.variant().canConvert<QByteArray>());

        auto servicePublicKeyBytes = sessionPubKeyVariant.variant().toByteArray();
        SETUP_SESSION_VERIFY(!servicePublicKeyBytes.isEmpty());

        auto servicePublicKey = QCA::DHPublicKey(dlGroup, KWalletFreedesktopSessionAlgorithmDhAes::decodeUnsignedDhValue(servicePublicKeyBytes));
        auto commonSecret = privateKey.deriveKey(servicePublicKey);
        result.commonSecretSize = commonSecret.size();
        if (requireShortDhSecret && result.commonSecretSize >= FDO_DH_PUBLIC_KEY_SIZE) {
            continue;
        }

        result.symmetricKey = deriveLibsecretStyleSymmetricKey(commonSecret);
        return result;
    }

    return SetupSessionT{QDBusObjectPath(), QCA::SymmetricKey(), 0, ""};
}

void FdoSecretsTest::sessionAlgorithmStaticFunctions()
{
    const QByteArray shortValue(127, '\x5a');
    const QByteArray fullValue(FDO_DH_PUBLIC_KEY_SIZE, '\x7f');
    const QByteArray highBitValue(FDO_DH_PUBLIC_KEY_SIZE, static_cast<char>(0x80));
    const QByteArray signExtendedHighBitValue = QByteArray(1, '\0') + highBitValue;

    auto normalizedShort = KWalletFreedesktopSessionAlgorithmDhAes::normalizeUnsignedDhValue(shortValue);
    QCOMPARE(normalizedShort.size(), FDO_DH_PUBLIC_KEY_SIZE);
    QCOMPARE(normalizedShort.front(), '\0');
    QCOMPARE(normalizedShort.mid(1), shortValue);

    QCOMPARE(KWalletFreedesktopSessionAlgorithmDhAes::normalizeUnsignedDhValue(fullValue), fullValue);
    QCOMPARE(KWalletFreedesktopSessionAlgorithmDhAes::normalizeUnsignedDhValue(signExtendedHighBitValue), highBitValue);

    auto decodedHighBit = KWalletFreedesktopSessionAlgorithmDhAes::decodeUnsignedDhValue(highBitValue);
    QCOMPARE(KWalletFreedesktopSessionAlgorithmDhAes::normalizeUnsignedDhValue(decodedHighBit.toArray().toByteArray()), highBitValue);
}

static QByteArray shortDhPublicKey()
{
    // clang-format off
    return QByteArray::fromBase64("bt4u/O+RSvVNOTszD/ThOeSNd+Ji7OVENIyRXsY45OzdocCMKZ8hSzxSmpqzwJL57I2yZtB897bBab1ujRK5tat6IIkMdOPf4WgExoU8c54VV1ohbehKraUktN1UzrDPsDrB4hJtm2GwFULMJXoEcdpkYaJSGEGWvjzlhrZZZA==");
    // clang-format on
}

void FdoSecretsTest::items()
{
    const QStringList wallets = {"wallet1"};
    const QStringList folders = {FDO_SECRETS_DEFAULT_DIR};
    const QStringList entries = {"item1", "item2", "item3"};
    SET_FUNCTION_RESULT(KSecretD::wallets, wallets);
    SET_FUNCTION_RESULT(KSecretD::folderList, folders);
    SET_FUNCTION_RESULT(KSecretD::entryList, entries);

    SET_FUNCTION_IMPL(KSecretD::entryType, [](int, const QString &, const QString &key, const QString &) -> int {
        if (key == "item1")
            return KWallet::Wallet::Password;
        else if (key == "item2")
            return KWallet::Wallet::Stream;
        else if (key == "item3")
            return KWallet::Wallet::Map;

        QTEST_ASSERT(false);
        return KWallet::Wallet::Unknown;
    });

    QString _secretHolder1 = "It's a password";
    QByteArray _secretHolder2;
    QByteArray _secretHolder3;

    {
        QByteArray a = "It's a";
        QString b = "stream";

        QDataStream ds{&_secretHolder2, QIODevice::WriteOnly};
        ds << a << b;
    }

    {
        StrStrMap map;
        map["it's a"] = "map";

        QDataStream ds{&_secretHolder3, QIODevice::WriteOnly};
        ds << map;
    }

    SET_FUNCTION_IMPL(KSecretD::readPassword, [&](int, const QString &, const QString &key, const QString &) -> QString {
        QTEST_ASSERT(key == "item1");
        return _secretHolder1;
    });

    SET_FUNCTION_IMPL(KSecretD::readEntry, [&](int, const QString &, const QString &key, const QString &) -> QByteArray {
        QTEST_ASSERT(key == "item2");
        return _secretHolder2;
    });

    SET_FUNCTION_IMPL(KSecretD::readMap, [&](int, const QString &, const QString &key, const QString &) -> QByteArray {
        QTEST_ASSERT(key == "item3");
        return _secretHolder3;

        QJsonObject obj = QJsonDocument::fromJson(_secretHolder3).object();

        QMap<QString, QString> map;
        for (auto it = obj.constBegin(); it != obj.constEnd(); it++) {
            map[it.key()] = it.value().toString();
        }

        QByteArray data;
        QDataStream ds(&data, QIODevice::WriteOnly);
        ds << map;

        return data;
    });

    SET_FUNCTION_IMPL(KSecretD::writePassword, [&](int, const QString &, const QString &key, const QString &value, const QString &) -> int {
        QTEST_ASSERT(key == "item1");
        _secretHolder1 = value;
        return 0;
    });

    using writeEntryT = int (KSecretD::*)(int, const QString &, const QString &, const QByteArray &, int, const QString &);
    SET_FUNCTION_IMPL_OVERLOADED(KSecretD::writeEntry,
                                 writeEntryT,
                                 [&](int, const QString &, const QString &key, const QByteArray &value, int, const QString &) -> int {
                                     QTEST_ASSERT(key == "item3" || key == "item2");
                                     if (key == "item2")
                                         _secretHolder2 = value;
                                     else
                                         _secretHolder3 = value;
                                     return 0;
                                 });

    std::unique_ptr<KSecretD> kwalletd{new KSecretD};
    std::unique_ptr<KWalletFreedesktopService> service{new KWalletFreedesktopService(kwalletd.get())};

    auto collection = service->getCollectionByWalletName("wallet1");
    QVERIFY(collection);

    /* Write some attributes */
    {
        collection->itemAttributes().newItem({FDO_SECRETS_DEFAULT_DIR, "item1"});
        collection->itemAttributes().newItem({FDO_SECRETS_DEFAULT_DIR, "item2"});

        const EntryLocation entryLoc = {FDO_SECRETS_DEFAULT_DIR, "item3"};
        collection->itemAttributes().newItem(entryLoc);
        collection->itemAttributes().setParam(entryLoc, FDO_KEY_CREATED, 100200300ULL);
        collection->itemAttributes().setParam(entryLoc, FDO_KEY_MODIFIED, 100200301ULL);
        auto attribs = collection->itemAttributes().getAttributes(entryLoc);
        attribs["Attrib1"] = "value1";
        attribs["Attrib2"] = "value2";
        collection->itemAttributes().setAttributes(entryLoc, attribs);
        auto &newItem = collection->pushNewItem(entryLoc.toUniqueLabel(), collection->nextItemPath());
        newItem.setAttributes(attribs);
    }

    /* Create collection */
    using OpenAsyncT = int (KSecretD::*)(const QString &, qlonglong, const QString &, bool, const QDBusConnection &, const QDBusMessage &);
    bool openAsyncCalled = false;
    SET_FUNCTION_IMPL_OVERLOADED(KSecretD::openAsync,
                                 OpenAsyncT,
                                 [&](const QString &, qlonglong, const QString &, bool, const QDBusConnection &, const QDBusMessage &) -> int {
                                     openAsyncCalled = true;
                                     return 0;
                                 });

    QDBusObjectPath promptPath;
    service->Unlock({collection->fdoObjectPath()}, promptPath);
    auto prompt = service->getPromptByObjectPath(promptPath);
    QVERIFY(prompt);
    prompt->Prompt("wndid");
    Q_EMIT kwalletd->walletAsyncOpened(0, 0);
    SET_FUNCTION_RESULT_OVERLOADED(KSecretD::isOpen, true, bool(KSecretD::*)(int));
    QVERIFY(!collection->locked());

    auto item1 = collection->findItemByEntryLocation({FDO_SECRETS_DEFAULT_DIR, "item1"});
    auto item2 = collection->findItemByEntryLocation({FDO_SECRETS_DEFAULT_DIR, "item2"});
    auto item3 = collection->findItemByEntryLocation({FDO_SECRETS_DEFAULT_DIR, "item3"});
    QVERIFY(item1 && item2 && item3);

    auto message = QDBusMessage::createSignal("dummy", "dummy", "dummy");
    auto [sessionPath, symmetricKey, commonSecretSize, errorStr] = setupSession(service.get());
    QVERIFY2(errorStr.isEmpty(), errorStr.constData());
    Q_UNUSED(commonSecretSize);

    /* Check secrets */
    auto secret1 = item1->GetSecret(sessionPath);
    service->desecret(message, secret1);
    QCOMPARE(secret1.value.toByteArray(), "It's a password");
    // QCOMPARE(secret1.mimeType, "text/plain");

    auto secret2 = item2->GetSecret(sessionPath);
    service->desecret(message, secret2);
    QByteArray secretBytes = secret2.value.toByteArray();
    QDataStream ds{secretBytes};
    QByteArray a;
    QString b;
    ds >> a >> b;

    QCOMPARE(secret2.mimeType, "application/octet-stream");
    QCOMPARE(a, "It's a");
    QCOMPARE(b, "stream");

    auto secret3 = item3->GetSecret(sessionPath);
    service->desecret(message, secret3);
    auto bytes3 = secret3.value.toByteArray();

    QJsonObject obj = QJsonDocument::fromJson(bytes3).object();
    StrStrMap map3;
    for (auto it = obj.constBegin(); it != obj.constEnd(); it++) {
        map3[it.key()] = it.value().toString();
    }

    QVERIFY(map3.find("it's a") != map3.end() && map3["it's a"] == "map");
    QCOMPARE(item3->created(), 100200300);
    QCOMPARE(item3->modified(), 100200301);
    QCOMPARE(item3->attributes()["Attrib1"], "value1");
    QCOMPARE(item3->attributes()["Attrib2"], "value2");

    /* Set new secrets */
    secret1.value = QByteArray("It's a new password");
    secret1.mimeType = "text/plain";
    service->ensecret(message, secret1);
    item1->SetSecret(secret1);
    secret1 = item1->GetSecret(sessionPath);
    service->desecret(message, secret1);
    QCOMPARE(secret1.value.toByteArray(), "It's a new password");
    QCOMPARE(secret1.mimeType, "text/plain");

    secret2.value = QByteArray("It's a new secret");
    secret2.mimeType = "application/octet-stream";
    service->ensecret(message, secret2);
    item2->SetSecret(secret2);
    auto attribs = item2->attributes();
    attribs["newAttrib"] = ")))";
    item2->setAttributes(attribs);

    secret2 = item2->GetSecret(sessionPath);
    service->desecret(message, secret2);
    QCOMPARE(secret2.value.toByteArray(), "It's a new secret");
    QCOMPARE(item2->attributes()["newAttrib"], ")))");

    /* Search items */
    attribs.clear();
    attribs["Attrib1"] = "value1";
    QList<QDBusObjectPath> lockedItems;
    auto unlockedItems = service->SearchItems(attribs, lockedItems);
    QCOMPARE(unlockedItems.size(), 1);
    QCOMPARE(unlockedItems.front(), item3->fdoObjectPath());
}

void FdoSecretsTest::createLockUnlockCollection()
{
    std::unique_ptr<KSecretD> kwalletd{new KSecretD};
    std::unique_ptr<KWalletFreedesktopService> service{new KWalletFreedesktopService(kwalletd.get())};

    /* Create collection */
    using OpenAsyncT = int (KSecretD::*)(const QString &, qlonglong, const QString &, bool, const QDBusConnection &, const QDBusMessage &);
    bool openAsyncCalled = false;
    SET_FUNCTION_IMPL_OVERLOADED(KSecretD::openAsync,
                                 OpenAsyncT,
                                 [&](const QString &, qlonglong, const QString &, bool, const QDBusConnection &, const QDBusMessage &) -> int {
                                     openAsyncCalled = true;
                                     return 0;
                                 });

    QVariantMap props;
    props["org.freedesktop.Secret.Collection.Label"] = QString("walletName");
    QDBusObjectPath promptPath;
    service->CreateCollection(props, "", promptPath);
    auto prompt = service->getPromptByObjectPath(promptPath);
    QVERIFY(prompt);
    prompt->Prompt("wndid");
    QVERIFY(openAsyncCalled);
    Q_EMIT kwalletd->walletAsyncOpened(0, 0);

    auto createdCollection = service->getCollectionByWalletName("walletName");
    QVERIFY(createdCollection);
    QCOMPARE(createdCollection->label(), "walletName");

    /* Check aliases */
    service->createCollectionAlias("alias", "walletName");
    service->createCollectionAlias("alias2", "walletName");
    service->createCollectionAlias("alias3", "walletName");

    QCOMPARE(service->resolveIfAlias(QStringLiteral(FDO_ALIAS_PATH) + "alias"), createdCollection->fdoObjectPath().path());
    QCOMPARE(service->resolveIfAlias(QStringLiteral(FDO_ALIAS_PATH) + "alias2"), createdCollection->fdoObjectPath().path());
    QCOMPARE(service->resolveIfAlias(QStringLiteral(FDO_ALIAS_PATH) + "alias3"), createdCollection->fdoObjectPath().path());
    QCOMPARE(service->ReadAlias("alias"), createdCollection->fdoObjectPath());

    service->removeAlias("alias");
    service->removeAlias("alias2");
    service->removeAlias("alias3");

    /* Lock/Unlock */
    auto lockedObjects = service->Lock({createdCollection->fdoObjectPath()}, promptPath);
    QCOMPARE(lockedObjects.size(), 1);
    QCOMPARE(lockedObjects.front(), createdCollection->fdoObjectPath());
    SET_FUNCTION_RESULT_OVERLOADED(KSecretD::isOpen, false, bool(KSecretD::*)(int));
    QVERIFY(createdCollection->locked());

    service->Unlock({createdCollection->fdoObjectPath()}, promptPath);
    prompt = service->getPromptByObjectPath(promptPath);
    QVERIFY(prompt);
    openAsyncCalled = false;
    prompt->Prompt("wndid");
    QVERIFY(openAsyncCalled);
    Q_EMIT kwalletd->walletAsyncOpened(0, 0);
    SET_FUNCTION_RESULT_OVERLOADED(KSecretD::isOpen, true, bool(KSecretD::*)(int));
    QVERIFY(!createdCollection->locked());
}

void FdoSecretsTest::session()
{
    std::unique_ptr<KSecretD> kwalletd{new KSecretD};
    std::unique_ptr<KWalletFreedesktopService> service{new KWalletFreedesktopService(kwalletd.get())};

    auto message = QDBusMessage::createSignal("dummy", "dummy", "dummy");
    auto [sessionPath, symmetricKey, commonSecretSize, errorStr] = setupSession(service.get());
    QVERIFY2(errorStr.isEmpty(), errorStr.constData());
    Q_UNUSED(commonSecretSize);

    /* Generate secret */
    auto secret = FreedesktopSecret(sessionPath, QByteArray("It's a secret"), "text/plain");
    QVERIFY(service->ensecret(message, secret));

    /* Try to decrypt by hand with symmetricKey */
    auto cipher = QCA::Cipher("aes128", QCA::Cipher::CBC, QCA::Cipher::PKCS7, QCA::Decode, symmetricKey, secret.parameters);
    QCA::SecureArray result;
    result.append(cipher.update(QCA::MemoryRegion(secret.value.toByteArray())));
    result.append(cipher.final());

    QCOMPARE(QString::fromUtf8(result.toByteArray()), "It's a secret");

    /* Try to decrypt by session */
    QVERIFY(service->desecret(message, secret));
    QCOMPARE(secret.value.toByteArray(), QByteArray("It's a secret"));
}

void FdoSecretsTest::sessionWithShortDhSecret()
{
    std::unique_ptr<KSecretD> kwalletd{new KSecretD};
    std::unique_ptr<KWalletFreedesktopService> service{new KWalletFreedesktopService(kwalletd.get())};

    auto message = QDBusMessage::createSignal("dummy", "dummy", "dummy");
    auto [sessionPath, symmetricKey, commonSecretSize, errorStr] = setupSession(service.get(), true);
    QVERIFY2(errorStr.isEmpty(), errorStr.constData());
    QVERIFY(commonSecretSize < FDO_DH_PUBLIC_KEY_SIZE);

    auto secret = FreedesktopSecret(sessionPath, QByteArray("It's a secret"), "text/plain");
    QVERIFY(service->ensecret(message, secret));

    auto cipher = QCA::Cipher("aes128", QCA::Cipher::CBC, QCA::Cipher::PKCS7, QCA::Decode, symmetricKey, secret.parameters);
    QCA::SecureArray result;
    result.append(cipher.update(QCA::MemoryRegion(secret.value.toByteArray())));
    result.append(cipher.final());

    QVERIFY(cipher.ok());
    QCOMPARE(QString::fromUtf8(result.toByteArray()), "It's a secret");
}

void FdoSecretsTest::sessionWithShortClientPublicKey()
{
    std::unique_ptr<KSecretD> kwalletd{new KSecretD};
    std::unique_ptr<KWalletFreedesktopService> service{new KWalletFreedesktopService(kwalletd.get())};

    auto publicKeyBytes = shortDhPublicKey();

    QDBusObjectPath sessionPath;
    auto sessionPubKeyVariant = service->OpenSession("dh-ietf1024-sha256-aes128-cbc-pkcs7", QDBusVariant(publicKeyBytes), sessionPath);
    QVERIFY(sessionPath.path() != "/");
    QVERIFY(sessionPubKeyVariant.variant().canConvert<QByteArray>());
    QVERIFY(!sessionPubKeyVariant.variant().toByteArray().isEmpty());
}

void FdoSecretsTest::sessionWithMsbSetClientPublicKey()
{
    std::unique_ptr<KSecretD> kwalletd{new KSecretD};
    std::unique_ptr<KWalletFreedesktopService> service{new KWalletFreedesktopService(kwalletd.get())};

    auto message = QDBusMessage::createSignal("dummy", "dummy", "dummy");
    auto [sessionPath, symmetricKey, commonSecretSize, errorStr] = setupSession(service.get(), false, true);
    QVERIFY2(errorStr.isEmpty(), errorStr.constData());
    QVERIFY(commonSecretSize > 0);

    auto secret = FreedesktopSecret(sessionPath, QByteArray("It's a secret"), "text/plain");
    QVERIFY(service->ensecret(message, secret));

    auto cipher = QCA::Cipher("aes128", QCA::Cipher::CBC, QCA::Cipher::PKCS7, QCA::Decode, symmetricKey, secret.parameters);
    QCA::SecureArray result;
    result.append(cipher.update(QCA::MemoryRegion(secret.value.toByteArray())));
    result.append(cipher.final());

    QVERIFY(cipher.ok());
    QCOMPARE(QString::fromUtf8(result.toByteArray()), "It's a secret");
}

void FdoSecretsTest::attributes()
{
    KWalletFreedesktopAttributes attribs{"test"};

    attribs.newItem({"dir", "name"});

    attribs.setParam({"dir", "name"}, "param1", 0xff00ff00ff00ff00);
    attribs.setParam({"dir", "name"}, "param2", "string_param");

    QCOMPARE(attribs.getULongLongParam({"dir", "name"}, "param1", 0), 0xff00ff00ff00ff00);
    QCOMPARE(attribs.getStringParam({"dir", "name"}, "param2", ""), "string_param");

    attribs.renameLabel({"dir", "name"}, {"newdir", "newname"});

    QCOMPARE(attribs.getULongLongParam({"newdir", "newname"}, "param1", 0), 0xff00ff00ff00ff00);
    QCOMPARE(attribs.getStringParam({"newdir", "newname"}, "param2", ""), "string_param");
    QCOMPARE(attribs.getULongLongParam({"dir", "name"}, "param1", 0xdef017), 0xdef017);
    QCOMPARE(attribs.getStringParam({"dir", "name"}, "param2", "default"), "default");

    attribs.setParam({"newdir", "newname"}, "param1", 100200300ULL);
    attribs.setParam({"newdir", "newname"}, "param2", "another_string_param");

    QCOMPARE(attribs.getULongLongParam({"newdir", "newname"}, "param1", 0), 100200300ULL);
    QCOMPARE(attribs.getStringParam({"newdir", "newname"}, "param2", ""), "another_string_param");

    QVERIFY(attribs.getAttributes({"newdir", "newname"}).empty());

    StrStrMap attribMap;
    attribMap["key1"] = "value1";
    attribMap["key2"] = "value2";

    attribs.setAttributes({"newdir", "newname"}, attribMap);
    QCOMPARE(attribs.getAttributes({"newdir", "newname"}), attribMap);

    attribs.setAttributes({"dir", "name"}, attribMap);
    /* Item not exists - expects empty attributes map */
    QVERIFY(attribs.getAttributes({"dir", "name"}).empty());

    attribs.setParam({"dir1", "name1"}, "param1", "some_param");
    QCOMPARE(attribs.getStringParam({"dir1", "name1"}, "param1", "default"), "default");
}

void FdoSecretsTest::invalidUtf8Rejected()
{
    const QStringList wallets = {"wallet1"};
    const QStringList folders = {FDO_SECRETS_DEFAULT_DIR};
    const QStringList entries = {"item1"};
    SET_FUNCTION_RESULT(KSecretD::wallets, wallets);
    SET_FUNCTION_RESULT(KSecretD::folderList, folders);
    SET_FUNCTION_RESULT(KSecretD::entryList, entries);

    SET_FUNCTION_IMPL(KSecretD::entryType, [](int, const QString &, const QString &, const QString &) -> int {
        return KWallet::Wallet::Password;
    });

    SET_FUNCTION_IMPL(KSecretD::readPassword, [](int, const QString &, const QString &, const QString &) -> QString {
        return QStringLiteral("valid-password");
    });

    bool writePasswordCalled = false;
    SET_FUNCTION_IMPL(KSecretD::writePassword, [&](int, const QString &, const QString &, const QString &, const QString &) -> int {
        writePasswordCalled = true;
        return 0;
    });

    std::unique_ptr<KSecretD> kwalletd{new KSecretD};
    std::unique_ptr<KWalletFreedesktopService> service{new KWalletFreedesktopService(kwalletd.get())};

    auto collection = service->getCollectionByWalletName("wallet1");
    QVERIFY(collection);

    collection->itemAttributes().newItem({FDO_SECRETS_DEFAULT_DIR, "item1"});

    using OpenAsyncT = int (KSecretD::*)(const QString &, qlonglong, const QString &, bool, const QDBusConnection &, const QDBusMessage &);
    SET_FUNCTION_IMPL_OVERLOADED(KSecretD::openAsync,
                                 OpenAsyncT,
                                 [](const QString &, qlonglong, const QString &, bool, const QDBusConnection &, const QDBusMessage &) -> int {
                                     return 0;
                                 });

    QDBusObjectPath promptPath;
    service->Unlock({collection->fdoObjectPath()}, promptPath);
    auto prompt = service->getPromptByObjectPath(promptPath);
    QVERIFY(prompt);
    prompt->Prompt("wndid");
    Q_EMIT kwalletd->walletAsyncOpened(0, 0);
    SET_FUNCTION_RESULT_OVERLOADED(KSecretD::isOpen, true, bool(KSecretD::*)(int));
    QVERIFY(!collection->locked());

    auto item = collection->findItemByEntryLocation({FDO_SECRETS_DEFAULT_DIR, "item1"});
    QVERIFY(item);

    auto message = QDBusMessage::createSignal("dummy", "dummy", "dummy");
    auto [sessionPath, symmetricKey, commonSecretSize, errorStr] = setupSession(service.get());
    QVERIFY2(errorStr.isEmpty(), errorStr.constData());
    Q_UNUSED(commonSecretSize);
    Q_UNUSED(symmetricKey);

    /* Invalid UTF-8 bytes (gzip magic + random binary) with text/plain must be rejected */
    QByteArray invalidUtf8("\x1f\x8b\x08\x00\xff\xfe\xfd\xfc\xfb\xfa", 10);
    QVERIFY(!invalidUtf8.isValidUtf8());

    FreedesktopSecret invalidSecret(sessionPath, invalidUtf8, "text/plain; charset=utf8");
    service->ensecret(message, invalidSecret);

    writePasswordCalled = false;
    item->SetSecret(invalidSecret);
    QVERIFY2(!writePasswordCalled, "writePassword must not be called for invalid UTF-8 with text/ content_type");

    /* Valid UTF-8 with text/plain must still be accepted */
    FreedesktopSecret validSecret(sessionPath, QByteArray("Hello, valid UTF-8!"), "text/plain; charset=utf8");
    service->ensecret(message, validSecret);

    writePasswordCalled = false;
    item->SetSecret(validSecret);
    QVERIFY2(writePasswordCalled, "writePassword must be called for valid UTF-8 with text/ content_type");
}

void FdoSecretsTest::walletNameEncodeDecode()
{
#define ENCODE_DECODE_CHECK(DECODED, ENCODED)                                                                                                                  \
    do {                                                                                                                                                       \
        auto encodedResult = KWallet::Backend::encodeWalletName(DECODED);                                                                                      \
        auto decodedResult = KWallet::Backend::decodeWalletName(ENCODED);                                                                                      \
        QCOMPARE(encodedResult, ENCODED);                                                                                                                      \
        QCOMPARE(decodedResult, DECODED);                                                                                                                      \
    } while (false)

    ENCODE_DECODE_CHECK("/", ";2F");
    QString allowedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789^&'@{}[],$=!-#()%.+_\r\n\t\f\v ";
    ENCODE_DECODE_CHECK(allowedChars, allowedChars);
    ENCODE_DECODE_CHECK("a/b/c\\", "a;2Fb;2Fc;5C");
    ENCODE_DECODE_CHECK("/\\/", ";2F;5C;2F");
    ENCODE_DECODE_CHECK(";;;", ";3B;3B;3B");
    ENCODE_DECODE_CHECK(";3B", ";3B3B");

#undef ENCODE_DECODE_CHECK
}

QTEST_GUILESS_MAIN(FdoSecretsTest)

#include "moc_fdo_secrets_test.cpp"
