/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the test suite 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$
**
****************************************************************************/

#include <QtTest/QtTest>
#include <QtGlobal>
#include <math.h>
#include <QMetaObject>
#include <qtest.h>
#include <QtTest/qsignalspy.h>
#include <QtQml/qqmlnetworkaccessmanagerfactory.h>
#include <QtNetwork/qnetworkaccessmanager.h>
#include <QtNetwork/qnetworkrequest.h>
#include <QtCore/qtimer.h>
#include <QtCore/qfile.h>
#include <QtCore/qtemporaryfile.h>
#include <QtCore/qsortfilterproxymodel.h>
#include "../../shared/util.h"
#include <private/qqmlengine_p.h>

#include <QtQml/qqmlengine.h>
#include <QtQml/qqmlcomponent.h>
#include "../../../src/imports/xmllistmodel/qqmlxmllistmodel_p.h"

#include <algorithm>

typedef QPair<int, int> QQuickXmlListRange;
typedef QList<QVariantList> QQmlXmlModelData;

Q_DECLARE_METATYPE(QList<QQuickXmlListRange>)
Q_DECLARE_METATYPE(QQmlXmlModelData)
Q_DECLARE_METATYPE(QQuickXmlListModel::Status)

class tst_qquickxmllistmodel : public QQmlDataTest

{
    Q_OBJECT
public:
    tst_qquickxmllistmodel() {}

private slots:
    void initTestCase() {
        QQmlDataTest::initTestCase();
        qRegisterMetaType<QQuickXmlListModel::Status>();
    }

    void buildModel();
    void testTypes();
    void testTypes_data();
    void cdata();
    void attributes();
    void roles();
    void roleErrors();
    void uniqueRoleNames();
    void headers();
    void xml();
    void xml_data();
    void source();
    void source_data();
    void data();
    void get();
    void reload();
    void useKeys();
    void useKeys_data();
    void noKeysValueChanges();
    void keysChanged();
    void threading();
    void threading_data();
    void propertyChanges();
    void selectAncestor();

    void roleCrash();
    void proxyCrash();

private:
    QString errorString(QAbstractItemModel *model) {
        QString ret;
        QMetaObject::invokeMethod(model, "errorString", Q_RETURN_ARG(QString, ret));
        return ret;
    }

    QString makeItemXmlAndData(const QString &data, QQmlXmlModelData *modelData = 0) const
    {
        if (modelData)
            modelData->clear();
        QString xml;

        if (!data.isEmpty()) {
            const QStringList items = data.split(QLatin1Char(';'));
            for (const QString &item : items) {
                if (item.isEmpty())
                    continue;
                QVariantList variants;
                xml += QLatin1String("<item>");
                const QStringList fields = item.split(QLatin1Char(','));
                for (const QString &field : fields) {
                    QStringList values = field.split(QLatin1Char('='));
                    if (values.count() != 2) {
                        qWarning() << "makeItemXmlAndData: invalid field:" << field;
                        continue;
                    }
                    xml += QString("<%1>%2</%1>").arg(values[0], values[1]);
                    if (!modelData)
                        continue;
                    bool isNum = false;
                    int number = values[1].toInt(&isNum);
                    if (isNum)
                        variants << number;
                    else
                        variants << values[1];
                }
                xml += QLatin1String("</item>");
                if (modelData)
                    modelData->append(variants);
            }
        }

        QString decl = "<?xml version=\"1.0\" encoding=\"iso-8859-1\" ?>";
        return decl + QLatin1String("<data>") + xml + QLatin1String("</data>");
    }

    QQmlEngine engine;
};

class CustomNetworkAccessManagerFactory : public QObject, public QQmlNetworkAccessManagerFactory
{
    Q_OBJECT
public:
    QVariantMap lastSentHeaders;

protected:
    QNetworkAccessManager *create(QObject *parent);
};

class CustomNetworkAccessManager : public QNetworkAccessManager
{
    Q_OBJECT
public:
    CustomNetworkAccessManager(CustomNetworkAccessManagerFactory *factory, QObject *parent)
        : QNetworkAccessManager(parent), m_factory(factory) {}

protected:
    QNetworkReply *createRequest(Operation op, const QNetworkRequest &req, QIODevice * outgoingData = 0)
    {
        if (m_factory) {
            QVariantMap map;
            const auto rawHeaderList = req.rawHeaderList();
            for (const QString &header : rawHeaderList)
                map[header] = req.rawHeader(header.toUtf8());
            m_factory->lastSentHeaders = map;
        }
        return QNetworkAccessManager::createRequest(op, req, outgoingData);
    }

    QPointer<CustomNetworkAccessManagerFactory> m_factory;
};

QNetworkAccessManager *CustomNetworkAccessManagerFactory::create(QObject *parent)
{
    return new CustomNetworkAccessManager(this, parent);
}


void tst_qquickxmllistmodel::buildModel()
{
    QQmlComponent component(&engine, testFileUrl("model.qml"));
    QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(component.create());
    QVERIFY(model != 0);
    QTRY_COMPARE(model->rowCount(), 9);

    QModelIndex index = model->index(3, 0);
    QCOMPARE(model->data(index, Qt::UserRole).toString(), QLatin1String("Spot"));
    QCOMPARE(model->data(index, Qt::UserRole+1).toString(), QLatin1String("Dog"));
    QCOMPARE(model->data(index, Qt::UserRole+2).toInt(), 9);
    QCOMPARE(model->data(index, Qt::UserRole+3).toString(), QLatin1String("Medium"));

    delete model;
}

