/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

//TESTED_COMPONENT=plugins/declarative/multimedia

#include <QtTest/QtTest>

#include <QtQml/qqmlengine.h>
#include <QtQml/qqmlcomponent.h>
#include <QQuickView>

#include "private/qdeclarativevideooutput_p.h"

#include <qabstractvideosurface.h>
#include <qvideorenderercontrol.h>
#include <qvideosurfaceformat.h>

#include <qmediaobject.h>

class SurfaceHolder : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QAbstractVideoSurface *videoSurface READ videoSurface WRITE setVideoSurface)
public:
    SurfaceHolder(QObject *parent)
        : QObject(parent)
        , m_surface(0)
    {
    }

    QAbstractVideoSurface *videoSurface() const
    {
        return m_surface;
    }
    void setVideoSurface(QAbstractVideoSurface *surface)
    {
        if (m_surface != surface && m_surface && m_surface->isActive()) {
            m_surface->stop();
        }
        m_surface = surface;
    }

    void presentDummyFrame(const QSize &size);

private:
    QAbstractVideoSurface *m_surface;

};

// Starts the surface and puts a frame
void SurfaceHolder::presentDummyFrame(const QSize &size)
{
    if (m_surface && m_surface->supportedPixelFormats().count() > 0) {
        QVideoFrame::PixelFormat pixelFormat = m_surface->supportedPixelFormats().value(0);
        QVideoSurfaceFormat format(size, pixelFormat);
        QVideoFrame frame(size.width() * size.height() * 4, size, size.width() * 4, pixelFormat);

        if (!m_surface->isActive())
            m_surface->start(format);
        m_surface->present(frame);

        // Have to spin an event loop or two for the surfaceFormatChanged() signal
        qApp->processEvents();
    }
}

class tst_QDeclarativeVideoOutput : public QObject
{
    Q_OBJECT
public:
    tst_QDeclarativeVideoOutput();

    ~tst_QDeclarativeVideoOutput()
    {
        delete m_mappingOutput;
        delete m_mappingSurface;
        delete m_mappingComponent;
    }

public slots:
    void initTestCase();

private slots:
    void fillMode();
    void flushMode();
    void orientation();
    void surfaceSource();
    void paintSurface();
    void sourceRect();

    void contentRect();
    void contentRect_data();

    void mappingPoint();
    void mappingPoint_data();
    void mappingRect();
    void mappingRect_data();

    // XXX May be worth adding tests that the surface activeChanged signals are sent appropriately
    // to holder?

private:
    QQmlEngine m_engine;

    // Variables used for the mapping test
    QQmlComponent *m_mappingComponent;
    QObject *m_mappingOutput;
    SurfaceHolder *m_mappingSurface;

    void updateOutputGeometry(QObject *output);

    QRectF invokeR2R(QObject *object, const char *signature, const QRectF &rect);
    QPointF invokeP2P(QObject *object, const char *signature, const QPointF &point);
};

void tst_QDeclarativeVideoOutput::initTestCase()
{
    // We initialize the mapping vars here
    m_mappingComponent = new QQmlComponent(&m_engine);
    m_mappingComponent->loadUrl(QUrl("qrc:/main.qml"));
    m_mappingSurface = new SurfaceHolder(this);

    m_mappingOutput = m_mappingComponent->create();
    QVERIFY(m_mappingOutput != 0);

    m_mappingOutput->setProperty("source", QVariant::fromValue(static_cast<QObject*>(m_mappingSurface)));

    m_mappingSurface->presentDummyFrame(QSize(200,100)); // this should start m_surface
    updateOutputGeometry(m_mappingOutput);
}

Q_DECLARE_METATYPE(QDeclarativeVideoOutput::FillMode)
Q_DECLARE_METATYPE(QDeclarativeVideoOutput::FlushMode)

tst_QDeclarativeVideoOutput::tst_QDeclarativeVideoOutput()
    : m_mappingComponent(0)
    , m_mappingOutput(0)
    , m_mappingSurface(0)
{
    qRegisterMetaType<QDeclarativeVideoOutput::FillMode>();
}

