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


#include <QtTest/QtTest>

#include <QJSEngine>
#include <QJSValue>
#include <QJSValueIterator>

class tst_QJSValueIterator : public QObject
{
    Q_OBJECT

public:
    tst_QJSValueIterator();
    virtual ~tst_QJSValueIterator();

private slots:
    void iterateForward_data();
    void iterateForward();
    void iterateArray_data();
    void iterateArray();
    void iterateString();
#if 0
    void iterateGetterSetter();
#endif
    void assignObjectToIterator();
    void iterateNonObject();
    void iterateOverObjectFromDeletedEngine();
    void iterateWithNext();
};

tst_QJSValueIterator::tst_QJSValueIterator()
{
}

tst_QJSValueIterator::~tst_QJSValueIterator()
{
}

void tst_QJSValueIterator::iterateForward_data()
{
    QTest::addColumn<QStringList>("propertyNames");
    QTest::addColumn<QStringList>("propertyValues");

    QTest::newRow("no properties")
        << QStringList() << QStringList();
    QTest::newRow("foo=bar")
        << (QStringList() << "foo")
        << (QStringList() << "bar");
    QTest::newRow("foo=bar, baz=123")
        << (QStringList() << "foo" << "baz")
        << (QStringList() << "bar" << "123");
    QTest::newRow("foo=bar, baz=123, rab=oof")
        << (QStringList() << "foo" << "baz" << "rab")
        << (QStringList() << "bar" << "123" << "oof");
}

void tst_QJSValueIterator::iterateForward()
{
    QFETCH(QStringList, propertyNames);
    QFETCH(QStringList, propertyValues);
    QMap<QString, QString> pmap;
    QCOMPARE(propertyNames.size(), propertyValues.size());

    QJSEngine engine;
    QJSValue object = engine.newObject();
    for (int i = 0; i < propertyNames.size(); ++i) {
        QString name = propertyNames.at(i);
        QString value = propertyValues.at(i);
        pmap.insert(name, value);
        object.setProperty(name, engine.toScriptValue(value));
    }
    QJSValue otherObject = engine.newObject();
    otherObject.setProperty("foo", engine.toScriptValue(123456));
    otherObject.setProperty("protoProperty", engine.toScriptValue(654321));
    object.setPrototype(otherObject); // should not affect iterator

    QStringList lst;
    QJSValueIterator it(object);
    while (!pmap.isEmpty()) {
        QCOMPARE(it.hasNext(), true);
        QCOMPARE(it.hasNext(), true);
        it.next();
        QString name = it.name();
        QCOMPARE(pmap.contains(name), true);
        QCOMPARE(it.name(), name);
        QCOMPARE(it.value().strictlyEquals(engine.toScriptValue(pmap.value(name))), true);
        pmap.remove(name);
        lst.append(name);
    }

    QCOMPARE(it.hasNext(), false);
    QCOMPARE(it.hasNext(), false);

    it = object;
    for (int i = 0; i < lst.size(); ++i) {
        QCOMPARE(it.hasNext(), true);
        it.next();
        QCOMPARE(it.name(), lst.at(i));
    }
}

void tst_QJSValueIterator::iterateArray_data()
{
    QTest::addColumn<QStringList>("propertyNames");
    QTest::addColumn<QStringList>("propertyValues");

    QTest::newRow("no elements") << QStringList() << QStringList();

    QTest::newRow("0=foo, 1=barr")
        << (QStringList() << "0" << "1")
        << (QStringList() << "foo" << "bar");


    QTest::newRow("0=foo, 3=barr")
        << (QStringList() << "0" << "1" << "2" << "3")
        << (QStringList() << "foo" << "" << "" << "bar");
}