void tst_qquickxmllistmodel::testTypes()
{
    QFETCH(QString, xml);
    QFETCH(QString, roleName);
    QFETCH(QVariant, expectedValue);

    QQmlComponent component(&engine, testFileUrl("testtypes.qml"));
    QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(component.create());
    QVERIFY(model != 0);
    model->setProperty("xml",xml.toUtf8());
    QMetaObject::invokeMethod(model, "reload");
    QTRY_COMPARE(model->rowCount(), 1);

    int role = model->roleNames().key(roleName.toUtf8(), -1);
    QVERIFY(role >= 0);

    QModelIndex index = model->index(0, 0);
    if (expectedValue.toString() == "nan")
        QVERIFY(qIsNaN(model->data(index, role).toDouble()));
    else
        QCOMPARE(model->data(index, role), expectedValue);

    delete model;
}

void tst_qquickxmllistmodel::testTypes_data()
{
    QTest::addColumn<QString>("xml");
    QTest::addColumn<QString>("roleName");
    QTest::addColumn<QVariant>("expectedValue");

    QTest::newRow("missing string field") << "<data></data>"
            << "stringValue" << QVariant("");
    QTest::newRow("empty string") << "<data><a-string></a-string></data>"
            << "stringValue" << QVariant("");
    QTest::newRow("1-char string") << "<data><a-string>5</a-string></data>"
            << "stringValue" << QVariant("5");
    QTest::newRow("string ok") << "<data><a-string>abc def g</a-string></data>"
            << "stringValue" << QVariant("abc def g");

    QTest::newRow("missing number field") << "<data></data>"
            << "numberValue" << QVariant("");
    double nan = qQNaN();
    QTest::newRow("empty number field") << "<data><a-number></a-number></data>"
            << "numberValue" << QVariant(nan);
    QTest::newRow("number field with string") << "<data><a-number>a string</a-number></data>"
            << "numberValue" << QVariant(nan);
    QTest::newRow("-1") << "<data><a-number>-1</a-number></data>"
            << "numberValue" << QVariant("-1");
    QTest::newRow("-1.5") << "<data><a-number>-1.5</a-number></data>"
            << "numberValue" << QVariant("-1.5");
    QTest::newRow("0") << "<data><a-number>0</a-number></data>"
            << "numberValue" << QVariant("0");
    QTest::newRow("+1") << "<data><a-number>1</a-number></data>"
            << "numberValue" << QVariant("1");
    QTest::newRow("+1.5") << "<data><a-number>1.5</a-number></data>"
            << "numberValue" << QVariant("1.5");
}

void tst_qquickxmllistmodel::cdata()
{
    QQmlComponent component(&engine, testFileUrl("recipes.qml"));
    QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(component.create());
    QVERIFY(model != 0);
    QTRY_COMPARE(model->rowCount(), 5);

    QVERIFY(model->data(model->index(2, 0), Qt::UserRole+2).toString().startsWith(QLatin1String("<html>")));

    delete model;
}

void tst_qquickxmllistmodel::attributes()
{
    QQmlComponent component(&engine, testFileUrl("recipes.qml"));
    QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(component.create());
    QVERIFY(model != 0);
    QTRY_COMPARE(model->rowCount(), 5);
    QCOMPARE(model->data(model->index(2, 0), Qt::UserRole).toString(), QLatin1String("Vegetable Soup"));

    delete model;
}

void tst_qquickxmllistmodel::roles()
{
    QQmlComponent component(&engine, testFileUrl("model.qml"));
    QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(component.create());
    QVERIFY(model != 0);
    QTRY_COMPARE(model->rowCount(), 9);

    QHash<int, QByteArray> roleNames = model->roleNames();
    QCOMPARE(roleNames.count(), 4);
    QVERIFY(roleNames.key("name", -1) >= 0);
    QVERIFY(roleNames.key("type", -1) >= 0);
    QVERIFY(roleNames.key("age", -1) >= 0);
    QVERIFY(roleNames.key("size", -1) >= 0);

    QSet<int> roles;
    roles.insert(roleNames.key("name"));
    roles.insert(roleNames.key("type"));
    roles.insert(roleNames.key("age"));
    roles.insert(roleNames.key("size"));
    QCOMPARE(roles.count(), 4);

    delete model;
}

void tst_qquickxmllistmodel::roleErrors()
{
    QQmlComponent component(&engine, testFileUrl("roleErrors.qml"));
    QTest::ignoreMessage(QtWarningMsg, (testFileUrl("roleErrors.qml").toString() + ":7:5: QML XmlRole: An XmlRole query must not start with '/'").toUtf8().constData());
    QTest::ignoreMessage(QtWarningMsg, (testFileUrl("roleErrors.qml").toString() + ":10:5: QML XmlRole: invalid query: \"age/\"").toUtf8().constData());

    //### make sure we receive all expected warning messages.
    QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(component.create());
    QVERIFY(model != 0);
    QTRY_COMPARE(model->rowCount(), 9);

    QModelIndex index = model->index(3, 0);
    //### should any of these return valid values?
    QCOMPARE(model->data(index, Qt::UserRole), QVariant());
    QCOMPARE(model->data(index, Qt::UserRole+1), QVariant());
    QCOMPARE(model->data(index, Qt::UserRole+2), QVariant());

    QEXPECT_FAIL("", "QTBUG-10797", Continue);
    QCOMPARE(model->data(index, Qt::UserRole+3), QVariant());

    delete model;
}

