// Copyright (C) 2016 Research In Motion.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

#include "delegatemodelkinds.h"
#include "stringmodel.h"

#include <private/qmlutils_p.h>
#include <private/qqmlinstantiator_p.h>

#include <QtTest/qsignalspy.h>
#include <QtTest/qtest.h>

#include <QtQml/qqmlcomponent.h>
#include <QtQml/qqmlcontext.h>
#include <QtQml/qqmlengine.h>
#include <QtQml/qqmlincubator.h>

#include <QtCore/qdebug.h>
#include <QtCore/qsortfilterproxymodel.h>

class tst_qqmlinstantiator: public QQmlDataTest
{
    Q_OBJECT

public:
    tst_qqmlinstantiator();

private slots:
    void createNone();
    void createSingle();
    void createMultiple();
    void stringModel();
    void activeProperty();
    void activeModelChangeInteraction();
    void intModelChange();
    void createAndRemove();
    void removeDuringModelChange();

    void asynchronous_data();
    void asynchronous();

    void handlerWithParent();
    void boundDelegateComponent();

    void listDataDestruction();

    void delegateModelAccess_data();
    void delegateModelAccess();
};

tst_qqmlinstantiator::tst_qqmlinstantiator()
    : QQmlDataTest(QT_QMLTEST_DATADIR)
{
    qmlRegisterNamespaceAndRevisions(&Model::staticMetaObject, "Test", 1);
    qmlRegisterNamespaceAndRevisions(&Delegate::staticMetaObject, "Test", 1);
}

void tst_qqmlinstantiator::createNone()
{
    QQmlEngine engine;
    QQmlComponent component(&engine, testFileUrl("createNone.qml"));
    QScopedPointer<QObject> o(component.create());
    QQmlInstantiator *instantiator = qobject_cast<QQmlInstantiator*>(o.data());
    QVERIFY(instantiator != nullptr);
    QCOMPARE(instantiator->isActive(), true);
    QCOMPARE(instantiator->count(), 0);
    QCOMPARE(instantiator->property("success").toBool(), true);
    QVERIFY(instantiator->delegate()->isReady());
}

void tst_qqmlinstantiator::createSingle()
{
    QQmlEngine engine;
    QQmlComponent component(&engine, testFileUrl("createSingle.qml"));
    QScopedPointer<QObject> o(component.create());
    QQmlInstantiator *instantiator = qobject_cast<QQmlInstantiator*>(o.data());
    QVERIFY(instantiator != nullptr);
    QCOMPARE(instantiator->isActive(), true);
    QCOMPARE(instantiator->count(), 1);
    QVERIFY(instantiator->delegate()->isReady());

    QObject *object = instantiator->object();
    QVERIFY(object);
    QCOMPARE(object->parent(), instantiator);
    QCOMPARE(object->property("success").toBool(), true);
    QCOMPARE(object->property("idx").toInt(), 0);
}

void tst_qqmlinstantiator::createMultiple()
{
    QQmlEngine engine;
    QQmlComponent component(&engine, testFileUrl("createMultiple.qml"));
    QScopedPointer<QObject> o(component.create());
    QQmlInstantiator *instantiator = qobject_cast<QQmlInstantiator*>(o.data());
    QVERIFY(instantiator != nullptr);
    QCOMPARE(instantiator->isActive(), true);
    QCOMPARE(instantiator->count(), 10);

    for (int i=0; i<10; i++) {
        QObject *object = instantiator->objectAt(i);
        QVERIFY(object);
        QCOMPARE(object->parent(), instantiator);
        QCOMPARE(object->property("success").toBool(), true);
        QCOMPARE(object->property("idx").toInt(), i);
    }
}

void tst_qqmlinstantiator::stringModel()
{
    QQmlEngine engine;
    QQmlComponent component(&engine, testFileUrl("stringModel.qml"));
    QScopedPointer<QObject> o(component.create());
    QQmlInstantiator *instantiator = qobject_cast<QQmlInstantiator*>(o.data());
    QVERIFY(instantiator != nullptr);
    QCOMPARE(instantiator->isActive(), true);
    QCOMPARE(instantiator->count(), 4);

    for (int i=0; i<4; i++) {
        QObject *object = instantiator->objectAt(i);
        QVERIFY(object);
        QCOMPARE(object->parent(), instantiator);
        QCOMPARE(object->property("success").toBool(), true);
    }
}

