// Copyright (C) 2025 Jarek Kobus
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

#include <QtTaskTree/qttasktreeglobal.h>

#if QT_CONFIG(concurrent)
#  include <QtTaskTree/qthreadfunctiontask.h>
#endif
#include <QtTaskTree/qbarriertask.h>
#include <QtTaskTree/qconditional.h>
#include <QtTaskTree/qtasktreerunner.h>

#include <QHash>
#include <QSignalSpy>
#include <QtTest>

using namespace QtTaskTree;

using namespace std::chrono;
using namespace std::chrono_literals;

using TaskObject = milliseconds;
using TestTask = QTimeoutTask;

constexpr milliseconds s_endlessTime = 1000000s;

namespace PrintableEnums {

Q_NAMESPACE

// TODO: Is it possible to check for synchronous invocation of subsequent events, so that
//       we may be sure that the control didn't go back to the main event loop between 2 events?
//       In theory: yes! We can add a signal / handler to the task tree sent before / after
//       receiving done signal from each task!
// TODO: Check if the QTaskTree's main guard isn't locked when receiving done signal from each task.

enum class Handler {
    Setup,
    Success,
    Error,
    Canceled,
    GroupSetup,
    GroupSuccess,
    GroupError,
    GroupCanceled,
    TweakSetupToSuccess,
    TweakSetupToError,
    TweakSetupToContinue,
    TweakDoneToSuccess,
    TweakDoneToError,
    Sync,
    BarrierAdvance,
    Timeout,
    Storage,
    Iteration,
};
Q_ENUM_NS(Handler);

enum class ThreadResult
{
    Success,
    FailOnTaskCountCheck,
    FailOnRunningCheck,
    FailOnProgressCheck,
    FailOnLogCheck,
    FailOnDoneStatusCheck,
    Canceled,
};
Q_ENUM_NS(ThreadResult);

enum class Execution
{
    Sync,
    Async,
};

} // namespace PrintableEnums

using namespace PrintableEnums;

using Log = QList<std::pair<int, Handler>>;

struct Message
{
    QString name;
    Handler handler;
    std::optional<Execution> execution = std::nullopt;
};

using MessageLog = QList<Message>;

struct CustomStorage
{
    CustomStorage() { s_count.fetch_add(1); }
    ~CustomStorage() { s_count.fetch_add(-1); }
    Log m_log;
    static int instanceCount() { return s_count.load(); }
private:
    static std::atomic_int s_count;
};

std::atomic_int CustomStorage::s_count = 0;

struct TestData
{
    Storage<CustomStorage> storage;
    Group root;
    Log expectedLog;
    int taskCount = 0;
    DoneWith result = DoneWith::Success;
    std::optional<int> asyncCount = std::nullopt;
    std::optional<MessageLog> messageLog = std::nullopt;
};

class tst_TaskTree : public QObject
{
    Q_OBJECT

private Q_SLOTS:
    void validConstructs(); // compile test
    void runtimeCheck(); // checks done on runtime
    void taskTreeRunner_data();
    void taskTreeRunner();
    void testTree_data();
    void testTree();
#if QT_CONFIG(concurrent)
    void testInThread_data();
    void testInThread();
    void testAutoDelayedSync();
#endif
    void storageIO_data();
    void storageIO();
    void storageOperators();
    void storageDestructor();
    void storageInitialization_data();
    void storageInitialization();
    void nestedBrokenStorage();
    void restart();
    void destructorOfTaskEmittingDone();
    void restartTaskTreeRunnerFromDoneHandler();
    void validConditionalConstructs();
    void exactHandlers();
    void boolDoneAdapter();
    void cleanupTestCase();
};

void tst_TaskTree::validConstructs()
{
    const DoneResult result = DoneResult::Success;

    const Group group1 {
        parallel,
        TestTask([](TaskObject &) {}, [](const TaskObject &, DoneWith) {}),
        TestTask([](TaskObject &) {}, [](const TaskObject &) {}),
        TestTask([](TaskObject &) {}, [](DoneWith) {}),
        TestTask([](TaskObject &) {}, [] {}),
        TestTask([](TaskObject &) {}, {}),
        TestTask([](TaskObject &) {}),
        TestTask({}, [](const TaskObject &, DoneWith) {}),
        TestTask({}, [](const TaskObject &) {}),
        TestTask({}, [](DoneWith) {}),
        TestTask({}, [] {}),
        TestTask({}, {}),
        TestTask({}, DoneResult::Error),
        TestTask({}, result),
        TestTask({})
    };

    const Group group2 {
        group1
    };

    const Group group3 {
        parallel,
        Group {
            parallel,
            TestTask([](TaskObject &) {}, [](const TaskObject &, DoneWith) {}),
            Group {
                parallel,
                TestTask([](TaskObject &) {}, [](const TaskObject &) {}),
                Group {
                    parallel,
                    TestTask([](TaskObject &) {}, [] {})
                }
            },
            Group {
                parallel,
                TestTask([](TaskObject &) {}, [](const TaskObject &, DoneWith) {}),
                onGroupDone([] {})
            }
        },
        group1,
        onGroupDone([] {})
    };

    const auto setupHandler = [](TaskObject &) {};
    const auto finishHandler = [](const TaskObject &) {};
    const auto errorHandler = [](const TaskObject &) {};
    const auto doneHandler = [](const TaskObject &, DoneWith) {};

    const Group group4 {
        parallel,
        TestTask(),
        TestTask(setupHandler),
        TestTask(setupHandler, finishHandler, CallDoneFlag::OnSuccess),
        TestTask(setupHandler, doneHandler),
        // need to explicitly pass empty handler for done
        TestTask(setupHandler, errorHandler, CallDoneFlag::OnError),
        TestTask({}, finishHandler, CallDoneFlag::OnSuccess),
        TestTask({}, errorHandler, CallDoneFlag::OnError)
    };

    const Group group5 {
        Forever {
            TestTask()
        }.withTimeout(1s)
    };

    // When turning each of below blocks on, you should see the specific compiler error message.

    // Sync handler needs to take no arguments and has to return void or bool.
#if 0
    Sync([] { return 7; });
#endif
#if 0
    Sync([](int) { });
#endif
#if 0
    Sync([](int) { return true; });
#endif

    // Group setup handler needs to take no arguments and has to return void or SetupResult.
#if 0
    onGroupSetup([] { return 7; });
#endif
#if 0
    onGroupSetup([](int) { });
#endif

    // Group done handler needs to take (DoneWith) or (void) as an argument and has to
    // return void or bool.
#if 0
    onGroupDone([] { return 7; });
#endif
#if 0
    onGroupDone([](DoneWith) { return 7; });
#endif
#if 0
    onGroupDone([](int) { });
#endif
#if 0
    onGroupDone([](DoneWith, int) { });
#endif

    // Task setup handler needs to take (Task &) as an argument and has to return void or
    // SetupResult.
#if 0
    TestTask([] {});
#endif
#if 0
    TestTask([] { return 7; });
#endif
#if 0
    TestTask([](TaskObject &) { return 7; });
#endif
#if 0
    TestTask([](TaskObject &, int) { return SetupResult::Continue; });
#endif

    // Task done handler needs to take (const Task &, DoneWith), (const Task &),
    // (DoneWith) or (void) as arguments and has to return void or bool.
#if 0
    TestTask({}, [](const TaskObject &, DoneWith) { return 7; });
#endif
#if 0
    TestTask({}, [](const TaskObject &) { return 7; });
#endif
#if 0
    TestTask({}, [] { return 7; });
#endif
#if 0
    TestTask({}, [](const TaskObject &, DoneWith, int) {});
#endif
#if 0
    TestTask({}, [](const TaskObject &, int) {});
#endif
#if 0
    TestTask({}, [](DoneWith, int) {});
#endif
#if 0
    TestTask({}, [](int) {});
#endif
#if 0
    TestTask({}, [](const TaskObject &, DoneWith, int) { return true; });
#endif
#if 0
    TestTask({}, [](const TaskObject &, int) { return true; });
#endif
#if 0
    TestTask({}, [](DoneWith, int) { return true; });
#endif
#if 0
    TestTask({}, [](int) { return true; });
#endif
}

void tst_TaskTree::runtimeCheck()
{
    {
        QTest::ignoreMessage(QtDebugMsg, QRegularExpression("^SOFT ASSERT: "));
        QTest::ignoreMessage(QtWarningMsg,
                             "Can't add the same storage into one Group twice, skipping...");
        const Storage<int> storage;

        Group {
            storage,
            storage,
        };
    }

    {
        QTest::ignoreMessage(QtDebugMsg, QRegularExpression("^SOFT ASSERT: "));
        QTest::ignoreMessage(QtWarningMsg,
                             "Can't add the same storage into one Group twice, skipping...");
        const Storage<int> storage1;
        const auto storage2 = storage1;

        Group {
            storage1,
            storage2,
        };
    }

    {
        QTest::ignoreMessage(QtDebugMsg, QRegularExpression("^SOFT ASSERT: "));
        QTest::ignoreMessage(QtWarningMsg,
                             "Group setup handler redefinition, overriding...");
        Group {
            onGroupSetup([] {}),
            onGroupSetup([] {}),
        };
    }

    {
        QTest::ignoreMessage(QtDebugMsg, QRegularExpression("^SOFT ASSERT: "));
        QTest::ignoreMessage(QtWarningMsg,
                             "Group done handler redefinition, overriding...");
        Group {
            onGroupDone([] {}),
            onGroupDone([] {}),
        };
    }

    {
        QTest::ignoreMessage(QtDebugMsg, QRegularExpression("^SOFT ASSERT: "));
        QTest::ignoreMessage(QtWarningMsg,
                             "Group execution mode redefinition, overriding...");
        Group {
            sequential,
            sequential,
        };
    }

    {
        QTest::ignoreMessage(QtDebugMsg, QRegularExpression("^SOFT ASSERT: "));
        QTest::ignoreMessage(QtWarningMsg,
                             "Group workflow policy redefinition, overriding...");
        Group {
            stopOnError,
            stopOnError,
        };
    }
}

struct RunnerData
{
    QList<std::pair<QString, Group>> recipes;
    Log singleLog;
    Log sequentialLog;
    Log parallelLog;
    Log mappedLog;
};

static Handler resultToGroupHandler(DoneWith doneWith)
{
    switch (doneWith) {
    case DoneWith::Success: return Handler::GroupSuccess;
    case DoneWith::Error: return Handler::GroupError;
    case DoneWith::Cancel: return Handler::GroupCanceled;
    }
    return Handler::GroupCanceled;
}

static int doneCount(const Log &log)
{
    int count = 0;
    for (const std::pair<int, Handler> &result : log) {
        if (result.second == Handler::GroupSuccess ||
            result.second == Handler::GroupError ||
            result.second == Handler::GroupCanceled) {
            ++count;
        }
    }
    return count;
}

static Log s_globalLog = {};

void tst_TaskTree::taskTreeRunner_data()
{
    QTest::addColumn<RunnerData>("runnerData");

    const auto setupTask = [](int taskId, milliseconds timeout) {
        return [taskId, timeout](TaskObject &taskObject) {
            taskObject = timeout;
            s_globalLog.append({taskId, Handler::Setup});
        };
    };

    const auto setupDone = [](int taskId, DoneResult result = DoneResult::Success) {
        return [taskId, result](DoneWith doneWith) {
            const Handler handler = doneWith == DoneWith::Cancel ? Handler::Canceled
                                    : result == DoneResult::Success ? Handler::Success : Handler::Error;
            s_globalLog.append({taskId, handler});
            return doneWith != DoneWith::Cancel && result == DoneResult::Success;
        };
    };

    const auto createTask = [setupTask, setupDone](
            int taskId, DoneResult result, milliseconds timeout = 0ms) {
        return TestTask(setupTask(taskId, timeout), setupDone(taskId, result));
    };

    const auto createSuccessTask = [createTask](int taskId, milliseconds timeout = 0ms) {
        return createTask(taskId, DoneResult::Success, timeout);
    };

    const auto createFailingTask = [createTask](int taskId, milliseconds timeout = 0ms) {
        return createTask(taskId, DoneResult::Error, timeout);
    };

    const auto groupSetup = [](int taskId) {
        return onGroupSetup([taskId] {
            s_globalLog.append({taskId, Handler::GroupSetup});
        });
    };

    const auto groupDone = [](int taskId, CallDone calldone = CallDoneFlag::Always) {
        return onGroupDone([taskId](DoneWith result) {
            s_globalLog.append({taskId, resultToGroupHandler(result)});
        }, calldone);
    };

    {
        const Group recipe_A {
            groupSetup(1),
            createSuccessTask(11),
            createFailingTask(12),
            groupDone(1)
        };

        const Group recipe_B {
            groupSetup(2),
            createSuccessTask(21),
            groupDone(2)
        };

        const Group recipe_C {
            groupSetup(3),
            createFailingTask(31),
            createSuccessTask(32),
            groupDone(3)
        };

        const Log singleLog {
            {1, Handler::GroupSetup},
            {11, Handler::Setup},
            {2, Handler::GroupSetup},
            {21, Handler::Setup},
            {3, Handler::GroupSetup},
            {31, Handler::Setup},
            {31, Handler::Error},
            {3, Handler::GroupError}
        };

        const Log sequentialLog {
            {1, Handler::GroupSetup},
            {11, Handler::Setup},
            {11, Handler::Success},
            {12, Handler::Setup},
            {12, Handler::Error},
            {1, Handler::GroupError},
            {2, Handler::GroupSetup},
            {21, Handler::Setup},
            {21, Handler::Success},
            {2, Handler::GroupSuccess},
            {3, Handler::GroupSetup},
            {31, Handler::Setup},
            {31, Handler::Error},
            {3, Handler::GroupError}
        };

        const Log parallelLog {
            {1, Handler::GroupSetup},
            {11, Handler::Setup},
            {2, Handler::GroupSetup},
            {21, Handler::Setup},
            {3, Handler::GroupSetup},
            {31, Handler::Setup},
            {11, Handler::Success},
            {12, Handler::Setup},
            {21, Handler::Success},
            {2, Handler::GroupSuccess},
            {31, Handler::Error},
            {3, Handler::GroupError},
            {12, Handler::Error},
            {1, Handler::GroupError}
        };

        const Log mappedLog {
            {1, Handler::GroupSetup},
            {11, Handler::Setup},
            {2, Handler::GroupSetup},
            {21, Handler::Setup},
            {3, Handler::GroupSetup},
            {31, Handler::Setup},
            {21, Handler::Success},
            {2, Handler::GroupSuccess},
            {31, Handler::Error},
            {3, Handler::GroupError}
        };

        QTest::newRow("TaskTreeRunner")
                << RunnerData{{{"keyA", recipe_A}, {"keyB", recipe_B}, {"keyA", recipe_C}},
                              singleLog, sequentialLog, parallelLog, mappedLog};
    }
}

void tst_TaskTree::taskTreeRunner()
{
    QFETCH(RunnerData, runnerData);

    int doneCounter = 0;
    const auto onDone = [&doneCounter] { ++doneCounter; };

    {
        s_globalLog = {};
        doneCounter = 0;
        const Log expectedLog = runnerData.singleLog;
        QSingleTaskTreeRunner taskTreeRunner;
        for (const std::pair<QString, Group> &recipe : runnerData.recipes)
            taskTreeRunner.start(recipe.second, {}, onDone);

        QTRY_VERIFY_WITH_TIMEOUT(!taskTreeRunner.isRunning(), 1s);

        QVERIFY(!taskTreeRunner.isRunning());
        QCOMPARE(s_globalLog, expectedLog);
        QCOMPARE(doneCounter, doneCount(expectedLog));
    }

    {
        s_globalLog = {};
        doneCounter = 0;
        const Log expectedLog = runnerData.sequentialLog;
        QSequentialTaskTreeRunner taskTreeRunner;
        for (const std::pair<QString, Group> &recipe : runnerData.recipes)
            taskTreeRunner.enqueue(recipe.second, {}, onDone);

        QTRY_VERIFY_WITH_TIMEOUT(!taskTreeRunner.isRunning(), 1s);

        QVERIFY(!taskTreeRunner.isRunning());
        QCOMPARE(s_globalLog, expectedLog);
        QCOMPARE(doneCounter, doneCount(expectedLog));
    }

    {
        s_globalLog = {};
        doneCounter = 0;
        const Log expectedLog = runnerData.parallelLog;
        QParallelTaskTreeRunner taskTreeRunner;
        for (const std::pair<QString, Group> &recipe : runnerData.recipes)
            taskTreeRunner.start(recipe.second, {}, onDone);

        QTRY_VERIFY_WITH_TIMEOUT(!taskTreeRunner.isRunning(), 1s);

        QVERIFY(!taskTreeRunner.isRunning());
        QCOMPARE(s_globalLog, expectedLog);
        QCOMPARE(doneCounter, doneCount(expectedLog));
    }

    {
        s_globalLog = {};
        doneCounter = 0;
        const Log expectedLog = runnerData.mappedLog;
        QMappedTaskTreeRunner<QString> taskTreeRunner;
        for (const std::pair<QString, Group> &recipe : runnerData.recipes)
            taskTreeRunner.start(recipe.first, recipe.second, {}, onDone);

        QTRY_VERIFY_WITH_TIMEOUT(!taskTreeRunner.isRunning(), 1s);

        QVERIFY(!taskTreeRunner.isRunning());
        QCOMPARE(s_globalLog, expectedLog);
        QCOMPARE(doneCounter, doneCount(expectedLog));
    }

    s_globalLog = {};
}

class TickAndDone final : public QObject
{
    Q_OBJECT

public:
    void setInterval(const milliseconds &interval) { m_interval = interval; }
    void start() {
        QTimer::singleShot(0, this, [this] {
            Q_EMIT tick();
            QTimer::singleShot(m_interval, this, [this] { Q_EMIT done(DoneResult::Success); });
        });
    }

Q_SIGNALS:
    void tick();
    void done(QtTaskTree::DoneResult result);

private:
    milliseconds m_interval;
};

using TickAndDoneTask = QCustomTask<TickAndDone>;

static ExecutableItem createBarrierAdvance(const Storage<CustomStorage> &storage,
                                           const QStoredBarrier &barrier, int taskId)
{
    return TickAndDoneTask([storage, barrier, taskId](TickAndDone &tickAndDone) {
        tickAndDone.setInterval(1ms);
        storage->m_log.append({taskId, Handler::Setup});

        CustomStorage *currentStorage = storage.activeStorage();
        QBarrier *activeBarrier = barrier.activeStorage();
        QObject::connect(&tickAndDone, &TickAndDone::tick, activeBarrier,
                         [currentStorage, activeBarrier, taskId] {
            currentStorage->m_log.append({taskId, Handler::BarrierAdvance});
            activeBarrier->advance();
        });
    });
}

static Handler toTweakSetupHandler(SetupResult result)
{
    switch (result) {
    case SetupResult::Continue: return Handler::TweakSetupToContinue;
    case SetupResult::StopWithSuccess: return Handler::TweakSetupToSuccess;
    case SetupResult::StopWithError: return Handler::TweakSetupToError;
    }
    return Handler::TweakSetupToContinue;
}

static Handler toTweakDoneHandler(DoneResult result)
{
    return result == DoneResult::Success ? Handler::TweakDoneToSuccess : Handler::TweakDoneToError;
}

