/****************************************************************************************
 * Copyright (c) 2013 Konrad Zemek <konrad.zemek@gmail.com>                             *
 *                                                                                      *
 * This program is free software; you can redistribute it and/or modify it under        *
 * the terms of the GNU General Public License as published by the Free Software        *
 * Foundation; either version 2 of the License, or (at your option) any later           *
 * version.                                                                             *
 *                                                                                      *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
 * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
 *                                                                                      *
 * You should have received a copy of the GNU General Public License along with         *
 * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
 ****************************************************************************************/

#include <TestImporterBase.h>

#include "MetaValues.h"
#include "core/meta/support/MetaConstants.h"
#include "statsyncing/Provider.h"
#include "statsyncing/Track.h"

#include <QTest>


using namespace StatSyncing;

TestImporterBase::TestImporterBase()
{
}

ProviderPtr
TestImporterBase::getWritableProvider()
{
    return getProvider();
}

bool
TestImporterBase::hasOddRatings() const
{
    return true;
}

#define skipIfNoSupport( fieldmask, metavalue ) \
{ \
    if( !( fieldmask & metavalue ) ) \
    { \
        const QString msg = QString( "Tested provider does not support %1 metadata" ) \
                                                .arg( Meta::nameForField( metavalue ) ); \
        QSKIP( msg.toLocal8Bit().constData(), SkipAll ); \
    } \
} do {} while(false)

#define amarokProviderSkipIfNoMysqld( provider ) \
    if( QString( provider->prettyName() ) == "Amarok2Test" ) \
        if( !QFileInfo( "/usr/bin/mysqld" ).isExecutable() ) \
            QSKIP( "/usr/bin/mysqld not executable, skipping Amarok provider tests", \
                   SkipAll )

void
TestImporterBase::titleShouldBeCaseSensitive()
{
    ProviderPtr provider( getProvider() );
    amarokProviderSkipIfNoMysqld( provider );

    const QString artist = "caseSensitiveTitle";
    QVERIFY( provider->artists().contains( artist ) );

    QSet<QString> trackNames;
    foreach( const TrackPtr &track, provider->artistTracks( artist ) )
        trackNames.insert( track->name() );

    QCOMPARE( trackNames.size(), 3 );
    QVERIFY( trackNames.contains( "title" ) );
    QVERIFY( trackNames.contains( "Title" ) );
    QVERIFY( trackNames.contains( "tiTle" ) );
}

void
TestImporterBase::artistShouldBeCaseSensitive()
{
    ProviderPtr provider( getProvider() );
    amarokProviderSkipIfNoMysqld( provider );

    const QVector<QString> artists = QVector<QString>()
            << "caseSensitiveArtist" << "casesensitiveartist" << "caseSensitiveartist";

    foreach( const QString &artist, artists )
    {
        const TrackList tracks = provider->artistTracks( artist );
        QCOMPARE( tracks.size(), 1 );
        QCOMPARE( tracks.front()->artist(), artist );
    }
}

void
TestImporterBase::albumShouldBeCaseSensitive()
{
    ProviderPtr provider( getProvider() );
    amarokProviderSkipIfNoMysqld( provider );

    const QString artist = "caseSensitiveAlbum";
    QVERIFY( provider->artists().contains( artist ) );

    QSet<QString> trackAlbums;
    foreach( const TrackPtr &track, provider->artistTracks( artist ) )
        trackAlbums.insert( track->album() );

    QCOMPARE( trackAlbums.size(), 3 );
    QVERIFY( trackAlbums.contains( "album" ) );
    QVERIFY( trackAlbums.contains( "Album" ) );
    QVERIFY( trackAlbums.contains( "alBum" ) );
}

void
TestImporterBase::composerShouldBeCaseSensitive()
{
    ProviderPtr provider( getProvider() );
    amarokProviderSkipIfNoMysqld( provider );
    skipIfNoSupport( provider->reliableTrackMetaData(), Meta::valComposer );

    const QString artist = "caseSensitiveComposer";
    QVERIFY( provider->artists().contains( artist ) );

    QSet<QString> trackComposers;
    foreach( const TrackPtr &track, provider->artistTracks( artist ) )
        trackComposers.insert( track->composer() );

    QCOMPARE( trackComposers.size(), 3 );
    QVERIFY( trackComposers.contains( "composer" ) );
    QVERIFY( trackComposers.contains( "Composer" ) );
    QVERIFY( trackComposers.contains( "comPoser" ) );
}

void
TestImporterBase::titleShouldSupportUTF()
{
    ProviderPtr provider( getProvider() );
    amarokProviderSkipIfNoMysqld( provider );

    const QString artist = "utfTitle";
    QVERIFY( provider->artists().contains( artist ) );

    const TrackList tracks = provider->artistTracks( artist );
    QCOMPARE( tracks.size(), 1 );
    QCOMPARE( tracks.front()->name(), QString::fromWCharArray( L"\xF906\xF907\xF908" ) );
}

