// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

#include <utils/algorithm.h>
#include <utils/async.h>

#include <QtTest>

using namespace Utils;

using namespace std::chrono_literals;

class tst_Async : public QObject
{
    Q_OBJECT

private slots:
    void runAsync();
    void crefFunction();
    void onResultReady();
    void futureSynchonizer();
    void taskTree();
    void mapReduce_data();
    void mapReduce();
private:
    QThreadPool m_threadPool;
};

void report3(QPromise<int> &promise)
{
    promise.addResult(0);
    promise.addResult(2);
    promise.addResult(1);
}

static void staticReport3(QPromise<int> &promise)
{
    promise.addResult(0);
    promise.addResult(2);
    promise.addResult(1);
}

void reportN(QPromise<double> &promise, int n)
{
    for (int i = 0; i < n; ++i)
        promise.addResult(0);
}

static void staticReportN(QPromise<double> &promise, int n)
{
    for (int i = 0; i < n; ++i)
        promise.addResult(0);
}

void reportString1(QPromise<QString> &promise, const QString &s)
{
    promise.addResult(s);
}

void reportString2(QPromise<QString> &promise, QString s)
{
    promise.addResult(s);
}

class Functor {
public:
    void operator()(QPromise<double> &promise, int n) const
    {
        for (int i = 0; i < n; ++i)
            promise.addResult(0);
    }
};

class MyObject {
public:
    static void staticMember0(QPromise<double> &promise)
    {
        promise.addResult(0);
        promise.addResult(2);
        promise.addResult(1);
    }

    static void staticMember1(QPromise<double> &promise, int n)
    {
        for (int i = 0; i < n; ++i)
            promise.addResult(0);
    }

    void member0(QPromise<double> &promise) const
    {
        promise.addResult(0);
        promise.addResult(2);
        promise.addResult(1);
    }

    void member1(QPromise<double> &promise, int n) const
    {
        for (int i = 0; i < n; ++i)
            promise.addResult(0);
    }

    void memberString1(QPromise<QString> &promise, const QString &s) const
    {
        promise.addResult(s);
    }

    void memberString2(QPromise<QString> &promise, QString s) const
    {
        promise.addResult(s);
    }

    void nonConstMember(QPromise<double> &promise)
    {
        promise.addResult(0);
        promise.addResult(2);
        promise.addResult(1);
    }
};

template <typename...>
struct FutureArgType;

template <typename Arg>
struct FutureArgType<QFuture<Arg>>
{
    using Type = Arg;
};

template <typename...>
struct ConcurrentResultType;

template<typename Function, typename ...Args>
struct ConcurrentResultType<Function, Args...>
{
    using Type = typename FutureArgType<decltype(QtConcurrent::run(
        std::declval<Function>(), std::declval<Args>()...))>::Type;
};

template <typename Function, typename ...Args,
          typename ResultType = typename ConcurrentResultType<Function, Args...>::Type>
std::shared_ptr<Async<ResultType>> createAsyncTask(Function &&function, Args &&...args)
{
    auto asyncTask = std::make_shared<Async<ResultType>>();
    asyncTask->setConcurrentCallData(std::forward<Function>(function), std::forward<Args>(args)...);
    asyncTask->start();
    return asyncTask;
}

