/****************************************************************************
**
** Copyright (C) 2018 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 <QtScript/qscriptengine.h>
#include <QtScript/qscriptclass.h>
#include <QtScript/qscriptclasspropertyiterator.h>
#include <QtScript/qscriptstring.h>
#include <QtScript/qscriptvalueiterator.h>

Q_DECLARE_METATYPE(QScriptContext*)
Q_DECLARE_METATYPE(QScriptValueList)
Q_DECLARE_METATYPE(QScriptValue)

class tst_QScriptClass : public QObject
{
    Q_OBJECT

public:
    tst_QScriptClass();
    virtual ~tst_QScriptClass();

private slots:
    void newInstance();
    void setScriptClassOfExistingObject();
    void setScriptClassOfNonQtScriptObject();
    void getAndSetPropertyFromCpp();
    void getAndSetPropertyFromJS();
    void deleteUndeletableProperty();
    void writeReadOnlyProperty();
    void writePropertyWithoutWriteAccess();
    void getProperty_invalidValue();
    void enumerate();
    void extension_None();
    void extension_Callable();
    void extension_Callable_construct();
    void extension_HasInstance();
    void originalProperties1();
    void originalProperties2();
    void originalProperties3();
    void originalProperties4();
    void defaultImplementations();
    void scriptClassObjectInPrototype();
    void scriptClassWithNullEngine();
    void scriptClassInOtherEngine();
};

tst_QScriptClass::tst_QScriptClass()
{
}

tst_QScriptClass::~tst_QScriptClass()
{
}



class TestClass : public QScriptClass
{
public:
    struct CustomProperty {
        QueryFlags qflags;
        uint id;
        QScriptValue::PropertyFlags pflags;
        QScriptValue value;

        CustomProperty(QueryFlags qf, uint i, QScriptValue::PropertyFlags pf,
                       const QScriptValue &val)
            : qflags(qf), id(i), pflags(pf), value(val) { }
    };

    enum CallableMode {
        NotCallable,
        CallableReturnsSum,
        CallableReturnsArgument,
        CallableReturnsInvalidVariant,
        CallableReturnsGlobalObject,
        CallableReturnsThisObject,
        CallableReturnsCallee,
        CallableReturnsArgumentsObject,
        CallableInitializesThisObject
    };

    TestClass(QScriptEngine *engine);
    ~TestClass();

    void addCustomProperty(const QScriptString &name, QueryFlags qflags,
                           uint id, QScriptValue::PropertyFlags pflags,
                           const QScriptValue &value);
    void removeCustomProperty(const QScriptString &name);

    QueryFlags queryProperty(const QScriptValue &object,
                             const QScriptString &name,
                             QueryFlags flags, uint *id);

    QScriptValue property(const QScriptValue &object,
                          const QScriptString &name, uint id);

    void setProperty(QScriptValue &object, const QScriptString &name,
                     uint id, const QScriptValue &value);

    QScriptValue::PropertyFlags propertyFlags(
        const QScriptValue &object, const QScriptString &name, uint id);

    QScriptClassPropertyIterator *newIterator(const QScriptValue &object);

    QScriptValue prototype() const;

    QString name() const;

    bool supportsExtension(Extension extension) const;
    QVariant extension(Extension extension,
                       const QVariant &argument = QVariant());

    QScriptValue lastQueryPropertyObject() const;
    QScriptString lastQueryPropertyName() const;
    QueryFlags lastQueryPropertyFlags() const;

    QScriptValue lastPropertyObject() const;
    QScriptString lastPropertyName() const;
    uint lastPropertyId() const;

    QScriptValue lastSetPropertyObject() const;
    QScriptString lastSetPropertyName() const;
    uint lastSetPropertyId() const;
    QScriptValue lastSetPropertyValue() const;

    QScriptValue lastPropertyFlagsObject() const;
    QScriptString lastPropertyFlagsName() const;
    uint lastPropertyFlagsId() const;

    QScriptClass::Extension lastExtensionType() const;
    QVariant lastExtensionArgument() const;

    void clearReceivedArgs();

    void setIterationEnabled(bool enable);
    bool isIterationEnabled() const;

    void setCallableMode(CallableMode mode);
    CallableMode callableMode() const;

    void setHasInstance(bool hasInstance);
    bool hasInstance() const;

private:
    CustomProperty *findCustomProperty(const QScriptString &name);

    QHash<QScriptString, CustomProperty*> customProperties;

    QScriptValue m_lastQueryPropertyObject;
    QScriptString m_lastQueryPropertyName;
    QScriptClass::QueryFlags m_lastQueryPropertyFlags;

    QScriptValue m_lastPropertyObject;
    QScriptString m_lastPropertyName;
    uint m_lastPropertyId;

    QScriptValue m_lastSetPropertyObject;
    QScriptString m_lastSetPropertyName;
    uint m_lastSetPropertyId;
    QScriptValue m_lastSetPropertyValue;

    QScriptValue m_lastPropertyFlagsObject;
    QScriptString m_lastPropertyFlagsName;
    uint m_lastPropertyFlagsId;

    QScriptClass::Extension m_lastExtensionType;
    QVariant m_lastExtensionArgument;

    QScriptValue m_prototype;
    bool m_iterationEnabled;
    CallableMode m_callableMode;
    bool m_hasInstance;
};

class TestClassPropertyIterator : public QScriptClassPropertyIterator
{
public:
    TestClassPropertyIterator(const QHash<QScriptString, TestClass::CustomProperty*> &props,
                      const QScriptValue &object);
    ~TestClassPropertyIterator();

    bool hasNext() const;
    void next();

    bool hasPrevious() const;
    void previous();

    void toFront();
    void toBack();

    QScriptString name() const;
    uint id() const;
    QScriptValue::PropertyFlags flags() const;

private:
    int m_index;
    int m_last;
    QHash<QScriptString, TestClass::CustomProperty*> m_props;
};



TestClass::TestClass(QScriptEngine *engine)
    : QScriptClass(engine), m_iterationEnabled(true),
      m_callableMode(NotCallable), m_hasInstance(false)
{
    m_prototype = engine->newObject();
    clearReceivedArgs();
}

TestClass::~TestClass()
{
    qDeleteAll(customProperties);
}

TestClass::CustomProperty* TestClass::findCustomProperty(const QScriptString &name)
{
    QHash<QScriptString, CustomProperty*>::const_iterator it;
    it = customProperties.constFind(name);
    if (it == customProperties.constEnd())
        return 0;
    return it.value();

}

void TestClass::addCustomProperty(const QScriptString &name, QueryFlags qflags,
                                  uint id, QScriptValue::PropertyFlags pflags,
                                  const QScriptValue &value)
{
    customProperties.insert(name, new CustomProperty(qflags, id, pflags, value));
}

void TestClass::removeCustomProperty(const QScriptString &name)
{
    CustomProperty *prop = customProperties.take(name);
    if (prop)
        delete prop;
}

QScriptClass::QueryFlags TestClass::queryProperty(const QScriptValue &object,
                                    const QScriptString &name,
                                    QueryFlags flags, uint *id)
{
    m_lastQueryPropertyObject = object;
    m_lastQueryPropertyName = name;
    m_lastQueryPropertyFlags = flags;
    CustomProperty *prop = findCustomProperty(name);
    if (!prop)
        return {};
    *id = prop->id;
    return prop->qflags & flags;
}

QScriptValue TestClass::property(const QScriptValue &object,
                                 const QScriptString &name, uint id)
{
    m_lastPropertyObject = object;
    m_lastPropertyName = name;
    m_lastPropertyId = id;
    CustomProperty *prop = findCustomProperty(name);
    if (!prop)
        return QScriptValue();
    return prop->value;
}

void TestClass::setProperty(QScriptValue &object, const QScriptString &name,
                            uint id, const QScriptValue &value)
{
    m_lastSetPropertyObject = object;
    m_lastSetPropertyName = name;
    m_lastSetPropertyId = id;
    m_lastSetPropertyValue = value;
    CustomProperty *prop = findCustomProperty(name);
    if (!prop)
        return;
    if (prop->pflags & QScriptValue::ReadOnly)
        return;
    if (!value.isValid()) // deleteProperty() requested
        removeCustomProperty(name);
    else
        prop->value = value;
}

QScriptValue::PropertyFlags TestClass::propertyFlags(
    const QScriptValue &object, const QScriptString &name, uint id)
{
    m_lastPropertyFlagsObject = object;
    m_lastPropertyFlagsName = name;
    m_lastPropertyFlagsId = id;
    CustomProperty *prop = findCustomProperty(name);
    if (!prop)
        return {};
    return prop->pflags;
}

QScriptClassPropertyIterator *TestClass::newIterator(const QScriptValue &object)
{
    if (!m_iterationEnabled)
        return {};
    return new TestClassPropertyIterator(customProperties, object);
}

QScriptValue TestClass::prototype() const
{
    return m_prototype;
}

QString TestClass::name() const
{
    return QLatin1String("TestClass");
}

bool TestClass::supportsExtension(Extension extension) const
{
    if (extension == Callable)
        return (m_callableMode != NotCallable);
    if (extension == HasInstance)
        return m_hasInstance;
    return false;
}

QVariant TestClass::extension(Extension extension,
                              const QVariant &argument)
{
    m_lastExtensionType = extension;
    m_lastExtensionArgument = argument;
    if (extension == Callable && m_callableMode != NotCallable) {
        QScriptContext *ctx = qvariant_cast<QScriptContext*>(argument);
        if (m_callableMode == CallableReturnsSum) {
            qsreal sum = 0;
            for (int i = 0; i < ctx->argumentCount(); ++i)
                sum += ctx->argument(i).toNumber();
            QScriptValueIterator it(ctx->callee());
            while (it.hasNext()) {
                it.next();
                sum += it.value().toNumber();
            }
            return sum;
        } else if (m_callableMode == CallableReturnsArgument) {
            return QVariant::fromValue(ctx->argument(0));
        } else if (m_callableMode == CallableReturnsInvalidVariant) {
            return QVariant();
        } else if (m_callableMode == CallableReturnsGlobalObject) {
            return QVariant::fromValue(engine()->globalObject());
        } else if (m_callableMode == CallableReturnsThisObject) {
            return QVariant::fromValue(ctx->thisObject());
        } else if (m_callableMode == CallableReturnsCallee) {
            return QVariant::fromValue(ctx->callee());
        } else if (m_callableMode == CallableReturnsArgumentsObject) {
            return QVariant::fromValue(ctx->argumentsObject());
        } else if (m_callableMode == CallableInitializesThisObject) {
            engine()->newQObject(ctx->thisObject(), engine());
            return QVariant();
        }
    } else if (extension == HasInstance && m_hasInstance) {
        QScriptValueList args = qvariant_cast<QScriptValueList>(argument);
        QScriptValue obj = args.at(0);
        QScriptValue value = args.at(1);
        return value.property("foo").equals(obj.property("foo"));
    }
    return QVariant();
}

QScriptValue TestClass::lastQueryPropertyObject() const
{
    return m_lastQueryPropertyObject;
}

QScriptString TestClass::lastQueryPropertyName() const
{
    return m_lastQueryPropertyName;
}

QScriptClass::QueryFlags TestClass::lastQueryPropertyFlags() const
{
    return m_lastQueryPropertyFlags;
}

QScriptValue TestClass::lastPropertyObject() const
{
    return m_lastPropertyObject;
}

QScriptString TestClass::lastPropertyName() const
{
    return m_lastPropertyName;
}

uint TestClass::lastPropertyId() const
{
    return m_lastPropertyId;
}

QScriptValue TestClass::lastSetPropertyObject() const
{
    return m_lastSetPropertyObject;
}

QScriptString TestClass::lastSetPropertyName() const
{
    return m_lastSetPropertyName;
}

uint TestClass::lastSetPropertyId() const
{
    return m_lastSetPropertyId;
}

QScriptValue TestClass::lastSetPropertyValue() const
{
    return m_lastSetPropertyValue;
}

QScriptValue TestClass::lastPropertyFlagsObject() const
{
    return m_lastPropertyFlagsObject;
}

QScriptString TestClass::lastPropertyFlagsName() const
{
    return m_lastPropertyFlagsName;
}

uint TestClass::lastPropertyFlagsId() const
{
    return m_lastPropertyFlagsId;
}

QScriptClass::Extension TestClass::lastExtensionType() const
{
    return m_lastExtensionType;
}

QVariant TestClass::lastExtensionArgument() const
{
    return m_lastExtensionArgument;
}

void TestClass::clearReceivedArgs()
{
    m_lastQueryPropertyObject = QScriptValue();
    m_lastQueryPropertyName = QScriptString();
    m_lastQueryPropertyFlags = {};

    m_lastPropertyObject = QScriptValue();
    m_lastPropertyName = QScriptString();
    m_lastPropertyId = uint(-1);

    m_lastSetPropertyObject = QScriptValue();
    m_lastSetPropertyName = QScriptString();
    m_lastSetPropertyId = uint(-1);
    m_lastSetPropertyValue = QScriptValue();

    m_lastPropertyFlagsObject = QScriptValue();
    m_lastPropertyFlagsName = QScriptString();
    m_lastPropertyFlagsId = uint(-1);

    m_lastExtensionType = static_cast<QScriptClass::Extension>(-1);
    m_lastExtensionArgument = QVariant();
}

void TestClass::setIterationEnabled(bool enable)
{
    m_iterationEnabled = enable;
}

bool TestClass::isIterationEnabled() const
{
    return m_iterationEnabled;
}

void TestClass::setCallableMode(CallableMode mode)
{
    m_callableMode = mode;
}

TestClass::CallableMode TestClass::callableMode() const
{
    return m_callableMode;
}

void TestClass::setHasInstance(bool hasInstance)
{
    m_hasInstance = hasInstance;
}

bool TestClass::hasInstance() const
{
    return m_hasInstance;
}


TestClassPropertyIterator::TestClassPropertyIterator(const QHash<QScriptString, TestClass::CustomProperty*> &props,
                                     const QScriptValue &object)
    : QScriptClassPropertyIterator(object)
{
    m_props = props;
    toFront();
}

TestClassPropertyIterator::~TestClassPropertyIterator()
{
}

bool TestClassPropertyIterator::hasNext() const
{
    return m_index < m_props.size();
}

void TestClassPropertyIterator::next()
{
    m_last = m_index;
    ++m_index;
}

bool TestClassPropertyIterator::hasPrevious() const
{
    return m_index > 0;
}

void TestClassPropertyIterator::previous()
{
    --m_index;
    m_last = m_index;
}

void TestClassPropertyIterator::toFront()
{
    m_index = 0;
    m_last = -1;
}

void TestClassPropertyIterator::toBack()
{
    m_index = m_props.size();
    m_last = -1;
}

QScriptString TestClassPropertyIterator::name() const
{
    return m_props.keys().value(m_last);
}

uint TestClassPropertyIterator::id() const
{
    QScriptString key = m_props.keys().value(m_last);
    if (!key.isValid())
        return 0;
    TestClass::CustomProperty *prop = m_props.value(key);
    return prop->id;
}

QScriptValue::PropertyFlags TestClassPropertyIterator::flags() const
{
    QScriptString key = m_props.keys().value(m_last);
    if (!key.isValid())
        return {};
    TestClass::CustomProperty *prop = m_props.value(key);
    return prop->pflags;
}



void tst_QScriptClass::newInstance()
{
    QScriptEngine eng;

    TestClass cls(&eng);

    QScriptValue obj1 = eng.newObject(&cls);
    QVERIFY(!obj1.data().isValid());
    QVERIFY(obj1.prototype().strictlyEquals(cls.prototype()));
    QEXPECT_FAIL("", "QTBUG-17599: classname is not implemented", Continue);
    QCOMPARE(obj1.toString(), QString::fromLatin1("[object TestClass]"));
    QCOMPARE(obj1.scriptClass(), (QScriptClass*)&cls);

    QScriptValue num(&eng, 456);
    QScriptValue obj2 = eng.newObject(&cls, num);
    QVERIFY(obj2.data().strictlyEquals(num));
    QVERIFY(obj2.prototype().strictlyEquals(cls.prototype()));
    QCOMPARE(obj2.scriptClass(), (QScriptClass*)&cls);
    QVERIFY(!obj2.equals(obj1));
    QVERIFY(!obj2.strictlyEquals(obj1));
}

void tst_QScriptClass::setScriptClassOfExistingObject()
{
    QScriptEngine eng;
    TestClass cls(&eng);
    QScriptValue obj3 = eng.newObject();
    QCOMPARE(obj3.scriptClass(), (QScriptClass*)0);
    obj3.setScriptClass(&cls);
    QCOMPARE(obj3.scriptClass(), (QScriptClass*)&cls);

    obj3.setScriptClass(0);
    QCOMPARE(obj3.scriptClass(), (QScriptClass*)0);
    obj3.setScriptClass(&cls);
    QCOMPARE(obj3.scriptClass(), (QScriptClass*)&cls);

    TestClass cls2(&eng);
    obj3.setScriptClass(&cls2);
    QCOMPARE(obj3.scriptClass(), (QScriptClass*)&cls2);
}

void tst_QScriptClass::setScriptClassOfNonQtScriptObject()
{
    QScriptEngine eng;
    TestClass cls(&eng);
    // undefined behavior really, but shouldn't crash
    QScriptValue arr = eng.newArray();
    QVERIFY(arr.isArray());
    QCOMPARE(arr.scriptClass(), (QScriptClass*)0);
    QTest::ignoreMessage(QtWarningMsg, "QScriptValue::setScriptClass() failed: cannot change class of non-QScriptObject");
    arr.setScriptClass(&cls);
    QEXPECT_FAIL("", "Changing class of arbitrary script object is not allowed (it's OK)", Continue);
    QCOMPARE(arr.scriptClass(), (QScriptClass*)&cls);
    QEXPECT_FAIL("", "Changing class of arbitrary script object is not allowed (it's OK)", Continue);
    QVERIFY(!arr.isArray());
    QVERIFY(arr.isObject());
}

void tst_QScriptClass::getAndSetPropertyFromCpp()
{
    QScriptEngine eng;

    TestClass cls(&eng);

    QScriptValue obj1 = eng.newObject(&cls);
    QScriptValue obj2 = eng.newObject(&cls);
    QScriptString foo = eng.toStringHandle("foo");
    QScriptString bar = eng.toStringHandle("bar");
    QScriptValue num(&eng, 123);

    // Initially our TestClass instances have no custom properties,
    // and queryProperty() will always return false.
    // Hence, the properties will be created as normal JS properties.
    for (int x = 0; x < 2; ++x) {
        QScriptValue &o = (x == 0) ? obj1 : obj2;
        for (int y = 0; y < 2; ++y) {
            QScriptString &s = (y == 0) ? foo : bar;

            // read property
            cls.clearReceivedArgs();
            QScriptValue ret = o.property(s);
            QVERIFY(!ret.isValid());
            QVERIFY(cls.lastQueryPropertyObject().strictlyEquals(o));
            QVERIFY(cls.lastQueryPropertyName() == s);
            QVERIFY(!cls.lastPropertyObject().isValid());
            QVERIFY(!cls.lastSetPropertyObject().isValid());
            QVERIFY(cls.lastQueryPropertyFlags() == QScriptClass::HandlesReadAccess);

            // write property
            cls.clearReceivedArgs();
            o.setProperty(s, num);
            QVERIFY(cls.lastQueryPropertyObject().strictlyEquals(o));
            QVERIFY(cls.lastQueryPropertyName() == s);
            QVERIFY(!cls.lastPropertyObject().isValid());
            QVERIFY(!cls.lastSetPropertyObject().isValid());
            QVERIFY(cls.lastQueryPropertyFlags() == QScriptClass::HandlesWriteAccess);

            // re-read property
            // When a QScriptClass doesn't want to handle a property write,
            // that property becomes a normal property and the QScriptClass
            // shall not be queried about it again.
            cls.clearReceivedArgs();
            QVERIFY(o.property(s).strictlyEquals(num));
            QVERIFY(!cls.lastQueryPropertyObject().isValid());
        }
    }

    // add a custom property
    QScriptString foo2 = eng.toStringHandle("foo2");
    const uint foo2Id = 123;
    const QScriptValue::PropertyFlags foo2Pflags = QScriptValue::Undeletable;
    QScriptValue foo2Value(&eng, 456);
    cls.addCustomProperty(foo2, QScriptClass::HandlesReadAccess | QScriptClass::HandlesWriteAccess,
                          foo2Id, foo2Pflags, foo2Value);

    {
        // read property
        cls.clearReceivedArgs();
        {
            QScriptValue ret = obj1.property(foo2);
            QVERIFY(ret.strictlyEquals(foo2Value));
        }
        QVERIFY(cls.lastQueryPropertyObject().strictlyEquals(obj1));
        QVERIFY(cls.lastQueryPropertyName() == foo2);
        QVERIFY(cls.lastPropertyObject().strictlyEquals(obj1));
        QVERIFY(cls.lastPropertyName() == foo2);
        QCOMPARE(cls.lastPropertyId(), foo2Id);

        // read flags
        cls.clearReceivedArgs();
        QCOMPARE(obj1.propertyFlags(foo2), foo2Pflags);
        QVERIFY(cls.lastQueryPropertyObject().strictlyEquals(obj1));
        QVERIFY(cls.lastQueryPropertyName() == foo2);
        QEXPECT_FAIL("", "QTBUG-17601: classObject.getOwnPropertyDescriptor() reads the property value", Continue);
        QVERIFY(!cls.lastPropertyObject().isValid());
        QVERIFY(cls.lastPropertyFlagsObject().strictlyEquals(obj1));
        QVERIFY(cls.lastPropertyFlagsName() == foo2);
        QCOMPARE(cls.lastPropertyFlagsId(), foo2Id);

        // write property
        cls.clearReceivedArgs();
        QScriptValue newFoo2Value(&eng, 789);
        obj1.setProperty(foo2, newFoo2Value);
        QVERIFY(cls.lastQueryPropertyObject().strictlyEquals(obj1));
        QVERIFY(cls.lastQueryPropertyName() == foo2);

        // read property again
        cls.clearReceivedArgs();
        {
            QScriptValue ret = obj1.property(foo2);
            QVERIFY(ret.strictlyEquals(newFoo2Value));
        }
        QVERIFY(cls.lastQueryPropertyObject().strictlyEquals(obj1));
        QVERIFY(cls.lastQueryPropertyName() == foo2);
        QVERIFY(cls.lastPropertyObject().strictlyEquals(obj1));
        QVERIFY(cls.lastPropertyName() == foo2);
        QCOMPARE(cls.lastPropertyId(), foo2Id);
    }

    // attempt to delete custom property
    obj1.setProperty(foo2, QScriptValue());
    // delete real property
    obj1.setProperty(foo, QScriptValue());
    QVERIFY(!obj1.property(foo).isValid());
    obj1.setProperty(foo, num);
    QVERIFY(obj1.property(foo).equals(num));

    // remove script class; normal properties should remain
    obj1.setScriptClass(0);
    QCOMPARE(obj1.scriptClass(), (QScriptClass*)0);
    QVERIFY(obj1.property(foo).equals(num));
    QVERIFY(obj1.property(bar).equals(num));
    obj1.setProperty(foo, QScriptValue());
    QVERIFY(!obj1.property(foo).isValid());
    obj1.setProperty(bar, QScriptValue());
    QVERIFY(!obj1.property(bar).isValid());
}

void tst_QScriptClass::getAndSetPropertyFromJS()
{
    QScriptEngine eng;
    TestClass cls(&eng);
    cls.addCustomProperty(eng.toStringHandle("x"),
                          QScriptClass::HandlesReadAccess
                          | QScriptClass::HandlesWriteAccess,
                          /*id=*/ 1, QScriptValue::PropertyFlags{}, /*value=*/ 123);
    eng.globalObject().setProperty("o", eng.newObject(&cls));

    // Accessing a custom property
    QCOMPARE(eng.evaluate("o.x").toInt32(), 123);
    QCOMPARE(eng.evaluate("o.x = 456; o.x").toInt32(), 456);

    // Accessing a new JS property
    QVERIFY(eng.evaluate("o.y").isUndefined());
    QCOMPARE(eng.evaluate("o.y = 789; o.y").toInt32(), 789);

    // Deleting custom property
    QVERIFY(eng.evaluate("delete o.x").toBool());
    QVERIFY(eng.evaluate("o.x").isUndefined());

    // Deleting JS property
    QVERIFY(eng.evaluate("delete o.y").toBool());
    QVERIFY(eng.evaluate("o.y").isUndefined());
}

