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

#include <QtTest/QtTest>

#include <QtQml/private/qparallelanimationgroupjob_p.h>

Q_DECLARE_METATYPE(QAbstractAnimationJob::State)

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

private slots:
    void construction();
    void setCurrentTime();
    void stateChanged();
    void clearGroup();
    void propagateGroupUpdateToChildren();
    void updateChildrenWithRunningGroup();
    void deleteChildrenWithRunningGroup();
    void startChildrenWithStoppedGroup();
    void stopGroupWithRunningChild();
    void startGroupWithRunningChild();
    void zeroDurationAnimation();
    void stopUncontrolledAnimations();
    void uncontrolledWithLoops();
    void loopCount_data();
    void loopCount();
    void addAndRemoveDuration();
    void pauseResume();

    void crashWhenRemovingUncontrolledAnimation();
};

void tst_QParallelAnimationGroupJob::initTestCase()
{
    qRegisterMetaType<QAbstractAnimationJob::State>("QAbstractAnimationJob::State");
#if defined(Q_OS_DARWIN)
    // give the Apple application's start event queue time to clear
    QTest::qWait(1000);
#endif
}

void tst_QParallelAnimationGroupJob::construction()
{
    QParallelAnimationGroupJob animationgroup;
}

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

private:
    int m_duration;
};

class UncontrolledAnimation : public QObject, public QAbstractAnimationJob
{
    Q_OBJECT
public:
    UncontrolledAnimation()
    {
    }

    int duration() const override { return -1; /* not time driven */ }

protected:
    void timerEvent(QTimerEvent *event) override
    {
        if (event->timerId() == id)
            stop();
    }

    void updateRunning(bool running)
    {
        if (running) {
            id = startTimer(500);
        } else {
            killTimer(id);
            id = 0;
        }
    }

private:
    int id = 0;
};

class StateChangeListener: public QAnimationJobChangeListener
{
public:
    void animationStateChanged(QAbstractAnimationJob *, QAbstractAnimationJob::State newState, QAbstractAnimationJob::State) override
    {
        states << newState;
    }

    void clear() { states.clear(); }
    int count() { return states.size(); }

    QList<QAbstractAnimationJob::State> states;
};

class FinishedListener: public QAnimationJobChangeListener
{
public:
    FinishedListener() {}

    void animationFinished(QAbstractAnimationJob *) override { ++m_count; }
    void clear() { m_count = 0; }
    int count() { return m_count; }

private:
    int m_count = 0;
};

void tst_QParallelAnimationGroupJob::setCurrentTime()
{
    // originally was parallel operating on different object/properties
    QAnimationGroupJob *parallel = new QParallelAnimationGroupJob();
    TestAnimation *a1_p_o1 = new TestAnimation;
    TestAnimation *a1_p_o2 = new TestAnimation;
    TestAnimation *a1_p_o3 = new TestAnimation;
    a1_p_o2->setLoopCount(3);
    parallel->appendAnimation(a1_p_o1);
    parallel->appendAnimation(a1_p_o2);
    parallel->appendAnimation(a1_p_o3);

    UncontrolledAnimation *notTimeDriven = new UncontrolledAnimation;
    QCOMPARE(notTimeDriven->totalDuration(), -1);

    TestAnimation *loopsForever = new TestAnimation;
    loopsForever->setLoopCount(-1);
    QCOMPARE(loopsForever->totalDuration(), -1);

    QParallelAnimationGroupJob group;
    group.appendAnimation(parallel);
    group.appendAnimation(notTimeDriven);
    group.appendAnimation(loopsForever);

    // Current time = 1
    group.setCurrentTime(1);
    QCOMPARE(group.state(), QAnimationGroupJob::Stopped);
    QCOMPARE(parallel->state(), QAnimationGroupJob::Stopped);
    QCOMPARE(a1_p_o1->state(), QAnimationGroupJob::Stopped);
    QCOMPARE(a1_p_o2->state(), QAnimationGroupJob::Stopped);
    QCOMPARE(a1_p_o3->state(), QAnimationGroupJob::Stopped);
    QCOMPARE(notTimeDriven->state(), QAnimationGroupJob::Stopped);
    QCOMPARE(loopsForever->state(), QAnimationGroupJob::Stopped);

    QCOMPARE(group.currentLoopTime(), 1);
    QCOMPARE(a1_p_o1->currentLoopTime(), 1);
    QCOMPARE(a1_p_o2->currentLoopTime(), 1);
    QCOMPARE(a1_p_o3->currentLoopTime(), 1);
    QCOMPARE(notTimeDriven->currentLoopTime(), 1);
    QCOMPARE(loopsForever->currentLoopTime(), 1);

    // Current time = 250
    group.setCurrentTime(250);
    QCOMPARE(group.currentLoopTime(), 250);
    QCOMPARE(a1_p_o1->currentLoopTime(), 250);
    QCOMPARE(a1_p_o2->currentLoopTime(), 0);
    QCOMPARE(a1_p_o2->currentLoop(), 1);
    QCOMPARE(a1_p_o3->currentLoopTime(), 250);
    QCOMPARE(notTimeDriven->currentLoopTime(), 250);
    QCOMPARE(loopsForever->currentLoopTime(), 0);
    QCOMPARE(loopsForever->currentLoop(), 1);

    // Current time = 251
    group.setCurrentTime(251);
    QCOMPARE(group.currentLoopTime(), 251);
    QCOMPARE(a1_p_o1->currentLoopTime(), 250);
    QCOMPARE(a1_p_o2->currentLoopTime(), 1);
    QCOMPARE(a1_p_o2->currentLoop(), 1);
    QCOMPARE(a1_p_o3->currentLoopTime(), 250);
    QCOMPARE(notTimeDriven->currentLoopTime(), 251);
    QCOMPARE(loopsForever->currentLoopTime(), 1);
}

