/*
    SPDX-FileCopyrightText: 2008 Kevin Ottens <ervin@kde.org>

    SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/

#include <QObject>
#include <QThread>
#include <QThreadPool>
#include <QtConcurrentRun>

#include <QTest>

#include <solid/device.h>
#include <solid/genericinterface.h>
#include <solid/predicate.h>
#include <solid/storagedrive.h>
#include <solid/storagevolume.h>

class SolidMtTest : public QObject
{
    Q_OBJECT
private Q_SLOTS:
    void testWorkerThread();
    void testThreadedPredicate();
    void testTextPredicates_data();
    void testTextPredicates();
    void testTextPredicatesExtended();
    void testDeviceMatching();
};

class WorkerThread : public QThread
{
    Q_OBJECT
protected:
    void run() override
    {
        Solid::Device dev(QStringLiteral("/org/freedesktop/Hal/devices/computer"));

        const QList<Solid::Device> driveList = Solid::Device::listFromType(Solid::DeviceInterface::StorageDrive);
        for (const Solid::Device &solidDevice : driveList) {
            const Solid::StorageDrive *solidDrive = solidDevice.as<Solid::StorageDrive>();
            Q_ASSERT(solidDrive);
            Q_UNUSED(solidDrive);
        }
    }
};

static void doPredicates()
{
    Solid::Predicate p5 = Solid::Predicate::fromString(
        QStringLiteral("[[Processor.maxSpeed == 3201 AND Processor.canChangeFrequency == false] OR StorageVolume.mountPoint == '/media/blup']"));

    Solid::Predicate p6 = Solid::Predicate::fromString(QStringLiteral("StorageVolume.usage == 'Other'"));
    Solid::Predicate p7 = Solid::Predicate::fromString(QStringLiteral("StorageVolume.usage == %1").arg((int)Solid::StorageVolume::Other));
    Solid::Predicate p8 = Solid::Predicate::fromString(QStringLiteral("StorageVolume.ignored == false"));
}

QTEST_MAIN(SolidMtTest)

void SolidMtTest::testWorkerThread()
{
    Solid::Device dev(QStringLiteral("/org/freedesktop/Hal/devices/acpi_ADP1"));

    WorkerThread *wt = new WorkerThread;
    wt->start();
    wt->wait();

    const QList<Solid::Device> driveList = Solid::Device::listFromType(Solid::DeviceInterface::StorageDrive);
    for (const Solid::Device &solidDevice : driveList) {
        const Solid::GenericInterface *solidDrive = solidDevice.as<Solid::GenericInterface>();
        Q_ASSERT(solidDrive);
        Q_UNUSED(solidDrive);
    }

    delete wt;
}

void SolidMtTest::testThreadedPredicate()
{
    QThreadPool::globalInstance()->setMaxThreadCount(10);
    const QList<QFuture<void>> futures = {
        QtConcurrent::run(&doPredicates),
        QtConcurrent::run(&doPredicates),
        QtConcurrent::run(&doPredicates),
        QtConcurrent::run(&doPredicates),
        QtConcurrent::run(&doPredicates),
        QtConcurrent::run(&doPredicates),
        QtConcurrent::run(&doPredicates),
        QtConcurrent::run(&doPredicates),
    };
    for (QFuture<void> f : futures) {
        f.waitForFinished();
    }
    QThreadPool::globalInstance()->setMaxThreadCount(1); // delete those threads
}


void SolidMtTest::testTextPredicates_data()
{
    QTest::addColumn<QString>("pstring");
    QTest::addColumn<bool>("isValidPredicate");

    QTest::newRow("empty") << QString() << false;
    QTest::newRow("simple") << QStringLiteral("@") << true;
    QTest::newRow("and") << QStringLiteral("@ AND @") << false;
    QTest::newRow("[and]") << QStringLiteral("[@ AND @]") << true;
    QTest::newRow("[3and]") << QStringLiteral("[@ AND @ AND @]") << false;
    QTest::newRow("[[and]and]") << QStringLiteral("[[@ AND @] AND @]") << true;
    QTest::newRow("[and[and]]") << QStringLiteral("[@ AND [@ AND @]]") << true;
    QTest::newRow("[[and][and] (unbalance)") << QStringLiteral("[ [@ AND @] AND [@ AND @]") << false;
    QTest::newRow("[[and][and]]") << QStringLiteral("[ [@ AND @] AND [@ AND @] ]") << true;
    QTest::newRow("[[[and]and]and]") << QStringLiteral("[ [ [ @ AND @] AND @ ] AND @]") << true;
}

/*! @brief Check validity (and hence, parser-code) for a bunch of text predicates
 *
 */