void tst_QScriptClass::deleteUndeletableProperty()
{
    QScriptEngine eng;
    TestClass cls(&eng);
    cls.addCustomProperty(eng.toStringHandle("x"), QScriptClass::HandlesWriteAccess,
                          /*id=*/0, QScriptValue::Undeletable, QScriptValue());
    eng.globalObject().setProperty("o", eng.newObject(&cls));
    QVERIFY(!eng.evaluate("delete o.x").toBool());
}

void tst_QScriptClass::writeReadOnlyProperty()
{
    QScriptEngine eng;
    TestClass cls(&eng);
    cls.addCustomProperty(eng.toStringHandle("x"),
                          QScriptClass::HandlesReadAccess
                          | QScriptClass::HandlesWriteAccess,
                          /*id=*/0, QScriptValue::ReadOnly, 123);
    eng.globalObject().setProperty("o", eng.newObject(&cls));
    // Note that if a property is read-only, the setProperty()
    // reimplementation will still get called; it's up to that
    // function to respect the ReadOnly flag.
    QCOMPARE(eng.evaluate("o.x = 456; o.x").toInt32(), 123);
}

void tst_QScriptClass::writePropertyWithoutWriteAccess()
{
    QScriptEngine eng;
    TestClass cls(&eng);
    cls.addCustomProperty(eng.toStringHandle("x"),
                          QScriptClass::HandlesReadAccess,
                          /*id=*/ 0,  QScriptValue::PropertyFlags{}, 123);
    eng.globalObject().setProperty("o", eng.newObject(&cls));
    QCOMPARE(eng.evaluate("o.x").toInt32(), 123);

    // This will create a JS property on the instance that
    // shadows the custom property.
    // This behavior is not documented. It might be more
    // intuitive to treat a property that only handles read
    // access as a read-only, non-shadowable property.
    QCOMPARE(eng.evaluate("o.x = 456; o.x").toInt32(), 456);

    QVERIFY(eng.evaluate("delete o.x").toBool());
    // Now the custom property is seen again.
    QCOMPARE(eng.evaluate("o.x").toInt32(), 123);
}