void tst_qquickxmllistmodel::uniqueRoleNames()
{
    QQmlComponent component(&engine, testFileUrl("unique.qml"));
    QTest::ignoreMessage(QtWarningMsg, (testFileUrl("unique.qml").toString() + ":8:5: QML XmlRole: \"name\" duplicates a previous role name and will be disabled.").toUtf8().constData());
    QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(component.create());
    QVERIFY(model != 0);
    QTRY_COMPARE(model->rowCount(), 9);

    QHash<int, QByteArray> roleNames = model->roleNames();
    QCOMPARE(roleNames.count(), 1);

    delete model;
}


void tst_qquickxmllistmodel::xml()
{
    QFETCH(QString, xml);
    QFETCH(int, count);

    QQmlComponent component(&engine, testFileUrl("model.qml"));
    QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(component.create());

    QSignalSpy spy(model, SIGNAL(statusChanged(QQuickXmlListModel::Status)));
    QVERIFY(errorString(model).isEmpty());
    QCOMPARE(model->property("progress").toDouble(), qreal(1.0));
    QCOMPARE(qvariant_cast<QQuickXmlListModel::Status>(model->property("status")),
             QQuickXmlListModel::Loading);
    QTRY_COMPARE(spy.count(), 1); spy.clear();
    QTest::qWait(50);
    QCOMPARE(qvariant_cast<QQuickXmlListModel::Status>(model->property("status")),
             QQuickXmlListModel::Ready);
    QVERIFY(errorString(model).isEmpty());
    QCOMPARE(model->property("progress").toDouble(), qreal(1.0));
    QCOMPARE(model->rowCount(), 9);

    // if xml is empty (i.e. clearing) it won't have any effect if a source is set
    if (xml.isEmpty())
        model->setProperty("source",QUrl());
    model->setProperty("xml",xml);
    QCOMPARE(model->property("progress").toDouble(), qreal(1.0));   // immediately goes to 1.0 if using setXml()
    QTRY_COMPARE(spy.count(), 1); spy.clear();
    QCOMPARE(qvariant_cast<QQuickXmlListModel::Status>(model->property("status")),
             QQuickXmlListModel::Loading);
    QTRY_COMPARE(spy.count(), 1); spy.clear();
    if (xml.isEmpty())
        QCOMPARE(qvariant_cast<QQuickXmlListModel::Status>(model->property("status")),
                 QQuickXmlListModel::Null);
    else
        QCOMPARE(qvariant_cast<QQuickXmlListModel::Status>(model->property("status")),
                 QQuickXmlListModel::Ready);
    QVERIFY(errorString(model).isEmpty());
    QCOMPARE(model->rowCount(), count);

    delete model;
}

void tst_qquickxmllistmodel::xml_data()
{
    QTest::addColumn<QString>("xml");
    QTest::addColumn<int>("count");

    QTest::newRow("xml with no items") << "<Pets></Pets>" << 0;
    QTest::newRow("empty xml") << "" << 0;
    QTest::newRow("one item") << "<Pets><Pet><name>Hobbes</name><type>Tiger</type><age>7</age><size>Large</size></Pet></Pets>" << 1;
}

void tst_qquickxmllistmodel::headers()
{
    // ensure the QNetworkAccessManagers created for this test are immediately deleted
    QQmlEngine qmlEng;

    CustomNetworkAccessManagerFactory factory;
    qmlEng.setNetworkAccessManagerFactory(&factory);

    QQmlComponent component(&qmlEng, testFileUrl("model.qml"));
    QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(component.create());
    QVERIFY(model != 0);
    QTRY_COMPARE(qvariant_cast<QQuickXmlListModel::Status>(model->property("status")),
                 QQuickXmlListModel::Ready);

    // It doesn't do a network request for a local file
    QCOMPARE(factory.lastSentHeaders.count(), 0);

    model->setProperty("source", QUrl("http://localhost/filethatdoesnotexist.xml"));
    QTRY_COMPARE(qvariant_cast<QQuickXmlListModel::Status>(model->property("status")),
                 QQuickXmlListModel::Error);

    QVariantMap expectedHeaders;
    expectedHeaders["Accept"] = "application/xml,*/*";

    QCOMPARE(factory.lastSentHeaders.count(), expectedHeaders.count());
    for (auto it = expectedHeaders.cbegin(), end = expectedHeaders.cend(); it != end; ++it) {
        QVERIFY(factory.lastSentHeaders.contains(it.key()));
        QCOMPARE(factory.lastSentHeaders[it.key()].toString(), it.value().toString());
    }

    delete model;
}

