// Copyright (C) 2017 Klaralvdalens Datakonsult AB (KDAB).
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

#include <QtTest/QTest>
#include <Qt3DAnimation/private/animationclip_p.h>
#include <Qt3DAnimation/private/animationutils_p.h>
#include <Qt3DAnimation/private/blendedclipanimator_p.h>
#include <Qt3DAnimation/private/clock_p.h>
#include <Qt3DAnimation/private/channelmapper_p.h>
#include <Qt3DAnimation/private/channelmapping_p.h>
#include <Qt3DAnimation/private/clipblendvalue_p.h>
#include <Qt3DAnimation/private/handler_p.h>
#include <Qt3DAnimation/private/additiveclipblend_p.h>
#include <Qt3DAnimation/private/lerpclipblend_p.h>
#include <Qt3DAnimation/private/managers_p.h>
#include <QtGui/qvector2d.h>
#include <QtGui/qvector3d.h>
#include <QtGui/qvector4d.h>
#include <QtGui/qquaternion.h>
#include <QtGui/qcolor.h>
#include <QtCore/qbitarray.h>

#include <qbackendnodetester.h>
#include <testarbiter.h>

using namespace Qt3DAnimation::Animation;

Q_DECLARE_METATYPE(Qt3DAnimation::Animation::Handler*)
Q_DECLARE_METATYPE(QVector<ChannelMapping *>)
Q_DECLARE_METATYPE(Clock *)
Q_DECLARE_METATYPE(ChannelMapper *)
Q_DECLARE_METATYPE(AnimationClip *)
Q_DECLARE_METATYPE(QVector<MappingData>)
Q_DECLARE_METATYPE(Channel)
Q_DECLARE_METATYPE(AnimatorEvaluationData)
Q_DECLARE_METATYPE(ClipEvaluationData)
Q_DECLARE_METATYPE(ClipAnimator *)
Q_DECLARE_METATYPE(BlendedClipAnimator *)
Q_DECLARE_METATYPE(QVector<ChannelNameAndType>)
Q_DECLARE_METATYPE(QVector<AnimationCallbackAndValue>)
Q_DECLARE_METATYPE(ClipFormat)
Q_DECLARE_METATYPE(ChannelNameAndType)

namespace {

class MeanBlendNode : public ClipBlendNode
{
public:
    MeanBlendNode()
        : ClipBlendNode(ClipBlendNode::LerpBlendType)
    {}

    void setValueNodeIds(Qt3DCore::QNodeId value1Id,
                         Qt3DCore::QNodeId value2Id)
    {
        m_value1Id = value1Id;
        m_value2Id = value2Id;
    }

    inline QList<Qt3DCore::QNodeId> allDependencyIds() const override
    {
        return currentDependencyIds();
    }

    QList<Qt3DCore::QNodeId> currentDependencyIds() const final
    {
        return { m_value1Id, m_value2Id };
    }

    using ClipBlendNode::setClipResults;

    double duration() const final { return 0.0f; }

protected:
    ClipResults doBlend(const QList<ClipResults> &blendData) const final
    {
        Q_ASSERT(blendData.size() == 2);
        const int elementCount = blendData.first().size();
        ClipResults blendResults(elementCount);

        for (int i = 0; i < elementCount; ++i)
            blendResults[i] = 0.5f * (blendData[0][i] + blendData[1][i]);

        return blendResults;
    }

private:
    Qt3DCore::QNodeId m_value1Id;
    Qt3DCore::QNodeId m_value2Id;
};

bool fuzzyCompare(float x1, float x2)
{
    if (qFuzzyIsNull(x1) && qFuzzyIsNull(x2)) {
        return true;
    } else if ((qFuzzyIsNull(x1) && !qFuzzyIsNull(x2)) ||
               (!qFuzzyIsNull(x1) && qFuzzyIsNull(x2))) {
        return false;
    } else {
        return qFuzzyCompare(x1, x2);
    }
}

class DummyCallback : public Qt3DAnimation::QAnimationCallback
{
public:
    void valueChanged(const QVariant &) override { }
};

} // anonymous


class tst_AnimationUtils : public Qt3DCore::QBackendNodeTester
{
    Q_OBJECT

public:
    ChannelMapping *createChannelMapping(Handler *handler,
                                         const QString &channelName,
                                         const Qt3DCore::QNodeId targetId,
                                         const char *propertyName,
                                         int type,
                                         int componentCount)
    {
        auto channelMappingId = Qt3DCore::QNodeId::createId();
        ChannelMapping *channelMapping = handler->channelMappingManager()->getOrCreateResource(channelMappingId);
        setPeerId(channelMapping, channelMappingId);
        channelMapping->setHandler(handler);
        channelMapping->setTargetId(targetId);
        channelMapping->setPropertyName(propertyName);
        channelMapping->setChannelName(channelName);
        channelMapping->setType(type);
        channelMapping->setMappingType(ChannelMapping::ChannelMappingType);
        channelMapping->setComponentCount(componentCount);
        return channelMapping;
    }

    ChannelMapping *createChannelMapping(Handler *handler,
                                         const Qt3DCore::QNodeId skeletonId)
    {
        auto channelMappingId = Qt3DCore::QNodeId::createId();
        ChannelMapping *channelMapping = handler->channelMappingManager()->getOrCreateResource(channelMappingId);
        setPeerId(channelMapping, channelMappingId);
        channelMapping->setHandler(handler);
        channelMapping->setSkeletonId(skeletonId);
        channelMapping->setMappingType(ChannelMapping::SkeletonMappingType);
        return channelMapping;
    }

    ChannelMapper *createChannelMapper(Handler *handler,
                                       const QList<Qt3DCore::QNodeId> &mappingIds)
    {
        auto channelMapperId = Qt3DCore::QNodeId::createId();
        ChannelMapper *channelMapper = handler->channelMapperManager()->getOrCreateResource(channelMapperId);
        setPeerId(channelMapper, channelMapperId);
        channelMapper->setMappingIds(mappingIds);
        return channelMapper;
    }

    AnimationClip *createAnimationClipLoader(Handler *handler,
                                                   const QUrl &source)
    {
        auto clipId = Qt3DCore::QNodeId::createId();
        AnimationClip *clip = handler->animationClipLoaderManager()->getOrCreateResource(clipId);
        setPeerId(clip, clipId);
        clip->setDataType(AnimationClip::File);
        clip->setSource(source);
        clip->loadAnimation();
        return clip;
    }

    ClipAnimator *createClipAnimator(Handler *handler,
                                     qint64 globalStartTimeNS,
                                     int loops)
    {
        auto animatorId = Qt3DCore::QNodeId::createId();
        ClipAnimator *animator = handler->clipAnimatorManager()->getOrCreateResource(animatorId);
        setPeerId(animator, animatorId);
        animator->setStartTime(globalStartTimeNS);
        animator->setLoops(loops);
        return animator;
    }

    BlendedClipAnimator *createBlendedClipAnimator(Handler *handler,
                                                   qint64 globalStartTimeNS,
                                                   int loops)
    {
        auto animatorId = Qt3DCore::QNodeId::createId();
        BlendedClipAnimator *animator = handler->blendedClipAnimatorManager()->getOrCreateResource(animatorId);
        setPeerId(animator, animatorId);
        animator->setStartTime(globalStartTimeNS);
        animator->setLoops(loops);
        return animator;
    }

    LerpClipBlend *createLerpClipBlend(Handler *handler)
    {
        auto lerpId = Qt3DCore::QNodeId::createId();
        LerpClipBlend *lerp = new LerpClipBlend();
        setPeerId(lerp, lerpId);
        lerp->setClipBlendNodeManager(handler->clipBlendNodeManager());
        lerp->setHandler(handler);
        handler->clipBlendNodeManager()->appendNode(lerpId, lerp);
        return lerp;
    }

    AdditiveClipBlend *createAdditiveClipBlend(Handler *handler)
    {
        auto additiveId = Qt3DCore::QNodeId::createId();
        AdditiveClipBlend *additive = new AdditiveClipBlend();
        setPeerId(additive, additiveId);
        additive->setClipBlendNodeManager(handler->clipBlendNodeManager());
        additive->setHandler(handler);
        handler->clipBlendNodeManager()->appendNode(additiveId, additive);
        return additive;
    }

    ClipBlendValue *createClipBlendValue(Handler *handler)
    {
        auto valueId = Qt3DCore::QNodeId::createId();
        ClipBlendValue *value = new ClipBlendValue();
        setPeerId(value, valueId);
        value->setClipBlendNodeManager(handler->clipBlendNodeManager());
        value->setHandler(handler);
        handler->clipBlendNodeManager()->appendNode(valueId, value);
        return value;
    }

    MeanBlendNode *createMeanBlendNode(Handler *handler)
    {
        auto id = Qt3DCore::QNodeId::createId();
        MeanBlendNode *node = new MeanBlendNode();
        setPeerId(node, id);
        node->setClipBlendNodeManager(handler->clipBlendNodeManager());
        node->setHandler(handler);
        handler->clipBlendNodeManager()->appendNode(id, node);
        return node;
    }

