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

#include <QtTest/QTest>
#include <QtTest/QSignalSpy>
#include <QtCore/QScopedPointer>
#include <QtQml/qqmlcomponent.h>
#include <QtQml/qqmlengine.h>
#include <QtQuickTestUtils/private/qmlutils_p.h>

Q_DECLARE_METATYPE(QMetaMethod::MethodType)

class MyQmlObject : public QObject
{
    Q_OBJECT
};
QML_DECLARE_TYPE(MyQmlObject)

class tst_QQmlMetaObject : public QQmlDataTest
{
    Q_OBJECT

public:
    tst_QQmlMetaObject();

private slots:
    void initTestCase() override;

    void property_data();
    void property();
    void method_data();
    void method();

private:
    MyQmlObject myQmlObject;
};

tst_QQmlMetaObject::tst_QQmlMetaObject()
    : QQmlDataTest(QT_QMLTEST_DATADIR)
{
}

void tst_QQmlMetaObject::initTestCase()
{
    QQmlDataTest::initTestCase();

    qmlRegisterType<MyQmlObject>("Qt.test", 1,0, "MyQmlObject");
}

void tst_QQmlMetaObject::property_data()
{
    QTest::addColumn<QString>("testFile");
    QTest::addColumn<QByteArray>("cppTypeName");
    QTest::addColumn<int>("cppType");
    QTest::addColumn<bool>("isDefault");
    QTest::addColumn<QVariant>("expectedValue");
    QTest::addColumn<bool>("isWritable");
    QTest::addColumn<QVariant>("newValue");

    QTest::newRow("int") << "property.int.qml"
            << QByteArray("int") << int(QMetaType::Int)
            << false // default
            << QVariant(19) << true << QVariant(42);
    QTest::newRow("bool") << "property.bool.qml"
            << QByteArray("bool") << int(QMetaType::Bool)
            << true // default
            << QVariant(true) << true << QVariant(false);
    QTest::newRow("double") << "property.double.qml"
             << QByteArray("double") << int(QMetaType::Double)
             << false // default
             << QVariant(double(1234567890.))
             << true // writable
             << QVariant(double(1.23456789));
    QTest::newRow("real") << "property.real.qml"
            << QByteArray("double") << int(QMetaType::Double)
            << false // default
            << QVariant(double(1234567890.))
            << true // writable
            << QVariant(double(1.23456789));
    QTest::newRow("string") << "property.string.qml"
            << QByteArray("QString") << int(QMetaType::QString)
            << true // default
            << QVariant(QString::fromLatin1("dog"))
            << true // writable
            << QVariant(QString::fromLatin1("food"));
    QTest::newRow("url") << "property.url.qml"
            << QByteArray("QUrl") << int(QMetaType::QUrl)
            << false // default
            << QVariant(QUrl("http://foo.bar"))
            << true //writable
            << QVariant(QUrl("http://bar.baz"));
    QTest::newRow("color") << "property.color.qml"
            << QByteArray("QColor") << int(QMetaType::QColor)
            << true // default
            << QVariant(QColor("#ff0000"))
            << true // writable
            << QVariant(QColor("#00ff00"));
    QTest::newRow("date") << "property.date.qml"
            << QByteArray("QDateTime") << int(QMetaType::QDateTime)
            << false // default
            << QVariant(QDate(2012, 2, 7).startOfDay())
            << true // writable
            << QVariant(QDate(2010, 7, 2).startOfDay());
    QTest::newRow("var") << "property.var.qml"
            << QByteArray("QVariant") << int(QMetaType::QVariant)
            << false // default
            << QVariant(QVariantList() << 5 << true << "ciao")
            << true // writable
            << QVariant(QVariantList() << 17.0);
    QTest::newRow("QtObject") << "property.QtObject.qml"
            << QByteArray("QObject*") << int(QMetaType::QObjectStar)
            << false // default
            << QVariant()
            << true // writable
            << QVariant::fromValue(static_cast<QObject*>(this));
    QTest::newRow("list<QtObject>") << "property.list.QtObject.qml"
            << QByteArray("QQmlListProperty<QObject>")
            << qMetaTypeId<QQmlListProperty<QObject> >()
            << false // default
            << QVariant()
            << false // writable
            << QVariant();
    QTest::newRow("MyQmlObject") << "property.MyQmlObject.qml"
            << QByteArray("MyQmlObject*") << qMetaTypeId<MyQmlObject*>()
            << false // default
            << QVariant()
            << true // writable
            << QVariant::fromValue(&myQmlObject);
    QTest::newRow("list<MyQmlObject>") << "property.list.MyQmlObject.qml"
            << QByteArray("QQmlListProperty<MyQmlObject>")
            << qMetaTypeId<QQmlListProperty<MyQmlObject> >()
            << false // default
            << QVariant()
            << false // writable
            << QVariant();
    QTest::newRow("alias") << "property.alias.qml"
            << QByteArray("QString") << int(QMetaType::QString)
            << false // default
            << QVariant(QString::fromLatin1("Joe"))
            << true // writable
            << QVariant(QString::fromLatin1("Bob"));
    QTest::newRow("alias-2") << "property.alias.2.qml"
            << QByteArray("QObject*") << int(QMetaType::QObjectStar)
            << false // default
            << QVariant()
            << false // writable
            << QVariant();
    QTest::newRow("alias-3") << "property.alias.3.qml"
            << QByteArray("QString") << int(QMetaType::QString)
            << false // default
            << QVariant(QString::fromLatin1("Arial"))
            << true // writable
            << QVariant(QString::fromLatin1("Helvetica"));
}