static TestData storageShadowingData()
{
    // This test checks if storage shadowing works OK.

    const Storage<CustomStorage> storage;
    // This helper storage collect the pointers to storages created by shadowedStorage.
    const Storage<QHash<int, int *>> helperStorage; // One instance in this test.
    // This storage is repeated in nested groups, the innermost storage will shadow outer ones.
    const Storage<int> shadowedStorage; // Three instances in this test.

    const auto groupSetupWithStorage = [storage, helperStorage, shadowedStorage](int taskId) {
        return onGroupSetup([storage, helperStorage, shadowedStorage, taskId] {
            storage->m_log.append({taskId, Handler::GroupSetup});
            helperStorage->insert(taskId, shadowedStorage.activeStorage());
            *shadowedStorage = taskId;
        });
    };
    const auto groupDoneWithStorage = [storage, helperStorage, shadowedStorage](int taskId) {
        return onGroupDone([storage, helperStorage, shadowedStorage, taskId](DoneWith result) {
            storage->m_log.append({taskId, resultToGroupHandler(result)});
            auto it = helperStorage->constFind(taskId);
            if (it == helperStorage->constEnd()) {
                qWarning() << "The helperStorage is missing the shadowedStorage.";
                return;
            } else if (*it != shadowedStorage.activeStorage()) {
                qWarning() << "Wrong active instance of the shadowedStorage.";
                return;
            } else if (**it != taskId) {
                qWarning() << "Wrong data of the active instance of the shadowedStorage.";
                return;
            }
            helperStorage->erase(it);
            storage->m_log.append({*shadowedStorage, Handler::Storage});
        });
    };

    const Group root {
        storage,
        helperStorage,
        shadowedStorage,
        groupSetupWithStorage(1),
        Group {
            shadowedStorage,
            groupSetupWithStorage(2),
            Group {
                shadowedStorage,
                groupSetupWithStorage(3),
                groupDoneWithStorage(3)
            },
            Group {
                shadowedStorage,
                groupSetupWithStorage(4),
                groupDoneWithStorage(4)
            },
            groupDoneWithStorage(2)
        },
        groupDoneWithStorage(1)
    };

    const Log log {
        {1, Handler::GroupSetup},
        {2, Handler::GroupSetup},
        {3, Handler::GroupSetup},
        {3, Handler::GroupSuccess},
        {3, Handler::Storage},
        {4, Handler::GroupSetup},
        {4, Handler::GroupSuccess},
        {4, Handler::Storage},
        {2, Handler::GroupSuccess},
        {2, Handler::Storage},
        {1, Handler::GroupSuccess},
        {1, Handler::Storage},
    };

    return {storage, root, log, 0, DoneWith::Success, 0};
}

static TestData parallelData()
{
    Storage<CustomStorage> storage;

    const auto setupTask = [storage](int taskId, milliseconds timeout) {
        return [storage, taskId, timeout](TaskObject &taskObject) {
            taskObject = timeout;
            storage->m_log.append({taskId, Handler::Setup});
        };
    };

    const auto setupDone = [storage](int taskId, DoneResult result = DoneResult::Success) {
        return [storage, taskId, result](DoneWith doneWith) {
            const Handler handler = doneWith == DoneWith::Cancel ? Handler::Canceled
                                    : result == DoneResult::Success ? Handler::Success : Handler::Error;
            storage->m_log.append({taskId, handler});
            return doneWith != DoneWith::Cancel && result == DoneResult::Success;
        };
    };

    const auto createTask = [storage, setupTask, setupDone](
                             int taskId, DoneResult result, milliseconds timeout = 0ms) {
        return TestTask(setupTask(taskId, timeout), setupDone(taskId, result));
    };

    const auto createSuccessTask = [createTask](int taskId, milliseconds timeout = 0ms) {
        return createTask(taskId, DoneResult::Success, timeout);
    };

    const auto groupDone = [storage](int taskId) {
        return onGroupDone([storage, taskId](DoneWith result) {
            storage->m_log.append({taskId, resultToGroupHandler(result)});
        });
    };

    const Group root {
        storage,
        parallel,
        createSuccessTask(1),
        createSuccessTask(2),
        createSuccessTask(3),
        createSuccessTask(4),
        createSuccessTask(5),
        groupDone(0)
    };

    const Log log {
        {1, Handler::Setup}, // Setup order is determined in parallel mode
        {2, Handler::Setup},
        {3, Handler::Setup},
        {4, Handler::Setup},
        {5, Handler::Setup},
        {1, Handler::Success},
        {2, Handler::Success},
        {3, Handler::Success},
        {4, Handler::Success},
        {5, Handler::Success},
        {0, Handler::GroupSuccess}
    };

    return {storage, root, log, 5, DoneWith::Success};
}