void tst_QDeclarativeVideoOutput::fillMode()
{
    QQmlComponent component(&m_engine);
    component.loadUrl(QUrl("qrc:/main.qml"));

    QObject *videoOutput = component.create();
    QVERIFY(videoOutput != 0);

    QSignalSpy propSpy(videoOutput, SIGNAL(fillModeChanged(QDeclarativeVideoOutput::FillMode)));

    // Default is preserveaspectfit
    QCOMPARE(videoOutput->property("fillMode").value<QDeclarativeVideoOutput::FillMode>(), QDeclarativeVideoOutput::PreserveAspectFit);
    QCOMPARE(propSpy.count(), 0);

    videoOutput->setProperty("fillMode", QVariant(int(QDeclarativeVideoOutput::PreserveAspectCrop)));
    QCOMPARE(videoOutput->property("fillMode").value<QDeclarativeVideoOutput::FillMode>(), QDeclarativeVideoOutput::PreserveAspectCrop);
    QCOMPARE(propSpy.count(), 1);

    videoOutput->setProperty("fillMode", QVariant(int(QDeclarativeVideoOutput::Stretch)));
    QCOMPARE(videoOutput->property("fillMode").value<QDeclarativeVideoOutput::FillMode>(), QDeclarativeVideoOutput::Stretch);
    QCOMPARE(propSpy.count(), 2);

    videoOutput->setProperty("fillMode", QVariant(int(QDeclarativeVideoOutput::Stretch)));
    QCOMPARE(videoOutput->property("fillMode").value<QDeclarativeVideoOutput::FillMode>(), QDeclarativeVideoOutput::Stretch);
    QCOMPARE(propSpy.count(), 2);

    delete videoOutput;
}

void tst_QDeclarativeVideoOutput::flushMode()
{
    QQmlComponent component(&m_engine);
    component.loadUrl(QUrl("qrc:/main.qml"));

    QObject *videoOutput = component.create();
    QVERIFY(videoOutput != 0);

    QSignalSpy propSpy(videoOutput, SIGNAL(flushModeChanged()));

    QCOMPARE(videoOutput->property("flushMode").value<QDeclarativeVideoOutput::FlushMode>(), QDeclarativeVideoOutput::EmptyFrame);
    QCOMPARE(propSpy.count(), 0);

    videoOutput->setProperty("flushMode", QVariant(int(QDeclarativeVideoOutput::FirstFrame)));
    QCOMPARE(videoOutput->property("fillMode").value<QDeclarativeVideoOutput::FlushMode>(), QDeclarativeVideoOutput::FirstFrame);
    QCOMPARE(propSpy.count(), 1);
}

void tst_QDeclarativeVideoOutput::orientation()
{
    QQmlComponent component(&m_engine);
    component.loadUrl(QUrl("qrc:/main.qml"));

    QObject *videoOutput = component.create();
    QVERIFY(videoOutput != 0);

    QSignalSpy propSpy(videoOutput, SIGNAL(orientationChanged()));

    // Default orientation is 0
    QCOMPARE(videoOutput->property("orientation").toInt(), 0);
    QCOMPARE(propSpy.count(), 0);

    videoOutput->setProperty("orientation", QVariant(90));
    QCOMPARE(videoOutput->property("orientation").toInt(), 90);
    QCOMPARE(propSpy.count(), 1);

    videoOutput->setProperty("orientation", QVariant(180));
    QCOMPARE(videoOutput->property("orientation").toInt(), 180);
    QCOMPARE(propSpy.count(), 2);

    videoOutput->setProperty("orientation", QVariant(270));
    QCOMPARE(videoOutput->property("orientation").toInt(), 270);
    QCOMPARE(propSpy.count(), 3);

    videoOutput->setProperty("orientation", QVariant(360));
    QCOMPARE(videoOutput->property("orientation").toInt(), 360);
    QCOMPARE(propSpy.count(), 4);

    // More than 360 should be fine
    videoOutput->setProperty("orientation", QVariant(540));
    QCOMPARE(videoOutput->property("orientation").toInt(), 540);
    QCOMPARE(propSpy.count(), 5);

    // Negative should be fine
    videoOutput->setProperty("orientation", QVariant(-180));
    QCOMPARE(videoOutput->property("orientation").toInt(), -180);
    QCOMPARE(propSpy.count(), 6);

    // Same value should not reemit
    videoOutput->setProperty("orientation", QVariant(-180));
    QCOMPARE(videoOutput->property("orientation").toInt(), -180);
    QCOMPARE(propSpy.count(), 6);

    // Non multiples of 90 should not work
    videoOutput->setProperty("orientation", QVariant(-1));
    QCOMPARE(videoOutput->property("orientation").toInt(), -180);
    QCOMPARE(propSpy.count(), 6);

    delete videoOutput;
}