void
TestImporterBase::artistShouldSupportUTF()
{
    ProviderPtr provider( getProvider() );
    amarokProviderSkipIfNoMysqld( provider );

    const QString artist = QString::fromWCharArray( L"utf\xF909\xF90A\xF90B" );
    QVERIFY( provider->artists().contains( artist ) );

    const TrackList tracks = provider->artistTracks( artist );
    QCOMPARE( tracks.size(), 1 );
    QCOMPARE( tracks.front()->artist(), artist );
}

void
TestImporterBase::albumShouldSupportUTF()
{
    ProviderPtr provider( getProvider() );
    amarokProviderSkipIfNoMysqld( provider );

    const QString artist = "utfAlbum";
    QVERIFY( provider->artists().contains( artist ) );

    const TrackList tracks = provider->artistTracks( artist );
    QCOMPARE( tracks.size(), 1 );
    QCOMPARE( tracks.front()->album(), QString::fromWCharArray( L"\xF903\xF904\xF905" ) );
}

void
TestImporterBase::composerShouldSupportUTF()
{
    ProviderPtr provider( getProvider() );
    amarokProviderSkipIfNoMysqld( provider );
    skipIfNoSupport( provider->reliableTrackMetaData(), Meta::valComposer );

    const QString artist = "utfComposer";
    QVERIFY( provider->artists().contains( artist ) );

    const TrackList tracks = provider->artistTracks( artist );
    QCOMPARE( tracks.size(), 1 );
    QCOMPARE( tracks.front()->composer(),
              QString::fromWCharArray( L"\xF900\xF901\xF902" ) );
}

void
TestImporterBase::titleShouldSupportMultipleWords()
{
    ProviderPtr provider( getProvider() );
    amarokProviderSkipIfNoMysqld( provider );

    const QString artist = "multiWordTitle";
    QVERIFY( provider->artists().contains( artist ) );

    const TrackList tracks = provider->artistTracks( artist );
    QCOMPARE( tracks.size(), 1 );
    QCOMPARE( tracks.front()->name(), QString( "ti tl e" ) );
}

void
TestImporterBase::artistShouldSupportMultipleWords()
{
    ProviderPtr provider( getProvider() );
    amarokProviderSkipIfNoMysqld( provider );

    const QString artist = "multi Word Artist";
    QVERIFY( provider->artists().contains( artist ) );

    const TrackList tracks = provider->artistTracks( artist );
    QCOMPARE( tracks.size(), 1 );
    QCOMPARE( tracks.front()->artist(), artist );
}

void
TestImporterBase::albumShouldSupportMultipleWords()
{
    ProviderPtr provider( getProvider() );
    amarokProviderSkipIfNoMysqld( provider );

    const QString artist = "multiWordAlbum";
    QVERIFY( provider->artists().contains( artist ) );

    const TrackList tracks = provider->artistTracks( artist );
    QCOMPARE( tracks.size(), 1 );
    QCOMPARE( tracks.front()->album(), QString( "al b um" ) );
}

void
TestImporterBase::composerShouldSupportMultipleWords()
{
    ProviderPtr provider( getProvider() );
    amarokProviderSkipIfNoMysqld( provider );
    skipIfNoSupport( provider->reliableTrackMetaData(), Meta::valComposer );

    const QString artist = "multiWordComposer";
    QVERIFY( provider->artists().contains( artist ) );

    const TrackList tracks = provider->artistTracks( artist );
    QCOMPARE( tracks.size(), 1 );
    QCOMPARE( tracks.front()->composer(), QString( "com po ser" ) );
}

void
TestImporterBase::titleShouldBeWhitespaceTrimmed()
{
    ProviderPtr provider( getProvider() );
    amarokProviderSkipIfNoMysqld( provider );

    const QString artist = "trimTitle";
    QVERIFY( provider->artists().contains( artist ) );

    QSet<QString> trackNames;
    foreach( const TrackPtr &track, provider->artistTracks( artist ) )
        trackNames.insert( track->name() );

    QCOMPARE( trackNames.size(), 3 );
    QVERIFY( trackNames.contains( "title1" ) );
    QVERIFY( trackNames.contains( "title2" ) );
    QVERIFY( trackNames.contains( "title3" ) );
}

void
TestImporterBase::artistShouldBeWhitespaceTrimmed()
{
    ProviderPtr provider( getProvider() );
    amarokProviderSkipIfNoMysqld( provider );

    const QString artist = "trimArtist";
    QVERIFY( provider->artists().contains( artist ) );

    const TrackList tracks = provider->artistTracks( artist );
    QCOMPARE( tracks.size(), 3 );

    foreach( const TrackPtr &track, tracks )
        QCOMPARE( track->artist(), artist );
}