void tst_QParallelAnimationGroupJob::stateChanged()
{
    //this ensures that the correct animations are started when starting the group
    TestAnimation *anim1 = new TestAnimation(1000);
    TestAnimation *anim2 = new TestAnimation(2000);
    TestAnimation *anim3 = new TestAnimation(3000);
    TestAnimation *anim4 = new TestAnimation(3000);

    QParallelAnimationGroupJob group;
    group.appendAnimation(anim1);
    group.appendAnimation(anim2);
    group.appendAnimation(anim3);
    group.appendAnimation(anim4);

    StateChangeListener spy1;
    anim1->addAnimationChangeListener(&spy1, QAbstractAnimationJob::StateChange);
    StateChangeListener spy2;
    anim2->addAnimationChangeListener(&spy2, QAbstractAnimationJob::StateChange);
    StateChangeListener spy3;
    anim3->addAnimationChangeListener(&spy3, QAbstractAnimationJob::StateChange);
    StateChangeListener spy4;
    anim4->addAnimationChangeListener(&spy4, QAbstractAnimationJob::StateChange);

    //first; let's start forward
    group.start();
    //all the animations should be started
    QCOMPARE(spy1.count(), 1);
    QCOMPARE(spy1.states.last(), TestAnimation::Running);
    QCOMPARE(spy2.count(), 1);
    QCOMPARE(spy2.states.last(), TestAnimation::Running);
    QCOMPARE(spy3.count(), 1);
    QCOMPARE(spy3.states.last(), TestAnimation::Running);
    QCOMPARE(spy4.count(), 1);
    QCOMPARE(spy4.states.last(), TestAnimation::Running);

    group.setCurrentTime(1500); //anim1 should be finished
    QCOMPARE(group.state(), QAnimationGroupJob::Running);
    QCOMPARE(spy1.count(), 2);
    QCOMPARE(spy1.states.last(), TestAnimation::Stopped);
    QCOMPARE(spy2.count(), 1); //no change
    QCOMPARE(spy3.count(), 1); //no change
    QCOMPARE(spy4.count(), 1); //no change

    group.setCurrentTime(2500); //anim2 should be finished
    QCOMPARE(group.state(), QAnimationGroupJob::Running);
    QCOMPARE(spy1.count(), 2); //no change
    QCOMPARE(spy2.count(), 2);
    QCOMPARE(spy2.states.last(), TestAnimation::Stopped);
    QCOMPARE(spy3.count(), 1); //no change
    QCOMPARE(spy4.count(), 1); //no change

    group.setCurrentTime(3500); //everything should be finished
    QCOMPARE(group.state(), QAnimationGroupJob::Stopped);
    QCOMPARE(spy1.count(), 2); //no change
    QCOMPARE(spy2.count(), 2); //no change
    QCOMPARE(spy3.count(), 2);
    QCOMPARE(spy3.states.last(), TestAnimation::Stopped);
    QCOMPARE(spy4.count(), 2);
    QCOMPARE(spy4.states.last(), TestAnimation::Stopped);

    //cleanup
    spy1.clear();
    spy2.clear();
    spy3.clear();
    spy4.clear();

    //now let's try to reverse that
    group.setDirection(QAbstractAnimationJob::Backward);
    group.start();

    //only anim3 and anim4 should be started
    QCOMPARE(group.state(), QAnimationGroupJob::Running);
    QCOMPARE(spy1.count(), 0);
    QCOMPARE(spy2.count(), 0);
    QCOMPARE(spy3.count(), 1);
    QCOMPARE(spy3.states.last(), TestAnimation::Running);
    QCOMPARE(spy4.count(), 1);
    QCOMPARE(spy4.states.last(), TestAnimation::Running);

    group.setCurrentTime(1500); //anim2 should be started
    QCOMPARE(group.state(), QAnimationGroupJob::Running);
    QCOMPARE(spy1.count(), 0); //no change
    QCOMPARE(spy2.count(), 1);
    QCOMPARE(spy2.states.last(), TestAnimation::Running);
    QCOMPARE(spy3.count(), 1); //no change
    QCOMPARE(spy4.count(), 1); //no change

    group.setCurrentTime(500); //anim1 is finally also started
    QCOMPARE(group.state(), QAnimationGroupJob::Running);
    QCOMPARE(spy1.count(), 1);
    QCOMPARE(spy1.states.last(), TestAnimation::Running);
    QCOMPARE(spy2.count(), 1); //no change
    QCOMPARE(spy3.count(), 1); //no change
    QCOMPARE(spy4.count(), 1); //no change

    group.setCurrentTime(0); //everything should be stopped
    QCOMPARE(group.state(), QAnimationGroupJob::Stopped);
    QCOMPARE(spy1.count(), 2);
    QCOMPARE(spy1.states.last(), TestAnimation::Stopped);
    QCOMPARE(spy2.count(), 2);
    QCOMPARE(spy2.states.last(), TestAnimation::Stopped);
    QCOMPARE(spy3.count(), 2);
    QCOMPARE(spy3.states.last(), TestAnimation::Stopped);
    QCOMPARE(spy4.count(), 2);
    QCOMPARE(spy4.states.last(), TestAnimation::Stopped);
}