void tst_qquickxmllistmodel::source()
{
    QFETCH(QUrl, source);
    QFETCH(int, count);
    QFETCH(QQuickXmlListModel::Status, status);

    QQmlComponent component(&engine, testFileUrl("model.qml"));
    QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(component.create());
    QSignalSpy spy(model, SIGNAL(statusChanged(QQuickXmlListModel::Status)));

    QVERIFY(errorString(model).isEmpty());
    QCOMPARE(model->property("progress").toDouble(), qreal(1.0));
    QCOMPARE(qvariant_cast<QQuickXmlListModel::Status>(model->property("status")),
             QQuickXmlListModel::Loading);
    QTRY_COMPARE(spy.count(), 1); spy.clear();
    QCOMPARE(qvariant_cast<QQuickXmlListModel::Status>(model->property("status")),
             QQuickXmlListModel::Ready);
    QVERIFY(errorString(model).isEmpty());
    QCOMPARE(model->property("progress").toDouble(), qreal(1.0));
    QCOMPARE(model->rowCount(), 9);

    model->setProperty("source",source);
    if (model->property("source").toString().isEmpty())
        QCOMPARE(qvariant_cast<QQuickXmlListModel::Status>(model->property("status")),
                 QQuickXmlListModel::Null);
    QCOMPARE(model->property("progress").toDouble(), qreal(source.isLocalFile() ? 1.0 : 0.0));
    QTRY_COMPARE(spy.count(), 1); spy.clear();
    QCOMPARE(qvariant_cast<QQuickXmlListModel::Status>(model->property("status")),
             QQuickXmlListModel::Loading);
    QVERIFY(errorString(model).isEmpty());

    QEventLoop loop;
    QTimer timer;
    timer.setSingleShot(true);
    connect(model, SIGNAL(statusChanged(QQuickXmlListModel::Status)), &loop, SLOT(quit()));
    connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit()));
    timer.start(20000);
    loop.exec();

    if (spy.count() == 0 && status != QQuickXmlListModel::Ready) {
        qWarning("QQuickXmlListModel invalid source test timed out");
    } else {
        QCOMPARE(spy.count(), 1); spy.clear();
    }

    QCOMPARE(qvariant_cast<QQuickXmlListModel::Status>(model->property("status")), status);
    QCOMPARE(model->rowCount(), count);

    if (status == QQuickXmlListModel::Ready)
        QCOMPARE(model->property("progress").toDouble(), qreal(1.0));

    QCOMPARE(errorString(model).isEmpty(), status == QQuickXmlListModel::Ready);

    delete model;
}

void tst_qquickxmllistmodel::source_data()
{
    QTest::addColumn<QUrl>("source");
    QTest::addColumn<int>("count");
    QTest::addColumn<QQuickXmlListModel::Status>("status");

    QTest::newRow("valid") << testFileUrl("model2.xml") << 2
                           << QQuickXmlListModel::Ready;
    QTest::newRow("invalid") << QUrl("http://blah.blah/blah.xml") << 0
                             << QQuickXmlListModel::Error;

    // empty file
    QTemporaryFile *temp = new QTemporaryFile(this);
    if (temp->open())
        QTest::newRow("empty file") << QUrl::fromLocalFile(temp->fileName()) << 0
                                    << QQuickXmlListModel::Ready;
    temp->close();
}

void tst_qquickxmllistmodel::data()
{
    QQmlComponent component(&engine, testFileUrl("model.qml"));
    QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(component.create());
    QVERIFY(model != 0);

    for (int i=0; i<9; i++)  {
        QModelIndex index = model->index(i, 0);
        for (int j=0; j<model->roleNames().count(); j++) {
            QCOMPARE(model->data(index, j), QVariant());
        }
    }
    QTRY_COMPARE(model->rowCount(), 9);

    delete model;
}

void tst_qquickxmllistmodel::get()
{
    QQmlComponent component(&engine, testFileUrl("get.qml"));
    QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(component.create());

    QVERIFY(model != 0);

    QVERIFY(QMetaObject::invokeMethod(model, "runPreTest"));
    QCOMPARE(model->property("preTest").toBool(), true);

    QTRY_COMPARE(model->rowCount(), 9);

    QVERIFY(QMetaObject::invokeMethod(model, "runPostTest"));
    QCOMPARE(model->property("postTest").toBool(), true);

    delete model;
}

void tst_qquickxmllistmodel::reload()
{
    // If no keys are used, the model should be rebuilt from scratch when
    // reload() is called.

    QQmlComponent component(&engine, testFileUrl("model.qml"));
    QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(component.create());
    QVERIFY(model != 0);
    QTRY_COMPARE(model->rowCount(), 9);

    QSignalSpy spyInsert(model, SIGNAL(rowsInserted(QModelIndex,int,int)));
    QSignalSpy spyRemove(model, SIGNAL(rowsRemoved(QModelIndex,int,int)));
    QSignalSpy spyCount(model, SIGNAL(countChanged()));
    //reload multiple times to test the xml query aborting
    QMetaObject::invokeMethod(model, "reload");
    QMetaObject::invokeMethod(model, "reload");
    QCoreApplication::processEvents();
    QMetaObject::invokeMethod(model, "reload");
    QMetaObject::invokeMethod(model, "reload");
    QTRY_COMPARE(spyCount.count(), 0);
    QTRY_COMPARE(spyInsert.count(), 1);
    QTRY_COMPARE(spyRemove.count(), 1);

    QCOMPARE(spyInsert[0][1].toInt(), 0);
    QCOMPARE(spyInsert[0][2].toInt(), 8);

    QCOMPARE(spyRemove[0][1].toInt(), 0);
    QCOMPARE(spyRemove[0][2].toInt(), 8);

    delete model;
}