void tst_QQmlMetaObject::property()
{
    QFETCH(QString, testFile);
    QFETCH(QByteArray, cppTypeName);
    QFETCH(int, cppType);
    QFETCH(bool, isDefault);
    QFETCH(QVariant, expectedValue);
    QFETCH(bool, isWritable);
    QFETCH(QVariant, newValue);

    QQmlEngine engine;
    QQmlComponent component(&engine, testFileUrl(testFile));
    QScopedPointer<QObject> obj(component.create());
    QVERIFY(obj);
    QObject *object = obj.get();

    const QMetaObject *mo = object->metaObject();
    QVERIFY(mo->superClass() != nullptr);
    QVERIFY(QByteArray(mo->className()).contains("_QML_"));
    QCOMPARE(mo->propertyOffset(), mo->superClass()->propertyCount());
    QCOMPARE(mo->propertyCount(), mo->superClass()->propertyCount() + 1);

    QMetaProperty prop = mo->property(mo->propertyOffset());
    QCOMPARE(prop.name(), "test");

    QCOMPARE(QByteArray(prop.typeName()), cppTypeName);
    QCOMPARE(prop.metaType().id(), cppType);

    QVERIFY(!prop.isConstant());
    QVERIFY(!prop.isDesignable());
    QVERIFY(!prop.isEnumType());
    QVERIFY(!prop.isFinal());
    QVERIFY(!prop.isFlagType());
    QVERIFY(prop.isReadable());
    QVERIFY(!prop.isResettable());
    QVERIFY(prop.isScriptable());
    QVERIFY(!prop.isStored());
    QVERIFY(!prop.isUser());
    QVERIFY(prop.isValid());
    QCOMPARE(prop.isWritable(), isWritable);

    QCOMPARE(mo->classInfoOffset(), mo->superClass()->classInfoCount());
    QCOMPARE(mo->classInfoCount(), mo->superClass()->classInfoCount() + (isDefault ? 1 : 0));
    if (isDefault) {
        QMetaClassInfo info = mo->classInfo(mo->classInfoOffset());
        QCOMPARE(info.name(), "DefaultProperty");
        QCOMPARE(info.value(), "test");
    }

    QCOMPARE(mo->methodOffset(), mo->superClass()->methodCount());
    QCOMPARE(mo->methodCount(), mo->superClass()->methodCount() + 1); // the signal

    QVERIFY(prop.notifySignalIndex() != -1);
    QMetaMethod signal = prop.notifySignal();
    QCOMPARE(signal.methodType(), QMetaMethod::Signal);
    QCOMPARE(signal.name(), QByteArray("testChanged"));
    QCOMPARE(signal.methodSignature(), QByteArray("testChanged()"));
    QCOMPARE(signal.access(), QMetaMethod::Public);
    QCOMPARE(signal.parameterCount(), 0);
    QCOMPARE(signal.parameterTypes(), QList<QByteArray>());
    QCOMPARE(signal.parameterNames(), QList<QByteArray>());
    QCOMPARE(signal.tag(), "");
    QCOMPARE(signal.typeName(), "void");
    QCOMPARE(signal.returnType(), int(QMetaType::Void));

    QSignalSpy changedSpy(object, SIGNAL(testChanged()));
    QObject::connect(object, SIGNAL(testChanged()), object, SLOT(deleteLater()));

    QVariant value = prop.read(object);
    if (value.userType() == qMetaTypeId<QJSValue>())
        value = value.value<QJSValue>().toVariant();
    if (expectedValue.isValid())
        QCOMPARE(value, expectedValue);
    else
        QVERIFY(value.isValid());
    QCOMPARE(changedSpy.size(), 0);

    if (isWritable) {
        QVERIFY(prop.write(object, newValue));
        QCOMPARE(changedSpy.size(), 1);
        QVariant value = prop.read(object);
        if (value.userType() == qMetaTypeId<QJSValue>())
            value = value.value<QJSValue>().toVariant();
        QCOMPARE(value, newValue);
    } else {
        QVERIFY(!prop.write(object, prop.read(object)));
        QCOMPARE(changedSpy.size(), 0);
    }
}