void tst_QDeclarativeVideoOutput::surfaceSource()
{
    QQmlComponent component(&m_engine);
    component.loadUrl(QUrl("qrc:/main.qml"));

    QObject *videoOutput = component.create();
    QVERIFY(videoOutput != 0);

    SurfaceHolder holder(this);

    QCOMPARE(holder.videoSurface(), static_cast<QAbstractVideoSurface*>(0));

    videoOutput->setProperty("source", QVariant::fromValue(static_cast<QObject*>(&holder)));

    QVERIFY(holder.videoSurface() != 0);

    // Now we could do things with the surface..
    const QList<QVideoFrame::PixelFormat> formats = holder.videoSurface()->supportedPixelFormats();
    QVERIFY(formats.count() > 0);

    // See if we can start and stop each pixel format (..)
    for (QVideoFrame::PixelFormat format : formats) {
        QVideoSurfaceFormat surfaceFormat(QSize(200,100), format);
        QVERIFY(holder.videoSurface()->isFormatSupported(surfaceFormat)); // This does kind of depend on node factories

        QVERIFY(holder.videoSurface()->start(surfaceFormat));
        QVERIFY(holder.videoSurface()->surfaceFormat() == surfaceFormat);
        QVERIFY(holder.videoSurface()->isActive());

        holder.videoSurface()->stop();

        QVERIFY(!holder.videoSurface()->isActive());
    }

    delete videoOutput;

    // This should clear the surface
    QCOMPARE(holder.videoSurface(), static_cast<QAbstractVideoSurface*>(0));

    // Also, creating two sources, setting them in order, and destroying the first
    // should not zero holder.videoSurface()
    videoOutput = component.create();
    videoOutput->setProperty("source", QVariant::fromValue(static_cast<QObject*>(&holder)));

    QAbstractVideoSurface *surface = holder.videoSurface();
    QVERIFY(holder.videoSurface());

    QObject *videoOutput2 = component.create();
    QVERIFY(videoOutput2);
    videoOutput2->setProperty("source", QVariant::fromValue(static_cast<QObject*>(&holder)));
    QVERIFY(holder.videoSurface());
    QVERIFY(holder.videoSurface() != surface); // Surface should have changed
    surface = holder.videoSurface();

    // Now delete first one
    delete videoOutput;
    QVERIFY(holder.videoSurface());
    QVERIFY(holder.videoSurface() == surface); // Should not have changed surface

    // Now create a second surface and assign it as the source
    // The old surface holder should be zeroed
    SurfaceHolder holder2(this);
    videoOutput2->setProperty("source", QVariant::fromValue(static_cast<QObject*>(&holder2)));

    QCOMPARE(holder.videoSurface(), static_cast<QAbstractVideoSurface*>(0));
    QVERIFY(holder2.videoSurface() != 0);

    // Finally a combination - set the same source to two things, then assign a new source
    // to the first output - should not reset the first source
    videoOutput = component.create();
    videoOutput->setProperty("source", QVariant::fromValue(static_cast<QObject*>(&holder2)));

    // Both vo and vo2 were pointed to holder2 - setting vo2 should not clear holder2
    QVERIFY(holder2.videoSurface() != 0);
    QVERIFY(holder.videoSurface() == 0);
    videoOutput2->setProperty("source", QVariant::fromValue(static_cast<QObject*>(&holder)));
    QVERIFY(holder2.videoSurface() != 0);
    QVERIFY(holder.videoSurface() != 0);

    // They should also be independent
    QVERIFY(holder.videoSurface() != holder2.videoSurface());

    delete videoOutput;
    delete videoOutput2;
}

static const uchar rgb32ImageData[] =
{//  B     G     R     A
    0x00, 0x01, 0x02, 0xff, 0x03, 0x04, 0x05, 0xff,
    0x06, 0x07, 0x08, 0xff, 0x09, 0x0a, 0x0b, 0xff
};

