/*
 * Copyright (c) Simon Brunel, https://github.com/simonbrunel
 *
 * This source code is licensed under the MIT license found in
 * the LICENSE file in the root directory of this source tree.
 */

#include "../shared/data.h"
#include "../shared/utils.h"

#include <QtPromise>
#include <QtTest>

class tst_qpromise_reduce : public QObject
{
    Q_OBJECT

private Q_SLOTS:
    void emptySequence();
    void regularValues();
    void promiseValues();
    void convertResultType();
    void delayedInitialValue();
    void delayedFulfilled();
    void delayedRejected();
    void functorThrows();
    void sequenceTypes();
};

QTEST_MAIN(tst_qpromise_reduce)
#include "tst_reduce.moc"

namespace {

template<class Sequence>
struct SequenceTester
{
    static void exec()
    {
        Sequence inputs{QtPromise::resolve(4).delay(400),
                        QtPromise::resolve(6).delay(300),
                        QtPromise::resolve(8).delay(200)};
        QVector<int> v0;
        QVector<int> v1;

        auto p0 = QtPromise::resolve(inputs).reduce([&](int acc, int cur, int idx) {
            v0 << acc << cur << idx;
            return acc + cur + idx;
        });
        auto p1 = QtPromise::resolve(inputs).reduce(
            [&](int acc, int cur, int idx) {
                v1 << acc << cur << idx;
                return acc + cur + idx;
            },
            QtPromise::resolve(2).delay(100));

        Q_STATIC_ASSERT((std::is_same<decltype(p0), QtPromise::QPromise<int>>::value));
        Q_STATIC_ASSERT((std::is_same<decltype(p1), QtPromise::QPromise<int>>::value));

        QCOMPARE(p0.isPending(), true);
        QCOMPARE(p1.isPending(), true);
        QCOMPARE(waitForValue(p0, -1), 21);
        QCOMPARE(waitForValue(p1, -1), 23);
        QCOMPARE(v0, (QVector<int>{4, 6, 1, 11, 8, 2}));
        QCOMPARE(v1, (QVector<int>{2, 4, 0, 6, 6, 1, 13, 8, 2}));
    }
};

} // anonymous namespace

void tst_qpromise_reduce::emptySequence()
{
    bool called = false;

    auto p = QtPromise::resolve(QVector<int>{})
                 .reduce(
                     [&](...) {
                         called = true;
                         return 43;
                     },
                     42);

    // NOTE(SB): reduce() on an empty sequence without an initial value is an error!

    Q_STATIC_ASSERT((std::is_same<decltype(p), QtPromise::QPromise<int>>::value));

    QCOMPARE(waitForValue(p, -1), 42);
    QCOMPARE(called, false);
}

void tst_qpromise_reduce::regularValues()
{
    QVector<int> inputs{4, 6, 8};
    QVector<int> v0;
    QVector<int> v1;

    auto p0 = QtPromise::resolve(inputs).reduce([&](int acc, int cur, int idx) {
        v0 << acc << cur << idx;
        return acc + cur + idx;
    });
    auto p1 = QtPromise::resolve(inputs).reduce(
        [&](int acc, int cur, int idx) {
            v1 << acc << cur << idx;
            return acc + cur + idx;
        },
        2);

    Q_STATIC_ASSERT((std::is_same<decltype(p0), QtPromise::QPromise<int>>::value));
    Q_STATIC_ASSERT((std::is_same<decltype(p1), QtPromise::QPromise<int>>::value));

    QCOMPARE(p0.isPending(), true);
    QCOMPARE(p1.isPending(), true);
    QCOMPARE(waitForValue(p0, -1), 21);
    QCOMPARE(waitForValue(p1, -1), 23);
    QCOMPARE(v0, (QVector<int>{4, 6, 1, 11, 8, 2}));
    QCOMPARE(v1, (QVector<int>{2, 4, 0, 6, 6, 1, 13, 8, 2}));
}

void tst_qpromise_reduce::promiseValues()
{
    QVector<QtPromise::QPromise<int>> inputs{QtPromise::resolve(4).delay(400),
                                             QtPromise::resolve(6).delay(300),
                                             QtPromise::resolve(8).delay(200)};
    QVector<int> v0;
    QVector<int> v1;

    auto p0 = QtPromise::resolve(inputs).reduce([&](int acc, int cur, int idx) {
        v0 << acc << cur << idx;
        return acc + cur + idx;
    });
    auto p1 = QtPromise::resolve(inputs).reduce(
        [&](int acc, int cur, int idx) {
            v1 << acc << cur << idx;
            return acc + cur + idx;
        },
        2);

    Q_STATIC_ASSERT((std::is_same<decltype(p0), QtPromise::QPromise<int>>::value));
    Q_STATIC_ASSERT((std::is_same<decltype(p1), QtPromise::QPromise<int>>::value));

    QCOMPARE(p0.isPending(), true);
    QCOMPARE(p1.isPending(), true);
    QCOMPARE(waitForValue(p0, -1), 21);
    QCOMPARE(waitForValue(p1, -1), 23);
    QCOMPARE(v0, (QVector<int>{4, 6, 1, 11, 8, 2}));
    QCOMPARE(v1, (QVector<int>{2, 4, 0, 6, 6, 1, 13, 8, 2}));
}

void tst_qpromise_reduce::convertResultType()
{
    QVector<int> inputs{4, 6, 8};

    auto p = QtPromise::resolve(inputs).reduce(
        [&](const QString& acc, int cur, int idx) {
            return QString{"%1:%2:%3"}.arg(acc).arg(cur).arg(idx);
        },
        QString{"foo"});

    // NOTE(SB): when no initial value is given, the result type is the sequence type.

    Q_STATIC_ASSERT((std::is_same<decltype(p), QtPromise::QPromise<QString>>::value));

    QCOMPARE(p.isPending(), true);
    QCOMPARE(waitForValue(p, QString{}), QString{"foo:4:0:6:1:8:2"});
}