void tst_qqmlinstantiator::activeProperty()
{
    QQmlEngine engine;
    QQmlComponent component(&engine, testFileUrl("inactive.qml"));
    QScopedPointer<QObject> o(component.create());
    QQmlInstantiator *instantiator = qobject_cast<QQmlInstantiator*>(o.data());
    QVERIFY(instantiator != nullptr);
    QSignalSpy activeSpy(instantiator, SIGNAL(activeChanged()));
    QSignalSpy countSpy(instantiator, SIGNAL(countChanged()));
    QSignalSpy objectSpy(instantiator, SIGNAL(objectChanged()));
    QSignalSpy modelSpy(instantiator, SIGNAL(modelChanged()));
    QCOMPARE(instantiator->isActive(), false);
    QCOMPARE(instantiator->count(), 0);
    QVERIFY(instantiator->delegate()->isReady());

    QCOMPARE(activeSpy.size(), 0);
    QCOMPARE(countSpy.size(), 0);
    QCOMPARE(objectSpy.size(), 0);
    QCOMPARE(modelSpy.size(), 0);

    instantiator->setActive(true);
    QCOMPARE(instantiator->isActive(), true);
    QCOMPARE(instantiator->count(), 1);

    QCOMPARE(activeSpy.size(), 1);
    QCOMPARE(countSpy.size(), 1);
    QCOMPARE(objectSpy.size(), 1);
    QCOMPARE(modelSpy.size(), 0);

    QObject *object = instantiator->object();
    QVERIFY(object);
    QCOMPARE(object->parent(), instantiator);
    QCOMPARE(object->property("success").toBool(), true);
    QCOMPARE(object->property("idx").toInt(), 0);
}

void tst_qqmlinstantiator::activeModelChangeInteraction()
{
    QQmlEngine engine;
    QQmlComponent component(&engine, testFileUrl("activeModelChangeInteraction.qml"));
    QScopedPointer<QObject> root(component.create());
    QVERIFY(root);

    // If the instantiator is inactive, a model change does not lead to items being loaded
    bool ok = false;
    int count = root->property("instanceCount").toInt(&ok);
    QVERIFY(ok);
    QCOMPARE(count, 0);

    // When turning the instantiator active, it will however reflect the model
    root->setProperty("active", true);
    count = root->property("instanceCount").toInt(&ok);
    QVERIFY(ok);
    QCOMPARE(count, 3);
}

void tst_qqmlinstantiator::intModelChange()
{
    QQmlEngine engine;
    QQmlComponent component(&engine, testFileUrl("createMultiple.qml"));
    QScopedPointer<QObject> o(component.create());
    QQmlInstantiator *instantiator = qobject_cast<QQmlInstantiator*>(o.data());
    QVERIFY(instantiator != nullptr);
    QSignalSpy activeSpy(instantiator, SIGNAL(activeChanged()));
    QSignalSpy countSpy(instantiator, SIGNAL(countChanged()));
    QSignalSpy objectSpy(instantiator, SIGNAL(objectChanged()));
    QSignalSpy modelSpy(instantiator, SIGNAL(modelChanged()));
    QCOMPARE(instantiator->count(), 10);

    QCOMPARE(activeSpy.size(), 0);
    QCOMPARE(countSpy.size(), 0);
    QCOMPARE(objectSpy.size(), 0);
    QCOMPARE(modelSpy.size(), 0);

    instantiator->setModel(QVariant(2));
    QCOMPARE(instantiator->count(), 2);

    QCOMPARE(activeSpy.size(), 0);
    QCOMPARE(countSpy.size(), 1);
    QCOMPARE(objectSpy.size(), 2);
    QCOMPARE(modelSpy.size(), 1);

    for (int i=0; i<2; i++) {
        QObject *object = instantiator->objectAt(i);
        QVERIFY(object);
        QCOMPARE(object->parent(), instantiator);
        QCOMPARE(object->property("success").toBool(), true);
        QCOMPARE(object->property("idx").toInt(), i);
    }
}

