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

#include <QtTest/QtTest>

#include <QtQml/private/qpauseanimationjob_p.h>
#include <QtQml/private/qsequentialanimationgroupjob_p.h>
#include <QtQml/private/qparallelanimationgroupjob_p.h>

#if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
static const char timerError[] = "On some platforms, consistent timing is not working properly due to bad timer resolution";
#endif

class TestablePauseAnimation : public QPauseAnimationJob
{
public:
    TestablePauseAnimation() { }

    TestablePauseAnimation(int duration)
        : QPauseAnimationJob(duration), m_updateCurrentTimeCount(0)
    {
    }

    int m_updateCurrentTimeCount = 0;
protected:
    void updateCurrentTime(int currentTime) override
    {
        QPauseAnimationJob::updateCurrentTime(currentTime);
        ++m_updateCurrentTimeCount;
    }
};

class TestableGenericAnimation : public QAbstractAnimationJob
{
public:
    TestableGenericAnimation(int duration = 250) : m_duration(duration) {}
    int duration() const override { return m_duration; }

private:
    int m_duration;
};

class EnableConsistentTiming
{
public:
    EnableConsistentTiming()
    {
        QUnifiedTimer *timer = QUnifiedTimer::instance();
        timer->setConsistentTiming(true);
    }
    ~EnableConsistentTiming()
    {
        QUnifiedTimer *timer = QUnifiedTimer::instance();
        timer->setConsistentTiming(false);
    }
};

class tst_QPauseAnimationJob : public QObject
{
  Q_OBJECT
public Q_SLOTS:
    void initTestCase();

private slots:
    void changeDirectionWhileRunning();
    void noTimerUpdates_data();
    void noTimerUpdates();
    void multiplePauseAnimations();
    void pauseAndPropertyAnimations();
    void pauseResume();
    void sequentialPauseGroup();
    void sequentialGroupWithPause();
    void multipleSequentialGroups();
    void zeroDuration();
};

void tst_QPauseAnimationJob::initTestCase()
{
//    qRegisterMetaType<QAbstractAnimationJob::State>("QAbstractAnimationJob::State");
}

void tst_QPauseAnimationJob::changeDirectionWhileRunning()
{
    EnableConsistentTiming enabled;

    TestablePauseAnimation animation;
    animation.setDuration(400);
    animation.start();
    QTest::qWait(100);
    QCOMPARE(animation.state(), QAbstractAnimationJob::Running);
    animation.setDirection(QAbstractAnimationJob::Backward);
    QTest::qWait(animation.totalDuration() + 100);
    QCOMPARE(animation.state(), QAbstractAnimationJob::Stopped);
}

void tst_QPauseAnimationJob::noTimerUpdates_data()
{
    QTest::addColumn<int>("duration");
    QTest::addColumn<int>("loopCount");

    QTest::newRow("0") << 200 << 1;
    QTest::newRow("1") << 160 << 1;
    QTest::newRow("2") << 160 << 2;
    QTest::newRow("3") << 200 << 3;
}

void tst_QPauseAnimationJob::noTimerUpdates()
{
    EnableConsistentTiming enabled;

    QFETCH(int, duration);
    QFETCH(int, loopCount);

    TestablePauseAnimation animation;
    animation.setDuration(duration);
    animation.setLoopCount(loopCount);
    animation.start();
    QTest::qWait(animation.totalDuration() + 100);

#if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
    if (animation.state() != QAbstractAnimationJob::Stopped)
        QEXPECT_FAIL("", timerError, Abort);
#endif

    QCOMPARE(animation.state(), QAbstractAnimationJob::Stopped);
    const int expectedLoopCount = 1 + loopCount;

#ifdef Q_OS_WIN
    if (animation.m_updateCurrentTimeCount != expectedLoopCount)
        QEXPECT_FAIL("", timerError, Abort);
#endif
    QCOMPARE(animation.m_updateCurrentTimeCount, expectedLoopCount);
}