void tst_TaskTree::testTree_data()
{
    QTest::addColumn<TestData>("testData");

    Storage<CustomStorage> storage;

    const auto setupTask = [storage](int taskId, milliseconds timeout) {
        return [storage, taskId, timeout](TaskObject &taskObject) {
            taskObject = timeout;
            storage->m_log.append({taskId, Handler::Setup});
        };
    };

    const auto setupTaskWithTweak = [storage](int taskId, SetupResult result) {
        return [storage, taskId, result](TaskObject &) {
            storage->m_log.append({taskId, Handler::Setup});
            storage->m_log.append({taskId, toTweakSetupHandler(result)});
            return result;
        };
    };

    const auto setupDone = [storage](int taskId, DoneResult result = DoneResult::Success) {
        return [storage, taskId, result](DoneWith doneWith) {
            const Handler handler = doneWith == DoneWith::Cancel ? Handler::Canceled
                                    : result == DoneResult::Success ? Handler::Success : Handler::Error;
            storage->m_log.append({taskId, handler});
            return doneWith != DoneWith::Cancel && result == DoneResult::Success;
        };
    };

    const auto setupTimeout = [storage](int taskId) {
        return [storage, taskId] {
            storage->m_log.append({taskId, Handler::Timeout});
        };
    };

    const auto createTask = [storage, setupTask, setupDone](
            int taskId, DoneResult result, milliseconds timeout = 0ms) {
        return TestTask(setupTask(taskId, timeout), setupDone(taskId, result));
    };

    const auto createSuccessTask = [createTask](int taskId, milliseconds timeout = 0ms) {
        return createTask(taskId, DoneResult::Success, timeout);
    };

    const auto createFailingTask = [createTask](int taskId, milliseconds timeout = 0ms) {
        return createTask(taskId, DoneResult::Error, timeout);
    };

    const auto createTaskWithSetupTweak = [storage, setupTaskWithTweak, setupDone](
                                          int taskId, SetupResult desiredResult) {
        return TestTask(setupTaskWithTweak(taskId, desiredResult), setupDone(taskId));
    };

    const auto groupSetup = [storage](int taskId) {
        return onGroupSetup([storage, taskId] {
            storage->m_log.append({taskId, Handler::GroupSetup});
        });
    };
    const auto groupDone = [storage](int taskId, CallDone callDone = CallDoneFlag::Always) {
        return onGroupDone([storage, taskId](DoneWith result) {
            storage->m_log.append({taskId, resultToGroupHandler(result)});
        }, callDone);
    };
    const auto groupSetupWithTweak = [storage](int taskId, SetupResult desiredResult) {
        return onGroupSetup([storage, taskId, desiredResult] {
            storage->m_log.append({taskId, Handler::GroupSetup});
            storage->m_log.append({taskId, toTweakSetupHandler(desiredResult)});
            return desiredResult;
        });
    };
    const auto groupDoneWithTweak = [storage](int taskId, DoneResult result) {
        return onGroupDone([storage, taskId, result](DoneWith doneWith) {
            storage->m_log.append({taskId, resultToGroupHandler(doneWith)});
            storage->m_log.append({taskId, toTweakDoneHandler(result)});
            return result;
        });
    };
    const auto createSync = [storage](int taskId) {
        return QSyncTask([storage, taskId] { storage->m_log.append({taskId, Handler::Sync}); });
    };
    const auto createSyncWithTweak = [storage](int taskId, DoneResult result) {
        return QSyncTask([storage, taskId, result] {
            storage->m_log.append({taskId, Handler::Sync});
            storage->m_log.append({taskId, toTweakDoneHandler(result)});
            return result;
        });
    };

    {
        const Group root1 {
            storage,
            groupDone(0)
        };
        const Group root2 {
            storage,
            onGroupSetup([] { return SetupResult::Continue; }),
            groupDone(0)
        };
        const Group root3 {
            storage,
            onGroupSetup([] { return SetupResult::StopWithSuccess; }),
            groupDone(0)
        };
        const Group root4 {
            storage,
            onGroupSetup([] { return SetupResult::StopWithError; }),
            groupDone(0)
        };

        const Log logDone {{0, Handler::GroupSuccess}};
        const Log logError {{0, Handler::GroupError}};

        QTest::newRow("Empty") << TestData{storage, root1, logDone, 0, DoneWith::Success, 0};
        QTest::newRow("EmptyContinue") << TestData{storage, root2, logDone, 0, DoneWith::Success, 0};
        QTest::newRow("EmptyDone") << TestData{storage, root3, logDone, 0, DoneWith::Success, 0};
        QTest::newRow("EmptyError") << TestData{storage, root4, logError, 0, DoneWith::Error, 0};
    }

    {
        const auto setupGroup = [=](SetupResult setupResult, WorkflowPolicy policy) {
            return Group {
                storage,
                workflowPolicy(policy),
                onGroupSetup([setupResult] { return setupResult; }),
                groupDone(0)
            };
        };

        const auto doneData = [storage, setupGroup](WorkflowPolicy policy) {
            return TestData{storage, setupGroup(SetupResult::StopWithSuccess, policy),
                            Log{{0, Handler::GroupSuccess}}, 0, DoneWith::Success, 0};
        };
        const auto errorData = [storage, setupGroup](WorkflowPolicy policy) {
            return TestData{storage, setupGroup(SetupResult::StopWithError, policy),
                            Log{{0, Handler::GroupError}}, 0, DoneWith::Error, 0};
        };

        QTest::newRow("DoneAndStopOnError") << doneData(WorkflowPolicy::StopOnError);
        QTest::newRow("DoneAndContinueOnError") << doneData(WorkflowPolicy::ContinueOnError);
        QTest::newRow("DoneAndStopOnSuccess") << doneData(WorkflowPolicy::StopOnSuccess);
        QTest::newRow("DoneAndContinueOnSuccess") << doneData(WorkflowPolicy::ContinueOnSuccess);
        QTest::newRow("DoneAndStopOnSuccessOrError") << doneData(WorkflowPolicy::StopOnSuccessOrError);
        QTest::newRow("DoneAndFinishAllAndSuccess") << doneData(WorkflowPolicy::FinishAllAndSuccess);
        QTest::newRow("DoneAndFinishAllAndError") << doneData(WorkflowPolicy::FinishAllAndError);

        QTest::newRow("ErrorAndStopOnError") << errorData(WorkflowPolicy::StopOnError);
        QTest::newRow("ErrorAndContinueOnError") << errorData(WorkflowPolicy::ContinueOnError);
        QTest::newRow("ErrorAndStopOnSuccess") << errorData(WorkflowPolicy::StopOnSuccess);
        QTest::newRow("ErrorAndContinueOnSuccess") << errorData(WorkflowPolicy::ContinueOnSuccess);
        QTest::newRow("ErrorAndStopOnSuccessOrError") << errorData(WorkflowPolicy::StopOnSuccessOrError);
        QTest::newRow("ErrorAndFinishAllAndSuccess") << errorData(WorkflowPolicy::FinishAllAndSuccess);
        QTest::newRow("ErrorAndFinishAllAndError") << errorData(WorkflowPolicy::FinishAllAndError);
    }

    {
        // These tests ensure that tweaking the done result in group's done handler takes priority
        // over the group's workflow policy. In this case the group's workflow policy is ignored.
        const auto setupGroup = [=](DoneResult doneResult, WorkflowPolicy policy) {
            return Group {
                storage,
                Group {
                    workflowPolicy(policy),
                    onGroupDone([doneResult] { return doneResult; })
                },
                groupDone(0)
            };
        };

        const auto doneData = [storage, setupGroup](WorkflowPolicy policy) {
            return TestData{storage, setupGroup(DoneResult::Success, policy),
                            Log{{0, Handler::GroupSuccess}}, 0, DoneWith::Success, 0};
        };
        const auto errorData = [storage, setupGroup](WorkflowPolicy policy) {
            return TestData{storage, setupGroup(DoneResult::Error, policy),
                            Log{{0, Handler::GroupError}}, 0, DoneWith::Error, 0};
        };

        QTest::newRow("GroupDoneTweakSuccessWithStopOnError")
            << doneData(WorkflowPolicy::StopOnError);
        QTest::newRow("GroupDoneTweakSuccessWithContinueOnError")
            << doneData(WorkflowPolicy::ContinueOnError);
        QTest::newRow("GroupDoneTweakSuccessWithStopOnSuccess")
            << doneData(WorkflowPolicy::StopOnSuccess);
        QTest::newRow("GroupDoneTweakSuccessWithContinueOnSuccess")
            << doneData(WorkflowPolicy::ContinueOnSuccess);
        QTest::newRow("GroupDoneTweakSuccessWithStopOnSuccessOrError")
            << doneData(WorkflowPolicy::StopOnSuccessOrError);
        QTest::newRow("GroupDoneTweakSuccessWithFinishAllAndSuccess")
            << doneData(WorkflowPolicy::FinishAllAndSuccess);
        QTest::newRow("GroupDoneTweakSuccessWithFinishAllAndError")
            << doneData(WorkflowPolicy::FinishAllAndError);

        QTest::newRow("GroupDoneTweakErrorWithStopOnError")
            << errorData(WorkflowPolicy::StopOnError);
        QTest::newRow("GroupDoneTweakErrorWithContinueOnError")
            << errorData(WorkflowPolicy::ContinueOnError);
        QTest::newRow("GroupDoneTweakErrorWithStopOnSuccess")
            << errorData(WorkflowPolicy::StopOnSuccess);
        QTest::newRow("GroupDoneTweakErrorWithContinueOnSuccess")
            << errorData(WorkflowPolicy::ContinueOnSuccess);
        QTest::newRow("GroupDoneTweakErrorWithStopOnSuccessOrError")
            << errorData(WorkflowPolicy::StopOnSuccessOrError);
        QTest::newRow("GroupDoneTweakErrorWithFinishAllAndSuccess")
            << errorData(WorkflowPolicy::FinishAllAndSuccess);
        QTest::newRow("GroupDoneTweakErrorWithFinishAllAndError")
            << errorData(WorkflowPolicy::FinishAllAndError);
    }

    {
        const Group root {
            storage,
            createTaskWithSetupTweak(1, SetupResult::StopWithSuccess),
            createTaskWithSetupTweak(2, SetupResult::StopWithSuccess)
        };
        const Log log {
            {1, Handler::Setup},
            {1, Handler::TweakSetupToSuccess},
            {2, Handler::Setup},
            {2, Handler::TweakSetupToSuccess}
        };
        QTest::newRow("TweakTaskSuccess") << TestData{storage, root, log, 2, DoneWith::Success, 0};
    }

    {
        const Group root {
            storage,
            createTaskWithSetupTweak(1, SetupResult::StopWithError),
            createTaskWithSetupTweak(2, SetupResult::StopWithError)
        };
        const Log log {
            {1, Handler::Setup},
            {1, Handler::TweakSetupToError}
        };
        QTest::newRow("TweakTaskError") << TestData{storage, root, log, 2, DoneWith::Error, 0};
    }

    {
        const Group root {
            storage,
            createTaskWithSetupTweak(1, SetupResult::Continue),
            createTaskWithSetupTweak(2, SetupResult::Continue),
            createTaskWithSetupTweak(3, SetupResult::StopWithError),
            createTaskWithSetupTweak(4, SetupResult::Continue)
        };
        const Log log {
            {1, Handler::Setup},
            {1, Handler::TweakSetupToContinue},
            {1, Handler::Success},
            {2, Handler::Setup},
            {2, Handler::TweakSetupToContinue},
            {2, Handler::Success},
            {3, Handler::Setup},
            {3, Handler::TweakSetupToError}
        };
        QTest::newRow("TweakMixed") << TestData{storage, root, log, 4, DoneWith::Error, 2};
    }

    {
        const Group root {
            parallel,
            storage,
            createTaskWithSetupTweak(1, SetupResult::Continue),
            createTaskWithSetupTweak(2, SetupResult::Continue),
            createTaskWithSetupTweak(3, SetupResult::StopWithError),
            createTaskWithSetupTweak(4, SetupResult::Continue)
        };
        const Log log {
            {1, Handler::Setup},
            {1, Handler::TweakSetupToContinue},
            {2, Handler::Setup},
            {2, Handler::TweakSetupToContinue},
            {3, Handler::Setup},
            {3, Handler::TweakSetupToError},
            {1, Handler::Canceled},
            {2, Handler::Canceled}
        };
        QTest::newRow("TweakParallel") << TestData{storage, root, log, 4, DoneWith::Error, 0};
    }

    {
        const Group root {
            parallel,
            storage,
            createTaskWithSetupTweak(1, SetupResult::Continue),
            createTaskWithSetupTweak(2, SetupResult::Continue),
            Group {
                createTaskWithSetupTweak(3, SetupResult::StopWithError)
            },
            createTaskWithSetupTweak(4, SetupResult::Continue)
        };
        const Log log {
            {1, Handler::Setup},
            {1, Handler::TweakSetupToContinue},
            {2, Handler::Setup},
            {2, Handler::TweakSetupToContinue},
            {3, Handler::Setup},
            {3, Handler::TweakSetupToError},
            {1, Handler::Canceled},
            {2, Handler::Canceled}
        };
        QTest::newRow("TweakParallelGroup") << TestData{storage, root, log, 4, DoneWith::Error, 0};
    }

    {
        const Group root {
            parallel,
            storage,
            createTaskWithSetupTweak(1, SetupResult::Continue),
            createTaskWithSetupTweak(2, SetupResult::Continue),
            Group {
                groupSetupWithTweak(0, SetupResult::StopWithError),
                createTaskWithSetupTweak(3, SetupResult::Continue)
            },
            createTaskWithSetupTweak(4, SetupResult::Continue)
        };
        const Log log {
            {1, Handler::Setup},
            {1, Handler::TweakSetupToContinue},
            {2, Handler::Setup},
            {2, Handler::TweakSetupToContinue},
            {0, Handler::GroupSetup},
            {0, Handler::TweakSetupToError},
            {1, Handler::Canceled},
            {2, Handler::Canceled}
        };
        QTest::newRow("TweakParallelGroupSetup")
            << TestData{storage, root, log, 4, DoneWith::Error, 0};
    }

    {
        const Group root {
            storage,
            Group {
                Group {
                    Group {
                        Group {
                            Group {
                                createSuccessTask(5),
                                groupSetup(5),
                                groupDone(5)
                            },
                            groupSetup(4),
                            groupDone(4)
                        },
                        groupSetup(3),
                        groupDone(3)
                    },
                    groupSetup(2),
                    groupDone(2)
                },
                groupSetup(1),
                groupDone(1)
            },
            groupDone(0)
        };
        const Log log {
            {1, Handler::GroupSetup},
            {2, Handler::GroupSetup},
            {3, Handler::GroupSetup},
            {4, Handler::GroupSetup},
            {5, Handler::GroupSetup},
            {5, Handler::Setup},
            {5, Handler::Success},
            {5, Handler::GroupSuccess},
            {4, Handler::GroupSuccess},
            {3, Handler::GroupSuccess},
            {2, Handler::GroupSuccess},
            {1, Handler::GroupSuccess},
            {0, Handler::GroupSuccess}
        };
        QTest::newRow("Nested") << TestData{storage, root, log, 1, DoneWith::Success, 1};
    }

    QTest::newRow("Parallel") << parallelData();

    {
        auto setupSubTree = [storage, createSuccessTask](QTaskTree &taskTree) {
            const Group nestedRoot {
                storage,
                createSuccessTask(2),
                createSuccessTask(3),
                createSuccessTask(4)
            };
            taskTree.setRecipe(nestedRoot);
            CustomStorage *activeStorage = storage.activeStorage();
            const auto collectSubLog = [activeStorage](const CustomStorage &subTreeStorage){
                activeStorage->m_log += subTreeStorage.m_log;
            };
            taskTree.onStorageDone(storage, collectSubLog);
        };
        const Group root1 {
            storage,
            createSuccessTask(1),
            createSuccessTask(2),
            createSuccessTask(3),
            createSuccessTask(4),
            createSuccessTask(5),
            groupDone(0)
        };
        const Group root2 {
            storage,
            Group { createSuccessTask(1) },
            Group { createSuccessTask(2) },
            Group { createSuccessTask(3) },
            Group { createSuccessTask(4) },
            Group { createSuccessTask(5) },
            groupDone(0)
        };
        const Group root3 {
            storage,
            createSuccessTask(1),
            QTaskTreeTask(setupSubTree),
            createSuccessTask(5),
            groupDone(0)
        };
        const Log log {
            {1, Handler::Setup},
            {1, Handler::Success},
            {2, Handler::Setup},
            {2, Handler::Success},
            {3, Handler::Setup},
            {3, Handler::Success},
            {4, Handler::Setup},
            {4, Handler::Success},
            {5, Handler::Setup},
            {5, Handler::Success},
            {0, Handler::GroupSuccess}
        };
        QTest::newRow("Sequential") << TestData{storage, root1, log, 5, DoneWith::Success, 5};
        QTest::newRow("SequentialEncapsulated") << TestData{storage, root2, log, 5, DoneWith::Success, 5};
        // We don't inspect subtrees, so taskCount is 3, not 5.
        QTest::newRow("SequentialSubTree") << TestData{storage, root3, log, 3, DoneWith::Success, 3};
    }

    {
        const Group root {
            storage,
            Group {
                createSuccessTask(1),
                Group {
                    createSuccessTask(2),
                    Group {
                        createSuccessTask(3),
                        Group {
                            createSuccessTask(4),
                            Group {
                                createSuccessTask(5),
                                groupDone(5)
                            },
                            groupDone(4)
                        },
                        groupDone(3)
                    },
                    groupDone(2)
                },
                groupDone(1)
            },
            groupDone(0)
        };
        const Log log {
            {1, Handler::Setup},
            {1, Handler::Success},
            {2, Handler::Setup},
            {2, Handler::Success},
            {3, Handler::Setup},
            {3, Handler::Success},
            {4, Handler::Setup},
            {4, Handler::Success},
            {5, Handler::Setup},
            {5, Handler::Success},
            {5, Handler::GroupSuccess},
            {4, Handler::GroupSuccess},
            {3, Handler::GroupSuccess},
            {2, Handler::GroupSuccess},
            {1, Handler::GroupSuccess},
            {0, Handler::GroupSuccess}
        };
        QTest::newRow("SequentialNested") << TestData{storage, root, log, 5, DoneWith::Success, 5};
    }

    {
        const Group root {
            storage,
            createSuccessTask(1),
            createSuccessTask(2),
            createFailingTask(3),
            createSuccessTask(4),
            createSuccessTask(5),
            groupDone(0)
        };
        const Log log {
            {1, Handler::Setup},
            {1, Handler::Success},
            {2, Handler::Setup},
            {2, Handler::Success},
            {3, Handler::Setup},
            {3, Handler::Error},
            {0, Handler::GroupError}
        };
        QTest::newRow("SequentialError") << TestData{storage, root, log, 5, DoneWith::Error, 3};
    }

    {
        const auto testData = [storage, groupDone](WorkflowPolicy policy, DoneWith result) {
            return TestData {
                storage,
                Group {
                    storage,
                    workflowPolicy(policy),
                    groupDone(0)
                },
                Log {{0, result == DoneWith::Success ? Handler::GroupSuccess : Handler::GroupError}},
                0,
                result,
                0
            };
        };

        QTest::newRow("EmptyStopOnError")
            << testData(WorkflowPolicy::StopOnError, DoneWith::Success);
        QTest::newRow("EmptyContinueOnError")
            << testData(WorkflowPolicy::ContinueOnError, DoneWith::Success);
        QTest::newRow("EmptyStopOnSuccess")
            << testData(WorkflowPolicy::StopOnSuccess, DoneWith::Error);
        QTest::newRow("EmptyContinueOnSuccess")
            << testData(WorkflowPolicy::ContinueOnSuccess, DoneWith::Error);
        QTest::newRow("EmptyStopOnSuccessOrError")
            << testData(WorkflowPolicy::StopOnSuccessOrError, DoneWith::Error);
        QTest::newRow("EmptyFinishAllAndSuccess")
            << testData(WorkflowPolicy::FinishAllAndSuccess, DoneWith::Success);
        QTest::newRow("EmptyFinishAllAndError")
            << testData(WorkflowPolicy::FinishAllAndError, DoneWith::Error);
    }

    {
        const auto testData = [storage, groupDone, createSuccessTask](WorkflowPolicy policy,
                                                                      DoneWith result) {
            return TestData {
                storage,
                Group {
                    storage,
                    workflowPolicy(policy),
                    createSuccessTask(1),
                    groupDone(0)
                },
                Log {
                    {1, Handler::Setup},
                    {1, Handler::Success},
                    {0, result == DoneWith::Success ? Handler::GroupSuccess : Handler::GroupError}
                },
                1,
                result,
                1
            };
        };

        QTest::newRow("DoneStopOnError")
            << testData(WorkflowPolicy::StopOnError, DoneWith::Success);
        QTest::newRow("DoneContinueOnError")
            << testData(WorkflowPolicy::ContinueOnError, DoneWith::Success);
        QTest::newRow("DoneStopOnSuccess")
            << testData(WorkflowPolicy::StopOnSuccess, DoneWith::Success);
        QTest::newRow("DoneContinueOnSuccess")
            << testData(WorkflowPolicy::ContinueOnSuccess, DoneWith::Success);
        QTest::newRow("DoneStopOnSuccessOrError")
            << testData(WorkflowPolicy::StopOnSuccessOrError, DoneWith::Success);
        QTest::newRow("DoneFinishAllAndSuccess")
            << testData(WorkflowPolicy::FinishAllAndSuccess, DoneWith::Success);
        QTest::newRow("DoneFinishAllAndError")
            << testData(WorkflowPolicy::FinishAllAndError, DoneWith::Error);
    }

    {
        const auto testData = [storage, groupDone, createFailingTask](WorkflowPolicy policy,
                                                                      DoneWith result) {
            return TestData {
                storage,
                Group {
                    storage,
                    workflowPolicy(policy),
                    createFailingTask(1),
                    groupDone(0)
                },
                Log {
                    {1, Handler::Setup},
                    {1, Handler::Error},
                    {0, result == DoneWith::Success ? Handler::GroupSuccess : Handler::GroupError}
                },
                1,
                result,
                1
            };
        };

        QTest::newRow("ErrorStopOnError")
            << testData(WorkflowPolicy::StopOnError, DoneWith::Error);
        QTest::newRow("ErrorContinueOnError")
            << testData(WorkflowPolicy::ContinueOnError, DoneWith::Error);
        QTest::newRow("ErrorStopOnSuccess")
            << testData(WorkflowPolicy::StopOnSuccess, DoneWith::Error);
        QTest::newRow("ErrorContinueOnSuccess")
            << testData(WorkflowPolicy::ContinueOnSuccess, DoneWith::Error);
        QTest::newRow("ErrorStopOnSuccessOrError")
            << testData(WorkflowPolicy::StopOnSuccessOrError, DoneWith::Error);
        QTest::newRow("ErrorFinishAllAndSuccess")
            << testData(WorkflowPolicy::FinishAllAndSuccess, DoneWith::Success);
        QTest::newRow("ErrorFinishAllAndError")
            << testData(WorkflowPolicy::FinishAllAndError, DoneWith::Error);
    }

    {
        // These tests check whether the proper root's group end handler is called
        // when the group is stopped. Test it with different workflow policies.
        // The root starts one short failing task together with one long task.
        const auto createRoot = [storage, createSuccessTask, createFailingTask, groupDone](
                                    WorkflowPolicy policy) {
            return Group {
                storage,
                parallel,
                workflowPolicy(policy),
                createFailingTask(1, 1ms),
                createSuccessTask(2, 1ms),
                groupDone(0)
            };
        };

        const Log errorErrorLog = {
            {1, Handler::Setup},
            {2, Handler::Setup},
            {1, Handler::Error},
            {2, Handler::Canceled},
            {0, Handler::GroupError}
        };

        const Log errorDoneLog = {
            {1, Handler::Setup},
            {2, Handler::Setup},
            {1, Handler::Error},
            {2, Handler::Success},
            {0, Handler::GroupError}
        };

        const Log doneLog = {
            {1, Handler::Setup},
            {2, Handler::Setup},
            {1, Handler::Error},
            {2, Handler::Success},
            {0, Handler::GroupSuccess}
        };

        const Group root1 = createRoot(WorkflowPolicy::StopOnError);
        QTest::newRow("StopRootWithStopOnError")
            << TestData{storage, root1, errorErrorLog, 2, DoneWith::Error, 1};

        const Group root2 = createRoot(WorkflowPolicy::ContinueOnError);
        QTest::newRow("StopRootWithContinueOnError")
            << TestData{storage, root2, errorDoneLog, 2, DoneWith::Error, 2};

        const Group root3 = createRoot(WorkflowPolicy::StopOnSuccess);
        QTest::newRow("StopRootWithStopOnSuccess")
            << TestData{storage, root3, doneLog, 2, DoneWith::Success, 2};

        const Group root4 = createRoot(WorkflowPolicy::ContinueOnSuccess);
        QTest::newRow("StopRootWithContinueOnSuccess")
            << TestData{storage, root4, doneLog, 2, DoneWith::Success, 2};

        const Group root5 = createRoot(WorkflowPolicy::StopOnSuccessOrError);
        QTest::newRow("StopRootWithStopOnSuccessOrError")
            << TestData{storage, root5, errorErrorLog, 2, DoneWith::Error, 1};

        const Group root6 = createRoot(WorkflowPolicy::FinishAllAndSuccess);
        QTest::newRow("StopRootWithFinishAllAndSuccess")
            << TestData{storage, root6, doneLog, 2, DoneWith::Success, 2};

        const Group root7 = createRoot(WorkflowPolicy::FinishAllAndError);
        QTest::newRow("StopRootWithFinishAllAndError")
            << TestData{storage, root7, errorDoneLog, 2, DoneWith::Error, 2};
    }

    {
        // These tests check whether the proper root's group end handler is called
        // when the group is stopped. Test it with different workflow policies.
        // The root starts in parallel: one very short successful task, one short failing task
        // and one long task.
        const auto createRoot = [storage, createSuccessTask, createFailingTask, groupDone](
                                    WorkflowPolicy policy) {
            return Group {
                storage,
                parallel,
                workflowPolicy(policy),
                createSuccessTask(1),
                createFailingTask(2, 1ms),
                createSuccessTask(3, 1ms),
                groupDone(0)
            };
        };

        const Log errorErrorLog = {
            {1, Handler::Setup},
            {2, Handler::Setup},
            {3, Handler::Setup},
            {1, Handler::Success},
            {2, Handler::Error},
            {3, Handler::Canceled},
            {0, Handler::GroupError}
        };

        const Log errorDoneLog = {
            {1, Handler::Setup},
            {2, Handler::Setup},
            {3, Handler::Setup},
            {1, Handler::Success},
            {2, Handler::Error},
            {3, Handler::Success},
            {0, Handler::GroupError}
        };

        const Log doneErrorLog = {
            {1, Handler::Setup},
            {2, Handler::Setup},
            {3, Handler::Setup},
            {1, Handler::Success},
            {2, Handler::Canceled},
            {3, Handler::Canceled},
            {0, Handler::GroupSuccess}
        };

        const Log doneDoneLog = {
            {1, Handler::Setup},
            {2, Handler::Setup},
            {3, Handler::Setup},
            {1, Handler::Success},
            {2, Handler::Error},
            {3, Handler::Success},
            {0, Handler::GroupSuccess}
        };

        const Group root1 = createRoot(WorkflowPolicy::StopOnError);
        QTest::newRow("StopRootAfterDoneWithStopOnError")
            << TestData{storage, root1, errorErrorLog, 3, DoneWith::Error, 2};

        const Group root2 = createRoot(WorkflowPolicy::ContinueOnError);
        QTest::newRow("StopRootAfterDoneWithContinueOnError")
            << TestData{storage, root2, errorDoneLog, 3, DoneWith::Error, 3};

        const Group root3 = createRoot(WorkflowPolicy::StopOnSuccess);
        QTest::newRow("StopRootAfterDoneWithStopOnSuccess")
            << TestData{storage, root3, doneErrorLog, 3, DoneWith::Success, 1};

        const Group root4 = createRoot(WorkflowPolicy::ContinueOnSuccess);
        QTest::newRow("StopRootAfterDoneWithContinueOnSuccess")
            << TestData{storage, root4, doneDoneLog, 3, DoneWith::Success, 3};

        const Group root5 = createRoot(WorkflowPolicy::StopOnSuccessOrError);
        QTest::newRow("StopRootAfterDoneWithStopOnSuccessOrError")
            << TestData{storage, root5, doneErrorLog, 3, DoneWith::Success, 1};

        const Group root6 = createRoot(WorkflowPolicy::FinishAllAndSuccess);
        QTest::newRow("StopRootAfterDoneWithFinishAllAndSuccess")
            << TestData{storage, root6, doneDoneLog, 3, DoneWith::Success, 3};

        const Group root7 = createRoot(WorkflowPolicy::FinishAllAndError);
        QTest::newRow("StopRootAfterDoneWithFinishAllAndError")
            << TestData{storage, root7, errorDoneLog, 3, DoneWith::Error, 3};
    }

    {
        // These tests check whether the proper subgroup's end handler is called
        // when the group is stopped. Test it with different workflow policies.
        // The subgroup starts one long task.
        const auto createRoot = [storage, createSuccessTask, createFailingTask, groupDone](
                                    WorkflowPolicy policy) {
            return Group {
                storage,
                parallel,
                Group {
                    workflowPolicy(policy),
                    createSuccessTask(1, 1000ms),
                    groupDone(1)
                },
                createFailingTask(2, 1ms),
                groupDone(2)
            };
        };

        const Log log = {
            {1, Handler::Setup},
            {2, Handler::Setup},
            {2, Handler::Error},
            {1, Handler::Canceled},
            {1, Handler::GroupCanceled},
            {2, Handler::GroupError}
        };

        const Group root1 = createRoot(WorkflowPolicy::StopOnError);
        QTest::newRow("StopGroupWithStopOnError")
            << TestData{storage, root1, log, 2, DoneWith::Error, 1};

        const Group root2 = createRoot(WorkflowPolicy::ContinueOnError);
        QTest::newRow("StopGroupWithContinueOnError")
            << TestData{storage, root2, log, 2, DoneWith::Error, 1};

        const Group root3 = createRoot(WorkflowPolicy::StopOnSuccess);
        QTest::newRow("StopGroupWithStopOnSuccess")
            << TestData{storage, root3, log, 2, DoneWith::Error, 1};

        const Group root4 = createRoot(WorkflowPolicy::ContinueOnSuccess);
        QTest::newRow("StopGroupWithContinueOnSuccess")
            << TestData{storage, root4, log, 2, DoneWith::Error, 1};

        const Group root5 = createRoot(WorkflowPolicy::StopOnSuccessOrError);
        QTest::newRow("StopGroupWithStopOnSuccessOrError")
            << TestData{storage, root5, log, 2, DoneWith::Error, 1};

        // TODO: Behavioral change! Fix Docs!
        // Cancellation always invokes error handler (i.e. DoneWith is Canceled)
        const Group root6 = createRoot(WorkflowPolicy::FinishAllAndSuccess);
        QTest::newRow("StopGroupWithFinishAllAndSuccess")
            << TestData{storage, root6, log, 2, DoneWith::Error, 1};

        const Group root7 = createRoot(WorkflowPolicy::FinishAllAndError);
        QTest::newRow("StopGroupWithFinishAllAndError")
            << TestData{storage, root7, log, 2, DoneWith::Error, 1};
    }

    {
        // These tests check whether the proper subgroup's end handler is called
        // when the group is stopped. Test it with different workflow policies.
        // The sequential subgroup starts one short successful task followed by one long task.
        const auto createRoot = [storage, createSuccessTask, createFailingTask, groupDone](
                                    WorkflowPolicy policy) {
            return Group {
                storage,
                parallel,
                Group {
                    workflowPolicy(policy),
                    createSuccessTask(1),
                    createSuccessTask(2, 1000ms),
                    groupDone(1)
                },
                createFailingTask(3, 1ms),
                groupDone(2)
            };
        };

        const Log errorLog = {
            {1, Handler::Setup},
            {3, Handler::Setup},
            {1, Handler::Success},
            {2, Handler::Setup},
            {3, Handler::Error},
            {2, Handler::Canceled},
            {1, Handler::GroupCanceled},
            {2, Handler::GroupError}
        };

        const Log doneLog = {
            {1, Handler::Setup},
            {3, Handler::Setup},
            {1, Handler::Success},
            {1, Handler::GroupSuccess},
            {3, Handler::Error},
            {2, Handler::GroupError}
        };

        const Group root1 = createRoot(WorkflowPolicy::StopOnError);
        QTest::newRow("StopGroupAfterDoneWithStopOnError")
            << TestData{storage, root1, errorLog, 3, DoneWith::Error, 2};

        const Group root2 = createRoot(WorkflowPolicy::ContinueOnError);
        QTest::newRow("StopGroupAfterDoneWithContinueOnError")
            << TestData{storage, root2, errorLog, 3, DoneWith::Error, 2};

        const Group root3 = createRoot(WorkflowPolicy::StopOnSuccess);
        QTest::newRow("StopGroupAfterDoneWithStopOnSuccess")
            << TestData{storage, root3, doneLog, 3, DoneWith::Error, 2};

        // TODO: Behavioral change!
        const Group root4 = createRoot(WorkflowPolicy::ContinueOnSuccess);
        QTest::newRow("StopGroupAfterDoneWithContinueOnSuccess")
            << TestData{storage, root4, errorLog, 3, DoneWith::Error, 2};

        const Group root5 = createRoot(WorkflowPolicy::StopOnSuccessOrError);
        QTest::newRow("StopGroupAfterDoneWithStopOnSuccessOrError")
            << TestData{storage, root5, doneLog, 3, DoneWith::Error, 2};

        // TODO: Behavioral change!
        const Group root6 = createRoot(WorkflowPolicy::FinishAllAndSuccess);
        QTest::newRow("StopGroupAfterDoneWithFinishAllAndSuccess")
            << TestData{storage, root6, errorLog, 3, DoneWith::Error, 2};

        const Group root7 = createRoot(WorkflowPolicy::FinishAllAndError);
        QTest::newRow("StopGroupAfterDoneWithFinishAllAndError")
            << TestData{storage, root7, errorLog, 3, DoneWith::Error, 2};
    }

    {
        // These tests check whether the proper subgroup's end handler is called
        // when the group is stopped. Test it with different workflow policies.
        // The sequential subgroup starts one short failing task followed by one long task.
        const auto createRoot = [storage, createSuccessTask, createFailingTask, groupDone](
                                    WorkflowPolicy policy) {
            return Group {
                storage,
                parallel,
                Group {
                    workflowPolicy(policy),
                    createFailingTask(1),
                    createSuccessTask(2, 1000ms),
                    groupDone(1)
                },
                createFailingTask(3, 1ms),
                groupDone(2)
            };
        };

        const Log shortLog = {
            {1, Handler::Setup},
            {3, Handler::Setup},
            {1, Handler::Error},
            {1, Handler::GroupError},
            {3, Handler::Canceled},
            {2, Handler::GroupError}
        };

        const Log longLog = {
            {1, Handler::Setup},
            {3, Handler::Setup},
            {1, Handler::Error},
            {2, Handler::Setup},
            {3, Handler::Error},
            {2, Handler::Canceled},
            {1, Handler::GroupCanceled},
            {2, Handler::GroupError}
        };

        const Group root1 = createRoot(WorkflowPolicy::StopOnError);
        QTest::newRow("StopGroupAfterErrorWithStopOnError")
            << TestData{storage, root1, shortLog, 3, DoneWith::Error, 1};

        const Group root2 = createRoot(WorkflowPolicy::ContinueOnError);
        QTest::newRow("StopGroupAfterErrorWithContinueOnError")
            << TestData{storage, root2, longLog, 3, DoneWith::Error, 2};

        const Group root3 = createRoot(WorkflowPolicy::StopOnSuccess);
        QTest::newRow("StopGroupAfterErrorWithStopOnSuccess")
            << TestData{storage, root3, longLog, 3, DoneWith::Error, 2};

        const Group root4 = createRoot(WorkflowPolicy::ContinueOnSuccess);
        QTest::newRow("StopGroupAfterErrorWithContinueOnSuccess")
            << TestData{storage, root4, longLog, 3, DoneWith::Error, 2};

        const Group root5 = createRoot(WorkflowPolicy::StopOnSuccessOrError);
        QTest::newRow("StopGroupAfterErrorWithStopOnSuccessOrError")
            << TestData{storage, root5, shortLog, 3, DoneWith::Error, 1};

        // TODO: Behavioral change!
        const Group root6 = createRoot(WorkflowPolicy::FinishAllAndSuccess);
        QTest::newRow("StopGroupAfterErrorWithFinishAllAndSuccess")
            << TestData{storage, root6, longLog, 3, DoneWith::Error, 2};

        const Group root7 = createRoot(WorkflowPolicy::FinishAllAndError);
        QTest::newRow("StopGroupAfterErrorWithFinishAllAndError")
            << TestData{storage, root7, longLog, 3, DoneWith::Error, 2};
    }

    {
        const auto createRoot = [storage, createSuccessTask, createFailingTask, groupDone](
                                    WorkflowPolicy policy) {
            return Group {
                storage,
                workflowPolicy(policy),
                createSuccessTask(1),
                createFailingTask(2),
                createSuccessTask(3),
                groupDone(0)
            };
        };

        const Group root1 = createRoot(WorkflowPolicy::StopOnError);
        const Log log1 {
            {1, Handler::Setup},
            {1, Handler::Success},
            {2, Handler::Setup},
            {2, Handler::Error},
            {0, Handler::GroupError}
        };
        QTest::newRow("StopOnError") << TestData{storage, root1, log1, 3, DoneWith::Error, 2};

        const Group root2 = createRoot(WorkflowPolicy::ContinueOnError);
        const Log errorLog {
            {1, Handler::Setup},
            {1, Handler::Success},
            {2, Handler::Setup},
            {2, Handler::Error},
            {3, Handler::Setup},
            {3, Handler::Success},
            {0, Handler::GroupError}
        };
        QTest::newRow("ContinueOnError") << TestData{storage, root2, errorLog, 3, DoneWith::Error, 3};

        const Group root3 = createRoot(WorkflowPolicy::StopOnSuccess);
        const Log log3 {
            {1, Handler::Setup},
            {1, Handler::Success},
            {0, Handler::GroupSuccess}
        };
        QTest::newRow("StopOnSuccess") << TestData{storage, root3, log3, 3, DoneWith::Success, 1};

        const Group root4 = createRoot(WorkflowPolicy::ContinueOnSuccess);
        const Log doneLog {
            {1, Handler::Setup},
            {1, Handler::Success},
            {2, Handler::Setup},
            {2, Handler::Error},
            {3, Handler::Setup},
            {3, Handler::Success},
            {0, Handler::GroupSuccess}
        };
        QTest::newRow("ContinueOnSuccess") << TestData{storage, root4, doneLog, 3, DoneWith::Success, 3};

        const Group root5 = createRoot(WorkflowPolicy::StopOnSuccessOrError);
        const Log log5 {
            {1, Handler::Setup},
            {1, Handler::Success},
            {0, Handler::GroupSuccess}
        };
        QTest::newRow("StopOnSuccessOrError") << TestData{storage, root5, log5, 3, DoneWith::Success, 1};

        const Group root6 = createRoot(WorkflowPolicy::FinishAllAndSuccess);
        QTest::newRow("FinishAllAndSuccess") << TestData{storage, root6, doneLog, 3, DoneWith::Success, 3};

        const Group root7 = createRoot(WorkflowPolicy::FinishAllAndError);
        QTest::newRow("FinishAllAndError") << TestData{storage, root7, errorLog, 3, DoneWith::Error, 3};
    }

    {
        const auto createRoot = [storage, createTask, groupDone](DoneResult firstResult,
                                                                 DoneResult secondResult) {
            return Group {
                parallel,
                stopOnSuccessOrError,
                storage,
                createTask(1, firstResult, 1000ms),
                createTask(2, secondResult, 1ms),
                groupDone(0)
            };
        };

        const Group root1 = createRoot(DoneResult::Success, DoneResult::Success);
        const Group root2 = createRoot(DoneResult::Success, DoneResult::Error);
        const Group root3 = createRoot(DoneResult::Error, DoneResult::Success);
        const Group root4 = createRoot(DoneResult::Error, DoneResult::Error);

        const Log success {
            {1, Handler::Setup},
            {2, Handler::Setup},
            {2, Handler::Success},
            {1, Handler::Canceled},
            {0, Handler::GroupSuccess}
        };
        const Log failure {
            {1, Handler::Setup},
            {2, Handler::Setup},
            {2, Handler::Error},
            {1, Handler::Canceled},
            {0, Handler::GroupError}
        };

        QTest::newRow("StopOnSuccessOrError1")
            << TestData{storage, root1, success, 2, DoneWith::Success, 1};
        QTest::newRow("StopOnSuccessOrError2")
            << TestData{storage, root2, failure, 2, DoneWith::Error, 1};
        QTest::newRow("StopOnSuccessOrError3")
            << TestData{storage, root3, success, 2, DoneWith::Success, 1};
        QTest::newRow("StopOnSuccessOrError4")
            << TestData{storage, root4, failure, 2, DoneWith::Error, 1};
    }

    {
        // This test checks whether group setup handler's result is properly dispatched.
        const auto createRoot = [storage, createSuccessTask, groupDone, groupSetupWithTweak](
                                    SetupResult desiredResult) {
            return Group {
                storage,
                Group {
                    groupSetupWithTweak(1, desiredResult),
                    createSuccessTask(1)
                },
                groupDone(0)
            };
        };

        const Group root1 = createRoot(SetupResult::StopWithSuccess);
        const Log log1 {
            {1, Handler::GroupSetup},
            {1, Handler::TweakSetupToSuccess},
            {0, Handler::GroupSuccess}
        };
        QTest::newRow("GroupSetupTweakToSuccess")
            << TestData{storage, root1, log1, 1, DoneWith::Success, 0};

        const Group root2 = createRoot(SetupResult::StopWithError);
        const Log log2 {
            {1, Handler::GroupSetup},
            {1, Handler::TweakSetupToError},
            {0, Handler::GroupError}
        };
        QTest::newRow("GroupSetupTweakToError")
            << TestData{storage, root2, log2, 1, DoneWith::Error, 0};

        const Group root3 = createRoot(SetupResult::Continue);
        const Log log3 {
            {1, Handler::GroupSetup},
            {1, Handler::TweakSetupToContinue},
            {1, Handler::Setup},
            {1, Handler::Success},
            {0, Handler::GroupSuccess}
        };
        QTest::newRow("GroupSetupTweakToContinue")
            << TestData{storage, root3, log3, 1, DoneWith::Success, 1};
    }

    {
        // This test checks whether group done handler's result is properly dispatched.
        const auto createRoot = [storage, createTask, groupDone, groupDoneWithTweak](
                                 DoneResult firstResult, DoneResult secondResult) {
            return Group {
                storage,
                Group {
                    createTask(1, firstResult),
                    groupDoneWithTweak(1, secondResult)
                },
                groupDone(0)
            };
        };

        const Group root1 = createRoot(DoneResult::Success, DoneResult::Success);
        const Log log1 {
            {1, Handler::Setup},
            {1, Handler::Success},
            {1, Handler::GroupSuccess},
            {1, Handler::TweakDoneToSuccess},
            {0, Handler::GroupSuccess}
        };
        QTest::newRow("GroupDoneWithSuccessTweakToSuccess")
            << TestData{storage, root1, log1, 1, DoneWith::Success, 1};

        const Group root2 = createRoot(DoneResult::Success, DoneResult::Error);
        const Log log2 {
            {1, Handler::Setup},
            {1, Handler::Success},
            {1, Handler::GroupSuccess},
            {1, Handler::TweakDoneToError},
            {0, Handler::GroupError}
        };
        QTest::newRow("GroupDoneWithSuccessTweakToError")
            << TestData{storage, root2, log2, 1, DoneWith::Error, 1};

        const Group root3 = createRoot(DoneResult::Error, DoneResult::Success);
        const Log log3 {
            {1, Handler::Setup},
            {1, Handler::Error},
            {1, Handler::GroupError},
            {1, Handler::TweakDoneToSuccess},
            {0, Handler::GroupSuccess}
        };
        QTest::newRow("GroupDoneWithErrorTweakToSuccess")
            << TestData{storage, root3, log3, 1, DoneWith::Success, 1};

        const Group root4 = createRoot(DoneResult::Error, DoneResult::Error);
        const Log log4 {
            {1, Handler::Setup},
            {1, Handler::Error},
            {1, Handler::GroupError},
            {1, Handler::TweakDoneToError},
            {0, Handler::GroupError}
        };
        QTest::newRow("GroupDoneWithErrorTweakToError")
            << TestData{storage, root4, log4, 1, DoneWith::Error, 1};
    }

    {
        // This test checks whether task setup handler's result is properly dispatched.
        const auto createRoot = [storage, createSuccessTask, groupDone, createTaskWithSetupTweak](
                                    SetupResult desiredResult) {
            return Group {
                storage,
                Group {
                    createTaskWithSetupTweak(1, desiredResult),
                    createSuccessTask(2)
                },
                groupDone(0)
            };
        };

        const Group root1 = createRoot(SetupResult::StopWithSuccess);
        const Log log1 {
            {1, Handler::Setup},
            {1, Handler::TweakSetupToSuccess},
            {2, Handler::Setup},
            {2, Handler::Success},
            {0, Handler::GroupSuccess}
        };
        QTest::newRow("TaskSetupTweakToSuccess")
            << TestData{storage, root1, log1, 2, DoneWith::Success, 1};

        const Group root2 = createRoot(SetupResult::StopWithError);
        const Log log2 {
            {1, Handler::Setup},
            {1, Handler::TweakSetupToError},
            {0, Handler::GroupError}
        };
        QTest::newRow("TaskSetupTweakToError")
            << TestData{storage, root2, log2, 2, DoneWith::Error, 0};

        const Group root3 = createRoot(SetupResult::Continue);
        const Log log3 {
            {1, Handler::Setup},
            {1, Handler::TweakSetupToContinue},
            {1, Handler::Success},
            {2, Handler::Setup},
            {2, Handler::Success},
            {0, Handler::GroupSuccess}
        };
        QTest::newRow("TaskSetupTweakToContinue")
            << TestData{storage, root3, log3, 2, DoneWith::Success, 2};
    }

    {
        const Group root {
            ParallelLimit(2),
            storage,
            Group {
                groupSetup(1),
                createSuccessTask(1)
            },
            Group {
                groupSetup(2),
                createSuccessTask(2)
            },
            Group {
                groupSetup(3),
                createSuccessTask(3)
            },
            Group {
                groupSetup(4),
                createSuccessTask(4)
            }
        };
        const Log log {
            {1, Handler::GroupSetup},
            {1, Handler::Setup},
            {2, Handler::GroupSetup},
            {2, Handler::Setup},
            {1, Handler::Success},
            {3, Handler::GroupSetup},
            {3, Handler::Setup},
            {2, Handler::Success},
            {4, Handler::GroupSetup},
            {4, Handler::Setup},
            {3, Handler::Success},
            {4, Handler::Success}
        };
        QTest::newRow("NestedParallel") << TestData{storage, root, log, 4, DoneWith::Success, 4};
    }

    {
        const Group root {
            ParallelLimit(2),
            storage,
            Group {
                groupSetup(1),
                createSuccessTask(1)
            },
            Group {
                groupSetup(2),
                createSuccessTask(2)
            },
            Group {
                groupSetup(3),
                createTaskWithSetupTweak(3, SetupResult::StopWithSuccess)
            },
            Group {
                groupSetup(4),
                createSuccessTask(4)
            },
            Group {
                groupSetup(5),
                createSuccessTask(5)
            }
        };
        const Log log {
            {1, Handler::GroupSetup},
            {1, Handler::Setup},
            {2, Handler::GroupSetup},
            {2, Handler::Setup},
            {1, Handler::Success},
            {3, Handler::GroupSetup},
            {3, Handler::Setup},
            {3, Handler::TweakSetupToSuccess},
            {4, Handler::GroupSetup},
            {4, Handler::Setup},
            {2, Handler::Success},
            {5, Handler::GroupSetup},
            {5, Handler::Setup},
            {4, Handler::Success},
            {5, Handler::Success}
        };
        QTest::newRow("NestedParallelDone") << TestData{storage, root, log, 5, DoneWith::Success, 4};
    }

    {
        const Group root1 {
            ParallelLimit(2),
            storage,
            Group {
                groupSetup(1),
                createSuccessTask(1)
            },
            Group {
                groupSetup(2),
                createSuccessTask(2)
            },
            Group {
                groupSetup(3),
                createTaskWithSetupTweak(3, SetupResult::StopWithError)
            },
            Group {
                groupSetup(4),
                createSuccessTask(4)
            },
            Group {
                groupSetup(5),
                createSuccessTask(5)
            }
        };
        const Log log1 {
            {1, Handler::GroupSetup},
            {1, Handler::Setup},
            {2, Handler::GroupSetup},
            {2, Handler::Setup},
            {1, Handler::Success},
            {3, Handler::GroupSetup},
            {3, Handler::Setup},
            {3, Handler::TweakSetupToError},
            {2, Handler::Canceled}
        };

        // Inside this test the task 2 should finish first, then synchonously:
        // - task 3 should exit setup with error
        // - task 1 should be stopped as a consequence of the error inside the group
        // - tasks 4 and 5 should be skipped
        const Group root2 {
            ParallelLimit(2),
            storage,
            Group {
                groupSetup(1),
                createSuccessTask(1, 1s)
            },
            Group {
                groupSetup(2),
                createSuccessTask(2)
            },
            Group {
                groupSetup(3),
                createTaskWithSetupTweak(3, SetupResult::StopWithError)
            },
            Group {
                groupSetup(4),
                createSuccessTask(4)
            },
            Group {
                groupSetup(5),
                createSuccessTask(5)
            }
        };
        const Log log2 {
            {1, Handler::GroupSetup},
            {1, Handler::Setup},
            {2, Handler::GroupSetup},
            {2, Handler::Setup},
            {2, Handler::Success},
            {3, Handler::GroupSetup},
            {3, Handler::Setup},
            {3, Handler::TweakSetupToError},
            {1, Handler::Canceled}
        };

        // This test ensures that the task 1 doesn't invoke its done handler,
        // being ready while sleeping in the task's 2 done handler.
        // Inside this test the task 2 should finish first, then synchonously:
        // - task 3 should exit setup with error
        // - task 1 should be stopped as a consequence of error inside the group
        // - task 4 should be skipped
        // - the first child group of root should finish with error
        // - task 5 should be started (because of root's continueOnError policy)
        const Group root3 {
            continueOnError,
            storage,
            Group {
                ParallelLimit(2),
                Group {
                    groupSetup(1),
                    createSuccessTask(1, 1s)
                },
                Group {
                    groupSetup(2),
                    createSuccessTask(2)
                },
                Group {
                    groupSetup(3),
                    createTaskWithSetupTweak(3, SetupResult::StopWithError)
                },
                Group {
                    groupSetup(4),
                    createSuccessTask(4)
                }
            },
            Group {
                groupSetup(5),
                createSuccessTask(5)
            }
        };
        const Log log3 {
            {1, Handler::GroupSetup},
            {1, Handler::Setup},
            {2, Handler::GroupSetup},
            {2, Handler::Setup},
            {2, Handler::Success},
            {3, Handler::GroupSetup},
            {3, Handler::Setup},
            {3, Handler::TweakSetupToError},
            {1, Handler::Canceled},
            {5, Handler::GroupSetup},
            {5, Handler::Setup},
            {5, Handler::Success}
        };
        QTest::newRow("NestedParallelError1")
            << TestData{storage, root1, log1, 5, DoneWith::Error, 1};
        QTest::newRow("NestedParallelError2")
            << TestData{storage, root2, log2, 5, DoneWith::Error, 1};
        QTest::newRow("NestedParallelError3")
            << TestData{storage, root3, log3, 5, DoneWith::Error, 2};
    }

    {
        const Group root {
            ParallelLimit(2),
            storage,
            Group {
                groupSetup(1),
                Group {
                    parallel,
                    createSuccessTask(1)
                }
            },
            Group {
                groupSetup(2),
                Group {
                    parallel,
                    createSuccessTask(2)
                }
            },
            Group {
                groupSetup(3),
                Group {
                    parallel,
                    createSuccessTask(3)
                }
            },
            Group {
                groupSetup(4),
                Group {
                    parallel,
                    createSuccessTask(4)
                }
            }
        };
        const Log log {
            {1, Handler::GroupSetup},
            {1, Handler::Setup},
            {2, Handler::GroupSetup},
            {2, Handler::Setup},
            {1, Handler::Success},
            {3, Handler::GroupSetup},
            {3, Handler::Setup},
            {2, Handler::Success},
            {4, Handler::GroupSetup},
            {4, Handler::Setup},
            {3, Handler::Success},
            {4, Handler::Success}
        };
        QTest::newRow("DeeplyNestedParallel") << TestData{storage, root, log, 4, DoneWith::Success, 4};
    }

    {
        const Group root {
            ParallelLimit(2),
            storage,
            Group {
                groupSetup(1),
                Group { createSuccessTask(1) }
            },
            Group {
                groupSetup(2),
                Group { createSuccessTask(2) }
            },
            Group {
                groupSetup(3),
                Group { createTaskWithSetupTweak(3, SetupResult::StopWithSuccess) }
            },
            Group {
                groupSetup(4),
                Group { createSuccessTask(4) }
            },
            Group {
                groupSetup(5),
                Group { createSuccessTask(5) }
            }
        };
        const Log log {
            {1, Handler::GroupSetup},
            {1, Handler::Setup},
            {2, Handler::GroupSetup},
            {2, Handler::Setup},
            {1, Handler::Success},
            {3, Handler::GroupSetup},
            {3, Handler::Setup},
            {3, Handler::TweakSetupToSuccess},
            {4, Handler::GroupSetup},
            {4, Handler::Setup},
            {2, Handler::Success},
            {5, Handler::GroupSetup},
            {5, Handler::Setup},
            {4, Handler::Success},
            {5, Handler::Success}
        };
        QTest::newRow("DeeplyNestedParallelSuccess")
            << TestData{storage, root, log, 5, DoneWith::Success, 4};
    }

    {
        const Group root {
            ParallelLimit(2),
            storage,
            Group {
                groupSetup(1),
                Group { createSuccessTask(1) }
            },
            Group {
                groupSetup(2),
                Group { createSuccessTask(2) }
            },
            Group {
                groupSetup(3),
                Group { createTaskWithSetupTweak(3, SetupResult::StopWithError) }
            },
            Group {
                groupSetup(4),
                Group { createSuccessTask(4) }
            },
            Group {
                groupSetup(5),
                Group { createSuccessTask(5) }
            }
        };
        const Log log {
            {1, Handler::GroupSetup},
            {1, Handler::Setup},
            {2, Handler::GroupSetup},
            {2, Handler::Setup},
            {1, Handler::Success},
            {3, Handler::GroupSetup},
            {3, Handler::Setup},
            {3, Handler::TweakSetupToError},
            {2, Handler::Canceled}
        };
        QTest::newRow("DeeplyNestedParallelError")
            << TestData{storage, root, log, 5, DoneWith::Error, 1};
    }

    {
        const Group root {
            storage,
            createSync(1),
            createSync(2),
            createSync(3),
            createSync(4),
            createSync(5)
        };
        const Log log {
            {1, Handler::Sync},
            {2, Handler::Sync},
            {3, Handler::Sync},
            {4, Handler::Sync},
            {5, Handler::Sync}
        };
        QTest::newRow("SyncSequential") << TestData{storage, root, log, 0, DoneWith::Success, 0};
    }

    {
        const Group root {
            storage,
            createSyncWithTweak(1, DoneResult::Success),
            createSyncWithTweak(2, DoneResult::Success),
            createSyncWithTweak(3, DoneResult::Success),
            createSyncWithTweak(4, DoneResult::Success),
            createSyncWithTweak(5, DoneResult::Success)
        };
        const Log log {
            {1, Handler::Sync},
            {1, Handler::TweakDoneToSuccess},
            {2, Handler::Sync},
            {2, Handler::TweakDoneToSuccess},
            {3, Handler::Sync},
            {3, Handler::TweakDoneToSuccess},
            {4, Handler::Sync},
            {4, Handler::TweakDoneToSuccess},
            {5, Handler::Sync},
            {5, Handler::TweakDoneToSuccess}
        };
        QTest::newRow("SyncWithReturn") << TestData{storage, root, log, 0, DoneWith::Success, 0};
    }

    {
        const Group root {
            storage,
            parallel,
            createSync(1),
            createSync(2),
            createSync(3),
            createSync(4),
            createSync(5)
        };
        const Log log {
            {1, Handler::Sync},
            {2, Handler::Sync},
            {3, Handler::Sync},
            {4, Handler::Sync},
            {5, Handler::Sync}
        };
        QTest::newRow("SyncParallel") << TestData{storage, root, log, 0, DoneWith::Success, 0};
    }

    {
        const Group root {
            storage,
            parallel,
            createSync(1),
            createSync(2),
            createSyncWithTweak(3, DoneResult::Error),
            createSync(4),
            createSync(5)
        };
        const Log log {
            {1, Handler::Sync},
            {2, Handler::Sync},
            {3, Handler::Sync},
            {3, Handler::TweakDoneToError}
        };
        QTest::newRow("SyncError") << TestData{storage, root, log, 0, DoneWith::Error, 0};
    }

    {
        const Group root {
            storage,
            createSync(1),
            createSuccessTask(2),
            createSync(3),
            createSuccessTask(4),
            createSync(5),
            groupDone(0)
        };
        const Log log {
            {1, Handler::Sync},
            {2, Handler::Setup},
            {2, Handler::Success},
            {3, Handler::Sync},
            {4, Handler::Setup},
            {4, Handler::Success},
            {5, Handler::Sync},
            {0, Handler::GroupSuccess}
        };
        QTest::newRow("SyncAndAsync") << TestData{storage, root, log, 2, DoneWith::Success, 2};
    }

    {
        const Group root {
            storage,
            createSync(1),
            createSuccessTask(2),
            createSyncWithTweak(3, DoneResult::Error),
            createSuccessTask(4),
            createSync(5),
            groupDone(0)
        };
        const Log log {
            {1, Handler::Sync},
            {2, Handler::Setup},
            {2, Handler::Success},
            {3, Handler::Sync},
            {3, Handler::TweakDoneToError},
            {0, Handler::GroupError}
        };
        QTest::newRow("SyncAndAsyncError") << TestData{storage, root, log, 2, DoneWith::Error, 1};
    }

    {
        QStoredBarrier barrier;

        // Test that barrier advance, triggered from inside the task described by
        // createBarrierAdvance, placed BEFORE the group containing the waitFor() element
        // in the tree order, works OK in SEQUENTIAL mode.
        const Group root1 {
            storage,
            barrier,
            sequential,
            createBarrierAdvance(storage, barrier, 1),
            Group {
                groupSetup(2),
                barrierAwaiterTask(barrier),
                createSuccessTask(2),
                createSuccessTask(3)
            }
        };
        const Log log1 {
            {1, Handler::Setup},
            {1, Handler::BarrierAdvance},
            {2, Handler::GroupSetup},
            {2, Handler::Setup},
            {2, Handler::Success},
            {3, Handler::Setup},
            {3, Handler::Success}
        };

        // Test that barrier advance, triggered from inside the task described by
        // createBarrierAdvance, placed BEFORE the group containing the waitFor() element
        // in the tree order, works OK in PARALLEL mode.
        const Group root2 {
            storage,
            barrier,
            parallel,
            createBarrierAdvance(storage, barrier, 1),
            Group {
                groupSetup(2),
                barrierAwaiterTask(barrier),
                createSuccessTask(2),
                createSuccessTask(3)
            }
        };
        const Log log2 {
            {1, Handler::Setup},
            {2, Handler::GroupSetup},
            {1, Handler::BarrierAdvance},
            {2, Handler::Setup},
            {2, Handler::Success},
            {3, Handler::Setup},
            {3, Handler::Success}
        };

        // Test that barrier advance, triggered from inside the task described by
        // createBarrierAdvance, placed AFTER the group containing the waitFor() element
        // in the tree order, works OK in PARALLEL mode.
        //
        // Notice: This won't work in SEQUENTIAL mode, since the advancing barrier, placed after the
        // group containing the WaitFor element, has no chance to be started in SEQUENTIAL mode,
        // as in SEQUENTIAL mode the next task may only be started after the previous one finished.
        // In this case, the previous task (Group element) awaits for the barrier's advance to
        // come from the not yet started next task, causing a deadlock.
        // The minimal requirement for this scenario to succeed is to set ParallelLimit(2) or more.
        const Group root3 {
            storage,
            barrier,
            parallel,
            Group {
                groupSetup(2),
                barrierAwaiterTask(barrier),
                createSuccessTask(2),
                createSuccessTask(3)
            },
            createBarrierAdvance(storage, barrier, 1)
        };
        const Log log3 {
            {2, Handler::GroupSetup},
            {1, Handler::Setup},
            {1, Handler::BarrierAdvance},
            {2, Handler::Setup},
            {2, Handler::Success},
            {3, Handler::Setup},
            {3, Handler::Success}
        };

        // Test that barrier advance, triggered from inside the task described by
        // createBarrierAdvance, placed BEFORE the groups containing the waitFor() element
        // in the tree order, wakes both waitFor tasks.
        const Group root4 {
            storage,
            barrier,
            parallel,
            createBarrierAdvance(storage, barrier, 1),
            Group {
                groupSetup(2),
                barrierAwaiterTask(barrier),
                createSuccessTask(4)
            },
            Group {
                groupSetup(3),
                barrierAwaiterTask(barrier),
                createSuccessTask(5)
            }
        };
        const Log log4 {
            {1, Handler::Setup},
            {2, Handler::GroupSetup},
            {3, Handler::GroupSetup},
            {1, Handler::BarrierAdvance},
            {4, Handler::Setup},
            {5, Handler::Setup},
            {4, Handler::Success},
            {5, Handler::Success}
        };

        // Test two separate single barriers.

        QStoredBarrier barrier2;

        const Group root5 {
            storage,
            barrier,
            barrier2,
            parallel,
            createBarrierAdvance(storage, barrier, 1),
            createBarrierAdvance(storage, barrier2, 2),
            Group {
                Group {
                    parallel,
                    groupSetup(1),
                    barrierAwaiterTask(barrier),
                    barrierAwaiterTask(barrier2)
                },
                createSuccessTask(3)
            },
        };
        const Log log5 {
            {1, Handler::Setup},
            {2, Handler::Setup},
            {1, Handler::GroupSetup},
            {1, Handler::BarrierAdvance},
            {2, Handler::BarrierAdvance},
            {3, Handler::Setup},
            {3, Handler::Success}
        };

        // Notice the different log order for each scenario.
        QTest::newRow("BarrierSequential")
            << TestData{storage, root1, log1, 4, DoneWith::Success, 3};
        QTest::newRow("BarrierParallelAdvanceFirst")
            << TestData{storage, root2, log2, 4, DoneWith::Success, 4};
        QTest::newRow("BarrierParallelWaitForFirst")
            << TestData{storage, root3, log3, 4, DoneWith::Success, 4};
        QTest::newRow("BarrierParallelMultiWaitFor")
            << TestData{storage, root4, log4, 5, DoneWith::Success, 5};
        QTest::newRow("BarrierParallelTwoSingleBarriers")
            << TestData{storage, root5, log5, 5, DoneWith::Success, 5};

        const auto barrierKicker = [storage](int taskId) {
            return [storage, taskId](const QStoredBarrier &barrier) {
                return createBarrierAdvance(storage, barrier, taskId);
            };
        };

        // The same as BarrierParallelAdvanceFirst, but in terms of When () >> Do {...}.
        const Group rootWhenDo {
            storage,
            When (barrierKicker(1)) >> Do {
                groupSetup(2),
                createSuccessTask(2),
                createSuccessTask(3)
            }
        };

        QTest::newRow("BarrierWhenDo")
            << TestData{storage, rootWhenDo, log2, 4, DoneWith::Success, 4};

        const auto onTickSetup = [storage](TickAndDone &tickAndDone) {
            tickAndDone.setInterval(1ms);
            storage->m_log.append({1, Handler::Setup});
        };
        const Group rootWhenSignalDo {
            storage,
            When (TickAndDoneTask(onTickSetup), &TickAndDone::tick) >> Do {
                groupSetup(2),
                QSyncTask([storage] { storage->m_log.append({1, Handler::BarrierAdvance}); }),
                createSuccessTask(2),
                createSuccessTask(3)
            }
        };
        QTest::newRow("BarrierWhenSignalDo")
                << TestData{storage, rootWhenSignalDo, log2, 4, DoneWith::Success, 4};

        const auto onTickSetupWithPolicy = [storage](TickAndDone &tickAndDone) {
            tickAndDone.setInterval(s_endlessTime);
            storage->m_log.append({1, Handler::Setup});
        };
        const Group rootWhenSignalDoWithPolicy {
            storage,
            When (TickAndDoneTask(onTickSetupWithPolicy, setupDone(1)), &TickAndDone::tick, WorkflowPolicy::StopOnSuccessOrError) >> Do {
                groupSetup(2),
                QSyncTask([storage] { storage->m_log.append({1, Handler::BarrierAdvance}); }),
                createSuccessTask(2),
                createSuccessTask(3)
            }
        };
        const Log logWhenSignalDoWithPolicy {
            {1, Handler::Setup},
            {2, Handler::GroupSetup},
            {1, Handler::BarrierAdvance},
            {2, Handler::Setup},
            {2, Handler::Success},
            {3, Handler::Setup},
            {3, Handler::Success},
            {1, Handler::Canceled}
        };
        QTest::newRow("BarrierWhenSignalDoWithPolicy")
                << TestData{storage, rootWhenSignalDoWithPolicy, logWhenSignalDoWithPolicy, 4, DoneWith::Success, 3};
    }

    {
        const QStoredBarrier barrier{2};

        // Test that multi barrier advance, triggered from inside the tasks described by
        // createBarrierAdvance, placed BEFORE the group containing the waitFor() element
        // in the tree order, works OK in SEQUENTIAL mode.
        const Group root1 {
            storage,
            barrier,
            sequential,
            createBarrierAdvance(storage, barrier, 1),
            createBarrierAdvance(storage, barrier, 2),
            Group {
                groupSetup(2),
                barrierAwaiterTask(barrier),
                createSuccessTask(2),
                createSuccessTask(3)
            }
        };
        const Log log1 {
            {1, Handler::Setup},
            {1, Handler::BarrierAdvance},
            {2, Handler::Setup},
            {2, Handler::BarrierAdvance},
            {2, Handler::GroupSetup},
            {2, Handler::Setup},
            {2, Handler::Success},
            {3, Handler::Setup},
            {3, Handler::Success}
        };

        // Test that multi barrier advance, triggered from inside the tasks described by
        // createBarrierAdvance, placed BEFORE the group containing the waitFor() element
        // in the tree order, works OK in PARALLEL mode.
        const Group root2 {
            storage,
            barrier,
            parallel,
            createBarrierAdvance(storage, barrier, 1),
            createBarrierAdvance(storage, barrier, 2),
            Group {
                groupSetup(2),
                barrierAwaiterTask(barrier),
                createSuccessTask(3),
                createSuccessTask(4)
            }
        };
        const Log log2 {
            {1, Handler::Setup},
            {2, Handler::Setup},
            {2, Handler::GroupSetup},
            {1, Handler::BarrierAdvance},
            {2, Handler::BarrierAdvance},
            {3, Handler::Setup},
            {3, Handler::Success},
            {4, Handler::Setup},
            {4, Handler::Success}
        };

        // Test that multi barrier advance, triggered from inside the tasks described by
        // createBarrierAdvance, placed AFTER the group containing the waitFor() element
        // in the tree order, works OK in PARALLEL mode.
        //
        // Notice: This won't work in SEQUENTIAL mode, since the advancing barriers, placed after
        // the group containing the WaitFor element, has no chance to be started in SEQUENTIAL mode,
        // as in SEQUENTIAL mode the next task may only be started after the previous one finished.
        // In this case, the previous task (Group element) awaits for the barrier's advance to
        // come from the not yet started next task, causing a deadlock.
        // The minimal requirement for this scenario to succeed is to set ParallelLimit(2) or more.
        const Group root3 {
            storage,
            barrier,
            parallel,
            Group {
                groupSetup(2),
                barrierAwaiterTask(barrier),
                createSuccessTask(3),
                createSuccessTask(4)
            },
            createBarrierAdvance(storage, barrier, 1),
            createBarrierAdvance(storage, barrier, 2)
        };
        const Log log3 {
            {2, Handler::GroupSetup},
            {1, Handler::Setup},
            {2, Handler::Setup},
            {1, Handler::BarrierAdvance},
            {2, Handler::BarrierAdvance},
            {3, Handler::Setup},
            {3, Handler::Success},
            {4, Handler::Setup},
            {4, Handler::Success}
        };

        // Test that multi barrier advance, triggered from inside the task described by
        // createBarrierAdvance, placed BEFORE the groups containing the waitFor() element
        // in the tree order, wakes both waitFor tasks.
        const Group root4 {
            storage,
            barrier,
            parallel,
            createBarrierAdvance(storage, barrier, 1),
            createBarrierAdvance(storage, barrier, 2),
            Group {
                groupSetup(2),
                barrierAwaiterTask(barrier),
                createSuccessTask(3)
            },
            Group {
                groupSetup(3),
                barrierAwaiterTask(barrier),
                createSuccessTask(4)
            }
        };
        const Log log4 {
            {1, Handler::Setup},
            {2, Handler::Setup},
            {2, Handler::GroupSetup},
            {3, Handler::GroupSetup},
            {1, Handler::BarrierAdvance},
            {2, Handler::BarrierAdvance},
            {3, Handler::Setup},
            {4, Handler::Setup},
            {3, Handler::Success},
            {4, Handler::Success}
        };

        // Notice the different log order for each scenario.
        QTest::newRow("MultiBarrierSequential")
            << TestData{storage, root1, log1, 5, DoneWith::Success, 4};
        QTest::newRow("MultiBarrierParallelAdvanceFirst")
            << TestData{storage, root2, log2, 5, DoneWith::Success, 5};
        QTest::newRow("MultiBarrierParallelWaitForFirst")
            << TestData{storage, root3, log3, 5, DoneWith::Success, 5};
        QTest::newRow("MultiBarrierParallelMultiWaitFor")
            << TestData{storage, root4, log4, 6, DoneWith::Success, 6};
    }

    {
        // Test QCustomTask::withTimeout() combinations:
        // 1. When the timeout has triggered or not.
        // 2. With and without timeout handler.
        const Group root1 {
            storage,
            TestTask(setupTask(1, 1000ms), setupDone(1))
                .withTimeout(1ms)
        };
        const Log log1 {
            {1, Handler::Setup},
            {1, Handler::Canceled}
        };
        QTest::newRow("TaskErrorWithTimeout")
            << TestData{storage, root1, log1, 2, DoneWith::Error, 1};

        const Group root2 {
            storage,
            TestTask(setupTask(1, 1000ms), setupDone(1))
                .withTimeout(1ms, setupTimeout(1))
        };
        const Log log2 {
            {1, Handler::Setup},
            {1, Handler::Timeout},
            {1, Handler::Canceled}
        };
        QTest::newRow("TaskErrorWithTimeoutHandler")
            << TestData{storage, root2, log2, 2, DoneWith::Error, 1};

        const Group root3 {
            storage,
            TestTask(setupTask(1, 1ms), setupDone(1))
                .withTimeout(1000ms)
        };
        const Log doneLog {
            {1, Handler::Setup},
            {1, Handler::Success}
        };
        QTest::newRow("TaskDoneWithTimeout")
            << TestData{storage, root3, doneLog, 2, DoneWith::Success, 1};

        const Group root4 {
            storage,
            TestTask(setupTask(1, 1ms), setupDone(1))
                .withTimeout(1000ms, setupTimeout(1))
        };
        QTest::newRow("TaskDoneWithTimeoutHandler")
            << TestData{storage, root4, doneLog, 2, DoneWith::Success, 1};
    }

    {
        // Test Group::withTimeout() combinations:
        // 1. When the timeout has triggered or not.
        // 2. With and without timeout handler.
        const Group root1 {
            storage,
            Group {
                createSuccessTask(1, 1000ms)
            }.withTimeout(1ms)
        };
        const Log log1 {
            {1, Handler::Setup},
            {1, Handler::Canceled}
        };
        QTest::newRow("GroupErrorWithTimeout")
            << TestData{storage, root1, log1, 2, DoneWith::Error, 1};

        // Test Group::withTimeout(), passing custom handler
        const Group root2 {
            storage,
            Group {
                createSuccessTask(1, 1000ms)
            }.withTimeout(1ms, setupTimeout(1))
        };
        const Log log2 {
            {1, Handler::Setup},
            {1, Handler::Timeout},
            {1, Handler::Canceled}
        };
        QTest::newRow("GroupErrorWithTimeoutHandler")
            << TestData{storage, root2, log2, 2, DoneWith::Error, 1};

        const Group root3 {
            storage,
            Group {
                createSuccessTask(1, 1ms)
            }.withTimeout(1000ms)
        };
        const Log doneLog {
            {1, Handler::Setup},
            {1, Handler::Success}
        };
        QTest::newRow("GroupDoneWithTimeout")
            << TestData{storage, root3, doneLog, 2, DoneWith::Success, 1};

        // Test Group::withTimeout(), passing custom handler
        const Group root4 {
            storage,
            Group {
                createSuccessTask(1, 1ms)
            }.withTimeout(1000ms, setupTimeout(1))
        };
        QTest::newRow("GroupDoneWithTimeoutHandler")
            << TestData{storage, root4, doneLog, 2, DoneWith::Success, 1};
    }

    {
        // Test if the task tree started from the setup handler may use the same storage.

        const auto groupSetup = [storage] {
            storage->m_log.append({0, Handler::GroupSetup});

            const auto nestedGroupSetup = [storage] {
                storage->m_log.append({2, Handler::GroupSetup});
            };
            const auto nestedGroupDone = [storage](DoneWith result) {
                storage->m_log.append({2, resultToGroupHandler(result)});
            };
            QTaskTree subTree({storage,
                              onGroupSetup(nestedGroupSetup),
                              onGroupDone(nestedGroupDone)});
            subTree.start();
            storage->m_log.append({1, Handler::GroupSetup}); // Ensure the storage is still active.
        };
        const auto groupDone = [storage](DoneWith result) {
            storage->m_log.append({0, resultToGroupHandler(result)});
        };

        const Group root {
            storage,
            onGroupSetup(groupSetup),
            onGroupDone(groupDone)
        };
        const Log log {
            {0, Handler::GroupSetup},
            {1, Handler::GroupSetup},
            {0, Handler::GroupSuccess}
        };
        QTest::newRow("CommonStorage") << TestData{storage, root, log, 0, DoneWith::Success, 0};
    }

    {
        const Group root {
            storage,
            Group {
                parallel,
                createSuccessTask(1, 100ms),
                Group {
                    createFailingTask(2, 1ms),
                    createSuccessTask(3, 1ms)
                },
                createSuccessTask(4, 1ms)
            },
            createSuccessTask(5, 1ms)
        };
        const Log log {
            {1, Handler::Setup},
            {2, Handler::Setup},
            {4, Handler::Setup},
            {2, Handler::Error},
            {1, Handler::Canceled},
            {4, Handler::Canceled}
        };
        QTest::newRow("NestedCancel") << TestData{storage, root, log, 5, DoneWith::Error, 1};
    }

    {
        const GroupItems successItems {
            storage,
            createSuccessTask(1),
            createSuccessTask(2)
        };

        const Group rootSequentialSuccess = For (RepeatIterator(2)) >> Do {
            sequential,
            successItems
        };
        const Log logSequentialSuccess {
            {1, Handler::Setup},
            {1, Handler::Success},
            {2, Handler::Setup},
            {2, Handler::Success},
            {1, Handler::Setup},
            {1, Handler::Success},
            {2, Handler::Setup},
            {2, Handler::Success}
        };

        const Group rootParallelSuccess = For (RepeatIterator(2)) >> Do {
            parallel,
            successItems
        };
        const Log logParallelSuccess {
            {1, Handler::Setup},
            {2, Handler::Setup},
            {1, Handler::Setup},
            {2, Handler::Setup},
            {1, Handler::Success},
            {2, Handler::Success},
            {1, Handler::Success},
            {2, Handler::Success}
        };

        const Group rootParallelLimitSuccess = For (RepeatIterator(2)) >> Do  {
            ParallelLimit(2),
            successItems
        };
        const Log logParallelLimitSuccess {
            {1, Handler::Setup},
            {2, Handler::Setup},
            {1, Handler::Success},
            {1, Handler::Setup},
            {2, Handler::Success},
            {2, Handler::Setup},
            {1, Handler::Success},
            {2, Handler::Success}
        };

        const GroupItems errorItems {
            storage,
            createSuccessTask(1),
            createFailingTask(2)
        };

        const Group rootSequentialError = For (RepeatIterator(2)) >> Do {
            sequential,
            errorItems
        };
        const Log logSequentialError {
            {1, Handler::Setup},
            {1, Handler::Success},
            {2, Handler::Setup},
            {2, Handler::Error}
        };

        const Group rootParallelError = For (RepeatIterator(2)) >> Do {
            parallel,
            errorItems
        };
        const Log logParallelError {
            {1, Handler::Setup},
            {2, Handler::Setup},
            {1, Handler::Setup},
            {2, Handler::Setup},
            {1, Handler::Success},
            {2, Handler::Error},
            {1, Handler::Canceled},
            {2, Handler::Canceled}
        };

        const Group rootParallelLimitError = For (RepeatIterator(2)) >> Do {
            ParallelLimit(2),
            errorItems
        };
        const Log logParallelLimitError {
            {1, Handler::Setup},
            {2, Handler::Setup},
            {1, Handler::Success},
            {1, Handler::Setup},
            {2, Handler::Error},
            {1, Handler::Canceled}
        };

        QTest::newRow("RepeatSequentialSuccess")
            << TestData{storage, rootSequentialSuccess, logSequentialSuccess, 4, DoneWith::Success, 4};
        QTest::newRow("RepeatParallelSuccess")
            << TestData{storage, rootParallelSuccess, logParallelSuccess, 4, DoneWith::Success, 4};
        QTest::newRow("RepeatParallelLimitSuccess")
            << TestData{storage, rootParallelLimitSuccess, logParallelLimitSuccess, 4, DoneWith::Success, 4};
        QTest::newRow("RepeatSequentialError")
            << TestData{storage, rootSequentialError, logSequentialError, 4, DoneWith::Error, 2};
        QTest::newRow("RepeatParallelError")
            << TestData{storage, rootParallelError, logParallelError, 4, DoneWith::Error, 2};
        QTest::newRow("RepeatParallelLimitError")
            << TestData{storage, rootParallelLimitError, logParallelLimitError, 4, DoneWith::Error, 2};
    }

    {
        const UntilIterator loop([](int index) { return index < 3; });

        const auto onSetupContinue = [storage, loop](int taskId) {
            return [storage, loop, taskId](TaskObject &) {
                storage->m_log.append({loop.iteration() * 10 + taskId, Handler::Setup});
            };
        };

        const auto onSetupStop = [storage, loop](int taskId) {
            return [storage, loop, taskId](TaskObject &) {
                storage->m_log.append({loop.iteration() * 10 + taskId, Handler::Setup});
                if (loop.iteration() == 2)
                    return SetupResult::StopWithSuccess;
                return SetupResult::Continue;
            };
        };

        const auto onDone = [storage, loop](int taskId) {
            return [storage, loop, taskId](DoneWith doneWith) {
                const Handler handler = doneWith == DoneWith::Cancel ? Handler::Canceled
                    : doneWith == DoneWith::Success ? Handler::Success : Handler::Error;
                storage->m_log.append({loop.iteration() * 10 + taskId, handler});
            };
        };

        const GroupItems items {
            storage,
            TestTask(onSetupContinue(1), onDone(1)),
            TestTask(onSetupStop(2), onDone(2))
        };

        const Group rootSequential = For(loop) >> Do {
            sequential,
            items
        };
        const Log logSequential {
            {1, Handler::Setup},
            {1, Handler::Success},
            {2, Handler::Setup},
            {2, Handler::Success},
            {11, Handler::Setup},
            {11, Handler::Success},
            {12, Handler::Setup},
            {12, Handler::Success},
            {21, Handler::Setup},
            {21, Handler::Success},
            {22, Handler::Setup}
        };

        const Group rootParallel = For(loop) >> Do {
            parallel,
            items
        };
        const Log logParallel {
            {1, Handler::Setup},
            {2, Handler::Setup},
            {11, Handler::Setup},
            {12, Handler::Setup},
            {21, Handler::Setup},
            {22, Handler::Setup},
            {1, Handler::Success},
            {2, Handler::Success},
            {11, Handler::Success},
            {12, Handler::Success},
            {21, Handler::Success}
        };

        const Group rootParallelLimit = For(loop) >> Do {
            ParallelLimit(2),
            items
        };
        const Log logParallelLimit {
            {1, Handler::Setup},
            {2, Handler::Setup},
            {1, Handler::Success},
            {11, Handler::Setup},
            {2, Handler::Success},
            {12, Handler::Setup},
            {11, Handler::Success},
            {21, Handler::Setup},
            {12, Handler::Success},
            {22, Handler::Setup},
            {21, Handler::Success}
        };

        QTest::newRow("LoopSequential")
            << TestData{storage, rootSequential, logSequential, 2, DoneWith::Success, 5};
        QTest::newRow("LoopParallel")
            << TestData{storage, rootParallel, logParallel, 2, DoneWith::Success, 5};
        QTest::newRow("LoopParallelLimit")
            << TestData{storage, rootParallelLimit, logParallelLimit, 2, DoneWith::Success, 5};
    }

    {
        // Check if task tree finishes with the right progress value when UntilIterator(false).
        const Group root = For(UntilIterator([](int) { return false; })) >> Do {
            storage,
            createSuccessTask(1)
        };
        QTest::newRow("ProgressWithLoopUntilFalse")
            << TestData{storage, root, {}, 1, DoneWith::Success, 0};
    }

    {
        // Check if task tree finishes with the right progress value when nested UntilIterator(false).
        const Group root = For (UntilIterator([](int index) { return index < 2; })) >> Do {
            storage,
            For (UntilIterator([](int) { return false; })) >> Do {
                createSuccessTask(1)
            }
        };
        QTest::newRow("ProgressWithNestedLoopUntilFalse")
            << TestData{storage, root, {}, 1, DoneWith::Success, 0};
    }

    {
        // Check if UntilIterator is executed with empty loop body.
        const UntilIterator iterator([storage](int iteration) {
            storage->m_log.append({iteration, Handler::Iteration});
            return iteration < 3;
        });

        const Group root = For(iterator) >> Do { storage };

        const Log log {
            {0, Handler::Iteration},
            {1, Handler::Iteration},
            {2, Handler::Iteration},
            {3, Handler::Iteration} // The last iteration returns false
        };

        QTest::newRow("EmptyLoopUntil")
            << TestData{storage, root, log, 0, DoneWith::Success, 0};
    }

    {
        // Check if task tree finishes with the right progress value when onGroupSetup(false).
        const Group root {
            storage,
            onGroupSetup([] { return SetupResult::StopWithSuccess; }),
            createSuccessTask(1)
        };
        QTest::newRow("ProgressWithGroupSetupFalse")
            << TestData{storage, root, {}, 1, DoneWith::Success, 0};
    }

    {
        // Check if task tree finishes with the right progress value when nested UntilIterator(false).
        const Group root = For (UntilIterator([](int index) { return index < 2; })) >> Do {
            storage,
            Group {
                onGroupSetup([] { return SetupResult::StopWithSuccess; }),
                createSuccessTask(1)
            }
        };
        QTest::newRow("ProgressWithNestedGroupSetupFalse")
            << TestData{storage, root, {}, 1, DoneWith::Success, 0};
    }

    {
        class DummyTaskAdapter final // bool is dummy
        {
        public:
            void operator()(bool *, QTaskInterface *iface) { iface->reportDone(DoneResult::Error); }
        };

        using DummyTask = QCustomTask<bool, DummyTaskAdapter>;

        // Check if progress is updated correctly on error when the 1st task finishes synchonously.
        const Group root {
            storage,
            DummyTask(),
            createSuccessTask(1)
        };
        QTest::newRow("ProgressOnSynchronousError")
            << TestData{storage, root, {}, 2, DoneWith::Error, 0};
    }

    {
        const auto recipe = [storage, createSuccessTask](bool withTask) {
            return Group {
                storage,
                withTask ? createSuccessTask(1) : nullItem
            };
        };

        const Log withTaskLog {
            {1, Handler::Setup},
            {1, Handler::Success}
        };

        QTest::newRow("RecipeWithTask")
            << TestData{storage, recipe(true), withTaskLog, 1, DoneWith::Success, 1};
        QTest::newRow("RecipeWithNull")
            << TestData{storage, recipe(false), {}, 0, DoneWith::Success, 0};
    }

    {
        // These tests confirm the expected message log.

        const TestData testSuccess {
            storage,
            Group {
                storage,
                Group {
                    createSuccessTask(1, 2ms).withLog("Task 1")
                }.withLog("Group 1")
            },
            Log {
                {1, Handler::Setup},
                {1, Handler::Success}
            },
            1,
            DoneWith::Success,
            1,
            MessageLog {
                {"Group 1", Handler::Setup},
                {"Task 1", Handler::Setup},
                {"Task 1", Handler::Success, Execution::Async},
                {"Group 1", Handler::Success, Execution::Async}
            }
        };
        const TestData testError {
            storage,
            Group {
                storage,
                Group {
                    createFailingTask(1, 1ms).withLog("Task 1")
                }.withLog("Group 1")
            },
            Log {
                {1, Handler::Setup},
                {1, Handler::Error}
            },
            1,
            DoneWith::Error,
            1,
            MessageLog {
                {"Group 1", Handler::Setup},
                {"Task 1", Handler::Setup},
                {"Task 1", Handler::Error, Execution::Async},
                {"Group 1", Handler::Error, Execution::Async}
            }
        };
        const TestData testCancel {
            storage,
            Group {
                storage,
                parallel,
                Group {
                    createSuccessTask(1).withLog("Task 1"),
                }.withLog("Group 1"),
                createSyncWithTweak(2, DoneResult::Error).withLog("Task 2")
            },
            Log {
                {1, Handler::Setup},
                {2, Handler::Sync},
                {2, Handler::TweakDoneToError},
                {1, Handler::Canceled}
            },
            1,
            DoneWith::Error,
            0,
            MessageLog {
                {"Group 1", Handler::Setup},
                {"Task 1", Handler::Setup},
                {"Task 2", Handler::Setup},
                {"Task 2", Handler::Error, Execution::Sync},
                {"Task 1", Handler::Canceled, Execution::Sync},
                {"Group 1", Handler::Canceled, Execution::Sync}
            }
        };

        QTest::newRow("LogSuccess") << testSuccess;
        QTest::newRow("LogError") << testError;
        QTest::newRow("LogCancel") << testCancel;
    }

    {
        // These tests ensure the task handlers are invoked according to the CallDone

        const auto createRoot = [storage, groupDone](DoneWith doneWith, CallDone callDone) {
            return Group {
                storage,
                parallel,
                Group {
                    TestTask({}, toDoneResult(doneWith == DoneWith::Success)),
                    groupDone(1, callDone)
                },
                doneWith == DoneWith::Cancel ? QSyncTask([] { return false; }) : nullItem
            };
        };

        const Log logNoDone {};
        const Log logSuccess {{1, Handler::GroupSuccess}};
        const Log logError {{1, Handler::GroupError}};
        const Log logCanceled {{1, Handler::GroupCanceled}};

        QTest::newRow("SuccessCallDoneNever")
            << TestData{storage, createRoot(DoneWith::Success, CallDoneFlag::Never),
                        logNoDone, 1, DoneWith::Success, 1};
        QTest::newRow("SuccessCallDoneOnSuccess")
            << TestData{storage, createRoot(DoneWith::Success, CallDoneFlag::OnSuccess),
                        logSuccess, 1, DoneWith::Success, 1};
        QTest::newRow("SuccessCallDoneOnError")
            << TestData{storage, createRoot(DoneWith::Success, CallDoneFlag::OnError),
                        logNoDone, 1, DoneWith::Success, 1};
        QTest::newRow("SuccessCallDoneOnCancel")
            << TestData{storage, createRoot(DoneWith::Success, CallDoneFlag::OnCancel),
                        logNoDone, 1, DoneWith::Success, 1};
        QTest::newRow("SuccessCallDoneOnSuccessOrError")
            << TestData{storage, createRoot(DoneWith::Success, CallDoneFlag::OnSuccess | CallDoneFlag::OnError),
                        logSuccess, 1, DoneWith::Success, 1};
        QTest::newRow("SuccessCallDoneOnSuccessOrCancel")
            << TestData{storage, createRoot(DoneWith::Success, CallDoneFlag::OnSuccess | CallDoneFlag::OnCancel),
                        logSuccess, 1, DoneWith::Success, 1};
        QTest::newRow("SuccessCallDoneOnErrorOrCancel")
            << TestData{storage, createRoot(DoneWith::Success, CallDoneFlag::OnError | CallDoneFlag::OnCancel),
                        logNoDone, 1, DoneWith::Success, 1};
        QTest::newRow("SuccessCallDoneAlways")
            << TestData{storage, createRoot(DoneWith::Success, CallDoneFlag::Always),
                        logSuccess, 1, DoneWith::Success, 1};

        QTest::newRow("ErrorCallDoneNever")
            << TestData{storage, createRoot(DoneWith::Error, CallDoneFlag::Never),
                        logNoDone, 1, DoneWith::Error, 1};
        QTest::newRow("ErrorCallDoneOnSuccess")
            << TestData{storage, createRoot(DoneWith::Error, CallDoneFlag::OnSuccess),
                        logNoDone, 1, DoneWith::Error, 1};
        QTest::newRow("ErrorCallDoneOnError")
            << TestData{storage, createRoot(DoneWith::Error, CallDoneFlag::OnError),
                        logError, 1, DoneWith::Error, 1};
        QTest::newRow("ErrorCallDoneOnCancel")
            << TestData{storage, createRoot(DoneWith::Error, CallDoneFlag::OnCancel),
                        logNoDone, 1, DoneWith::Error, 1};
        QTest::newRow("ErrorCallDoneOnSuccessOrError")
            << TestData{storage, createRoot(DoneWith::Error, CallDoneFlag::OnSuccess | CallDoneFlag::OnError),
                        logError, 1, DoneWith::Error, 1};
        QTest::newRow("ErrorCallDoneOnSuccessOrCancel")
            << TestData{storage, createRoot(DoneWith::Error, CallDoneFlag::OnSuccess | CallDoneFlag::OnCancel),
                        logNoDone, 1, DoneWith::Error, 1};
        QTest::newRow("ErrorCallDoneOnErrorOrCancel")
            << TestData{storage, createRoot(DoneWith::Error, CallDoneFlag::OnError | CallDoneFlag::OnCancel),
                        logError, 1, DoneWith::Error, 1};
        QTest::newRow("ErrorCallDoneAlways")
            << TestData{storage, createRoot(DoneWith::Error, CallDoneFlag::Always),
                        logError, 1, DoneWith::Error, 1};

        QTest::newRow("CancelCallDoneNever")
            << TestData{storage, createRoot(DoneWith::Cancel, CallDoneFlag::Never),
                        logNoDone, 1, DoneWith::Error, 0};
        QTest::newRow("CancelCallDoneOnSuccess")
            << TestData{storage, createRoot(DoneWith::Cancel, CallDoneFlag::OnSuccess),
                        logNoDone, 1, DoneWith::Error, 0};
        QTest::newRow("CancelCallDoneOnError")
            << TestData{storage, createRoot(DoneWith::Cancel, CallDoneFlag::OnError),
                        logNoDone, 1, DoneWith::Error, 0};
        QTest::newRow("CancelCallDoneOnCancel")
            << TestData{storage, createRoot(DoneWith::Cancel, CallDoneFlag::OnCancel),
                        logCanceled, 1, DoneWith::Error, 0};
        QTest::newRow("CancelCallDoneOnSuccessOrError")
            << TestData{storage, createRoot(DoneWith::Cancel, CallDoneFlag::OnSuccess | CallDoneFlag::OnError),
                        logNoDone, 1, DoneWith::Error, 0};
        QTest::newRow("CancelCallDoneOnSuccessOrCancel")
            << TestData{storage, createRoot(DoneWith::Cancel, CallDoneFlag::OnSuccess | CallDoneFlag::OnCancel),
                        logCanceled, 1, DoneWith::Error, 0};
        QTest::newRow("CancelCallDoneOnErrorOrCancel")
            << TestData{storage, createRoot(DoneWith::Cancel, CallDoneFlag::OnError | CallDoneFlag::OnCancel),
                        logCanceled, 1, DoneWith::Error, 0};
        QTest::newRow("CancelCallDoneAlways")
            << TestData{storage, createRoot(DoneWith::Cancel, CallDoneFlag::Always),
                        logCanceled, 1, DoneWith::Error, 0};
    }

    {
        // This test ensures the task done handlers are invoked in a different order
        // than the corresponding setup handlers.

        const QList<milliseconds> tasks { s_endlessTime, 0ms };
        const ListIterator iterator(tasks);

        const auto onSetup = [storage, iterator](TaskObject &taskObject) {
            taskObject = *iterator;
            storage->m_log.append({iterator.iteration(), Handler::Setup});
        };

        const auto onDone = [storage, iterator](DoneWith result) {
            const Handler handler = result == DoneWith::Cancel ? Handler::Canceled : Handler::Error;
            storage->m_log.append({iterator.iteration(), handler});
            return DoneResult::Error;
        };

        const Group root = For (iterator) >> Do {
            storage,
            parallel,
            TestTask(onSetup, onDone)
        };

        const Log log {
            {0, Handler::Setup},
            {1, Handler::Setup},
            {1, Handler::Error},
            {0, Handler::Canceled}
        };

        QTest::newRow("ParallelDisorder") << TestData{storage, root, log, 2, DoneWith::Error, 1};
    }

    {
        // This test ensures the task done handler or onGroupDone accepts the DoneResult as an
        // argument.

        const Group groupSuccess {
            storage,
            Group {
                onGroupDone(DoneResult::Success)
            },
            groupDone(0)
        };
        const Group groupError {
            storage,
            Group {
                onGroupDone(DoneResult::Error)
            },
            groupDone(0)
        };
        const Group taskSuccess {
            storage,
            TestTask({}, DoneResult::Success),
            groupDone(0)
        };
        const Group taskError {
            storage,
            TestTask({}, DoneResult::Error),
            groupDone(0)
        };

        QTest::newRow("DoneResultGroupSuccess")
            << TestData{storage, groupSuccess, {{0, Handler::GroupSuccess}}, 0, DoneWith::Success, 0};
        QTest::newRow("DoneResultGroupError")
            << TestData{storage, groupError, {{0, Handler::GroupError}}, 0, DoneWith::Error, 0};
        QTest::newRow("DoneResultTaskSuccess")
            << TestData{storage, taskSuccess, {{0, Handler::GroupSuccess}}, 1, DoneWith::Success, 1};
        QTest::newRow("DoneResultTaskError")
            << TestData{storage, taskError, {{0, Handler::GroupError}}, 1, DoneWith::Error, 1};
    }

    {
        // These tests ensure the task done handler or onGroupDone accepts the DoneResult as an
        // argument.

        const Group groupSuccess {
            storage,
            Group { createFailingTask(0) } || DoneResult::Success,
            groupDone(0)
        };

        const Group groupError {
            storage,
            Group { createSuccessTask(0) } && DoneResult::Error,
            groupDone(0)
        };

        const Group taskSuccess {
            storage,
            createFailingTask(0) || DoneResult::Success,
            groupDone(0)
        };

        const Group taskError {
            storage,
            createSuccessTask(0) && DoneResult::Error,
            groupDone(0)
        };

        const Log successLog {{0, Handler::Setup}, {0, Handler::Error}, {0, Handler::GroupSuccess}};
        const Log errorLog {{0, Handler::Setup}, {0, Handler::Success}, {0, Handler::GroupError}};

        QTest::newRow("LogicGroupSuccess")
            << TestData{storage, groupSuccess, successLog, 1, DoneWith::Success, 1};
        QTest::newRow("LogicGroupError")
            << TestData{storage, groupError, errorLog, 1, DoneWith::Error, 1};
        QTest::newRow("LogicTaskSuccess")
            << TestData{storage, taskSuccess, successLog, 1, DoneWith::Success, 1};
        QTest::newRow("LogicTaskError")
            << TestData{storage, taskError, errorLog, 1, DoneWith::Error, 1};
    }

    {
        // This test checks if ExecutableItem's negation works OK.

        const Group negateSuccessTask {
            storage,
            !createSuccessTask(0)
        };

        const Group negateErrorTask {
            storage,
            !createFailingTask(0)
        };

        const Group negateSuccessGroup {
            storage,
            !Group {
                createSuccessTask(0)
            }
        };

        const Group negateErrorGroup {
            storage,
            !Group {
                createFailingTask(0)
            }
        };

        const Group doubleNegation {
            storage,
            !!createSuccessTask(0)
        };

        const Log successLog {{0, Handler::Setup}, {0, Handler::Success}};
        const Log errorLog {{0, Handler::Setup}, {0, Handler::Error}};

        QTest::newRow("NegateSuccessTask")
            << TestData{storage, negateSuccessTask, successLog, 1, DoneWith::Error, 1};
        QTest::newRow("NegateErrorTask")
            << TestData{storage, negateErrorTask, errorLog, 1, DoneWith::Success, 1};
        QTest::newRow("NegateSuccessGroup")
            << TestData{storage, negateSuccessGroup, successLog, 1, DoneWith::Error, 1};
        QTest::newRow("NegateErrorGroup")
            << TestData{storage, negateErrorGroup, errorLog, 1, DoneWith::Success, 1};
        QTest::newRow("DoubleNegation")
            << TestData{storage, doubleNegation, successLog, 1, DoneWith::Success, 1};
    }

    {
        // This test checks if ExecutableItem's AND and OR works OK.

        const Group successAndSuccessTask {
            storage,
            createSuccessTask(0) && createSuccessTask(1)
        };
        const Group successAndErrorTask {
            storage,
            createSuccessTask(0) && createFailingTask(1)
        };
        const Group errorAndSuccessTask {
            storage,
            createFailingTask(0) && createSuccessTask(1)
        };
        const Group errorAndErrorTask {
            storage,
            createFailingTask(0) && createFailingTask(1)
        };

        const Group successOrSuccessTask {
            storage,
            createSuccessTask(0) || createSuccessTask(1)
        };
        const Group successOrErrorTask {
            storage,
            createSuccessTask(0) || createFailingTask(1)
        };
        const Group errorOrSuccessTask {
            storage,
            createFailingTask(0) || createSuccessTask(1)
        };
        const Group errorOrErrorTask {
            storage,
            createFailingTask(0) || createFailingTask(1)
        };

        const Log successLog {{0, Handler::Setup}, {0, Handler::Success}};
        const Log errorLog {{0, Handler::Setup}, {0, Handler::Error}};

        const Log successSuccessLog {
            {0, Handler::Setup},
            {0, Handler::Success},
            {1, Handler::Setup},
            {1, Handler::Success},
        };
        const Log successErrorLog {
            {0, Handler::Setup},
            {0, Handler::Success},
            {1, Handler::Setup},
            {1, Handler::Error},
        };
        const Log errorSuccessLog {
            {0, Handler::Setup},
            {0, Handler::Error},
            {1, Handler::Setup},
            {1, Handler::Success},
        };
        const Log errorErrorLog {
            {0, Handler::Setup},
            {0, Handler::Error},
            {1, Handler::Setup},
            {1, Handler::Error},
        };

        QTest::newRow("SuccessAndSuccessTask")
            << TestData{storage, successAndSuccessTask, successSuccessLog, 2, DoneWith::Success, 2};
        QTest::newRow("SuccessAndErrorTask")
            << TestData{storage, successAndErrorTask, successErrorLog, 2, DoneWith::Error, 2};
        QTest::newRow("ErrorAndSuccessTask")
            << TestData{storage, errorAndSuccessTask, errorLog, 2, DoneWith::Error, 1};
        QTest::newRow("ErrorAndErrorTask")
            << TestData{storage, errorAndErrorTask, errorLog, 2, DoneWith::Error, 1};

        QTest::newRow("SuccessOrSuccessTask")
            << TestData{storage, successOrSuccessTask, successLog, 2, DoneWith::Success, 1};
        QTest::newRow("SuccessOrErrorTask")
            << TestData{storage, successOrErrorTask, successLog, 2, DoneWith::Success, 1};
        QTest::newRow("ErrorOrSuccessTask")
            << TestData{storage, errorOrSuccessTask, errorSuccessLog, 2, DoneWith::Success, 2};
        QTest::newRow("ErrorOrErrorTask")
            << TestData{storage, errorOrErrorTask, errorErrorLog, 2, DoneWith::Error, 2};
    }

    {
        // This test ensures the nullItem in conditional expression works OK.

        const auto recipe = [storage, createSuccessTask](bool condition) {
            return Group {
                storage,
                condition ? createSuccessTask(0) : nullItem
            };
        };

        const Log trueLog {{0, Handler::Setup}, {0, Handler::Success}};
        const Log falseLog {};

        QTest::newRow("NullItemTrue")
            << TestData{storage, recipe(true), trueLog, 1, DoneWith::Success, 1};
        QTest::newRow("NullItemFalse")
            << TestData{storage, recipe(false), falseLog, 0, DoneWith::Success, 0};
    }

    {
        // These tests ensure the successItem and errorItem work OK.

        const auto recipe = [storage, createSuccessTask](bool success) {
            return Group {
                storage,
                success ? successItem : errorItem
            };
        };

        QTest::newRow("SuccessItem")
            << TestData{storage, recipe(true), {}, 0, DoneWith::Success, 0};
        QTest::newRow("ErrorItem")
            << TestData{storage, recipe(false), {}, 0, DoneWith::Error, 0};
    }

    // This test checks if storage shadowing works OK.
    QTest::newRow("StorageShadowing") << storageShadowingData();

    // CONDITIONAL API

    {
        const Group root {
            storage,
            If (createSuccessTask(0)) >> Then {
                createSuccessTask(1)
            }
        };

        const Log log {
            {0, Handler::Setup},
            {0, Handler::Success},
            {1, Handler::Setup},
            {1, Handler::Success}
        };

        QTest::newRow("CondIfSuccessThenSuccess")
            << TestData{storage, root, log, 2, DoneWith::Success, 2};
    }

    {
        const Group root {
            storage,
            If (createSuccessTask(0)) >> Then {
                createFailingTask(1)
            }
        };

        const Log log {
            {0, Handler::Setup},
            {0, Handler::Success},
            {1, Handler::Setup},
            {1, Handler::Error}
        };

        QTest::newRow("CondIfSuccessThenError")
            << TestData{storage, root, log, 2, DoneWith::Error, 2};
    }

    {
        const Group root {
            storage,
            If (createFailingTask(0)) >> Then {
                createSuccessTask(1)
            }
        };

        const Log log {
            {0, Handler::Setup},
            {0, Handler::Error}
        };

        QTest::newRow("CondIfErrorThenSuccess")
            << TestData{storage, root, log, 2, DoneWith::Success, 1};
    }

    {
        const Group root {
            storage,
            If (createSuccessTask(0)) >> Then {
                createSuccessTask(1)
            } >> Else {
                createSuccessTask(2)
            }
        };

        const Log log {
            {0, Handler::Setup},
            {0, Handler::Success},
            {1, Handler::Setup},
            {1, Handler::Success}
        };

        QTest::newRow("CondIfSuccessThenSuccessElseSuccess")
            << TestData{storage, root, log, 3, DoneWith::Success, 2};
    }

    {
        const Group root {
            storage,
            If (createFailingTask(0)) >> Then {
                createSuccessTask(1)
            } >> Else {
                createSuccessTask(2)
            }
        };

        const Log log {
            {0, Handler::Setup},
            {0, Handler::Error},
            {2, Handler::Setup},
            {2, Handler::Success}
        };

        QTest::newRow("CondIfErrorThenSuccessElseSuccess")
            << TestData{storage, root, log, 3, DoneWith::Success, 2};
    }

    {
        const Group root {
            storage,
            If (createFailingTask(0)) >> Then {
                createSuccessTask(1)
            } >> Else {
                createFailingTask(2)
            }
        };

        const Log log {
            {0, Handler::Setup},
            {0, Handler::Error},
            {2, Handler::Setup},
            {2, Handler::Error}
        };

        QTest::newRow("CondIfErrorThenSuccessElseError")
            << TestData{storage, root, log, 3, DoneWith::Error, 2};
    }

    {
        const Group root {
            storage,
            If (createFailingTask(0)) >> Then {
                createSuccessTask(1)
            } >> ElseIf (createFailingTask(2)) >> Then {
                createSuccessTask(3)
            }
        };

        const Log log {
            {0, Handler::Setup},
            {0, Handler::Error},
            {2, Handler::Setup},
            {2, Handler::Error}
        };

        QTest::newRow("CondIfErrorThenSuccessElseIfErrorThenSuccess")
            << TestData{storage, root, log, 4, DoneWith::Success, 2};
    }

    {
        const Group root {
            storage,
            If (createFailingTask(0)) >> Then {
                createSuccessTask(1)
            } >> ElseIf (createSuccessTask(2)) >> Then {
                createSuccessTask(3)
            }
        };

        const Log log {
            {0, Handler::Setup},
            {0, Handler::Error},
            {2, Handler::Setup},
            {2, Handler::Success},
            {3, Handler::Setup},
            {3, Handler::Success}
        };

        QTest::newRow("CondIfErrorThenSuccessElseIfSuccessThenSuccess")
            << TestData{storage, root, log, 4, DoneWith::Success, 3};
    }

    {
        const Group root {
            storage,
            If (createFailingTask(0)) >> Then {
                createSuccessTask(1)
            } >> ElseIf (createSuccessTask(2)) >> Then {
                createFailingTask(3)
            }
        };

        const Log log {
            {0, Handler::Setup},
            {0, Handler::Error},
            {2, Handler::Setup},
            {2, Handler::Success},
            {3, Handler::Setup},
            {3, Handler::Error}
        };

        QTest::newRow("CondIfErrorThenSuccessElseIfSuccessThenError")
            << TestData{storage, root, log, 4, DoneWith::Error, 3};
    }

    {
        const Group root {
            storage,
            If (createSuccessTask(0)) >> Then {
                createSuccessTask(1)
            } >> ElseIf (createSuccessTask(2)) >> Then {
                createSuccessTask(3)
            } >> Else {
                createSuccessTask(4)
            }
        };

        const Log log {
            {0, Handler::Setup},
            {0, Handler::Success},
            {1, Handler::Setup},
            {1, Handler::Success}
        };

        QTest::newRow("CondIfSuccessThenSuccessElseIfSuccessThenSuccessElseSuccess")
            << TestData{storage, root, log, 5, DoneWith::Success, 2};
    }

    {
        const Group root {
            storage,
            If (createFailingTask(0)) >> Then {
                createSuccessTask(1)
            } >> ElseIf (createSuccessTask(2)) >> Then {
                createSuccessTask(3)
            } >> Else {
                createSuccessTask(4)
            }
        };

        const Log log {
            {0, Handler::Setup},
            {0, Handler::Error},
            {2, Handler::Setup},
            {2, Handler::Success},
            {3, Handler::Setup},
            {3, Handler::Success}
        };

        QTest::newRow("CondIfErrorThenSuccessElseIfSuccessThenSuccessElseSuccess")
            << TestData{storage, root, log, 5, DoneWith::Success, 3};
    }

    {
        const Group root {
            storage,
            If (createSuccessTask(0)) >> Then {
                createSuccessTask(1)
            },
            createSuccessTask(2)
        };

        const Log log {
            {0, Handler::Setup},
            {0, Handler::Success},
            {1, Handler::Setup},
            {1, Handler::Success},
            {2, Handler::Setup},
            {2, Handler::Success}
        };

        QTest::newRow("CondIfSuccessThenSuccessWithContinuation")
            << TestData{storage, root, log, 3, DoneWith::Success, 3};
    }

    {
        const Group root {
            storage,
            If (createSuccessTask(0)) >> Then {
                createFailingTask(1)
            },
            createSuccessTask(2)
        };

        const Log log {
            {0, Handler::Setup},
            {0, Handler::Success},
            {1, Handler::Setup},
            {1, Handler::Error}
        };

        QTest::newRow("CondIfSuccessThenErrorWithContinuation")
            << TestData{storage, root, log, 3, DoneWith::Error, 2};
    }

    {
        const Group root {
            storage,
            If (createFailingTask(0)) >> Then {
                createFailingTask(1)
            },
            createSuccessTask(2)
        };

        const Log log {
            {0, Handler::Setup},
            {0, Handler::Error},
            {2, Handler::Setup},
            {2, Handler::Success}
        };

        QTest::newRow("CondIfErrorThenErrorWithContinuation")
            << TestData{storage, root, log, 3, DoneWith::Success, 2};
    }

    {
        // These tests ensure the successItem and errorItem work OK in if-statement.

        const auto recipe = [storage, createSuccessTask, createFailingTask]
            (bool ifCondition, bool bodyResult, bool elseResult) {
            return Group {
                storage,
                If (ifCondition ? createSuccessTask(0) : createFailingTask(0)) >> Then {
                    bodyResult ? successItem : errorItem
                } >> Else {
                    elseResult ? successItem : errorItem
                },
                createSuccessTask(1)
            };
        };

        const Log trueTrueTrue {
            {0, Handler::Setup},
            {0, Handler::Success},
            {1, Handler::Setup},
            {1, Handler::Success}
        };
        const Log trueTrueFalse = trueTrueTrue; // Else branch skipped.
        const Log trueFalseTrue {
            {0, Handler::Setup},
            {0, Handler::Success}
        };
        const Log trueFalseFalse = trueFalseTrue; // Else branch skipped.
        const Log falseTrueTrue {
            {0, Handler::Setup},
            {0, Handler::Error},
            {1, Handler::Setup}, // Else branch returns successItem -> continue after If.
            {1, Handler::Success}
        };
        const Log falseTrueFalse {
            {0, Handler::Setup},
            {0, Handler::Error}
        }; // Else branch returns errorItem -> main group skips task 2 and stops with error.
        const Log falseFalseTrue = falseTrueTrue; // Then branch skipped.
        const Log falseFalseFalse = falseTrueFalse; // Then branch skipped

        QTest::newRow("BoolItemTrueTrueTrue")
            << TestData{storage, recipe(true, true, true), trueTrueTrue, 2, DoneWith::Success, 2};
        QTest::newRow("BoolItemTrueTrueFalse")
            << TestData{storage, recipe(true, true, false), trueTrueFalse, 2, DoneWith::Success, 2};
        QTest::newRow("BoolItemTrueFalseTrue")
            << TestData{storage, recipe(true, false, true), trueFalseTrue, 2, DoneWith::Error, 1};
        QTest::newRow("BoolItemTrueFalseFalse")
            << TestData{storage, recipe(true, false, false), trueFalseFalse, 2, DoneWith::Error, 1};
        QTest::newRow("BoolItemFalseTrueTrue")
            << TestData{storage, recipe(false, true, true), falseTrueTrue, 2, DoneWith::Success, 2};
        QTest::newRow("BoolItemFalseTrueFalse")
            << TestData{storage, recipe(false, true, false), falseTrueFalse, 2, DoneWith::Error, 1};
        QTest::newRow("BoolItemFalseFalseTrue")
            << TestData{storage, recipe(false, false, true), falseFalseTrue, 2, DoneWith::Success, 2};
        QTest::newRow("BoolItemFalseFalseFalse")
            << TestData{storage, recipe(false, false, false), falseFalseFalse, 2, DoneWith::Error, 1};
    }

    {
        // withCancel / withAccept

        const Storage<std::unique_ptr<QTimer>> tickStorage;

        const auto onSetup = [tickStorage](milliseconds timeout) {
            return [tickStorage, timeout] {
                tickStorage->reset(new QTimer);
                tickStorage->get()->start(timeout);
            };
        };

        const auto onTickSetup = [tickStorage] {
            return makeObjectSignal(tickStorage->get(), &QTimer::timeout);
        };

        const Group cancelTriggeredRecipe {
            storage,
            tickStorage,
            onGroupSetup(onSetup(0ms)), // 1st queued call
            Group {
                groupSetup(1),
                createSuccessTask(2, s_endlessTime),
                groupDone(1)
            }.withCancel(onTickSetup, {
                createSuccessTask(3), // 2nd queued call
                createFailingTask(4), // 3rd queued call
                createSuccessTask(5)
            })
        };

        const Log cancelTriggeredLog {
            {1, Handler::GroupSetup},
            {2, Handler::Setup},
            {2, Handler::Canceled},
            {1, Handler::GroupCanceled},
            {3, Handler::Setup},
            {3, Handler::Success},
            {4, Handler::Setup},
            {4, Handler::Error}
        };

        QTest::newRow("WithCancelTriggered")
            << TestData{storage, cancelTriggeredRecipe, cancelTriggeredLog, 5, DoneWith::Error, 3};

        const Group cancelSkippedSuccessRecipe {
            storage,
            tickStorage,
            onGroupSetup(onSetup(s_endlessTime)),
            Group {
                groupSetup(1),
                createSuccessTask(2), // 1st queued call
                groupDone(1)
            }.withCancel(onTickSetup, { // This group is skipped since the previous group wasn't canceled
                createSuccessTask(3),
                createFailingTask(4),
                createSuccessTask(5)
            })
        };

        const Log cancelSkippedSuccessLog {
            {1, Handler::GroupSetup},
            {2, Handler::Setup},
            {2, Handler::Success},
            {1, Handler::GroupSuccess}
        };

        QTest::newRow("WithCancelSkippedSuccess")
            << TestData{storage, cancelSkippedSuccessRecipe, cancelSkippedSuccessLog, 5, DoneWith::Success, 1};

        const Group cancelSkippedErrorRecipe {
            storage,
            tickStorage,
            onGroupSetup(onSetup(s_endlessTime)),
            Group {
                groupSetup(1),
                createFailingTask(2), // 1st queued call
                groupDone(1)
            }.withCancel(onTickSetup, { // This group is skipped since the previous group wasn't canceled
                createSuccessTask(3),
                createFailingTask(4),
                createSuccessTask(5)
            })
        };

        const Log cancelSkippedErrorLog {
            {1, Handler::GroupSetup},
            {2, Handler::Setup},
            {2, Handler::Error},
            {1, Handler::GroupError}
        };

        QTest::newRow("WithCancelSkippedError")
            << TestData{storage, cancelSkippedErrorRecipe, cancelSkippedErrorLog, 5, DoneWith::Error, 1};

        const Group acceptRecipe {
            storage,
            tickStorage,
            onGroupSetup(onSetup(1ms)),
            Group {
                groupSetup(1),
                createSuccessTask(2, 0ms).withAccept(onTickSetup),
                groupDone(1)
            }
        };

        const Log acceptLog {
            {1, Handler::GroupSetup},
            {2, Handler::Setup},
            {2, Handler::Success},
            {1, Handler::GroupSuccess},
        };

        QTest::newRow("WithAccept")
            << TestData{storage, acceptRecipe, acceptLog, 2, DoneWith::Success, 2};
    }
}