void tst_QScriptClass::getProperty_invalidValue()
{
    QScriptEngine eng;
    TestClass cls(&eng);
    cls.addCustomProperty(eng.toStringHandle("foo"), QScriptClass::HandlesReadAccess,
                          /*id=*/0, QScriptValue::ReadOnly, QScriptValue());
    QScriptValue obj = eng.newObject(&cls);

    QVERIFY(obj.property("foo").isUndefined());

    eng.globalObject().setProperty("obj", obj);
    QVERIFY(eng.evaluate("obj.hasOwnProperty('foo'))").toBool());
    // The JS environment expects that a valid value is returned,
    // otherwise we could crash.
    QVERIFY(eng.evaluate("obj.foo").isUndefined());
    QVERIFY(eng.evaluate("obj.foo + ''").isString());
    QVERIFY(eng.evaluate("Object.getOwnPropertyDescriptor(obj, 'foo').value").isUndefined());
    QVERIFY(eng.evaluate("Object.getOwnPropertyDescriptor(obj, 'foo').value +''").isString());
}

void tst_QScriptClass::enumerate()
{
    QScriptEngine eng;

    TestClass cls(&eng);

    QScriptValue obj = eng.newObject(&cls);
    QScriptString foo = eng.toStringHandle("foo");
    obj.setProperty(foo, QScriptValue(&eng, 123));

    cls.setIterationEnabled(false);
    {
        QScriptValueIterator it(obj);
        QVERIFY(it.hasNext());
        it.next();
        QVERIFY(it.scriptName() == foo);
        QVERIFY(!it.hasNext());
    }

    // add a custom property
    QScriptString foo2 = eng.toStringHandle("foo2");
    const uint foo2Id = 123;
    const QScriptValue::PropertyFlags foo2Pflags = QScriptValue::Undeletable;
    QScriptValue foo2Value(&eng, 456);
    cls.addCustomProperty(foo2, QScriptClass::HandlesReadAccess | QScriptClass::HandlesWriteAccess,
                          foo2Id, foo2Pflags, QScriptValue());

    cls.setIterationEnabled(true);
    QScriptValueIterator it(obj);
    // This test relies on the order in which properties are enumerated,
    // which we don't guarantee. However, for compatibility's sake we prefer
    // that normal JS properties come before QScriptClass properties.
    for (int x = 0; x < 2; ++x) {
        QVERIFY(it.hasNext());
        it.next();
        QVERIFY(it.scriptName() == foo);
        QVERIFY(it.hasNext());
        it.next();
        QVERIFY(it.scriptName() == foo2);
        QCOMPARE(it.flags(), foo2Pflags);
        QVERIFY(!it.hasNext());

        QVERIFY(it.hasPrevious());
        it.previous();
        QVERIFY(it.scriptName() == foo2);
        QCOMPARE(it.flags(), foo2Pflags);
        QVERIFY(it.hasPrevious());
        it.previous();
        QVERIFY(it.scriptName() == foo);
        QVERIFY(!it.hasPrevious());
    }
}