void tst_Async::runAsync()
{
    // tesing QtConcurrent::run()
    QCOMPARE(QtConcurrent::run(&report3).results(),
             QList<int>({0, 2, 1}));
    QCOMPARE(QtConcurrent::run(report3).results(),
             QList<int>({0, 2, 1}));
    QCOMPARE(QtConcurrent::run(&staticReport3).results(),
             QList<int>({0, 2, 1}));
    QCOMPARE(QtConcurrent::run(staticReport3).results(),
             QList<int>({0, 2, 1}));
    QCOMPARE(QtConcurrent::run(&reportN, 2).results(),
             QList<double>({0, 0}));
    QCOMPARE(QtConcurrent::run(reportN, 2).results(),
             QList<double>({0, 0}));
    QCOMPARE(QtConcurrent::run(&staticReportN, 2).results(),
             QList<double>({0, 0}));
    QCOMPARE(QtConcurrent::run(staticReportN, 2).results(),
             QList<double>({0, 0}));

    // free function pointer
    QCOMPARE(createAsyncTask(&report3)->results(),
             QList<int>({0, 2, 1}));
    QCOMPARE(Utils::asyncRun(&report3).results(),
             QList<int>({0, 2, 1}));
    QCOMPARE(createAsyncTask(report3)->results(),
             QList<int>({0, 2, 1}));
    QCOMPARE(Utils::asyncRun(report3).results(),
             QList<int>({0, 2, 1}));
    QCOMPARE(createAsyncTask(&staticReport3)->results(),
             QList<int>({0, 2, 1}));
    QCOMPARE(Utils::asyncRun(&staticReport3).results(),
             QList<int>({0, 2, 1}));
    QCOMPARE(createAsyncTask(staticReport3)->results(),
             QList<int>({0, 2, 1}));
    QCOMPARE(Utils::asyncRun(staticReport3).results(),
             QList<int>({0, 2, 1}));

    QCOMPARE(createAsyncTask(&reportN, 2)->results(),
             QList<double>({0, 0}));
    QCOMPARE(Utils::asyncRun(&reportN, 2).results(),
             QList<double>({0, 0}));
    QCOMPARE(createAsyncTask(reportN, 2)->results(),
             QList<double>({0, 0}));
    QCOMPARE(Utils::asyncRun(reportN, 2).results(),
             QList<double>({0, 0}));
    QCOMPARE(createAsyncTask(&staticReportN, 2)->results(),
             QList<double>({0, 0}));
    QCOMPARE(Utils::asyncRun(&staticReportN, 2).results(),
             QList<double>({0, 0}));
    QCOMPARE(createAsyncTask(staticReportN, 2)->results(),
             QList<double>({0, 0}));
    QCOMPARE(Utils::asyncRun(staticReportN, 2).results(),
             QList<double>({0, 0}));

    QString s = QLatin1String("string");
    const QString &crs = QLatin1String("cr string");
    const QString cs = QLatin1String("c string");

    QCOMPARE(createAsyncTask(reportString1, s)->results(),
             QList<QString>({s}));
    QCOMPARE(Utils::asyncRun(reportString1, s).results(),
             QList<QString>({s}));
    QCOMPARE(createAsyncTask(reportString1, crs)->results(),
             QList<QString>({crs}));
    QCOMPARE(Utils::asyncRun(reportString1, crs).results(),
             QList<QString>({crs}));
    QCOMPARE(createAsyncTask(reportString1, cs)->results(),
             QList<QString>({cs}));
    QCOMPARE(Utils::asyncRun(reportString1, cs).results(),
             QList<QString>({cs}));
    QCOMPARE(createAsyncTask(reportString1, QString(QLatin1String("rvalue")))->results(),
             QList<QString>({QString(QLatin1String("rvalue"))}));
    QCOMPARE(Utils::asyncRun(reportString1, QString(QLatin1String("rvalue"))).results(),
             QList<QString>({QString(QLatin1String("rvalue"))}));

    QCOMPARE(createAsyncTask(reportString2, s)->results(),
             QList<QString>({s}));
    QCOMPARE(Utils::asyncRun(reportString2, s).results(),
             QList<QString>({s}));
    QCOMPARE(createAsyncTask(reportString2, crs)->results(),
             QList<QString>({crs}));
    QCOMPARE(Utils::asyncRun(reportString2, crs).results(),
             QList<QString>({crs}));
    QCOMPARE(createAsyncTask(reportString2, cs)->results(),
             QList<QString>({cs}));
    QCOMPARE(Utils::asyncRun(reportString2, cs).results(),
             QList<QString>({cs}));
    QCOMPARE(createAsyncTask(reportString2, QString(QLatin1String("rvalue")))->results(),
             QList<QString>({QString(QLatin1String("rvalue"))}));
    QCOMPARE(Utils::asyncRun(reportString2, QString(QLatin1String("rvalue"))).results(),
             QList<QString>({QString(QLatin1String("rvalue"))}));

    // lambda
    QCOMPARE(createAsyncTask([](QPromise<double> &promise, int n) {
                 for (int i = 0; i < n; ++i)
                     promise.addResult(0);
             }, 3)->results(),
             QList<double>({0, 0, 0}));
    QCOMPARE(Utils::asyncRun([](QPromise<double> &promise, int n) {
                 for (int i = 0; i < n; ++i)
                     promise.addResult(0);
             }, 3).results(),
             QList<double>({0, 0, 0}));

    // std::function
    const std::function<void(QPromise<double>&,int)> fun = [](QPromise<double> &promise, int n) {
        for (int i = 0; i < n; ++i)
            promise.addResult(0);
    };
    QCOMPARE(createAsyncTask(fun, 2)->results(),
             QList<double>({0, 0}));
    QCOMPARE(Utils::asyncRun(fun, 2).results(),
             QList<double>({0, 0}));

    // operator()
    QCOMPARE(createAsyncTask(Functor(), 3)->results(),
             QList<double>({0, 0, 0}));
    QCOMPARE(Utils::asyncRun(Functor(), 3).results(),
             QList<double>({0, 0, 0}));
    const Functor c{};
    QCOMPARE(createAsyncTask(c, 2)->results(),
             QList<double>({0, 0}));
    QCOMPARE(Utils::asyncRun(c, 2).results(),
             QList<double>({0, 0}));

    // static member functions
    QCOMPARE(createAsyncTask(&MyObject::staticMember0)->results(),
             QList<double>({0, 2, 1}));
    QCOMPARE(Utils::asyncRun(&MyObject::staticMember0).results(),
             QList<double>({0, 2, 1}));
    QCOMPARE(createAsyncTask(&MyObject::staticMember1, 2)->results(),
             QList<double>({0, 0}));
    QCOMPARE(Utils::asyncRun(&MyObject::staticMember1, 2).results(),
             QList<double>({0, 0}));

    // member functions
    const MyObject obj{};
    QCOMPARE(createAsyncTask(&MyObject::member0, &obj)->results(),
             QList<double>({0, 2, 1}));
    QCOMPARE(Utils::asyncRun(&MyObject::member0, &obj).results(),
             QList<double>({0, 2, 1}));
    QCOMPARE(createAsyncTask(&MyObject::member1, &obj, 4)->results(),
             QList<double>({0, 0, 0, 0}));
    QCOMPARE(Utils::asyncRun(&MyObject::member1, &obj, 4).results(),
             QList<double>({0, 0, 0, 0}));
    QCOMPARE(createAsyncTask(&MyObject::memberString1, &obj, s)->results(),
             QList<QString>({s}));
    QCOMPARE(Utils::asyncRun(&MyObject::memberString1, &obj, s).results(),
             QList<QString>({s}));
    QCOMPARE(createAsyncTask(&MyObject::memberString1, &obj, crs)->results(),
             QList<QString>({crs}));
    QCOMPARE(Utils::asyncRun(&MyObject::memberString1, &obj, crs).results(),
             QList<QString>({crs}));
    QCOMPARE(createAsyncTask(&MyObject::memberString1, &obj, cs)->results(),
             QList<QString>({cs}));
    QCOMPARE(Utils::asyncRun(&MyObject::memberString1, &obj, cs).results(),
             QList<QString>({cs}));
    QCOMPARE(createAsyncTask(&MyObject::memberString1, &obj, QString(QLatin1String("rvalue")))->results(),
             QList<QString>({QString(QLatin1String("rvalue"))}));
    QCOMPARE(Utils::asyncRun(&MyObject::memberString1, &obj, QString(QLatin1String("rvalue"))).results(),
             QList<QString>({QString(QLatin1String("rvalue"))}));
    QCOMPARE(createAsyncTask(&MyObject::memberString2, &obj, s)->results(),
             QList<QString>({s}));
    QCOMPARE(Utils::asyncRun(&MyObject::memberString2, &obj, s).results(),
             QList<QString>({s}));
    QCOMPARE(createAsyncTask(&MyObject::memberString2, &obj, crs)->results(),
             QList<QString>({crs}));
    QCOMPARE(Utils::asyncRun(&MyObject::memberString2, &obj, crs).results(),
             QList<QString>({crs}));
    QCOMPARE(createAsyncTask(&MyObject::memberString2, &obj, cs)->results(),
             QList<QString>({cs}));
    QCOMPARE(Utils::asyncRun(&MyObject::memberString2, &obj, cs).results(),
             QList<QString>({cs}));
    QCOMPARE(createAsyncTask(&MyObject::memberString2, &obj, QString(QLatin1String("rvalue")))->results(),
             QList<QString>({QString(QLatin1String("rvalue"))}));
    QCOMPARE(Utils::asyncRun(&MyObject::memberString2, &obj, QString(QLatin1String("rvalue"))).results(),
             QList<QString>({QString(QLatin1String("rvalue"))}));
    MyObject nonConstObj{};
    QCOMPARE(createAsyncTask(&MyObject::nonConstMember, &nonConstObj)->results(),
             QList<double>({0, 2, 1}));
    QCOMPARE(Utils::asyncRun(&MyObject::nonConstMember, &nonConstObj).results(),
             QList<double>({0, 2, 1}));
}