void tst_QPauseAnimationJob::multiplePauseAnimations()
{
    EnableConsistentTiming enabled;

    TestablePauseAnimation animation;
    animation.setDuration(200);

    TestablePauseAnimation animation2;
    animation2.setDuration(800);

    animation.start();
    animation2.start();
    QTest::qWait(animation.totalDuration() + 100);

#if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
    if (animation.state() != QAbstractAnimationJob::Stopped)
        QEXPECT_FAIL("", timerError, Abort);
#endif
    QCOMPARE(animation.state(), QAbstractAnimationJob::Stopped);

#ifdef Q_OS_WIN
    if (animation2.state() != QAbstractAnimationJob::Running)
        QEXPECT_FAIL("", timerError, Abort);
#endif
    QCOMPARE(animation2.state(), QAbstractAnimationJob::Running);

#ifdef Q_OS_WIN
    if (animation.m_updateCurrentTimeCount != 2)
        QEXPECT_FAIL("", timerError, Abort);
#endif
    QCOMPARE(animation.m_updateCurrentTimeCount, 2);

#ifdef Q_OS_WIN
    if (animation2.m_updateCurrentTimeCount != 2)
        QEXPECT_FAIL("", timerError, Abort);
#endif
    QCOMPARE(animation2.m_updateCurrentTimeCount, 2);

    QTRY_COMPARE(animation2.state(), QAbstractAnimationJob::Stopped);
    QVERIFY(animation2.m_updateCurrentTimeCount >= 3);
}

void tst_QPauseAnimationJob::pauseAndPropertyAnimations()
{
    EnableConsistentTiming enabled;

    TestablePauseAnimation pause;
    pause.setDuration(200);

    TestableGenericAnimation animation;

    pause.start();

    QTest::qWait(100);
    animation.start();

    QCOMPARE(animation.state(), QAbstractAnimationJob::Running);

    QTRY_COMPARE(animation.state(), QAbstractAnimationJob::Running);
    QCOMPARE(pause.state(), QAbstractAnimationJob::Running);
    QVERIFY2(pause.m_updateCurrentTimeCount >= 2,
             QByteArrayLiteral("pause.m_updateCurrentTimeCount=") + QByteArray::number(pause.m_updateCurrentTimeCount));

    QTRY_COMPARE(animation.state(), QAbstractAnimationJob::Stopped);
    QCOMPARE(pause.state(), QAbstractAnimationJob::Stopped);
    QVERIFY2(pause.m_updateCurrentTimeCount > 3,
             QByteArrayLiteral("pause.m_updateCurrentTimeCount=") + QByteArray::number(pause.m_updateCurrentTimeCount));
}

void tst_QPauseAnimationJob::pauseResume()
{
    TestablePauseAnimation animation;
    animation.setDuration(400);
    animation.start();
    QCOMPARE(animation.state(), QAbstractAnimationJob::Running);
    QTest::qWait(200);
    animation.pause();
    QCOMPARE(animation.state(), QAbstractAnimationJob::Paused);
    animation.start();
    QTest::qWait(300);
    QTRY_COMPARE(animation.state(), QAbstractAnimationJob::Stopped);
    QVERIFY2(animation.m_updateCurrentTimeCount >= 3,
            QByteArrayLiteral("animation.m_updateCurrentTimeCount=") + QByteArray::number(animation.m_updateCurrentTimeCount));
}