void tst_qqmlinstantiator::createAndRemove()
{
    QQmlEngine engine;
    QScopedPointer<StringModel> model {new StringModel("model1")};
    qmlRegisterSingletonInstance("Test", 1, 0, "Model1", model.get());
    QQmlComponent component(&engine, testFileUrl("createAndRemove.qml"));
    QScopedPointer<QObject> rootObject(component.create());
    QVERIFY(rootObject != nullptr);

    QQmlInstantiator *instantiator =
        qobject_cast<QQmlInstantiator*>(rootObject->findChild<QObject*>("instantiator1"));
    QVERIFY(instantiator != nullptr);
    model->drop(1);
    QVector<QString> names;
    names << "Beta" << "Gamma" << "Delta";
    for (int i=0; i<3; i++) {
        QObject *object = instantiator->objectAt(i);
        QVERIFY(object);
        QCOMPARE(object->property("datum").toString(), names[i]);
    }
}

void tst_qqmlinstantiator::asynchronous_data()
{
    QTest::addColumn<bool>("asyncIncubator");
    QTest::addColumn<QString>("fileName");

    QTest::newRow("Asynchronous Instantiator") << false << "createMultipleAsync.qml";
    QTest::newRow("Nested-asynchronous Instantiator") << true << "createMultiple.qml";
}

void tst_qqmlinstantiator::asynchronous()
{
    QFETCH(bool, asyncIncubator);
    QFETCH(QString, fileName);

    QQmlEngine engine;
    QQmlIncubationController incubationController;
    engine.setIncubationController(&incubationController);
    QQmlComponent component(&engine, testFileUrl(fileName));
    QQmlIncubator incubator(asyncIncubator ? QQmlIncubator::Asynchronous : QQmlIncubator::Synchronous);
    component.create(incubator);
    while (!incubator.isReady())
        incubationController.incubateFor(10);
    QQmlInstantiator *instantiator = qobject_cast<QQmlInstantiator *>(incubator.object());
    while (incubationController.incubatingObjectCount() > 0)
        incubationController.incubateFor(10);
    QVERIFY(instantiator != nullptr);
    QCOMPARE(instantiator->isActive(), true);
    QCOMPARE(instantiator->count(), 10);

    for (int i=0; i<10; i++) {
        QObject *object = instantiator->objectAt(i);
        QVERIFY(object);
        QCOMPARE(object->parent(), instantiator);
        QCOMPARE(object->property("success").toBool(), true);
        QCOMPARE(object->property("idx").toInt(), i);
    }
}

void tst_qqmlinstantiator::handlerWithParent()
{
    QQmlEngine engine;
    QQmlComponent component(&engine, testFileUrl("handlerWithParent.qml"));
    QScopedPointer<QObject> rootObject(component.create());
    QVERIFY(rootObject != nullptr);
    const auto handlers = rootObject->findChildren<QObject *>("pointHandler");
    QCOMPARE(handlers.size(), 2);
    for (const auto *h : handlers) {
        QCOMPARE(h->parent(), rootObject.data());
    }
}

void tst_qqmlinstantiator::boundDelegateComponent()
{
    QQmlEngine engine;
    const QUrl url(testFileUrl("boundDelegateComponent.qml"));
    QQmlComponent component(&engine, url);
    QVERIFY2(component.isReady(), qPrintable(component.errorString()));

    for (int i = 0; i < 3; ++i) {
        QTest::ignoreMessage(
                QtWarningMsg,
                qPrintable(QLatin1String("%1:12: ReferenceError: modelData is not defined")
                                   .arg(url.toString())));
    }

    QScopedPointer<QObject> o(component.create());
    QVERIFY2(!o.isNull(), qPrintable(component.errorString()));

    QQmlInstantiator *a = qobject_cast<QQmlInstantiator *>(
            qmlContext(o.data())->objectForName(QStringLiteral("undefinedModelData")));
    QVERIFY(a);
    QCOMPARE(a->count(), 3);
    for (int i = 0; i < 3; ++i)
        QCOMPARE(a->objectAt(i)->objectName(), QStringLiteral("rootundefined"));

    QQmlInstantiator *b = qobject_cast<QQmlInstantiator *>(
            qmlContext(o.data())->objectForName(QStringLiteral("requiredModelData")));
    QVERIFY(b);
    QCOMPARE(b->count(), 3);
    QCOMPARE(b->objectAt(0)->objectName(), QStringLiteral("root1"));
    QCOMPARE(b->objectAt(1)->objectName(), QStringLiteral("root2"));
    QCOMPARE(b->objectAt(2)->objectName(), QStringLiteral("root3"));
}