void tst_Async::crefFunction()
{
    // free function pointer with promise
    auto fun = &report3;
    QCOMPARE(createAsyncTask(std::cref(fun))->results(),
             QList<int>({0, 2, 1}));
    QCOMPARE(Utils::asyncRun(std::cref(fun)).results(),
             QList<int>({0, 2, 1}));

    // lambda with promise
    auto lambda = [](QPromise<double> &promise, int n) {
        for (int i = 0; i < n; ++i)
            promise.addResult(0);
    };
    QCOMPARE(createAsyncTask(std::cref(lambda), 3)->results(),
             QList<double>({0, 0, 0}));
    QCOMPARE(Utils::asyncRun(std::cref(lambda), 3).results(),
             QList<double>({0, 0, 0}));

    // std::function with promise
    const std::function<void(QPromise<double>&,int)> funObj = [](QPromise<double> &promise, int n) {
        for (int i = 0; i < n; ++i)
            promise.addResult(0);
    };
    QCOMPARE(createAsyncTask(std::cref(funObj), 2)->results(),
             QList<double>({0, 0}));
    QCOMPARE(Utils::asyncRun(std::cref(funObj), 2).results(),
             QList<double>({0, 0}));

    // callable with promise
    const Functor c{};
    QCOMPARE(createAsyncTask(std::cref(c), 2)->results(),
             QList<double>({0, 0}));
    QCOMPARE(Utils::asyncRun(std::cref(c), 2).results(),
             QList<double>({0, 0}));

    // member functions with promise
    auto member = &MyObject::member0;
    const MyObject obj{};
    QCOMPARE(createAsyncTask(std::cref(member), &obj)->results(),
             QList<double>({0, 2, 1}));
    QCOMPARE(Utils::asyncRun(std::cref(member), &obj).results(),
             QList<double>({0, 2, 1}));
}