void tst_QPauseAnimationJob::sequentialPauseGroup()
{
    QSequentialAnimationGroupJob group;

    TestablePauseAnimation animation1(200);
    group.appendAnimation(&animation1);
    TestablePauseAnimation animation2(200);
    group.appendAnimation(&animation2);
    TestablePauseAnimation animation3(200);
    group.appendAnimation(&animation3);

    group.start();
    QCOMPARE(animation1.m_updateCurrentTimeCount, 1);
    QCOMPARE(animation2.m_updateCurrentTimeCount, 0);
    QCOMPARE(animation3.m_updateCurrentTimeCount, 0);

    QCOMPARE(group.state(), QAbstractAnimationJob::Running);
    QCOMPARE(animation1.state(), QAbstractAnimationJob::Running);
    QCOMPARE(animation2.state(), QAbstractAnimationJob::Stopped);
    QCOMPARE(animation3.state(), QAbstractAnimationJob::Stopped);

    group.setCurrentTime(250);
    QCOMPARE(animation1.m_updateCurrentTimeCount, 2);
    QCOMPARE(animation2.m_updateCurrentTimeCount, 1);
    QCOMPARE(animation3.m_updateCurrentTimeCount, 0);

    QCOMPARE(group.state(), QAbstractAnimationJob::Running);
    QCOMPARE(animation1.state(), QAbstractAnimationJob::Stopped);
    QCOMPARE((QAbstractAnimationJob*)&animation2, group.currentAnimation());
    QCOMPARE(animation2.state(), QAbstractAnimationJob::Running);
    QCOMPARE(animation3.state(), QAbstractAnimationJob::Stopped);

    group.setCurrentTime(500);
    QCOMPARE(animation1.m_updateCurrentTimeCount, 2);
    QCOMPARE(animation2.m_updateCurrentTimeCount, 2);
    QCOMPARE(animation3.m_updateCurrentTimeCount, 1);

    QCOMPARE(group.state(), QAbstractAnimationJob::Running);
    QCOMPARE(animation1.state(), QAbstractAnimationJob::Stopped);
    QCOMPARE(animation2.state(), QAbstractAnimationJob::Stopped);
    QCOMPARE((QAbstractAnimationJob*)&animation3, group.currentAnimation());
    QCOMPARE(animation3.state(), QAbstractAnimationJob::Running);

    group.setCurrentTime(750);

    QCOMPARE(group.state(), QAbstractAnimationJob::Stopped);
    QCOMPARE(animation1.state(), QAbstractAnimationJob::Stopped);
    QCOMPARE(animation2.state(), QAbstractAnimationJob::Stopped);
    QCOMPARE(animation3.state(), QAbstractAnimationJob::Stopped);

    QCOMPARE(animation1.m_updateCurrentTimeCount, 2);
    QCOMPARE(animation2.m_updateCurrentTimeCount, 2);
    QCOMPARE(animation3.m_updateCurrentTimeCount, 2);
}

void tst_QPauseAnimationJob::sequentialGroupWithPause()
{
    QSequentialAnimationGroupJob group;

    TestableGenericAnimation animation;
    group.appendAnimation(&animation);

    TestablePauseAnimation pause;
    pause.setDuration(250);
    group.appendAnimation(&pause);

    group.start();

    QCOMPARE(group.state(), QAbstractAnimationJob::Running);
    QCOMPARE(animation.state(), QAbstractAnimationJob::Running);
    QCOMPARE(pause.state(), QAbstractAnimationJob::Stopped);

    group.setCurrentTime(300);

    QCOMPARE(group.state(), QAbstractAnimationJob::Running);
    QCOMPARE(animation.state(), QAbstractAnimationJob::Stopped);
    QCOMPARE((QAbstractAnimationJob*)&pause, group.currentAnimation());
    QCOMPARE(pause.state(), QAbstractAnimationJob::Running);

    group.setCurrentTime(600);

    QCOMPARE(group.state(), QAbstractAnimationJob::Stopped);
    QCOMPARE(animation.state(), QAbstractAnimationJob::Stopped);
    QCOMPARE(pause.state(), QAbstractAnimationJob::Stopped);

    QCOMPARE(pause.m_updateCurrentTimeCount, 2);
}