void tst_QScriptClass::extension_None()
{
    QScriptEngine eng;
    TestClass cls(&eng);
    cls.setCallableMode(TestClass::NotCallable);
    QVERIFY(!cls.supportsExtension(QScriptClass::Callable));
    QVERIFY(!cls.supportsExtension(QScriptClass::HasInstance));
    QScriptValue obj = eng.newObject(&cls);
    QVERIFY(!obj.call().isValid());
    QCOMPARE((int)cls.lastExtensionType(), -1);
    QVERIFY(!obj.instanceOf(obj));
    QCOMPARE((int)cls.lastExtensionType(), -1);
    QVERIFY(!obj.construct().isValid());
}

void tst_QScriptClass::extension_Callable()
{
    QScriptEngine eng;
    TestClass cls(&eng);
    cls.setCallableMode(TestClass::CallableReturnsSum);
    QVERIFY(cls.supportsExtension(QScriptClass::Callable));

    QScriptValue obj = eng.newObject(&cls);
    eng.globalObject().setProperty("obj", obj);
    obj.setProperty("one", QScriptValue(&eng, 1));
    obj.setProperty("two", QScriptValue(&eng, 2));
    obj.setProperty("three", QScriptValue(&eng, 3));
    // From C++
    cls.clearReceivedArgs();
    {
        QScriptValueList args;
        args << QScriptValue(&eng, 4) << QScriptValue(&eng, 5);
        QScriptValue ret = obj.call(obj, args);
        QCOMPARE(cls.lastExtensionType(), QScriptClass::Callable);
        QCOMPARE(cls.lastExtensionArgument().userType(), qMetaTypeId<QScriptContext*>());
        QVERIFY(ret.isNumber());
        QCOMPARE(ret.toNumber(), qsreal(1+2+3+4+5));
    }
    // From JS
    cls.clearReceivedArgs();
    {
        QScriptValue ret = eng.evaluate("obj(4, 5)");
        QCOMPARE(cls.lastExtensionType(), QScriptClass::Callable);
        QCOMPARE(cls.lastExtensionArgument().userType(), qMetaTypeId<QScriptContext*>());
        QVERIFY(ret.isNumber());
        QCOMPARE(ret.toNumber(), qsreal(1+2+3+4+5));
    }

    cls.setCallableMode(TestClass::CallableReturnsArgument);
    // From C++
    cls.clearReceivedArgs();
    {
        QScriptValue ret = obj.call(obj, QScriptValueList() << 123);
        QCOMPARE(cls.lastExtensionType(), QScriptClass::Callable);
        QCOMPARE(cls.lastExtensionArgument().userType(), qMetaTypeId<QScriptContext*>());
        QVERIFY(ret.isNumber());
        QCOMPARE(ret.toInt32(), 123);
    }
    cls.clearReceivedArgs();
    {
        QScriptValue ret = obj.call(obj, QScriptValueList() << true);
        QCOMPARE(cls.lastExtensionType(), QScriptClass::Callable);
        QCOMPARE(cls.lastExtensionArgument().userType(), qMetaTypeId<QScriptContext*>());
        QVERIFY(ret.isBoolean());
        QCOMPARE(ret.toBoolean(), true);
    }
    {
        QScriptValue ret = obj.call(obj, QScriptValueList() << QString::fromLatin1("ciao"));
        QVERIFY(ret.isString());
        QCOMPARE(ret.toString(), QString::fromLatin1("ciao"));
    }
    {
        QScriptValue objobj = eng.newObject();
        QScriptValue ret = obj.call(obj, QScriptValueList() << objobj);
        QVERIFY(ret.isObject());
        QVERIFY(ret.strictlyEquals(objobj));
    }
    {
        QScriptValue ret = obj.call(obj, QScriptValueList() << QScriptValue());
        QVERIFY(ret.isUndefined());
    }
    // From JS
    cls.clearReceivedArgs();
    {
        QScriptValue ret = eng.evaluate("obj(123)");
        QVERIFY(ret.isNumber());
        QCOMPARE(ret.toInt32(), 123);
    }

    cls.setCallableMode(TestClass::CallableReturnsInvalidVariant);
    {
        QScriptValue ret = obj.call(obj);
        QVERIFY(ret.isUndefined());
    }

    cls.setCallableMode(TestClass::CallableReturnsThisObject);
    // From C++
    {
        QScriptValue ret = obj.call(obj);
        QVERIFY(ret.isObject());
        QVERIFY(ret.strictlyEquals(obj));
    }
    // From JS
    {
        QScriptValue ret = eng.evaluate("obj()");
        QVERIFY(ret.isObject());
        QVERIFY(ret.strictlyEquals(eng.globalObject()));
    }

    cls.setCallableMode(TestClass::CallableReturnsCallee);
    // From C++
    {
        QScriptValue ret = obj.call();
        QVERIFY(ret.isObject());
        QVERIFY(ret.strictlyEquals(obj));
    }
    // From JS
    {
        QScriptValue ret = eng.evaluate("obj()");
        QVERIFY(ret.isObject());
        QVERIFY(ret.strictlyEquals(obj));
    }

    cls.setCallableMode(TestClass::CallableReturnsArgumentsObject);
    // From C++
    {
        QScriptValue ret = obj.call(obj, QScriptValueList() << 123);
        QVERIFY(ret.isObject());
        QVERIFY(ret.property("length").isNumber());
        QCOMPARE(ret.property("length").toInt32(), 1);
        QVERIFY(ret.property(0).isNumber());
        QCOMPARE(ret.property(0).toInt32(), 123);
    }
    // From JS
    {
        QScriptValue ret = eng.evaluate("obj(123)");
        QVERIFY(ret.isObject());
        QVERIFY(ret.property("length").isNumber());
        QCOMPARE(ret.property("length").toInt32(), 1);
        QVERIFY(ret.property(0).isNumber());
        QCOMPARE(ret.property(0).toInt32(), 123);
    }
}