static QtMessageHandler s_oldMessageHandler = nullptr;
static QStringList s_messages;

class LogMessageHandler
{
public:
    LogMessageHandler()
    {
        s_messages.clear();
        s_oldMessageHandler = qInstallMessageHandler(handler);
    }
    ~LogMessageHandler() { qInstallMessageHandler(s_oldMessageHandler); }

private:
    static void handler(QtMsgType type, const QMessageLogContext &context, const QString &msg)
    {
        QString message = msg;
        message.remove('\r');
        s_messages.append(message);
        // Defer to old message handler.
        if (s_oldMessageHandler)
            s_oldMessageHandler(type, context, msg);
    }
};

static const QList<Handler> s_logHandlers
    = {Handler::Setup, Handler::Success, Handler::Error, Handler::Canceled};

static QString messageInfix(const Message &message)
{
    if (message.handler == Handler::Setup)
        return "started";

    DoneWith doneWith;
    switch (message.handler) {
    case Handler::Success:
        doneWith = DoneWith::Success;
        break;
    case Handler::Error:
        doneWith = DoneWith::Error;
        break;
    case Handler::Canceled:
        doneWith = DoneWith::Cancel;
        break;
    default:
        return {};
    }

    const QString execution = message.execution == Execution::Async ? "asynchronously"
                                                                    : "synchronously";
    const QMetaEnum doneWithEnum = QMetaEnum::fromType<DoneWith>();
    return QString("finished %1 with %2").arg(execution, doneWithEnum.valueToKey(int(doneWith)));
}