void tst_QParallelAnimationGroupJob::clearGroup()
{
    QParallelAnimationGroupJob group;
    static const int animationCount = 10;

    for (int i = 0; i < animationCount; ++i) {
        group.appendAnimation(new QParallelAnimationGroupJob);
    }

    QCOMPARE(group.children()->count(), animationCount);

    group.clear();

    QVERIFY(group.children()->isEmpty());
    QCOMPARE(group.currentLoopTime(), 0);
}

void tst_QParallelAnimationGroupJob::propagateGroupUpdateToChildren()
{
    // this test verifies if group state changes are updating its children correctly
    QParallelAnimationGroupJob group;

    TestAnimation anim1(100);
    TestAnimation anim2(200);

    QCOMPARE(group.state(), QAnimationGroupJob::Stopped);
    QCOMPARE(anim1.state(), QAnimationGroupJob::Stopped);
    QCOMPARE(anim2.state(), QAnimationGroupJob::Stopped);

    group.appendAnimation(&anim1);
    group.appendAnimation(&anim2);

    group.start();

    QCOMPARE(group.state(), QAnimationGroupJob::Running);
    QCOMPARE(anim1.state(), QAnimationGroupJob::Running);
    QCOMPARE(anim2.state(), QAnimationGroupJob::Running);

    group.pause();

    QCOMPARE(group.state(), QAnimationGroupJob::Paused);
    QCOMPARE(anim1.state(), QAnimationGroupJob::Paused);
    QCOMPARE(anim2.state(), QAnimationGroupJob::Paused);

    group.stop();

    QCOMPARE(group.state(), QAnimationGroupJob::Stopped);
    QCOMPARE(anim1.state(), QAnimationGroupJob::Stopped);
    QCOMPARE(anim2.state(), QAnimationGroupJob::Stopped);
}

void tst_QParallelAnimationGroupJob::updateChildrenWithRunningGroup()
{
    // assert that its possible to modify a child's state directly while their group is running
    QParallelAnimationGroupJob group;

    TestAnimation anim(200);

    StateChangeListener groupStateChangedSpy;
    group.addAnimationChangeListener(&groupStateChangedSpy, QAbstractAnimationJob::StateChange);
    StateChangeListener childStateChangedSpy;
    anim.addAnimationChangeListener(&childStateChangedSpy, QAbstractAnimationJob::StateChange);

    QCOMPARE(groupStateChangedSpy.count(), 0);
    QCOMPARE(childStateChangedSpy.count(), 0);
    QCOMPARE(group.state(), QAnimationGroupJob::Stopped);
    QCOMPARE(anim.state(), QAnimationGroupJob::Stopped);

    group.appendAnimation(&anim);

    group.start();

    QCOMPARE(group.state(), QAnimationGroupJob::Running);
    QCOMPARE(anim.state(), QAnimationGroupJob::Running);

    QCOMPARE(groupStateChangedSpy.count(), 1);
    QCOMPARE(childStateChangedSpy.count(), 1);

    QCOMPARE(groupStateChangedSpy.states.at(0), QAnimationGroupJob::Running);
    QCOMPARE(childStateChangedSpy.states.at(0), QAnimationGroupJob::Running);

    // starting directly a running child will not have any effect
    anim.start();

    QCOMPARE(groupStateChangedSpy.count(), 1);
    QCOMPARE(childStateChangedSpy.count(), 1);

    anim.pause();

    QCOMPARE(group.state(), QAnimationGroupJob::Running);
    QCOMPARE(anim.state(), QAnimationGroupJob::Paused);

    // in the animation stops directly, the group will still be running
    anim.stop();

    QCOMPARE(group.state(), QAnimationGroupJob::Running);
    QCOMPARE(anim.state(), QAnimationGroupJob::Stopped);

    //cleanup
    group.removeAnimationChangeListener(&groupStateChangedSpy, QAbstractAnimationJob::StateChange);
    anim.removeAnimationChangeListener(&childStateChangedSpy, QAbstractAnimationJob::StateChange);
}

void tst_QParallelAnimationGroupJob::deleteChildrenWithRunningGroup()
{
    // test if children can be activated when their group is stopped
    QParallelAnimationGroupJob group;

    std::unique_ptr<TestAnimation> anim1 = std::make_unique<TestAnimation>(200);
    group.appendAnimation(anim1.get());

    QCOMPARE(group.duration(), anim1->duration());

    group.start();
    QCOMPARE(group.state(), QAnimationGroupJob::Running);
    QCOMPARE(anim1->state(), QAnimationGroupJob::Running);

    QTRY_VERIFY(group.currentLoopTime() > 0);

    anim1.reset();
    QVERIFY(group.children()->isEmpty());
    QCOMPARE(group.duration(), 0);
    QCOMPARE(group.state(), QAnimationGroupJob::Stopped);
    QCOMPARE(group.currentLoopTime(), 0); //that's the invariant
}