void tst_QScriptClass::extension_Callable_construct()
{
    QScriptEngine eng;
    TestClass cls(&eng);
    QScriptValue obj = eng.newObject(&cls);
    eng.globalObject().setProperty("obj", obj);

    // From C++
    cls.clearReceivedArgs();
    cls.setCallableMode(TestClass::CallableReturnsGlobalObject);
    {
        QScriptValue ret = obj.construct();
        QCOMPARE(cls.lastExtensionType(), QScriptClass::Callable);
        QCOMPARE(cls.lastExtensionArgument().userType(), qMetaTypeId<QScriptContext*>());
        QVERIFY(ret.isObject());
        QVERIFY(ret.strictlyEquals(eng.globalObject()));
    }
    // From JS
    cls.clearReceivedArgs();
    {
        QScriptValue ret = eng.evaluate("new obj()");
        QCOMPARE(cls.lastExtensionType(), QScriptClass::Callable);
        QCOMPARE(cls.lastExtensionArgument().userType(), qMetaTypeId<QScriptContext*>());
        QVERIFY(ret.isObject());
        QVERIFY(ret.strictlyEquals(eng.globalObject()));
    }
    // From C++
    cls.clearReceivedArgs();
    cls.setCallableMode(TestClass::CallableInitializesThisObject);
    {
        QScriptValue ret = obj.construct();
        QCOMPARE(cls.lastExtensionType(), QScriptClass::Callable);
        QCOMPARE(cls.lastExtensionArgument().userType(), qMetaTypeId<QScriptContext*>());
        QVERIFY(ret.isQObject());
        QCOMPARE(ret.toQObject(), (QObject*)&eng);
    }
    // From JS
    cls.clearReceivedArgs();
    {
        QScriptValue ret = eng.evaluate("new obj()");
        QCOMPARE(cls.lastExtensionType(), QScriptClass::Callable);
        QCOMPARE(cls.lastExtensionArgument().userType(), qMetaTypeId<QScriptContext*>());
        QVERIFY(ret.isQObject());
        QCOMPARE(ret.toQObject(), (QObject*)&eng);
    }
}