    Skeleton *createSkeleton(Handler *handler, int jointCount)
    {
        auto skeletonId = Qt3DCore::QNodeId::createId();
        Skeleton *skeleton = handler->skeletonManager()->getOrCreateResource(skeletonId);
        setPeerId(skeleton, skeletonId);
        skeleton->setJointCount(jointCount);
        return skeleton;
    }

private Q_SLOTS:
    void checkBuildPropertyMappings_data()
    {
        QTest::addColumn<Handler *>("handler");
        QTest::addColumn<QVector<ChannelMapping *>>("channelMappings");
        QTest::addColumn<QVector<ChannelNameAndType>>("channelNamesAndTypes");
        QTest::addColumn<QVector<ComponentIndices>>("channelComponentIndices");
        QTest::addColumn<QVector<QBitArray>>("sourceClipMask");
        QTest::addColumn<QVector<MappingData>>("expectedResults");

        // Single ChannelMapping
        {
            Handler *handler = new Handler();

            auto channelMapping = createChannelMapping(handler,
                                                       QLatin1String("Location"),
                                                       Qt3DCore::QNodeId::createId(),
                                                       "translation",
                                                       static_cast<int>(QMetaType::QVector3D),
                                                       3);

            const QVector<ChannelMapping *> channelMappings = { channelMapping };

            // Create a few channels in the format description
            const ChannelNameAndType rotation = { QLatin1String("Rotation"),
                                            static_cast<int>(QMetaType::QQuaternion),
                                            4,
                                            channelMapping->peerId() };
            const ChannelNameAndType location = { QLatin1String("Location"),
                                            static_cast<int>(QMetaType::QVector3D),
                                            3,
                                            channelMapping->peerId() };
            const ChannelNameAndType baseColor = { QLatin1String("BaseColor"),
                                            static_cast<int>(QMetaType::QVector3D),
                                            3,
                                            channelMapping->peerId() };
            const ChannelNameAndType metalness = { QLatin1String("Metalness"),
                                            static_cast<int>(QMetaType::Double),
                                            1,
                                            channelMapping->peerId() };
            const ChannelNameAndType roughness = { QLatin1String("Roughness"),
                                            static_cast<int>(QMetaType::Double),
                                            1,
                                            channelMapping->peerId() };
            const ChannelNameAndType morphTargetWeightsList = { QLatin1String("MorphTargetWeightsList"),
                                            static_cast<int>(QMetaType::QVariantList),
                                            5,
                                            channelMapping->peerId() };
            const ChannelNameAndType morphTargetWeightsVec = { QLatin1String("MorphTargetWeightsVec"),
                                            qMetaTypeId<QVector<float>>(),
                                            6,
                                            channelMapping->peerId() };
            const ChannelNameAndType rgbColor = { QLatin1String("rgbColor"),
                                            static_cast<int>(QMetaType::QColor),
                                            3,
                                            channelMapping->peerId() };
            const ChannelNameAndType rgbaColor = { QLatin1String("rgbaColor"),
                                            static_cast<int>(QMetaType::QColor),
                                            4,
                                            channelMapping->peerId() };

            const QVector<ChannelNameAndType> channelNamesAndTypes
                    = { rotation, location, baseColor, metalness, roughness,
                        morphTargetWeightsList, morphTargetWeightsVec, rgbColor, rgbaColor };

            // And the matching indices
            const ComponentIndices rotationIndices = { 0, 1, 2, 3 };
            const ComponentIndices locationIndices = { 4, 5, 6 };
            const ComponentIndices baseColorIndices = { 7, 8, 9 };
            const ComponentIndices metalnessIndices = { 10 };
            const ComponentIndices roughnessIndices = { 11 };
            const ComponentIndices morphTargetListIndices = { 12, 13, 14, 15, 16 };
            const ComponentIndices morphTargetVecIndices = { 17, 18, 19, 20, 21, 22 };
            const ComponentIndices rgbColorIndices = { 23, 24, 25 };
            const ComponentIndices rgbaColorIndices = { 26, 27, 28, 29 };
            const QVector<ComponentIndices> channelComponentIndices
                    = { rotationIndices, locationIndices, baseColorIndices,
                        metalnessIndices, roughnessIndices, morphTargetListIndices,
                        morphTargetVecIndices, rgbColorIndices, rgbaColorIndices };

            const QVector<QBitArray> sourceClipMask = { QBitArray(4, true),
                                                       QBitArray(3, true),
                                                       QBitArray(3, true),
                                                       QBitArray(1, true),
                                                       QBitArray(1, true),
                                                       QBitArray(5, true),
                                                       QBitArray(6, true),
                                                       QBitArray(3, true),
                                                       QBitArray(4, true) };

            MappingData expectedMapping;
            expectedMapping.targetId = channelMapping->targetId();
            expectedMapping.propertyName = channelMapping->propertyName();
            expectedMapping.type = channelMapping->type();
            expectedMapping.channelIndices = locationIndices;
            const QVector<MappingData> expectedResults = { expectedMapping };

            QTest::newRow("single mapping")
                    << handler
                    << channelMappings
                    << channelNamesAndTypes
                    << channelComponentIndices
                    << sourceClipMask
                    << expectedResults;
        }

        // Multiple ChannelMappings
        {
            Handler *handler = new Handler();

            auto locationMapping = createChannelMapping(handler,
                                                        QLatin1String("Location"),
                                                        Qt3DCore::QNodeId::createId(),
                                                        "translation",
                                                        static_cast<int>(QMetaType::QVector3D),
                                                        3);

            auto metalnessMapping = createChannelMapping(handler,
                                                         QLatin1String("Metalness"),
                                                         Qt3DCore::QNodeId::createId(),
                                                         "metalness",
                                                         static_cast<int>(QMetaType::Double),
                                                         1);

            auto baseColorMapping = createChannelMapping(handler,
                                                         QLatin1String("BaseColor"),
                                                         Qt3DCore::QNodeId::createId(),
                                                         "baseColor",
                                                         static_cast<int>(QMetaType::QVector3D),
                                                         3);

            auto roughnessMapping = createChannelMapping(handler,
                                                         QLatin1String("Roughness"),
                                                         Qt3DCore::QNodeId::createId(),
                                                         "roughness",
                                                         static_cast<int>(QMetaType::Double),
                                                         1);

            auto rotationMapping = createChannelMapping(handler,
                                                        QLatin1String("Rotation"),
                                                        Qt3DCore::QNodeId::createId(),
                                                        "rotation",
                                                        static_cast<int>(QMetaType::QQuaternion),
                                                        4);

            auto morphTargetMapping = createChannelMapping(handler,
                                                           QLatin1String("MorphTargetWeights"),
                                                           Qt3DCore::QNodeId::createId(),
                                                           "weights",
                                                           static_cast<int>(QMetaType::QVariantList),
                                                           5);


            const QVector<ChannelMapping *> channelMappings
                    = { locationMapping, metalnessMapping,
                        baseColorMapping, roughnessMapping,
                        rotationMapping, morphTargetMapping };

            // Create a few channels in the format description
            const ChannelNameAndType rotation = { QLatin1String("Rotation"),
                                            static_cast<int>(QMetaType::QQuaternion),
                                            4,
                                            rotationMapping->peerId() };
            const ChannelNameAndType location = { QLatin1String("Location"),
                                            static_cast<int>(QMetaType::QVector3D),
                                            3,
                                            locationMapping->peerId() };
            const ChannelNameAndType baseColor = { QLatin1String("BaseColor"),
                                             static_cast<int>(QMetaType::QVector3D),
                                             3,
                                             baseColorMapping->peerId() };
            const ChannelNameAndType metalness = { QLatin1String("Metalness"),
                                             static_cast<int>(QMetaType::Double),
                                             1,
                                             metalnessMapping->peerId() };
            const ChannelNameAndType roughness = { QLatin1String("Roughness"),
                                             static_cast<int>(QMetaType::Double),
                                             1,
                                             roughnessMapping->peerId() };
            const ChannelNameAndType morphTarget = { QLatin1String("MorphTargetWeights"),
                                               static_cast<int>(QMetaType::QVariantList),
                                               5,
                                               morphTargetMapping->peerId() };
            const QVector<ChannelNameAndType> channelNamesAndTypes
                    = { rotation, location, baseColor, metalness, roughness,
                        morphTarget };

            // And the matching indices
            const ComponentIndices rotationIndices = { 0, 1, 2, 3 };
            const ComponentIndices locationIndices = { 4, 5, 6 };
            const ComponentIndices baseColorIndices = { 7, 8, 9 };
            const ComponentIndices metalnessIndices = { 10 };
            const ComponentIndices roughnessIndices = { 11 };
            const ComponentIndices morphTargetIndices = { 12, 13, 14, 15, 16 };
            const QVector<ComponentIndices> channelComponentIndices
                    = { rotationIndices, locationIndices, baseColorIndices,
                        metalnessIndices, roughnessIndices, morphTargetIndices };

            const QVector<QBitArray> sourceClipMask = { QBitArray(4, true),
                                                       QBitArray(3, true),
                                                       QBitArray(3, true),
                                                       QBitArray(1, true),
                                                       QBitArray(1, true),
                                                       QBitArray(5, true) };

            MappingData expectedLocationMapping;
            expectedLocationMapping.targetId = locationMapping->targetId();
            expectedLocationMapping.propertyName = locationMapping->propertyName();
            expectedLocationMapping.type = locationMapping->type();
            expectedLocationMapping.channelIndices = locationIndices;

            MappingData expectedMetalnessMapping;
            expectedMetalnessMapping.targetId = metalnessMapping->targetId();
            expectedMetalnessMapping.propertyName = metalnessMapping->propertyName();
            expectedMetalnessMapping.type = metalnessMapping->type();
            expectedMetalnessMapping.channelIndices = metalnessIndices;

            MappingData expectedBaseColorMapping;
            expectedBaseColorMapping.targetId = baseColorMapping->targetId();
            expectedBaseColorMapping.propertyName = baseColorMapping->propertyName();
            expectedBaseColorMapping.type = baseColorMapping->type();
            expectedBaseColorMapping.channelIndices = baseColorIndices;

            MappingData expectedRoughnessMapping;
            expectedRoughnessMapping.targetId = roughnessMapping->targetId();
            expectedRoughnessMapping.propertyName = roughnessMapping->propertyName();
            expectedRoughnessMapping.type = roughnessMapping->type();
            expectedRoughnessMapping.channelIndices = roughnessIndices;

            MappingData expectedRotationMapping;
            expectedRotationMapping.targetId = rotationMapping->targetId();
            expectedRotationMapping.propertyName = rotationMapping->propertyName();
            expectedRotationMapping.type = rotationMapping->type();
            expectedRotationMapping.channelIndices = rotationIndices;

            MappingData expectedMorphTargetMapping;
            expectedMorphTargetMapping.targetId = morphTargetMapping->targetId();
            expectedMorphTargetMapping.propertyName = morphTargetMapping->propertyName();
            expectedMorphTargetMapping.type = morphTargetMapping->type();
            expectedMorphTargetMapping.channelIndices = morphTargetIndices;

            const QVector<MappingData> expectedResults
                    = { expectedLocationMapping,
                        expectedMetalnessMapping,
                        expectedBaseColorMapping,
                        expectedRoughnessMapping,
                        expectedRotationMapping,
                        expectedMorphTargetMapping };

            QTest::newRow("multiple mappings")
                    << handler
                    << channelMappings
                    << channelNamesAndTypes
                    << channelComponentIndices
                    << sourceClipMask
                    << expectedResults;
        }

        // Single skeleton mapping
        {
            Handler *handler = new Handler();
            const int jointCount = 4;
            auto skeleton = createSkeleton(handler, jointCount);
            auto channelMapping = createChannelMapping(handler, skeleton->peerId());

            const QVector<ChannelMapping *> channelMappings = { channelMapping };

            // Create a few channels in the format description
            QVector<ChannelNameAndType> channelNamesAndTypes;
            for (int i = 0; i < jointCount; ++i) {
                ChannelNameAndType locationDescription = { QLatin1String("Location"),
                                                           static_cast<int>(QMetaType::QVector3D),
                                                           3,
                                                           channelMapping->peerId() };
                locationDescription.jointIndex = i;
                locationDescription.jointTransformComponent = Translation;
                channelNamesAndTypes.push_back(locationDescription);

                ChannelNameAndType rotationDescription = { QLatin1String("Rotation"),
                                                           static_cast<int>(QMetaType::QQuaternion),
                                                           4,
                                                           channelMapping->peerId() };
                rotationDescription.jointIndex = i;
                rotationDescription.jointTransformComponent = Rotation;
                channelNamesAndTypes.push_back(rotationDescription);

                ChannelNameAndType scaleDescription = { QLatin1String("Scale"),
                                                        static_cast<int>(QMetaType::QVector3D),
                                                        3,
                                                        channelMapping->peerId() };
                scaleDescription.jointIndex = i;
                scaleDescription.jointTransformComponent = Scale;
                channelNamesAndTypes.push_back(scaleDescription);
            }

            // And the matching indices
            const QVector<ComponentIndices> channelComponentIndices = {
                {  0,  1,  2 }, {  3,  4,  5,  6 }, {  7,  8,  9 },
                { 10, 11, 12 }, { 13, 14, 15, 16 }, { 17, 18, 19 },
                { 20, 21, 22 }, { 23, 24, 25, 26 }, { 27, 28, 29 },
                { 30, 31, 32 }, { 33, 34, 35, 36 }, { 37, 38, 39 }
            };

            const QVector<QBitArray> sourceClipMask = { QBitArray(3, true),
                                                       QBitArray(4, true),
                                                       QBitArray(3, true),
                                                       QBitArray(3, true),
                                                       QBitArray(4, true),
                                                       QBitArray(3, true),
                                                       QBitArray(3, true),
                                                       QBitArray(4, true),
                                                       QBitArray(3, true),
                                                       QBitArray(3, true),
                                                       QBitArray(4, true),
                                                       QBitArray(3, true) };

            QVector<MappingData> expectedResults;
            int componentIndicesIndex = 0;
            for (int i = 0; i < jointCount; ++i) {
                MappingData locationMapping;
                locationMapping.targetId = channelMapping->skeletonId();
                locationMapping.propertyName = "translation";
                locationMapping.type = static_cast<int>(QMetaType::QVector3D);
                locationMapping.channelIndices = channelComponentIndices[componentIndicesIndex++];
                locationMapping.jointIndex = i;

                MappingData rotationMapping;
                rotationMapping.targetId = channelMapping->skeletonId();
                rotationMapping.propertyName = "rotation";
                rotationMapping.type = static_cast<int>(QMetaType::QQuaternion);
                rotationMapping.channelIndices = channelComponentIndices[componentIndicesIndex++];
                rotationMapping.jointIndex = i;

                MappingData scaleMapping;
                scaleMapping.targetId = channelMapping->skeletonId();
                scaleMapping.propertyName = "scale";
                scaleMapping.type = static_cast<int>(QMetaType::QVector3D);
                scaleMapping.channelIndices = channelComponentIndices[componentIndicesIndex++];
                scaleMapping.jointIndex = i;

                expectedResults << locationMapping << rotationMapping << scaleMapping;
            }

            QTest::newRow("single skeleton mapping")
                    << handler
                    << channelMappings
                    << channelNamesAndTypes
                    << channelComponentIndices
                    << sourceClipMask
                    << expectedResults;
        }
    }

    void checkBuildPropertyMappings()
    {
        // GIVEN
        QFETCH(Handler *, handler);
        QFETCH(QVector<ChannelMapping *>, channelMappings);
        QFETCH(QVector<ChannelNameAndType>, channelNamesAndTypes);
        QFETCH(QVector<ComponentIndices>, channelComponentIndices);
        QFETCH(QVector<QBitArray>, sourceClipMask);
        QFETCH(QVector<MappingData>, expectedResults);

        // WHEN
        const auto actualResults = buildPropertyMappings(channelMappings,
                                                         channelNamesAndTypes,
                                                         channelComponentIndices,
                                                         sourceClipMask);

        // THEN
        QCOMPARE(actualResults.size(), expectedResults.size());
        for (int i = 0; i < actualResults.size(); ++i) {
            const auto actualMapping = actualResults[i];
            const auto expectedMapping = expectedResults[i];

            QCOMPARE(actualMapping.targetId, expectedMapping.targetId);
            QCOMPARE(actualMapping.jointIndex, expectedMapping.jointIndex);
            QCOMPARE(actualMapping.propertyName, expectedMapping.propertyName);
            QCOMPARE(actualMapping.type, expectedMapping.type);
            QCOMPARE(actualMapping.channelIndices.size(), expectedMapping.channelIndices.size());
            for (int j = 0; j < actualMapping.channelIndices.size(); ++j) {
                QCOMPARE(actualMapping.channelIndices[j], expectedMapping.channelIndices[j]);
            }
        }

        // Cleanup
        delete handler;
    }

    void checkLocalTimeFromElapsedTime_data()
    {
        QTest::addColumn<double>("elapsedTime");
        QTest::addColumn<double>("currentTime");
        QTest::addColumn<double>("playbackRate");
        QTest::addColumn<double>("duration");
        QTest::addColumn<int>("loopCount");
        QTest::addColumn<int>("currentLoop");
        QTest::addColumn<double>("expectedLocalTime");
        QTest::addColumn<int>("expectedCurrentLoop");

        double elapsedTime;
        double currentTime;
        double playbackRate;
        double duration;
        int loopCount;
        int currentLoop;
        double expectedLocalTime;
        int expectedCurrentLoop;

        elapsedTime = 0.0;
        currentTime = 0.0;
        playbackRate = 1.0;
        duration = 1.0;
        loopCount = 1;
        currentLoop = 0;
        expectedLocalTime = 0.0;
        expectedCurrentLoop = 0;
        QTest::newRow("simple, t_current = 0, t_elapsed = 0, loop_current = 0")
                << elapsedTime << currentTime << playbackRate << duration << loopCount << currentLoop
                << expectedLocalTime << expectedCurrentLoop;

        elapsedTime = 0.5;
        currentTime = 0.0;
        playbackRate = 1.0;
        duration = 1.0;
        loopCount = 1;
        currentLoop = 0;
        expectedLocalTime = 0.5;
        expectedCurrentLoop = 0;
        QTest::newRow("simple, t_current = 0, t_elapsed = 0.5, loop_current = 0")
                << elapsedTime << currentTime << playbackRate << duration << loopCount << currentLoop
                << expectedLocalTime << expectedCurrentLoop;

        elapsedTime = 1.5;
        currentTime = 0.0;
        playbackRate = 1.0;
        duration = 1.0;
        loopCount = 1;
        currentLoop = 0;
        expectedLocalTime = 1.0;
        expectedCurrentLoop = 0;
        QTest::newRow("simple, t_current = 0, t_elapsed = 1.5, loop_current = 0")
                << elapsedTime << currentTime << playbackRate << duration << loopCount << currentLoop
                << expectedLocalTime << expectedCurrentLoop;

        elapsedTime = 0.5;
        currentTime = 0.6;
        playbackRate = 1.0;
        duration = 1.0;
        loopCount = 1;
        currentLoop = 0;
        expectedLocalTime = 1.0;
        expectedCurrentLoop = 0;
        QTest::newRow("simple, t_current = 0.5, t_elapsed = 0.6, loop_current = 0")
                << elapsedTime << currentTime << playbackRate << duration << loopCount << currentLoop
                << expectedLocalTime << expectedCurrentLoop;

        elapsedTime = 0.5;
        currentTime = 0.6;
        playbackRate = 1.0;
        duration = 1.0;
        loopCount = 2;
        currentLoop = 0;
        expectedLocalTime = 0.1;
        expectedCurrentLoop = 1;
        QTest::newRow("simple, t_current = 0.5, t_elapsed = 0.6, loop_current = 0, loop_count = 2")
                << elapsedTime << currentTime << playbackRate << duration << loopCount << currentLoop
                << expectedLocalTime << expectedCurrentLoop;

        elapsedTime = 0.5;
        currentTime = 0.6;
        playbackRate = 1.0;
        duration = 1.0;
        loopCount = 2;
        currentLoop = 1;
        expectedLocalTime = 1.0;
        expectedCurrentLoop = 1; // We clamp at end of final loop
        QTest::newRow("simple, t_current = 0.5, t_elapsed = 0.6, loop_current = 1, loop_count = 2")
                << elapsedTime << currentTime << playbackRate << duration << loopCount << currentLoop
                << expectedLocalTime << expectedCurrentLoop;

        elapsedTime = 0.5;
        currentTime = 0.6;
        playbackRate = 0.1;
        duration = 1.0;
        loopCount = 2;
        currentLoop = 1;
        expectedLocalTime = 0.65;
        expectedCurrentLoop = 1;
        QTest::newRow("simple, t_current = 0.5, t_elapsed = 0.6, loop_current = 1, loop_count = 2")
                << elapsedTime << currentTime << playbackRate << duration << loopCount << currentLoop
                << expectedLocalTime << expectedCurrentLoop;
    }

    void checkLocalTimeFromElapsedTime()
    {
        // GIVEN
        QFETCH(double, elapsedTime);
        QFETCH(double, currentTime);
        QFETCH(double, playbackRate);
        QFETCH(double, duration);
        QFETCH(int, loopCount);
        QFETCH(int, currentLoop);
        QFETCH(double, expectedLocalTime);
        QFETCH(int, expectedCurrentLoop);

        // WHEN
        int actualCurrentLoop = currentLoop;
        double actualLocalTime = localTimeFromElapsedTime(currentTime,
                                                          elapsedTime,
                                                          playbackRate,
                                                          duration,
                                                          loopCount,
                                                          actualCurrentLoop);

        // THEN
        QCOMPARE(actualCurrentLoop, expectedCurrentLoop);
        QCOMPARE(actualLocalTime, expectedLocalTime);
    }

