// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

#include <qtest.h>
#include <private/qqmlpropertycache_p.h>
#include <QtQml/qqmlengine.h>
#include <QtQml/qqmlcontext.h>
#include <QtQml/qqmlcomponent.h>
#include <private/qmetaobjectbuilder_p.h>
#include <private/qqmlcontextdata_p.h>
#include <private/qqmldata_p.h>
#include <private/qqmlpropertycachecreator_p.h>
#include <QCryptographicHash>
#include <QtQuickTestUtils/private/qmlutils_p.h>

class tst_qqmlpropertycache : public QQmlDataTest
{
    Q_OBJECT
public:
    tst_qqmlpropertycache() : QQmlDataTest(QT_QMLTEST_DATADIR) {}

private slots:
    void properties();
    void propertiesDerived();
    void revisionedProperties();
    void methods();
    void methodsDerived();
    void signalHandlers();
    void signalHandlersDerived();
    void passForeignEnums();
    void passQGadget();
    void metaObjectSize_data();
    void metaObjectSize();
    void metaObjectChecksum();
    void metaObjectsForRootElements();
    void derivedGadgetMethod();
    void restrictRegistrationVersion();
    void rejectOverriddenFinal();
    void overriddenSignals();
    void duplicateIdsAndGeneralizedGroupProperties();

    void appendPropertyAttr_logging_data();
    void appendPropertyAttr_logging();

    void appendPropertyAttr_InvalidOverride_data();
    void appendPropertyAttr_InvalidOverride();

    void appendPropertyAttr_ValidOverride_data();
    void appendPropertyAttr_ValidOverride();

    void handleOverride_data();
    void handleOverride();

    void nonExistentGeneralizedGroup();

    void append_propertyAttr_data();
    void append_propertyAttr();
    void isComposite();
    void rebase();

private:
    QQmlEngine engine;
};

class BaseGadget
{
    Q_GADGET
    QML_ANONYMOUS
public:
    Q_INVOKABLE QString stringValue() { return QLatin1String("base"); }
};

class DerivedGadget : public BaseGadget
{
    Q_GADGET
    QML_ANONYMOUS
public:
    Q_INVOKABLE QString stringValue() { return QLatin1String("derived"); }
};

class GadgetUser : public QObject
{
    Q_OBJECT
    Q_PROPERTY(BaseGadget base READ base CONSTANT)
    Q_PROPERTY(DerivedGadget derived READ derived CONSTANT)
    Q_PROPERTY(QString baseString READ baseString WRITE setBaseString NOTIFY baseStringChanged)
    Q_PROPERTY(QString derivedString READ derivedString WRITE setDerivedString NOTIFY derivedStringChanged)
    QML_ELEMENT

public:
    BaseGadget base() const { return m_base; }
    DerivedGadget derived() const { return m_derived; }
    QString baseString() const { return m_baseString; }
    QString derivedString() const { return m_derivedString; }

public slots:
    void setBaseString(QString baseString)
    {
        if (m_baseString == baseString)
            return;

        m_baseString = baseString;
        emit baseStringChanged(m_baseString);
    }

    void setDerivedString(QString derivedString)
    {
        if (m_derivedString == derivedString)
            return;

        m_derivedString = derivedString;
        emit derivedStringChanged(m_derivedString);
    }

signals:
    void baseStringChanged(QString baseString);
    void derivedStringChanged(QString derivedString);

private:
    BaseGadget m_base;
    DerivedGadget m_derived;
    QString m_baseString;
    QString m_derivedString;
};

class BaseObject : public QObject
{
    Q_OBJECT
    QML_ELEMENT
    Q_PROPERTY(int propertyA READ propertyA NOTIFY propertyAChanged)
    Q_PROPERTY(QString propertyB READ propertyB NOTIFY propertyBChanged)
    Q_PROPERTY(int highVersion READ highVersion WRITE setHighVersion NOTIFY highVersionChanged REVISION(4, 0))
    Q_PROPERTY(int finalProp READ finalProp CONSTANT FINAL)

public:
    BaseObject(QObject *parent = nullptr) : QObject(parent) {}

    int propertyA() const { return 0; }
    QString propertyB() const { return QString(); }
    int highVersion() const { return m_highVersion; }
    int finalProp() const { return 8; }

public Q_SLOTS:
    void slotA() {}

    void setHighVersion(int highVersion)
    {
        if (m_highVersion == highVersion)
            return;

        m_highVersion = highVersion;
        emit highVersionChanged();
    }

Q_SIGNALS:
    void propertyAChanged();
    void propertyBChanged();
    void signalA();
    void highVersionChanged();

private:
    int m_highVersion = 0;
};

class DerivedObject : public BaseObject
{
    Q_OBJECT
    Q_PROPERTY(int propertyC READ propertyC NOTIFY propertyCChanged)
    Q_PROPERTY(QString propertyD READ propertyD NOTIFY propertyDChanged)
    Q_PROPERTY(int propertyE READ propertyE NOTIFY propertyEChanged REVISION 1)
    Q_PROPERTY(int finalProp READ finalProp CONSTANT) // bad!
public:
    DerivedObject(QObject *parent = nullptr) : BaseObject(parent) {}

    int propertyC() const { return 0; }
    QString propertyD() const { return QString(); }
    int propertyE() const { return 0; }
    int finalProp() const { return 9; }

public Q_SLOTS:
    void slotB() {}

Q_SIGNALS:
    void propertyCChanged();
    void propertyDChanged();
    Q_REVISION(1) void propertyEChanged();
    void signalB();
};

