/****************************************************************************
**
** 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 "qdeclarativeaudio_p.h"
#include "qdeclarativemediametadata_p.h"

#include "mockmediaserviceprovider.h"
#include "mockmediaplayerservice.h"

#include <QtMultimedia/qmediametadata.h>
#include <qmediaplayercontrol.h>
#include <qmediaservice.h>
#include <private/qmediaserviceprovider_p.h>
#include <private/qdeclarativevideooutput_p.h>
#include <qmetadatareadercontrol.h>
#include <QAbstractVideoSurface>

#include <QtGui/qguiapplication.h>
#include <QtQml/qqmlengine.h>
#include <QtQml/qqmlcomponent.h>

class tst_QDeclarativeAudio : public QObject
{
    Q_OBJECT
public slots:
    void initTestCase();

private slots:
    void nullPlayerControl();
    void nullMetaDataControl();
    void nullService();

    void source();
    void autoLoad();
    void playing();
    void paused();
    void duration();
    void position();
    void volume();
    void muted();
    void bufferProgress();
    void seekable();
    void playbackRate();
    void status();
    void metaData_data();
    void metaData();
    void error();
    void loops();
    void audioRole();
    void customAudioRole();
    void videoOutput();

private:
    void enumerator(const QMetaObject *object, const char *name, QMetaEnum *result);
    QMetaEnum enumerator(const QMetaObject *object, const char *name);
    void keyToValue(const QMetaEnum &enumeration, const char *key, int *result);
    int keyToValue(const QMetaEnum &enumeration, const char *key);
};

Q_DECLARE_METATYPE(QDeclarativeAudio::Error);
Q_DECLARE_METATYPE(QDeclarativeAudio::AudioRole);

class QtTestMediaPlayerControl : public QMediaPlayerControl
{
    Q_OBJECT
public:
    QtTestMediaPlayerControl(QObject *parent = 0)
        : QMediaPlayerControl(parent)
        , m_state(QMediaPlayer::StoppedState)
        , m_mediaStatus(QMediaPlayer::NoMedia)
        , m_duration(0)
        , m_position(0)
        , m_playbackRate(1.0)
        , m_volume(100)
        , m_bufferStatus(0)
        , m_muted(false)
        , m_audioAvailable(false)
        , m_videoAvailable(false)
        , m_seekable(false)
    {
    }

    QMediaPlayer::State state() const { return m_state; }
    void updateState(QMediaPlayer::State state) { emit stateChanged(m_state = state); }

    QMediaPlayer::MediaStatus mediaStatus() const { return m_mediaStatus; }
    void updateMediaStatus(QMediaPlayer::MediaStatus status) {
        emit mediaStatusChanged(m_mediaStatus = status); }
    void updateMediaStatus(QMediaPlayer::MediaStatus status, QMediaPlayer::State state)
    {
        m_mediaStatus = status;
        m_state = state;

        emit mediaStatusChanged(m_mediaStatus);
        emit stateChanged(m_state);
    }

    qint64 duration() const { return m_duration; }
    void setDuration(qint64 duration) { emit durationChanged(m_duration = duration); }

    qint64 position() const { return m_position; }
    void setPosition(qint64 position) { emit positionChanged(m_position = position); }

    int volume() const { return m_volume; }
    void setVolume(int volume) { emit volumeChanged(m_volume = volume); }

    bool isMuted() const { return m_muted; }
    void setMuted(bool muted) { emit mutedChanged(m_muted = muted); }

    int bufferStatus() const { return m_bufferStatus; }
    void setBufferStatus(int status) { emit bufferStatusChanged(m_bufferStatus = status); }

    bool isAudioAvailable() const { return m_audioAvailable; }
    void setAudioAvailable(bool available) {
        emit audioAvailableChanged(m_audioAvailable = available); }
    bool isVideoAvailable() const { return m_videoAvailable; }
    void setVideoAvailable(bool available) {
        emit videoAvailableChanged(m_videoAvailable = available); }

    bool isSeekable() const { return m_seekable; }
    void setSeekable(bool seekable) { emit seekableChanged(m_seekable = seekable); }

    QMediaTimeRange availablePlaybackRanges() const { return QMediaTimeRange(); }

    qreal playbackRate() const { return m_playbackRate; }
    void setPlaybackRate(qreal rate) { emit playbackRateChanged(m_playbackRate = rate); }

    QMediaContent media() const { return m_media; }
    const QIODevice *mediaStream() const { return 0; }
    void setMedia(const QMediaContent &media, QIODevice *)
    {
        m_media = media;

        m_mediaStatus = m_media.isNull()
                ? QMediaPlayer::NoMedia
                : QMediaPlayer::LoadingMedia;

        emit mediaChanged(m_media);
        emit mediaStatusChanged(m_mediaStatus);
    }

    void play()
    {
        m_state = QMediaPlayer::PlayingState;
        if (m_mediaStatus == QMediaPlayer::EndOfMedia)
            updateMediaStatus(QMediaPlayer::LoadedMedia);
        emit stateChanged(m_state);
    }
    void pause() { emit stateChanged(m_state = QMediaPlayer::PausedState); }
    void stop() { emit stateChanged(m_state = QMediaPlayer::StoppedState); }

    void emitError(QMediaPlayer::Error err, const QString &errorString) {
        emit error(err, errorString); }

private:
    QMediaPlayer::State m_state;
    QMediaPlayer::MediaStatus m_mediaStatus;
    qint64 m_duration;
    qint64 m_position;
    qreal m_playbackRate;
    int m_volume;
    int m_bufferStatus;
    bool m_muted;
    bool m_audioAvailable;
    bool m_videoAvailable;
    bool m_seekable;
    QMediaContent m_media;
};

class QtTestMetaDataControl : public QMetaDataReaderControl
{
    Q_OBJECT
public:
    QtTestMetaDataControl(QObject *parent = 0)
        : QMetaDataReaderControl(parent)
    {
    }

    bool isMetaDataAvailable() const { return true; }

    QVariant metaData(const QString &key) const { return m_metaData.value(key); }
    void setMetaData(const QString &key, const QVariant &value) {
        m_metaData.insert(key, value); emit metaDataChanged(); }

    QStringList availableMetaData() const { return m_metaData.keys(); }

private:
    QMap<QString, QVariant> m_metaData;
};

class QtTestMediaService : public QMediaService
{
    Q_OBJECT
public:
    QtTestMediaService(
            QtTestMediaPlayerControl *playerControl,
            QtTestMetaDataControl *metaDataControl,
            QObject *parent)
        : QMediaService(parent)
        , playerControl(playerControl)
        , metaDataControl(metaDataControl)
    {
    }

    QMediaControl *requestControl(const char *name)
    {
        if (qstrcmp(name, QMediaPlayerControl_iid) == 0)
            return playerControl;
        else if (qstrcmp(name, QMetaDataReaderControl_iid) == 0)
            return metaDataControl;
        else
            return 0;
    }

    void releaseControl(QMediaControl *) {}

    QtTestMediaPlayerControl *playerControl;
    QtTestMetaDataControl *metaDataControl;
};

class QtTestMediaServiceProvider : public QMediaServiceProvider
{
    Q_OBJECT
public:
    QtTestMediaServiceProvider()
        : service(new QtTestMediaService(
                new QtTestMediaPlayerControl(this), new QtTestMetaDataControl(this), this))
    {
        setDefaultServiceProvider(this);
    }

    QtTestMediaServiceProvider(QtTestMediaService *service)
        : service(service)
    {
        setDefaultServiceProvider(this);
    }

    QtTestMediaServiceProvider(
            QtTestMediaPlayerControl *playerControl, QtTestMetaDataControl *metaDataControl)
        : service(new QtTestMediaService(playerControl, metaDataControl, this))
    {
        setDefaultServiceProvider(this);
    }

    ~QtTestMediaServiceProvider()
    {
        setDefaultServiceProvider(0);
    }

    QMediaService *requestService(
            const QByteArray &type,
            const QMediaServiceProviderHint & = QMediaServiceProviderHint())
    {
        requestedService = type;

        return service;
    }

    void releaseService(QMediaService *) {}

    inline QtTestMediaPlayerControl *playerControl() { return service->playerControl; }
    inline QtTestMetaDataControl *metaDataControl() { return service->metaDataControl; }

    QtTestMediaService *service;
    QByteArray requestedService;
};

void tst_QDeclarativeAudio::initTestCase()
{
    qRegisterMetaType<QDeclarativeAudio::Error>();
    qRegisterMetaType<QDeclarativeAudio::AudioRole>();
}

void tst_QDeclarativeAudio::nullPlayerControl()
{
    QtTestMetaDataControl metaDataControl;
    QtTestMediaServiceProvider provider(0, &metaDataControl);

    QDeclarativeAudio audio;
    audio.classBegin();

    QCOMPARE(audio.source(), QUrl());
    audio.setSource(QUrl("http://example.com"));
    QCOMPARE(audio.source(), QUrl("http://example.com"));

    QCOMPARE(audio.playbackState(), audio.StoppedState);
    audio.play();
    QCOMPARE(audio.playbackState(), audio.StoppedState);
    audio.pause();
    QCOMPARE(audio.playbackState(), audio.StoppedState);

    QCOMPARE(audio.duration(), 0);

    QCOMPARE(audio.position(), 0);
    audio.seek(10000);
    QCOMPARE(audio.position(), 10000);

    QCOMPARE(audio.volume(), qreal(1.0));
    audio.setVolume(0.5);
    QCOMPARE(audio.volume(), qreal(0.5));

    QCOMPARE(audio.isMuted(), false);
    audio.setMuted(true);
    QCOMPARE(audio.isMuted(), true);

    QCOMPARE(audio.bufferProgress(), qreal(0));

    QCOMPARE(audio.isSeekable(), false);

    QCOMPARE(audio.playbackRate(), qreal(1.0));

    QCOMPARE(audio.status(), QDeclarativeAudio::NoMedia);

    QCOMPARE(audio.error(), QDeclarativeAudio::ServiceMissing);
}

void tst_QDeclarativeAudio::nullMetaDataControl()
{
    QtTestMediaPlayerControl playerControl;
    QtTestMediaServiceProvider provider(&playerControl, 0);

    QDeclarativeAudio audio;
    audio.classBegin();
    audio.componentComplete();

    QVERIFY(audio.metaData());
}

void tst_QDeclarativeAudio::nullService()
{
    QtTestMediaServiceProvider provider(0);

    QDeclarativeAudio audio;
    audio.classBegin();

    QCOMPARE(audio.source(), QUrl());
    audio.setSource(QUrl("http://example.com"));
    QCOMPARE(audio.source(), QUrl("http://example.com"));

    QCOMPARE(audio.playbackState(), audio.StoppedState);
    audio.play();
    QCOMPARE(audio.playbackState(), audio.StoppedState);
    audio.pause();
    QCOMPARE(audio.playbackState(), audio.StoppedState);

    QCOMPARE(audio.duration(), 0);

    QCOMPARE(audio.position(), 0);
    audio.seek(10000);
    QCOMPARE(audio.position(), 10000);

    QCOMPARE(audio.volume(), qreal(1.0));
    audio.setVolume(0.5);
    QCOMPARE(audio.volume(), qreal(0.5));

    QCOMPARE(audio.isMuted(), false);
    audio.setMuted(true);
    QCOMPARE(audio.isMuted(), true);

    QCOMPARE(audio.bufferProgress(), qreal(0));

    QCOMPARE(audio.isSeekable(), false);

    QCOMPARE(audio.playbackRate(), qreal(1.0));

    QCOMPARE(audio.status(), QDeclarativeAudio::NoMedia);

    QCOMPARE(audio.error(), QDeclarativeAudio::ServiceMissing);

    QVERIFY(audio.metaData());
}

void tst_QDeclarativeAudio::source()
{
    const QUrl url1("http://example.com");
    const QUrl url2("file:///local/path");
    const QUrl url3;

    QtTestMediaServiceProvider provider;
    QDeclarativeAudio audio;
    audio.classBegin();
    audio.componentComplete();

    QSignalSpy spy(&audio, SIGNAL(sourceChanged()));

    audio.setSource(url1);
    QCOMPARE(audio.source(), url1);
    QCOMPARE(provider.playerControl()->media().request().url(), url1);
    QCOMPARE(spy.count(), 1);

    audio.setSource(url2);
    QCOMPARE(audio.source(), url2);
    QCOMPARE(provider.playerControl()->media().request().url(), url2);
    QCOMPARE(spy.count(), 2);

    audio.setSource(url3);
    QCOMPARE(audio.source(), url3);
    QCOMPARE(provider.playerControl()->media().request().url(), url3);
    QCOMPARE(spy.count(), 3);
}

void tst_QDeclarativeAudio::autoLoad()
{
    QtTestMediaServiceProvider provider;
    QDeclarativeAudio audio;
    audio.classBegin();
    audio.componentComplete();

    QSignalSpy spy(&audio, SIGNAL(autoLoadChanged()));

    QCOMPARE(audio.isAutoLoad(), true);

    audio.setAutoLoad(false);
    QCOMPARE(audio.isAutoLoad(), false);
    QCOMPARE(spy.count(), 1);

    audio.setSource(QUrl("http://example.com"));
    QCOMPARE(audio.source(), QUrl("http://example.com"));
    audio.play();
    QCOMPARE(audio.playbackState(), audio.PlayingState);
    audio.stop();

    audio.setAutoLoad(true);
    audio.setSource(QUrl("http://example.com"));
    audio.pause();
    QCOMPARE(spy.count(), 2);
    QCOMPARE(audio.playbackState(), audio.PausedState);
}

void tst_QDeclarativeAudio::playing()
{
    QtTestMediaServiceProvider provider;
    QDeclarativeAudio audio;
    audio.classBegin();

    QSignalSpy stateChangedSpy(&audio, SIGNAL(playbackStateChanged()));
    QSignalSpy playingSpy(&audio, SIGNAL(playing()));
    QSignalSpy stoppedSpy(&audio, SIGNAL(stopped()));

    int stateChanged = 0;
    int playing = 0;
    int stopped = 0;

    audio.componentComplete();
    audio.setSource(QUrl("http://example.com"));

    QCOMPARE(audio.playbackState(), audio.StoppedState);

    // play() when stopped.
    audio.play();
    QCOMPARE(audio.playbackState(), audio.PlayingState);
    QCOMPARE(provider.playerControl()->state(), QMediaPlayer::PlayingState);
    QCOMPARE(stateChangedSpy.count(), ++stateChanged);
    QCOMPARE(playingSpy.count(),        ++playing);
    QCOMPARE(stoppedSpy.count(),          stopped);

    // stop() when playing.
    audio.stop();
    QCOMPARE(audio.playbackState(), audio.StoppedState);
    QCOMPARE(provider.playerControl()->state(), QMediaPlayer::StoppedState);
    QCOMPARE(stateChangedSpy.count(), ++stateChanged);
    QCOMPARE(playingSpy.count(),          playing);
    QCOMPARE(stoppedSpy.count(),        ++stopped);

    // stop() when stopped.
    audio.stop();
    QCOMPARE(audio.playbackState(), audio.StoppedState);
    QCOMPARE(provider.playerControl()->state(), QMediaPlayer::StoppedState);
    QCOMPARE(stateChangedSpy.count(),   stateChanged);
    QCOMPARE(playingSpy.count(),          playing);
    QCOMPARE(stoppedSpy.count(),          stopped);

    audio.play();
    QCOMPARE(audio.playbackState(), audio.PlayingState);
    QCOMPARE(provider.playerControl()->state(), QMediaPlayer::PlayingState);
    QCOMPARE(stateChangedSpy.count(), ++stateChanged);
    QCOMPARE(playingSpy.count(),        ++playing);
    QCOMPARE(stoppedSpy.count(),          stopped);

    // play() when playing.
    audio.play();
    QCOMPARE(audio.playbackState(), audio.PlayingState);
    QCOMPARE(provider.playerControl()->state(), QMediaPlayer::PlayingState);
    QCOMPARE(stateChangedSpy.count(),   stateChanged);
    QCOMPARE(playingSpy.count(),          playing);
    QCOMPARE(stoppedSpy.count(),          stopped);
}

void tst_QDeclarativeAudio::paused()
{
    QtTestMediaServiceProvider provider;
    QDeclarativeAudio audio;
    audio.classBegin();

    QSignalSpy stateChangedSpy(&audio, SIGNAL(playbackStateChanged()));
    QSignalSpy pausedSpy(&audio, SIGNAL(paused()));
    int stateChanged = 0;
    int pausedCount = 0;

    audio.componentComplete();
    audio.setSource(QUrl("http://example.com"));

    QCOMPARE(audio.playbackState(), audio.StoppedState);

    // play() when stopped.
    audio.play();
    QCOMPARE(audio.playbackState(), audio.PlayingState);
    QCOMPARE(provider.playerControl()->state(), QMediaPlayer::PlayingState);
    QCOMPARE(stateChangedSpy.count(), ++stateChanged);
    QCOMPARE(pausedSpy.count(), pausedCount);

    // pause() when playing.
    audio.pause();
    QCOMPARE(audio.playbackState(), audio.PausedState);
    QCOMPARE(provider.playerControl()->state(), QMediaPlayer::PausedState);
    QCOMPARE(stateChangedSpy.count(), ++stateChanged);
    QCOMPARE(pausedSpy.count(), ++pausedCount);

    // pause() when paused.
    audio.pause();
    QCOMPARE(audio.playbackState(), audio.PausedState);
    QCOMPARE(provider.playerControl()->state(), QMediaPlayer::PausedState);
    QCOMPARE(stateChangedSpy.count(),   stateChanged);
    QCOMPARE(pausedSpy.count(), pausedCount);

    // stop() when paused.
    audio.stop();
    QCOMPARE(audio.playbackState(), audio.StoppedState);
    QCOMPARE(provider.playerControl()->state(), QMediaPlayer::StoppedState);
    QCOMPARE(stateChangedSpy.count(), ++stateChanged);
    QCOMPARE(pausedSpy.count(), pausedCount);

    // pause() when stopped.
    audio.pause();
    QCOMPARE(audio.playbackState(), audio.PausedState);
    QCOMPARE(provider.playerControl()->state(), QMediaPlayer::PausedState);
    QCOMPARE(stateChangedSpy.count(), ++stateChanged);
    QCOMPARE(pausedSpy.count(), ++pausedCount);

    // play() when paused.
    audio.play();
    QCOMPARE(audio.playbackState(), audio.PlayingState);
    QCOMPARE(provider.playerControl()->state(), QMediaPlayer::PlayingState);
    QCOMPARE(stateChangedSpy.count(), ++stateChanged);
    QCOMPARE(pausedSpy.count(), pausedCount);
}

void tst_QDeclarativeAudio::duration()
{
    QtTestMediaServiceProvider provider;
    QDeclarativeAudio audio;
    audio.classBegin();
    audio.componentComplete();

    QSignalSpy spy(&audio, SIGNAL(durationChanged()));

    QCOMPARE(audio.duration(), 0);

    provider.playerControl()->setDuration(4040);
    QCOMPARE(audio.duration(), 4040);
    QCOMPARE(spy.count(), 1);

    provider.playerControl()->setDuration(-129);
    QCOMPARE(audio.duration(), -129);
    QCOMPARE(spy.count(), 2);

    provider.playerControl()->setDuration(0);
    QCOMPARE(audio.duration(), 0);
    QCOMPARE(spy.count(), 3);

    // Unnecessary duration changed signals aren't filtered.
    provider.playerControl()->setDuration(0);
    QCOMPARE(audio.duration(), 0);
    QCOMPARE(spy.count(), 4);
}

void tst_QDeclarativeAudio::position()
{
    QtTestMediaServiceProvider provider;
    QDeclarativeAudio audio;
    audio.classBegin();
    audio.componentComplete();

    QSignalSpy spy(&audio, SIGNAL(positionChanged()));

    QCOMPARE(audio.position(), 0);

    // QDeclarativeAudio won't bound set positions to the duration.  A media service may though.
    QCOMPARE(audio.duration(), 0);

    audio.seek(450);
    QCOMPARE(audio.position(), 450);
    QCOMPARE(provider.playerControl()->position(), qint64(450));
    QCOMPARE(spy.count(), 1);

    audio.seek(-5403);
    QCOMPARE(audio.position(), 0);
    QCOMPARE(provider.playerControl()->position(), qint64(0));
    QCOMPARE(spy.count(), 2);

    audio.seek(-5403);
    QCOMPARE(audio.position(), 0);
    QCOMPARE(provider.playerControl()->position(), qint64(0));
    QCOMPARE(spy.count(), 2);

    // Check the signal change signal is emitted if the change originates from the media service.
    provider.playerControl()->setPosition(450);
    QCOMPARE(audio.position(), 450);
    QCOMPARE(spy.count(), 3);

    connect(&audio, SIGNAL(positionChanged()), &QTestEventLoop::instance(), SLOT(exitLoop()));

    provider.playerControl()->updateState(QMediaPlayer::PlayingState);
    QTestEventLoop::instance().enterLoop(2);
    QVERIFY(spy.count() > 3 && spy.count() < 6); // 4 or 5

    provider.playerControl()->updateState(QMediaPlayer::PausedState);
    QTestEventLoop::instance().enterLoop(1);
    QVERIFY(spy.count() < 6);
}

void tst_QDeclarativeAudio::volume()
{
    QtTestMediaServiceProvider provider;
    QDeclarativeAudio audio;
    audio.classBegin();
    audio.componentComplete();

    QSignalSpy spy(&audio, SIGNAL(volumeChanged()));

    QCOMPARE(audio.volume(), qreal(1.0));

    audio.setVolume(0.7);
    QCOMPARE(audio.volume(), qreal(0.7));
    QCOMPARE(provider.playerControl()->volume(), 70);
    QCOMPARE(spy.count(), 1);

    audio.setVolume(0.7);
    QCOMPARE(audio.volume(), qreal(0.7));
    QCOMPARE(provider.playerControl()->volume(), 70);
    QCOMPARE(spy.count(), 1);

    provider.playerControl()->setVolume(30);
    QCOMPARE(audio.volume(), qreal(0.3));
    QCOMPARE(spy.count(), 2);
}

void tst_QDeclarativeAudio::muted()
{
    QtTestMediaServiceProvider provider;
    QDeclarativeAudio audio;
    audio.classBegin();
    audio.componentComplete();

    QSignalSpy spy(&audio, SIGNAL(mutedChanged()));

    QCOMPARE(audio.isMuted(), false);

    audio.setMuted(true);
    QCOMPARE(audio.isMuted(), true);
    QCOMPARE(provider.playerControl()->isMuted(), true);
    QCOMPARE(spy.count(), 1);

    provider.playerControl()->setMuted(false);
    QCOMPARE(audio.isMuted(), false);
    QCOMPARE(spy.count(), 2);

    audio.setMuted(false);
    QCOMPARE(audio.isMuted(), false);
    QCOMPARE(provider.playerControl()->isMuted(), false);
    QCOMPARE(spy.count(), 2);
}

void tst_QDeclarativeAudio::bufferProgress()
{
    QtTestMediaServiceProvider provider;
    QDeclarativeAudio audio;
    audio.classBegin();
    audio.componentComplete();

    QSignalSpy spy(&audio, SIGNAL(bufferProgressChanged()));

    QCOMPARE(audio.bufferProgress(), qreal(0.0));

    provider.playerControl()->setBufferStatus(20);
    QCOMPARE(audio.bufferProgress(), qreal(0.2));
    QCOMPARE(spy.count(), 1);

    provider.playerControl()->setBufferStatus(20);
    QCOMPARE(audio.bufferProgress(), qreal(0.2));
    QCOMPARE(spy.count(), 2);

    provider.playerControl()->setBufferStatus(40);
    QCOMPARE(audio.bufferProgress(), qreal(0.4));
    QCOMPARE(spy.count(), 3);

    connect(&audio, SIGNAL(positionChanged()), &QTestEventLoop::instance(), SLOT(exitLoop()));

    provider.playerControl()->updateMediaStatus(
            QMediaPlayer::BufferingMedia, QMediaPlayer::PlayingState);
    QTestEventLoop::instance().enterLoop(2);
    QVERIFY(spy.count() > 3 && spy.count() < 6); // 4 or 5

    provider.playerControl()->updateMediaStatus(QMediaPlayer::BufferedMedia);
    QTestEventLoop::instance().enterLoop(1);
    QVERIFY(spy.count() < 6);
}

void tst_QDeclarativeAudio::seekable()
{
    QtTestMediaServiceProvider provider;
    QDeclarativeAudio audio;
    audio.classBegin();
    audio.componentComplete();

    QSignalSpy spy(&audio, SIGNAL(seekableChanged()));

    QCOMPARE(audio.isSeekable(), false);

    provider.playerControl()->setSeekable(true);
    QCOMPARE(audio.isSeekable(), true);
    QCOMPARE(spy.count(), 1);

    provider.playerControl()->setSeekable(true);
    QCOMPARE(audio.isSeekable(), true);
    QCOMPARE(spy.count(), 2);

    provider.playerControl()->setSeekable(false);
    QCOMPARE(audio.isSeekable(), false);
    QCOMPARE(spy.count(), 3);
}

void tst_QDeclarativeAudio::playbackRate()
{
    QtTestMediaServiceProvider provider;
    QDeclarativeAudio audio;
    audio.classBegin();
    audio.componentComplete();

    QSignalSpy spy(&audio, SIGNAL(playbackRateChanged()));

    QCOMPARE(audio.playbackRate(), qreal(1.0));

    audio.setPlaybackRate(0.5);
    QCOMPARE(audio.playbackRate(), qreal(0.5));
    QCOMPARE(provider.playerControl()->playbackRate(), qreal(0.5));
    QCOMPARE(spy.count(), 1);

    provider.playerControl()->setPlaybackRate(2.0);
    QCOMPARE(provider.playerControl()->playbackRate(), qreal(2.0));
    QCOMPARE(spy.count(), 2);

    audio.setPlaybackRate(2.0);
    QCOMPARE(audio.playbackRate(), qreal(2.0));
    QCOMPARE(provider.playerControl()->playbackRate(), qreal(2.0));
    QCOMPARE(spy.count(), 2);
}

void tst_QDeclarativeAudio::status()
{
    QtTestMediaServiceProvider provider;
    QDeclarativeAudio audio;
    audio.classBegin();
    audio.componentComplete();

    QSignalSpy statusChangedSpy(&audio, SIGNAL(statusChanged()));

    QCOMPARE(audio.status(), QDeclarativeAudio::NoMedia);

    // Set media, start loading.
    provider.playerControl()->updateMediaStatus(QMediaPlayer::LoadingMedia);
    QCOMPARE(audio.status(), QDeclarativeAudio::Loading);
    QCOMPARE(statusChangedSpy.count(), 1);

    // Finish loading.
    provider.playerControl()->updateMediaStatus(QMediaPlayer::LoadedMedia);
    QCOMPARE(audio.status(), QDeclarativeAudio::Loaded);
    QCOMPARE(statusChangedSpy.count(), 2);

    // Play, start buffering.
    provider.playerControl()->updateMediaStatus(
            QMediaPlayer::StalledMedia, QMediaPlayer::PlayingState);
    QCOMPARE(audio.status(), QDeclarativeAudio::Stalled);
    QCOMPARE(statusChangedSpy.count(), 3);

    // Enough data buffered to proceed.
    provider.playerControl()->updateMediaStatus(QMediaPlayer::BufferingMedia);
    QCOMPARE(audio.status(), QDeclarativeAudio::Buffering);
    QCOMPARE(statusChangedSpy.count(), 4);

    // Errant second buffering status changed.
    provider.playerControl()->updateMediaStatus(QMediaPlayer::BufferingMedia);
    QCOMPARE(audio.status(), QDeclarativeAudio::Buffering);
    QCOMPARE(statusChangedSpy.count(), 4);

    // Buffer full.
    provider.playerControl()->updateMediaStatus(QMediaPlayer::BufferedMedia);
    QCOMPARE(audio.status(), QDeclarativeAudio::Buffered);
    QCOMPARE(statusChangedSpy.count(), 5);

    // Buffer getting low.
    provider.playerControl()->updateMediaStatus(QMediaPlayer::BufferingMedia);
    QCOMPARE(audio.status(), QDeclarativeAudio::Buffering);
    QCOMPARE(statusChangedSpy.count(), 6);

    // Buffer full.
    provider.playerControl()->updateMediaStatus(QMediaPlayer::BufferedMedia);
    QCOMPARE(audio.status(), QDeclarativeAudio::Buffered);
    QCOMPARE(statusChangedSpy.count(), 7);

    // Finished.
    provider.playerControl()->updateMediaStatus(
            QMediaPlayer::EndOfMedia, QMediaPlayer::StoppedState);
    QCOMPARE(audio.status(), QDeclarativeAudio::EndOfMedia);
    QCOMPARE(statusChangedSpy.count(), 8);
}

void tst_QDeclarativeAudio::metaData_data()
{
    QTest::addColumn<QByteArray>("propertyName");
    QTest::addColumn<QString>("propertyKey");
    QTest::addColumn<QVariant>("value");

    QTest::newRow("title")
            << QByteArray("title")
            << QMediaMetaData::Title
            << QVariant(QString::fromLatin1("This is a title"));

    QTest::newRow("genre")
            << QByteArray("genre")
            << QMediaMetaData::Genre
            << QVariant(QString::fromLatin1("rock"));

    QTest::newRow("trackNumber")
            << QByteArray("trackNumber")
            << QMediaMetaData::TrackNumber
            << QVariant(8);
}

void tst_QDeclarativeAudio::metaData()
{
    QFETCH(QByteArray, propertyName);
    QFETCH(QString, propertyKey);
    QFETCH(QVariant, value);

    QtTestMediaServiceProvider provider;
    QDeclarativeAudio audio;
    audio.classBegin();
    audio.componentComplete();

    QSignalSpy spy(audio.metaData(), SIGNAL(metaDataChanged()));

    const int index = audio.metaData()->metaObject()->indexOfProperty(propertyName.constData());
    QVERIFY(index != -1);

    QMetaProperty property = audio.metaData()->metaObject()->property(index);
    QCOMPARE(property.read(&audio), QVariant());

    property.write(audio.metaData(), value);
    QCOMPARE(property.read(audio.metaData()), QVariant());
    QCOMPARE(provider.metaDataControl()->metaData(propertyKey), QVariant());
    QCOMPARE(spy.count(), 0);

    provider.metaDataControl()->setMetaData(propertyKey, value);
    QCOMPARE(property.read(audio.metaData()), value);
    QCOMPARE(spy.count(), 1);
}

void tst_QDeclarativeAudio::error()
{
    const QString errorString = QLatin1String("Failed to open device.");

    QtTestMediaServiceProvider provider;
    QDeclarativeAudio audio;
    audio.classBegin();
    audio.componentComplete();

    QSignalSpy errorSpy(&audio, SIGNAL(error(QDeclarativeAudio::Error,QString)));
    QSignalSpy errorChangedSpy(&audio, SIGNAL(errorChanged()));

    QCOMPARE(audio.error(), QDeclarativeAudio::NoError);
    QCOMPARE(audio.errorString(), QString());

    provider.playerControl()->emitError(QMediaPlayer::ResourceError, errorString);

    QCOMPARE(audio.error(), QDeclarativeAudio::ResourceError);
    QCOMPARE(audio.errorString(), errorString);
    QCOMPARE(errorSpy.count(), 1);
    QCOMPARE(errorChangedSpy.count(), 1);

    // Changing the source resets the error properties.
    audio.setSource(QUrl("http://example.com"));
    QCOMPARE(audio.error(), QDeclarativeAudio::NoError);
    QCOMPARE(audio.errorString(), QString());
    QCOMPARE(errorSpy.count(), 1);
    QCOMPARE(errorChangedSpy.count(), 2);

    // But isn't noisy.
    audio.setSource(QUrl("file:///file/path"));
    QCOMPARE(audio.error(), QDeclarativeAudio::NoError);
    QCOMPARE(audio.errorString(), QString());
    QCOMPARE(errorSpy.count(), 1);
    QCOMPARE(errorChangedSpy.count(), 2);
}

void tst_QDeclarativeAudio::loops()
{
    QtTestMediaServiceProvider provider;
    QDeclarativeAudio audio;

    QSignalSpy loopsChangedSpy(&audio, SIGNAL(loopCountChanged()));
    QSignalSpy stateChangedSpy(&audio, SIGNAL(playbackStateChanged()));
    QSignalSpy stoppedSpy(&audio, SIGNAL(stopped()));

    int stateChanged = 0;
    int loopsChanged = 0;
    int stoppedCount = 0;

    audio.classBegin();
    audio.componentComplete();

    QCOMPARE(audio.playbackState(), audio.StoppedState);

    //setLoopCount(3) when stopped.
    audio.setLoopCount(3);
    QCOMPARE(audio.loopCount(), 3);
    QCOMPARE(loopsChangedSpy.count(), ++loopsChanged);

    //play till end
    audio.play();
    QCOMPARE(audio.playbackState(), audio.PlayingState);
    QCOMPARE(provider.playerControl()->state(), QMediaPlayer::PlayingState);
    QCOMPARE(stateChangedSpy.count(), ++stateChanged);
    QCOMPARE(stoppedSpy.count(), stoppedCount);

    // play() when playing.
    audio.play();
    QCOMPARE(audio.playbackState(), audio.PlayingState);
    QCOMPARE(provider.playerControl()->state(), QMediaPlayer::PlayingState);
    QCOMPARE(stateChangedSpy.count(),   stateChanged);
    QCOMPARE(stoppedSpy.count(), stoppedCount);

    provider.playerControl()->updateMediaStatus(QMediaPlayer::EndOfMedia, QMediaPlayer::StoppedState);
    QCOMPARE(audio.playbackState(), audio.PlayingState);
    QCOMPARE(provider.playerControl()->state(), QMediaPlayer::PlayingState);
    QCOMPARE(stateChangedSpy.count(),   stateChanged);
    QCOMPARE(stoppedSpy.count(), stoppedCount);

    //play to end
    provider.playerControl()->updateMediaStatus(QMediaPlayer::EndOfMedia, QMediaPlayer::StoppedState);
    QCOMPARE(stoppedSpy.count(), stoppedCount);
    //play to end
    provider.playerControl()->updateMediaStatus(QMediaPlayer::EndOfMedia, QMediaPlayer::StoppedState);
    QCOMPARE(audio.playbackState(), audio.StoppedState);
    QCOMPARE(provider.playerControl()->state(), QMediaPlayer::StoppedState);
    QCOMPARE(stateChangedSpy.count(), ++stateChanged);
    QCOMPARE(stoppedSpy.count(), ++stoppedCount);

    // stop when playing
    audio.play();
    QCOMPARE(provider.playerControl()->state(), QMediaPlayer::PlayingState);
    QCOMPARE(stateChangedSpy.count(), ++stateChanged);
    QCOMPARE(stoppedSpy.count(), stoppedCount);
    provider.playerControl()->updateMediaStatus(QMediaPlayer::EndOfMedia, QMediaPlayer::StoppedState);
    audio.stop();
    QCOMPARE(audio.playbackState(), audio.StoppedState);
    QCOMPARE(provider.playerControl()->state(), QMediaPlayer::StoppedState);
    QCOMPARE(stateChangedSpy.count(), ++stateChanged);
    QCOMPARE(stoppedSpy.count(), ++stoppedCount);

    //play() with infinite loop
    audio.setLoopCount(-1);
    QCOMPARE(audio.loopCount(), -1);
    QCOMPARE(loopsChangedSpy.count(), ++loopsChanged);
    audio.play();
    QCOMPARE(audio.playbackState(), audio.PlayingState);
    QCOMPARE(stateChangedSpy.count(), ++stateChanged);
    QCOMPARE(provider.playerControl()->state(), QMediaPlayer::PlayingState);
    provider.playerControl()->updateMediaStatus(QMediaPlayer::EndOfMedia, QMediaPlayer::StoppedState);
    QCOMPARE(audio.playbackState(), audio.PlayingState);
    QCOMPARE(provider.playerControl()->state(), QMediaPlayer::PlayingState);
    QCOMPARE(stateChangedSpy.count(), stateChanged);

    // stop() when playing in infinite loop.
    audio.stop();
    QCOMPARE(audio.playbackState(), audio.StoppedState);
    QCOMPARE(provider.playerControl()->state(), QMediaPlayer::StoppedState);
    QCOMPARE(stateChangedSpy.count(), ++stateChanged);
    QCOMPARE(stoppedSpy.count(), ++stoppedCount);

    qDebug() << "Testing version 5";
}

void tst_QDeclarativeAudio::audioRole()
{
    MockMediaPlayerService mockService;
    MockMediaServiceProvider mockProvider(&mockService);
    QMediaServiceProvider::setDefaultServiceProvider(&mockProvider);

    QQmlEngine engine;
    QQmlComponent component(&engine);
    component.setData("import QtQuick 2.0 \n import QtMultimedia 5.6 \n Audio { }", QUrl());

    {
        mockService.setHasAudioRole(false);
        QDeclarativeAudio *audio = static_cast<QDeclarativeAudio*>(component.create());

        QCOMPARE(audio->audioRole(), QDeclarativeAudio::UnknownRole);
        QVERIFY(audio->supportedAudioRoles().isArray());
        QVERIFY(audio->supportedAudioRoles().toVariant().toList().isEmpty());

        QSignalSpy spy(audio, SIGNAL(audioRoleChanged()));
        audio->setAudioRole(QDeclarativeAudio::MusicRole);
        QCOMPARE(audio->audioRole(), QDeclarativeAudio::UnknownRole);
        QCOMPARE(spy.count(), 0);
    }

    {
        mockService.reset();
        mockService.setHasAudioRole(true);
        QDeclarativeAudio *audio = static_cast<QDeclarativeAudio*>(component.create());
        QSignalSpy spy(audio, SIGNAL(audioRoleChanged()));

        QCOMPARE(audio->audioRole(), QDeclarativeAudio::UnknownRole);
        QVERIFY(audio->supportedAudioRoles().isArray());
        QVERIFY(!audio->supportedAudioRoles().toVariant().toList().isEmpty());

        audio->setAudioRole(QDeclarativeAudio::MusicRole);
        QCOMPARE(audio->audioRole(), QDeclarativeAudio::MusicRole);
        QCOMPARE(mockService.mockAudioRoleControl->audioRole(), QAudio::MusicRole);
        QCOMPARE(spy.count(), 1);
    }
}

void tst_QDeclarativeAudio::customAudioRole()
{
    MockMediaPlayerService mockService;
    MockMediaServiceProvider mockProvider(&mockService);
    QMediaServiceProvider::setDefaultServiceProvider(&mockProvider);

    QQmlEngine engine;
    QQmlComponent component(&engine);
    component.setData("import QtQuick 2.0 \n import QtMultimedia 5.11 \n Audio { }", QUrl());

    {
        mockService.setHasCustomAudioRole(false);
        QObject *audio = component.create();
        QVERIFY(audio);

        QMetaEnum audioRoleEnum = enumerator(audio->metaObject(), "AudioRole");
        int AudioRole_UnknownRoleValue = keyToValue(audioRoleEnum, "UnknownRole");
        QVERIFY(!QTest::currentTestFailed());

        QVERIFY(audio->property("customAudioRole").toString().isEmpty());

        QSignalSpy spyRole(audio, SIGNAL(audioRoleChanged()));
        QSignalSpy spyCustomRole(audio, SIGNAL(customAudioRoleChanged()));
        audio->setProperty("customAudioRole", QStringLiteral("customRole"));
        QCOMPARE(audio->property("audioRole").toInt(), AudioRole_UnknownRoleValue);
        QVERIFY(audio->property("customAudioRole").toString().isEmpty());
        QCOMPARE(spyRole.count(), 0);
        QCOMPARE(spyCustomRole.count(), 0);
    }

    {
        mockService.reset();
        mockService.setHasAudioRole(false);

        QObject *audio = component.create();
        QVERIFY(audio);

        QMetaEnum audioRoleEnum = enumerator(audio->metaObject(), "AudioRole");
        int AudioRole_UnknownRoleValue = keyToValue(audioRoleEnum, "UnknownRole");
        QVERIFY(!QTest::currentTestFailed());

        QVERIFY(audio->property("customAudioRole").toString().isEmpty());

        QSignalSpy spyRole(audio, SIGNAL(audioRoleChanged()));
        QSignalSpy spyCustomRole(audio, SIGNAL(customAudioRoleChanged()));
        audio->setProperty("customAudioRole", QStringLiteral("customRole"));
        QCOMPARE(audio->property("audioRole").toInt(), AudioRole_UnknownRoleValue);
        QVERIFY(audio->property("customAudioRole").toString().isEmpty());
        QCOMPARE(spyRole.count(), 0);
        QCOMPARE(spyCustomRole.count(), 0);
    }

    {
        mockService.reset();

        QObject *audio = component.create();
        QVERIFY(audio);

        QMetaEnum audioRoleEnum = enumerator(audio->metaObject(), "AudioRole");
        int AudioRole_UnknownRoleValue = keyToValue(audioRoleEnum, "UnknownRole");
        int AudioRole_CustomRoleValue = keyToValue(audioRoleEnum, "CustomRole");
        int AudioRole_MusicRoleValue = keyToValue(audioRoleEnum, "MusicRole");
        QVERIFY(!QTest::currentTestFailed());

        QSignalSpy spyRole(audio, SIGNAL(audioRoleChanged()));
        QSignalSpy spyCustomRole(audio, SIGNAL(customAudioRoleChanged()));

        QCOMPARE(audio->property("audioRole").toInt(), AudioRole_UnknownRoleValue);
        QVERIFY(audio->property("customAudioRole").toString().isEmpty());

        QString customRole(QStringLiteral("customRole"));
        audio->setProperty("customAudioRole", customRole);
        QCOMPARE(audio->property("audioRole").toInt(), AudioRole_CustomRoleValue);
        QCOMPARE(audio->property("customAudioRole").toString(), customRole);
        QCOMPARE(mockService.mockAudioRoleControl->audioRole(), QAudio::CustomRole);
        QCOMPARE(mockService.mockCustomAudioRoleControl->customAudioRole(), customRole);
        QCOMPARE(spyRole.count(), 1);
        QCOMPARE(spyCustomRole.count(), 1);

        spyRole.clear();
        spyCustomRole.clear();

        QString customRole2(QStringLiteral("customRole2"));
        audio->setProperty("customAudioRole", customRole2);
        QCOMPARE(audio->property("customAudioRole").toString(), customRole2);
        QCOMPARE(mockService.mockCustomAudioRoleControl->customAudioRole(), customRole2);
        QCOMPARE(spyRole.count(), 0);
        QCOMPARE(spyCustomRole.count(), 1);

        spyRole.clear();
        spyCustomRole.clear();

        audio->setProperty("audioRole", AudioRole_MusicRoleValue);
        QCOMPARE(audio->property("audioRole").toInt(), AudioRole_MusicRoleValue);
        QVERIFY(audio->property("customAudioRole").toString().isEmpty());
        QCOMPARE(mockService.mockAudioRoleControl->audioRole(), QAudio::MusicRole);
        QVERIFY(mockService.mockCustomAudioRoleControl->customAudioRole().isEmpty());
        QCOMPARE(spyRole.count(), 1);
        QCOMPARE(spyCustomRole.count(), 1);

        spyRole.clear();
        spyCustomRole.clear();

        audio->setProperty("audioRole", AudioRole_CustomRoleValue);
        QCOMPARE(audio->property("audioRole").toInt(), AudioRole_CustomRoleValue);
        QVERIFY(audio->property("customAudioRole").toString().isEmpty());
        QCOMPARE(mockService.mockAudioRoleControl->audioRole(), QAudio::CustomRole);
        QVERIFY(mockService.mockCustomAudioRoleControl->customAudioRole().isEmpty());
        QCOMPARE(spyRole.count(), 1);
        QCOMPARE(spyCustomRole.count(), 0);
    }
}

void tst_QDeclarativeAudio::enumerator(const QMetaObject *object,
                                       const char *name,
                                       QMetaEnum *result)
{
    int index = object->indexOfEnumerator(name);
    QVERIFY(index >= 0);
    *result = object->enumerator(index);
}

QMetaEnum tst_QDeclarativeAudio::enumerator(const QMetaObject *object, const char *name)
{
    QMetaEnum result;
    enumerator(object, name, &result);
    return result;
}

void tst_QDeclarativeAudio::keyToValue(const QMetaEnum &enumeration, const char *key, int *result)
{
    bool ok = false;
    *result = enumeration.keyToValue(key, &ok);
    QVERIFY(ok);
}

int tst_QDeclarativeAudio::keyToValue(const QMetaEnum &enumeration, const char *key)
{
    int result;
    keyToValue(enumeration, key, &result);
    return result;
}

struct Surface : QAbstractVideoSurface
{
    Surface(QObject *parent = nullptr) : QAbstractVideoSurface(parent) { }
    QList<QVideoFrame::PixelFormat> supportedPixelFormats(QAbstractVideoBuffer::HandleType) const override
    {
        return QList<QVideoFrame::PixelFormat>() << QVideoFrame::Format_RGB32;
    }

    bool present(const QVideoFrame &) override { return true; }
};

void tst_QDeclarativeAudio::videoOutput()
{
    QtTestMediaPlayerControl playerControl;
    QtTestMediaServiceProvider provider(&playerControl, 0);

    QDeclarativeAudio audio;
    QSignalSpy spy(&audio, &QDeclarativeAudio::videoOutputChanged);

    audio.classBegin();
    audio.componentComplete();

    QVERIFY(audio.videoOutput().isNull());

    QVariant surface;
    surface.setValue(new Surface(this));
    audio.setVideoOutput(surface);
    QCOMPARE(audio.videoOutput(), surface);
    QCOMPARE(spy.count(), 1);

    QQmlEngine engine;
    QJSValue jsArray = engine.newArray(5);
    jsArray.setProperty(0, engine.newQObject(new Surface(this)));
    jsArray.setProperty(1, engine.newQObject(new Surface(this)));
    QDeclarativeVideoOutput output;
    jsArray.setProperty(2, engine.newQObject(&output));
    jsArray.setProperty(3, 123);
    jsArray.setProperty(4, QLatin1String("ignore this"));

    QVariant surfaces;
    surfaces.setValue(jsArray);
    audio.setVideoOutput(surfaces);
    QCOMPARE(audio.videoOutput(), surfaces);
    QCOMPARE(spy.count(), 2);
}

QTEST_MAIN(tst_QDeclarativeAudio)

#include "tst_qdeclarativeaudio.moc"