void tst_QScriptClass::extension_HasInstance()
{
    QScriptEngine eng;
    TestClass cls(&eng);
    cls.setHasInstance(true);
    QVERIFY(cls.supportsExtension(QScriptClass::HasInstance));

    QScriptValue obj = eng.newObject(&cls);
    obj.setProperty("foo", QScriptValue(&eng, 123));
    QScriptValue plain = eng.newObject();
    QVERIFY(!plain.instanceOf(obj));

    eng.globalObject().setProperty("HasInstanceTester", obj);
    eng.globalObject().setProperty("hasInstanceValue", plain);
    cls.clearReceivedArgs();
    {
        QScriptValue ret = eng.evaluate("hasInstanceValue instanceof HasInstanceTester");
        QCOMPARE(cls.lastExtensionType(), QScriptClass::HasInstance);
        QCOMPARE(cls.lastExtensionArgument().userType(), qMetaTypeId<QScriptValueList>());
        QScriptValueList lst = qvariant_cast<QScriptValueList>(cls.lastExtensionArgument());
        QCOMPARE(lst.size(), 2);
        QVERIFY(lst.at(0).strictlyEquals(obj));
        QVERIFY(lst.at(1).strictlyEquals(plain));
        QVERIFY(ret.isBoolean());
        QVERIFY(!ret.toBoolean());
    }

    plain.setProperty("foo", QScriptValue(&eng, 456));
    QVERIFY(!plain.instanceOf(obj));
    {
        QScriptValue ret = eng.evaluate("hasInstanceValue instanceof HasInstanceTester");
        QVERIFY(ret.isBoolean());
        QVERIFY(!ret.toBoolean());
    }

    plain.setProperty("foo", obj.property("foo"));
    QVERIFY(plain.instanceOf(obj));
    {
        QScriptValue ret = eng.evaluate("hasInstanceValue instanceof HasInstanceTester");
        QVERIFY(ret.isBoolean());
        QVERIFY(ret.toBoolean());
    }
}

// tests made to match Qt 4.7 (JSC) behaviour
void tst_QScriptClass::originalProperties1()
{
    QScriptEngine eng;

    QScriptString orig1 = eng.toStringHandle("orig1");
    QScriptString orig2 = eng.toStringHandle("orig2");
    QScriptString orig3 = eng.toStringHandle("orig3");
    QScriptString new1 = eng.toStringHandle("new1");
    QScriptString new2 = eng.toStringHandle("new2");

    {
        TestClass cls1(&eng);
        cls1.addCustomProperty(orig2, QScriptClass::HandlesReadAccess | QScriptClass::HandlesWriteAccess, 1, {}, 89);
        cls1.addCustomProperty(new1, QScriptClass::HandlesReadAccess | QScriptClass::HandlesWriteAccess, 1, {}, "hello");

        TestClass cls2(&eng);
        cls2.addCustomProperty(orig2, QScriptClass::HandlesReadAccess | QScriptClass::HandlesWriteAccess, 1, {}, 59);
        cls2.addCustomProperty(new2, QScriptClass::HandlesReadAccess | QScriptClass::HandlesWriteAccess, 1, {}, "world");

        QScriptValue obj1 = eng.newObject();
        obj1.setProperty(orig1 , 42);
        obj1.setProperty(orig2 , "foo");
        obj1.prototype().setProperty(orig3, "bar");

        QCOMPARE(obj1.property(orig1).toInt32(), 42);
        QCOMPARE(obj1.property(orig2).toString(), QString::fromLatin1("foo"));
        QCOMPARE(obj1.property(orig3).toString(), QString::fromLatin1("bar"));
        QVERIFY(!obj1.property(new1).isValid());
        QVERIFY(!obj1.property(new2).isValid());

        eng.globalObject().setProperty("obj" , obj1);

        obj1.setScriptClass(&cls1);
        QCOMPARE(obj1.property(orig1).toInt32(), 42);
        QCOMPARE(obj1.property(orig2).toString(), QString::fromLatin1("foo"));
        QCOMPARE(obj1.property(orig3).toString(), QString::fromLatin1("bar"));
        QCOMPARE(obj1.property(new1).toString(), QString::fromLatin1("hello"));
        QVERIFY(!obj1.property(new2).isValid());

        QScriptValue obj2 = eng.evaluate("obj");
        QCOMPARE(obj2.scriptClass(), &cls1);
        QCOMPARE(obj2.property(orig1).toInt32(), 42);
        QCOMPARE(obj2.property(orig2).toString(), QString::fromLatin1("foo"));
        QCOMPARE(obj2.property(orig3).toString(), QString::fromLatin1("bar"));
        QCOMPARE(obj2.property(new1).toString(), QString::fromLatin1("hello"));
        QVERIFY(!obj2.property(new2).isValid());

        obj1.setScriptClass(&cls2);
        QCOMPARE(obj1.property(orig1).toInt32(), 42);
        QCOMPARE(obj1.property(orig2).toString(), QString::fromLatin1("foo"));
        QCOMPARE(obj1.property(orig3).toString(), QString::fromLatin1("bar"));
        QVERIFY(!obj1.property(new1).isValid());
        QCOMPARE(obj1.property(new2).toString(), QString::fromLatin1("world"));

        QCOMPARE(obj2.scriptClass(), &cls2);
        QCOMPARE(obj2.property(orig1).toInt32(), 42);
        QCOMPARE(obj2.property(orig2).toString(), QString::fromLatin1("foo"));
        QCOMPARE(obj2.property(orig3).toString(), QString::fromLatin1("bar"));
        QVERIFY(!obj2.property(new1).isValid());
        QCOMPARE(obj2.property(new2).toString(), QString::fromLatin1("world"));

        obj1.setScriptClass(0);
        QCOMPARE(obj1.property(orig1).toInt32(), 42);
        QCOMPARE(obj1.property(orig2).toString(), QString::fromLatin1("foo"));
        QCOMPARE(obj1.property(orig3).toString(), QString::fromLatin1("bar"));
        QVERIFY(!obj1.property(new1).isValid());
        QVERIFY(!obj1.property(new2).isValid());
    }
}

