/* This file is part of the KDE project

   Copyright 2009 David Faure <faure@kde.org>

   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 ) version 3 or, at the discretion of KDE e.V.
   ( which shall act as a proxy as in section 14 of the GPLv3 ), 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 <kservicegroup.h>
#include <qmimedatabase.h>
#include <QtTest>

#include <kconfig.h>
#include <kconfiggroup.h>
#include <kdesktopfile.h>
#include <ksycoca.h>

#include <kservicetype.h>
#include <kservicetypeprofile.h>

#include <qstandardpaths.h>
#include "kwaitforsignal.h" // for kWaitForSignal
#include <QDebug>

// Helper method for all the trader tests
static bool offerListHasService(const KService::List &offers,
                                const QString &entryPath)
{
    bool found = false;
    KService::List::const_iterator it = offers.begin();
    for (; it != offers.end(); ++it) {
        if ((*it)->entryPath() == entryPath) {
            if (found) {  // should be there only once
                qWarning("ERROR: %s was found twice in the list", qPrintable(entryPath));
                return false; // make test fail
            }
            found = true;
        }
    }
    return found;
}

class WorkerObject : public QObject
{
    Q_OBJECT
public:
    WorkerObject() : QObject() {}

public Q_SLOTS:
    void work()
    {
        qDebug() << QThread::currentThread() << "working...";

        const KServiceType::List allServiceTypes = KServiceType::allServiceTypes();
        Q_ASSERT(!allServiceTypes.isEmpty());

        QMimeDatabase db;
        const QList<QMimeType> allMimeTypes = db.allMimeTypes();
        Q_ASSERT(!allMimeTypes.isEmpty());

        const KService::List lst = KService::allServices();
        Q_ASSERT(!lst.isEmpty());

        for (KService::List::ConstIterator it = lst.begin();
                it != lst.end(); ++it) {
            const KService::Ptr service = (*it);
            Q_ASSERT(service->isType(KST_KService));
            const QString name = service->name();
            const QString entryPath = service->entryPath();
            //qDebug() << name << "entryPath=" << entryPath << "menuId=" << service->menuId();
            Q_ASSERT(!name.isEmpty());
            Q_ASSERT(!entryPath.isEmpty());

            KService::Ptr lookedupService = KService::serviceByDesktopPath(entryPath);
            Q_ASSERT(lookedupService);   // not null
            QCOMPARE(lookedupService->entryPath(), entryPath);

            if (service->isApplication()) {
                const QString menuId = service->menuId();
                if (menuId.isEmpty()) {
                    qWarning("%s has an empty menuId!", qPrintable(entryPath));
                }
                Q_ASSERT(!menuId.isEmpty());
                lookedupService = KService::serviceByMenuId(menuId);
                Q_ASSERT(lookedupService);   // not null
                QCOMPARE(lookedupService->menuId(), menuId);
            }
        }

        KService::List offers = KServiceTypeTrader::self()->query("KPluginInfo");
        Q_ASSERT(offerListHasService(offers, "faketextplugin.desktop"));

        offers = KServiceTypeTrader::self()->query("KPluginInfo", "Library == 'faketextplugin'");
        Q_ASSERT(offers.count() == 1);
        QVERIFY(offerListHasService(offers, "faketextplugin.desktop"));

        KServiceGroup::Ptr root = KServiceGroup::root();
        Q_ASSERT(root);
    }
};

class WorkerThread : public QThread
{
    Q_OBJECT
public:
    WorkerThread() : QThread()
    {
        m_stop = false;
    }
    virtual void run()
    {
        WorkerObject wo;
        while (!m_stop) {
            wo.work();
        }
    }
    virtual void stop()
    {
        m_stop = true;
    }
private:
    bool m_stop;
};

/**
 * Threads with an event loop will be able to process "database changed" signals.
 * Threads without an event loop (like WorkerThread) cannot, so they will keep using
 * the old data.
 */
class EventLoopThread : public WorkerThread
{
    Q_OBJECT
public:
    virtual void run()
    {
        // WorkerObject must belong to this thread, this is why we don't
        // have the slot work() in WorkerThread itself. Typical QThread trap!
        WorkerObject wo;
        QTimer timer;
        connect(&timer, SIGNAL(timeout()), &wo, SLOT(work()));
        timer.start(100);
        exec();
    }
    virtual void stop()
    {
        quit();
    }
};

extern KSERVICE_EXPORT bool kservice_require_kded;

class KSycocaThreadTest : public QObject
{
    Q_OBJECT
public:
    KSycocaThreadTest()
    {
        kservice_require_kded = false;
    }
    void launch();

private Q_SLOTS:
    // Note that this isn't a QTest based test.
    // All these methods are called manually.
    void cleanupTestCase();
    void testCreateService();
    void testDeleteService()
    {
        deleteFakeService();
        QTimer::singleShot(1000, this, SLOT(slotFinish()));
    }
    void slotFinish()
    {
        qDebug() << "Terminating";
        for (int i = 0; i < threads.size(); i++) {
            threads[i]->stop();
        }
        for (int i = 0; i < threads.size(); i++) {
            threads[i]->wait();
        }
        cleanupTestCase();
        QCoreApplication::instance()->quit();
    }

private:
    void createFakeService();
    void deleteFakeService();
    QVector<WorkerThread *> threads;
};