void tst_QDeclarativeVideoOutput::paintSurface()
{
    QQuickView window;
    window.setSource(QUrl("qrc:/main.qml"));
    window.show();
    QVERIFY(QTest::qWaitForWindowExposed(&window));

    auto videoOutput = qobject_cast<QDeclarativeVideoOutput *>(window.rootObject());
    QVERIFY(videoOutput);

    auto surface = videoOutput->property("videoSurface").value<QAbstractVideoSurface *>();
    QVERIFY(surface);
    QVERIFY(!surface->isActive());
    videoOutput->setSize(QSize(2, 2));
    QVideoSurfaceFormat format(QSize(2, 2), QVideoFrame::Format_RGB32);
    QVERIFY(surface->isFormatSupported(format));
    QVERIFY(surface->start(format));
    QVERIFY(surface->isActive());

    QImage img(rgb32ImageData, 2, 2, 8, QImage::Format_RGB32);
    QVERIFY(surface->present(img));

    if (QGuiApplication::platformName() == QLatin1String("offscreen")
        || QGuiApplication::platformName() == QLatin1String("minimal"))
        return;

    QImage capture = window.grabWindow();
    QCOMPARE(capture.pixelColor(0, 0), QColor(rgb32ImageData[2], rgb32ImageData[1], rgb32ImageData[0], rgb32ImageData[3]));
    QCOMPARE(capture.pixelColor(1, 0), QColor(rgb32ImageData[6], rgb32ImageData[5], rgb32ImageData[4], rgb32ImageData[7]));
    QCOMPARE(capture.pixelColor(0, 1), QColor(rgb32ImageData[10], rgb32ImageData[9], rgb32ImageData[8], rgb32ImageData[11]));
    QCOMPARE(capture.pixelColor(1, 1), QColor(rgb32ImageData[14], rgb32ImageData[13], rgb32ImageData[12], rgb32ImageData[15]));
}

void tst_QDeclarativeVideoOutput::sourceRect()
{
    QQmlComponent component(&m_engine);
    component.loadUrl(QUrl("qrc:/main.qml"));

    QObject *videoOutput = component.create();
    QVERIFY(videoOutput != 0);

    SurfaceHolder holder(this);

    QSignalSpy propSpy(videoOutput, SIGNAL(sourceRectChanged()));

    videoOutput->setProperty("source", QVariant::fromValue(static_cast<QObject*>(&holder)));

    QRectF invalid(0,0,-1,-1);

    QCOMPARE(videoOutput->property("sourceRect").toRectF(), invalid);

    holder.presentDummyFrame(QSize(200,100));

    QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100));
    QCOMPARE(propSpy.count(), 1);

    // Another frame shouldn't cause a source rect change
    holder.presentDummyFrame(QSize(200,100));
    QCOMPARE(propSpy.count(), 1);
    QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100));

    // Changing orientation and stretch modes should not affect this
    videoOutput->setProperty("orientation", QVariant(90));
    updateOutputGeometry(videoOutput);
    QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100));

    videoOutput->setProperty("orientation", QVariant(180));
    updateOutputGeometry(videoOutput);
    QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100));

    videoOutput->setProperty("orientation", QVariant(270));
    updateOutputGeometry(videoOutput);
    QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100));

    videoOutput->setProperty("orientation", QVariant(-90));
    updateOutputGeometry(videoOutput);
    QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100));

    videoOutput->setProperty("fillMode", QVariant(int(QDeclarativeVideoOutput::PreserveAspectCrop)));
    updateOutputGeometry(videoOutput);
    QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100));

    videoOutput->setProperty("fillMode", QVariant(int(QDeclarativeVideoOutput::Stretch)));
    updateOutputGeometry(videoOutput);
    QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100));

    videoOutput->setProperty("fillMode", QVariant(int(QDeclarativeVideoOutput::Stretch)));
    updateOutputGeometry(videoOutput);
    QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100));

    delete videoOutput;
}

void tst_QDeclarativeVideoOutput::mappingPoint()
{
    QFETCH(QPointF, point);
    QFETCH(int, orientation);
    QFETCH(QDeclarativeVideoOutput::FillMode, fillMode);
    QFETCH(QPointF, expected);

    QVERIFY(m_mappingOutput);
    m_mappingOutput->setProperty("orientation", QVariant(orientation));
    m_mappingOutput->setProperty("fillMode", QVariant::fromValue(fillMode));

    updateOutputGeometry(m_mappingOutput);

    QPointF output = invokeP2P(m_mappingOutput, "mapPointToItem", point);
    QPointF reverse = invokeP2P(m_mappingOutput, "mapPointToSource", output);

    QCOMPARE(output, expected);
    QCOMPARE(reverse, point);

    // Now the normalized versions
    // Source rectangle is 200x100
    QPointF normal(point.x() / 200, point.y() / 100);

    output = invokeP2P(m_mappingOutput, "mapNormalizedPointToItem", normal);
    reverse = invokeP2P(m_mappingOutput, "mapPointToSourceNormalized", output);

    QCOMPARE(output, expected);
    QCOMPARE(reverse, normal);
}