static bool matchesLogPattern(const QString &log, const Message &message)
{
    QStringView logView(log);
    const static QLatin1String part1("TASK TREE LOG [");
    if (!logView.startsWith(part1))
        return false;

    logView = logView.mid(part1.size());
    const static QLatin1String part2("HH:mm:ss.zzz"); // check only size

    logView = logView.mid(part2.size());
    const QString part3 = QString("] \"%1\" ").arg(message.name);
    if (!logView.startsWith(part3))
        return false;

    logView = logView.mid(part3.size());
    const QString part4(messageInfix(message));
    if (!logView.startsWith(part4))
        return false;

    logView = logView.mid(part4.size());
    if (message.handler == Handler::Setup)
        return logView == QLatin1String(".");

    const static QLatin1String part5(" within ");
    if (!logView.startsWith(part5))
        return false;

    return logView.endsWith(QLatin1String("ms."));
}

const QMetaEnum s_handlerEnum = QMetaEnum::fromType<Handler>();

void tst_TaskTree::testTree()
{
    QFETCH(TestData, testData);

    LogMessageHandler handler;

    QTaskTree taskTree({testData.root.withTimeout(1s)});
    const int taskCount = taskTree.taskCount() - 1; // -1 for the timeout task above
    QCOMPARE(taskCount, testData.taskCount);
    Log actualLog;
    const auto collectLog = [&actualLog](const CustomStorage &storage) {
        actualLog = storage.m_log;
    };
    taskTree.onStorageDone(testData.storage, collectLog);
    const DoneWith result = taskTree.runBlocking();
    QCOMPARE(taskTree.isRunning(), false);

    QCOMPARE(taskTree.progressValue(), taskTree.progressMaximum());
    QCOMPARE(actualLog, testData.expectedLog);
    QCOMPARE(CustomStorage::instanceCount(), 0);

    QCOMPARE(result, testData.result);

    if (testData.asyncCount)
        QCOMPARE(taskTree.asyncCount(), *testData.asyncCount);

    if (testData.messageLog) {
        QCOMPARE(s_messages.count(), testData.messageLog->count());

        for (int i = 0; i < s_messages.count(); ++i) {
            const auto &message = testData.messageLog->at(i);
            QVERIFY2(s_logHandlers.contains(message.handler), qPrintable(QString(
                "The expected log message at index %1 contains invalid %2 handler.")
                .arg(i).arg(s_handlerEnum.valueToKey(int(message.handler)))));
            if (message.handler != Handler::Setup) {
                QVERIFY2(message.execution, qPrintable(QString(
                    "The expected log message at index %1 of %2 type doesn't specify the required "
                    "execution mode.").arg(i).arg(s_handlerEnum.valueToKey(int(message.handler)))));
            }
            const QString &log = s_messages.at(i);
            QVERIFY2(matchesLogPattern(log, message), qPrintable(QString(
                "The log message at index %1: \"%2\" doesn't match the expected pattern: "
                "\"%3\" %4.").arg(i).arg(log, message.name, messageInfix(message))));
        }
    }
}