void tst_qquickxmllistmodel::useKeys()
{
    // If using incremental updates through keys, the model should only
    // insert & remove some of the items, instead of throwing everything
    // away and causing the view to repaint the whole view.

    QFETCH(QString, oldXml);
    QFETCH(int, oldCount);
    QFETCH(QString, newXml);
    QFETCH(QQmlXmlModelData, newData);
    QFETCH(QList<QQuickXmlListRange>, insertRanges);
    QFETCH(QList<QQuickXmlListRange>, removeRanges);

    QQmlComponent component(&engine, testFileUrl("roleKeys.qml"));
    QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(component.create());
    QVERIFY(model != 0);

    model->setProperty("xml",oldXml);
    QTRY_COMPARE(model->rowCount(), oldCount);

    QSignalSpy spyInsert(model, SIGNAL(rowsInserted(QModelIndex,int,int)));
    QSignalSpy spyRemove(model, SIGNAL(rowsRemoved(QModelIndex,int,int)));
    QSignalSpy spyCount(model, SIGNAL(countChanged()));

    model->setProperty("xml",newXml);

    if (oldCount != newData.count()) {
        QTRY_COMPARE(model->rowCount(), newData.count());
        QCOMPARE(spyCount.count(), 1);
    } else {
        QTRY_VERIFY(spyInsert.count() > 0 || spyRemove.count() > 0);
        QCOMPARE(spyCount.count(), 0);
    }

    QList<int> roles = model->roleNames().keys();
    std::sort(roles.begin(), roles.end());
    for (int i=0; i<model->rowCount(); i++) {
        QModelIndex index = model->index(i, 0);
        for (int j=0; j<roles.count(); j++)
            QCOMPARE(model->data(index, roles.at(j)), newData[i][j]);
    }

    QCOMPARE(spyInsert.count(), insertRanges.count());
    for (int i=0; i<spyInsert.count(); i++) {
        QCOMPARE(spyInsert[i][1].toInt(), insertRanges[i].first);
        QCOMPARE(spyInsert[i][2].toInt(), insertRanges[i].first + insertRanges[i].second - 1);
    }

    QCOMPARE(spyRemove.count(), removeRanges.count());
    for (int i=0; i<spyRemove.count(); i++) {
        QCOMPARE(spyRemove[i][1].toInt(), removeRanges[i].first);
        QCOMPARE(spyRemove[i][2].toInt(), removeRanges[i].first + removeRanges[i].second - 1);
    }

    delete model;
}