void tst_QJSValueIterator::iterateArray()
{
    QFETCH(QStringList, propertyNames);
    QFETCH(QStringList, propertyValues);

    QJSEngine engine;
    QJSValue array = engine.newArray();

    // Fill the array
    for (int i = 0; i < propertyNames.size(); ++i) {
        array.setProperty(propertyNames.at(i), propertyValues.at(i));
    }

    // Iterate thru array properties. Note that the QJSValueIterator doesn't guarantee
    // any order on the iteration!
    int length = array.property("length").toInt();
    QCOMPARE(length, propertyNames.size());

    bool iteratedThruLength = false;
    QHash<QString, QJSValue> arrayProperties;
    QJSValueIterator it(array);

    // Iterate forward
    while (it.hasNext()) {
        it.next();

        const QString name = it.name();
        if (name == QString::fromLatin1("length")) {
            QVERIFY(it.value().isNumber());
            QCOMPARE(it.value().toInt(), length);
            QVERIFY2(!iteratedThruLength, "'length' appeared more than once during iteration.");
            iteratedThruLength = true;
            continue;
        }

        // Storing the properties we iterate in a hash to compare with test data.
        QVERIFY2(!arrayProperties.contains(name), "property appeared more than once during iteration.");
        arrayProperties.insert(name, it.value());
        QVERIFY(it.value().strictlyEquals(array.property(name)));
    }

    // Verify properties
    QVERIFY(iteratedThruLength);
    QCOMPARE(arrayProperties.size(), propertyNames.size());
    for (int i = 0; i < propertyNames.size(); ++i) {
        QVERIFY(arrayProperties.contains(propertyNames.at(i)));
        QCOMPARE(arrayProperties.value(propertyNames.at(i)).toString(), propertyValues.at(i));
    }

#if 0

    // Iterate backwards
    arrayProperties.clear();
    iteratedThruLength = false;
    it.toBack();

    while (it.hasPrevious()) {
        it.previous();

        const QString name = it.name();
        if (name == QString::fromLatin1("length")) {
            QVERIFY(it.value().isNumber());
            QCOMPARE(it.value().toInt(), length);
            QCOMPARE(it.flags(), QScriptValue::SkipInEnumeration | QScriptValue::Undeletable);
            QVERIFY2(!iteratedThruLength, "'length' appeared more than once during iteration.");
            iteratedThruLength = true;
            continue;
        }

        // Storing the properties we iterate in a hash to compare with test data.
        QVERIFY2(!arrayProperties.contains(name), "property appeared more than once during iteration.");
        arrayProperties.insert(name, it.value());
        QCOMPARE(it.flags(), array.propertyFlags(name));
        QVERIFY(it.value().strictlyEquals(array.property(name)));
    }

    // Verify properties
    QVERIFY(iteratedThruLength);
    QCOMPARE(arrayProperties.size(), propertyNames.size());
    for (int i = 0; i < propertyNames.size(); ++i) {
        QVERIFY(arrayProperties.contains(propertyNames.at(i)));
        QCOMPARE(arrayProperties.value(propertyNames.at(i)).toString(), propertyValues.at(i));
    }

    // ### Do we still need this test?
    // Forward test again but as object
    arrayProperties.clear();
    iteratedThruLength = false;
    QJSValue arrayObject = engine.toObject(array);
    QJSValueIterator it2(arrayObject);

    while (it2.hasNext()) {
        it2.next();

        const QString name = it2.name();
        if (name == QString::fromLatin1("length")) {
            QVERIFY(it2.value().isNumber());
            QCOMPARE(it2.value().toInt(), length);
            QCOMPARE(it2.flags(), QScriptValue::SkipInEnumeration | QScriptValue::Undeletable);
            QVERIFY2(!iteratedThruLength, "'length' appeared more than once during iteration.");
            iteratedThruLength = true;
            continue;
        }

        // Storing the properties we iterate in a hash to compare with test data.
        QVERIFY2(!arrayProperties.contains(name), "property appeared more than once during iteration.");
        arrayProperties.insert(name, it2.value());
        QCOMPARE(it2.flags(), arrayObject.propertyFlags(name));
        QVERIFY(it2.value().strictlyEquals(arrayObject.property(name)));
    }

    // Verify properties
    QVERIFY(iteratedThruLength);
    QCOMPARE(arrayProperties.size(), propertyNames.size());
    for (int i = 0; i < propertyNames.size(); ++i) {
        QVERIFY(arrayProperties.contains(propertyNames.at(i)));
        QCOMPARE(arrayProperties.value(propertyNames.at(i)).toString(), propertyValues.at(i));
    }
#endif
}