#if QT_CONFIG(concurrent)

void tst_TaskTree::testInThread_data()
{
    QTest::addColumn<TestData>("testData");
    QTest::newRow("StorageShadowing") << storageShadowingData();
    QTest::newRow("Parallel") << parallelData();
}

struct TestResult
{
    int executeCount = 0;
    ThreadResult threadResult = ThreadResult::Success;
    Log actualLog = {};
};

static const int s_loopCount = 1000;
static const int s_threadCount = 12;

static void runInThread(QPromise<TestResult> &promise, const TestData &testData)
{
    for (int i = 0; i < s_loopCount; ++i) {
        if (promise.isCanceled()) {
            promise.addResult(TestResult{i, ThreadResult::Canceled});
            return;
        }

        QTaskTree taskTree({testData.root.withTimeout(1000ms)});
        if (taskTree.taskCount() - 1 != testData.taskCount) { // -1 for the timeout task above
            promise.addResult(TestResult{i, ThreadResult::FailOnTaskCountCheck});
            return;
        }
        Log actualLog;
        const auto collectLog = [&actualLog](const CustomStorage &storage) {
            actualLog = storage.m_log;
        };
        taskTree.onStorageDone(testData.storage, collectLog);

        const DoneWith result = taskTree.runBlocking(QFuture<void>(promise.future()));

        if (taskTree.isRunning()) {
            promise.addResult(TestResult{i, ThreadResult::FailOnRunningCheck});
            return;
        }
        if (taskTree.progressValue() != taskTree.progressMaximum()) {
            promise.addResult(TestResult{i, ThreadResult::FailOnProgressCheck});
            return;
        }
        if (actualLog != testData.expectedLog) {
            promise.addResult(TestResult{i, ThreadResult::FailOnLogCheck, actualLog});
            return;
        }
        if (result != testData.result) {
            promise.addResult(TestResult{i, ThreadResult::FailOnDoneStatusCheck});
            return;
        }
    }
    promise.addResult(TestResult{s_loopCount, ThreadResult::Success, testData.expectedLog});
}