void
TestImporterBase::albumShouldBeWhitespaceTrimmed()
{
    ProviderPtr provider( getProvider() );
    amarokProviderSkipIfNoMysqld( provider );

    const QString artist = "trimAlbum";
    QVERIFY( provider->artists().contains( artist ) );

    const TrackList tracks = provider->artistTracks( artist );
    QCOMPARE( tracks.size(), 3 );

    foreach( const TrackPtr &track, tracks )
        QCOMPARE( track->album(), QString( "album" ) );
}

void
TestImporterBase::composerShouldBeWhitespaceTrimmed()
{
    ProviderPtr provider( getProvider() );
    amarokProviderSkipIfNoMysqld( provider );
    skipIfNoSupport( provider->reliableTrackMetaData(), Meta::valComposer );

    const QString artist = "trimComposer";
    QVERIFY( provider->artists().contains( artist ) );

    const TrackList tracks = provider->artistTracks( artist );
    QCOMPARE( tracks.size(), 3 );

    foreach( const TrackPtr &track, tracks )
        QCOMPARE( track->composer(), QString( "composer" ) );
}

void
TestImporterBase::albumShouldBeUnsetIfTagIsUnset()
{
    ProviderPtr provider( getProvider() );
    amarokProviderSkipIfNoMysqld( provider );

    const QString artist = "albumUnset";
    QVERIFY( provider->artists().contains( artist ) );

    const TrackList tracks = provider->artistTracks( artist );
    QCOMPARE( tracks.size(), 1 );
    QCOMPARE( tracks.front()->album(), QString() );
}

void
TestImporterBase::composerShouldBeUnsetIfTagIsUnset()
{
    ProviderPtr provider( getProvider() );
    amarokProviderSkipIfNoMysqld( provider );
    skipIfNoSupport( provider->reliableTrackMetaData(), Meta::valComposer );

    const QString artist = "composerUnset";
    QVERIFY( provider->artists().contains( artist ) );

    const TrackList tracks = provider->artistTracks( artist );
    QCOMPARE( tracks.size(), 1 );
    QCOMPARE( tracks.front()->composer(), QString() );
}

void
TestImporterBase::yearShouldBeUnsetIfTagIsUnset()
{
    ProviderPtr provider( getProvider() );
    amarokProviderSkipIfNoMysqld( provider );
    skipIfNoSupport( provider->reliableTrackMetaData(), Meta::valYear );

    const QString artist = "yearUnset";
    QVERIFY( provider->artists().contains( artist ) );

    const TrackList tracks = provider->artistTracks( artist );
    QCOMPARE( tracks.size(), 1 );
    QCOMPARE( tracks.front()->year(), 0 );
}

void
TestImporterBase::trackShouldBeUnsetIfTagIsUnset()
{
    ProviderPtr provider( getProvider() );
    amarokProviderSkipIfNoMysqld( provider );
    skipIfNoSupport( provider->reliableTrackMetaData(), Meta::valTrackNr );

    const QString artist = "trackUnset";
    QVERIFY( provider->artists().contains( artist ) );

    const TrackList tracks = provider->artistTracks( artist );
    QCOMPARE( tracks.size(), 1 );
    QCOMPARE( tracks.front()->trackNumber(), 0 );
}

void
TestImporterBase::discShouldBeUnsetIfTagIsUnset()
{
    ProviderPtr provider( getProvider() );
    amarokProviderSkipIfNoMysqld( provider );
    skipIfNoSupport( provider->reliableTrackMetaData(), Meta::valDiscNr );

    const QString artist = "discUnset";
    QVERIFY( provider->artists().contains( artist ) );

    const TrackList tracks = provider->artistTracks( artist );
    QCOMPARE( tracks.size(), 1 );
    QCOMPARE( tracks.front()->discNumber(), 0 );
}

void
TestImporterBase::checkStatistics( const QString &artist )
{
    ProviderPtr provider( getProvider() );
    amarokProviderSkipIfNoMysqld( provider );
    QVERIFY( provider->artists().contains( artist ) );

    QMap<QString, TrackPtr> trackForName;
    foreach( const TrackPtr &track, provider->artistTracks( artist ) )
        trackForName.insert( track->name(), track );

    const QString testName( QTest::currentDataTag() );
    QCOMPARE( trackForName.size(), 10 );
    QVERIFY( trackForName.contains( testName ) );

    const TrackPtr &track = trackForName.value( testName );

    if( reliableStatistics() & Meta::valFirstPlayed )
        QTEST( track->firstPlayed(), "firstPlayed" );
    if( reliableStatistics() & Meta::valLastPlayed )
        QTEST( track->lastPlayed(), "lastPlayed" );
    if( reliableStatistics() & Meta::valPlaycount )
        QTEST( track->playCount(), "playCount" );

    if( reliableStatistics() & Meta::valRating )
    {
        QFETCH( int, rating );
        if( !hasOddRatings() && (rating & 1) )
            ++rating;

        QCOMPARE( track->rating(), rating );
    }
}