void tst_QQmlMetaObject::method_data()
{
    QTest::addColumn<QString>("testFile");
    QTest::addColumn<QString>("signature");
    QTest::addColumn<QMetaMethod::MethodType>("methodType");
    QTest::addColumn<int>("returnType");
    QTest::addColumn<QString>("returnTypeName");
    QTest::addColumn<QList<int> >("parameterTypes");
    QTest::addColumn<QList<QByteArray> >("parameterTypeNames");
    QTest::addColumn<QList<QByteArray> >("parameterNames");

    QTest::newRow("testFunction()") << "method.1.qml"
            << "testFunction()"
            << QMetaMethod::Slot
            << int(QMetaType::QVariant) << "QVariant"
            << QList<int>()
            << QList<QByteArray>()
            << QList<QByteArray>();
    QTest::newRow("testFunction(foo)") << "method.2.qml"
            << "testFunction(QVariant)"
            << QMetaMethod::Slot
            << int(QMetaType::QVariant) << "QVariant"
            << (QList<int>() << QMetaType::QVariant)
            << (QList<QByteArray>() << "QVariant")
            << (QList<QByteArray>() << "foo");
    QTest::newRow("testFunction(foo, bar, baz)") << "method.3.qml"
            << "testFunction(QVariant,QVariant,QVariant)"
            << QMetaMethod::Slot
            << int(QMetaType::QVariant) << "QVariant"
            << (QList<int>() << QMetaType::QVariant << QMetaType::QVariant << QMetaType::QVariant)
            << (QList<QByteArray>() << "QVariant" << "QVariant" << "QVariant")
            << (QList<QByteArray>() << "foo" << "bar" << "baz");
    QTest::newRow("testSignal") << "signal.1.qml"
            << "testSignal()"
            << QMetaMethod::Signal
            << int(QMetaType::Void) << "void"
            << QList<int>()
            << QList<QByteArray>()
            << QList<QByteArray>();
    QTest::newRow("testSignal(string foo)") << "signal.2.qml"
            << "testSignal(QString)"
            << QMetaMethod::Signal
            << int(QMetaType::Void) << "void"
            << (QList<int>() << QMetaType::QString)
            << (QList<QByteArray>() << "QString")
            << (QList<QByteArray>() << "foo");
    QTest::newRow("testSignal(int foo, bool bar, real baz)") << "signal.3.qml"
            << "testSignal(int,bool,double)"
            << QMetaMethod::Signal
            << int(QMetaType::Void) << "void"
            << (QList<int>() << QMetaType::Int << QMetaType::Bool << QMetaType::Double)
            << (QList<QByteArray>() << "int" << "bool" << "double")
            << (QList<QByteArray>() << "foo" << "bar" << "baz");
    QTest::newRow("testSignal(variant foo, var bar)") << "signal.4.qml"
            << "testSignal(QVariant,QVariant)"
            << QMetaMethod::Signal
            << int(QMetaType::Void) << "void"
            << (QList<int>() << QMetaType::QVariant << QMetaType::QVariant)
            << (QList<QByteArray>() << "QVariant" << "QVariant")
            << (QList<QByteArray>() << "foo" << "bar");
    QTest::newRow("testSignal(color foo, date bar, url baz)") << "signal.5.qml"
            << "testSignal(QColor,QDateTime,QUrl)"
            << QMetaMethod::Signal
            << int(QMetaType::Void) << "void"
            << (QList<int>() << QMetaType::QColor << QMetaType::QDateTime << QMetaType::QUrl)
            << (QList<QByteArray>() << "QColor" << "QDateTime" << "QUrl")
            << (QList<QByteArray>() << "foo" << "bar" << "baz");
    QTest::newRow("testSignal(double foo)") << "signal.6.qml"
            << "testSignal(double)"
            << QMetaMethod::Signal
            << int(QMetaType::Void) << "void"
            << (QList<int>() << QMetaType::Double)
            << (QList<QByteArray>() << "double")
            << (QList<QByteArray>() << "foo");
}