void tst_qquickxmllistmodel::useKeys_data()
{
    QTest::addColumn<QString>("oldXml");
    QTest::addColumn<int>("oldCount");
    QTest::addColumn<QString>("newXml");
    QTest::addColumn<QQmlXmlModelData>("newData");
    QTest::addColumn<QList<QQuickXmlListRange> >("insertRanges");
    QTest::addColumn<QList<QQuickXmlListRange> >("removeRanges");

    QQmlXmlModelData modelData;

    QTest::newRow("append 1")
        << makeItemXmlAndData("name=A,age=25,sport=Football") << 1
        << makeItemXmlAndData("name=A,age=25,sport=Football;name=B,age=35,sport=Athletics", &modelData)
        << modelData
        << (QList<QQuickXmlListRange>() << qMakePair(1, 1))
        << QList<QQuickXmlListRange>();

    QTest::newRow("append multiple")
        << makeItemXmlAndData("name=A,age=25,sport=Football") << 1
        << makeItemXmlAndData("name=A,age=25,sport=Football;name=B,age=35,sport=Athletics;name=C,age=45,sport=Curling", &modelData)
        << modelData
        << (QList<QQuickXmlListRange>() << qMakePair(1, 2))
        << QList<QQuickXmlListRange>();

    QTest::newRow("insert in different spots")
        << makeItemXmlAndData("name=B,age=35,sport=Athletics") << 1
        << makeItemXmlAndData("name=A,age=25,sport=Football;name=B,age=35,sport=Athletics;name=C,age=45,sport=Curling;name=D,age=55,sport=Golf", &modelData)
        << modelData
        << (QList<QQuickXmlListRange>() << qMakePair(0, 1) << qMakePair(2,2))
        << QList<QQuickXmlListRange>();

    QTest::newRow("insert in middle")
        << makeItemXmlAndData("name=A,age=25,sport=Football;name=D,age=55,sport=Golf") << 2
        << makeItemXmlAndData("name=A,age=25,sport=Football;name=B,age=35,sport=Athletics;name=C,age=45,sport=Curling;name=D,age=55,sport=Golf", &modelData)
        << modelData
        << (QList<QQuickXmlListRange>() << qMakePair(1, 2))
        << QList<QQuickXmlListRange>();

    QTest::newRow("remove first")
        << makeItemXmlAndData("name=A,age=25,sport=Football;name=B,age=35,sport=Athletics") << 2
        << makeItemXmlAndData("name=B,age=35,sport=Athletics", &modelData)
        << modelData
        << QList<QQuickXmlListRange>()
        << (QList<QQuickXmlListRange>() << qMakePair(0, 1));

    QTest::newRow("remove last")
        << makeItemXmlAndData("name=A,age=25,sport=Football;name=B,age=35,sport=Athletics") << 2
        << makeItemXmlAndData("name=A,age=25,sport=Football", &modelData)
        << modelData
        << QList<QQuickXmlListRange>()
        << (QList<QQuickXmlListRange>() << qMakePair(1, 1));

    QTest::newRow("remove from multiple spots")
        << makeItemXmlAndData("name=A,age=25,sport=Football;name=B,age=35,sport=Athletics;name=C,age=45,sport=Curling;name=D,age=55,sport=Golf;name=E,age=65,sport=Fencing") << 5
        << makeItemXmlAndData("name=A,age=25,sport=Football;name=C,age=45,sport=Curling", &modelData)
        << modelData
        << QList<QQuickXmlListRange>()
        << (QList<QQuickXmlListRange>() << qMakePair(1, 1) << qMakePair(3,2));

    QTest::newRow("remove all")
        << makeItemXmlAndData("name=A,age=25,sport=Football;name=B,age=35,sport=Athletics;name=C,age=45,sport=Curling") << 3
        << makeItemXmlAndData("", &modelData)
        << modelData
        << QList<QQuickXmlListRange>()
        << (QList<QQuickXmlListRange>() << qMakePair(0, 3));

    QTest::newRow("replace item")
        << makeItemXmlAndData("name=A,age=25,sport=Football") << 1
        << makeItemXmlAndData("name=ZZZ,age=25,sport=Football", &modelData)
        << modelData
        << (QList<QQuickXmlListRange>() << qMakePair(0, 1))
        << (QList<QQuickXmlListRange>() << qMakePair(0, 1));

    QTest::newRow("add and remove simultaneously, in different spots")
        << makeItemXmlAndData("name=A,age=25,sport=Football;name=B,age=35,sport=Athletics;name=C,age=45,sport=Curling;name=D,age=55,sport=Golf") << 4
        << makeItemXmlAndData("name=B,age=35,sport=Athletics;name=E,age=65,sport=Fencing", &modelData)
        << modelData
        << (QList<QQuickXmlListRange>() << qMakePair(1, 1))
        << (QList<QQuickXmlListRange>() << qMakePair(0, 1) << qMakePair(2,2));

    QTest::newRow("insert at start, remove at end i.e. rss feed")
        << makeItemXmlAndData("name=C,age=45,sport=Curling;name=D,age=55,sport=Golf;name=E,age=65,sport=Fencing") << 3
        << makeItemXmlAndData("name=A,age=25,sport=Football;name=B,age=35,sport=Athletics;name=C,age=45,sport=Curling", &modelData)
        << modelData
        << (QList<QQuickXmlListRange>() << qMakePair(0, 2))
        << (QList<QQuickXmlListRange>() << qMakePair(1, 2));

    QTest::newRow("remove at start, insert at end")
        << makeItemXmlAndData("name=A,age=25,sport=Football;name=B,age=35,sport=Athletics;name=C,age=45,sport=Curling") << 3
        << makeItemXmlAndData("name=C,age=45,sport=Curling;name=D,age=55,sport=Golf;name=E,age=65,sport=Fencing", &modelData)
        << modelData
        << (QList<QQuickXmlListRange>() << qMakePair(1, 2))
        << (QList<QQuickXmlListRange>() << qMakePair(0, 2));

    QTest::newRow("all data has changed")
        << makeItemXmlAndData("name=A,age=25,sport=Football;name=B,age=35") << 2
        << makeItemXmlAndData("name=C,age=45,sport=Curling;name=D,age=55,sport=Golf", &modelData)
        << modelData
        << (QList<QQuickXmlListRange>() << qMakePair(0, 2))
        << (QList<QQuickXmlListRange>() << qMakePair(0, 2));
}

void tst_qquickxmllistmodel::noKeysValueChanges()
{
    // The 'key' roles are 'name' and 'age', as defined in roleKeys.qml.
    // If a 'sport' value is changed, the model should not be reloaded,
    // since 'sport' is not marked as a key.

    QQmlComponent component(&engine, testFileUrl("roleKeys.qml"));
    QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(component.create());
    QVERIFY(model != 0);

    QString xml;

    xml = makeItemXmlAndData("name=A,age=25,sport=Football;name=B,age=35,sport=Athletics");
    model->setProperty("xml",xml);
    QTRY_COMPARE(model->rowCount(), 2);

    model->setProperty("xml","");

    QSignalSpy spyInsert(model, SIGNAL(rowsInserted(QModelIndex,int,int)));
    QSignalSpy spyRemove(model, SIGNAL(rowsRemoved(QModelIndex,int,int)));
    QSignalSpy spyCount(model, SIGNAL(countChanged()));

    xml = makeItemXmlAndData("name=A,age=25,sport=AussieRules;name=B,age=35,sport=Athletics");
    model->setProperty("xml",xml);

    QList<int> roles = model->roleNames().keys();
    std::sort(roles.begin(), roles.end());
    // wait for the new xml data to be set, and verify no signals were emitted
    QTRY_VERIFY(model->data(model->index(0, 0), roles.at(2)).toString() != QLatin1String("Football"));
    QCOMPARE(model->data(model->index(0, 0), roles.at(2)).toString(), QLatin1String("AussieRules"));

    QCOMPARE(spyInsert.count(), 0);
    QCOMPARE(spyRemove.count(), 0);
    QCOMPARE(spyCount.count(), 0);

    QCOMPARE(model->rowCount(), 2);

    delete model;
}