void
TestImporterBase::tracksShouldHaveStatistics_data()
{
    QTest::addColumn<QDateTime> ( "firstPlayed" );
    QTest::addColumn<QDateTime> ( "lastPlayed" );
    QTest::addColumn<int>       ( "rating" );
    QTest::addColumn<int>       ( "playCount" );

    QVector<QDateTime> d;
    for( uint t = 0; t < 20; ++t )
        d.push_back( QDateTime::fromSecsSinceEpoch( 1378125780u + t ) );

    QTest::newRow( "title0" ) << d[ 0] << d[ 1] <<  1 << 20;
    QTest::newRow( "title1" ) << d[ 2] << d[ 3] <<  2 << 15;
    QTest::newRow( "title2" ) << d[ 4] << d[ 5] <<  3 << 14;
    QTest::newRow( "title3" ) << d[ 6] << d[ 7] <<  4 << 13;
    QTest::newRow( "title4" ) << d[ 8] << d[ 9] <<  5 << 11;
    QTest::newRow( "title5" ) << d[10] << d[11] <<  6 << 10;
    QTest::newRow( "title6" ) << d[12] << d[13] <<  7 <<  7;
    QTest::newRow( "title7" ) << d[14] << d[15] <<  8 <<  5;
    QTest::newRow( "title8" ) << d[16] << d[17] <<  9 <<  3;
    QTest::newRow( "title9" ) << d[18] << d[19] << 10 <<  2;
}

void
TestImporterBase::tracksShouldHaveStatistics()
{
    checkStatistics( "testStatistics" );
}

void
TestImporterBase::tracksShouldBehaveNicelyWithNoStatistics_data()
{
    QTest::addColumn<QDateTime> ( "firstPlayed" );
    QTest::addColumn<QDateTime> ( "lastPlayed" );
    QTest::addColumn<int>       ( "rating" );
    QTest::addColumn<int>       ( "playCount" );

    QVector<QDateTime> d;
    for( uint t = 0; t < 20; ++t )
        d.push_back( QDateTime::fromSecsSinceEpoch( 1378125780u + t ) );

    QTest::newRow( "title0" ) << QDateTime() << QDateTime() <<  0 <<  0;
    QTest::newRow( "title1" ) << QDateTime() <<       d[ 3] <<  2 << 15;
    QTest::newRow( "title2" ) << QDateTime() << QDateTime() <<  3 << 14;
    QTest::newRow( "title3" ) << QDateTime() <<       d[ 7] <<  0 << 13;
    QTest::newRow( "title4" ) << QDateTime() << QDateTime() <<  5 <<  0;
    QTest::newRow( "title5" ) <<       d[10] <<       d[11] <<  6 << 10;
    QTest::newRow( "title6" ) <<       d[12] << QDateTime() <<  0 <<  7;
    QTest::newRow( "title7" ) <<       d[14] <<       d[15] <<  8 <<  5;
    QTest::newRow( "title8" ) <<       d[16] << QDateTime() <<  9 <<  0;
    QTest::newRow( "title9" ) <<       d[18] <<       d[19] <<  0 <<  2;
}

void
TestImporterBase::tracksShouldBehaveNicelyWithNoStatistics()
{
    checkStatistics( "testStatisticsNotSet" );
}

void TestImporterBase::labels( const ProviderPtr &provider, const QString &trackName )
{
    m_lbl.clear();

    const QString artist = "testStatistics";

    QVERIFY( provider->artists().contains( artist ) );

    QMap<QString, TrackPtr> trackForName;
    foreach( const TrackPtr &track, provider->artistTracks( artist ) )
    {
        QVERIFY( !trackForName.contains( track->name() ) );
        trackForName.insert( track->name(), track );
    }

    QVERIFY( trackForName.contains( trackName ) );
    m_lbl = trackForName.value( trackName )->labels();
}

void
TestImporterBase::tracksShouldWorkWithSingleLabel()
{
    ProviderPtr provider( getProvider() );
    amarokProviderSkipIfNoMysqld( provider );
    skipIfNoSupport( reliableStatistics(), Meta::valLabel );

    labels( provider, "title0" );

    QCOMPARE( m_lbl.size(), 1 );
    QVERIFY( m_lbl.contains( "singleTag" ) );
}