    void checkPreparePropertyChanges_data()
    {
        QTest::addColumn<Qt3DCore::QNodeId>("animatorId");
        QTest::addColumn<QVector<MappingData>>("mappingData");
        QTest::addColumn<QVector<float>>("channelResults");
        QTest::addColumn<AnimationRecord>("expectedChanges");

        Qt3DCore::QNodeId animatorId;
        QVector<MappingData> mappingData;
        QVector<float> channelResults;
        AnimationRecord expectedChanges;

        // Single property, vec3
        {
            animatorId = Qt3DCore::QNodeId::createId();
            MappingData mapping;
            mapping.targetId = Qt3DCore::QNodeId::createId();
            mapping.propertyName = "translation";
            mapping.type = static_cast<int>(QMetaType::QVector3D);
            mapping.channelIndices = QVector<int> { 0, 1, 2 };
            mappingData.push_back(mapping);
            channelResults = QVector<float> { 1.0f, 2.0f, 3.0f };
            expectedChanges.normalizedTime = 1.1f; // Invalid
            expectedChanges.finalFrame = false;
            expectedChanges.targetChanges.push_back({mapping.targetId, mapping.propertyName, QVariant::fromValue(QVector3D(1.0f, 2.0f, 3.0f))});

            QTest::newRow("vec3 translation, final = false")
                    << animatorId << mappingData << channelResults << expectedChanges;

            expectedChanges.normalizedTime = 1.0f;
            expectedChanges.finalFrame = true;

            QTest::newRow("vec3 translation, final = true, normalizedTime = 1.0f")
                    << animatorId << mappingData << channelResults << expectedChanges;

            mappingData.clear();
            channelResults.clear();
            expectedChanges.targetChanges.clear();
        }

        // Multiple properties, all vec3
        {
            animatorId = Qt3DCore::QNodeId::createId();
            MappingData translationMapping;
            translationMapping.targetId = Qt3DCore::QNodeId::createId();
            translationMapping.propertyName = "translation";
            translationMapping.type = static_cast<int>(QMetaType::QVector3D);
            translationMapping.channelIndices = QVector<int> { 0, 1, 2 };
            mappingData.push_back(translationMapping);

            MappingData scaleMapping;
            scaleMapping.targetId = Qt3DCore::QNodeId::createId();
            scaleMapping.propertyName = "scale";
            scaleMapping.type = static_cast<int>(QMetaType::QVector3D);
            scaleMapping.channelIndices = QVector<int> { 3, 4, 5 };
            mappingData.push_back(scaleMapping);

            channelResults = QVector<float> { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f };
            expectedChanges.finalFrame = false;
            expectedChanges.normalizedTime = -0.1f; // Invalid

            expectedChanges.targetChanges.push_back({translationMapping.targetId, translationMapping.propertyName, QVariant::fromValue(QVector3D(1.0f, 2.0f, 3.0f))});
            expectedChanges.targetChanges.push_back({scaleMapping.targetId, scaleMapping.propertyName, QVariant::fromValue(QVector3D(4.0f, 5.0f, 6.0f))});

            QTest::newRow("vec3 translation, vec3 scale, final = false")
                    << animatorId << mappingData << channelResults << expectedChanges;

            expectedChanges.normalizedTime = 0.5f;
            expectedChanges.finalFrame = true;

            QTest::newRow("vec3 translation, vec3 scale, final = true")
                    << animatorId << mappingData << channelResults << expectedChanges;

            mappingData.clear();
            channelResults.clear();
            expectedChanges.targetChanges.clear();
        }

        // Single property, double
        {
            animatorId = Qt3DCore::QNodeId::createId();
            MappingData mapping;
            mapping.targetId = Qt3DCore::QNodeId::createId();
            mapping.propertyName = "mass";
            mapping.type = static_cast<int>(QMetaType::Double);
            mapping.channelIndices = QVector<int> { 0 };
            mappingData.push_back(mapping);
            channelResults = QVector<float> { 3.5f };
            expectedChanges.finalFrame = false;
            expectedChanges.normalizedTime = -1.0f; // Invalid
            expectedChanges.targetChanges.push_back({mapping.targetId, mapping.propertyName, QVariant::fromValue(3.5f)});

            QTest::newRow("double mass")
                    << animatorId << mappingData << channelResults << expectedChanges;

            mappingData.clear();
            channelResults.clear();
            expectedChanges.targetChanges.clear();
        }

        // Single property, vec2
        {
            animatorId = Qt3DCore::QNodeId::createId();
            MappingData mapping;
            mapping.targetId = Qt3DCore::QNodeId::createId();
            mapping.propertyName = "pos";
            mapping.type = static_cast<int>(QMetaType::QVector2D);
            mapping.channelIndices = QVector<int> { 0, 1 };
            mappingData.push_back(mapping);
            channelResults = QVector<float> { 2.0f, 1.0f };
            expectedChanges.finalFrame = false;
            expectedChanges.normalizedTime = 1.1f; // Invalid
            expectedChanges.targetChanges.push_back({mapping.targetId, mapping.propertyName, QVariant::fromValue(QVector2D(2.0f, 1.0f))});

            QTest::newRow("vec2 pos")
                    << animatorId << mappingData << channelResults << expectedChanges;

            mappingData.clear();
            channelResults.clear();
            expectedChanges.targetChanges.clear();
        }

        // Single property, vec4
        {
            animatorId = Qt3DCore::QNodeId::createId();
            MappingData mapping;
            mapping.targetId = Qt3DCore::QNodeId::createId();
            mapping.propertyName = "foo";
            mapping.type = static_cast<int>(QMetaType::QVector4D);
            mapping.channelIndices = QVector<int> { 0, 1, 2, 3 };
            mappingData.push_back(mapping);
            channelResults = QVector<float> { 4.0f, 3.0f, 2.0f, 1.0f };
            expectedChanges.finalFrame = false;
            expectedChanges.normalizedTime = 1.1f; // Invalid
            expectedChanges.targetChanges.push_back({mapping.targetId, mapping.propertyName, QVariant::fromValue(QVector4D(4.0f, 3.0f, 2.0f, 1.0f))});

            QTest::newRow("vec4 foo")
                    << animatorId << mappingData << channelResults << expectedChanges;

            mappingData.clear();
            channelResults.clear();
            expectedChanges.targetChanges.clear();
        }

        // Single property, quaternion
        {
            animatorId = Qt3DCore::QNodeId::createId();
            MappingData mapping;
            mapping.targetId = Qt3DCore::QNodeId::createId();
            mapping.propertyName = "rotation";
            mapping.type = static_cast<int>(QMetaType::QQuaternion);
            mapping.channelIndices = QVector<int> { 0, 1, 2, 3 };
            mappingData.push_back(mapping);
            channelResults = QVector<float> { 1.0f, 0.0f, 0.0f, 1.0f };
            expectedChanges.finalFrame = false;
            expectedChanges.normalizedTime = -0.1f; // Invalid
            expectedChanges.targetChanges.push_back({mapping.targetId, mapping.propertyName, QVariant::fromValue(QQuaternion(1.0f, 0.0f, 0.0f, 1.0f).normalized())});

            QTest::newRow("quaternion rotation")
                    << animatorId << mappingData << channelResults << expectedChanges;

            mappingData.clear();
            channelResults.clear();
            expectedChanges.targetChanges.clear();
        }

        // Single property, QColor
        {
            animatorId = Qt3DCore::QNodeId::createId();
            MappingData mapping;
            mapping.targetId = Qt3DCore::QNodeId::createId();
            mapping.propertyName = "color";
            mapping.type = static_cast<int>(QMetaType::QColor);
            mapping.channelIndices = QVector<int> { 0, 1, 2 };
            mappingData.push_back(mapping);
            channelResults = QVector<float> { 0.5f, 0.4f, 0.3f };
            expectedChanges.finalFrame = false;
            expectedChanges.normalizedTime = 1.1f; // Invalid
            expectedChanges.targetChanges.push_back({mapping.targetId, mapping.propertyName, QVariant::fromValue(QColor::fromRgbF(0.5f, 0.4f, 0.3f))});

            QTest::newRow("QColor rgb color")
                    << animatorId << mappingData << channelResults << expectedChanges;

            mappingData.clear();
            channelResults.clear();
            expectedChanges.targetChanges.clear();
        }

        // Single property, QColor
        {
            animatorId = Qt3DCore::QNodeId::createId();
            MappingData mapping;
            mapping.targetId = Qt3DCore::QNodeId::createId();
            mapping.propertyName = "color";
            mapping.type = static_cast<int>(QMetaType::QColor);
            mapping.channelIndices = QVector<int> { 0, 1, 2, 3 };
            mappingData.push_back(mapping);
            channelResults = QVector<float> { 0.5f, 0.4f, 0.3f, 0.2f };
            expectedChanges.finalFrame = false;
            expectedChanges.normalizedTime = 1.1f; // Invalid
            expectedChanges.targetChanges.push_back({mapping.targetId, mapping.propertyName, QVariant::fromValue(QColor::fromRgbF(0.5f, 0.4f, 0.3f, 0.2f))});

            QTest::newRow("QColor rgba color")
                    << animatorId << mappingData << channelResults << expectedChanges;

            mappingData.clear();
            channelResults.clear();
            expectedChanges.targetChanges.clear();
        }

        // Single property, QVariantList
        {
            animatorId = Qt3DCore::QNodeId::createId();
            MappingData mapping;
            mapping.targetId = Qt3DCore::QNodeId::createId();
            mapping.propertyName = "weights";
            mapping.type = static_cast<int>(QMetaType::QVariantList);
            mapping.channelIndices = QVector<int> { 0, 1, 2, 3, 4, 5, 6 };
            mappingData.push_back(mapping);
            channelResults = QVector<float> { 0.5f, 0.4f, 0.3f, 0.0f, 1.0f, 0.6f, 0.9f };
            expectedChanges.finalFrame = false;
            expectedChanges.normalizedTime = 1.1f; // Invalid
            QVariantList expectedValue = QVariantList { 0.5f, 0.4f, 0.3f, 0.0f, 1.0f, 0.6f, 0.9f };
            expectedChanges.targetChanges.push_back({mapping.targetId, mapping.propertyName, QVariant::fromValue(expectedValue)});

            QTest::newRow("QVariantList weights")
                    << animatorId << mappingData << channelResults << expectedChanges;

            mappingData.clear();
            channelResults.clear();
            expectedChanges.targetChanges.clear();
        }

    }

    void checkPreparePropertyChanges()
    {
        // GIVEN
        QFETCH(Qt3DCore::QNodeId, animatorId);
        QFETCH(QVector<MappingData>, mappingData);
        QFETCH(QVector<float>, channelResults);
        QFETCH(AnimationRecord, expectedChanges);

        // WHEN
        AnimationRecord actualChanges = prepareAnimationRecord(animatorId, mappingData, channelResults,
                                                               expectedChanges.finalFrame, expectedChanges.normalizedTime);

        // THEN
        QCOMPARE(actualChanges.targetChanges.size(), expectedChanges.targetChanges.size());
        for (int i = 0; i < actualChanges.targetChanges.size(); ++i) {
            const auto &expectedChange = expectedChanges.targetChanges[i];
            const auto &actualChange = actualChanges.targetChanges[i];

            QCOMPARE(actualChange.targetId, expectedChange.targetId);
            QCOMPARE(actualChange.propertyName, expectedChange.propertyName);
            QCOMPARE(actualChange.value, expectedChange.value);
        }
    }

    void checkPrepareCallbacks_data()
    {
        QTest::addColumn<QVector<MappingData>>("mappingData");
        QTest::addColumn<QVector<float>>("channelResults");
        QTest::addColumn<QVector<AnimationCallbackAndValue> >("expectedValues");

        QVector<MappingData> mappingData;
        QVector<float> channelResults;
        QVector<AnimationCallbackAndValue> expectedValues;

        // vec3
        {
            DummyCallback callback; // safe since the object is never used, just the address
            MappingData mapping;
            mapping.targetId = Qt3DCore::QNodeId::createId();
            mapping.propertyName = "translation";
            mapping.type = static_cast<int>(QMetaType::QVector3D);
            mapping.channelIndices = QVector<int> { 0, 1, 2 };
            mapping.callback = &callback;
            mapping.callbackFlags = {};
            mappingData.push_back(mapping);
            channelResults = QVector<float> { 1.0f, 2.0f, 3.0f };

            AnimationCallbackAndValue cbv;
            cbv.callback = mapping.callback;
            cbv.flags = mapping.callbackFlags;
            cbv.value = QVariant::fromValue<QVector3D>(QVector3D(1.0f, 2.0f, 3.0f));
            expectedValues.push_back(cbv);

            QTest::newRow("vec3 translation, no flags") << mappingData << channelResults << expectedValues;

            mappingData.clear();
            channelResults.clear();
            expectedValues.clear();
        }

        // double
        {
            DummyCallback callback;
            MappingData mapping;
            mapping.targetId = Qt3DCore::QNodeId::createId();
            mapping.propertyName = "something";
            mapping.type = static_cast<int>(QMetaType::Double);
            mapping.channelIndices = QVector<int> { 0 };
            mapping.callback = &callback;
            mapping.callbackFlags = {};
            mappingData.push_back(mapping);
            channelResults = QVector<float> { 1.0f };

            AnimationCallbackAndValue cbv;
            cbv.callback = mapping.callback;
            cbv.flags = mapping.callbackFlags;
            cbv.value = QVariant(double(1.0));
            expectedValues.push_back(cbv);

            QTest::newRow("double, no flags") << mappingData << channelResults << expectedValues;

            mappingData.clear();
            channelResults.clear();
            expectedValues.clear();
        }

        // float, set a flag
        {
            DummyCallback callback;
            MappingData mapping;
            mapping.targetId = Qt3DCore::QNodeId::createId();
            mapping.propertyName = "opacity";
            mapping.type = static_cast<int>(QMetaType::Float);
            mapping.channelIndices = QVector<int> { 0 };
            mapping.callback = &callback;
            mapping.callbackFlags = Qt3DAnimation::QAnimationCallback::OnThreadPool;
            mappingData.push_back(mapping);
            channelResults = QVector<float> { 0.5f };

            AnimationCallbackAndValue cbv;
            cbv.callback = mapping.callback;
            cbv.flags = mapping.callbackFlags;
            cbv.value = QVariant(float(0.5f));
            expectedValues.push_back(cbv);

            QTest::newRow("float, OnThreadPool") << mappingData << channelResults << expectedValues;

            mappingData.clear();
            channelResults.clear();
            expectedValues.clear();
        }
    }

    void checkPrepareCallbacks()
    {
        // GIVEN
        QFETCH(QVector<MappingData>, mappingData);
        QFETCH(QVector<float>, channelResults);
        QFETCH(QVector<AnimationCallbackAndValue>, expectedValues);

        // WHEN
        QVector<AnimationCallbackAndValue> callbacks = prepareCallbacks(mappingData, channelResults);

        // THEN
        QCOMPARE(callbacks.size(), expectedValues.size());
        for (int i = 0; i < callbacks.size(); ++i) {
            auto expected = expectedValues[i];
            auto actual = callbacks[i];

            QCOMPARE(actual.callback, expected.callback);
            QCOMPARE(actual.flags, expected.flags);
            QCOMPARE(actual.value, expected.value);
        }
    }

    void checkEvaluateClipAtLocalTime_data()
    {
        QTest::addColumn<Handler *>("handler");
        QTest::addColumn<AnimationClip *>("clip");
        QTest::addColumn<float>("localTime");
        QTest::addColumn<ClipResults>("expectedResults");

        Handler *handler;
        AnimationClip *clip;
        float localTime;
        ClipResults expectedResults;

        {
            handler = new Handler();
            clip = createAnimationClipLoader(handler, QUrl("qrc:/clip1.json"));
            localTime = 0.0f;
            expectedResults = QVector<float> { 0.0f, 0.0f, 0.0f };

            QTest::newRow("clip1.json, t = 0.0")
                    << handler << clip << localTime << expectedResults;
            expectedResults.clear();
        }

        {
            handler = new Handler();
            clip = createAnimationClipLoader(handler, QUrl("qrc:/clip1.json"));
            localTime = clip->duration();
            expectedResults = QVector<float> { 5.0f, 0.0f, 0.0f };

            QTest::newRow("clip1.json, t = duration")
                    << handler << clip << localTime << expectedResults;
            expectedResults.clear();
        }

        {
            handler = new Handler();
            clip = createAnimationClipLoader(handler, QUrl("qrc:/clip1.json"));
            localTime = clip->duration() / 2.0f;
            expectedResults = QVector<float> { 2.5f, 0.0f, 0.0f };

            QTest::newRow("clip1.json, t = duration/2")
                    << handler << clip << localTime << expectedResults;
            expectedResults.clear();
        }

        {
            handler = new Handler();
            clip = createAnimationClipLoader(handler, QUrl("qrc:/clip2.json"));
            localTime = 0.0f;
            expectedResults = QVector<float> { 0.0f, 0.0f, 0.0f,         // Translation
                                             1.0f, 0.0f, 0.0f, 0.0f }; // Rotation

            QTest::newRow("clip2.json, t = 0.0")
                    << handler << clip << localTime << expectedResults;
            expectedResults.clear();
        }
        {
            handler = new Handler();
            clip = createAnimationClipLoader(handler, QUrl("qrc:/clip2.json"));
            localTime = clip->duration();
            expectedResults = QVector<float> { 5.0f, 0.0f, 0.0f,          // Translation
                                             0.0f, 0.0f, -1.0f, 0.0f }; // Rotation

            QTest::newRow("clip2.json, t = duration")
                    << handler << clip << localTime << expectedResults;
            expectedResults.clear();
        }
        {
            handler = new Handler();
            clip = createAnimationClipLoader(handler, QUrl("qrc:/clip2.json"));
            localTime = clip->duration() / 2.0f;
            expectedResults = QVector<float> { 2.5f, 0.0f, 0.0f,          // Translation
                                             0.5f, 0.0f, -0.5f, 0.0f }; // Rotation

            QTest::newRow("clip2.json, t = duration/2")
                    << handler << clip << localTime << expectedResults;
            expectedResults.clear();
        }
        {
            // a clip with linear interpolation
            handler = new Handler();
            clip = createAnimationClipLoader(handler, QUrl("qrc:/clip4.json"));
            localTime = clip->duration();
            expectedResults = QVector<float> { 5.0, -2.0f, 6.0f };

            QTest::newRow("clip4.json, linear, t = duration")
                    << handler << clip << localTime << expectedResults;
            expectedResults.clear();
        }
        {
            // a clip with linear interpolation
            handler = new Handler();
            clip = createAnimationClipLoader(handler, QUrl("qrc:/clip4.json"));
            localTime = clip->duration() / 2.0f;
            expectedResults = QVector<float> { 2.5f, -1.0f, 3.0f };

            QTest::newRow("clip4.json, linear, t = duration/2")
                    << handler << clip << localTime << expectedResults;
            expectedResults.clear();
        }
        {
            // a clip with slerp interpolation
            handler = new Handler();
            clip = createAnimationClipLoader(handler, QUrl("qrc:/clip6.json"));
            localTime = clip->duration() / 2.0f;
            expectedResults = QVector<float> { 0.923822f, 0.382626f, 0.0f, 0.0f };

            QTest::newRow("clip6.json, slerp, t = duration/2")
                    << handler << clip << localTime << expectedResults;
            expectedResults.clear();
        }
    }

