// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include <QtTest/QtTest>
#include <QtQml/QtQml>
#include <QtQuickTestUtils/private/qmlutils_p.h>

Q_DECLARE_METATYPE(QJsonValue::Type)

class JsonPropertyObject : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QJsonValue value READ value WRITE setValue)
    Q_PROPERTY(QJsonObject object READ object WRITE setObject)
    Q_PROPERTY(QJsonArray array READ array WRITE setArray)
public:
    QJsonValue value() const { return m_value; }
    void setValue(const QJsonValue &v) { m_value = v; }
    QJsonObject object() const { return m_object; }
    void setObject(const QJsonObject &o) { m_object = o; }
    QJsonArray array() const { return m_array; }
    void setArray(const QJsonArray &a) { m_array = a; }

private:
    QJsonValue m_value;
    QJsonObject m_object;
    QJsonArray m_array;
};

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

private slots:
    void cppJsConversion_data();
    void cppJsConversion();

    void readValueProperty_data();
    void readValueProperty();
    void readObjectOrArrayProperty_data();
    void readObjectOrArrayProperty();

    void writeValueProperty_data();
    void writeValueProperty();
    void writeObjectOrArrayProperty_data();
    void writeObjectOrArrayProperty();

    void writeProperty_incompatibleType_data();
    void writeProperty_incompatibleType();

    void writeProperty_javascriptExpression_data();
    void writeProperty_javascriptExpression();

    void cyclicStringify();
    void recursiveStringify();

private:
    QByteArray readAsUtf8(const QString &fileName);
    static QJsonValue valueFromJson(const QByteArray &json);

    void addPrimitiveDataTestFiles();
    void addObjectDataTestFiles();
    void addArrayDataTestFiles();
};

QByteArray tst_qjsonbinding::readAsUtf8(const QString &fileName)
{
    QFile file(testFile(fileName));
    file.open(QIODevice::ReadOnly);
    QTextStream stream(&file);
    return stream.readAll().trimmed().toUtf8();
}

QJsonValue tst_qjsonbinding::valueFromJson(const QByteArray &json)
{
    if (json.isEmpty())
        return QJsonValue(QJsonValue::Undefined);

    QJsonDocument doc = QJsonDocument::fromJson(json);
    if (!doc.isEmpty())
        return doc.isObject() ? QJsonValue(doc.object()) : QJsonValue(doc.array());

    // QJsonDocument::fromJson() only handles objects and arrays...
    // Wrap the JSON inside a dummy object and extract the value.
    QByteArray wrappedJson = "{\"prop\":" + json + '}';
    doc = QJsonDocument::fromJson(wrappedJson);
    Q_ASSERT(doc.isObject());
    return doc.object().value("prop");
}

void tst_qjsonbinding::addPrimitiveDataTestFiles()
{
    QTest::newRow("true") << "true.json";
    QTest::newRow("false") << "false.json";

    QTest::newRow("null") << "null.json";

    QTest::newRow("number.0") << "number.0.json";
    QTest::newRow("number.1") << "number.1.json";

    QTest::newRow("string.0") << "string.0.json";

    QTest::newRow("undefined") << "empty.json";
}

void tst_qjsonbinding::addObjectDataTestFiles()
{
    QTest::newRow("object.0") << "object.0.json";
    QTest::newRow("object.1") << "object.1.json";
    QTest::newRow("object.2") << "object.2.json";
    QTest::newRow("object.3") << "object.3.json";
    QTest::newRow("object.4") << "object.4.json";
}

void tst_qjsonbinding::addArrayDataTestFiles()
{
    QTest::newRow("array.0") << "array.0.json";
    QTest::newRow("array.1") << "array.1.json";
    QTest::newRow("array.2") << "array.2.json";
    QTest::newRow("array.3") << "array.3.json";
    QTest::newRow("array.4") << "array.4.json";
}

void tst_qjsonbinding::cppJsConversion_data()
{
    QTest::addColumn<QString>("fileName");

    addPrimitiveDataTestFiles();
    addObjectDataTestFiles();
    addArrayDataTestFiles();
}