void tst_TaskTree::testInThread()
{
    QFETCH(TestData, testData);

    const auto onSetup = [testData](QThreadFunction<TestResult> &task) {
        task.setThreadFunctionData(&runInThread, testData);
    };
    const auto onDone = [testData](const QThreadFunction<TestResult> &task) {
        QVERIFY(task.future().resultCount());
        const TestResult result = task.result();
        QCOMPARE(result.actualLog, testData.expectedLog);
        QCOMPARE(result.threadResult, ThreadResult::Success);
        QCOMPARE(result.executeCount, s_loopCount);
    };

    const Group recipe = For (RepeatIterator(s_threadCount)) >> Do {
        parallel,
        QThreadFunctionTask<TestResult>(onSetup, onDone)
    };

    QTaskTree taskTree(recipe);
    const DoneWith result = taskTree.runBlocking();
    QCOMPARE(taskTree.isRunning(), false);

    QCOMPARE(CustomStorage::instanceCount(), 0);
    QCOMPARE(result, testData.result);
}

static std::atomic_bool s_jobStarted = false;
static std::atomic_bool s_treeFinished = false;
static std::atomic_bool s_syncDone = false;

static void autoSyncOn(QPromise<void> &promise)
{
    QThread::sleep(1ms);
    s_jobStarted = true; // Let know the threadJob has started.
    while (s_treeFinished == false)
        QThread::sleep(1ms); // Ensures the tree has finished.
    Q_ASSERT(promise.isCanceled()); // Tree has already canceled the promise.
    QThread::sleep(1ms); // Spin for a while so that QThreadFunctionBase::syncAll() awaits for us.
    s_syncDone = true;
}