void
TestImporterBase::tracksShouldWorkWithMultipleLabels()
{
    ProviderPtr provider( getProvider() );
    amarokProviderSkipIfNoMysqld( provider );
    skipIfNoSupport( reliableStatistics(), Meta::valLabel );

    labels( provider, "title1" );

    QCOMPARE( m_lbl.size(), 2 );
    QVERIFY( m_lbl.contains( "multiple" ) );
    QVERIFY( m_lbl.contains( "tags" ) );
}

void
TestImporterBase::tracksShouldWorkWithCaseSensitiveLabels()
{
    ProviderPtr provider( getProvider() );
    amarokProviderSkipIfNoMysqld( provider );
    skipIfNoSupport( reliableStatistics(), Meta::valLabel );

    labels( provider, "title2" );

    QCOMPARE( m_lbl.size(), 2 );
    QVERIFY( m_lbl.contains( "caseSensitive" ) );
    QVERIFY( m_lbl.contains( "casesensitive" ) );
}

void
TestImporterBase::tracksShouldWorkWithUTFLabels()
{
    ProviderPtr provider( getProvider() );
    amarokProviderSkipIfNoMysqld( provider );
    skipIfNoSupport( reliableStatistics(), Meta::valLabel );

    labels( provider, "title3" );

    QCOMPARE( m_lbl.size(), 1 );
    QVERIFY( m_lbl.contains( QString::fromWCharArray( L"\x2622" ) ) );
}

void
TestImporterBase::providerShouldReturnNoTracksForNonexistentArtist()
{
    ProviderPtr provider( getProvider() );
    amarokProviderSkipIfNoMysqld( provider );

    const QString artist = "I'mNotHere";
    QVERIFY( !provider->artists().contains( artist ) );
    QVERIFY( provider->artistTracks( artist ).isEmpty() );
}

void
TestImporterBase::providerShouldNotBreakOnLittleBobbyTables()
{
    ProviderPtr provider( getProvider() );
    amarokProviderSkipIfNoMysqld( provider );

    const QString artist = "Robert'); DROP TABLE students;--";
    QVERIFY( !provider->artists().contains( artist ) );
    QVERIFY( provider->artistTracks( artist ).isEmpty() );
}

static TrackPtr
trackForName( ProviderPtr &provider, const QString &name, const QString &artist )
{
    foreach( const TrackPtr &track, provider->artistTracks( artist ) )
        if( track->name() == name )
            return track;

    return TrackPtr( nullptr );
}

static Meta::FieldHash
saveData( const TrackPtr &track )
{
    Meta::FieldHash data;
    data.insert( Meta::valTitle, track->name() );
    data.insert( Meta::valArtist, track->artist() );
    data.insert( Meta::valAlbum, track->album() );
    data.insert( Meta::valComposer, track->composer() );
    data.insert( Meta::valTrackNr, track->trackNumber() );
    data.insert( Meta::valDiscNr, track->discNumber() );
    data.insert( Meta::valFirstPlayed, track->firstPlayed() );
    data.insert( Meta::valLastPlayed, track->lastPlayed() );
    data.insert( Meta::valRating, track->rating() );
    data.insert( Meta::valPlaycount, track->playCount() );
    data.insert( Meta::valLabel, QStringList( track->labels().values() ) );
    return data;
}

static void
verifyEqualExcept( const Meta::FieldHash &lhs, const TrackPtr &track,
                   const qint64 except )
{
    const QList<qint64> fields = QList<qint64>() << Meta::valTitle << Meta::valArtist
                              << Meta::valAlbum << Meta::valComposer << Meta::valTrackNr
                              << Meta::valDiscNr << Meta::valFirstPlayed
                              << Meta::valLastPlayed << Meta::valRating
                              << Meta::valPlaycount << Meta::valLabel;

    const Meta::FieldHash rhs = saveData( track );
    foreach( const qint64 field, fields )
        if( !( except & field ) )
            QCOMPARE( lhs.value( field ), rhs.value( field ) );
}