void tst_qjsonbinding::cppJsConversion()
{
    QFETCH(QString, fileName);

    QByteArray json = readAsUtf8(fileName);
    QJsonValue jsonValue = valueFromJson(json);

    QJSEngine eng;
    QJSValue stringify = eng.globalObject().property("JSON").property("stringify");
    QVERIFY(stringify.isCallable());

    {
        QJSValue jsValue = eng.toScriptValue(jsonValue);
        QVERIFY(!jsValue.isVariant());
        switch (jsonValue.type()) {
        case QJsonValue::Null:
            QVERIFY(jsValue.isNull());
            break;
        case QJsonValue::Bool:
            QVERIFY(jsValue.isBool());
            QCOMPARE(jsValue.toBool(), jsonValue.toBool());
            break;
        case QJsonValue::Double:
            QVERIFY(jsValue.isNumber());
            QCOMPARE(jsValue.toNumber(), jsonValue.toDouble());
            break;
        case QJsonValue::String:
            QVERIFY(jsValue.isString());
            QCOMPARE(jsValue.toString(), jsonValue.toString());
            break;
        case QJsonValue::Array:
            QVERIFY(jsValue.isArray());
            break;
        case QJsonValue::Object:
            QVERIFY(jsValue.isObject());
            break;
        case QJsonValue::Undefined:
            QVERIFY(jsValue.isUndefined());
            break;
        }

        if (jsValue.isUndefined()) {
            QVERIFY(json.isEmpty());
        } else {
            QJSValue stringified = stringify.call(QJSValueList() << jsValue);
            QVERIFY(!stringified.isError());
            QCOMPARE(stringified.toString().toUtf8(), json);
        }

        QJsonValue roundtrip = qjsvalue_cast<QJsonValue>(jsValue);
        // Workarounds for QTBUG-25164
        if (jsonValue.isObject() && jsonValue.toObject().isEmpty())
            QVERIFY(roundtrip.isObject() && roundtrip.toObject().isEmpty());
        else if (jsonValue.isArray() && jsonValue.toArray().isEmpty())
            QVERIFY(roundtrip.isArray() && roundtrip.toArray().isEmpty());
        else
            QCOMPARE(roundtrip, jsonValue);
    }

    if (jsonValue.isObject()) {
        QJsonObject jsonObject = jsonValue.toObject();
        QJSValue jsObject = eng.toScriptValue(jsonObject);
        QVERIFY(!jsObject.isVariant());
        QVERIFY(jsObject.isObject());

        QJSValue stringified = stringify.call(QJSValueList() << jsObject);
        QVERIFY(!stringified.isError());
        QCOMPARE(stringified.toString().toUtf8(), json);

        QJsonObject roundtrip = qjsvalue_cast<QJsonObject>(jsObject);
        QCOMPARE(roundtrip, jsonObject);
    } else if (jsonValue.isArray()) {
        QJsonArray jsonArray = jsonValue.toArray();
        QJSValue jsArray = eng.toScriptValue(jsonArray);
        QVERIFY(!jsArray.isVariant());
        QVERIFY(jsArray.isArray());

        QJSValue stringified = stringify.call(QJSValueList() << jsArray);
        QVERIFY(!stringified.isError());
        QCOMPARE(stringified.toString().toUtf8(), json);

        QJsonArray roundtrip = qjsvalue_cast<QJsonArray>(jsArray);
        QCOMPARE(roundtrip, jsonArray);
    }
}

void tst_qjsonbinding::readValueProperty_data()
{
    cppJsConversion_data();
}

void tst_qjsonbinding::readValueProperty()
{
    QFETCH(QString, fileName);

    QByteArray json = readAsUtf8(fileName);
    QJsonValue jsonValue = valueFromJson(json);

    QJSEngine eng;
    JsonPropertyObject obj;
    obj.setValue(jsonValue);
    eng.globalObject().setProperty("obj", eng.newQObject(&obj));
    QJSValue stringified = eng.evaluate(
                "var v = obj.value; (typeof v == 'undefined') ? '' : JSON.stringify(v)");
    QVERIFY(!stringified.isError());
    QCOMPARE(stringified.toString().toUtf8(), json);
}