void tst_QDeclarativeVideoOutput::mappingPoint_data()
{
    QTest::addColumn<QPointF>("point");
    QTest::addColumn<int>("orientation");
    QTest::addColumn<QDeclarativeVideoOutput::FillMode>("fillMode");
    QTest::addColumn<QPointF>("expected");

    QDeclarativeVideoOutput::FillMode stretch = QDeclarativeVideoOutput::Stretch;
    QDeclarativeVideoOutput::FillMode fit = QDeclarativeVideoOutput::PreserveAspectFit;
    QDeclarativeVideoOutput::FillMode crop = QDeclarativeVideoOutput::PreserveAspectCrop;

    // First make sure the component has processed the frame
    QCOMPARE(m_mappingOutput->property("sourceRect").toRectF(), QRectF(0,0,200,100));

    // 200x100 -> 150,100 stretch, 150x75 fit @ 12.5f, 200x100 @-25,0 crop

    // Corners, then the center, then a point in the middle somewhere
    QTest::newRow("s0-0") << QPointF(0,0) << 0 << stretch << QPointF(0,0);
    QTest::newRow("s1-0") << QPointF(200,0) << 0 << stretch << QPointF(150,0);
    QTest::newRow("s2-0") << QPointF(0,100) << 0 << stretch << QPointF(0,100);
    QTest::newRow("s3-0") << QPointF(200,100) << 0 << stretch << QPointF(150,100);
    QTest::newRow("s4-0") << QPointF(100,50) << 0 << stretch << QPointF(75,50);
    QTest::newRow("s5-0") << QPointF(40,80) << 0 << stretch << QPointF(30,80);

    QTest::newRow("f0-0") << QPointF(0,0) << 0 << fit << QPointF(0,12.5f);
    QTest::newRow("f1-0") << QPointF(200,0) << 0 << fit << QPointF(150,12.5f);
    QTest::newRow("f2-0") << QPointF(0,100) << 0 << fit << QPointF(0,87.5f);
    QTest::newRow("f3-0") << QPointF(200,100) << 0 << fit << QPointF(150,87.5f);
    QTest::newRow("f4-0") << QPointF(100,50) << 0 << stretch << QPointF(75,50);
    QTest::newRow("f5-0") << QPointF(40,80) << 0 << stretch << QPointF(30,80);

    QTest::newRow("c0-0") << QPointF(0,0) << 0 << crop << QPointF(-25,0);
    QTest::newRow("c1-0") << QPointF(200,0) << 0 << crop << QPointF(175,0);
    QTest::newRow("c2-0") << QPointF(0,100) << 0 << crop << QPointF(-25,100);
    QTest::newRow("c3-0") << QPointF(200,100) << 0 << crop << QPointF(175,100);
    QTest::newRow("c4-0") << QPointF(100,50) << 0 << stretch << QPointF(75,50);
    QTest::newRow("c5-0") << QPointF(40,80) << 0 << stretch << QPointF(30,80);

    // 90 degrees (anti clockwise)
    QTest::newRow("s0-90") << QPointF(0,0) << 90 << stretch << QPointF(0,100);
    QTest::newRow("s1-90") << QPointF(200,0) << 90 << stretch << QPointF(0,0);
    QTest::newRow("s2-90") << QPointF(0,100) << 90 << stretch << QPointF(150,100);
    QTest::newRow("s3-90") << QPointF(200,100) << 90 << stretch << QPointF(150,0);
    QTest::newRow("s4-90") << QPointF(100,50) << 90 << stretch << QPointF(75,50);
    QTest::newRow("s5-90") << QPointF(40,80) << 90 << stretch << QPointF(120,80);

    QTest::newRow("f0-90") << QPointF(0,0) << 90 << fit << QPointF(50,100);
    QTest::newRow("f1-90") << QPointF(200,0) << 90 << fit << QPointF(50,0);
    QTest::newRow("f2-90") << QPointF(0,100) << 90 << fit << QPointF(100,100);
    QTest::newRow("f3-90") << QPointF(200,100) << 90 << fit << QPointF(100,0);
    QTest::newRow("f4-90") << QPointF(100,50) << 90 << fit << QPointF(75,50);
    QTest::newRow("f5-90") << QPointF(40,80) << 90 << fit << QPointF(90,80);

    QTest::newRow("c0-90") << QPointF(0,0) << 90 << crop << QPointF(0,200);
    QTest::newRow("c1-90") << QPointF(200,0) << 90 << crop << QPointF(0,-100);
    QTest::newRow("c2-90") << QPointF(0,100) << 90 << crop << QPointF(150,200);
    QTest::newRow("c3-90") << QPointF(200,100) << 90 << crop << QPointF(150,-100);
    QTest::newRow("c4-90") << QPointF(100,50) << 90 << crop << QPointF(75,50);
    QTest::newRow("c5-90") << QPointF(40,80) << 90 << crop << QPointF(120,140);

    // 180
    QTest::newRow("s0-180") << QPointF(0,0) << 180 << stretch << QPointF(150,100);
    QTest::newRow("s1-180") << QPointF(200,0) << 180 << stretch << QPointF(0,100);
    QTest::newRow("s2-180") << QPointF(0,100) << 180 << stretch << QPointF(150,0);
    QTest::newRow("s3-180") << QPointF(200,100) << 180 << stretch << QPointF(0,0);
    QTest::newRow("s4-180") << QPointF(100,50) << 180 << stretch << QPointF(75,50);
    QTest::newRow("s5-180") << QPointF(40,80) << 180 << stretch << QPointF(120,20);

    QTest::newRow("f0-180") << QPointF(0,0) << 180 << fit << QPointF(150,87.5f);
    QTest::newRow("f1-180") << QPointF(200,0) << 180 << fit << QPointF(0,87.5f);
    QTest::newRow("f2-180") << QPointF(0,100) << 180 << fit << QPointF(150,12.5f);
    QTest::newRow("f3-180") << QPointF(200,100) << 180 << fit << QPointF(0,12.5f);
    QTest::newRow("f4-180") << QPointF(100,50) << 180 << fit << QPointF(75,50);
    QTest::newRow("f5-180") << QPointF(40,80) << 180 << fit << QPointF(120,27.5f);

    QTest::newRow("c0-180") << QPointF(0,0) << 180 << crop << QPointF(175,100);
    QTest::newRow("c1-180") << QPointF(200,0) << 180 << crop << QPointF(-25,100);
    QTest::newRow("c2-180") << QPointF(0,100) << 180 << crop << QPointF(175,0);
    QTest::newRow("c3-180") << QPointF(200,100) << 180 << crop << QPointF(-25,0);
    QTest::newRow("c4-180") << QPointF(100,50) << 180 << crop << QPointF(75,50);
    QTest::newRow("c5-180") << QPointF(40,80) << 180 << crop << QPointF(135,20);

    // 270
    QTest::newRow("s0-270") << QPointF(0,0) << 270 << stretch << QPointF(150,0);
    QTest::newRow("s1-270") << QPointF(200,0) << 270 << stretch << QPointF(150,100);
    QTest::newRow("s2-270") << QPointF(0,100) << 270 << stretch << QPointF(0,0);
    QTest::newRow("s3-270") << QPointF(200,100) << 270 << stretch << QPointF(0,100);
    QTest::newRow("s4-270") << QPointF(100,50) << 270 << stretch << QPointF(75,50);
    QTest::newRow("s5-270") << QPointF(40,80) << 270 << stretch << QPointF(30,20);

    QTest::newRow("f0-270") << QPointF(0,0) << 270 << fit << QPointF(100,0);
    QTest::newRow("f1-270") << QPointF(200,0) << 270 << fit << QPointF(100,100);
    QTest::newRow("f2-270") << QPointF(0,100) << 270 << fit << QPointF(50,0);
    QTest::newRow("f3-270") << QPointF(200,100) << 270 << fit << QPointF(50,100);
    QTest::newRow("f4-270") << QPointF(100,50) << 270 << fit << QPointF(75,50);
    QTest::newRow("f5-270") << QPointF(40,80) << 270 << fit << QPointF(60,20);

    QTest::newRow("c0-270") << QPointF(0,0) << 270 << crop << QPointF(150,-100);
    QTest::newRow("c1-270") << QPointF(200,0) << 270 << crop << QPointF(150,200);
    QTest::newRow("c2-270") << QPointF(0,100) << 270 << crop << QPointF(0,-100);
    QTest::newRow("c3-270") << QPointF(200,100) << 270 << crop << QPointF(0,200);
    QTest::newRow("c4-270") << QPointF(100,50) << 270 << crop << QPointF(75,50);
    QTest::newRow("c5-270") << QPointF(40,80) << 270 << crop << QPointF(30,-40);
}