void tst_QJSValueIterator::iterateString()
{
    QJSEngine engine;
    QJSValue obj = engine.evaluate("new String('ciao')");
    QVERIFY(obj.property("length").isNumber());
    int length = obj.property("length").toInt();
    QCOMPARE(length, 4);

    QJSValueIterator it(obj);
    QHash<QString, QJSValue> stringProperties;
    bool iteratedThruLength = false;

    while (it.hasNext()) {
        it.next();
        const QString name = it.name();

        if (name == QString::fromLatin1("length")) {
            QVERIFY(it.value().isNumber());
            QCOMPARE(it.value().toInt(), length);
            QVERIFY2(!iteratedThruLength, "'length' appeared more than once during iteration.");
            iteratedThruLength = true;
            continue;
        }

        QVERIFY2(!stringProperties.contains(name), "property appeared more than once during iteration.");
        stringProperties.insert(name, it.value());
        QVERIFY(it.value().strictlyEquals(obj.property(name)));
    }

    QVERIFY(iteratedThruLength);
    QCOMPARE(stringProperties.size(), length);
#if 0
    // And going backwards
    iteratedThruLength = false;
    stringProperties.clear();
    it.toBack();

    while (it.hasPrevious()) {
        it.previous();
        const QString name = it.name();

        if (name == QString::fromLatin1("length")) {
            QVERIFY(it.value().isNumber());
            QCOMPARE(it.value().toInt(), length);
            QVERIFY2(!iteratedThruLength, "'length' appeared more than once during iteration.");
            iteratedThruLength = true;
            continue;
        }

        QVERIFY2(!stringProperties.contains(name), "property appeared more than once during iteration.");
        stringProperties.insert(name, it.value());
        QVERIFY(it.value().strictlyEquals(obj.property(name)));
    }
#endif
}

#if 0 // FIXME what we should to keep from here?
static QJSValue myGetterSetter(QScriptContext *ctx, QJSEngine *)
{
    if (ctx->argumentCount() == 1)
        ctx->thisObject().setProperty("bar", ctx->argument(0));
    return ctx->thisObject().property("bar");
}

static QJSValue myGetter(QScriptContext *ctx, QJSEngine *)
{
    return ctx->thisObject().property("bar");
}

static QJSValue mySetter(QScriptContext *ctx, QJSEngine *)
{
    ctx->thisObject().setProperty("bar", ctx->argument(0));
    return ctx->argument(0);
}

void tst_QJSValueIterator::iterateGetterSetter()
{
    // unified getter/setter function
    {
        QJSEngine eng;
        QJSValue obj = eng.newObject();
        obj.setProperty("foo", eng.newFunction(myGetterSetter),
                        QScriptValue::PropertyGetter | QScriptValue::PropertySetter);
        QJSValue val(&eng, 123);
        obj.setProperty("foo", val);
        QVERIFY(obj.property("bar").strictlyEquals(val));
        QVERIFY(obj.property("foo").strictlyEquals(val));

        QJSValueIterator it(obj);
        QVERIFY(it.hasNext());
        it.next();
        QCOMPARE(it.name(), QString::fromLatin1("foo"));
        QCOMPARE(it.flags(), QScriptValue::PropertyFlags(QScriptValue::PropertyGetter | QScriptValue::PropertySetter));
        QVERIFY(it.value().strictlyEquals(val));
        QJSValue val2(&eng, 456);
        it.setValue(val2);
        QVERIFY(obj.property("bar").strictlyEquals(val2));
        QVERIFY(obj.property("foo").strictlyEquals(val2));

        QVERIFY(it.hasNext());
        it.next();
        QCOMPARE(it.name(), QString::fromLatin1("bar"));
        QVERIFY(!it.hasNext());

        QVERIFY(it.hasPrevious());
        it.previous();
        QCOMPARE(it.name(), QString::fromLatin1("bar"));
        QVERIFY(it.hasPrevious());
        it.previous();
        QCOMPARE(it.name(), QString::fromLatin1("foo"));
        QCOMPARE(it.flags(), QScriptValue::PropertyFlags(QScriptValue::PropertyGetter | QScriptValue::PropertySetter));
        QVERIFY(it.value().strictlyEquals(val2));
        it.setValue(val);
        QVERIFY(obj.property("bar").strictlyEquals(val));
        QVERIFY(obj.property("foo").strictlyEquals(val));
    }
    // separate getter/setter function
    for (int x = 0; x < 2; ++x) {
        QJSEngine eng;
        QJSValue obj = eng.newObject();
        if (x == 0) {
            obj.setProperty("foo", eng.newFunction(myGetter), QScriptValue::PropertyGetter);
            obj.setProperty("foo", eng.newFunction(mySetter), QScriptValue::PropertySetter);
        } else {
            obj.setProperty("foo", eng.newFunction(mySetter), QScriptValue::PropertySetter);
            obj.setProperty("foo", eng.newFunction(myGetter), QScriptValue::PropertyGetter);
        }
        QJSValue val(&eng, 123);
        obj.setProperty("foo", val);
        QVERIFY(obj.property("bar").strictlyEquals(val));
        QVERIFY(obj.property("foo").strictlyEquals(val));

        QJSValueIterator it(obj);
        QVERIFY(it.hasNext());
        it.next();
        QCOMPARE(it.name(), QString::fromLatin1("foo"));
        QVERIFY(it.value().strictlyEquals(val));
        QJSValue val2(&eng, 456);
        it.setValue(val2);
        QVERIFY(obj.property("bar").strictlyEquals(val2));
        QVERIFY(obj.property("foo").strictlyEquals(val2));

        QVERIFY(it.hasNext());
        it.next();
        QCOMPARE(it.name(), QString::fromLatin1("bar"));
        QVERIFY(!it.hasNext());

        QVERIFY(it.hasPrevious());
        it.previous();
        QCOMPARE(it.name(), QString::fromLatin1("bar"));
        QVERIFY(it.hasPrevious());
        it.previous();
        QCOMPARE(it.name(), QString::fromLatin1("foo"));
        QVERIFY(it.value().strictlyEquals(val2));
        it.setValue(val);
        QVERIFY(obj.property("bar").strictlyEquals(val));
        QVERIFY(obj.property("foo").strictlyEquals(val));
    }
}
#endif