    void checkEvaluateClipAtLocalTime()
    {
        // GIVEN
        QFETCH(Handler *, handler);
        QFETCH(AnimationClip *, clip);
        QFETCH(float, localTime);
        QFETCH(ClipResults, expectedResults);

        // WHEN
        const ClipResults actualResults = evaluateClipAtLocalTime(clip, localTime);

        // THEN
        QCOMPARE(actualResults.size(), expectedResults.size());
        for (int i = 0; i < actualResults.size(); ++i) {
            auto actual = actualResults[i];
            auto expected = expectedResults[i];

            QVERIFY(fuzzyCompare(actual, expected) == true);
        }

        // Cleanup
        delete handler;
    }

    void checkEvaluateClipAtPhase_data()
    {
        QTest::addColumn<Handler *>("handler");
        QTest::addColumn<AnimationClip *>("clip");
        QTest::addColumn<float>("phase");
        QTest::addColumn<ClipResults>("expectedResults");

        Handler *handler;
        AnimationClip *clip;
        float phase;
        ClipResults expectedResults;

        {
            handler = new Handler();
            clip = createAnimationClipLoader(handler, QUrl("qrc:/clip1.json"));
            phase = 0.0f;
            expectedResults = QVector<float> { 0.0f, 0.0f, 0.0f };

            QTest::newRow("clip1.json, phi = 0.0")
                    << handler << clip << phase << expectedResults;
            expectedResults.clear();
        }

        {
            handler = new Handler();
            clip = createAnimationClipLoader(handler, QUrl("qrc:/clip1.json"));
            phase = 1.0f;
            expectedResults = QVector<float> { 5.0f, 0.0f, 0.0f };

            QTest::newRow("clip1.json, phi = 1.0")
                    << handler << clip << phase << expectedResults;
            expectedResults.clear();
        }

        {
            handler = new Handler();
            clip = createAnimationClipLoader(handler, QUrl("qrc:/clip1.json"));
            phase = 0.5f;
            expectedResults = QVector<float> { 2.5f, 0.0f, 0.0f };

            QTest::newRow("clip1.json, phi = 0.5")
                    << handler << clip << phase << expectedResults;
            expectedResults.clear();
        }

        {
            handler = new Handler();
            clip = createAnimationClipLoader(handler, QUrl("qrc:/clip2.json"));
            phase = 0.0f;
            expectedResults = QVector<float> { 0.0f, 0.0f, 0.0f,         // Translation
                                             1.0f, 0.0f, 0.0f, 0.0f }; // Rotation

            QTest::newRow("clip2.json, phi = 0.0")
                    << handler << clip << phase << expectedResults;
            expectedResults.clear();
        }
        {
            handler = new Handler();
            clip = createAnimationClipLoader(handler, QUrl("qrc:/clip2.json"));
            phase = 1.0f;
            expectedResults = QVector<float> { 5.0f, 0.0f, 0.0f,          // Translation
                                             0.0f, 0.0f, -1.0f, 0.0f }; // Rotation

            QTest::newRow("clip2.json, t = 1.0")
                    << handler << clip << phase << expectedResults;
            expectedResults.clear();
        }
        {
            handler = new Handler();
            clip = createAnimationClipLoader(handler, QUrl("qrc:/clip2.json"));
            phase = 0.5f;
            expectedResults = QVector<float> { 2.5f, 0.0f, 0.0f,          // Translation
                                             0.5f, 0.0f, -0.5f, 0.0f }; // Rotation

            QTest::newRow("clip2.json, phi = 0.5")
                    << handler << clip << phase << expectedResults;
            expectedResults.clear();
        }
    }

    void checkEvaluateClipAtPhase()
    {
        // GIVEN
        QFETCH(Handler *, handler);
        QFETCH(AnimationClip *, clip);
        QFETCH(float, phase);
        QFETCH(ClipResults, expectedResults);

        // WHEN
        const ClipResults actualResults = evaluateClipAtPhase(clip, phase);

        // THEN
        QCOMPARE(actualResults.size(), expectedResults.size());
        for (int i = 0; i < actualResults.size(); ++i) {
            auto actual = actualResults[i];
            auto expected = expectedResults[i];

            QVERIFY(fuzzyCompare(actual, expected) == true);
        }

        // Cleanup
        delete handler;
    }

    void checkChannelComponentsToIndicesHelper_data()
    {
        QTest::addColumn<Channel>("channel");
        QTest::addColumn<int>("dataType");
        QTest::addColumn<int>("expectedChannelComponentCount");
        QTest::addColumn<int>("offset");
        QTest::addColumn<QList<char>>("suffixes");
        QTest::addColumn<QList<int>>("expectedResults");

        Channel channel;
        int dataType;
        int expectedChannelComponentCount;
        int offset;
        QList<char> suffixes;
        QList<int> expectedResults;

        // already sorted vec3, no component names, with and without offset
        {
            channel = Channel();
            channel.name = QLatin1String("Location");
            channel.channelComponents.resize(3);
            // leave 'name' empty

            dataType = static_cast<int>(QMetaType::QVector3D);
            expectedChannelComponentCount = 3;
            offset = 0;
            // suffixes expected to be ignored
            expectedResults = QList<int> { 0, 1, 2 };

            QTest::newRow("vec3 location, pre-sorted, no component names, offset = 0")
                    << channel << dataType << expectedChannelComponentCount
                    << offset << suffixes << expectedResults;

            expectedResults.clear();

            offset = 4;
            expectedResults = QList<int> { 4, 5, 6 };
            QTest::newRow("vec3 location, pre-sorted, no component names, offset = 4")
                    << channel << dataType << expectedChannelComponentCount
                    << offset << suffixes << expectedResults;
            expectedResults.clear();
        }

        // vec3 with and without offset
        {
            channel = Channel();
            channel.name = QLatin1String("Location");
            channel.channelComponents.resize(3);
            channel.channelComponents[0].name = QLatin1String("Location X");
            channel.channelComponents[1].name = QLatin1String("Location Y");
            channel.channelComponents[2].name = QLatin1String("Location Z");

            dataType = static_cast<int>(QMetaType::QVector3D);
            expectedChannelComponentCount = 3;
            offset = 0;
            suffixes = QList<char> { 'X', 'Y', 'Z', 'W' };
            expectedResults = QList<int> { 0, 1, 2 };

            QTest::newRow("vec3 location, offset = 0")
                    << channel << dataType << expectedChannelComponentCount
                    << offset << suffixes << expectedResults;

            expectedResults.clear();

            offset = 4;
            expectedResults = QList<int> { 4, 5, 6 };
            QTest::newRow("vec3 location, offset = 4")
                    << channel << dataType << expectedChannelComponentCount
                    << offset << suffixes << expectedResults;

            suffixes.clear();
            expectedResults.clear();
        }

        // vec2 with and without offset
        {
            channel = Channel();
            channel.name = QLatin1String("pos");
            channel.channelComponents.resize(2);
            channel.channelComponents[0].name = QLatin1String("pos X");
            channel.channelComponents[1].name = QLatin1String("pos Y");

            dataType = static_cast<int>(QMetaType::QVector2D);
            expectedChannelComponentCount = 2;
            offset = 0;
            suffixes = QList<char> { 'X', 'Y', 'Z', 'W' };
            expectedResults = QList<int> { 0, 1 };

            QTest::newRow("vec2 pos, offset = 0")
                    << channel << dataType << expectedChannelComponentCount
                    << offset << suffixes << expectedResults;

            expectedResults.clear();

            offset = 2;
            expectedResults = QList<int> { 2, 3 };
            QTest::newRow("vec2 pos, offset = 2")
                    << channel << dataType << expectedChannelComponentCount
                    << offset << suffixes << expectedResults;

            suffixes.clear();
            expectedResults.clear();
        }

        // vec4 with and without offset
        {
            channel = Channel();
            channel.name = QLatin1String("foo");
            channel.channelComponents.resize(4);
            channel.channelComponents[0].name = QLatin1String("foo X");
            channel.channelComponents[1].name = QLatin1String("foo Y");
            channel.channelComponents[2].name = QLatin1String("foo Z");
            channel.channelComponents[3].name = QLatin1String("foo W");

            dataType = static_cast<int>(QMetaType::QVector4D);
            expectedChannelComponentCount = 4;
            offset = 0;
            suffixes = QList<char> { 'X', 'Y', 'Z', 'W' };
            expectedResults = QList<int> { 0, 1, 2, 3 };

            QTest::newRow("vec4 foo, offset = 0")
                    << channel << dataType << expectedChannelComponentCount
                    << offset << suffixes << expectedResults;

            expectedResults.clear();

            offset = 10;
            expectedResults = QList<int> { 10, 11, 12, 13 };
            QTest::newRow("vec4 foo, offset = 10")
                    << channel << dataType << expectedChannelComponentCount
                    << offset << suffixes << expectedResults;

            suffixes.clear();
            expectedResults.clear();
        }

        // double with and without offset
        {
            channel = Channel();
            channel.name = QLatin1String("foo");
            channel.channelComponents.resize(1);
            channel.channelComponents[0].name = QLatin1String("Mass X");

            dataType = static_cast<int>(QMetaType::Double);
            expectedChannelComponentCount = 1;
            offset = 0;
            suffixes = QList<char> { 'X', 'Y', 'Z', 'W' };
            expectedResults = QList<int> { 0 };

            QTest::newRow("double Mass, offset = 0")
                    << channel << dataType << expectedChannelComponentCount
                    << offset << suffixes << expectedResults;

            expectedResults.clear();

            offset = 5;
            expectedResults = QList<int> { 5 };
            QTest::newRow("double Mass, offset = 5")
                    << channel << dataType << expectedChannelComponentCount
                    << offset << suffixes << expectedResults;

            suffixes.clear();
            expectedResults.clear();
        }

        // quaternion with and without offset
        {
            channel = Channel();
            channel.name = QLatin1String("Rotation");
            channel.channelComponents.resize(4);
            channel.channelComponents[0].name = QLatin1String("Rotation W");
            channel.channelComponents[1].name = QLatin1String("Rotation X");
            channel.channelComponents[2].name = QLatin1String("Rotation Y");
            channel.channelComponents[3].name = QLatin1String("Rotation Z");

            dataType = static_cast<int>(QMetaType::QQuaternion);
            expectedChannelComponentCount = 4;
            offset = 0;
            suffixes = QList<char> { 'W', 'X', 'Y', 'Z' };
            expectedResults = QList<int> { 0, 1, 2, 3 };

            QTest::newRow("quaternion Rotation, offset = 0")
                    << channel << dataType << expectedChannelComponentCount
                    << offset << suffixes << expectedResults;

            expectedResults.clear();

            offset = 10;
            expectedResults = QList<int> { 10, 11, 12, 13 };
            QTest::newRow("quaternion Rotation, offset = 10")
                    << channel << dataType << expectedChannelComponentCount
                    << offset << suffixes << expectedResults;

            suffixes.clear();
            expectedResults.clear();
        }

        // quaternion with and without offset, randomized
        {
            channel = Channel();
            channel.name = QLatin1String("Rotation");
            channel.channelComponents.resize(4);
            channel.channelComponents[0].name = QLatin1String("Rotation X");
            channel.channelComponents[1].name = QLatin1String("Rotation W");
            channel.channelComponents[2].name = QLatin1String("Rotation Z");
            channel.channelComponents[3].name = QLatin1String("Rotation Y");

            dataType = static_cast<int>(QMetaType::QQuaternion);
            expectedChannelComponentCount = 4;
            offset = 0;
            suffixes = QList<char> { 'W', 'X', 'Y', 'Z' };
            expectedResults = QList<int> { 1, 0, 3, 2 };

            QTest::newRow("quaternion Rotation, offset = 0, randomized")
                    << channel << dataType << expectedChannelComponentCount
                    << offset << suffixes << expectedResults;

            expectedResults.clear();

            offset = 10;
            expectedResults = QList<int> { 11, 10, 13, 12 };
            QTest::newRow("quaternion Rotation, offset = 10, randomized")
                    << channel << dataType << expectedChannelComponentCount
                    << offset << suffixes << expectedResults;

            suffixes.clear();
            expectedResults.clear();
        }

        // color with and without offset 3 components
        {
            channel = Channel();
            channel.name = QLatin1String("Color");
            channel.channelComponents.resize(3);
            channel.channelComponents[0].name = QLatin1String("Color R");
            channel.channelComponents[1].name = QLatin1String("Color G");
            channel.channelComponents[2].name = QLatin1String("Color B");

            dataType = static_cast<int>(QMetaType::QColor);
            expectedChannelComponentCount = 3;
            offset = 0;
            suffixes = QList<char> { 'R', 'G', 'B' };
            expectedResults = QList<int> { 0, 1, 2 };

            QTest::newRow("QColor RGB Color, offset = 0")
                    << channel << dataType << expectedChannelComponentCount
                    << offset << suffixes << expectedResults;

            expectedResults.clear();

            offset = 10;
            expectedResults = QList<int> { 10, 11, 12 };
            QTest::newRow("QColor RGB Color, offset = 10")
                    << channel << dataType << expectedChannelComponentCount
                    << offset << suffixes << expectedResults;

            suffixes.clear();
            expectedResults.clear();
        }

        // color with and without offset 4 components
        {
            channel = Channel();
            channel.name = QLatin1String("Color");
            channel.channelComponents.resize(4);
            channel.channelComponents[0].name = QLatin1String("Color R");
            channel.channelComponents[1].name = QLatin1String("Color G");
            channel.channelComponents[2].name = QLatin1String("Color B");
            channel.channelComponents[3].name = QLatin1String("Color A");

            dataType = static_cast<int>(QMetaType::QColor);
            expectedChannelComponentCount = 4;
            offset = 0;
            suffixes = QList<char> { 'R', 'G', 'B', 'A' };
            expectedResults = QList<int> { 0, 1, 2, 3 };

            QTest::newRow("QColor RGBA Color, offset = 0")
                    << channel << dataType << expectedChannelComponentCount
                    << offset << suffixes << expectedResults;

            expectedResults.clear();

            offset = 10;
            expectedResults = QList<int> { 10, 11, 12, 13 };
            QTest::newRow("QColor RGBA Color, offset = 10")
                    << channel << dataType << expectedChannelComponentCount
                    << offset << suffixes << expectedResults;

            suffixes.clear();
            expectedResults.clear();
        }

        // weights as list with and without offset
        {
            channel = Channel();
            channel.name = QLatin1String("MorphWeights");
            channel.channelComponents.resize(6);
            // leave channel component names empty

            dataType = static_cast<int>(QMetaType::QVariantList);
            expectedChannelComponentCount = 6;
            offset = 0;
            // suffixes expected to be ignored
            expectedResults = QList<int> { 0, 1, 2, 3, 4, 5 };

            QTest::newRow("MorphWeights List count = 6, offset = 0")
                    << channel << dataType << expectedChannelComponentCount
                    << offset << suffixes << expectedResults;
            expectedResults.clear();

            offset = 10;
            expectedResults = QList<int> { 10, 11, 12, 13, 14, 15 };
            QTest::newRow("MorphWeights List count = 6, offset = 10")
                    << channel << dataType << expectedChannelComponentCount
                    << offset << suffixes << expectedResults;
            suffixes.clear();
            expectedResults.clear();
        }

        // weights as vec of float with and without offset
        {
            channel = Channel();
            channel.name = QLatin1String("MorphWeights");
            channel.channelComponents.resize(6);
            // leave channel component names empty

            dataType = qMetaTypeId<QList<float>>();
            expectedChannelComponentCount = 6;
            offset = 0;
            // suffixes expected to be ignored
            expectedResults = QList<int> { 0, 1, 2, 3, 4, 5 };

            QTest::newRow("MorphWeights Vec count = 6, offset = 0")
                    << channel << dataType << expectedChannelComponentCount
                    << offset << suffixes << expectedResults;
            expectedResults.clear();

            offset = 10;
            expectedResults = QList<int> { 10, 11, 12, 13, 14, 15 };
            QTest::newRow("MorphWeights Vec count = 6, offset = 10")
                    << channel << dataType << expectedChannelComponentCount
                    << offset << suffixes << expectedResults;
            suffixes.clear();
            expectedResults.clear();
        }
    }