/* Test all rectangle mapping */
void tst_QDeclarativeVideoOutput::mappingRect()
{
    QFETCH(QRectF, rect);
    QFETCH(int, orientation);
    QFETCH(QDeclarativeVideoOutput::FillMode, fillMode);
    QFETCH(QRectF, expected);

    QVERIFY(m_mappingOutput);
    m_mappingOutput->setProperty("orientation", QVariant(orientation));
    m_mappingOutput->setProperty("fillMode", QVariant::fromValue(fillMode));

    updateOutputGeometry(m_mappingOutput);

    QRectF output = invokeR2R(m_mappingOutput, "mapRectToItem", rect);
    QRectF reverse = invokeR2R(m_mappingOutput, "mapRectToSource", output);

    QCOMPARE(output, expected);
    QCOMPARE(reverse, rect);

    // Now the normalized versions
    // Source rectangle is 200x100
    QRectF normal(rect.x() / 200, rect.y() / 100, rect.width() / 200, rect.height() / 100);

    output = invokeR2R(m_mappingOutput, "mapNormalizedRectToItem", normal);
    reverse = invokeR2R(m_mappingOutput, "mapRectToSourceNormalized", output);

    QCOMPARE(output, expected);
    QCOMPARE(reverse, normal);
}

void tst_QDeclarativeVideoOutput::mappingRect_data()
{
    QTest::addColumn<QRectF>("rect");
    QTest::addColumn<int>("orientation");
    QTest::addColumn<QDeclarativeVideoOutput::FillMode>("fillMode");
    QTest::addColumn<QRectF>("expected");

    // First make sure the component has processed the frame
    QCOMPARE(m_mappingOutput->property("sourceRect").toRectF(), QRectF(0,0,200,100));

    QDeclarativeVideoOutput::FillMode stretch = QDeclarativeVideoOutput::Stretch;
    QDeclarativeVideoOutput::FillMode fit = QDeclarativeVideoOutput::PreserveAspectFit;
    QDeclarativeVideoOutput::FillMode crop = QDeclarativeVideoOutput::PreserveAspectCrop;

    // Full rectangle mapping
    // Stretch
    QTest::newRow("s0")   << QRectF(0,0, 200, 100) << 0 << stretch << QRectF(0,0,150,100);
    QTest::newRow("s90")  << QRectF(0,0, 200, 100) << 90 << stretch << QRectF(0,0,150,100);
    QTest::newRow("s180") << QRectF(0,0, 200, 100) << 180 << stretch << QRectF(0,0,150,100);
    QTest::newRow("s270") << QRectF(0,0, 200, 100) << 270 << stretch << QRectF(0,0,150,100);

    // Fit
    QTest::newRow("f0")   << QRectF(0,0, 200, 100) << 0 << fit << QRectF(0,12.5f,150,75);
    QTest::newRow("f90") << QRectF(0,0, 200, 100) << 90 << fit << QRectF(50,0,50,100);
    QTest::newRow("f180") << QRectF(0,0, 200, 100) << 180 << fit << QRectF(0,12.5f,150,75);
    QTest::newRow("f270")  << QRectF(0,0, 200, 100) << 270 << fit << QRectF(50,0,50,100);

    // Crop
    QTest::newRow("c0")   << QRectF(0,0, 200, 100) << 0 << crop << QRectF(-25,0,200,100);
    QTest::newRow("c90") << QRectF(0,0, 200, 100) << 90 << crop << QRectF(0,-100,150,300);
    QTest::newRow("c180") << QRectF(0,0, 200, 100) << 180 << crop << QRectF(-25,0,200,100);
    QTest::newRow("c270")  << QRectF(0,0, 200, 100) << 270 << crop << QRectF(0,-100,150,300);

    // Partial rectangle mapping
    // Stretch
    // 50-130 in x (0.25 - 0.65), 25-50 (0.25 - 0.5) in y (out of 200, 100) -> 150x100
    QTest::newRow("p-s0")   << QRectF(50, 25, 80, 25) << 0 << stretch << QRectF(37.5f,25,60,25);
    QTest::newRow("p-s90") << QRectF(50, 25, 80, 25) << 90 << stretch << QRectF(37.5f,35,37.5f,40);
    QTest::newRow("p-s180") << QRectF(50, 25, 80, 25) << 180 << stretch << QRectF(52.5f,50,60,25);
    QTest::newRow("p-s270")  << QRectF(50, 25, 80, 25) << 270 << stretch << QRectF(75,25,37.5f,40);

    // Fit
    QTest::newRow("p-f0")   << QRectF(50, 25, 80, 25) << 0 << fit << QRectF(37.5f,31.25f,60,18.75f);
    QTest::newRow("p-f90")  << QRectF(50, 25, 80, 25) << 90 << fit << QRectF(62.5f,35,12.5f,40);
    QTest::newRow("p-f180") << QRectF(50, 25, 80, 25) << 180 << fit << QRectF(52.5f,50,60,18.75f);
    QTest::newRow("p-f270") << QRectF(50, 25, 80, 25) << 270 << fit << QRectF(75,25,12.5f,40);

    // Crop
    QTest::newRow("p-c0")   << QRectF(50, 25, 80, 25) << 0 << crop << QRectF(25,25,80,25);
    QTest::newRow("p-c90")  << QRectF(50, 25, 80, 25) << 90 << crop << QRectF(37.5f,5,37.5f,120);
    QTest::newRow("p-c180") << QRectF(50, 25, 80, 25) << 180 << crop << QRectF(45,50,80,25);
    QTest::newRow("p-c270") << QRectF(50, 25, 80, 25) << 270 << crop << QRectF(75,-25,37.5f,120);
}