static void runKBuildSycoca()
{
    QProcess proc;
    const QString kbuildsycoca = QStandardPaths::findExecutable(KBUILDSYCOCA_EXENAME);
    QVERIFY(!kbuildsycoca.isEmpty());
    QStringList args;
    args << "--testmode";
    proc.setProcessChannelMode(QProcess::MergedChannels); // silence kbuildsycoca output
    proc.start(kbuildsycoca, args);
    qDebug() << "waiting for signal";
    QSignalSpy spy(KSycoca::self(), SIGNAL(databaseChanged(QStringList)));
    QVERIFY(spy.wait(10000));

    qDebug() << "got signal";
    proc.waitForFinished();
    QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
}

void KSycocaThreadTest::launch()
{
    // This service is always there. Used in the trader queries from the thread.
    const QString fakeTextPlugin = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kde5/services/") + "faketextplugin.desktop";
    if (!QFile::exists(fakeTextPlugin)) {
        KDesktopFile file(fakeTextPlugin);
        KConfigGroup group = file.desktopGroup();
        group.writeEntry("Name", "FakeTextPlugin");
        group.writeEntry("Type", "Service");
        group.writeEntry("X-KDE-Library", "faketextplugin");
        group.writeEntry("X-KDE-Protocols", "http,ftp");
        group.writeEntry("ServiceTypes", "KPluginInfo");
        group.writeEntry("MimeType", "text/plain;");
        file.sync();
        qDebug() << "Created" << fakeTextPlugin << ", running kbuilsycoca";
        runKBuildSycoca();
        // Process the event
        int count = 0;
        while (!KService::serviceByDesktopPath("faketextplugin.desktop")) {
            qApp->processEvents();
            if (++count == 20) {
                qFatal("sycoca doesn't have faketextplugin.desktop");
            }
        }
    }

    // Start clean
    const QString servPath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kde5/services/") + "fakeservice.desktop";
    if (QFile::exists(servPath)) {
        QFile::remove(servPath);
    }
    if (KService::serviceByDesktopPath("fakeservice.desktop")) {
        deleteFakeService();
    }
    threads.resize(5);
    for (int i = 0; i < threads.size(); i++) {
        threads[i] = i < 3 ? new WorkerThread : new EventLoopThread;
        threads[i]->start();
    }
    testCreateService();
}

void KSycocaThreadTest::cleanupTestCase()
{
    const QString fakeTextPlugin = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kde5/services/") + "faketextplugin.desktop";
    QFile::remove(fakeTextPlugin);
}

void KSycocaThreadTest::testCreateService()
{
    createFakeService();
    const QString servPath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kde5/services/") + "fakeservice.desktop";

    Q_ASSERT(QFile::exists(servPath));
    qDebug() << "executing kbuildsycoca (1)";
    runKBuildSycoca();

    QTimer::singleShot(1000, this, SLOT(testDeleteService()));
}

void KSycocaThreadTest::deleteFakeService()
{
    KService::Ptr fakeService = KService::serviceByDesktopPath("fakeservice.desktop");
    Q_ASSERT(fakeService);
    const QString servPath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kde5/services/") + "fakeservice.desktop";
    QFile::remove(servPath);

    QSignalSpy spy(KSycoca::self(), SIGNAL(databaseChanged(QStringList)));
    Q_ASSERT(spy.isValid());

    qDebug() << "executing kbuildsycoca (2)";
    runKBuildSycoca();
    Q_ASSERT(spy[0][0].toStringList().contains("services"));

    Q_ASSERT(fakeService); // the whole point of refcounting is that this KService instance is still valid.
    Q_ASSERT(!QFile::exists(servPath));
}

void KSycocaThreadTest::createFakeService()
{
    const QString fakeService = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kde5/services/") + "fakeservice.desktop";
    KDesktopFile file(fakeService);
    KConfigGroup group = file.desktopGroup();
    group.writeEntry("Name", "FakeService");
    group.writeEntry("Type", "Service");
    group.writeEntry("X-KDE-Library", "fakeservice");
    group.writeEntry("X-KDE-Protocols", "http,ftp");
    group.writeEntry("ServiceTypes", "KPluginInfo");
    group.writeEntry("MimeType", "text/plain;");
}

int main(int argc, char **argv)
{
    QStandardPaths::enableTestMode(true);
    QCoreApplication app(argc, argv);
    KSycocaThreadTest mainObj;
    mainObj.launch();
    return app.exec();
}

#include "ksycocathreadtest.moc"