void tst_QJSValueIterator::assignObjectToIterator()
{
    QJSEngine eng;
    QJSValue obj1 = eng.newObject();
    obj1.setProperty("foo", 123);
    QJSValue obj2 = eng.newObject();
    obj2.setProperty("bar", 456);

    QJSValueIterator it(obj1);
    QVERIFY(it.hasNext());
    it.next();
    it = obj2;
    QVERIFY(it.hasNext());
    it.next();
    QCOMPARE(it.name(), QString::fromLatin1("bar"));

    it = obj1;
    QVERIFY(it.hasNext());
    it.next();
    QCOMPARE(it.name(), QString::fromLatin1("foo"));

    it = obj2;
    QVERIFY(it.hasNext());
    it.next();
    QCOMPARE(it.name(), QString::fromLatin1("bar"));

    it = obj2;
    QVERIFY(it.hasNext());
    it.next();
    QCOMPARE(it.name(), QString::fromLatin1("bar"));
}

void tst_QJSValueIterator::iterateNonObject()
{
    QJSValueIterator it(123);
    QVERIFY(!it.hasNext());
    it.next();
    it.name();
    it.value();
    QJSValue num(5);
    it = num;
    QVERIFY(!it.hasNext());
}

void tst_QJSValueIterator::iterateOverObjectFromDeletedEngine()
{
    std::unique_ptr<QJSEngine> engine = std::make_unique<QJSEngine>();
    QJSValue objet = engine->newObject();

    // populate object with properties
    QHash<QString, int> properties;
    properties.insert("foo",1235);
    properties.insert("oof",5321);
    properties.insert("ofo",3521);
    QHash<QString, int>::const_iterator i = properties.constBegin();
    for (; i != properties.constEnd(); ++i) {
        objet.setProperty(i.key(), i.value());
    }

    // start iterating
    QJSValueIterator it(objet);
    it.next();
    QVERIFY(properties.contains(it.name()));

    engine.reset();

    QVERIFY(objet.isUndefined());
    QVERIFY(it.name().isEmpty());
    QVERIFY(it.value().isUndefined());

    QVERIFY(!it.hasNext());
    it.next();

    QVERIFY(it.name().isEmpty());
    QVERIFY(it.value().isUndefined());

}

void tst_QJSValueIterator::iterateWithNext()
{
    QJSEngine engine;
    QJSValue value = engine.newObject();
    value.setProperty("one", 1);
    value.setProperty("two", 2);
    value.setProperty("three", 3);

    QStringList list;
    list << QStringLiteral("one") << QStringLiteral("three") << QStringLiteral("two");

    int counter = 0;
    QJSValueIterator it(value);
    QStringList actualList;
    while (it.next()) {
        ++counter;
        actualList << it.name();
    }

    std::sort(actualList.begin(), actualList.end());

    QCOMPARE(counter, 3);
    QCOMPARE(list, actualList);

}

QTEST_MAIN(tst_QJSValueIterator)
#include "tst_qjsvalueiterator.moc"