class ObjWithProperty : public QObject
{
    Q_OBJECT

public slots:
    void setValue(const QString &s)
    {
        value = s;
    }

public:
    QString value;
};

void tst_Async::onResultReady()
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
    { // lambda
        QObject context;
        QFuture<QString> f = Utils::asyncRun([](QPromise<QString> &fi) {
            fi.addResult("Hi");
            fi.addResult("there");
        });
        int count = 0;
        QString res;
        Utils::onResultReady(f, &context, [&count, &res](const QString &s) {
            ++count;
            res = s;
        });
        f.waitForFinished();
        QCoreApplication::processEvents();
        QCOMPARE(count, 2);
        QCOMPARE(res, QString("there"));
    }
    { // lambda with guard
        QFuture<QString> f = Utils::asyncRun([](QPromise<QString> &fi) {
            fi.addResult("Hi");
            fi.addResult("there");
        });
        int count = 0;
        ObjWithProperty obj;
        Utils::onResultReady(f, &obj, [&count, &obj](const QString &s) {
            ++count;
            obj.setValue(s);
        });
        f.waitForFinished();
        QCoreApplication::processEvents();
        QCOMPARE(count, 2);
        QCOMPARE(obj.value, QString("there"));
    }
#endif
    { // member
        QFuture<QString> f = Utils::asyncRun([] { return QString("Hi"); });
        ObjWithProperty obj;
        Utils::onResultReady(f, &obj, &ObjWithProperty::setValue);
        f.waitForFinished();
        QCoreApplication::processEvents();
        QCOMPARE(obj.value, QString("Hi"));
    }
}