    void checkChannelComponentsToIndicesHelper()
    {
        // GIVEN
        QFETCH(Channel, channel);
        QFETCH(int, offset);
        QFETCH(int, expectedChannelComponentCount);
        QFETCH(QList<char>, suffixes);
        QFETCH(QList<int>, expectedResults);

        // WHEN
        const QVector<int> actualResults = channelComponentsToIndicesHelper(channel,
                                                                            expectedChannelComponentCount, offset, suffixes);

        // THEN
        QCOMPARE(actualResults.size(), expectedResults.size());
        for (int i = 0; i < actualResults.size(); ++i) {
            QCOMPARE(actualResults[i], expectedResults[i]);
        }
    }

    void checkChannelComponentsToIndices_data()
    {
        QTest::addColumn<Channel>("channel");
        QTest::addColumn<int>("dataType");
        QTest::addColumn<int>("componentCount");
        QTest::addColumn<int>("offset");
        QTest::addColumn<QList<int>>("expectedResults");

        Channel channel;
        int dataType;
        int componentCount;
        int offset;
        QList<int> expectedResults;

        // Quaternion
        {
            channel = Channel();
            channel.name = QLatin1String("Rotation");
            channel.channelComponents.resize(4);
            channel.channelComponents[0].name = QLatin1String("Rotation W");
            channel.channelComponents[1].name = QLatin1String("Rotation X");
            channel.channelComponents[2].name = QLatin1String("Rotation Y");
            channel.channelComponents[3].name = QLatin1String("Rotation Z");

            dataType = static_cast<int>(QMetaType::QQuaternion);
            componentCount = 4;
            offset = 0;
            expectedResults = QList<int> { 0, 1, 2, 3 };

            QTest::newRow("quaternion Rotation, offset = 0")
                    << channel << dataType << componentCount << offset << expectedResults;

            expectedResults.clear();

            offset = 10;
            expectedResults = QList<int> { 10, 11, 12, 13 };
            QTest::newRow("quaternion Rotation, offset = 10")
                    << channel << dataType << componentCount << offset << expectedResults;

            expectedResults.clear();
        }

        // vec3 with and without offset
        {
            channel = Channel();
            channel.name = QLatin1String("Location");
            channel.channelComponents.resize(3);
            channel.channelComponents[0].name = QLatin1String("Location X");
            channel.channelComponents[1].name = QLatin1String("Location Y");
            channel.channelComponents[2].name = QLatin1String("Location Z");

            dataType = static_cast<int>(QMetaType::QVector3D);
            componentCount = 3;
            offset = 0;
            expectedResults = QList<int> { 0, 1, 2 };

            QTest::newRow("vec3 location, offset = 0")
                    << channel << dataType << componentCount << offset << expectedResults;

            expectedResults.clear();

            offset = 4;
            expectedResults = QList<int> { 4, 5, 6 };
            QTest::newRow("vec3 location, offset = 4")
                    << channel << dataType << componentCount << offset << expectedResults;

            expectedResults.clear();
        }

        // QColor
        {
            channel = Channel();
            channel.name = QLatin1String("Color");
            channel.channelComponents.resize(3);
            channel.channelComponents[0].name = QLatin1String("Color R");
            channel.channelComponents[1].name = QLatin1String("Color G");
            channel.channelComponents[2].name = QLatin1String("Color B");

            dataType = static_cast<int>(QMetaType::QColor);
            componentCount = 3;
            offset = 0;
            expectedResults = QList<int> { 0, 1, 2 };

            QTest::newRow("QColor RGB Color, offset = 0")
                    << channel << dataType << componentCount << offset << expectedResults;

            expectedResults.clear();

            offset = 10;
            expectedResults = QList<int> { 10, 11, 12 };
            QTest::newRow("QColor RGB Color, offset = 10")
                    << channel << dataType << componentCount << offset << expectedResults;

            expectedResults.clear();
        }

        {
            channel = Channel();
            channel.name = QLatin1String("Color");
            channel.channelComponents.resize(4);
            channel.channelComponents[0].name = QLatin1String("Color R");
            channel.channelComponents[1].name = QLatin1String("Color G");
            channel.channelComponents[2].name = QLatin1String("Color B");
            channel.channelComponents[3].name = QLatin1String("Color A");

            dataType = static_cast<int>(QMetaType::QColor);
            componentCount = 4;
            offset = 0;
            expectedResults = QList<int> { 0, 1, 2, 3 };

            QTest::newRow("QColor RGBA Color, offset = 0")
                    << channel << dataType << componentCount << offset << expectedResults;

            expectedResults.clear();

            offset = 10;
            expectedResults = QList<int> { 10, 11, 12, 13 };
            QTest::newRow("QColor RGBA Color, offset = 10")
                    << channel << dataType << componentCount << offset << expectedResults;

            expectedResults.clear();
        }
    }

    void checkChannelComponentsToIndices()
    {
        QFETCH(Channel, channel);
        QFETCH(int, dataType);
        QFETCH(int, componentCount);
        QFETCH(int, offset);
        QFETCH(QList<int>, expectedResults);

        // WHEN
        const QVector<int> actualResults = channelComponentsToIndices(channel,
                                                                      dataType, componentCount, offset);

        // THEN
        QCOMPARE(actualResults.size(), expectedResults.size());
        for (int i = 0; i < actualResults.size(); ++i) {
            QCOMPARE(actualResults[i], expectedResults[i]);
        }
    }

    void checkEvaluationDataForClip_data()
    {
        QTest::addColumn<Handler *>("handler");
        QTest::addColumn<AnimationClip *>("clip");
        QTest::addColumn<AnimatorEvaluationData>("animatorData");
        QTest::addColumn<ClipEvaluationData>("expectedClipData");

        Handler *handler;
        AnimationClip *clip;
        AnimatorEvaluationData animatorData;
        ClipEvaluationData clipData;
        auto* clock = new Clock;

        {
            handler = new Handler();
            clip = createAnimationClipLoader(handler, QUrl("qrc:/clip1.json"));
            const qint64 globalStartTimeNS = 0;
            const int loops = 1;
            auto animator = createClipAnimator(handler, globalStartTimeNS, loops);
            animator->setCurrentLoop(0);
            clipData.currentLoop = animator->currentLoop();
            const qint64 elapsedTimeNS = 0;
            animatorData = evaluationDataForAnimator(animator, clock, elapsedTimeNS); // Tested elsewhere

            clipData.localTime = localTimeFromElapsedTime(animatorData.currentTime,
                                                         animatorData.elapsedTime,
                                                         animatorData.playbackRate,
                                                         clip->duration(),
                                                         animatorData.loopCount,
                                                         clipData.currentLoop); // Tested elsewhere
            clipData.isFinalFrame = false;

            QTest::newRow("clip1.json, globalTime = 0")
                    << handler << clip << animatorData << clipData;
        }

        {
            handler = new Handler();
            clip = createAnimationClipLoader(handler, QUrl("qrc:/clip1.json"));
            const qint64 globalStartTimeNS = 0;
            const int loops = 1;
            auto animator = createClipAnimator(handler, globalStartTimeNS, loops);
            animator->setCurrentLoop(0);
            clipData.currentLoop = animator->currentLoop();
            const qint64 elapsedTimeNS = toNsecs(clip->duration()+1); // +1 to ensure beyond end
            animatorData = evaluationDataForAnimator(animator, nullptr, elapsedTimeNS); // Tested elsewhere

            clipData.localTime = localTimeFromElapsedTime(animatorData.currentTime,
                                                         animatorData.elapsedTime,
                                                         animatorData.playbackRate,
                                                         clip->duration(),
                                                         animatorData.loopCount,
                                                         clipData.currentLoop); // Tested elsewhere
            clipData.isFinalFrame = true;

            QTest::newRow("clip1.json, elapsedTime = duration + 1")
                    << handler << clip << animatorData << clipData;
        }

        {
            handler = new Handler();
            clip = createAnimationClipLoader(handler, QUrl("qrc:/clip1.json"));
            const qint64 globalStartTimeNS = 0;
            const int loops = 0; // Infinite loops
            auto animator = createClipAnimator(handler, globalStartTimeNS, loops);
            animator->setCurrentLoop(0);
            clipData.currentLoop = animator->currentLoop();
            const qint64 elapsedTimeNS = toNsecs(2.0 * clip->duration());
            animatorData = evaluationDataForAnimator(animator, clock, elapsedTimeNS); // Tested elsewhere

            clipData.localTime = localTimeFromElapsedTime(animatorData.currentTime,
                                                         animatorData.elapsedTime,
                                                         animatorData.playbackRate,
                                                         clip->duration(),
                                                         animatorData.loopCount,
                                                         clipData.currentLoop); // Tested elsewhere
            clipData.isFinalFrame = false;

            QTest::newRow("clip1.json, elapsedTime = 2 * duration, loops = infinite")
                    << handler << clip << animatorData << clipData;
        }

        {
            handler = new Handler();
            clip = createAnimationClipLoader(handler, QUrl("qrc:/clip1.json"));
            const qint64 globalStartTimeNS = 0;
            const int loops = 2;
            auto animator = createClipAnimator(handler, globalStartTimeNS, loops);
            animator->setCurrentLoop(0);
            clipData.currentLoop = animator->currentLoop();
            const qint64 elapsedTimeNS = toNsecs(2.0 * clip->duration() + 1.0); // +1 to ensure beyond end of clip
            animatorData = evaluationDataForAnimator(animator, nullptr, elapsedTimeNS); // Tested elsewhere

            clipData.localTime = localTimeFromElapsedTime(animatorData.currentTime,
                                                         animatorData.elapsedTime,
                                                         animatorData.playbackRate,
                                                         clip->duration(),
                                                         animatorData.loopCount,
                                                         clipData.currentLoop); // Tested elsewhere
            clipData.isFinalFrame = true;

            QTest::newRow("clip1.json, elapsedTime = 2 * duration + 1, loops = 2")
                    << handler << clip << animatorData << clipData;
        }

        {
            handler = new Handler();
            clip = createAnimationClipLoader(handler, QUrl("qrc:/clip1.json"));
            const qint64 globalStartTimeNS = 0;
            const int loops = 2;
            auto animator = createClipAnimator(handler, globalStartTimeNS, loops);
            animator->setCurrentLoop(1);
            clipData.currentLoop = animator->currentLoop();
            const qint64 elapsedTimeNS = toNsecs(clip->duration() + 1.0); // +1 to ensure beyond end of clip
            animatorData = evaluationDataForAnimator(animator, nullptr, elapsedTimeNS); // Tested elsewhere

            clipData.localTime = localTimeFromElapsedTime(animatorData.currentTime,
                                                         animatorData.elapsedTime,
                                                         animatorData.playbackRate,
                                                         clip->duration(),
                                                         animatorData.loopCount,
                                                         clipData.currentLoop); // Tested elsewhere
            clipData.isFinalFrame = true;

            QTest::newRow("clip1.json, elapsedTime = duration + 1, loops = 2, current_loop = 1")
                    << handler << clip << animatorData << clipData;
        }

        {
            handler = new Handler();
            clip = createAnimationClipLoader(handler, QUrl("qrc:/clip1.json"));
            const qint64 globalStartTimeNS = clip->duration();
            const int loops = 1;
            auto animator = createClipAnimator(handler, globalStartTimeNS, loops);
            animator->setCurrentLoop(1);
            clipData.currentLoop = animator->currentLoop();
            const qint64 elapsedTimeNS = toNsecs(clip->duration() * 0.5); // +1 to ensure beyond end of clip

            Clock clock;
            clock.setPlaybackRate(-1.0);

            animatorData = evaluationDataForAnimator(animator, &clock, elapsedTimeNS); // Tested elsewhere

            clipData.localTime = localTimeFromElapsedTime(animatorData.currentTime,
                                                         animatorData.elapsedTime,
                                                         animatorData.playbackRate,
                                                         clip->duration(),
                                                         animatorData.loopCount,
                                                         clipData.currentLoop); // Tested elsewhere
            clipData.isFinalFrame = false;

            QTest::newRow("clip1.json, elapsedTime = duration / 2, loops = 1, current_loop = 1, playback_rate = -1")
                    << handler << clip << animatorData << clipData;
        }

        {
            handler = new Handler();
            clip = createAnimationClipLoader(handler, QUrl("qrc:/clip1.json"));
            const qint64 globalStartTimeNS = clip->duration();
            const int loops = 1;
            auto animator = createClipAnimator(handler, globalStartTimeNS, loops);
            animator->setCurrentLoop(1);
            clipData.currentLoop = animator->currentLoop();
            const qint64 elapsedTimeNS = toNsecs(clip->duration() + 1); // +1 to ensure beyond end of clip

            Clock clock;
            clock.setPlaybackRate(-1.0);

            animatorData = evaluationDataForAnimator(animator, &clock, elapsedTimeNS); // Tested elsewhere

            clipData.localTime = localTimeFromElapsedTime(animatorData.currentTime,
                                                          animatorData.elapsedTime,
                                                          animatorData.playbackRate,
                                                          clip->duration(),
                                                          animatorData.loopCount,
                                                          clipData.currentLoop); // Tested elsewhere
            clipData.isFinalFrame = true;

            QTest::newRow("clip1.json, elapsedTime = duration + 1, loops = 1, current_loop = 1, playback_rate = -1")
                    << handler << clip << animatorData << clipData;
        }

        {
            handler = new Handler();
            clip = createAnimationClipLoader(handler, QUrl("qrc:/clip1.json"));
            const qint64 globalStartTimeNS = clip->duration();
            const int loops = 2;
            auto animator = createClipAnimator(handler, globalStartTimeNS, loops);
            animator->setCurrentLoop(0);
            clipData.currentLoop = animator->currentLoop();
            const qint64 elapsedTimeNS = toNsecs(clip->duration() + 1); // +1 to ensure beyond end of clip

            Clock clock;
            clock.setPlaybackRate(-1.0);

            animatorData = evaluationDataForAnimator(animator, &clock, elapsedTimeNS); // Tested elsewhere

            clipData.localTime = localTimeFromElapsedTime(animatorData.currentTime,
                                                          animatorData.elapsedTime,
                                                          animatorData.playbackRate,
                                                          clip->duration(),
                                                          animatorData.loopCount,
                                                          clipData.currentLoop); // Tested elsewhere
            clipData.isFinalFrame = true;

            QTest::newRow("clip1.json, elapsedTime = duration + 1, loops = 2, current_loop = 0, playback_rate = -1")
                    << handler << clip << animatorData << clipData;
        }

        {
            handler = new Handler();
            clip = createAnimationClipLoader(handler, QUrl("qrc:/clip1.json"));
            const qint64 globalStartTimeNS = clip->duration();
            const int loops = 2;
            auto animator = createClipAnimator(handler, globalStartTimeNS, loops);
            animator->setCurrentLoop(1);
            clipData.currentLoop = animator->currentLoop();
            const qint64 elapsedTimeNS = toNsecs(clip->duration() * 2.0 + 1); // +1 to ensure beyond end of clip

            Clock clock;
            clock.setPlaybackRate(-1.0);

            animatorData = evaluationDataForAnimator(animator, &clock, elapsedTimeNS); // Tested elsewhere

            clipData.localTime = localTimeFromElapsedTime(animatorData.currentTime,
                                                          animatorData.elapsedTime,
                                                          animatorData.playbackRate,
                                                          clip->duration(),
                                                          animatorData.loopCount,
                                                          clipData.currentLoop); // Tested elsewhere
            clipData.isFinalFrame = true;

            QTest::newRow("clip1.json, elapsedTime = duration + 1, loops = 2, current_loop = 1, playback_rate = -1")
                    << handler << clip << animatorData << clipData;
        }
    }