void tst_QParallelAnimationGroupJob::startChildrenWithStoppedGroup()
{
    // test if children can be activated when their group is stopped
    QParallelAnimationGroupJob group;

    TestAnimation anim1(200);
    TestAnimation anim2(200);

    QCOMPARE(group.state(), QAnimationGroupJob::Stopped);
    QCOMPARE(anim1.state(), QAnimationGroupJob::Stopped);
    QCOMPARE(anim2.state(), QAnimationGroupJob::Stopped);

    group.appendAnimation(&anim1);
    group.appendAnimation(&anim2);

    group.stop();

    QCOMPARE(group.state(), QAnimationGroupJob::Stopped);
    QCOMPARE(anim1.state(), QAnimationGroupJob::Stopped);
    QCOMPARE(anim2.state(), QAnimationGroupJob::Stopped);

    anim1.start();
    anim2.start();
    anim2.pause();

    QCOMPARE(group.state(), QAnimationGroupJob::Stopped);
    QCOMPARE(anim1.state(), QAnimationGroupJob::Running);
    QCOMPARE(anim2.state(), QAnimationGroupJob::Paused);
}

void tst_QParallelAnimationGroupJob::stopGroupWithRunningChild()
{
    // children that started independently will not be affected by a group stop
    QParallelAnimationGroupJob group;

    TestAnimation anim1(200);
    TestAnimation anim2(200);

    QCOMPARE(group.state(), QAnimationGroupJob::Stopped);
    QCOMPARE(anim1.state(), QAnimationGroupJob::Stopped);
    QCOMPARE(anim2.state(), QAnimationGroupJob::Stopped);

    group.appendAnimation(&anim1);
    group.appendAnimation(&anim2);

    anim1.start();
    anim2.start();
    anim2.pause();

    QCOMPARE(group.state(), QAnimationGroupJob::Stopped);
    QCOMPARE(anim1.state(), QAnimationGroupJob::Running);
    QCOMPARE(anim2.state(), QAnimationGroupJob::Paused);

    group.stop();

    QCOMPARE(group.state(), QAnimationGroupJob::Stopped);
    QCOMPARE(anim1.state(), QAnimationGroupJob::Running);
    QCOMPARE(anim2.state(), QAnimationGroupJob::Paused);

    anim1.stop();
    anim2.stop();

    QCOMPARE(group.state(), QAnimationGroupJob::Stopped);
    QCOMPARE(anim1.state(), QAnimationGroupJob::Stopped);
    QCOMPARE(anim2.state(), QAnimationGroupJob::Stopped);
}

void tst_QParallelAnimationGroupJob::startGroupWithRunningChild()
{
    // as the group has precedence over its children, starting a group will restart all the children
    QParallelAnimationGroupJob group;

    TestAnimation anim1(200);
    TestAnimation anim2(200);

    StateChangeListener stateChangedSpy1;
    anim1.addAnimationChangeListener(&stateChangedSpy1, QAbstractAnimationJob::StateChange);
    StateChangeListener stateChangedSpy2;
    anim2.addAnimationChangeListener(&stateChangedSpy2, QAbstractAnimationJob::StateChange);

    QCOMPARE(stateChangedSpy1.count(), 0);
    QCOMPARE(stateChangedSpy2.count(), 0);
    QCOMPARE(group.state(), QAnimationGroupJob::Stopped);
    QCOMPARE(anim1.state(), QAnimationGroupJob::Stopped);
    QCOMPARE(anim2.state(), QAnimationGroupJob::Stopped);

    group.appendAnimation(&anim1);
    group.appendAnimation(&anim2);

    anim1.start();
    anim2.start();
    anim2.pause();

    QCOMPARE(stateChangedSpy1.states.at(0), QAnimationGroupJob::Running);
    QCOMPARE(stateChangedSpy2.states.at(0), QAnimationGroupJob::Running);
    QCOMPARE(stateChangedSpy2.states.at(1), QAnimationGroupJob::Paused);

    QCOMPARE(group.state(), QAnimationGroupJob::Stopped);
    QCOMPARE(anim1.state(), QAnimationGroupJob::Running);
    QCOMPARE(anim2.state(), QAnimationGroupJob::Paused);

    group.start();

    QCOMPARE(stateChangedSpy1.count(), 3);
    QCOMPARE(stateChangedSpy1.states.at(1), QAnimationGroupJob::Stopped);
    QCOMPARE(stateChangedSpy1.states.at(2), QAnimationGroupJob::Running);

    QCOMPARE(stateChangedSpy2.count(), 4);
    QCOMPARE(stateChangedSpy2.states.at(2), QAnimationGroupJob::Stopped);
    QCOMPARE(stateChangedSpy2.states.at(3), QAnimationGroupJob::Running);

    QCOMPARE(group.state(), QAnimationGroupJob::Running);
    QCOMPARE(anim1.state(), QAnimationGroupJob::Running);
    QCOMPARE(anim2.state(), QAnimationGroupJob::Running);

    //cleanup
    anim1.removeAnimationChangeListener(&stateChangedSpy1, QAbstractAnimationJob::StateChange);
    anim2.removeAnimationChangeListener(&stateChangedSpy2, QAbstractAnimationJob::StateChange);
}