void tst_QScriptClass::originalProperties2()
{
    QScriptEngine eng;

    QScriptString orig1 = eng.toStringHandle("orig1");
    QScriptString orig2 = eng.toStringHandle("orig2");
    QScriptString orig3 = eng.toStringHandle("orig3");
    QScriptString new1 = eng.toStringHandle("new1");
    QScriptString new2 = eng.toStringHandle("new2");

    {
        TestClass cls1(&eng);
        cls1.addCustomProperty(orig2, QScriptClass::HandlesReadAccess | QScriptClass::HandlesWriteAccess, 1, {}, 89);
        cls1.addCustomProperty(new1, QScriptClass::HandlesReadAccess | QScriptClass::HandlesWriteAccess, 1, {}, "hello");

        TestClass cls2(&eng);
        cls2.addCustomProperty(orig2, QScriptClass::HandlesReadAccess | QScriptClass::HandlesWriteAccess, 1, {}, 59);
        cls2.addCustomProperty(new2, QScriptClass::HandlesReadAccess | QScriptClass::HandlesWriteAccess, 1, {}, "world");

        QScriptValue obj1 = eng.newObject();
        obj1.setProperty(orig1 , 42);
        obj1.setProperty(orig2 , "foo");
        obj1.prototype().setProperty(orig3, "bar");

        QCOMPARE(obj1.property(orig1).toInt32(), 42);
        QCOMPARE(obj1.property(orig2).toString(), QString::fromLatin1("foo"));
        QCOMPARE(obj1.property(orig3).toString(), QString::fromLatin1("bar"));
        QVERIFY(!obj1.property(new1).isValid());
        QVERIFY(!obj1.property(new2).isValid());

        obj1.setScriptClass(&cls1);
        obj1.setProperty(orig1 , QScriptValue(&eng, 852));
        obj1.setProperty(orig2 , "oli");
        obj1.setProperty(orig3 , "fu*c");
        obj1.setProperty(new1 , "moo");
        obj1.setProperty(new2 , "allo?");
        QCOMPARE(obj1.property(orig1).toInt32(), 852);
        QCOMPARE(obj1.property(orig2).toString(), QString::fromLatin1("foo"));
        QCOMPARE(obj1.property(orig3).toString(), QString::fromLatin1("fu*c"));
        QCOMPARE(obj1.property(new1).toString(), QString::fromLatin1("moo"));
        QCOMPARE(obj1.property(new2).toString(), QString::fromLatin1("allo?"));

        obj1.setScriptClass(&cls2);
        QCOMPARE(obj1.property(orig1).toInt32(), 852);
        QCOMPARE(obj1.property(orig2).toString(), QString::fromLatin1("foo"));
        QCOMPARE(obj1.property(orig3).toString(), QString::fromLatin1("fu*c"));
        QVERIFY(!obj1.property(new1).isValid());
        QCOMPARE(obj1.property(new2).toString(), QString::fromLatin1("allo?"));

        obj1.setScriptClass(0);
        QCOMPARE(obj1.property(orig1).toInt32(), 852);
        QCOMPARE(obj1.property(orig2).toString(), QString::fromLatin1("foo"));
        QCOMPARE(obj1.property(orig3).toString(), QString::fromLatin1("fu*c"));
        QVERIFY(!obj1.property(new1).isValid());
        QCOMPARE(obj1.property(new2).toString(), QString::fromLatin1("allo?"));
    }
}

void tst_QScriptClass::originalProperties3()
{
    QScriptEngine eng;

    QScriptString orig1 = eng.toStringHandle("orig1");
    QScriptString orig2 = eng.toStringHandle("orig2");
    QScriptString orig3 = eng.toStringHandle("orig3");
    QScriptString new1 = eng.toStringHandle("new1");
    QScriptString new2 = eng.toStringHandle("new2");

    {
        TestClass cls1(&eng);
        cls1.addCustomProperty(orig2, QScriptClass::HandlesReadAccess | QScriptClass::HandlesWriteAccess, 1, {}, 89);
        cls1.addCustomProperty(new1, QScriptClass::HandlesReadAccess | QScriptClass::HandlesWriteAccess, 1, {}, "hello");

        TestClass cls2(&eng);
        cls2.addCustomProperty(orig2, QScriptClass::HandlesReadAccess | QScriptClass::HandlesWriteAccess, 1, {}, 59);
        cls2.addCustomProperty(new2, QScriptClass::HandlesReadAccess | QScriptClass::HandlesWriteAccess, 1, {}, "world");

        QScriptValue obj1 = eng.newObject(&cls1);
        QVERIFY(!obj1.property(orig1).isValid());
        QCOMPARE(obj1.property(orig2).toInt32(), 89);
        QCOMPARE(obj1.property(new1).toString(), QString::fromLatin1("hello"));
        QVERIFY(!obj1.property(new2).isValid());
        obj1.setProperty(orig1, 42);
        QCOMPARE(obj1.property(orig1).toInt32(), 42);

        eng.globalObject().setProperty("obj" , obj1);
        obj1.setScriptClass(&cls2);
        QCOMPARE(obj1.property(orig1).toInt32(), 42);
        QCOMPARE(obj1.property(orig2).toInt32(), 59);
        QVERIFY(!obj1.property(new1).isValid());
        QCOMPARE(obj1.property(new2).toString(), QString::fromLatin1("world"));

        QScriptValue obj2 = eng.evaluate("obj");
        QCOMPARE(obj2.scriptClass(), &cls2);
        QCOMPARE(obj2.property(orig1).toInt32(), 42);
        QCOMPARE(obj2.property(orig2).toInt32(), 59);
        QVERIFY(!obj2.property(new1).isValid());
        QCOMPARE(obj2.property(new2).toString(), QString::fromLatin1("world"));

        obj1.setScriptClass(0);
        QCOMPARE(obj1.property(orig1).toInt32(), 42);
        QVERIFY(!obj1.property(orig2).isValid());
        QVERIFY(!obj1.property(new1).isValid());
        QVERIFY(!obj1.property(new2).isValid());

        QCOMPARE(obj2.scriptClass(), (QScriptClass *)0);
        QCOMPARE(obj2.property(orig1).toInt32(), 42);
        QVERIFY(!obj2.property(orig2).isValid());
        QVERIFY(!obj2.property(new1).isValid());
        QVERIFY(!obj2.property(new2).isValid());
    }
}