void tst_qjsonbinding::readObjectOrArrayProperty_data()
{
    QTest::addColumn<QString>("fileName");

    addObjectDataTestFiles();
    addArrayDataTestFiles();
}

void tst_qjsonbinding::readObjectOrArrayProperty()
{
    QFETCH(QString, fileName);

    QByteArray json = readAsUtf8(fileName);
    QJsonValue jsonValue = valueFromJson(json);
    QVERIFY(jsonValue.isObject() || jsonValue.isArray());

    QJSEngine eng;
    JsonPropertyObject obj;
    if (jsonValue.isObject())
        obj.setObject(jsonValue.toObject());
    else
        obj.setArray(jsonValue.toArray());
    eng.globalObject().setProperty("obj", eng.newQObject(&obj));

    QJSValue stringified = eng.evaluate(
                QString::fromLatin1("JSON.stringify(obj.%0)").arg(
                    jsonValue.isObject() ? "object" : "array"));
    QVERIFY(!stringified.isError());
    QCOMPARE(stringified.toString().toUtf8(), json);
}

void tst_qjsonbinding::writeValueProperty_data()
{
    readValueProperty_data();
}

void tst_qjsonbinding::writeValueProperty()
{
    QFETCH(QString, fileName);

    QByteArray json = readAsUtf8(fileName);
    QJsonValue jsonValue = valueFromJson(json);

    QJSEngine eng;
    JsonPropertyObject obj;
    eng.globalObject().setProperty("obj", eng.newQObject(&obj));

    QJSValue fun = eng.evaluate(
                "(function(json) {"
                "  void(obj.value = (json == '') ? undefined : JSON.parse(json));"
                "})");
    QVERIFY(fun.isCallable());

    QVERIFY(obj.value().isNull());
    QVERIFY(fun.call(QJSValueList() << QString::fromUtf8(json)).isUndefined());

    // Workarounds for QTBUG-25164
    if (jsonValue.isObject() && jsonValue.toObject().isEmpty())
        QVERIFY(obj.value().isObject() && obj.value().toObject().isEmpty());
    else if (jsonValue.isArray() && jsonValue.toArray().isEmpty())
        QVERIFY(obj.value().isArray() && obj.value().toArray().isEmpty());
    else
        QCOMPARE(obj.value(), jsonValue);
}

void tst_qjsonbinding::writeObjectOrArrayProperty_data()
{
    readObjectOrArrayProperty_data();
}

void tst_qjsonbinding::writeObjectOrArrayProperty()
{
    QFETCH(QString, fileName);

    QByteArray json = readAsUtf8(fileName);
    QJsonValue jsonValue = valueFromJson(json);
    QVERIFY(jsonValue.isObject() || jsonValue.isArray());

    QJSEngine eng;
    JsonPropertyObject obj;
    eng.globalObject().setProperty("obj", eng.newQObject(&obj));

    QJSValue fun = eng.evaluate(
                QString::fromLatin1(
                    "(function(json) {"
                    "  void(obj.%0 = JSON.parse(json));"
                    "})").arg(jsonValue.isObject() ? "object" : "array")
                );
    QVERIFY(fun.isCallable());

    QVERIFY(obj.object().isEmpty() && obj.array().isEmpty());
    QVERIFY(fun.call(QJSValueList() << QString::fromUtf8(json)).isUndefined());

    if (jsonValue.isObject())
        QCOMPARE(obj.object(), jsonValue.toObject());
    else
        QCOMPARE(obj.array(), jsonValue.toArray());
}