    void checkEvaluationDataForClip()
    {
        // GIVEN
        QFETCH(Handler *, handler);
        QFETCH(AnimationClip *, clip);
        QFETCH(AnimatorEvaluationData, animatorData);
        QFETCH(ClipEvaluationData, expectedClipData);

        // WHEN
        ClipEvaluationData actualClipData = evaluationDataForClip(clip, animatorData);

        // THEN
        QCOMPARE(actualClipData.currentLoop, expectedClipData.currentLoop);
        QVERIFY(fuzzyCompare(actualClipData.localTime, expectedClipData.localTime) == true);
        QCOMPARE(actualClipData.isFinalFrame, expectedClipData.isFinalFrame);

        // Cleanup
        delete handler;
    }

    void checkEvaluationDataForAnimator_data()
    {
        QTest::addColumn<Handler *>("handler");
        QTest::addColumn<ClipAnimator *>("animator");
        QTest::addColumn<qint64>("elapsedTime");
        QTest::addColumn<AnimatorEvaluationData>("expectedAnimatorData");

        Handler *handler;
        ClipAnimator *animator;
        qint64 elapsedTimeNS;
        AnimatorEvaluationData expectedAnimatorData;

        {
            handler = new Handler();
            const qint64 globalStartTimeNS = 0;
            const int loops = 1;
            animator = createClipAnimator(handler, globalStartTimeNS, loops);
            elapsedTimeNS = 0;

            expectedAnimatorData.loopCount = loops;
            expectedAnimatorData.playbackRate = 1.0; // hard-wired for now
            expectedAnimatorData.elapsedTime = 0.0;

            QTest::newRow("globalStartTime = 0, elapsedTime = 0, loops = 1")
                    << handler << animator << elapsedTimeNS << expectedAnimatorData;
        }

        {
            handler = new Handler();
            const qint64 globalStartTimeNS = 0;
            const int loops = 5;
            animator = createClipAnimator(handler, globalStartTimeNS, loops);
            elapsedTimeNS = 0;

            expectedAnimatorData.loopCount = loops;
            expectedAnimatorData.playbackRate = 1.0; // hard-wired for now
            expectedAnimatorData.elapsedTime = 0.0;

            QTest::newRow("globalStartTime = 0, elapsedTime = 0, loops = 5")
                    << handler << animator << elapsedTimeNS << expectedAnimatorData;
        }

        {
            handler = new Handler();
            const qint64 globalStartTimeNS = 0;
            const int loops = 1;
            animator = createClipAnimator(handler, globalStartTimeNS, loops);
            elapsedTimeNS = 5000000000;

            expectedAnimatorData.loopCount = loops;
            expectedAnimatorData.playbackRate = 1.0; // hard-wired for now
            expectedAnimatorData.elapsedTime = 5.0;

            QTest::newRow("globalStartTime = 0, elapsedTime = 5, loops = 1")
                    << handler << animator << elapsedTimeNS << expectedAnimatorData;
        }

        {
            handler = new Handler();
            const qint64 globalStartTimeNS = 3000000000;
            const int loops = 1;
            animator = createClipAnimator(handler, globalStartTimeNS, loops);
            elapsedTimeNS = 2000000000;

            expectedAnimatorData.loopCount = loops;
            expectedAnimatorData.playbackRate = 1.0; // hard-wired for now
            expectedAnimatorData.elapsedTime = 2.0;

            QTest::newRow("globalStartTime = 3, elapsedTime = 2, loops = 1")
                    << handler << animator << elapsedTimeNS << expectedAnimatorData;
        }
    }

    void checkEvaluationDataForAnimator()
    {
        // GIVEN
        QFETCH(Handler *, handler);
        QFETCH(ClipAnimator *, animator);
        QFETCH(qint64, elapsedTime);
        QFETCH(AnimatorEvaluationData, expectedAnimatorData);

        // WHEN
        AnimatorEvaluationData actualAnimatorData = evaluationDataForAnimator(animator, nullptr, elapsedTime);

        // THEN
        QCOMPARE(actualAnimatorData.loopCount, expectedAnimatorData.loopCount);
        QVERIFY(fuzzyCompare(actualAnimatorData.playbackRate, expectedAnimatorData.playbackRate) == true);
        QVERIFY(fuzzyCompare(actualAnimatorData.elapsedTime, expectedAnimatorData.elapsedTime) == true);

        // Cleanup
        delete handler;
    }

    void checkGatherValueNodesToEvaluate_data()
    {
        QTest::addColumn<Handler *>("handler");
        QTest::addColumn<Qt3DCore::QNodeId>("blendTreeRootId");
        QTest::addColumn<QList<Qt3DCore::QNodeId>>("expectedIds");

        {
            Handler *handler = new Handler;

            const auto lerp = createLerpClipBlend(handler);
            const auto value1 = createClipBlendValue(handler);
            const auto clip1Id = Qt3DCore::QNodeId::createId();
            value1->setClipId(clip1Id);
            lerp->setStartClipId(value1->peerId());

            const auto value2 = createClipBlendValue(handler);
            const auto clip2Id = Qt3DCore::QNodeId::createId();
            value2->setClipId(clip2Id);
            lerp->setEndClipId(value2->peerId());

            const QList<Qt3DCore::QNodeId> expectedIds = { value1->peerId(), value2->peerId() };

            QTest::newRow("simple lerp") << handler << lerp->peerId() << expectedIds;
        }

        {
            Handler *handler = new Handler;

            const auto value1 = createClipBlendValue(handler);
            const auto clip1Id = Qt3DCore::QNodeId::createId();
            value1->setClipId(clip1Id);

            const QList<Qt3DCore::QNodeId> expectedIds = { value1->peerId() };

            QTest::newRow("value only") << handler << value1->peerId() << expectedIds;
        }
    }

    void checkGatherValueNodesToEvaluate()
    {
        // GIVEN
        QFETCH(Handler *, handler);
        QFETCH(Qt3DCore::QNodeId, blendTreeRootId);
        QFETCH(QList<Qt3DCore::QNodeId>, expectedIds);

        // WHEN
        const QVector<Qt3DCore::QNodeId> actualIds = gatherValueNodesToEvaluate(handler, blendTreeRootId);

        // THEN
        QCOMPARE(actualIds.size(), expectedIds.size());
        for (int i = 0; i < actualIds.size(); ++i)
            QCOMPARE(actualIds[i], expectedIds[i]);

        // Cleanup
        delete handler;
    }

    void checkEvaluateBlendTree_data()
    {
        QTest::addColumn<Handler *>("handler");
        QTest::addColumn<BlendedClipAnimator *>("animator");
        QTest::addColumn<Qt3DCore::QNodeId>("blendNodeId");
        QTest::addColumn<ClipResults>("expectedResults");

        {
            /*
                ValueNode1----
                             |
                             MeanBlendNode
                             |
                ValueNode2----
            */

            auto handler = new Handler();
            const qint64 globalStartTimeNS = 0;
            const int loopCount = 1;
            auto animator = createBlendedClipAnimator(handler, globalStartTimeNS, loopCount);

            // Set up the blend node and dependencies (evaluated clip results of the
            // dependent nodes in the animator indexed by their ids).
            MeanBlendNode *blendNode = createMeanBlendNode(handler);

            // First clip to use in the mean
            auto valueNode1 = createClipBlendValue(handler);
            ClipResults valueNode1Results = { 0.0f, 0.0f, 0.0f };
            valueNode1->setClipResults(animator->peerId(), valueNode1Results);

            // Second clip to use in the mean
            auto valueNode2 = createClipBlendValue(handler);
            ClipResults valueNode2Results = { 1.0f, 1.0f, 1.0f };
            valueNode2->setClipResults(animator->peerId(), valueNode2Results);

            blendNode->setValueNodeIds(valueNode1->peerId(), valueNode2->peerId());

            ClipResults expectedResults = { 0.5f, 0.5f, 0.5f };

            QTest::newRow("mean node, 1 channel")
                    << handler << animator << blendNode->peerId() << expectedResults;
        }

        {
            /*
                ValueNode1----
                             |
                             MeanBlendNode
                             |
                ValueNode2----
            */

            auto handler = new Handler();
            const qint64 globalStartTimeNS = 0;
            const int loopCount = 1;
            auto animator = createBlendedClipAnimator(handler, globalStartTimeNS, loopCount);

            // Set up the blend node and dependencies (evaluated clip results of the
            // dependent nodes in the animator indexed by their ids).
            MeanBlendNode *blendNode = createMeanBlendNode(handler);

            // First clip to use in the mean
            auto valueNode1 = createClipBlendValue(handler);
            ClipResults valueNode1Results = { 0.0f, 0.0f, 0.0f, 1.0f, 2.0f, 3.0f };
            valueNode1->setClipResults(animator->peerId(), valueNode1Results);

            // Second clip to use in the mean
            auto valueNode2 = createClipBlendValue(handler);
            ClipResults valueNode2Results = { 1.0f, 1.0f, 1.0f, 2.0f, 4.0f, 6.0f };
            valueNode2->setClipResults(animator->peerId(), valueNode2Results);

            blendNode->setValueNodeIds(valueNode1->peerId(), valueNode2->peerId());

            ClipResults expectedResults = { 0.5f, 0.5f, 0.5f, 1.5f, 3.0f, 4.5f };

            QTest::newRow("mean node, 2 channels")
                    << handler << animator << blendNode->peerId() << expectedResults;
        }

        {
            /*
                ValueNode1----
                             |
                             MeanBlendNode1------
                             |                  |
                ValueNode2----                  |
                                                MeanBlendNode3
                ValueNode3----                  |
                             |                  |
                             MeanBlendNode2------
                             |
                ValueNode4----
            */

            auto handler = new Handler();
            const qint64 globalStartTimeNS = 0;
            const int loopCount = 1;
            auto animator = createBlendedClipAnimator(handler, globalStartTimeNS, loopCount);

            // Set up the blend node and dependencies (evaluated clip results of the
            // dependent nodes in the animator indexed by their ids).

            // MeanBlendNode1
            MeanBlendNode *meanNode1 = createMeanBlendNode(handler);

            // First clip to use in mean1
            auto valueNode1 = createClipBlendValue(handler);
            ClipResults valueNode1Results = { 0.0f, 0.0f, 0.0f };
            valueNode1->setClipResults(animator->peerId(), valueNode1Results);

            // Second clip to use in mean1
            auto valueNode2 = createClipBlendValue(handler);
            ClipResults valueNode2Results = { 2.0f, 2.0f, 2.0f };
            valueNode2->setClipResults(animator->peerId(), valueNode2Results);

            meanNode1->setValueNodeIds(valueNode1->peerId(), valueNode2->peerId());


            // MeanBlendNode2
            MeanBlendNode *meanNode2 = createMeanBlendNode(handler);

            // First clip to use in mean1
            auto valueNode3 = createClipBlendValue(handler);
            ClipResults valueNode3Results = { 10.0f, 10.0f, 10.0f };
            valueNode3->setClipResults(animator->peerId(), valueNode3Results);

            // Second clip to use in mean1
            auto valueNode4 = createClipBlendValue(handler);
            ClipResults valueNode4Results = { 20.0f, 20.0f, 20.0f };
            valueNode4->setClipResults(animator->peerId(), valueNode4Results);

            meanNode2->setValueNodeIds(valueNode3->peerId(), valueNode4->peerId());


            // MeanBlendNode3
            MeanBlendNode *meanNode3 = createMeanBlendNode(handler);
            meanNode3->setValueNodeIds(meanNode1->peerId(), meanNode2->peerId());

            // Mean1 = 1
            // Mean2 = 15
            // Mean3 = (1 + 15 ) / 2 = 8
            ClipResults expectedResults = { 8.0f, 8.0f, 8.0f };

            QTest::newRow("3 mean nodes, 1 channel")
                    << handler << animator << meanNode3->peerId() << expectedResults;
        }

        {
            /*
                ValueNode1----
                             |
                             MeanBlendNode1------
                             |                  |
                ValueNode2----                  |
                                                MeanBlendNode3---
                ValueNode3----                  |               |
                             |                  |               |
                             MeanBlendNode2------               AdditiveBlendNode1
                             |                                  |
                ValueNode4----                                  |
                                                ValueNode5-------
            */

            auto handler = new Handler();
            const qint64 globalStartTimeNS = 0;
            const int loopCount = 1;
            auto animator = createBlendedClipAnimator(handler, globalStartTimeNS, loopCount);

            // Set up the blend node and dependencies (evaluated clip results of the
            // dependent nodes in the animator indexed by their ids).

            // MeanBlendNode1
            MeanBlendNode *meanNode1 = createMeanBlendNode(handler);

            // First clip to use in mean1
            auto valueNode1 = createClipBlendValue(handler);
            ClipResults valueNode1Results = { 0.0f, 0.0f, 0.0f };
            valueNode1->setClipResults(animator->peerId(), valueNode1Results);

            // Second clip to use in mean1
            auto valueNode2 = createClipBlendValue(handler);
            ClipResults valueNode2Results = { 2.0f, 2.0f, 2.0f };
            valueNode2->setClipResults(animator->peerId(), valueNode2Results);

            meanNode1->setValueNodeIds(valueNode1->peerId(), valueNode2->peerId());


            // MeanBlendNode2
            MeanBlendNode *meanNode2 = createMeanBlendNode(handler);

            // First clip to use in mean2
            auto valueNode3 = createClipBlendValue(handler);
            ClipResults valueNode3Results = { 10.0f, 10.0f, 10.0f };
            valueNode3->setClipResults(animator->peerId(), valueNode3Results);

            // Second clip to use in mean2
            auto valueNode4 = createClipBlendValue(handler);
            ClipResults valueNode4Results = { 20.0f, 20.0f, 20.0f };
            valueNode4->setClipResults(animator->peerId(), valueNode4Results);

            meanNode2->setValueNodeIds(valueNode3->peerId(), valueNode4->peerId());


            // MeanBlendNode3
            MeanBlendNode *meanNode3 = createMeanBlendNode(handler);
            meanNode3->setValueNodeIds(meanNode1->peerId(), meanNode2->peerId());


            // AdditiveBlendNode1
            AdditiveClipBlend *additiveBlendNode1 = createAdditiveClipBlend(handler);
            auto valueNode5 = createClipBlendValue(handler);
            ClipResults valueNode5Results = { 1.0f, 2.0f, 3.0f };
            valueNode5->setClipResults(animator->peerId(), valueNode5Results);

            additiveBlendNode1->setBaseClipId(meanNode3->peerId());
            additiveBlendNode1->setAdditiveClipId(valueNode5->peerId());
            additiveBlendNode1->setAdditiveFactor(0.5);

            // Mean1 = 1
            // Mean2 = 15
            // Mean3 = (1 + 15 ) / 2 = 8
            // Additive1 = 8 + 0.5 * (1, 2, 3) = (8.5, 9, 9.5)
            ClipResults expectedResults = { 8.5f, 9.0f, 9.5f };

            QTest::newRow("3 mean nodes + additive, 1 channel")
                    << handler << animator << additiveBlendNode1->peerId() << expectedResults;
        }
    }