void tst_QParallelAnimationGroupJob::zeroDurationAnimation()
{
    QParallelAnimationGroupJob group;

    TestAnimation anim1(0);
    TestAnimation anim2(100);
    TestAnimation anim3(10);

    StateChangeListener stateChangedSpy1;
    anim1.addAnimationChangeListener(&stateChangedSpy1, QAbstractAnimationJob::StateChange);
    FinishedListener finishedSpy1;
    anim1.addAnimationChangeListener(&finishedSpy1, QAbstractAnimationJob::Completion);

    StateChangeListener stateChangedSpy2;
    anim2.addAnimationChangeListener(&stateChangedSpy2, QAbstractAnimationJob::StateChange);
    FinishedListener finishedSpy2;
    anim2.addAnimationChangeListener(&finishedSpy2, QAbstractAnimationJob::Completion);

    StateChangeListener stateChangedSpy3;
    anim3.addAnimationChangeListener(&stateChangedSpy3, QAbstractAnimationJob::StateChange);
    FinishedListener finishedSpy3;
    anim3.addAnimationChangeListener(&finishedSpy3, QAbstractAnimationJob::Completion);

    group.appendAnimation(&anim1);
    group.appendAnimation(&anim2);
    group.appendAnimation(&anim3);
    QCOMPARE(stateChangedSpy1.count(), 0);
    group.start();
    QCOMPARE(stateChangedSpy1.count(), 2);
    QCOMPARE(finishedSpy1.count(), 1);
    QCOMPARE(stateChangedSpy1.states.at(0), QAnimationGroupJob::Running);
    QCOMPARE(stateChangedSpy1.states.at(1), QAnimationGroupJob::Stopped);

    QCOMPARE(stateChangedSpy2.count(), 1);
    QCOMPARE(finishedSpy2.count(), 0);
    QCOMPARE(stateChangedSpy1.states.at(0), QAnimationGroupJob::Running);

    QCOMPARE(stateChangedSpy3.count(), 1);
    QCOMPARE(finishedSpy3.count(), 0);
    QCOMPARE(stateChangedSpy3.states.at(0), QAnimationGroupJob::Running);

    QCOMPARE(anim1.state(), QAnimationGroupJob::Stopped);
    QCOMPARE(anim2.state(), QAnimationGroupJob::Running);
    QCOMPARE(anim3.state(), QAnimationGroupJob::Running);
    QCOMPARE(group.state(), QAnimationGroupJob::Running);

    group.stop();
    group.setLoopCount(4);
    stateChangedSpy1.clear();
    stateChangedSpy2.clear();
    stateChangedSpy3.clear();

    group.start();
    QCOMPARE(stateChangedSpy1.count(), 2);
    QCOMPARE(stateChangedSpy2.count(), 1);
    QCOMPARE(stateChangedSpy3.count(), 1);
    group.setCurrentTime(50);
    QCOMPARE(stateChangedSpy1.count(), 2);
    QCOMPARE(stateChangedSpy2.count(), 1);
    QCOMPARE(stateChangedSpy3.count(), 2);
    group.setCurrentTime(150);
    QCOMPARE(stateChangedSpy1.count(), 4);
    QCOMPARE(stateChangedSpy2.count(), 3);
    QCOMPARE(stateChangedSpy3.count(), 4);
    group.setCurrentTime(50);
    QCOMPARE(stateChangedSpy1.count(), 6);
    QCOMPARE(stateChangedSpy2.count(), 5);
    QCOMPARE(stateChangedSpy3.count(), 6);

    //cleanup
    anim1.removeAnimationChangeListener(&stateChangedSpy1, QAbstractAnimationJob::StateChange);
    anim1.removeAnimationChangeListener(&finishedSpy1, QAbstractAnimationJob::Completion);
    anim2.removeAnimationChangeListener(&stateChangedSpy2, QAbstractAnimationJob::StateChange);
    anim2.removeAnimationChangeListener(&finishedSpy2, QAbstractAnimationJob::Completion);
    anim3.removeAnimationChangeListener(&stateChangedSpy3, QAbstractAnimationJob::StateChange);
    anim3.removeAnimationChangeListener(&finishedSpy3, QAbstractAnimationJob::Completion);
}

void tst_QParallelAnimationGroupJob::stopUncontrolledAnimations()
{
    QParallelAnimationGroupJob group;

    TestAnimation anim1(0);

    UncontrolledAnimation notTimeDriven;
    QCOMPARE(notTimeDriven.totalDuration(), -1);

    TestAnimation loopsForever(100);
    loopsForever.setLoopCount(-1);

    StateChangeListener stateChangedSpy;
    anim1.addAnimationChangeListener(&stateChangedSpy, QAbstractAnimationJob::StateChange);

    group.appendAnimation(&anim1);
    group.appendAnimation(&notTimeDriven);
    group.appendAnimation(&loopsForever);

    group.start();

    QCOMPARE(stateChangedSpy.count(), 2);
    QCOMPARE(stateChangedSpy.states.at(0), QAnimationGroupJob::Running);
    QCOMPARE(stateChangedSpy.states.at(1), QAnimationGroupJob::Stopped);

    QCOMPARE(group.state(), QAnimationGroupJob::Running);
    QCOMPARE(notTimeDriven.state(), QAnimationGroupJob::Running);
    QCOMPARE(loopsForever.state(), QAnimationGroupJob::Running);
    QCOMPARE(anim1.state(), QAnimationGroupJob::Stopped);

    notTimeDriven.stop();

    QCOMPARE(group.state(), QAnimationGroupJob::Running);
    QCOMPARE(notTimeDriven.state(), QAnimationGroupJob::Stopped);
    QCOMPARE(loopsForever.state(), QAnimationGroupJob::Running);

    loopsForever.stop();

    QCOMPARE(group.state(), QAnimationGroupJob::Stopped);
    QCOMPARE(notTimeDriven.state(), QAnimationGroupJob::Stopped);
    QCOMPARE(loopsForever.state(), QAnimationGroupJob::Stopped);
}