void tst_qjsonbinding::writeProperty_incompatibleType_data()
{
    QTest::addColumn<QString>("property");
    QTest::addColumn<QString>("expression");

    QTest::newRow("value=function") << "value" << "(function(){})";

    QTest::newRow("object=undefined") << "object" << "undefined";
    QTest::newRow("object=null") << "object" << "null";
    QTest::newRow("object=false") << "object" << "false";
    QTest::newRow("object=true") << "object" << "true";
    QTest::newRow("object=123") << "object" << "123";
    QTest::newRow("object=42.35") << "object" << "42.35";
    QTest::newRow("object='foo'") << "object" << "'foo'";
    QTest::newRow("object=[]") << "object" << "[]";
    QTest::newRow("object=function") << "object" << "(function(){})";

    QTest::newRow("array=undefined") << "array" << "undefined";
    QTest::newRow("array=null") << "array" << "null";
    QTest::newRow("array=false") << "array" << "false";
    QTest::newRow("array=true") << "array" << "true";
    QTest::newRow("array=123") << "array" << "123";
    QTest::newRow("array=42.35") << "array" << "42.35";
    QTest::newRow("array='foo'") << "array" << "'foo'";
    QTest::newRow("array={}") << "array" << "{}";
    QTest::newRow("array=function") << "array" << "(function(){})";
}

void tst_qjsonbinding::writeProperty_incompatibleType()
{
    QFETCH(QString, property);
    QFETCH(QString, expression);

    QJSEngine eng;
    JsonPropertyObject obj;
    eng.globalObject().setProperty("obj", eng.newQObject(&obj));

    QJSValue ret = eng.evaluate(QString::fromLatin1("obj.%0 = %1")
                                .arg(property).arg(expression));
    QVERIFY(ret.isError());
    QVERIFY(ret.toString().contains("Cannot assign"));
}

void tst_qjsonbinding::writeProperty_javascriptExpression_data()
{
    QTest::addColumn<QString>("property");
    QTest::addColumn<QString>("expression");
    QTest::addColumn<QString>("expectedJson");

    // Function properties should be omitted.
    QTest::newRow("value = object with function property")
            << "value" << "{ foo: function() {} }" << "{}";
    QTest::newRow("object = object with function property")
            << "object" << "{ foo: function() {} }" << "{}";
    QTest::newRow("array = array with function property")
            << "array" << "[function() {}]" << "[null]";

    // Inherited properties should not be included.
    QTest::newRow("value = object with inherited property")
            << "value" << "{ __proto__: { proto_foo: 123 } }"
            << "{}";
    QTest::newRow("value = object with inherited property 2")
            << "value" << "{ foo: 123, __proto__: { proto_foo: 456 } }"
            << "{\"foo\":123}";
    QTest::newRow("value = array with inherited property")
            << "value" << "(function() { var a = []; a.__proto__ = { proto_foo: 123 }; return a; })()"
            << "[]";
    QTest::newRow("value = array with inherited property 2")
            << "value" << "(function() { var a = [10, 20]; a.__proto__ = { proto_foo: 123 }; return a; })()"
            << "[10,20]";

    QTest::newRow("object = object with inherited property")
            << "object" << "{ __proto__: { proto_foo: 123 } }"
            << "{}";
    QTest::newRow("object = object with inherited property 2")
            << "object" << "{ foo: 123, __proto__: { proto_foo: 456 } }"
            << "{\"foo\":123}";
    QTest::newRow("array = array with inherited property")
            << "array" << "(function() { var a = []; a.__proto__ = { proto_foo: 123 }; return a; })()"
            << "[]";
    QTest::newRow("array = array with inherited property 2")
            << "array" << "(function() { var a = [10, 20]; a.__proto__ = { proto_foo: 123 }; return a; })()"
            << "[10,20]";

    // Non-enumerable properties should not be included.
    QTest::newRow("value = object with non-enumerable property")
            << "value" << "Object.defineProperty({}, 'foo', { value: 123, enumerable: false })"
            << "{}";
    QTest::newRow("object = object with non-enumerable property")
            << "object" << "Object.defineProperty({}, 'foo', { value: 123, enumerable: false })"
            << "{}";

    // Cyclic data structures are permitted, but the cyclic links become
    // empty objects.
    QTest::newRow("value = cyclic object")
            << "value" << "(function() { var o = { foo: 123 }; o.o = o; return o; })()"
            << "{\"foo\":123,\"o\":{}}";
    QTest::newRow("value = cyclic array")
            << "value" << "(function() { var a = [10, 20]; a.push(a); return a; })()"
            << "[10,20,[]]";
    QTest::newRow("object = cyclic object")
            << "object" << "(function() { var o = { bar: true }; o.o = o; return o; })()"
            << "{\"bar\":true,\"o\":{}}";
    QTest::newRow("array = cyclic array")
            << "array" << "(function() { var a = [30, 40]; a.unshift(a); return a; })()"
            << "[[],30,40]";

    // Properties with undefined value are excluded.
    QTest::newRow("value = { foo: undefined }")
            << "value" << "{ foo: undefined }" << "{}";
    QTest::newRow("value = { foo: undefined, bar: 123 }")
            << "value" << "{ foo: undefined, bar: 123 }" << "{\"bar\":123}";
    QTest::newRow("value = { foo: 456, bar: undefined }")
            << "value" << "{ foo: 456, bar: undefined }" << "{\"foo\":456}";

    QTest::newRow("object = { foo: undefined }")
            << "object" << "{ foo: undefined }" << "{}";
    QTest::newRow("object = { foo: undefined, bar: 123 }")
            << "object" << "{ foo: undefined, bar: 123 }" << "{\"bar\":123}";
    QTest::newRow("object = { foo: 456, bar: undefined }")
            << "object" << "{ foo: 456, bar: undefined }" << "{\"foo\":456}";

    // QJsonArray::append() implicitly converts undefined values to null.
    QTest::newRow("value = [undefined]")
            << "value" << "[undefined]" << "[null]";
    QTest::newRow("value = [undefined, 10]")
            << "value" << "[undefined, 10]" << "[null,10]";
    QTest::newRow("value = [10, undefined, 20]")
            << "value" << "[10, undefined, 20]" << "[10,null,20]";

    QTest::newRow("array = [undefined]")
            << "array" << "[undefined]" << "[null]";
    QTest::newRow("array = [undefined, 10]")
            << "array" << "[undefined, 10]" << "[null,10]";
    QTest::newRow("array = [10, undefined, 20]")
            << "array" << "[10, undefined, 20]" << "[10,null,20]";
}