void tst_qpromise_reduce::delayedInitialValue()
{
    QVector<int> values;

    auto p = QtPromise::resolve(QVector<int>{4, 6, 8})
                 .reduce(
                     [&](int acc, int cur, int idx) {
                         values << acc << cur << idx;
                         return acc + cur + idx;
                     },
                     QtPromise::resolve(2).delay(100));

    Q_STATIC_ASSERT((std::is_same<decltype(p), QtPromise::QPromise<int>>::value));

    QCOMPARE(p.isPending(), true);
    QCOMPARE(waitForValue(p, -1), 23);
    QCOMPARE(values, (QVector<int>{2, 4, 0, 6, 6, 1, 13, 8, 2}));
}

void tst_qpromise_reduce::delayedFulfilled()
{
    QVector<int> inputs{4, 6, 8};
    QVector<int> v0;
    QVector<int> v1;

    auto p0 = QtPromise::resolve(inputs).reduce([&](int acc, int cur, int idx) {
        v0 << acc << cur << idx;
        return QtPromise::resolve(acc + cur + idx).delay(100);
    });
    auto p1 = QtPromise::resolve(inputs).reduce(
        [&](int acc, int cur, int idx) {
            v1 << acc << cur << idx;
            return QtPromise::resolve(acc + cur + idx).delay(100);
        },
        2);

    Q_STATIC_ASSERT((std::is_same<decltype(p0), QtPromise::QPromise<int>>::value));
    Q_STATIC_ASSERT((std::is_same<decltype(p1), QtPromise::QPromise<int>>::value));

    QCOMPARE(p0.isPending(), true);
    QCOMPARE(p1.isPending(), true);
    QCOMPARE(waitForValue(p0, -1), 21);
    QCOMPARE(waitForValue(p1, -1), 23);
    QCOMPARE(v0, (QVector<int>{4, 6, 1, 11, 8, 2}));
    QCOMPARE(v1, (QVector<int>{2, 4, 0, 6, 6, 1, 13, 8, 2}));
}

void tst_qpromise_reduce::delayedRejected()
{
    QVector<int> inputs{4, 6, 8};
    QVector<int> v0;
    QVector<int> v1;

    auto p0 = QtPromise::resolve(inputs).reduce([&](int acc, int cur, int idx) {
        v0 << acc << cur << idx;
        if (cur == 6) {
            return QtPromise::QPromise<int>::reject(QString{"foo"});
        }
        return QtPromise::resolve(acc + cur + idx);
    });
    auto p1 = QtPromise::resolve(inputs).reduce(
        [&](int acc, int cur, int idx) {
            v1 << acc << cur << idx;
            if (cur == 6) {
                return QtPromise::QPromise<int>::reject(QString{"bar"});
            }
            return QtPromise::resolve(acc + cur + idx);
        },
        2);

    Q_STATIC_ASSERT((std::is_same<decltype(p0), QtPromise::QPromise<int>>::value));
    Q_STATIC_ASSERT((std::is_same<decltype(p1), QtPromise::QPromise<int>>::value));

    QCOMPARE(p0.isPending(), true);
    QCOMPARE(p1.isPending(), true);
    QCOMPARE(waitForError(p0, QString{}), QString{"foo"});
    QCOMPARE(waitForError(p1, QString{}), QString{"bar"});
    QCOMPARE(v0, (QVector<int>{4, 6, 1}));
    QCOMPARE(v1, (QVector<int>{2, 4, 0, 6, 6, 1}));
}

void tst_qpromise_reduce::functorThrows()
{
    QVector<int> inputs{4, 6, 8};
    QVector<int> v0;
    QVector<int> v1;

    auto p0 = QtPromise::resolve(inputs).reduce([&](int acc, int cur, int idx) {
        v0 << acc << cur << idx;
        if (cur == 6) {
            throw QString{"foo"};
        }
        return acc + cur + idx;
    });
    auto p1 = QtPromise::resolve(inputs).reduce(
        [&](int acc, int cur, int idx) {
            v1 << acc << cur << idx;
            if (cur == 6) {
                throw QString{"bar"};
            }
            return acc + cur + idx;
        },
        2);

    Q_STATIC_ASSERT((std::is_same<decltype(p0), QtPromise::QPromise<int>>::value));
    Q_STATIC_ASSERT((std::is_same<decltype(p1), QtPromise::QPromise<int>>::value));

    QCOMPARE(p0.isPending(), true);
    QCOMPARE(p1.isPending(), true);
    QCOMPARE(waitForError(p0, QString{}), QString{"foo"});
    QCOMPARE(waitForError(p1, QString{}), QString{"bar"});
    QCOMPARE(v0, (QVector<int>{4, 6, 1}));
    QCOMPARE(v1, (QVector<int>{2, 4, 0, 6, 6, 1}));
}

void tst_qpromise_reduce::sequenceTypes()
{
#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0))
    // QLinkedList is deprecated since Qt 5.15.
    SequenceTester<QLinkedList<QtPromise::QPromise<int>>>::exec();
#endif
    SequenceTester<QList<QtPromise::QPromise<int>>>::exec();
    SequenceTester<QVector<QtPromise::QPromise<int>>>::exec();
    SequenceTester<std::list<QtPromise::QPromise<int>>>::exec();
    SequenceTester<std::vector<QtPromise::QPromise<int>>>::exec();
}