void tst_qquickxmllistmodel::keysChanged()
{
    // If the key roles change, the next time the data is reloaded, it should
    // delete all its data and build a clean model (i.e. same behavior as
    // if no keys are set).

    QQmlComponent component(&engine, testFileUrl("roleKeys.qml"));
    QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(component.create());
    QVERIFY(model != 0);

    QString xml = makeItemXmlAndData("name=A,age=25,sport=Football;name=B,age=35,sport=Athletics");
    model->setProperty("xml",xml);
    QTRY_COMPARE(model->rowCount(), 2);

    model->setProperty("xml","");

    QSignalSpy spyInsert(model, SIGNAL(rowsInserted(QModelIndex,int,int)));
    QSignalSpy spyRemove(model, SIGNAL(rowsRemoved(QModelIndex,int,int)));
    QSignalSpy spyCount(model, SIGNAL(countChanged()));

    QVERIFY(QMetaObject::invokeMethod(model, "disableNameKey"));
    model->setProperty("xml",xml);

    QTRY_VERIFY(spyInsert.count() > 0 && spyRemove.count() > 0);

    QCOMPARE(spyInsert.count(), 1);
    QCOMPARE(spyInsert[0][1].toInt(), 0);
    QCOMPARE(spyInsert[0][2].toInt(), 1);

    QCOMPARE(spyRemove.count(), 1);
    QCOMPARE(spyRemove[0][1].toInt(), 0);
    QCOMPARE(spyRemove[0][2].toInt(), 1);

    QCOMPARE(spyCount.count(), 0);

    delete model;
}

void tst_qquickxmllistmodel::threading()
{
    QFETCH(int, xmlDataCount);

    QQmlComponent component(&engine, testFileUrl("roleKeys.qml"));

    QAbstractItemModel *m1 = qobject_cast<QAbstractItemModel *>(component.create());
    QVERIFY(m1 != 0);
    QAbstractItemModel *m2 = qobject_cast<QAbstractItemModel *>(component.create());
    QVERIFY(m2 != 0);
    QAbstractItemModel *m3 = qobject_cast<QAbstractItemModel *>(component.create());
    QVERIFY(m3 != 0);

    for (int dataCount=0; dataCount<xmlDataCount; dataCount++) {

        QString data1, data2, data3;
        for (int i=0; i<dataCount; i++) {
            data1 += "name=A" + QString::number(i) + ",age=1" + QString::number(i) + ",sport=Football;";
            data2 += "name=B" + QString::number(i) + ",age=2" + QString::number(i) + ",sport=Athletics;";
            data3 += "name=C" + QString::number(i) + ",age=3" + QString::number(i) + ",sport=Curling;";
        }

        //Set the xml data multiple times with randomized order and mixed with multiple event loops
        //to test the xml query reloading/aborting, the result should be stable.
        m1->setProperty("xml",makeItemXmlAndData(data1));
        m2->setProperty("xml",makeItemXmlAndData(data2));
        m3->setProperty("xml",makeItemXmlAndData(data3));
        QCoreApplication::processEvents();
        m2->setProperty("xml",makeItemXmlAndData(data2));
        m1->setProperty("xml",makeItemXmlAndData(data1));
        m2->setProperty("xml",makeItemXmlAndData(data2));
        QCoreApplication::processEvents();
        m3->setProperty("xml",makeItemXmlAndData(data3));
        QCoreApplication::processEvents();
        m2->setProperty("xml",makeItemXmlAndData(data2));
        m1->setProperty("xml",makeItemXmlAndData(data1));
        m2->setProperty("xml",makeItemXmlAndData(data2));
        m3->setProperty("xml",makeItemXmlAndData(data3));
        QCoreApplication::processEvents();
        m2->setProperty("xml",makeItemXmlAndData(data2));
        m3->setProperty("xml",makeItemXmlAndData(data3));
        m3->setProperty("xml",makeItemXmlAndData(data3));
        QCoreApplication::processEvents();

        QTRY_VERIFY(m1->rowCount() == dataCount && m2->rowCount() == dataCount && m3->rowCount() == dataCount);

        for (int i=0; i<dataCount; i++) {
            QModelIndex index = m1->index(i, 0);
            QList<int> roles = m1->roleNames().keys();
            std::sort(roles.begin(), roles.end());
            QCOMPARE(m1->data(index, roles.at(0)).toString(), QLatin1Char('A') + QString::number(i));
            QCOMPARE(m1->data(index, roles.at(1)).toString(), QLatin1Char('1') + QString::number(i));
            QCOMPARE(m1->data(index, roles.at(2)).toString(), QString("Football"));

            index = m2->index(i, 0);
            roles = m2->roleNames().keys();
            std::sort(roles.begin(), roles.end());
            QCOMPARE(m2->data(index, roles.at(0)).toString(), QLatin1Char('B') + QString::number(i));
            QCOMPARE(m2->data(index, roles.at(1)).toString(), QLatin1Char('2') + QString::number(i));
            QCOMPARE(m2->data(index, roles.at(2)).toString(), QString("Athletics"));

            index = m3->index(i, 0);
            roles = m3->roleNames().keys();
            std::sort(roles.begin(), roles.end());
            QCOMPARE(m3->data(index, roles.at(0)).toString(), QLatin1Char('C') + QString::number(i));
            QCOMPARE(m3->data(index, roles.at(1)).toString(), QLatin1Char('3') + QString::number(i));
            QCOMPARE(m3->data(index, roles.at(2)).toString(), QString("Curling"));
        }
    }

    delete m1;
    delete m2;
    delete m3;
}