void tst_qjsonbinding::writeProperty_javascriptExpression()
{
    QFETCH(QString, property);
    QFETCH(QString, expression);
    QFETCH(QString, expectedJson);

    QJSEngine eng;
    JsonPropertyObject obj;
    eng.globalObject().setProperty("obj", eng.newQObject(&obj));

    QJSValue ret = eng.evaluate(QString::fromLatin1("obj.%0 = %1; JSON.stringify(obj.%0)")
                                .arg(property).arg(expression));
    QVERIFY(!ret.isError());
    QCOMPARE(ret.toString(), expectedJson);
}

void tst_qjsonbinding::cyclicStringify()
{
    QJSEngine e;
    const QString program = QStringLiteral(R"(
        var a = {};
        a.a = a;
        JSON.stringify(a);
    )");

    QJSValue result = e.evaluate(program);
    QVERIFY(result.isError());
    QCOMPARE(result.errorType(), QJSValue::TypeError);
    QVERIFY(result.toString().contains(QLatin1String("Cannot convert circular structure to JSON")));
}

void tst_qjsonbinding::recursiveStringify()
{
    QJSEngine e;
    const QString program = QStringLiteral(R"(
        function create() {
            var a = {}
            Object.defineProperty(a, "a", {
                enumerable: true,
                get: function() { return create(); }
            });
            return a;
        }

        JSON.stringify(create());
    )");

    QJSValue result = e.evaluate(program);
    QVERIFY(result.isError());
    QCOMPARE(result.errorType(), QJSValue::RangeError);
    QVERIFY(result.toString().contains(QLatin1String("Maximum call stack size exceeded")));
}

QTEST_MAIN(tst_qjsonbinding)

#include "tst_qjsonbinding.moc"