void tst_Async::futureSynchonizer()
{
    auto lambda = [](QPromise<int> &promise) {
        while (true) {
            if (promise.isCanceled()) {
                promise.future().cancel();
                promise.finish();
                return;
            }
            QThread::msleep(100);
        }
    };

    FutureSynchronizer synchronizer;
    synchronizer.setCancelOnWait(false);
    {
        Async<int> task;
        task.setConcurrentCallData(lambda);
        task.setFutureSynchronizer(&synchronizer);
        task.start();
        QThread::msleep(10);
        // We assume here that worker thread will still work for about 90 ms.
        QVERIFY(!task.isCanceled());
        QVERIFY(!task.isDone());
    }
    synchronizer.flushFinishedFutures();
    QVERIFY(!synchronizer.isEmpty());
    // The destructor of synchronizer should wait for about 90 ms for worker thread to be canceled
}

void multiplyBy2(QPromise<int> &promise, int input) { promise.addResult(input * 2); }

void tst_Async::taskTree()
{
    using namespace Tasking;

    int value = 1;

    const auto setupIntAsync = [&](Async<int> &task) {
        task.setConcurrentCallData(multiplyBy2, value);
    };
    const auto handleIntAsync = [&](const Async<int> &task) {
        value = task.result();
    };

    const Group recipe {
        AsyncTask<int>(setupIntAsync, handleIntAsync, CallDoneIf::Success),
        AsyncTask<int>(setupIntAsync, handleIntAsync, CallDoneIf::Success),
        AsyncTask<int>(setupIntAsync, handleIntAsync, CallDoneIf::Success),
        AsyncTask<int>(setupIntAsync, handleIntAsync, CallDoneIf::Success),
    };

    QCOMPARE(TaskTree::runBlocking(recipe.withTimeout(1000ms)), DoneWith::Success);
    QCOMPARE(value, 16);
}

static int returnxx(int x)
{
    return x * x;
}

static void returnxxWithPromise(QPromise<int> &promise, int x)
{
    promise.addResult(x * x);
}

static double s_sum = 0;
static QList<double> s_results;