void
TestImporterBase::commitAfterSettingAllStatisticsShouldSaveThem_data()
{
    QTest::addColumn<QString>( "title" );
    QTest::addColumn<QString>( "artist" );
    QTest::addColumn<QDateTime>( "newFirstPlayed" );
    QTest::addColumn<QDateTime>( "newLastPlayed" );
    QTest::addColumn<int>( "newRating" );
    QTest::addColumn<int>( "newPlayCount" );
    QTest::addColumn<QStringList>( "newLabels" );

    const uint now = QDateTime::currentDateTimeUtc().toSecsSinceEpoch();

    QTest::newRow( "Replace all" ) << "title0" << "testStatistics"
                                   << QDateTime::fromSecsSinceEpoch( now - 100 )
                                   << QDateTime::fromSecsSinceEpoch( now + 100 )
                                   << 9 << 25 << ( QStringList() << "teh" << "lab'ls" );

    QTest::newRow( "Add all" ) << "title0" << "testStatisticsNotSet"
                               << QDateTime::fromSecsSinceEpoch( now - 100 )
                               << QDateTime::fromSecsSinceEpoch(now + 100 )
                               << 9 << 25 << ( QStringList() << "teh" << "lab'ls" );

    QTest::newRow( "Add some 1" ) << "title2" << "testStatisticsNotSet"
                                  << QDateTime::fromSecsSinceEpoch( now - 100 )
                                  << QDateTime::fromSecsSinceEpoch( now + 100 )
                                  << 9 << 25 << ( QStringList() << "teh" << "lab'ls" );

    QTest::newRow( "Add some 1" ) << "title4" << "testStatisticsNotSet"
                                  << QDateTime::fromSecsSinceEpoch( now - 100 )
                                  << QDateTime::fromSecsSinceEpoch( now + 100 )
                                  << 9 << 25 << ( QStringList() << "teh" << "lab'ls" );

    QTest::newRow( "Add some 1" ) << "title6" << "testStatisticsNotSet"
                                  << QDateTime::fromSecsSinceEpoch( now - 100 )
                                  << QDateTime::fromSecsSinceEpoch( now + 100 )
                                  << 9 << 25 << ( QStringList() << "teh" << "lab'ls" );
}

void
TestImporterBase::commitAfterSettingAllStatisticsShouldSaveThem()
{
    ProviderPtr provider( getWritableProvider() );
    amarokProviderSkipIfNoMysqld( provider );

    QFETCH( QString, title );
    QFETCH( QString, artist );
    TrackPtr track = trackForName( provider, title, artist );
    QVERIFY( track );

    const Meta::FieldHash data = saveData( track );

    if( provider->writableTrackStatsData() & Meta::valFirstPlayed )
    {
        QFETCH( QDateTime, newFirstPlayed );
        track->setFirstPlayed( newFirstPlayed );
    }
    if( provider->writableTrackStatsData() & Meta::valLastPlayed )
    {
        QFETCH( QDateTime, newLastPlayed );
        track->setLastPlayed( newLastPlayed );
    }
    if( provider->writableTrackStatsData() & Meta::valRating )
    {
        QFETCH( int, newRating );
        track->setRating( newRating );
    }
    if( provider->writableTrackStatsData() & Meta::valPlaycount )
    {
        QFETCH( int, newPlayCount );
        track->setPlayCount( newPlayCount );
    }
    if( provider->writableTrackStatsData() & Meta::valLabel )
    {
        QFETCH( QStringList, newLabels );
        track->setLabels( QSet<QString> ( newLabels.begin(), newLabels.end() ) );
    }

    track->commit();
    provider->commitTracks();

    track = trackForName( provider, title, artist );
    QVERIFY( track );

    if( provider->writableTrackStatsData() & Meta::valFirstPlayed )
    {
        QFETCH( QDateTime, newFirstPlayed );
        QCOMPARE( track->firstPlayed(), newFirstPlayed );
    }
    if( provider->writableTrackStatsData() & Meta::valLastPlayed )
    {
        QFETCH( QDateTime, newLastPlayed );
        QCOMPARE( track->lastPlayed(), newLastPlayed );
    }
    if( provider->writableTrackStatsData() & Meta::valRating )
    {
        QFETCH( int, newRating );
        if( !hasOddRatings() && (newRating & 1) )
            ++newRating;
        QCOMPARE( track->rating(), newRating );
    }
    if( provider->writableTrackStatsData() & Meta::valPlaycount )
    {
        QFETCH( int, newPlayCount );
        QCOMPARE( track->playCount(), newPlayCount );
    }
    if( provider->writableTrackStatsData() & Meta::valLabel )
    {
        QFETCH( QStringList, newLabels );
        QCOMPARE( track->labels(), QSet<QString> ( newLabels.begin(), newLabels.end() ) );
    }

    verifyEqualExcept( data, track, Meta::valFirstPlayed | Meta::valLastPlayed |
                       Meta::valRating | Meta::valPlaycount | Meta::valLabel );
}