struct AnimState {
    AnimState(int time = -1) : time(time) {}
    AnimState(int time, int state) : time(time), state(state) {}
    int time;
    int state = -1;
};

#define Running QAbstractAnimationJob::Running
#define Stopped QAbstractAnimationJob::Stopped

Q_DECLARE_METATYPE(AnimState)
void tst_QParallelAnimationGroupJob::loopCount_data()
{
    QTest::addColumn<bool>("directionBackward");
    QTest::addColumn<int>("setLoopCount");
    QTest::addColumn<int>("initialGroupTime");
    QTest::addColumn<int>("currentGroupTime");
    QTest::addColumn<AnimState>("expected1");
    QTest::addColumn<AnimState>("expected2");
    QTest::addColumn<AnimState>("expected3");

    //                                                                                  D U R A T I O N
    //                                                              100                           60*2                           0
    // direction = Forward
    QTest::newRow("50")  << false << 3 << 0 <<  50 << AnimState( 50, Running) << AnimState( 50, Running) << AnimState(  0, Stopped);
    QTest::newRow("100") << false << 3 << 0 << 100 << AnimState(100         ) << AnimState( 40, Running) << AnimState(  0, Stopped);
    QTest::newRow("110") << false << 3 << 0 << 110 << AnimState(100, Stopped) << AnimState( 50, Running) << AnimState(  0, Stopped);
    QTest::newRow("120") << false << 3 << 0 << 120 << AnimState(  0, Running) << AnimState(  0, Running) << AnimState(  0, Stopped);

    QTest::newRow("170") << false << 3 << 0 << 170 << AnimState( 50, Running) << AnimState( 50, Running) << AnimState(  0, Stopped);
    QTest::newRow("220") << false << 3 << 0 << 220 << AnimState(100         ) << AnimState( 40, Running) << AnimState(  0, Stopped);
    QTest::newRow("230") << false << 3 << 0 << 230 << AnimState(100, Stopped) << AnimState( 50, Running) << AnimState(  0, Stopped);
    QTest::newRow("240") << false << 3 << 0 << 240 << AnimState(  0, Running) << AnimState(  0, Running) << AnimState(  0, Stopped);

    QTest::newRow("290") << false << 3 << 0 << 290 << AnimState( 50, Running) << AnimState( 50, Running) << AnimState(  0, Stopped);
    QTest::newRow("340") << false << 3 << 0 << 340 << AnimState(100         ) << AnimState( 40, Running) << AnimState(  0, Stopped);
    QTest::newRow("350") << false << 3 << 0 << 350 << AnimState(100, Stopped) << AnimState( 50, Running) << AnimState(  0, Stopped);
    QTest::newRow("360") << false << 3 << 0 << 360 << AnimState(100, Stopped) << AnimState( 60         ) << AnimState(  0, Stopped);

    QTest::newRow("410") << false << 3 << 0 << 410 << AnimState(100, Stopped) << AnimState( 60, Stopped) << AnimState(  0, Stopped);
    QTest::newRow("460") << false << 3 << 0 << 460 << AnimState(100, Stopped) << AnimState( 60, Stopped) << AnimState(  0, Stopped);
    QTest::newRow("470") << false << 3 << 0 << 470 << AnimState(100, Stopped) << AnimState( 60, Stopped) << AnimState(  0, Stopped);
    QTest::newRow("480") << false << 3 << 0 << 480 << AnimState(100, Stopped) << AnimState( 60, Stopped) << AnimState(  0, Stopped);

    // direction = Forward, rewind
    QTest::newRow("120-110") << false << 3 << 120 << 110 << AnimState(   0, Stopped) << AnimState( 50, Running) << AnimState(  0, Stopped);
    QTest::newRow("120-50")  << false << 3 << 120 <<  50 << AnimState(  50, Running) << AnimState( 50, Running) << AnimState(  0, Stopped);
    QTest::newRow("120-0")   << false << 3 << 120 <<  0  << AnimState(   0, Running) << AnimState(  0, Running) << AnimState(  0, Stopped);
    QTest::newRow("300-110") << false << 3 << 300 << 110 << AnimState(   0, Stopped) << AnimState( 50, Running) << AnimState(  0, Stopped);
    QTest::newRow("300-50")  << false << 3 << 300 <<  50 << AnimState(  50, Running) << AnimState( 50, Running) << AnimState(  0, Stopped);
    QTest::newRow("300-0")   << false << 3 << 300 <<  0  << AnimState(   0, Running) << AnimState(  0, Running) << AnimState(  0, Stopped);
    QTest::newRow("115-105") << false << 3 << 115 << 105 << AnimState(  42, Stopped) << AnimState( 45, Running) << AnimState(  0, Stopped);

    // direction = Backward
    QTest::newRow("b120-120") << true << 3 << 120 << 120 << AnimState( 42, Stopped) << AnimState( 60, Running) << AnimState(  0, Stopped);
    QTest::newRow("b120-110") << true << 3 << 120 << 110 << AnimState( 42, Stopped) << AnimState( 50, Running) << AnimState(  0, Stopped);
    QTest::newRow("b120-100") << true << 3 << 120 << 100 << AnimState(100, Running) << AnimState( 40, Running) << AnimState(  0, Stopped);
    QTest::newRow("b120-50")  << true << 3 << 120 <<  50 << AnimState( 50, Running) << AnimState( 50, Running) << AnimState(  0, Stopped);
    QTest::newRow("b120-0")   << true << 3 << 120 <<   0 << AnimState(  0, Stopped) << AnimState(  0, Stopped) << AnimState(  0, Stopped);
    QTest::newRow("b360-170") << true << 3 << 360 << 170 << AnimState( 50, Running) << AnimState( 50, Running) << AnimState(  0, Stopped);
    QTest::newRow("b360-220") << true << 3 << 360 << 220 << AnimState(100, Running) << AnimState( 40, Running) << AnimState(  0, Stopped);
    QTest::newRow("b360-210") << true << 3 << 360 << 210 << AnimState( 90, Running) << AnimState( 30, Running) << AnimState(  0, Stopped);
    QTest::newRow("b360-120") << true << 3 << 360 << 120 << AnimState(  0, Stopped) << AnimState( 60, Running) << AnimState(  0, Stopped);

    // rewind, direction = Backward
    QTest::newRow("b50-110")  << true << 3 <<  50 << 110 << AnimState(100, Stopped) << AnimState( 50, Running) << AnimState(  0, Stopped);
    QTest::newRow("b50-120")  << true << 3 <<  50 << 120 << AnimState(100, Stopped) << AnimState( 60, Running) << AnimState(  0, Stopped);
    QTest::newRow("b50-140")  << true << 3 <<  50 << 140 << AnimState( 20, Running) << AnimState( 20, Running) << AnimState(  0, Stopped);
    QTest::newRow("b50-240")  << true << 3 <<  50 << 240 << AnimState(100, Stopped) << AnimState( 60, Running) << AnimState(  0, Stopped);
    QTest::newRow("b50-260")  << true << 3 <<  50 << 260 << AnimState( 20, Running) << AnimState( 20, Running) << AnimState(  0, Stopped);
    QTest::newRow("b50-350")  << true << 3 <<  50 << 350 << AnimState(100, Stopped) << AnimState( 50, Running) << AnimState(  0, Stopped);

    // infinite looping
    QTest::newRow("inf1220")  << false << -1 <<  0 << 1220 << AnimState( 20, Running) << AnimState( 20, Running) << AnimState(  0, Stopped);
    QTest::newRow("inf1310")  << false << -1 <<  0 << 1310 << AnimState( 100, Stopped) << AnimState( 50, Running) << AnimState(  0, Stopped);
    // infinite looping, direction = Backward (will only loop once)
    QTest::newRow("b.inf120-120") << true  << -1 << 120 << 120 << AnimState( 42, Stopped) << AnimState( 60, Running) << AnimState(  0, Stopped);
    QTest::newRow("b.inf120-20")  << true  << -1 << 120 <<  20 << AnimState( 20, Running) << AnimState( 20, Running) << AnimState(  0, Stopped);
    QTest::newRow("b.inf120-110") << true  << -1 << 120 << 110 << AnimState( 42, Stopped) << AnimState( 50, Running) << AnimState(  0, Stopped);


}