void tst_Async::mapReduce_data()
{
    using namespace Tasking;

    QTest::addColumn<Group>("recipe");
    QTest::addColumn<double>("sum");
    QTest::addColumn<QList<double>>("results");

    const auto initTree = [] {
        s_sum = 0;
        s_results.append(s_sum);
    };
    const auto setupAsync = [](Async<int> &task, int input) {
        task.setConcurrentCallData(returnxx, input);
    };
    const auto setupAsyncWithFI = [](Async<int> &task, int input) {
        task.setConcurrentCallData(returnxxWithPromise, input);
    };
    const auto setupAsyncWithTP = [this](Async<int> &task, int input) {
        task.setConcurrentCallData(returnxx, input);
        task.setThreadPool(&m_threadPool);
    };
    const auto handleAsync = [](const Async<int> &task) {
        s_sum += task.result();
        s_results.append(task.result());
    };
    const auto handleTreeParallel = [] {
        s_sum /= 2;
        s_results.append(s_sum);
        Utils::sort(s_results); // mapping order is undefined
    };
    const auto handleTreeSequential = [] {
        s_sum /= 2;
        s_results.append(s_sum);
    };

    using namespace Tasking;
    using namespace std::placeholders;

    using SetupHandler = std::function<void(Async<int> &task, int input)>;
    using DoneHandler = std::function<void()>;

    const auto createTask = [=](const GroupItem &executeMode,
                                const SetupHandler &setupHandler,
                                const DoneHandler &doneHandler) {
        return Group {
            executeMode,
            onGroupSetup(initTree),
            AsyncTask<int>(std::bind(setupHandler, _1, 1), handleAsync, CallDoneIf::Success),
            AsyncTask<int>(std::bind(setupHandler, _1, 2), handleAsync, CallDoneIf::Success),
            AsyncTask<int>(std::bind(setupHandler, _1, 3), handleAsync, CallDoneIf::Success),
            AsyncTask<int>(std::bind(setupHandler, _1, 4), handleAsync, CallDoneIf::Success),
            AsyncTask<int>(std::bind(setupHandler, _1, 5), handleAsync, CallDoneIf::Success),
            onGroupDone(doneHandler, CallDoneIf::Success)
        };
    };

    const Group parallelRoot = createTask(parallel, setupAsync, handleTreeParallel);
    const Group parallelRootWithFI = createTask(parallel, setupAsyncWithFI, handleTreeParallel);
    const Group parallelRootWithTP = createTask(parallel, setupAsyncWithTP, handleTreeParallel);
    const Group sequentialRoot = createTask(sequential, setupAsync, handleTreeSequential);
    const Group sequentialRootWithFI = createTask(sequential, setupAsyncWithFI, handleTreeSequential);
    const Group sequentialRootWithTP = createTask(sequential, setupAsyncWithTP, handleTreeSequential);

    const double defaultSum = 27.5;
    const QList<double> defaultResult{0., 1., 4., 9., 16., 25., 27.5};

    QTest::newRow("Parallel") << parallelRoot << defaultSum << defaultResult;
    QTest::newRow("ParallelWithFutureInterface") << parallelRootWithFI << defaultSum << defaultResult;
    QTest::newRow("ParallelWithThreadPool") << parallelRootWithTP << defaultSum << defaultResult;
    QTest::newRow("Sequential") << sequentialRoot << defaultSum << defaultResult;
    QTest::newRow("SequentialWithFutureInterface") << sequentialRootWithFI << defaultSum << defaultResult;
    QTest::newRow("SequentialWithThreadPool") << sequentialRootWithTP << defaultSum << defaultResult;

    const auto setupSimpleAsync = [](Async<int> &task, int input) {
        task.setConcurrentCallData([](int input) { return input * 2; }, input);
    };
    const auto handleSimpleAsync = [](const Async<int> &task) {
        s_sum += task.result() / 4.;
        s_results.append(s_sum);
    };
    const Group simpleRoot = {
        sequential,
        onGroupSetup([] { s_sum = 0; }),
        AsyncTask<int>(std::bind(setupSimpleAsync, _1, 1), handleSimpleAsync, CallDoneIf::Success),
        AsyncTask<int>(std::bind(setupSimpleAsync, _1, 2), handleSimpleAsync, CallDoneIf::Success),
        AsyncTask<int>(std::bind(setupSimpleAsync, _1, 3), handleSimpleAsync, CallDoneIf::Success)
    };
    QTest::newRow("Simple") << simpleRoot << 3.0 << QList<double>({.5, 1.5, 3.});

    const auto setupStringAsync = [](Async<int> &task, const QString &input) {
        task.setConcurrentCallData([](const QString &input) -> int { return input.size(); }, input);
    };
    const auto handleStringAsync = [](const Async<int> &task) {
        s_sum /= task.result();
    };
    const Group stringRoot = {
        parallel,
        onGroupSetup([] { s_sum = 90.0; }),
        AsyncTask<int>(std::bind(setupStringAsync, _1, "blubb"), handleStringAsync, CallDoneIf::Success),
        AsyncTask<int>(std::bind(setupStringAsync, _1, "foo"), handleStringAsync, CallDoneIf::Success),
        AsyncTask<int>(std::bind(setupStringAsync, _1, "blah"), handleStringAsync, CallDoneIf::Success)
    };
    QTest::newRow("String") << stringRoot << 1.5 << QList<double>({});
}

void tst_Async::mapReduce()
{
    QThreadPool pool;

    s_sum = 0;
    s_results.clear();

    using namespace Tasking;

    QFETCH(Group, recipe);
    QFETCH(double, sum);
    QFETCH(QList<double>, results);

    QCOMPARE(TaskTree::runBlocking(recipe.withTimeout(1000ms)), DoneWith::Success);
    QCOMPARE(s_results, results);
    QCOMPARE(s_sum, sum);
}

QTEST_GUILESS_MAIN(tst_Async)

#include "tst_async.moc"