void tst_QQmlMetaObject::method()
{
    QFETCH(QString, testFile);
    QFETCH(QString, signature);
    QFETCH(QMetaMethod::MethodType, methodType);
    QFETCH(int, returnType);
    QFETCH(QString, returnTypeName);
    QFETCH(QList<int>, parameterTypes);
    QFETCH(QList<QByteArray>, parameterTypeNames);
    QFETCH(QList<QByteArray>, parameterNames);

    QCOMPARE(parameterTypes.size(), parameterTypeNames.size());
    QCOMPARE(parameterTypeNames.size(), parameterNames.size());

    QQmlEngine engine;
    QQmlComponent component(&engine, testFileUrl(testFile));
    QScopedPointer<QObject> obj(component.create());
    QVERIFY(obj);
    QObject *object = obj.get();

    const QMetaObject *mo = object->metaObject();
    QVERIFY(mo->superClass() != nullptr);
    QVERIFY(QByteArray(mo->className()).contains("_QML_"));
    QCOMPARE(mo->methodOffset(), mo->superClass()->methodCount());
    QCOMPARE(mo->methodCount(), mo->superClass()->methodCount() + 1);

    QMetaMethod method = mo->method(mo->methodOffset());
    QCOMPARE(method.methodType(), methodType);
    QCOMPARE(QString::fromUtf8(method.methodSignature().constData()), signature);
    QCOMPARE(method.access(), QMetaMethod::Public);

    QString computedName = signature.left(signature.indexOf('('));
    QCOMPARE(QString::fromUtf8(method.name()), computedName);

    QCOMPARE(method.parameterCount(), parameterTypes.size());
    for (int i = 0; i < parameterTypes.size(); ++i)
        QCOMPARE(method.parameterType(i), parameterTypes.at(i));
    QCOMPARE(method.parameterTypes(), parameterTypeNames);
    QCOMPARE(method.tag(), "");

    QCOMPARE(QString::fromUtf8(method.typeName()), returnTypeName);
    QCOMPARE(method.returnType(), returnType);
}

QTEST_MAIN(tst_QQmlMetaObject)

#include "tst_qqmlmetaobject.moc"