#undef Stopped
#undef Running

void tst_QParallelAnimationGroupJob::loopCount()
{
    QFETCH(bool, directionBackward);
    QFETCH(int, setLoopCount);
    QFETCH(int, initialGroupTime);
    QFETCH(int, currentGroupTime);
    QFETCH(AnimState, expected1);
    QFETCH(AnimState, expected2);
    QFETCH(AnimState, expected3);

    QParallelAnimationGroupJob group;

    TestAnimation anim1(100);
    TestAnimation anim2(60);  //total 120
    anim2.setLoopCount(2);
    TestAnimation anim3(0);

    group.appendAnimation(&anim1);
    group.appendAnimation(&anim2);
    group.appendAnimation(&anim3);

    group.setLoopCount(setLoopCount);
    if (initialGroupTime >= 0)
        group.setCurrentTime(initialGroupTime);
    if (directionBackward)
        group.setDirection(QAbstractAnimationJob::Backward);

    group.start();
    if (initialGroupTime >= 0)
        group.setCurrentTime(initialGroupTime);

    anim1.setCurrentTime(42);   // 42 is "untouched"
    anim2.setCurrentTime(42);

    group.setCurrentTime(currentGroupTime);

    QCOMPARE(anim1.currentLoopTime(), expected1.time);
    QCOMPARE(anim2.currentLoopTime(), expected2.time);
    QCOMPARE(anim3.currentLoopTime(), expected3.time);

    if (expected1.state >=0)
        QCOMPARE(int(anim1.state()), expected1.state);
    if (expected2.state >=0)
        QCOMPARE(int(anim2.state()), expected2.state);
    if (expected3.state >=0)
        QCOMPARE(int(anim3.state()), expected3.state);

}

void tst_QParallelAnimationGroupJob::addAndRemoveDuration()
{
    QParallelAnimationGroupJob group;
    QCOMPARE(group.duration(), 0);
    std::unique_ptr<TestAnimation> test = std::make_unique<TestAnimation>(250);      // 0, duration = 250;
    group.appendAnimation(test.get());
    QCOMPARE(test->group(), static_cast<QAnimationGroupJob*>(&group));
    QCOMPARE(test->duration(), 250);
    QCOMPARE(group.duration(), 250);

    std::unique_ptr<TestAnimation> test2 = std::make_unique<TestAnimation>(750);     // 1
    group.appendAnimation(test2.get());
    QCOMPARE(test2->group(), static_cast<QAnimationGroupJob*>(&group));
    QCOMPARE(group.duration(), 750);

    std::unique_ptr<TestAnimation> test3 = std::make_unique<TestAnimation>(500);     // 2
    group.appendAnimation(test3.get());
    QCOMPARE(test3->group(), static_cast<QAnimationGroupJob*>(&group));
    QCOMPARE(group.duration(), 750);

    group.removeAnimation(test2.get());    // remove the one with duration = 750
    test2.reset();
    QCOMPARE(group.duration(), 500);

    group.removeAnimation(test3.get());    // remove the one with duration = 500
    test3.reset();
    QCOMPARE(group.duration(), 250);

    group.removeAnimation(test.get());    // remove the last one (with duration = 250)
    QCOMPARE(test->group(), static_cast<QAnimationGroupJob*>(nullptr));
    QCOMPARE(group.duration(), 0);
    test.reset();
}