void tst_QDeclarativeVideoOutput::updateOutputGeometry(QObject *output)
{
    // Since the object isn't visible, update() doesn't do anything
    // so we manually force this
    QMetaObject::invokeMethod(output, "_q_updateGeometry");
}

void tst_QDeclarativeVideoOutput::contentRect()
{
    QFETCH(int, orientation);
    QFETCH(QDeclarativeVideoOutput::FillMode, fillMode);
    QFETCH(QRectF, expected);

    QVERIFY(m_mappingOutput);
    m_mappingOutput->setProperty("orientation", QVariant(orientation));
    m_mappingOutput->setProperty("fillMode", QVariant::fromValue(fillMode));

    updateOutputGeometry(m_mappingOutput);

    QRectF output = m_mappingOutput->property("contentRect").toRectF();
    QCOMPARE(output, expected);
}

void tst_QDeclarativeVideoOutput::contentRect_data()
{
    QTest::addColumn<int>("orientation");
    QTest::addColumn<QDeclarativeVideoOutput::FillMode>("fillMode");
    QTest::addColumn<QRectF>("expected");

    // First make sure the component has processed the frame
    QCOMPARE(m_mappingOutput->property("sourceRect").toRectF(), QRectF(0,0,200,100));

    QDeclarativeVideoOutput::FillMode stretch = QDeclarativeVideoOutput::Stretch;
    QDeclarativeVideoOutput::FillMode fit = QDeclarativeVideoOutput::PreserveAspectFit;
    QDeclarativeVideoOutput::FillMode crop = QDeclarativeVideoOutput::PreserveAspectCrop;

    // Stretch just keeps the full render rect regardless of orientation
    QTest::newRow("s0") << 0 << stretch << QRectF(0,0,150,100);
    QTest::newRow("s90") << 90 << stretch << QRectF(0,0,150,100);
    QTest::newRow("s180") << 180 << stretch << QRectF(0,0,150,100);
    QTest::newRow("s270") << 270 << stretch << QRectF(0,0,150,100);

    // Fit depends on orientation
    // Source is 200x100, fitting in 150x100 -> 150x75
    // or 100x200 -> 50x100
    QTest::newRow("f0") << 0 << fit << QRectF(0,12.5f,150,75);
    QTest::newRow("f90") << 90 << fit << QRectF(50,0,50,100);
    QTest::newRow("f180") << 180 << fit << QRectF(0,12.5,150,75);
    QTest::newRow("f270") << 270 << fit << QRectF(50,0,50,100);

    // Crop also depends on orientation, may go outside render rect
    // 200x100 -> -25,0 200x100
    // 100x200 -> 0,-100 150x300
    QTest::newRow("c0") << 0 << crop << QRectF(-25,0,200,100);
    QTest::newRow("c90") << 90 << crop << QRectF(0,-100,150,300);
    QTest::newRow("c180") << 180 << crop << QRectF(-25,0,200,100);
    QTest::newRow("c270") << 270 << crop << QRectF(0,-100,150,300);
}


QRectF tst_QDeclarativeVideoOutput::invokeR2R(QObject *object, const char *signature, const QRectF &rect)
{
    QRectF r;
    QMetaObject::invokeMethod(object, signature, Q_RETURN_ARG(QRectF, r), Q_ARG(QRectF, rect));
    return r;
}

QPointF tst_QDeclarativeVideoOutput::invokeP2P(QObject *object, const char *signature, const QPointF &point)
{
    QPointF p;
    QMetaObject::invokeMethod(object, signature, Q_RETURN_ARG(QPointF, p), Q_ARG(QPointF, point));
    return p;
}


QTEST_MAIN(tst_QDeclarativeVideoOutput)

#include "tst_qdeclarativevideooutput.moc"