void
TestImporterBase::commitAfterSettingFirstPlayedShouldSaveIt_data()
{
     QTest::addColumn<QString>( "title" );
     QTest::addColumn<QDateTime>( "newFirstPlayed" );

     const uint now = QDateTime::currentDateTimeUtc().toSecsSinceEpoch();

     QTest::newRow( "Add stat 1" ) << "title0" << QDateTime::fromSecsSinceEpoch( now );
     QTest::newRow( "Add stat 2" ) << "title1" << QDateTime::fromSecsSinceEpoch( now + 2 );
     QTest::newRow( "Add stat 3" ) << "title2" << QDateTime::fromSecsSinceEpoch( now + 3 );

     QTest::newRow( "Replace stat 1" ) << "title5" << QDateTime::fromSecsSinceEpoch( now + 11 );
     QTest::newRow( "Replace stat 2" ) << "title6" << QDateTime::fromSecsSinceEpoch( now + 13 );
     QTest::newRow( "Replace stat 3" ) << "title7" << QDateTime::fromSecsSinceEpoch( now + 17 );

     QTest::newRow( "Remove stat 1" ) << "title5" << QDateTime();
     QTest::newRow( "Remove stat 2" ) << "title6" << QDateTime();
     QTest::newRow( "Remove stat 3" ) << "title7" << QDateTime();
}

void
TestImporterBase::commitAfterSettingFirstPlayedShouldSaveIt()
{
    ProviderPtr provider( getWritableProvider() );
    amarokProviderSkipIfNoMysqld( provider );
    skipIfNoSupport( provider->writableTrackStatsData(), Meta::valFirstPlayed );

    QFETCH( QString, title );
    QFETCH( QDateTime, newFirstPlayed );

    TrackPtr track = trackForName( provider, title, "testStatisticsNotSet" );
    QVERIFY( track );

    const Meta::FieldHash data = saveData( track );
    track->setFirstPlayed( newFirstPlayed );
    track->commit();
    provider->commitTracks();

    track = trackForName( provider, title, "testStatisticsNotSet" );
    QVERIFY( track );
    QCOMPARE( track->firstPlayed(), newFirstPlayed );
    verifyEqualExcept( data, track, Meta::valFirstPlayed );
}

void
TestImporterBase::commitAfterSettingLastPlayedShouldSaveIt_data()
{
    QTest::addColumn<QString>( "title" );
    QTest::addColumn<QDateTime>( "newLastPlayed" );

    const uint now = QDateTime::currentDateTimeUtc().toSecsSinceEpoch();

    QTest::newRow( "Add stat 1" ) << "title0" << QDateTime::fromSecsSinceEpoch( now );
    QTest::newRow( "Add stat 2" ) << "title2" << QDateTime::fromSecsSinceEpoch( now + 2 );
    QTest::newRow( "Add stat 3" ) << "title4" << QDateTime::fromSecsSinceEpoch( now + 3 );

    QTest::newRow( "Replace stat 1" ) << "title1" << QDateTime::fromSecsSinceEpoch( now + 11 );
    QTest::newRow( "Replace stat 2" ) << "title3" << QDateTime::fromSecsSinceEpoch( now + 13 );
    QTest::newRow( "Replace stat 3" ) << "title5" << QDateTime::fromSecsSinceEpoch( now + 17 );

    QTest::newRow( "Remove stat 1" ) << "title1" << QDateTime();
    QTest::newRow( "Remove stat 2" ) << "title3" << QDateTime();
    QTest::newRow( "Remove stat 3" ) << "title5" << QDateTime();
}

void
TestImporterBase::commitAfterSettingLastPlayedShouldSaveIt()
{
    ProviderPtr provider( getWritableProvider() );
    amarokProviderSkipIfNoMysqld( provider );
    skipIfNoSupport( provider->writableTrackStatsData(), Meta::valLastPlayed );

    QFETCH( QString, title );
    QFETCH( QDateTime, newLastPlayed );

    TrackPtr track = trackForName( provider, title, "testStatisticsNotSet" );
    QVERIFY( track );

    const Meta::FieldHash data = saveData( track );
    track->setLastPlayed( newLastPlayed );
    track->commit();
    provider->commitTracks();

    track = trackForName( provider, title, "testStatisticsNotSet" );
    QVERIFY( track );
    QCOMPARE( track->lastPlayed(), newLastPlayed );
    verifyEqualExcept( data, track, Meta::valLastPlayed );
}

void
TestImporterBase::commitAfterSettingRatingShouldSaveIt_data()
{
    QTest::addColumn<QString>( "title" );
    QTest::addColumn<int>( "newRating" );

    QTest::newRow( "Add stat 1" ) << "title0" << 2;
    QTest::newRow( "Add stat 2" ) << "title3" << 3;
    QTest::newRow( "Add stat 3" ) << "title6" << 5;

    QTest::newRow( "Replace stat 1" ) << "title1" << 1;
    QTest::newRow( "Replace stat 2" ) << "title2" << 3;
    QTest::newRow( "Replace stat 3" ) << "title4" << 6;

    QTest::newRow( "Remove stat 1" ) << "title1" << 0;
    QTest::newRow( "Remove stat 2" ) << "title2" << 0;
    QTest::newRow( "Remove stat 3" ) << "title4" << 0;
}