void tst_QParallelAnimationGroupJob::pauseResume()
{
#ifdef Q_CC_MINGW
    QSKIP("QTBUG-36290 - MinGW Animation tests are flaky.");
#endif
    QParallelAnimationGroupJob group;
    TestAnimation *anim = new TestAnimation(250);      // 0, duration = 250;
    group.appendAnimation(anim);
    StateChangeListener spy;
    anim->addAnimationChangeListener(&spy, QAbstractAnimationJob::StateChange);
    QCOMPARE(group.duration(), 250);
    group.start();
    QTest::qWait(100);
    QCOMPARE(group.state(), QAnimationGroupJob::Running);
    QCOMPARE(anim->state(), QAnimationGroupJob::Running);
    QCOMPARE(spy.count(), 1);
    spy.clear();
    const int currentTime = group.currentLoopTime();
    QCOMPARE(anim->currentLoopTime(), currentTime);

    group.pause();
    QCOMPARE(group.state(), QAnimationGroupJob::Paused);
    QCOMPARE(group.currentLoopTime(), currentTime);
    QCOMPARE(anim->state(), QAnimationGroupJob::Paused);
    QCOMPARE(anim->currentLoopTime(), currentTime);
    QCOMPARE(spy.count(), 1);
    spy.clear();

    group.resume();
    QCOMPARE(group.state(), QAnimationGroupJob::Running);
    QCOMPARE(group.currentLoopTime(), currentTime);
    QCOMPARE(anim->state(), QAnimationGroupJob::Running);
    QCOMPARE(anim->currentLoopTime(), currentTime);
    QCOMPARE(spy.count(), 1);

    group.stop();
    spy.clear();
    group.appendAnimation(new TestAnimation(500));
    group.start();
    QCOMPARE(spy.count(), 1); //the animation should have been started
    QCOMPARE(spy.states.at(0), TestAnimation::Running);
    group.setCurrentTime(250); //end of first animation
    QCOMPARE(spy.count(), 2); //the animation should have been stopped
    QCOMPARE(spy.states.at(1), TestAnimation::Stopped);
    group.pause();
    QCOMPARE(spy.count(), 2); //this shouldn't have changed
    group.resume();
    QCOMPARE(spy.count(), 2); //this shouldn't have changed
}

// This is a regression test for QTBUG-8910, where a crash occurred when the
// last animation was removed from a group.
void tst_QParallelAnimationGroupJob::crashWhenRemovingUncontrolledAnimation()
{
    QParallelAnimationGroupJob group;
    std::unique_ptr<TestAnimation> anim = std::make_unique<TestAnimation>();
    anim->setLoopCount(-1);
    std::unique_ptr<TestAnimation> anim2 = std::make_unique<TestAnimation>();
    anim2->setLoopCount(-1);
    group.appendAnimation(anim.get());
    group.appendAnimation(anim2.get());
    group.start();
    anim.reset();
    // it would crash here because the internals of the group would still have a reference to anim
    anim2.reset();
}

void tst_QParallelAnimationGroupJob::uncontrolledWithLoops()
{
    QParallelAnimationGroupJob group;

    TestAnimation *plain = new TestAnimation(100);
    TestAnimation *loopsForever = new TestAnimation();
    UncontrolledAnimation *notTimeBased = new UncontrolledAnimation();

    loopsForever->setLoopCount(-1);

    group.appendAnimation(plain);
    group.appendAnimation(loopsForever);
    group.appendAnimation(notTimeBased);

    StateChangeListener listener;
    group.addAnimationChangeListener(&listener, QAbstractAnimationJob::CurrentLoop);
    group.setLoopCount(2);

    group.start();

    QCOMPARE(group.currentLoop(), 0);
    QCOMPARE(group.state(), QAbstractAnimationJob::Running);
    QCOMPARE(plain->state(), QAbstractAnimationJob::Running);
    QCOMPARE(loopsForever->state(), QAbstractAnimationJob::Running);
    QCOMPARE(notTimeBased->state(), QAbstractAnimationJob::Running);

    loopsForever->stop();
    notTimeBased->stop();

    QTRY_COMPARE(group.currentLoop(), 1);
    QCOMPARE(group.state(), QAbstractAnimationJob::Running);
    QCOMPARE(loopsForever->state(), QAbstractAnimationJob::Running);
    QCOMPARE(notTimeBased->state(), QAbstractAnimationJob::Running);

    loopsForever->stop();
    notTimeBased->stop();

    QTRY_COMPARE(group.state(), QAbstractAnimationJob::Stopped);
}


QTEST_MAIN(tst_QParallelAnimationGroupJob)
#include "tst_qparallelanimationgroupjob.moc"