void SolidMtTest::testTextPredicates()
{
    QFETCH(QString, pstring);
    QFETCH(bool, isValidPredicate);

    // The expressions contain @ as a placeholder, so they are
    // short enough to easily see the structure of the expression
    // in _data(); replace all those @s with an actual atom.
    const QString atom(QStringLiteral("StorageVolume.ignored == false"));
    while (pstring.contains(QLatin1Char('@'))) {
        pstring = pstring.replace(QLatin1Char('@'), atom);
    }

    Solid::Predicate p = Solid::Predicate::fromString(pstring);
    QCOMPARE(p.isValid(), isValidPredicate);
}

void SolidMtTest::testTextPredicatesExtended()
{
    Solid::Predicate p = Solid::Predicate::fromString(QStringLiteral(
        "[[StorageVolume.ignored == false AND StorageVolume.usage == 'FileSystem'] AND [StorageVolume.ignored == false AND StorageDrive.removable == true]]"));
    QVERIFY(p.isValid());
    QCOMPARE(p.type(), Solid::Predicate::Conjunction);
    QCOMPARE(p.firstOperand().type(), Solid::Predicate::Conjunction);
    QCOMPARE(p.firstOperand().firstOperand().type(), Solid::Predicate::PropertyCheck);
    QCOMPARE(p.secondOperand().secondOperand().type(), Solid::Predicate::PropertyCheck);

    QCOMPARE(p.firstOperand().firstOperand().propertyName(), QStringLiteral("ignored"));
    QCOMPARE(p.firstOperand().firstOperand().interfaceType(), Solid::DeviceInterface::StorageVolume);
    // Let's not call firstOperand() quite so much, and copy into a local atom
    {
        auto atom = p.firstOperand().firstOperand();
        QVERIFY(atom.isValid());
        QCOMPARE(atom.propertyName(), QStringLiteral("ignored"));
        QCOMPARE(atom.matchingValue().userType(), QMetaType::Bool);
        QCOMPARE(atom.matchingValue().toBool(), false);
    }
    {
        auto atom = p.firstOperand().secondOperand();
        QVERIFY(atom.isValid());
        QCOMPARE(atom.interfaceType(), Solid::DeviceInterface::StorageVolume);
        QCOMPARE(atom.propertyName(), QStringLiteral("usage"));
        QCOMPARE(atom.matchingValue().userType(), QMetaType::QString);
        QCOMPARE(atom.matchingValue().toString(), QStringLiteral("FileSystem"));
    }
    {
        auto atom = p.secondOperand().firstOperand();
        QVERIFY(atom.isValid());
        QCOMPARE(atom.interfaceType(), Solid::DeviceInterface::StorageVolume);
        QCOMPARE(atom.propertyName(), QStringLiteral("ignored"));
        QCOMPARE(atom.matchingValue().userType(), QMetaType::Bool);
        QCOMPARE(atom.matchingValue().toBool(), false);
    }
    {
        auto atom = p.secondOperand().secondOperand();
        QVERIFY(atom.isValid());
        QCOMPARE(atom.interfaceType(), Solid::DeviceInterface::StorageDrive);
        QCOMPARE(atom.propertyName(), QStringLiteral("removable"));
        QCOMPARE(atom.matchingValue().userType(), QMetaType::Bool);
        QCOMPARE(atom.matchingValue().toBool(), true);
    }
}