static void autoSyncOff(QPromise<void> &promise)
{
    Q_ASSERT(!promise.isCanceled()); // Tree is still running.
    s_jobStarted = true; // Let know the threadJob has started.
    while (!promise.isCanceled())
        QThread::sleep(1ms); // Spin for a while so that ~QThreadFunction awaits for us.
    s_syncDone = true;
}

void tst_TaskTree::testAutoDelayedSync()
{
    const auto onAutoSyncOnSetup = [](QThreadFunction<void> &task) {
        task.setThreadFunctionData(&autoSyncOn);
    };

    const auto isNotStarted = [](int) { return s_jobStarted == false; };

    const Group awaiter = For (UntilIterator(isNotStarted)) >> Do {
        timeoutTask(1ms, DoneResult::Success)
    } && errorItem;

    const Group recipeAutoSyncOn {
        parallel,
        QThreadFunctionTask<void>(onAutoSyncOnSetup),
        awaiter
    };

    const DoneWith resultOn = QTaskTree::runBlocking(recipeAutoSyncOn);
    QCOMPARE(resultOn, DoneWith::Error);
    QCOMPARE(s_syncDone, false);
    s_treeFinished = true;
    QThreadFunctionBase::syncAll();
    QCOMPARE(s_syncDone, true);

    s_jobStarted = false;
    s_treeFinished = false;
    s_syncDone = false ;

    const auto onAutoSyncOffSetup = [](QThreadFunction<void> &task) {
        task.setAutoDelayedSync(false);
        task.setThreadFunctionData(autoSyncOff);
    };

    const Group recipeAutoSyncOff {
        parallel,
        QThreadFunctionTask<void>(onAutoSyncOffSetup),
        awaiter
    };

    const DoneWith resultOff = QTaskTree::runBlocking(recipeAutoSyncOff);
    QCOMPARE(resultOff, DoneWith::Error);
    QCOMPARE(s_syncDone, true);
}

#endif // QT_CONFIG(concurrent)

struct StorageIO
{
    int value = 0;
};

static Group inputOutputRecipe(const Storage<StorageIO> &storage)
{
    return Group {
        storage,
        onGroupSetup([storage] { ++storage->value; }),
        onGroupDone([storage] { storage->value *= 2; })
    };
}

void tst_TaskTree::storageIO_data()
{
    QTest::addColumn<int>("input");
    QTest::addColumn<int>("output");

    QTest::newRow("-1 -> 0") << -1 << 0;
    QTest::newRow("0 -> 2") <<  0 << 2;
    QTest::newRow("1 -> 4") <<  1 << 4;
    QTest::newRow("2 -> 6") <<  2 << 6;
}

void tst_TaskTree::storageIO()
{
    QFETCH(int, input);
    QFETCH(int, output);

    int actualOutput = 0;

    const Storage<StorageIO> storage;
    QTaskTree taskTree(inputOutputRecipe(storage));

    const auto setInput = [input](StorageIO &storage) { storage.value = input; };
    const auto getOutput = [&actualOutput](const StorageIO &storage) { actualOutput = storage.value; };

    taskTree.onStorageSetup(storage, setInput);
    taskTree.onStorageDone(storage, getOutput);
    taskTree.runBlocking();

    QCOMPARE(taskTree.isRunning(), false);
    QCOMPARE(actualOutput, output);
}

void tst_TaskTree::storageOperators()
{
    StorageBase storage1 = Storage<CustomStorage>();
    StorageBase storage2 = Storage<CustomStorage>();
    StorageBase storage3 = storage1;

    QVERIFY(storage1 == storage3);
    QVERIFY(storage1 != storage2);
    QVERIFY(storage2 != storage3);
}

// This test checks whether a running task tree may be safely destructed.
// It also checks whether the destructor of a task tree deletes properly the storage created
// while starting the task tree. When running task tree is destructed, the storage done
// handler shouldn't be invoked.
void tst_TaskTree::storageDestructor()
{
    bool setupCalled = false;
    const auto setupHandler = [&setupCalled](CustomStorage &) {
        setupCalled = true;
    };
    bool doneCalled = false;
    const auto doneHandler = [&doneCalled](const CustomStorage &) {
        doneCalled = true;
    };
    QCOMPARE(CustomStorage::instanceCount(), 0);
    {
        const Storage<CustomStorage> storage;
        const auto setupSleepingTask = [](TaskObject &taskObject) {
            taskObject = 1000ms;
        };
        const Group root {
            storage,
            TestTask(setupSleepingTask)
        };

        QTaskTree taskTree(root);
        QCOMPARE(CustomStorage::instanceCount(), 0);
        taskTree.onStorageSetup(storage, setupHandler);
        taskTree.onStorageDone(storage, doneHandler);
        taskTree.start();
        QCOMPARE(CustomStorage::instanceCount(), 1);
    }
    QCOMPARE(CustomStorage::instanceCount(), 0);
    QVERIFY(setupCalled);
    QVERIFY(!doneCalled);
}

void tst_TaskTree::storageInitialization_data()
{
    QTest::addColumn<Storage<int>>("storage");
    QTest::addColumn<int>("initValue");

    // This test ensures that the storage data is zero-initialized.
    QTest::newRow("zero initialization") << Storage<int>() << 0;
    // This test ensures that the storage c'tor with data is initialized properly.
    QTest::newRow("data initialization") << Storage<int>(42) << 42;
}

void tst_TaskTree::storageInitialization()
{
    QFETCH(Storage<int>, storage);
    QFETCH(int, initValue);

    std::optional<int> storageValue = std::nullopt;

    const auto onSetup = [storage, &storageValue] { storageValue = *storage; };

    QTaskTree taskTree({ storage, onGroupSetup(onSetup) });
    taskTree.runBlocking();

    QVERIFY(storageValue);
    QCOMPARE(*storageValue, initValue);
}

// This test ensures that when a missing storage object inside the nested task tree is accessed
// directly from the outer task tree's handler, containing the same storage object, then we
// detect this misconfigured recipe, issue a warning, and return nullptr for the storage
// being accessed (instead of returning parent's task tree's storage instance).
// This test should also trigger a runtime assert that we are accessing the nullptr storage.
void tst_TaskTree::nestedBrokenStorage()
{
    int *outerStorage = nullptr;
    int *innerStorage1 = nullptr;
    int *innerStorage2 = nullptr;
    const Storage<int> storage;

    const auto onOuterSync = [storage, &outerStorage, &innerStorage1, &innerStorage2] {
        outerStorage = &*storage;

        const auto onInnerSync1 = [storage, &innerStorage1] {
            innerStorage1 = &*storage; // Triggers the runtime assert on purpose.
        };
        const auto onInnerSync2 = [storage, &innerStorage2] {
            innerStorage2 = &*storage; // Storage is accessible in currently running tree - all OK.
        };

        const Group innerRecipe {
            Group {
                // Broken subrecipe, the storage wasn't placed inside the recipe.
                // storage,
                QSyncTask(onInnerSync1)
            },
            Group {
                storage, // Subrecipe OK, another instance for the nested storage will be created.
                QSyncTask(onInnerSync2)
            }
        };

        QTaskTree::runBlocking(innerRecipe);
    };

    const Group outerRecipe {
        storage,
        QSyncTask(onOuterSync)
    };

    QTaskTree::runBlocking(outerRecipe);
    QVERIFY(outerStorage != nullptr);
    QCOMPARE(innerStorage1, nullptr);
    QVERIFY(innerStorage2 != nullptr);
    QVERIFY(innerStorage2 != outerStorage);
}

void tst_TaskTree::restart()
{
    QTaskTree taskTree({TestTask([](TaskObject &taskObject) { taskObject = 1000ms; })});
    taskTree.start();
    QVERIFY(taskTree.isRunning());
    taskTree.cancel();
    QVERIFY(!taskTree.isRunning());
    taskTree.start();
    QVERIFY(taskTree.isRunning());
}

class BrokenTaskAdapter final
{
public:
    // QTCREATORBUG-30204
    ~BrokenTaskAdapter()
    {
        if (m_iface)
            m_iface->reportDone(DoneResult::Success);
    }
    void operator()(int *, QTaskInterface *iface) { m_iface = iface; }
private:
    QTaskInterface *m_iface = nullptr;
};

using BrokenTask = QCustomTask<int, BrokenTaskAdapter>;

void tst_TaskTree::destructorOfTaskEmittingDone()
{
    QTaskTree taskTree({BrokenTask()});
    taskTree.start();
}

void tst_TaskTree::restartTaskTreeRunnerFromDoneHandler()
{
    QSingleTaskTreeRunner runner;
    QStringList log;
    QStringList expectedLog{"1", "2"};

    const auto onFirstDone = [&runner, &log] {
        log.append("1");
        runner.start({TestTask()}, {}, [&log] { log.append("2"); });
    };
    runner.start({TestTask()}, {}, onFirstDone);
    QTRY_VERIFY(!runner.isRunning());
    QCOMPARE(log, expectedLog);
}

void tst_TaskTree::validConditionalConstructs()
{
    const TestTask condition;
    const TestTask continuation;

    Group {
        If (condition) >> Then {
            continuation
        } >> ElseIf (condition) >> Then {
            continuation
        } >> ElseIf (condition) >> Then {
            continuation
        } >> Else {
            continuation
        }
    };

    Group {
        parallel,
        TestTask(),
        If (condition) >> Then {
            continuation
        },
        If (condition) >> Then {
            continuation
        } >> Else {
            continuation
        }
    };

    Group {
        If (condition) >> Then {
            continuation
        }
    };

    Group {
        If (condition) >> Then {
            continuation
        } >> Else {
            continuation
        }
    };

    Group {
        If (condition) >> Then {
            continuation
        } >> ElseIf (condition) >> Then {
            continuation
        }
    };

    Group {
        If (condition) >> Then {
            continuation
        } >> ElseIf (condition) >> Then {
            continuation
        } >> ElseIf (condition) >> Then {
            continuation
        } >> Else {
            continuation
        }
    };

    Group {
        If (condition) >> Then {
            continuation
        } >> ElseIf (condition) >> Then {
            continuation
        } >> ElseIf (condition) >> Then {
            continuation
        } >> ElseIf (condition) >> Then {
            continuation
        }
    };

    Group {
        If (condition) >> Then {
            continuation
        } >> ElseIf (condition) >> Then {
            continuation
        } >> Else {
            continuation
        }
    };

    Group {
        If (condition && condition) >> Then {
            continuation
        } >> ElseIf (!condition) >> Then {
            continuation
        } >> Else {
            continuation
        }
    };

    // The following constucts are invalid and won't compile:
#if 0

    // Lack of "Then" body.
    Group {
        If (condition)
    };

    // Can't start with "ElseIf".
    Group {
        ElseIf (condition)
    };

    // Can't start with "Else".
    Group {
        Else { continuation }
    };

    // Can't start with "Then".
    Group {
        Then { continuation }
    };

    // "Then" can't be followed by "If".
    // Replace ">>" with "," after the first "Then" to construct 2 independent if conditions.
    Group {
        If (condition) >> Then {
            continuation
        } >> If (condition) >> Then {
            continuation
        }
    };

    // "Else" can't be followed by anything, it must be the final statement.
    Group {
        If (condition) >> Then {
            continuation
        } >> Else {
            continuation
        } >> Else {
            continuation
        }
    };

#endif
}

void tst_TaskTree::exactHandlers()
{
    bool taskSetupCalled = false;
    bool taskDoneCalled = false;
    bool treeSetupCalled = false;
    bool treeDoneCalled = false;

    const TestTask::TaskSetupHandler onTaskSetup = [&taskSetupCalled](TaskObject &) {
        taskSetupCalled = true;
        return SetupResult::Continue;
    };

    const TestTask::TaskDoneHandler onTaskDone = [&taskDoneCalled](const TaskObject &, DoneWith) {
        taskDoneCalled = true;
        return DoneResult::Success;
    };

    const TreeSetupHandler onTreeSetup = [&treeSetupCalled](QTaskTree &) {
        treeSetupCalled = true;
    };

    const TreeDoneHandler onTreeDone
            = [&treeDoneCalled](const QTaskTree &, DoneWith) {
        treeDoneCalled = true;
    };

    QSingleTaskTreeRunner taskTreeRunner;
    taskTreeRunner.start({TestTask(onTaskSetup, onTaskDone)}, onTreeSetup, onTreeDone);

    QTRY_VERIFY_WITH_TIMEOUT(!taskTreeRunner.isRunning(), 1s);

    QVERIFY(taskSetupCalled);
    QVERIFY(taskDoneCalled);
    QVERIFY(treeSetupCalled);
    QVERIFY(treeDoneCalled);
}

class BoolDone : public QObject
{
    Q_OBJECT

public:
    void start() { Q_EMIT done(true); }

Q_SIGNALS:
    void done(bool);
};

using BoolDoneTask = QCustomTask<BoolDone>;

void tst_TaskTree::boolDoneAdapter()
{
    std::optional<bool> result = std::nullopt;
    const auto onDone = [&result](DoneWith doneWith) {
        result = doneWith == DoneWith::Success ? true : false;
    };
    QSingleTaskTreeRunner taskTreeRunner;
    taskTreeRunner.start({BoolDoneTask()}, {}, onDone);
    QVERIFY(!taskTreeRunner.isRunning());
    QVERIFY(result);
    QVERIFY(*result);
}

void tst_TaskTree::cleanupTestCase()
{
#if QT_CONFIG(concurrent)
    QThreadFunctionBase::syncAll();
#endif
}

QTEST_GUILESS_MAIN(tst_TaskTree)

#include "tst_qttasktree.moc"