void tst_qqmlinstantiator::listDataDestruction()
{
    QQmlEngine engine;
    QQmlComponent component(&engine, testFileUrl("listDataDestruction.qml"));
    QVERIFY2(component.isReady(), qPrintable(component.errorString()));
    QScopedPointer<QObject> o(component.create());
    QVERIFY(!o.isNull());
    QCOMPARE(o->objectName(), QLatin1String("A"));
}

class SingleBoolItemModel : public QAbstractListModel
{
    Q_OBJECT

public:
    SingleBoolItemModel(QObject *parent = nullptr) : QAbstractListModel(parent) {}

    int rowCount(const QModelIndex &parent = QModelIndex()) const override
    {
        if (parent.isValid())
            return 0;
        return 1;
    }

    QVariant data(const QModelIndex &index, int role) const override
    {
        if (index.parent().isValid() || index.row() != 0 || index.column() != 0
                || role != Qt::UserRole)
            return QVariant();

        return m_active;
    }

    bool setData(const QModelIndex &index, const QVariant &value,
                 int role) override {
        if (index.parent().isValid() || index.row() != 0 || index.column() != 0
                || role != Qt::UserRole || m_active == value.toBool())
            return false;

        m_active = value.toBool();
        Q_EMIT dataChanged(index, index, QList<int>{Qt::UserRole});
        return true;
    }

    QHash<int, QByteArray> roleNames() const override
    {
        return { {Qt::UserRole, "active"} };
    }

private:
    bool m_active = true;
};

class FilterBoolRoleProxyModel : public QSortFilterProxyModel
{
    Q_OBJECT

public:
    FilterBoolRoleProxyModel(QObject *parent = nullptr)
        : QSortFilterProxyModel(parent) {}

    bool filterAcceptsRow(int source_row,
                          const QModelIndex &source_parent) const override
    {
        return sourceModel()->index(source_row, 0, source_parent).data(Qt::UserRole).toBool();
    }
};

void tst_qqmlinstantiator::removeDuringModelChange()
{
    SingleBoolItemModel model;

    FilterBoolRoleProxyModel proxyModel;
    proxyModel.setSourceModel(&model);
    proxyModel.setFilterRole(Qt::UserRole);
    QCOMPARE(proxyModel.rowCount(), 1);

    QQmlEngine engine;
    const QUrl url(testFileUrl("removeDuringModelChange.qml"));
    QQmlComponent component(&engine, url);
    QVERIFY2(component.isReady(), qPrintable(component.errorString()));

    QScopedPointer<QObject> o(component.create());
    QVERIFY2(!o.isNull(), qPrintable(component.errorString()));

    QQmlInstantiator *instantiator = qobject_cast<QQmlInstantiator *>(o.data());

    instantiator->setModel(QVariant::fromValue(&proxyModel));

    QSignalSpy removedSpy(instantiator, &QQmlInstantiator::objectRemoved);
    QMetaObject::invokeMethod(instantiator->object(), "deactivate");

    // We should still be alive at this point.
    QCOMPARE(removedSpy.size(), 1);
    QCOMPARE(proxyModel.rowCount(), 0);
}

template<typename Enum>
const char *enumKey(Enum value) {
    const QMetaObject *mo = qt_getEnumMetaObject(value);
    const QMetaEnum metaEnum = mo->enumerator(mo->indexOfEnumerator(qt_getEnumName(value)));
    return metaEnum.valueToKey(value);
}