    void checkEvaluateBlendTree()
    {
        // GIVEN
        QFETCH(Handler *, handler);
        QFETCH(BlendedClipAnimator *, animator);
        QFETCH(Qt3DCore::QNodeId, blendNodeId);
        QFETCH(ClipResults, expectedResults);

        // WHEN
        const ClipResults actualResults = evaluateBlendTree(handler, animator, blendNodeId);

        // THEN
        QCOMPARE(actualResults.size(), expectedResults.size());
        for (int i = 0; i < actualResults.size(); ++i)
            QCOMPARE(actualResults[i], expectedResults[i]);

        // Cleanup
        delete handler;
    }

    void checkFormatClipResults_data()
    {
        QTest::addColumn<ClipResults>("rawClipResults");
        QTest::addColumn<ComponentIndices>("format");
        QTest::addColumn<ClipResults>("expectedResults");

        {
            ClipResults rawClipResults = { 1.0f, 2.0f, 3.0f };
            ComponentIndices format = { 0, 1, 2 };
            ClipResults expectedResults = { 1.0f, 2.0f, 3.0f };

            QTest::newRow("identity")
                    << rawClipResults << format << expectedResults;
        }

        {
            ClipResults rawClipResults = { 1.0f, 2.0f };
            ComponentIndices format = { 1, 0 };
            ClipResults expectedResults = { 2.0f, 1.0f };

            QTest::newRow("swap")
                    << rawClipResults << format << expectedResults;
        }

        {
            ClipResults rawClipResults = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f };
            ComponentIndices format = { 0, 2, 1, 3, 4 };
            ClipResults expectedResults = { 1.0f, 3.0f, 2.0f, 4.0f, 5.0f };

            QTest::newRow("swap subset")
                    << rawClipResults << format << expectedResults;
        }

        {
            ClipResults rawClipResults = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f };
            ComponentIndices format = { 4, 3, 2, 1, 0 };
            ClipResults expectedResults = { 5.0f, 4.0f, 3.0f, 2.0f, 1.0f };