void tst_QScriptClass::originalProperties4()
{
    QScriptEngine eng;

    QScriptString orig1 = eng.toStringHandle("orig1");
    QScriptString orig2 = eng.toStringHandle("orig2");
    QScriptString orig3 = eng.toStringHandle("orig3");
    QScriptString new1 = eng.toStringHandle("new1");
    QScriptString new2 = eng.toStringHandle("new2");

    {
        TestClass cls1(&eng);
        cls1.addCustomProperty(orig2, QScriptClass::HandlesReadAccess | QScriptClass::HandlesWriteAccess, 1, {}, 89);
        cls1.addCustomProperty(new1, QScriptClass::HandlesReadAccess | QScriptClass::HandlesWriteAccess, 1, {}, "hello");

        TestClass cls2(&eng);
        cls2.addCustomProperty(orig2, QScriptClass::HandlesReadAccess | QScriptClass::HandlesWriteAccess, 1, {}, 59);
        cls2.addCustomProperty(new2, QScriptClass::HandlesReadAccess | QScriptClass::HandlesWriteAccess, 1, {}, "world");

        QScriptValue obj1 = eng.newObject(&cls1);
        QVERIFY(!obj1.property(orig1).isValid());
        QCOMPARE(obj1.property(orig2).toInt32(), 89);
        QCOMPARE(obj1.property(new1).toString(), QString::fromLatin1("hello"));
        QVERIFY(!obj1.property(new2).isValid());

        eng.globalObject().setProperty("obj" , obj1);

        obj1.setScriptClass(0);
        QVERIFY(obj1.isObject());
        QVERIFY(!obj1.property(orig1).isValid());
        QVERIFY(!obj1.property(orig2).isValid());
        QVERIFY(!obj1.property(new1).isValid());
        QVERIFY(!obj1.property(new2).isValid());
        obj1.setProperty(orig1, 42);
        QCOMPARE(obj1.property(orig1).toInt32(), 42);

        QScriptValue obj2 = eng.evaluate("obj");
        QCOMPARE(obj2.scriptClass(), (QScriptClass *)0);
        QVERIFY(obj2.isObject());
        QCOMPARE(obj2.property(orig1).toInt32(), 42);
        QVERIFY(!obj2.property(orig2).isValid());
        QVERIFY(!obj2.property(new1).isValid());
        QVERIFY(!obj2.property(new2).isValid());

        obj1.setScriptClass(&cls2);
        QCOMPARE(obj1.property(orig1).toInt32(), 42);
        QCOMPARE(obj1.property(orig2).toInt32(), 59);
        QVERIFY(!obj1.property(new1).isValid());
        QCOMPARE(obj1.property(new2).toString(), QString::fromLatin1("world"));

        QCOMPARE(obj2.scriptClass(), (QScriptClass *)(&cls2));
        QCOMPARE(obj2.property(orig1).toInt32(), 42);
        QCOMPARE(obj2.property(orig2).toInt32(), 59);
        QVERIFY(!obj2.property(new1).isValid());
        QCOMPARE(obj2.property(new2).toString(), QString::fromLatin1("world"));
    }
}

void tst_QScriptClass::defaultImplementations()
{
    QScriptEngine eng;

    QScriptClass defaultClass(&eng);
    QCOMPARE(defaultClass.engine(), &eng);
    QVERIFY(!defaultClass.prototype().isValid());
    QCOMPARE(defaultClass.name(), QString());

    QScriptValue obj = eng.newObject(&defaultClass);
    QCOMPARE(obj.scriptClass(), &defaultClass);

    QScriptString name = eng.toStringHandle("foo");
    uint id = -1;
    QCOMPARE(defaultClass.queryProperty(obj, name, QScriptClass::HandlesReadAccess, &id), QScriptClass::QueryFlags{});
    QVERIFY(!defaultClass.property(obj, name, id).isValid());
    QCOMPARE(defaultClass.propertyFlags(obj, name, id), QScriptValue::PropertyFlags{});
    defaultClass.setProperty(obj, name, id, 123);
    QVERIFY(!obj.property(name).isValid());

    QCOMPARE(defaultClass.newIterator(obj), (QScriptClassPropertyIterator*)0);

    QVERIFY(!defaultClass.supportsExtension(QScriptClass::Callable));
    QVERIFY(!defaultClass.supportsExtension(QScriptClass::HasInstance));
    QVERIFY(!defaultClass.extension(QScriptClass::Callable).isValid());
    QVERIFY(!defaultClass.extension(QScriptClass::HasInstance).isValid());
}

void tst_QScriptClass::scriptClassObjectInPrototype()
{
    QScriptEngine eng;
    TestClass cls(&eng);
    QScriptValue plainObject = eng.newObject();
    QScriptValue classObject = eng.newObject(&cls);
    plainObject.setPrototype(classObject);
    QVERIFY(plainObject.prototype().equals(classObject));
    eng.globalObject().setProperty("plainObject", plainObject);
    eng.globalObject().setProperty("classObject", classObject);

    QScriptString name = eng.toStringHandle("x");
    cls.addCustomProperty(name, QScriptClass::HandlesReadAccess, /*id=*/ 1,
                          QScriptValue::PropertyFlags{}, /*value=*/ 123);
    QVERIFY(plainObject.property(name).equals(classObject.property(name)));
    QVERIFY(eng.evaluate("plainObject.x == classObject.x").toBool());

    // Add a property that shadows the one in the script class.
    plainObject.setProperty(name, 456);
    QVERIFY(!plainObject.property(name).equals(classObject.property(name)));
    QVERIFY(eng.evaluate("plainObject.x != classObject.x").toBool());

    QVERIFY(eng.evaluate("delete plainObject.x").toBool());
    QVERIFY(eng.evaluate("plainObject.x == classObject.x").toBool());
}

void tst_QScriptClass::scriptClassWithNullEngine()
{
    QScriptClass cls(0);
    QCOMPARE(cls.engine(), (QScriptEngine*)0);
    QScriptEngine eng;
    QScriptValue obj = eng.newObject(&cls);
    QVERIFY(obj.isObject());
    QCOMPARE(obj.scriptClass(), &cls);
    // The class could have been "bound" to the engine at this point,
    // but it's currently not.
    // This behavior is not documented and is subject to change.
    QCOMPARE(cls.engine(), (QScriptEngine*)0);
    // The engine pointer stored in the QScriptClass is not actually used
    // during property access, so this still works.
    obj.setProperty("x", 123);
    QVERIFY(obj.property("x").isNumber());
}

void tst_QScriptClass::scriptClassInOtherEngine()
{
    QScriptEngine eng;
    TestClass cls(&eng);
    QScriptEngine eng2;
    // We don't check that the class is associated with another engine, so
    // we only get a warning when trying to set the prototype of the new
    // instance.
    // This behavior is not documented and is subject to change.
    QTest::ignoreMessage(QtWarningMsg, "QScriptValue::setPrototype() failed: cannot set a prototype created in a different engine");
    QScriptValue obj = eng2.newObject(&cls);
    QVERIFY(obj.isObject());
    QCOMPARE(obj.scriptClass(), &cls);

    obj.setProperty("x", 123);
    QVERIFY(obj.property("x").isNumber());
}

QTEST_MAIN(tst_QScriptClass)
#include "tst_qscriptclass.moc"