void SolidMtTest::testDeviceMatching()
{
    auto deviceList = Solid::Device::allDevices();
    QVERIFY(!deviceList.isEmpty());

    Solid::Predicate volumesPredicate = Solid::Predicate::fromString(QStringLiteral("StorageVolume.ignored == false"));
    QVERIFY(volumesPredicate.isValid());

    int volumes = 0;
    for (const auto &device : std::as_const(deviceList)) {
        if(volumesPredicate.matches(device))
        {
            volumes++;
        }
    }
    // If there are no removable volumes, that's fine; below we will be matching empty lists.
    // QVERIFY(volumes > 0);

    // From the pre-parsed query
    {
        auto matchedDeviceList = Solid::Device::listFromQuery(volumesPredicate);
        QCOMPARE(matchedDeviceList.size(), volumes);
    }
    // Same from text again
    {
        auto matchedDeviceList = Solid::Device::listFromQuery(QStringLiteral("StorageVolume.ignored == false"));
        QCOMPARE(matchedDeviceList.size(), volumes);
    }

    // Are there removables? Hope so.
    //
    // The removableFSVolumesPredicate is checking two incompatible things:
    // a given Solid::Device isn't a Volume and a Drive at the same time,
    // so it cannot match.
    {
        Solid::Predicate fsVolumesPredicate =
            Solid::Predicate::fromString(QStringLiteral("[StorageVolume.ignored == false AND StorageVolume.usage == 'FileSystem']"));
        Solid::Predicate removablePredicate = Solid::Predicate::fromString(QStringLiteral("StorageDrive.removable == true"));
        Solid::Predicate removableFSVolumesPredicate = Solid::Predicate::fromString(
            QStringLiteral("[[StorageVolume.ignored == false AND StorageVolume.usage == 'FileSystem'] AND StorageDrive.removable == true]"));
        volumes = 0;
        for (const auto &device : std::as_const(deviceList)) {
            qDebug() << device.displayName() << "fs?" << fsVolumesPredicate.matches(device) << "removable?" << removablePredicate.matches(device) << "removableFS?" << removableFSVolumesPredicate.matches(device);
            if (removableFSVolumesPredicate.matches(device))
            {
                volumes++;
            }
        }
        QCOMPARE(volumes, 0);

        auto matchedDeviceList = Solid::Device::listFromQuery(removableFSVolumesPredicate);
        QCOMPARE(matchedDeviceList.size(), volumes);
    }

    // Internally, the predicate is going to check interfaces,
    // so let's do that here as well. This will show no device is
    // both a Volume and a Drive.
    {
        const Solid::DeviceInterface::Type storageVolume = Solid::DeviceInterface::Type::StorageVolume;
        const Solid::DeviceInterface::Type storageDrive = Solid::DeviceInterface::Type::StorageDrive;

        int volumeCount = 0;
        int driveCount = 0;
        int bothCount = 0;

        for (const auto &device : std::as_const(deviceList)) {
            // Copied from internals in predicate.cpp:
            //        const DeviceInterface *iface = device.asDeviceInterface(d->ifaceType);
            const Solid::DeviceInterface *volumeIface = device.asDeviceInterface(storageVolume);
            const Solid::DeviceInterface *driveIface = device.asDeviceInterface(storageDrive);
            volumeCount += volumeIface ? 1 : 0;
            driveCount += driveIface ? 1 : 0;
            bothCount += (volumeIface && driveIface) ? 1 : 0;
        }
        QCOMPARE(bothCount, 0);
        if (volumeCount + driveCount > 0)
        {
            // On Linux CI, there is no DBus connection, so no drives or volumes at all.
            // On a local machine, or on FreeBSD CI, there are volumes and/or drives,
            // so verify that there's at least one of each.
            QVERIFY(volumeCount > 0);
            QVERIFY(driveCount > 0);
        }
    }
}

#include "solidmttest.moc"