void tst_QPauseAnimationJob::multipleSequentialGroups()
{
    EnableConsistentTiming enabled;

    QParallelAnimationGroupJob group;
    group.setLoopCount(2);

    QSequentialAnimationGroupJob subgroup1;
    group.appendAnimation(&subgroup1);

    TestableGenericAnimation animation(300);
    subgroup1.appendAnimation(&animation);

    TestablePauseAnimation pause(200);
    subgroup1.appendAnimation(&pause);

    QSequentialAnimationGroupJob subgroup2;
    group.appendAnimation(&subgroup2);

    TestableGenericAnimation animation2(200);
    subgroup2.appendAnimation(&animation2);

    TestablePauseAnimation pause2(250);
    subgroup2.appendAnimation(&pause2);

    QSequentialAnimationGroupJob subgroup3;
    group.appendAnimation(&subgroup3);

    TestablePauseAnimation pause3(400);
    subgroup3.appendAnimation(&pause3);

    TestableGenericAnimation animation3(200);
    subgroup3.appendAnimation(&animation3);

    QSequentialAnimationGroupJob subgroup4;
    group.appendAnimation(&subgroup4);

    TestablePauseAnimation pause4(310);
    subgroup4.appendAnimation(&pause4);

    TestablePauseAnimation pause5(60);
    subgroup4.appendAnimation(&pause5);

    group.start();

    QCOMPARE(group.state(), QAbstractAnimationJob::Running);
    QCOMPARE(subgroup1.state(), QAbstractAnimationJob::Running);
    QCOMPARE(subgroup2.state(), QAbstractAnimationJob::Running);
    QCOMPARE(subgroup3.state(), QAbstractAnimationJob::Running);
    QCOMPARE(subgroup4.state(), QAbstractAnimationJob::Running);

    // This is a pretty long animation so it tends to get rather out of sync
    // when using the consistent timer, so run for an extra half second for good
    // measure...
    QTest::qWait(group.totalDuration() + 500);

#ifdef Q_OS_WIN
    if (group.state() != QAbstractAnimationJob::Stopped)
        QEXPECT_FAIL("", timerError, Abort);
    QCOMPARE(group.state(), QAbstractAnimationJob::Stopped);
#else
    QTRY_COMPARE(group.state(), QAbstractAnimationJob::Stopped);
#endif

#ifdef Q_OS_WIN
    if (subgroup1.state() != QAbstractAnimationJob::Stopped)
        QEXPECT_FAIL("", timerError, Abort);
#endif
    QCOMPARE(subgroup1.state(), QAbstractAnimationJob::Stopped);

#ifdef Q_OS_WIN
    if (subgroup2.state() != QAbstractAnimationJob::Stopped)
        QEXPECT_FAIL("", timerError, Abort);
#endif
    QCOMPARE(subgroup2.state(), QAbstractAnimationJob::Stopped);

#ifdef Q_OS_WIN
    if (subgroup3.state() != QAbstractAnimationJob::Stopped)
        QEXPECT_FAIL("", timerError, Abort);
#endif
    QCOMPARE(subgroup3.state(), QAbstractAnimationJob::Stopped);

#ifdef Q_OS_WIN
    if (subgroup4.state() != QAbstractAnimationJob::Stopped)
        QEXPECT_FAIL("", timerError, Abort);
#endif
    QCOMPARE(subgroup4.state(), QAbstractAnimationJob::Stopped);

#if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
    if (pause5.m_updateCurrentTimeCount != 4)
        QEXPECT_FAIL("", timerError, Abort);
#endif
    QCOMPARE(pause5.m_updateCurrentTimeCount, 4);
}

void tst_QPauseAnimationJob::zeroDuration()
{
    TestablePauseAnimation animation;
    animation.setDuration(0);
    animation.start();
    QTest::qWait(animation.totalDuration() + 100);
    QCOMPARE(animation.state(), QAbstractAnimationJob::Stopped);
    QCOMPARE(animation.m_updateCurrentTimeCount, 1);
}

QTEST_MAIN(tst_QPauseAnimationJob)
#include "tst_qpauseanimationjob.moc"