void tst_qqmlinstantiator::delegateModelAccess_data()
{
    QTest::addColumn<QQmlDelegateModel::DelegateModelAccess>("access");
    QTest::addColumn<Model::Kind>("modelKind");
    QTest::addColumn<Delegate::Kind>("delegateKind");
    using Access = QQmlDelegateModel::DelegateModelAccess;
    for (auto access : { Access::Qt5ReadWrite, Access::ReadOnly, Access::ReadWrite }) {
        for (auto model : { Model::Singular, Model::List, Model::Array, Model::Object }) {
            for (auto delegate : { Delegate::Untyped, Delegate::Typed }) {
                QTest::addRow("%s-%s-%s", enumKey(access), enumKey(model), enumKey(delegate))
                        << access << model << delegate;
            }
        }
    }
}

void tst_qqmlinstantiator::delegateModelAccess()
{
    QFETCH(QQmlDelegateModel::DelegateModelAccess, access);
    QFETCH(Model::Kind, modelKind);
    QFETCH(Delegate::Kind, delegateKind);

    QQmlEngine engine;
    const QUrl url = testFileUrl("delegateModelAccess.qml");
    QQmlComponent c(&engine, url);
    QVERIFY2(c.isReady(), qPrintable(c.errorString()));
    QScopedPointer<QObject> object(c.create());

    QQmlInstantiator *instantiator = qobject_cast<QQmlInstantiator *>(object.data());
    QVERIFY(instantiator);

    QSignalSpy modelChangedSpy(instantiator, &QQmlInstantiator::modelChanged);

    if (delegateKind == Delegate::Untyped && modelKind == Model::Array)
        QSKIP("Properties of objects in arrays are not exposed as context properties");

    if (access == QQmlDelegateModel::ReadOnly) {
        const QRegularExpression message(
                url.toString() + ":[0-9]+: TypeError: Cannot assign to read-only property \"a\"");

        QTest::ignoreMessage(QtWarningMsg, message);
        if (delegateKind == Delegate::Untyped)
            QTest::ignoreMessage(QtWarningMsg, message);
    }
    object->setProperty("delegateModelAccess", access);
    object->setProperty("modelIndex", modelKind);
    object->setProperty("delegateIndex", delegateKind);

    QObject *delegate = instantiator->objectAt(0);
    QVERIFY(delegate);

    const bool modelWritable = access != QQmlDelegateModel::ReadOnly;
    const bool immediateWritable = (delegateKind == Delegate::Untyped)
            ? access != QQmlDelegateModel::ReadOnly
            : access == QQmlDelegateModel::ReadWrite;

    // Only the array is actually updated itself. The other models are pointers
    const bool writeShouldSignal = modelKind == Model::Kind::Array;

    double expected = 11;

    // Initial setting of the model, signals one update
    int expectedModelUpdates = 1;
    QCOMPARE(modelChangedSpy.count(), expectedModelUpdates);

    QCOMPARE(delegate->property("immediateX").toDouble(), expected);
    QCOMPARE(delegate->property("modelX").toDouble(), expected);

    if (modelWritable) {
        expected = 3;
        if (writeShouldSignal)
            ++expectedModelUpdates;
    }

    QMetaObject::invokeMethod(delegate, "writeThroughModel");
    QCOMPARE(delegate->property("immediateX").toDouble(), expected);
    QCOMPARE(delegate->property("modelX").toDouble(), expected);
    QCOMPARE(modelChangedSpy.count(), expectedModelUpdates);

    if (immediateWritable) {
        expected = 1;
        if (writeShouldSignal)
            ++expectedModelUpdates;
    }

    QMetaObject::invokeMethod(delegate, "writeImmediate");

    // Writes to required properties always succeed, but might not be propagated to the model
    QCOMPARE(delegate->property("immediateX").toDouble(),
             delegateKind == Delegate::Untyped ? expected : 1);

    QCOMPARE(delegate->property("modelX").toDouble(), expected);
    QCOMPARE(modelChangedSpy.count(), expectedModelUpdates);
}

QTEST_MAIN(tst_qqmlinstantiator)

#include "tst_qqmlinstantiator.moc"