void
TestImporterBase::commitAfterSettingRatingShouldSaveIt()
{
    ProviderPtr provider( getWritableProvider() );
    amarokProviderSkipIfNoMysqld( provider );
    skipIfNoSupport( provider->writableTrackStatsData(), Meta::valRating );

    QFETCH( QString, title );
    QFETCH( int, newRating );

    TrackPtr track = trackForName( provider, title, "testStatisticsNotSet" );
    QVERIFY( track );

    const Meta::FieldHash data = saveData( track );
    track->setRating( newRating );
    track->commit();
    provider->commitTracks();

    if( !hasOddRatings() && (newRating & 1) )
        ++newRating;

    track = trackForName( provider, title, "testStatisticsNotSet" );
    QVERIFY( track );
    QCOMPARE( track->rating(), newRating );
    verifyEqualExcept( data, track, Meta::valRating );
}

void
TestImporterBase::commitAfterSettingPlaycountShouldSaveIt_data()
{
    QTest::addColumn<QString>( "title" );
    QTest::addColumn<int>( "newPlayCount" );

    QTest::newRow( "Add stat 1" ) << "title0" << 13;
    QTest::newRow( "Add stat 2" ) << "title4" << 17;
    QTest::newRow( "Add stat 3" ) << "title8" << 23;

    QTest::newRow( "Replace stat 1" ) << "title1" << 1;
    QTest::newRow( "Replace stat 2" ) << "title2" << 3;
    QTest::newRow( "Replace stat 3" ) << "title3" << 6;

    QTest::newRow( "Remove stat 1" ) << "title1" << 0;
    QTest::newRow( "Remove stat 2" ) << "title2" << 0;
    QTest::newRow( "Remove stat 3" ) << "title3" << 0;
}

void
TestImporterBase::commitAfterSettingPlaycountShouldSaveIt()
{
    ProviderPtr provider( getWritableProvider() );
    amarokProviderSkipIfNoMysqld( provider );
    skipIfNoSupport( provider->writableTrackStatsData(), Meta::valPlaycount );

    QFETCH( QString, title );
    QFETCH( int, newPlayCount );

    TrackPtr track = trackForName( provider, title, "testStatisticsNotSet" );
    QVERIFY( track );

    const Meta::FieldHash data = saveData( track );
    track->setPlayCount( newPlayCount );
    track->commit();
    provider->commitTracks();

    track = trackForName( provider, title, "testStatisticsNotSet" );
    QVERIFY( track );
    QCOMPARE( track->playCount(), newPlayCount );
    verifyEqualExcept( data, track, Meta::valPlaycount );
}

void
TestImporterBase::commitAfterSettingLabelsShouldSaveThem_data()
{
    QTest::addColumn<QString>( "title" );
    QTest::addColumn<QStringList>( "newLabels" );

    QTest::newRow( "Add new label" ) << "title4" << ( QStringList() << "singleTag2" );
    QTest::newRow( "Add existing label" ) << "title5" << ( QStringList() << "singleTag" );
    QTest::newRow( "Add labels" ) << "title6" << ( QStringList() << "multi" << "labels" );
    QTest::newRow( "Add existing labels" ) << "title7"
                                           << ( QStringList() << "multiple" << "labels" );
    QTest::newRow( "Add case-sensitive labels" ) << "title8"
                                                 << ( QStringList() << "cs" << "Cs" );

    QTest::newRow( "Replace all labels" ) << "title1" << ( QStringList() << "a" << "l" );
    QTest::newRow( "Replace some labels" ) << "title1"
                                           << ( QStringList() << "a" << "tags" );
    QTest::newRow( "Add additional labels" ) << "title1"
                                      << ( QStringList() << "multiple" << "tags" << "2" );

    QTest::newRow( "Remove labels 1" ) << "title0" << QStringList();
    QTest::newRow( "Remove labels 2" ) << "title1" << QStringList();
    QTest::newRow( "Remove labels 3" ) << "title2" << QStringList();
}

void
TestImporterBase::commitAfterSettingLabelsShouldSaveThem()
{
    ProviderPtr provider( getWritableProvider() );
    amarokProviderSkipIfNoMysqld( provider );
    skipIfNoSupport( provider->writableTrackStatsData(), Meta::valLabel );

    QFETCH( QString, title );
    QFETCH( QStringList, newLabels );

    TrackPtr track = trackForName( provider, title, "testStatistics" );
    QVERIFY( track );

    const Meta::FieldHash data = saveData( track );
    track->setLabels( QSet<QString> ( newLabels.begin(), newLabels.end() ) );
    track->commit();
    provider->commitTracks();

    track = trackForName( provider, title, "testStatistics" );
    QVERIFY( track );
    QCOMPARE( track->labels(), QSet<QString> ( newLabels.begin(), newLabels.end() ) );
    verifyEqualExcept( data, track, Meta::valLabel );
}