void tst_qquickxmllistmodel::threading_data()
{
    QTest::addColumn<int>("xmlDataCount");

    QTest::newRow("1") << 1;
    QTest::newRow("2") << 2;
    QTest::newRow("10") << 10;
}

void tst_qquickxmllistmodel::propertyChanges()
{
    QQmlComponent component(&engine, testFileUrl("propertychanges.qml"));
    QAbstractItemModel *model = qobject_cast<QAbstractItemModel*>(component.create());
    QVERIFY(model != 0);
    QTRY_COMPARE(model->rowCount(), 9);

    QObject *role = model->findChild<QObject*>("role");
    QVERIFY(role);

    QSignalSpy nameSpy(role, SIGNAL(nameChanged()));
    QSignalSpy querySpy(role, SIGNAL(queryChanged()));
    QSignalSpy isKeySpy(role, SIGNAL(isKeyChanged()));

    role->setProperty("name","size");
    role->setProperty("query","size/string()");
    role->setProperty("isKey",true);

    QCOMPARE(role->property("name").toString(), QString("size"));
    QCOMPARE(role->property("query").toString(), QString("size/string()"));
    QVERIFY(role->property("isKey").toBool());

    QCOMPARE(nameSpy.count(),1);
    QCOMPARE(querySpy.count(),1);
    QCOMPARE(isKeySpy.count(),1);

    role->setProperty("name","size");
    role->setProperty("query","size/string()");
    role->setProperty("isKey",true);

    QCOMPARE(nameSpy.count(),1);
    QCOMPARE(querySpy.count(),1);
    QCOMPARE(isKeySpy.count(),1);

    QSignalSpy sourceSpy(model, SIGNAL(sourceChanged()));
    QSignalSpy xmlSpy(model, SIGNAL(xmlChanged()));
    QSignalSpy modelQuerySpy(model, SIGNAL(queryChanged()));
    QSignalSpy namespaceDeclarationsSpy(model, SIGNAL(namespaceDeclarationsChanged()));

    model->setProperty("source",QUrl(""));
    model->setProperty("xml","<Pets><Pet><name>Polly</name><type>Parrot</type><age>12</age><size>Small</size></Pet></Pets>");
    model->setProperty("query","/Pets");
    model->setProperty("namespaceDeclarations","declare namespace media=\"http://search.yahoo.com/mrss/\";");

    QCOMPARE(model->property("source").toUrl(), QUrl(""));
    QCOMPARE(model->property("xml").toString(), QString("<Pets><Pet><name>Polly</name><type>Parrot</type><age>12</age><size>Small</size></Pet></Pets>"));
    QCOMPARE(model->property("query").toString(), QString("/Pets"));
    QCOMPARE(model->property("namespaceDeclarations").toString(), QString("declare namespace media=\"http://search.yahoo.com/mrss/\";"));

    QTRY_COMPARE(model->rowCount(), 1);

    QCOMPARE(sourceSpy.count(),1);
    QCOMPARE(xmlSpy.count(),1);
    QCOMPARE(modelQuerySpy.count(),1);
    QCOMPARE(namespaceDeclarationsSpy.count(),1);

    model->setProperty("source",QUrl(""));
    model->setProperty("xml","<Pets><Pet><name>Polly</name><type>Parrot</type><age>12</age><size>Small</size></Pet></Pets>");
    model->setProperty("query","/Pets");
    model->setProperty("namespaceDeclarations","declare namespace media=\"http://search.yahoo.com/mrss/\";");

    QCOMPARE(sourceSpy.count(),1);
    QCOMPARE(xmlSpy.count(),1);
    QCOMPARE(modelQuerySpy.count(),1);
    QCOMPARE(namespaceDeclarationsSpy.count(),1);

    QTRY_COMPARE(model->rowCount(), 1);
    delete model;
}

void tst_qquickxmllistmodel::selectAncestor()
{
    QQmlComponent component(&engine, testFileUrl("groups.qml"));
    QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(component.create());
    QVERIFY(model != 0);
    QTRY_COMPARE(model->rowCount(), 1);

    QModelIndex index = model->index(0, 0);
    QCOMPARE(model->data(index, Qt::UserRole).toInt(), 12);
    QCOMPARE(model->data(index, Qt::UserRole+1).toString(), QLatin1String("cats"));
}

void tst_qquickxmllistmodel::roleCrash()
{
    // don't crash
    QQmlComponent component(&engine, testFileUrl("roleCrash.qml"));
    QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(component.create());
    QVERIFY(model != 0);
    delete model;
}

class SortFilterProxyModel : public QSortFilterProxyModel
{
    Q_OBJECT
    Q_PROPERTY(QObject *source READ source WRITE setSource)

public:
    SortFilterProxyModel(QObject *parent = 0) : QSortFilterProxyModel(parent) { sort(0); }
    QObject *source() const { return sourceModel(); }
    void setSource(QObject *source) { setSourceModel(qobject_cast<QAbstractItemModel *>(source)); }
};

void tst_qquickxmllistmodel::proxyCrash()
{
    qmlRegisterType<SortFilterProxyModel>("SortFilterProxyModel", 1, 0, "SortFilterProxyModel");

    // don't crash
    QQmlComponent component(&engine, testFileUrl("proxyCrash.qml"));
    QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(component.create());
    QVERIFY(model != 0);
    delete model;
}

QTEST_MAIN(tst_qquickxmllistmodel)

#include "tst_qquickxmllistmodel.moc"