class OverriddenSignal : public BaseObject
{
    Q_OBJECT
public:
    OverriddenSignal(QObject *parent = nullptr) : BaseObject(parent) {}

    Q_INVOKABLE void propertyAChanged() {  ++propertyAChangedCalled; }
    int propertyAChangedCalled = 0;

Q_SIGNALS:
    void signalA();
};

const QQmlPropertyData *cacheProperty(const QQmlPropertyCache::ConstPtr &cache, const char *name)
{
    return cache->property(QLatin1String(name), nullptr, nullptr);
}

void tst_qqmlpropertycache::properties()
{
    QQmlEngine engine;
    DerivedObject object;
    const QMetaObject *metaObject = object.metaObject();

    QQmlPropertyCache::ConstPtr cache = QQmlPropertyCache::createStandalone(metaObject);
    const QQmlPropertyData *data;

    QVERIFY((data = cacheProperty(cache, "propertyA")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfProperty("propertyA"));

    QVERIFY((data = cacheProperty(cache, "propertyB")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfProperty("propertyB"));

    QVERIFY((data = cacheProperty(cache, "propertyC")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfProperty("propertyC"));

    QVERIFY((data = cacheProperty(cache, "propertyD")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfProperty("propertyD"));
}

void tst_qqmlpropertycache::propertiesDerived()
{
    QQmlEngine engine;
    DerivedObject object;
    const QMetaObject *metaObject = object.metaObject();

    QQmlPropertyCache::ConstPtr parentCache
            = QQmlPropertyCache::createStandalone(&BaseObject::staticMetaObject);
    QQmlPropertyCache::ConstPtr cache
            = parentCache->copyAndAppend(object.metaObject(), QTypeRevision());
    const QQmlPropertyData *data;

    QVERIFY((data = cacheProperty(cache, "propertyA")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfProperty("propertyA"));

    QVERIFY((data = cacheProperty(cache, "propertyB")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfProperty("propertyB"));

    QVERIFY((data = cacheProperty(cache, "propertyC")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfProperty("propertyC"));

    QVERIFY((data = cacheProperty(cache, "propertyD")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfProperty("propertyD"));
}

void tst_qqmlpropertycache::revisionedProperties()
{
    // Check that if you create a QQmlPropertyCache from a QMetaObject together
    // with an explicit revision, the cache will then, and only then, report a
    // property with a matching revision as available.
    DerivedObject object;
    const QMetaObject *metaObject = object.metaObject();

    QQmlPropertyCache::ConstPtr cacheWithoutVersion(
                QQmlPropertyCache::createStandalone(metaObject));
    QQmlPropertyCache::ConstPtr cacheWithVersion(
                QQmlPropertyCache::createStandalone(
                    metaObject, QTypeRevision::fromMinorVersion(1)));
    const QQmlPropertyData *data;

    QVERIFY((data = cacheProperty(cacheWithoutVersion, "propertyE")));
    QCOMPARE(cacheWithoutVersion->isAllowedInRevision(data), false);
    QCOMPARE(cacheWithVersion->isAllowedInRevision(data), true);
}

void tst_qqmlpropertycache::methods()
{
    QQmlEngine engine;
    DerivedObject object;
    const QMetaObject *metaObject = object.metaObject();

    QQmlPropertyCache::ConstPtr cache(QQmlPropertyCache::createStandalone(metaObject));
    const QQmlPropertyData *data;

    QVERIFY((data = cacheProperty(cache, "slotA")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfMethod("slotA()"));

    QVERIFY((data = cacheProperty(cache, "slotB")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfMethod("slotB()"));

    QVERIFY((data = cacheProperty(cache, "signalA")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfMethod("signalA()"));

    QVERIFY((data = cacheProperty(cache, "signalB")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfMethod("signalB()"));

    QVERIFY((data = cacheProperty(cache, "propertyAChanged")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfMethod("propertyAChanged()"));

    QVERIFY((data = cacheProperty(cache, "propertyBChanged")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfMethod("propertyBChanged()"));

    QVERIFY((data = cacheProperty(cache, "propertyCChanged")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfMethod("propertyCChanged()"));

    QVERIFY((data = cacheProperty(cache, "propertyDChanged")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfMethod("propertyDChanged()"));
}

void tst_qqmlpropertycache::methodsDerived()
{
    QQmlEngine engine;
    DerivedObject object;
    const QMetaObject *metaObject = object.metaObject();

    QQmlPropertyCache::ConstPtr parentCache(
                QQmlPropertyCache::createStandalone(&BaseObject::staticMetaObject));
    QQmlPropertyCache::ConstPtr cache
            = parentCache->copyAndAppend(object.metaObject(), QTypeRevision {});
    const QQmlPropertyData *data;

    QVERIFY((data = cacheProperty(cache, "slotA")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfMethod("slotA()"));

    QVERIFY((data = cacheProperty(cache, "slotB")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfMethod("slotB()"));

    QVERIFY((data = cacheProperty(cache, "signalA")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfMethod("signalA()"));

    QVERIFY((data = cacheProperty(cache, "signalB")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfMethod("signalB()"));

    QVERIFY((data = cacheProperty(cache, "propertyAChanged")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfMethod("propertyAChanged()"));

    QVERIFY((data = cacheProperty(cache, "propertyBChanged")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfMethod("propertyBChanged()"));

    QVERIFY((data = cacheProperty(cache, "propertyCChanged")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfMethod("propertyCChanged()"));

    QVERIFY((data = cacheProperty(cache, "propertyDChanged")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfMethod("propertyDChanged()"));
}

void tst_qqmlpropertycache::signalHandlers()
{
    QQmlEngine engine;
    DerivedObject object;
    const QMetaObject *metaObject = object.metaObject();

    QQmlPropertyCache::ConstPtr cache(QQmlPropertyCache::createStandalone(metaObject));
    const QQmlPropertyData *data;

    QVERIFY((data = cacheProperty(cache, "onSignalA")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfMethod("signalA()"));

    QVERIFY((data = cacheProperty(cache, "onSignalB")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfMethod("signalB()"));

    QVERIFY((data = cacheProperty(cache, "onPropertyAChanged")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfMethod("propertyAChanged()"));

    QVERIFY((data = cacheProperty(cache, "onPropertyBChanged")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfMethod("propertyBChanged()"));

    QVERIFY((data = cacheProperty(cache, "onPropertyCChanged")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfMethod("propertyCChanged()"));

    QVERIFY((data = cacheProperty(cache, "onPropertyDChanged")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfMethod("propertyDChanged()"));
}

void tst_qqmlpropertycache::signalHandlersDerived()
{
    QQmlEngine engine;
    DerivedObject object;
    const QMetaObject *metaObject = object.metaObject();

    QQmlPropertyCache::ConstPtr parentCache(
                QQmlPropertyCache::createStandalone(&BaseObject::staticMetaObject));
    QQmlPropertyCache::ConstPtr cache
            = parentCache->copyAndAppend(object.metaObject(), QTypeRevision{});
    const QQmlPropertyData *data;

    QVERIFY((data = cacheProperty(cache, "onSignalA")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfMethod("signalA()"));

    QVERIFY((data = cacheProperty(cache, "onSignalB")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfMethod("signalB()"));

    QVERIFY((data = cacheProperty(cache, "onPropertyAChanged")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfMethod("propertyAChanged()"));

    QVERIFY((data = cacheProperty(cache, "onPropertyBChanged")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfMethod("propertyBChanged()"));

    QVERIFY((data = cacheProperty(cache, "onPropertyCChanged")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfMethod("propertyCChanged()"));

    QVERIFY((data = cacheProperty(cache, "onPropertyDChanged")));
    QCOMPARE(data->coreIndex(), metaObject->indexOfMethod("propertyDChanged()"));
}

class MyEnum : public QObject
 {
    Q_OBJECT
 public:
     enum Option1Flag {
         Option10 = 0,
         Option1A = 1,
         Option1B = 2,
         Option1C = 4,
         Option1D = 8,
         Option1E = 16,
         Option1F = 32,
         Option1AD = Option1A | Option1D,
     };
     Q_DECLARE_FLAGS(Option1, Option1Flag)
     Q_FLAG(Option1)

     enum ShortEnum: quint16 {
         Short0  = 0,
         Short8  = 0xff,
         Short16 = 0xffff
     };
     Q_ENUM(ShortEnum);
};

class MyData : public QObject
{
    Q_OBJECT
    Q_PROPERTY(MyEnum::Option1 opt1 READ opt1 WRITE setOpt1 NOTIFY opt1Changed)
    Q_PROPERTY(MyEnum::ShortEnum opt2 READ opt2 WRITE setOpt2 NOTIFY opt2Changed)
public:
    MyEnum::Option1 opt1() const { return m_opt1;  }
    MyEnum::ShortEnum opt2() const { return m_opt2;  }

signals:
    void opt1Changed(MyEnum::Option1 opt1);
    void opt2Changed(MyEnum::ShortEnum opt2);

public slots:
    void setOpt1(MyEnum::Option1 opt1)
    {
        QCOMPARE(opt1, MyEnum::Option1AD);
        if (opt1 != m_opt1) {
            m_opt1 = opt1;
            emit opt1Changed(opt1);
        }
    }

    void setOpt2(MyEnum::ShortEnum opt2)
    {
        QCOMPARE(opt2, MyEnum::Short16);
        if (opt2 != m_opt2) {
            m_opt2 = opt2;
            emit opt2Changed(opt2);
        }
    }

    void setOption1(MyEnum::Option1 opt1) { setOpt1(opt1); }
    void setOption2(MyEnum::ShortEnum opt2) { setOpt2(opt2); }

private:
    MyEnum::Option1 m_opt1 = MyEnum::Option10;
    MyEnum::ShortEnum m_opt2 = MyEnum::Short8;
};

void tst_qqmlpropertycache::passForeignEnums()
{
    qmlRegisterType<MyEnum>("example", 1, 0, "MyEnum");
    qmlRegisterType<MyData>("example", 1, 0, "MyData");

    MyEnum myenum;
    MyData data;

    engine.rootContext()->setContextProperty("myenum", &myenum);
    engine.rootContext()->setContextProperty("mydata", &data);

    QQmlComponent component(&engine, testFile("foreignEnums.qml"));
    QVERIFY(component.isReady());

    QScopedPointer<QObject> obj(component.create(engine.rootContext()));
    QVERIFY(!obj.isNull());
    QCOMPARE(data.opt1(), MyEnum::Option1AD);
    QCOMPARE(data.opt2(), MyEnum::Short16);
}

Q_DECLARE_METATYPE(MyEnum::Option1)
Q_DECLARE_METATYPE(MyEnum::ShortEnum)

QT_BEGIN_NAMESPACE
class SimpleGadget
{
    Q_GADGET
    Q_PROPERTY(bool someProperty READ someProperty)
public:
    bool someProperty() const { return true; }
};

// Avoids NeedsCreation and NeedsDestruction flags
Q_DECLARE_TYPEINFO(SimpleGadget, Q_PRIMITIVE_TYPE);
QT_END_NAMESPACE

class GadgetEmitter : public QObject
{
    Q_OBJECT
signals:
    void emitGadget(SimpleGadget);
};

void tst_qqmlpropertycache::passQGadget()
{
    qRegisterMetaType<SimpleGadget>();

    GadgetEmitter emitter;
    engine.rootContext()->setContextProperty("emitter", &emitter);
    QQmlComponent component(&engine, testFile("passQGadget.qml"));
    QVERIFY(component.isReady());

    QScopedPointer<QObject> obj(component.create(engine.rootContext()));
    QVariant before = obj->property("result");
    QVERIFY(before.isNull());
    emit emitter.emitGadget(SimpleGadget());
    QVariant after = obj->property("result");
    QCOMPARE(after.typeId(), QMetaType::Bool);
    QVERIFY(after.toBool());
}

class TestClass : public QObject
{
    Q_OBJECT
    Q_PROPERTY(int prop READ prop WRITE setProp NOTIFY propChanged)
    int m_prop;

public:
    enum MyEnum {
        First, Second
    };
    Q_ENUM(MyEnum)

    Q_CLASSINFO("Foo", "Bar")

    TestClass() {}

    int prop() const
    {
        return m_prop;
    }

public slots:
    void setProp(int prop)
    {
        if (m_prop == prop)
            return;

        m_prop = prop;
        emit propChanged(prop);
    }
signals:
    void propChanged(int prop);
};

class TestClassWithParameters : public QObject
{
    Q_OBJECT

public:
    Q_INVOKABLE void slotWithArguments(int firstArg) {
        Q_UNUSED(firstArg);
    }
};

class TestClassWithClassInfo : public QObject
{
    Q_OBJECT
    Q_CLASSINFO("Key", "Value")
};

template <typename T, typename = void>
struct SizeofOffsetsAndSizes_helper
{
    static constexpr size_t value = sizeof(T::offsetsAndSize); // old moc
};

template <typename T>
struct SizeofOffsetsAndSizes_helper<T, std::void_t<decltype(T::offsetsAndSizes)>>
{
    static constexpr size_t value = sizeof(T::offsetsAndSizes); // new moc
};

template <typename T>
constexpr size_t sizeofOffsetsAndSizes(const T &)
{
    return SizeofOffsetsAndSizes_helper<T>::value;
}

// NOTE: ScopeCounter might change new metaobjects are added
// check the moc file and adjust as necessary if tests fails to compile
#define TEST_CLASS(Class, Fields, Strings) \
    QTest::newRow(#Class) \
            << &Class::staticMetaObject \
            << Fields \
            << Strings

Q_DECLARE_METATYPE(const QMetaObject*);

void tst_qqmlpropertycache::metaObjectSize_data()
{
    QTest::addColumn<const QMetaObject*>("metaObject");
    QTest::addColumn<int>("expectedFieldCount");
    QTest::addColumn<int>("expectedStringCount");

    TEST_CLASS(TestClass, 49, 10);
    TEST_CLASS(TestClassWithParameters, 24, 4);
    TEST_CLASS(TestClassWithClassInfo, 17, 3);
}

void tst_qqmlpropertycache::metaObjectSize()
{
    QFETCH(const QMetaObject *, metaObject);
    QFETCH(int, expectedFieldCount);
    QFETCH(int, expectedStringCount);

    int size = 0;
    int stringDataSize = 0;
    bool valid = QQmlPropertyCache::determineMetaObjectSizes(*metaObject, &size, &stringDataSize);
    QVERIFY(valid);

    QCOMPARE(size, expectedFieldCount - 1); // Remove trailing zero field until fixed in moc.
    QCOMPARE(stringDataSize, expectedStringCount);
}

void tst_qqmlpropertycache::metaObjectChecksum()
{
    QMetaObjectBuilder builder;
    builder.setClassName("Test");
    builder.addClassInfo("foo", "bar");

    QCryptographicHash hash(QCryptographicHash::Md5);

    QScopedPointer<QMetaObject, QScopedPointerPodDeleter> mo(builder.toMetaObject());
    QVERIFY(!mo.isNull());

    QVERIFY(QQmlPropertyCache::addToHash(hash, *mo.data()));
    QByteArray initialHash = hash.result();
    QVERIFY(!initialHash.isEmpty());
    hash.reset();

    {
        QVERIFY(QQmlPropertyCache::addToHash(hash, *mo.data()));
        QByteArray nextHash = hash.result();
        QVERIFY(!nextHash.isEmpty());
        hash.reset();
        QCOMPARE(initialHash, nextHash);
    }

    builder.addProperty("testProperty", "int", -1);

    mo.reset(builder.toMetaObject());
    {
        QVERIFY(QQmlPropertyCache::addToHash(hash, *mo.data()));
        QByteArray nextHash = hash.result();
        QVERIFY(!nextHash.isEmpty());
        hash.reset();
        QVERIFY(initialHash != nextHash);
    }
}

void tst_qqmlpropertycache::metaObjectsForRootElements()
{
    QQmlEngine engine;
    QQmlComponent c(&engine, testFileUrl("noDuckType.qml"));
    QVERIFY(c.isReady());
    QScopedPointer<QObject> obj(c.create());
    QVERIFY(!obj.isNull());
    QCOMPARE(obj->property("result").toString(), QString::fromLatin1("good"));
}

void tst_qqmlpropertycache::derivedGadgetMethod()
{
    metaObjectsForRootElements();

    qmlRegisterTypesAndRevisions<BaseGadget, DerivedGadget, GadgetUser>("Test.PropertyCache", 1);
    QQmlEngine engine;
    QQmlComponent c(&engine, testFileUrl("derivedGadgetMethod.qml"));
    QVERIFY(c.isReady());
    QScopedPointer<QObject> obj(c.create());
    QVERIFY(!obj.isNull());
    GadgetUser *gadgetUser = qobject_cast<GadgetUser *>(obj.data());
    QVERIFY(gadgetUser);
    QCOMPARE(gadgetUser->baseString(), QString::fromLatin1("base"));
    QCOMPARE(gadgetUser->derivedString(), QString::fromLatin1("derived"));
}

void tst_qqmlpropertycache::restrictRegistrationVersion()
{
    qmlRegisterTypesAndRevisions<BaseObject>("Test.PropertyCache", 3);
    QQmlEngine engine;
    QQmlComponent c(&engine, testFileUrl("highVersion.qml"));
    QVERIFY(c.isError());
}

void tst_qqmlpropertycache::rejectOverriddenFinal()
{
    qmlRegisterTypesAndRevisions<BaseObject>("Test.PropertyCache", 3);
    QQmlEngine engine;

    QTest::ignoreMessage(QtWarningMsg, QRegularExpression(
                             "Final member finalProp is overridden in class BaseObject_QML_.* "
                             "The override won't be used."));

    QQmlComponent c(&engine, testFileUrl("finalProp.qml"));
    QVERIFY2(!c.isError(), qPrintable(c.errorString()));

    QTest::ignoreMessage(QtWarningMsg, QRegularExpression(
                             "finalProp.qml:15: TypeError: Property 'finalProp' of object "
                             "BaseObject_QML_.* is not a function"));
    QTest::ignoreMessage(QtWarningMsg, QRegularExpression(
                             "finalProp.qml:11: TypeError: Property 'finalProp' of object "
                             "BaseObject_QML_.* is not a function"));

    QScopedPointer<QObject> o(c.create());
    QCOMPARE(o->property("a").toInt(), 8);

    DerivedObject *derived = new DerivedObject(o.data());
    QTest::ignoreMessage(QtWarningMsg,
                         "Final member finalProp is overridden in class DerivedObject. "
                         "The override won't be used.");
    o->setProperty("obj", QVariant::fromValue(derived));
    QCOMPARE(derived->finalProp(), 9);

    // rejects override of final property
    QCOMPARE(o->property("a").toInt(), 8);

    // Cannot override final property with method, either
    QCOMPARE(o->property("b").toInt(), 8);

    // Cannot call the method overridding a final property
    QCOMPARE(o->property("c").toInt(), 0);
}

void tst_qqmlpropertycache::overriddenSignals()
{
    qmlRegisterTypesAndRevisions<BaseObject>("Test.PropertyCache", 3);
    QQmlEngine engine;

    QQmlComponent c1(&engine, testFileUrl("overriddenSignal.qml"));
    QVERIFY2(!c1.isError(), qPrintable(c1.errorString()));

    QScopedPointer<QObject> o(c1.create());

    // the propertyAChanged _signal_ is sent once (initially).
    QCOMPARE(o->property("a").toInt(), 1);

    // signalA() is invoked once as signal, and the other time as method since both are C++.
    QCOMPARE(o->property("b").toInt(), 1);

    OverriddenSignal *derived = new OverriddenSignal(o.data());

    // Does call our overridden method, since that is defined in C++
    QCOMPARE(derived->propertyAChangedCalled, 0);
    o->setProperty("obj", QVariant::fromValue(derived));
    QCOMPARE(derived->propertyAChangedCalled, 1);

    o->setProperty("obj2", QVariant::fromValue(derived));

    // the propertyAChanged _signal_ is sent once (initially).
    QCOMPARE(o->property("a").toInt(), 1);

    // We get to receive both signalA() signals since we only match by name.
    QCOMPARE(o->property("b").toInt(), 2);

    // We shouldn't be allowed to define such things in QML, though.

    const QUrl c2Url = testFileUrl("qmlOverriddenSignal.qml");
    QTest::ignoreMessage(
            QtWarningMsg,
            qPrintable(c2Url.toString() + QLatin1String(
                            ":6:14: Duplicate method name: "
                            "invalid override of property change signal or superclass signal")));
    QQmlComponent c2(&engine, c2Url);
    // Should be an error, but we can't enforce it yet.

    const QUrl c3Url = testFileUrl("qmlOverriddenSignal2.qml");
    QTest::ignoreMessage(
            QtWarningMsg,
            qPrintable(c3Url.toString() + QLatin1String(
                            ":5:12: Duplicate signal name: "
                            "invalid override of property change signal or superclass signal")));
    QQmlComponent c3(&engine, c3Url);
    // Should be an error, but we can't enforce it yet.
}

void tst_qqmlpropertycache::duplicateIdsAndGeneralizedGroupProperties()
{
    QQmlEngine engine;
    QQmlComponent c(&engine, testFileUrl("duplicateIdsAndGeneralizedGroupProperties.qml"));
    QVERIFY2(c.isReady(), qPrintable(c.errorString()));

    QTest::ignoreMessage(QtDebugMsg, "1 true true true");
    QTest::ignoreMessage(QtDebugMsg, "2 false true true");
    QTest::ignoreMessage(QtDebugMsg, "3 false false true");
    QTest::ignoreMessage(QtDebugMsg, "4 false false false");

    QScopedPointer<QObject> o(c.create());
}

// TODO consider moving to test utils
static inline auto propertyWithFlags(QQmlPropertyData::Flags &&flags = QQmlPropertyData::Flags())
        -> QQmlPropertyData
{
    QQmlPropertyData property{};
    property.setFlags(std::move(flags));
    return property;
};

using namespace OverrideSemantics;

static inline auto newPropertyCache(HandlerRef fakeOverrideHandler) -> QQmlPropertyCache::Ptr
{
    return QQmlPropertyCache::Ptr(new QQmlPropertyCache(fakeOverrideHandler),
                                  QQmlPropertyCache::Ptr::Adopt);
}

void tst_qqmlpropertycache::appendPropertyAttr_logging_data()
{
    QTest::addColumn<OverrideSemantics::Status>("overrideStatus");
    QTest::addColumn<QString>("warningPattern");
    QTest::addColumn<QtMsgType>("level");

    QTest::newRow("NoOverride") << Status::NoOverride << "" << QtInfoMsg;
    QTest::newRow("Valid") << Status::Valid << "" << QtInfoMsg;

    QTest::newRow("MissingBase")
            << Status::MissingBase
            << "Member (.*) of the object (.*) does not override anything. Consider "
               "removing \"override\"."
            << QtWarningMsg;
    QTest::newRow("OverridingFinal")
            << Status::OverridingFinal
            << "Final member (.*) is overridden in class (.*). The override won't be used."
            << QtWarningMsg;
    QTest::newRow("OverridingNonVirtual")
            << Status::OverridingNonVirtual
            << "Member (.*) of the object (.*) overrides a non-virtual "
               "member. Consider renaming it or mark it virtual in the base object"
            << QtDebugMsg;
    QTest::newRow("OverridingNonVirtualError")
            << Status::OverridingNonVirtualError
            << "Member (.*) of the object (.*) overrides a non-virtual "
               "member. Consider renaming it or mark it virtual in the base object"
            << QtWarningMsg;
    QTest::newRow("MissingOverrideSpecifier")
            << Status::MissingOverrideOrFinalSpecifier
            << "Member (.*) of the object (.*) overrides a member of the base object. "
               "Consider renaming it or adding final or override specifier"
            << QtWarningMsg;
}

static QLoggingCategory::CategoryFilter parentFilter;
void logFilter(QLoggingCategory *category)
{
    if (qstrcmp(category->categoryName(), "qt.qml.propertyCache.append") == 0)
        category->setEnabled(QtDebugMsg, true);
    else if (parentFilter)
        parentFilter(category);
}


void tst_qqmlpropertycache::appendPropertyAttr_logging()
{
    QFETCH(OverrideSemantics::Status, overrideStatus);
    QFETCH(QString, warningPattern);
    QFETCH(QtMsgType, level);

    parentFilter = QLoggingCategory::installFilter(logFilter);
    const auto restoreFilter = qScopeGuard([]() { QLoggingCategory::installFilter(parentFilter); });

    const auto fakeOverrideHandler = [&overrideStatus](QQmlPropertyData &, QQmlPropertyData *,
                                                       CheckMode) -> Status {
        return overrideStatus;
    };

    if (!warningPattern.isEmpty())
        QTest::ignoreMessage(level, QRegularExpression(warningPattern.toLatin1()));

    newPropertyCache(fakeOverrideHandler)->appendPropertyAttr("p", QQmlPropertyData());
}

void tst_qqmlpropertycache::appendPropertyAttr_InvalidOverride_data()
{
    QTest::addColumn<OverrideSemantics::Status>("overrideStatus");

    QTest::newRow("MissingBase") << Status::MissingBase;
    QTest::newRow("OverridingFinal") << Status::OverridingFinal;
    QTest::newRow("OverridingNonVirtualError") << Status::OverridingNonVirtualError;
}

void tst_qqmlpropertycache::appendPropertyAttr_InvalidOverride()
{
    QFETCH(OverrideSemantics::Status, overrideStatus);

    QVERIFY(!isValidOverride(overrideStatus));

    const auto fakeOverrideHandler = [&overrideStatus](QQmlPropertyData &, QQmlPropertyData *,
                                                       CheckMode) -> Status {
        return overrideStatus;
    };

    QQmlPropertyCache::Ptr cache = newPropertyCache(fakeOverrideHandler);
    QCOMPARE(cache->propertyCount(), 0);

    const auto res = cache->appendPropertyAttr("p", QQmlPropertyData());

    QVERIFY(!res);
    QCOMPARE(res.error(), overrideStatus);
    QCOMPARE(cache->propertyCount(), 1);

    // since append is invalid the property is not accessible by name
    QVERIFY(!cacheProperty(cache, "p"));
}

void tst_qqmlpropertycache::appendPropertyAttr_ValidOverride_data()
{
    QTest::addColumn<OverrideSemantics::Status>("overrideStatus");

    QTest::newRow("NoOverride") << Status::NoOverride;
    QTest::newRow("Valid") << Status::Valid;
    QTest::newRow("MissingOverrideSpecifier") << Status::MissingOverrideOrFinalSpecifier;
    QTest::newRow("OverridingNonVirtual") << Status::OverridingNonVirtual;
}

void tst_qqmlpropertycache::appendPropertyAttr_ValidOverride()
{
    QFETCH(OverrideSemantics::Status, overrideStatus);

    QVERIFY(isValidOverride(overrideStatus));

    const auto fakeOverrideHandler = [&overrideStatus](QQmlPropertyData &, QQmlPropertyData *,
                                                       CheckMode) -> Status {
        return overrideStatus;
    };

    QQmlPropertyCache::Ptr cache = newPropertyCache(fakeOverrideHandler);
    auto propCount = 0;
    QCOMPARE(cache->propertyCount(), propCount);

    const QString propName = "p";
    {
        QQmlPropertyData data{};
        // set just to verify upon query that it's the same data
        data.setCoreIndex(6);

        const auto res = cache->appendPropertyAttr(propName, QQmlPropertyData(data));

        QVERIFY(res);
        QCOMPARE(cache->propertyCount(), ++propCount);
        const auto *addedProperty = cache->property(propName, nullptr, nullptr);
        QVERIFY(addedProperty);
        QCOMPARE(*addedProperty, data);
    }
    // Now override the property
    {
        QQmlPropertyData data{};
        // set just to verify upon query that it's the same data
        data.setCoreIndex(3);
        data.setWritable(false);

        const auto res = cache->appendPropertyAttr(propName, QQmlPropertyData(data));

        QVERIFY(res);
        QCOMPARE(cache->propertyCount(), ++propCount);
        const auto *addedProperty = cache->property(propName, nullptr, nullptr);
        QVERIFY(addedProperty);
        QCOMPARE(*addedProperty, data);
    }
}

void tst_qqmlpropertycache::handleOverride_data()
{
    QTest::addColumn<QQmlPropertyData>("overridingProperty");
    QTest::addColumn<std::optional<QQmlPropertyData>>("existingProperty");
    QTest::addColumn<CheckMode>("checkMode");
    QTest::addColumn<Status>("expectedResult");

    const auto makeNullopt = []() -> std::optional<QQmlPropertyData> { return std::nullopt; };

    // Minimal checks
    QTest::newRow("Derived{property var p} Base{}")
            << propertyWithFlags() << makeNullopt() << CheckMode::Minimal << Status::NoOverride;

    QQmlPropertyData::Flags final{};
    final.setIsFinal(true);
    const auto finalProperty = propertyWithFlags(std::move(final));
    QTest::newRow("Derived{property var p} Base{final property var p}")
            << propertyWithFlags() << std::make_optional(finalProperty) << CheckMode::Minimal
            << Status::OverridingFinal;

    QTest::newRow("Derived{property var p} Base{ property var p}")
            << propertyWithFlags() << std::make_optional(propertyWithFlags()) << CheckMode::Minimal
            << Status::Valid;

    // Full
    QTest::newRow("Derived{property var p} Base{} full")
            << propertyWithFlags() << makeNullopt() << CheckMode::Full << Status::NoOverride;

    QQmlPropertyData::Flags override{};
    override.setDoesOverride(true);
    const auto propertyWithOverride = propertyWithFlags(std::move(override));

    QQmlPropertyData::Flags virtual_{};
    virtual_.setIsVirtual(true);
    const auto propertyWithVirtual = propertyWithFlags(std::move(virtual_));

    QTest::newRow("Derived{override property var p} Base{}")
            << propertyWithOverride << makeNullopt() << CheckMode::Full << Status::MissingBase;

    QTest::newRow("Derived{property var p} Base{final property var p} full")
            << propertyWithFlags() << std::make_optional(finalProperty) << CheckMode::Full
            << Status::OverridingFinal;
    QTest::newRow("Derived{override property var p} Base{final property var p}")
            << propertyWithOverride << std::make_optional(finalProperty) << CheckMode::Full
            << Status::OverridingFinal;

    QTest::newRow("Derived{property var p} Base{property var p}")
            << propertyWithFlags() << std::make_optional(propertyWithFlags()) << CheckMode::Full
            << Status::OverridingNonVirtual;

    QTest::newRow("Derived{final property var p} Base{property var p}")
            << finalProperty << std::make_optional(propertyWithFlags()) << CheckMode::Full
            << Status::OverridingNonVirtual;

    QTest::newRow("Derived{virtual property var p} Base{property var p}")
            << propertyWithVirtual << std::make_optional(propertyWithFlags()) << CheckMode::Full
            << Status::OverridingNonVirtual;

    QTest::newRow("Derived{override property var p} Base{property var p}")
            << propertyWithOverride << std::make_optional(propertyWithFlags()) << CheckMode::Full
            << Status::OverridingNonVirtualError;

    QTest::newRow("Derived{property var p} Base{virtual property var p}")
            << propertyWithFlags() << std::make_optional(propertyWithVirtual) << CheckMode::Full
            << Status::MissingOverrideOrFinalSpecifier;

    QTest::newRow("Derived{virtual property var p} Base{virtual property var p}")
            << propertyWithVirtual << std::make_optional(propertyWithVirtual) << CheckMode::Full
            << Status::MissingOverrideOrFinalSpecifier;

    QTest::newRow("Derived{override property var p} Base{virtual property var p}")
            << propertyWithOverride << std::make_optional(propertyWithVirtual) << CheckMode::Full
            << Status::Valid;

    QTest::newRow("Derived{final property var p} Base{virtual property var p}")
            << finalProperty << std::make_optional(propertyWithVirtual) << CheckMode::Full
            << Status::Valid;

    QQmlPropertyData::Flags invokable{};
    invokable.setType(QQmlPropertyData::Flags::Type::FunctionType);
    auto funcProperty = propertyWithFlags(std::move(invokable));

    QTest::newRow("Derived{override p()} Base{virtual property var p}")
            << funcProperty << std::make_optional(propertyWithFlags(std::move(virtual_)))
            << CheckMode::Full << Status::InvokabilityMismatch;

    QTest::newRow("Derived{override p} Base{p()}")
            << propertyWithOverride << std::make_optional(funcProperty) << CheckMode::Full
            << Status::InvokabilityMismatch;
}

void tst_qqmlpropertycache::handleOverride()
{
    QFETCH(QQmlPropertyData, overridingProperty);
    QFETCH(std::optional<QQmlPropertyData>, existingProperty);
    QFETCH(CheckMode, checkMode);
    QFETCH(Status, expectedResult);

    auto *existing = existingProperty.has_value() ? &existingProperty.value() : nullptr;
    const auto res = ::handleOverride(overridingProperty, existing, checkMode);
    QCOMPARE(res, expectedResult);
    if (isValidOverride(res) && existing) {
        // test markOverrideOf
        QCOMPARE(overridingProperty.isVirtual(), existing->isVirtual());
        QCOMPARE(overridingProperty.overrideIndexIsProperty(), !existing->isFunction());
        QCOMPARE(overridingProperty.overrideIndex(), existing->coreIndex());
        QVERIFY(existing->isOverridden());
    }
}

void tst_qqmlpropertycache::nonExistentGeneralizedGroup()
{
    QQmlEngine engine;
    QQmlComponent component(&engine);
    component.setData(R"(
        import QtQml
        QtObject{grid{id:grid}}
    )", QUrl());
    QVERIFY(component.isError());
    QVERIFY(component.errorString().contains("Cannot assign to non-existent property \"grid\""));
}

void tst_qqmlpropertycache::append_propertyAttr_data()
{
    appendPropertyAttr_logging_data();
}

/*
 * Because of the tightly coupling of the QQmlPropertyCache, this case covers very
 * narrowed (far from exhaustive) pieces of "append" method through publicly exposed "update"
 * method.
 * It verifies that depending on the Override Semantics the corresponding logging takes
 * place. It also verifies that the property is being added and accessible depending on the status of
 * override semantics.
 */
void tst_qqmlpropertycache::append_propertyAttr()
{
    QFETCH(OverrideSemantics::Status, overrideStatus);
    QFETCH(QString, warningPattern);
    QFETCH(QtMsgType, level);

    parentFilter = QLoggingCategory::installFilter(logFilter);
    const auto restoreFilter = qScopeGuard([]() { QLoggingCategory::installFilter(parentFilter); });


    const auto fakeOverrideHandler = [&overrideStatus](QQmlPropertyData &, QQmlPropertyData *,
                                                       CheckMode) -> Status {
        return overrideStatus;
    };

    QQmlPropertyCache::Ptr cache = newPropertyCache(fakeOverrideHandler);
    QCOMPARE(cache->propertyCount(), 0);

    QMetaObjectBuilder moBuilder;
    const QString propName = "p";
    moBuilder.addProperty(propName.toUtf8(), "int", -1);
    std::unique_ptr<QMetaObject, decltype(&free)> mo(moBuilder.toMetaObject(), &free);

    if (!warningPattern.isEmpty())
        QTest::ignoreMessage(level, QRegularExpression(warningPattern.toLatin1()));
    cache->update(mo.get());

    // no matter what the property is added
    QCOMPARE(cache->propertyCount(), mo->propertyCount());
    const auto *addedProperty = cache->property(propName, nullptr, nullptr);

    // however it is accessible by name only in cases of valid override
    if (isValidOverride(overrideStatus)) {
        QVERIFY(addedProperty);
    } else {
        QVERIFY(!addedProperty);
    }
}

void tst_qqmlpropertycache::isComposite()
{
    // A property cache from a C++ QMetaObject should not be composite.
    QQmlPropertyCache::Ptr cppCache(
            QQmlPropertyCache::createStandalone(&QObject::staticMetaObject));
    QVERIFY(!cppCache->isComposite());

    // A property cache from a QML component should be composite.
    QQmlComponent component(&engine, testFileUrl("Simple.qml"));
    QVERIFY2(component.isReady(), qPrintable(component.errorString()));
    QScopedPointer<QObject> obj(component.create());
    QVERIFY(obj);
    QQmlData *ddata = QQmlData::get(obj.data());
    QVERIFY(ddata);
    QVERIFY(ddata->propertyCache);
    QVERIFY(ddata->propertyCache->isComposite());
}

void tst_qqmlpropertycache::rebase()
{
    QQmlComponent component(&engine, testFileUrl("Simple.qml"));
    QVERIFY2(component.isReady(), qPrintable(component.errorString()));
    QScopedPointer<QObject> obj(component.create());
    QVERIFY(obj);

    QQmlData *ddata = QQmlData::get(obj.data());
    QVERIFY(ddata);
    QVERIFY(ddata->propertyCache);

    const QQmlPropertyCache::ConstPtr original = ddata->propertyCache;
    QVERIFY(original->parent());

    // Rebase onto the same parent – property counts should match.
    const QQmlPropertyCache::Ptr rebased = original->rebased(original->parent());

    QCOMPARE(rebased->propertyCount(), original->propertyCount());
    QCOMPARE(rebased->methodCount(), original->methodCount());
    QCOMPARE(rebased->signalCount(), original->signalCount());
    QVERIFY(rebased->parent().data() == original->parent().data());

    // Verify that the custom property "testProp" is still reachable.
    const QQmlPropertyData *prop = rebased->property(
            QLatin1String("testProp"), /*object=*/nullptr, /*context=*/nullptr);
    QVERIFY(prop);
    QVERIFY(prop->propType() == QMetaType::fromType<int>());
}

QTEST_MAIN(tst_qqmlpropertycache)

#include "tst_qqmlpropertycache.moc"