            QTest::newRow("reverse")
                    << rawClipResults << format << expectedResults;
        }

        {
            ClipResults rawClipResults = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f };
            ComponentIndices format = { 0, 1, -1, 3, 4 };
            ClipResults expectedResults = { 1.0f, 2.0f, 0.0f, 4.0f, 5.0f };

            QTest::newRow("include missing")
                    << rawClipResults << format << expectedResults;
        }
    }

    void checkFormatClipResults()
    {
        // GIVEN
        QFETCH(ClipResults, rawClipResults);
        QFETCH(ComponentIndices, format);
        QFETCH(ClipResults, expectedResults);

        // WHEN
        const ClipResults actualResults = formatClipResults(rawClipResults, format);

        // THEN
        QCOMPARE(actualResults.size(), expectedResults.size());
        for (int i = 0; i < actualResults.size(); ++i)
            QCOMPARE(actualResults[i], expectedResults[i]);
    }

    void checkBuildRequiredChannelsAndTypes_data()
    {
        QTest::addColumn<Handler *>("handler");
        QTest::addColumn<ChannelMapper *>("mapper");
        QTest::addColumn<QList<ChannelNameAndType>>("expectedResults");

        {
            auto handler = new Handler();
            auto channelMapping = createChannelMapping(handler,
                                                       QLatin1String("Location"),
                                                       Qt3DCore::QNodeId::createId(),
                                                       "translation",
                                                       static_cast<int>(QMetaType::QVector3D),
                                                       3);
            auto channelMapper = createChannelMapper(handler,
                                                     QList<Qt3DCore::QNodeId> { channelMapping->peerId() });

            const QList<ChannelNameAndType> expectedResults = {
                { QLatin1String("Location"),
                  static_cast<int>(QMetaType::QVector3D),
                  3,
                  channelMapping->peerId() }
            };

            QTest::addRow("Location, vec3") << handler << channelMapper << expectedResults;
        }

        {
            auto handler = new Handler();
            auto channelMapping1 = createChannelMapping(handler,
                                                        QLatin1String("Location"),
                                                        Qt3DCore::QNodeId::createId(),
                                                        "translation",
                                                        static_cast<int>(QMetaType::QVector3D),
                                                        3);
            auto channelMapping2 = createChannelMapping(handler,
                                                        QLatin1String("Rotation"),
                                                        Qt3DCore::QNodeId::createId(),
                                                        "rotation",
                                                        static_cast<int>(QMetaType::QQuaternion),
                                                        4);

            const QList<Qt3DCore::QNodeId> channelMappingIds
                    = { channelMapping1->peerId(), channelMapping2->peerId() };
            auto channelMapper = createChannelMapper(handler, channelMappingIds);

            const QList<ChannelNameAndType> expectedResults = {
                { QLatin1String("Location"),
                  static_cast<int>(QMetaType::QVector3D),
                  3,
                  channelMapping1->peerId() },
                { QLatin1String("Rotation"),
                  static_cast<int>(QMetaType::QQuaternion),
                  4,
                  channelMapping2->peerId() }
            };

            QTest::addRow("Multiple unique channels") << handler << channelMapper << expectedResults;
        }

        {
            auto handler = new Handler();
            auto channelMapping1 = createChannelMapping(handler,
                                                        QLatin1String("Location"),
                                                        Qt3DCore::QNodeId::createId(),
                                                        "translation",
                                                        static_cast<int>(QMetaType::QVector3D),
                                                        3);
            auto channelMapping2 = createChannelMapping(handler,
                                                        QLatin1String("Rotation"),
                                                        Qt3DCore::QNodeId::createId(),
                                                        "rotation",
                                                        static_cast<int>(QMetaType::QQuaternion),
                                                        4);
            auto channelMapping3 = createChannelMapping(handler,
                                                        QLatin1String("Location"),
                                                        Qt3DCore::QNodeId::createId(),
                                                        "translation",
                                                        static_cast<int>(QMetaType::QVector3D),
                                                        3);
            auto channelMapping4 = createChannelMapping(handler,
                                                        QLatin1String("Location"),
                                                        Qt3DCore::QNodeId::createId(),
                                                        "translation",
                                                        static_cast<int>(QMetaType::QVector3D),
                                                        3);

            const QList<Qt3DCore::QNodeId> channelMappingIds
                    = { channelMapping1->peerId(), channelMapping2->peerId(),
                        channelMapping3->peerId(), channelMapping4->peerId() };
            auto channelMapper = createChannelMapper(handler, channelMappingIds);

            QList<ChannelNameAndType> expectedResults = {
                { QLatin1String("Location"),
                  static_cast<int>(QMetaType::QVector3D),
                  3,
                  channelMapping1->peerId() },
                { QLatin1String("Rotation"),
                  static_cast<int>(QMetaType::QQuaternion),
                  4,
                  channelMapping2->peerId() },
                { QLatin1String("Location"),
                  static_cast<int>(QMetaType::QVector3D),
                  3,
                  channelMapping3->peerId() },
                { QLatin1String("Location"),
                  static_cast<int>(QMetaType::QVector3D),
                  3,
                  channelMapping4->peerId() }
            };

            QTest::addRow("Multiple channels with repeats") << handler << channelMapper << expectedResults;
        }

        {
            auto handler = new Handler();
            const int jointCount = 10;
            auto skeleton = createSkeleton(handler, jointCount);
            auto channelMapping = createChannelMapping(handler, skeleton->peerId());
            auto channelMapper = createChannelMapper(handler,
                                                     QList<Qt3DCore::QNodeId> { channelMapping->peerId() });

            QList<ChannelNameAndType> expectedResults;
            for (int i = 0; i < jointCount; ++i) {
                ChannelNameAndType locationDescription = { QLatin1String("Location"),
                                                           static_cast<int>(QMetaType::QVector3D),
                                                           3,
                                                           channelMapping->peerId() };
                locationDescription.jointIndex = i;
                locationDescription.jointTransformComponent = Translation;
                expectedResults.push_back(locationDescription);

                ChannelNameAndType rotationDescription = { QLatin1String("Rotation"),
                                                           static_cast<int>(QMetaType::QQuaternion),
                                                           4,
                                                           channelMapping->peerId() };
                rotationDescription.jointIndex = i;
                rotationDescription.jointTransformComponent = Rotation;
                expectedResults.push_back(rotationDescription);

                ChannelNameAndType scaleDescription = { QLatin1String("Scale"),
                                                        static_cast<int>(QMetaType::QVector3D),
                                                        3,
                                                        channelMapping->peerId() };
                scaleDescription.jointIndex = i;
                scaleDescription.jointTransformComponent = Scale;
                expectedResults.push_back(scaleDescription);
            }

            QTest::addRow("Skeleton, 10 joints") << handler << channelMapper << expectedResults;
        }
    }

    void checkBuildRequiredChannelsAndTypes()
    {
        // GIVEN
        QFETCH(Handler *, handler);
        QFETCH(ChannelMapper *, mapper);
        QFETCH(QList<ChannelNameAndType>, expectedResults);

        // WHEN
        const QVector<ChannelNameAndType> actualResults = buildRequiredChannelsAndTypes(handler, mapper);

        // THEN
        QCOMPARE(actualResults.size(), expectedResults.size());
        for (int i = 0; i < actualResults.size(); ++i)
            QCOMPARE(actualResults[i], expectedResults[i]);

        // Cleanup
        delete handler;
    }

    void checkAssignChannelComponentIndices_data()
    {
        QTest::addColumn<QVector<ChannelNameAndType>>("allChannels");
        QTest::addColumn<QVector<ComponentIndices>>("expectedResults");

        {
            const QVector<ChannelNameAndType> allChannels = {
                { QLatin1String("Location"), static_cast<int>(QMetaType::QVector3D), 3 }
            };

            const QVector<ComponentIndices> expectedResults = { { 0, 1, 2 } };

            QTest::newRow("vec3 location") << allChannels << expectedResults;
        }

        {
            const QVector<ChannelNameAndType> allChannels = {
                { QLatin1String("Location"), static_cast<int>(QMetaType::QVector3D), 3 },
                { QLatin1String("Rotation"), static_cast<int>(QMetaType::QQuaternion), 4 }
            };

            const QVector<ComponentIndices> expectedResults = { { 0, 1, 2 },
                                                              { 3, 4, 5, 6 } };

            QTest::newRow("vec3 location, quaterion rotation") << allChannels << expectedResults;
        }

        {
            const QVector<ChannelNameAndType> allChannels = {
                { QLatin1String("Location"), static_cast<int>(QMetaType::QVector3D), 3 },
                { QLatin1String("Rotation"), static_cast<int>(QMetaType::QQuaternion), 4 },
                { QLatin1String("BaseColor"), static_cast<int>(QMetaType::QVector3D), 3 },
                { QLatin1String("Metalness"), static_cast<int>(QMetaType::Double), 1 },
                { QLatin1String("Roughness"), static_cast<int>(QMetaType::Double), 1 },
                { QLatin1String("MorphWeights"), static_cast<int>(QMetaType::QVariantList), 6 }
            };

            const QVector<ComponentIndices> expectedResults = {
                { 0, 1, 2 },
                { 3, 4, 5, 6 },
                { 7, 8, 9 },
                { 10 },
                { 11 },
                { 12, 13, 14, 15, 16, 17 } };

            QTest::newRow("vec3 location, quaterion rotation, pbr metal-rough morphweights") << allChannels << expectedResults;
        }

        {
            QVector<ChannelNameAndType> allChannels;
            const int jointCount = 4;
            for (int i = 0; i < jointCount; ++i) {
                ChannelNameAndType locationDescription = { QLatin1String("Location"), static_cast<int>(QMetaType::QVector3D), 3 };
                locationDescription.jointIndex = i;
                allChannels.push_back(locationDescription);

                ChannelNameAndType rotationDescription = { QLatin1String("Rotation"), static_cast<int>(QMetaType::QQuaternion), 4 };
                rotationDescription.jointIndex = i;
                allChannels.push_back(rotationDescription);

                ChannelNameAndType scaleDescription = { QLatin1String("Scale"), static_cast<int>(QMetaType::QVector3D), 3 };
                scaleDescription.jointIndex = i;
                allChannels.push_back(scaleDescription);
            }

            const QVector<ComponentIndices> expectedResults = {
                {  0,  1,  2 }, {  3,  4,  5,  6 }, {  7,  8,  9 },
                { 10, 11, 12 }, { 13, 14, 15, 16 }, { 17, 18, 19 },
                { 20, 21, 22 }, { 23, 24, 25, 26 }, { 27, 28, 29 },
                { 30, 31, 32 }, { 33, 34, 35, 36 }, { 37, 38, 39 }
            };

            QTest::newRow("skeleton, 4 joints") << allChannels << expectedResults;
        }
    }

    void checkAssignChannelComponentIndices()
    {
        // GIVEN
        QFETCH(QVector<ChannelNameAndType>, allChannels);
        QFETCH(QVector<ComponentIndices>, expectedResults);

        // WHEN
        const QVector<ComponentIndices> actualResults = assignChannelComponentIndices(allChannels);

        // THEN
        QCOMPARE(actualResults.size(), expectedResults.size());
        for (int i = 0; i < actualResults.size(); ++i) {
            const ComponentIndices &actualResult = actualResults[i];
            const ComponentIndices &expectedResult = expectedResults[i];

            for (int j = 0; j < actualResult.size(); ++j)
                QCOMPARE(actualResult[j], expectedResult[j]);
        }
    }

    void checkGenerateClipFormatIndices_data()
    {
        QTest::addColumn<QVector<ChannelNameAndType>>("targetChannels");
        QTest::addColumn<QVector<ComponentIndices>>("targetIndices");
        QTest::addColumn<AnimationClip *>("clip");
        QTest::addColumn<ClipFormat>("expectedResults");

        {
            const QVector<ChannelNameAndType> targetChannels = {
                { QLatin1String("Rotation"), static_cast<int>(QMetaType::QQuaternion), 4 },
                { QLatin1String("Location"), static_cast<int>(QMetaType::QVector3D), 3 },
                { QLatin1String("Base Color"), static_cast<int>(QMetaType::QVector3D), 3 },
                { QLatin1String("Metalness"), static_cast<int>(QMetaType::Double), 1 },
                { QLatin1String("Roughness"), static_cast<int>(QMetaType::Double), 1 }
            };

            const QVector<ComponentIndices> targetIndices = {
                { 0, 1, 2, 3 },
                { 4, 5, 6 },
                { 7, 8, 9 },
                { 10 },
                { 11 }
            };

            auto *clip = new AnimationClip();
            clip->setDataType(AnimationClip::File);
            clip->setSource(QUrl("qrc:/clip3.json"));
            clip->loadAnimation();

            ClipFormat expectedResults;
            expectedResults.sourceClipIndices = { 0, 1, 3, 2,    // Rotation (y/z swapped in clip3.json)
                                                  4, 6, 5,       // Location (y/z swapped in clip3.json)
                                                  7, 8, 9,       // Base Color
                                                  10,            // Metalness
                                                  11 };          // Roughness
            expectedResults.sourceClipMask = { QBitArray(4, true),
                                               QBitArray(3, true),
                                               QBitArray(3, true),
                                               QBitArray(1, true),
                                               QBitArray(1, true) };
            expectedResults.namesAndTypes = targetChannels;
            expectedResults.formattedComponentIndices = targetIndices;

            QTest::newRow("rotation, location, pbr metal-rough")
                    << targetChannels << targetIndices << clip << expectedResults;
        }

        {
            const QVector<ChannelNameAndType> targetChannels = {
                { QLatin1String("Location"), static_cast<int>(QMetaType::QVector3D), 3 },
                { QLatin1String("Rotation"), static_cast<int>(QMetaType::QQuaternion), 4 },
                { QLatin1String("Base Color"), static_cast<int>(QMetaType::QVector3D), 3 },
                { QLatin1String("Metalness"), static_cast<int>(QMetaType::Double), 1 },
                { QLatin1String("Roughness"), static_cast<int>(QMetaType::Double), 1 }
            };

            const QVector<ComponentIndices> targetIndices = {
                { 0, 1, 2 },
                { 3, 4, 5, 6 },
                { 7, 8, 9 },
                { 10 },
                { 11 }
            };

            auto *clip = new AnimationClip();
            clip->setDataType(AnimationClip::File);
            clip->setSource(QUrl("qrc:/clip3.json"));
            clip->loadAnimation();

            ClipFormat expectedResults;
            expectedResults.sourceClipIndices = { 4, 6, 5,       // Location (y/z swapped in clip3.json)
                                                  0, 1, 3, 2,    // Rotation (y/z swapped in clip3.json)
                                                  7, 8, 9,       // Base Color
                                                  10,            // Metalness
                                                  11 };          // Roughness
            expectedResults.sourceClipMask = { QBitArray(3, true),
                                               QBitArray(4, true),
                                               QBitArray(3, true),
                                               QBitArray(1, true),
                                               QBitArray(1, true) };
            expectedResults.namesAndTypes = targetChannels;
            expectedResults.formattedComponentIndices = targetIndices;

            QTest::newRow("location, rotation, pbr metal-rough")
                    << targetChannels << targetIndices << clip << expectedResults;
        }

        {
            const QVector<ChannelNameAndType> targetChannels = {
                { QLatin1String("Rotation"), static_cast<int>(QMetaType::QQuaternion), 4 },
                { QLatin1String("Location"), static_cast<int>(QMetaType::QVector3D), 3 },
                { QLatin1String("Albedo"), static_cast<int>(QMetaType::QVector3D), 3 },
                { QLatin1String("Metalness"), static_cast<int>(QMetaType::Double), 1 },
                { QLatin1String("Roughness"), static_cast<int>(QMetaType::Double), 1 }
            };

            const QVector<ComponentIndices> targetIndices = {
                { 0, 1, 2, 3 },
                { 4, 5, 6 },
                { 7, 8, 9 },
                { 10 },
                { 11 }
            };

            auto *clip = new AnimationClip();
            clip->setDataType(AnimationClip::File);
            clip->setSource(QUrl("qrc:/clip3.json"));
            clip->loadAnimation();

            ClipFormat expectedResults;
            expectedResults.sourceClipIndices = { 0, 1, 3, 2,    // Rotation (y/z swapped in clip3.json)
                                                  4, 6, 5,       // Location (y/z swapped in clip3.json)
                                                  -1, -1, -1,    // Albedo (missing from clip)
                                                  10,            // Metalness
                                                  11 };          // Roughness
            expectedResults.sourceClipMask = { QBitArray(4, true),
                                               QBitArray(3, true),
                                               QBitArray(3, false),
                                               QBitArray(1, true),
                                               QBitArray(1, true) };
            expectedResults.namesAndTypes = targetChannels;
            expectedResults.formattedComponentIndices = targetIndices;

            QTest::newRow("rotation, location, albedo (missing), metal-rough")
                    << targetChannels << targetIndices << clip << expectedResults;
        }

        {
            const QVector<ChannelNameAndType> targetChannels = {
                { QLatin1String("Location"), static_cast<int>(QMetaType::QVector3D), 3 },
                { QLatin1String("Rotation"), static_cast<int>(QMetaType::QQuaternion), 4 },
                { QLatin1String("Albedo"), static_cast<int>(QMetaType::QVector3D), 3 },
                { QLatin1String("Metalness"), static_cast<int>(QMetaType::Double), 1 },
                { QLatin1String("Roughness"), static_cast<int>(QMetaType::Double), 1 }
            };

            const QVector<ComponentIndices> targetIndices = {
                { 0, 1, 2 },
                { 3, 4, 5, 6 },
                { 7, 8, 9 },
                { 10 },
                { 11 }
            };

            auto *clip = new AnimationClip();
            clip->setDataType(AnimationClip::File);
            clip->setSource(QUrl("qrc:/clip3.json"));
            clip->loadAnimation();

            ClipFormat expectedResults;
            expectedResults.sourceClipIndices = { 4, 6, 5,       // Location (y/z swapped in clip3.json)
                                                  0, 1, 3, 2,    // Rotation (y/z swapped in clip3.json)
                                                  -1, -1, -1,    // Albedo (missing from clip)
                                                  10,            // Metalness
                                                  11 };          // Roughness
            expectedResults.sourceClipMask = { QBitArray(3, true),
                                               QBitArray(4, true),
                                               QBitArray(3, false),
                                               QBitArray(1, true),
                                               QBitArray(1, true) };
            expectedResults.namesAndTypes = targetChannels;
            expectedResults.formattedComponentIndices = targetIndices;

            QTest::newRow("location, rotation, albedo (missing), metal-rough")
                    << targetChannels << targetIndices << clip << expectedResults;
        }

        {
            QVector<ChannelNameAndType> targetChannels;
            const int jointCount = 4;
            for (int i = 0; i < jointCount; ++i) {
                ChannelNameAndType locationDescription = { QLatin1String("Location"), static_cast<int>(QMetaType::QVector3D), 3 };
                locationDescription.jointIndex = i;
                targetChannels.push_back(locationDescription);

                ChannelNameAndType rotationDescription = { QLatin1String("Rotation"), static_cast<int>(QMetaType::QQuaternion), 4 };
                rotationDescription.jointIndex = i;
                targetChannels.push_back(rotationDescription);

                ChannelNameAndType scaleDescription = { QLatin1String("Scale"), static_cast<int>(QMetaType::QVector3D), 3 };
                scaleDescription.jointIndex = i;
                targetChannels.push_back(scaleDescription);
            }

            const QVector<ComponentIndices> targetIndices = {
                {  0,  1,  2 }, {  3,  4,  5,  6 }, {  7,  8,  9 },
                { 10, 11, 12 }, { 13, 14, 15, 16 }, { 17, 18, 19 },
                { 20, 21, 22 }, { 23, 24, 25, 26 }, { 27, 28, 29 },
                { 30, 31, 32 }, { 33, 34, 35, 36 }, { 37, 38, 39 }
            };

            auto *clip = new AnimationClip();
            clip->setDataType(AnimationClip::File);
            clip->setSource(QUrl("qrc:/clip5.json"));
            clip->loadAnimation();

            ClipFormat expectedResults;
            expectedResults.sourceClipIndices = { 4, 6, 5,           // Location, joint 0 (y/z swapped in clip5.json)
                                                  0, 1, 3, 2,        // Rotation, joint 0 (y/z swapped in clip5.json)
                                                  7, 8, 9,           // Scale,    joint 0
                                                  14, 16, 15,        // Location, joint 1 (y/z swapped in clip5.json)
                                                  10, 11, 13, 12,    // Rotation, joint 1 (y/z swapped in clip5.json)
                                                  17, 18, 19,        // Scale,    joint 1
                                                  24, 26, 25,        // Location, joint 2 (y/z swapped in clip5.json)
                                                  20, 21, 23, 22,    // Rotation, joint 2 (y/z swapped in clip5.json)
                                                  27, 28, 29,        // Scale,    joint 2
                                                  34, 36, 35,        // Location, joint 3 (y/z swapped in clip5.json)
                                                  30, 31, 33, 32,    // Rotation, joint 3 (y/z swapped in clip5.json)
                                                  37, 38, 39 };      // Scale,    joint 3
            expectedResults.sourceClipMask = { QBitArray(3, true),
                                               QBitArray(4, true),
                                               QBitArray(3, true),
                                               QBitArray(3, true),
                                               QBitArray(4, true),
                                               QBitArray(3, true),
                                               QBitArray(3, true),
                                               QBitArray(4, true),
                                               QBitArray(3, true),
                                               QBitArray(3, true),
                                               QBitArray(4, true),
                                               QBitArray(3, true) };
            expectedResults.namesAndTypes = targetChannels;
            expectedResults.formattedComponentIndices = targetIndices;

            QTest::newRow("skeleton (SQT), 4 joints")
                    << targetChannels << targetIndices << clip << expectedResults;
        }
    }

    void checkGenerateClipFormatIndices()
    {
        // GIVEN
        QFETCH(QVector<ChannelNameAndType>, targetChannels);
        QFETCH(QVector<ComponentIndices>, targetIndices);
        QFETCH(AnimationClip *, clip);
        QFETCH(ClipFormat, expectedResults);

        // WHEN
        const ClipFormat actualResults = generateClipFormatIndices(targetChannels,
                                                                   targetIndices,
                                                                   clip);

        // THEN
        QCOMPARE(actualResults.sourceClipIndices.size(), expectedResults.sourceClipIndices.size());
        for (int i = 0; i < actualResults.sourceClipIndices.size(); ++i)
            QCOMPARE(actualResults.sourceClipIndices[i], expectedResults.sourceClipIndices[i]);

        QCOMPARE(actualResults.sourceClipMask.size(), expectedResults.sourceClipMask.size());
        for (int i = 0; i < actualResults.sourceClipMask.size(); ++i)
            QCOMPARE(actualResults.sourceClipMask[i], expectedResults.sourceClipMask[i]);

        QCOMPARE(actualResults.formattedComponentIndices.size(), expectedResults.formattedComponentIndices.size());
        for (int i = 0; i < actualResults.formattedComponentIndices.size(); ++i)
            QCOMPARE(actualResults.formattedComponentIndices[i], expectedResults.formattedComponentIndices[i]);

        QCOMPARE(actualResults.namesAndTypes.size(), expectedResults.namesAndTypes.size());
        for (int i = 0; i < actualResults.namesAndTypes.size(); ++i)
            QCOMPARE(actualResults.namesAndTypes[i], expectedResults.namesAndTypes[i]);

        // Cleanup
        delete clip;
    }

    void checkDefaultValueForChannel_data()
    {
        QTest::addColumn<Handler *>("handler");
        QTest::addColumn<ChannelNameAndType>("channelDescription");
        QTest::addColumn<QVector<float>>("expectedResults");

        {
            auto handler = new Handler();
            auto channelMapping = createChannelMapping(handler,
                                                       QLatin1String("Location"),
                                                       Qt3DCore::QNodeId::createId(),
                                                       "translation",
                                                       static_cast<int>(QMetaType::QVector3D),
                                                       3);
            ChannelNameAndType channelDescription;
            channelDescription.mappingId = channelMapping->peerId();
            channelDescription.type = static_cast<int>(QMetaType::QVector3D);
            channelDescription.name = QLatin1String("translation");
            const QVector<float> expectedResults = { 0.0f, 0.0f, 0.0f };
            QTest::newRow("translation") << handler << channelDescription << expectedResults;
        }

        {
            auto handler = new Handler();
            auto channelMapping = createChannelMapping(handler,
                                                       QLatin1String("Rotation"),
                                                       Qt3DCore::QNodeId::createId(),
                                                       "rotation",
                                                       static_cast<int>(QMetaType::QQuaternion),
                                                       4);
            ChannelNameAndType channelDescription;
            channelDescription.mappingId = channelMapping->peerId();
            channelDescription.type = static_cast<int>(QMetaType::QQuaternion);
            channelDescription.name = QLatin1String("rotation");
            const QVector<float> expectedResults = { 1.0f, 0.0f, 0.0f, 0.0f };
            QTest::newRow("rotation") << handler << channelDescription << expectedResults;
        }

        {
            auto handler = new Handler();
            auto channelMapping = createChannelMapping(handler,
                                                       QLatin1String("Scale"),
                                                       Qt3DCore::QNodeId::createId(),
                                                       "scale",
                                                       static_cast<int>(QMetaType::QVector3D),
                                                       3);
            ChannelNameAndType channelDescription;
            channelDescription.mappingId = channelMapping->peerId();
            channelDescription.type = static_cast<int>(QMetaType::QVector3D);
            channelDescription.name = QLatin1String("scale");
            const QVector<float> expectedResults = { 1.0f, 1.0f, 1.0f };
            QTest::newRow("scale") << handler << channelDescription << expectedResults;
        }

        // Test skeleton cases
        {
            auto handler = new Handler();
            auto skeleton = createSkeleton(handler, 2);
            skeleton->setJointScale(0, QVector3D(2.0f, 3.0f, 4.0f));

            auto channelMapping = createChannelMapping(handler, skeleton->peerId());
            ChannelNameAndType channelDescription;
            channelDescription.mappingId = channelMapping->peerId();
            channelDescription.type = static_cast<int>(QMetaType::QVector3D);
            channelDescription.jointIndex = 0;
            channelDescription.jointTransformComponent = Scale;
            const QVector<float> expectedResults = { 2.0f, 3.0f, 4.0f };
            QTest::newRow("joint 0 scale") << handler << channelDescription << expectedResults;
        }

        {
            auto handler = new Handler();
            auto skeleton = createSkeleton(handler, 2);
            skeleton->setJointRotation(0, QQuaternion(1.0f, 0.0f, 0.0f, 0.0f));

            auto channelMapping = createChannelMapping(handler, skeleton->peerId());
            ChannelNameAndType channelDescription;
            channelDescription.mappingId = channelMapping->peerId();
            channelDescription.type = static_cast<int>(QMetaType::QVector3D);
            channelDescription.jointIndex = 0;
            channelDescription.jointTransformComponent = Rotation;
            const QVector<float> expectedResults = { 1.0f, 0.0f, 0.0f, 0.0f };
            QTest::newRow("joint 0 rotation") << handler << channelDescription << expectedResults;
        }

        {
            auto handler = new Handler();
            auto skeleton = createSkeleton(handler, 2);
            skeleton->setJointTranslation(0, QVector3D(2.0f, 3.0f, 4.0f));

            auto channelMapping = createChannelMapping(handler, skeleton->peerId());
            ChannelNameAndType channelDescription;
            channelDescription.mappingId = channelMapping->peerId();
            channelDescription.type = static_cast<int>(QMetaType::QVector3D);
            channelDescription.jointIndex = 0;
            channelDescription.jointTransformComponent = Translation;
            const QVector<float> expectedResults = { 2.0f, 3.0f, 4.0f };
            QTest::newRow("joint 0 translation") << handler << channelDescription << expectedResults;
        }

        {
            auto handler = new Handler();
            auto skeleton = createSkeleton(handler, 2);
            skeleton->setJointScale(1, QVector3D(20.0f, 30.0f, 40.0f));

            auto channelMapping = createChannelMapping(handler, skeleton->peerId());
            ChannelNameAndType channelDescription;
            channelDescription.mappingId = channelMapping->peerId();
            channelDescription.type = static_cast<int>(QMetaType::QVector3D);
            channelDescription.jointIndex = 1;
            channelDescription.jointTransformComponent = Scale;
            const QVector<float> expectedResults = { 20.0f, 30.0f, 40.0f };
            QTest::newRow("joint 1 scale") << handler << channelDescription << expectedResults;
        }

        {
            auto handler = new Handler();
            auto skeleton = createSkeleton(handler, 2);
            skeleton->setJointRotation(1, QQuaternion(1.0f, 0.0f, 0.0f, 0.0f));

            auto channelMapping = createChannelMapping(handler, skeleton->peerId());
            ChannelNameAndType channelDescription;
            channelDescription.mappingId = channelMapping->peerId();
            channelDescription.type = static_cast<int>(QMetaType::QVector3D);
            channelDescription.jointIndex = 1;
            channelDescription.jointTransformComponent = Rotation;
            const QVector<float> expectedResults = { 1.0f, 0.0f, 0.0f, 0.0f };
            QTest::newRow("joint 1 rotation") << handler << channelDescription << expectedResults;
        }

        {
            auto handler = new Handler();
            auto skeleton = createSkeleton(handler, 2);
            skeleton->setJointTranslation(1, QVector3D(4.0f, 5.0f, 6.0f));

            auto channelMapping = createChannelMapping(handler, skeleton->peerId());
            ChannelNameAndType channelDescription;
            channelDescription.mappingId = channelMapping->peerId();
            channelDescription.type = static_cast<int>(QMetaType::QVector3D);
            channelDescription.jointIndex = 1;
            channelDescription.jointTransformComponent = Translation;
            const QVector<float> expectedResults = { 4.0f, 5.0f, 6.0f };
            QTest::newRow("joint 1 translation") << handler << channelDescription << expectedResults;
        }
    }

    void checkDefaultValueForChannel()
    {
        // GIVEN
        QFETCH(Handler *, handler);
        QFETCH(ChannelNameAndType, channelDescription);
        QFETCH(QVector<float>, expectedResults);

        // WHEN
        auto actualResults = defaultValueForChannel(handler, channelDescription);

        // THEN
        QCOMPARE(actualResults.size(), expectedResults.size());
        for (int i = 0; i < actualResults.size(); ++i) {
            QCOMPARE(actualResults[i], expectedResults[i]);
        }
    }
};

QTEST_MAIN(tst_AnimationUtils)

#include "tst_animationutils.moc"
