/****************************************************************************
**
** Copyright (C) 2017 Klaralvdalens Datakonsult AB (KDAB).
** 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 <QtCore/qbuffer.h>

#include <Qt3DRender/private/qshadergraphloader_p.h>
#include <Qt3DRender/private/qshaderlanguage_p.h>

using namespace Qt3DRender;

using QBufferPointer = QSharedPointer<QBuffer>;
Q_DECLARE_METATYPE(QBufferPointer);

using PrototypeHash = QHash<QString, QShaderNode>;
Q_DECLARE_METATYPE(PrototypeHash);

namespace
{
    QBufferPointer createBuffer(const QByteArray &data, QIODevice::OpenMode openMode = QIODevice::ReadOnly)
    {
        auto buffer = QBufferPointer::create();
        buffer->setData(data);
        if (openMode != QIODevice::NotOpen)
            buffer->open(openMode);
        return buffer;
    }

    QShaderFormat createFormat(QShaderFormat::Api api, int majorVersion, int minorVersion)
    {
        auto format = QShaderFormat();
        format.setApi(api);
        format.setVersion(QVersionNumber(majorVersion, minorVersion));
        return format;
    }

    QShaderNodePort createPort(QShaderNodePort::Direction portDirection, const QString &portName)
    {
        auto port = QShaderNodePort();
        port.direction = portDirection;
        port.name = portName;
        return port;
    }

    QShaderNode createNode(const QVector<QShaderNodePort> &ports, const QStringList &layers = QStringList())
    {
        auto node = QShaderNode();
        node.setUuid(QUuid::createUuid());
        node.setLayers(layers);
        for (const auto &port : ports)
            node.addPort(port);
        return node;
    }

    QShaderGraph::Edge createEdge(const QUuid &sourceUuid, const QString &sourceName,
                                  const QUuid &targetUuid, const QString &targetName,
                                  const QStringList &layers = QStringList())
    {
        auto edge = QShaderGraph::Edge();
        edge.sourceNodeUuid = sourceUuid;
        edge.sourcePortName = sourceName;
        edge.targetNodeUuid = targetUuid;
        edge.targetPortName = targetName;
        edge.layers = layers;
        return edge;
    }

    QShaderGraph createGraph()
    {
        const auto openGLES2 = createFormat(QShaderFormat::OpenGLES, 2, 0);
        const auto openGL3 = createFormat(QShaderFormat::OpenGLCoreProfile, 3, 0);

        auto graph = QShaderGraph();

        auto worldPosition = createNode({
            createPort(QShaderNodePort::Output, "value")
        });
        worldPosition.setUuid(QUuid("{00000000-0000-0000-0000-000000000001}"));
        worldPosition.setParameter("name", "worldPosition");
        worldPosition.setParameter("qualifier", QVariant::fromValue<QShaderLanguage::StorageQualifier>(QShaderLanguage::Input));
        worldPosition.setParameter("type", QVariant::fromValue<QShaderLanguage::VariableType>(QShaderLanguage::Vec3));
        worldPosition.addRule(openGLES2, QShaderNode::Rule("highp $type $value = $name;",
                                                           QByteArrayList() << "$qualifier highp $type $name;"));
        worldPosition.addRule(openGL3, QShaderNode::Rule("$type $value = $name;",
                                                         QByteArrayList() << "$qualifier $type $name;"));

        auto texture = createNode({
            createPort(QShaderNodePort::Output, "texture")
        });
        texture.setUuid(QUuid("{00000000-0000-0000-0000-000000000002}"));
        texture.addRule(openGLES2, QShaderNode::Rule("sampler2D $texture = texture;",
                                                     QByteArrayList() << "uniform sampler2D texture;"));
        texture.addRule(openGL3, QShaderNode::Rule("sampler2D $texture = texture;",
                                                   QByteArrayList() << "uniform sampler2D texture;"));

        auto texCoord = createNode({
            createPort(QShaderNodePort::Output, "texCoord")
        });
        texCoord.setUuid(QUuid("{00000000-0000-0000-0000-000000000003}"));
        texCoord.addRule(openGLES2, QShaderNode::Rule("highp vec2 $texCoord = texCoord;",
                                                      QByteArrayList() << "varying highp vec2 texCoord;"));
        texCoord.addRule(openGL3, QShaderNode::Rule("vec2 $texCoord = texCoord;",
                                                    QByteArrayList() << "in vec2 texCoord;"));

        auto lightIntensity = createNode({
            createPort(QShaderNodePort::Output, "value")
        });
        lightIntensity.setUuid(QUuid("{00000000-0000-0000-0000-000000000004}"));
        lightIntensity.setParameter("name", "defaultName");
        lightIntensity.setParameter("qualifier", QVariant::fromValue<QShaderLanguage::StorageQualifier>(QShaderLanguage::Uniform));
        lightIntensity.setParameter("type", QVariant::fromValue<QShaderLanguage::VariableType>(QShaderLanguage::Float));
        lightIntensity.addRule(openGLES2, QShaderNode::Rule("highp $type $value = $name;",
                                                            QByteArrayList() << "$qualifier highp $type $name;"));
        lightIntensity.addRule(openGL3, QShaderNode::Rule("$type $value = $name;",
                                                          QByteArrayList() << "$qualifier $type $name;"));

        auto exposure = createNode({
            createPort(QShaderNodePort::Output, "exposure")
        });
        exposure.setUuid(QUuid("{00000000-0000-0000-0000-000000000005}"));
        exposure.addRule(openGLES2, QShaderNode::Rule("highp float $exposure = exposure;",
                                                      QByteArrayList() << "uniform highp float exposure;"));
        exposure.addRule(openGL3, QShaderNode::Rule("float $exposure = exposure;",
                                                    QByteArrayList() << "uniform float exposure;"));

        auto fragColor = createNode({
            createPort(QShaderNodePort::Input, "fragColor")
        });
        fragColor.setUuid(QUuid("{00000000-0000-0000-0000-000000000006}"));
        fragColor.addRule(openGLES2, QShaderNode::Rule("gl_fragColor = $fragColor;"));
        fragColor.addRule(openGL3, QShaderNode::Rule("fragColor = $fragColor;",
                                                     QByteArrayList() << "out vec4 fragColor;"));

        auto sampleTexture = createNode({
            createPort(QShaderNodePort::Input, "sampler"),
            createPort(QShaderNodePort::Input, "coord"),
            createPort(QShaderNodePort::Output, "color")
        });
        sampleTexture.setUuid(QUuid("{00000000-0000-0000-0000-000000000007}"));
        sampleTexture.addRule(openGLES2, QShaderNode::Rule("highp vec4 $color = texture2D($sampler, $coord);"));
        sampleTexture.addRule(openGL3, QShaderNode::Rule("vec4 $color = texture2D($sampler, $coord);"));

        auto lightFunction = createNode({
            createPort(QShaderNodePort::Input, "baseColor"),
            createPort(QShaderNodePort::Input, "position"),
            createPort(QShaderNodePort::Input, "lightIntensity"),
            createPort(QShaderNodePort::Output, "outputColor")
        });
        lightFunction.setUuid(QUuid("{00000000-0000-0000-0000-000000000008}"));
        lightFunction.addRule(openGLES2, QShaderNode::Rule("highp vec4 $outputColor = lightModel($baseColor, $position, $lightIntensity);",
                                                           QByteArrayList() << "#pragma include es2/lightmodel.frag.inc"));
        lightFunction.addRule(openGL3, QShaderNode::Rule("vec4 $outputColor = lightModel($baseColor, $position, $lightIntensity);",
                                                         QByteArrayList() << "#pragma include gl3/lightmodel.frag.inc"));

        auto exposureFunction = createNode({
            createPort(QShaderNodePort::Input, "inputColor"),
            createPort(QShaderNodePort::Input, "exposure"),
            createPort(QShaderNodePort::Output, "outputColor")
        });
        exposureFunction.setUuid(QUuid("{00000000-0000-0000-0000-000000000009}"));
        exposureFunction.addRule(openGLES2, QShaderNode::Rule("highp vec4 $outputColor = $inputColor * pow(2.0, $exposure);"));
        exposureFunction.addRule(openGL3, QShaderNode::Rule("vec4 $outputColor = $inputColor * pow(2.0, $exposure);"));

        graph.addNode(worldPosition);
        graph.addNode(texture);
        graph.addNode(texCoord);
        graph.addNode(lightIntensity);
        graph.addNode(exposure);
        graph.addNode(fragColor);
        graph.addNode(sampleTexture);
        graph.addNode(lightFunction);
        graph.addNode(exposureFunction);

        graph.addEdge(createEdge(texture.uuid(), "texture", sampleTexture.uuid(), "sampler"));
        graph.addEdge(createEdge(texCoord.uuid(), "texCoord", sampleTexture.uuid(), "coord"));

        graph.addEdge(createEdge(worldPosition.uuid(), "value", lightFunction.uuid(), "position"));
        graph.addEdge(createEdge(sampleTexture.uuid(), "color", lightFunction.uuid(), "baseColor"));
        graph.addEdge(createEdge(lightIntensity.uuid(), "value", lightFunction.uuid(), "lightIntensity"));

        graph.addEdge(createEdge(lightFunction.uuid(), "outputColor", exposureFunction.uuid(), "inputColor"));
        graph.addEdge(createEdge(exposure.uuid(), "exposure", exposureFunction.uuid(), "exposure"));

        graph.addEdge(createEdge(exposureFunction.uuid(), "outputColor", fragColor.uuid(), "fragColor"));

        return graph;
    }

    void debugStatement(const QString &prefix, const QShaderGraph::Statement &statement)
    {
        qDebug() << prefix << statement.inputs << statement.uuid().toString() << statement.outputs;
    }

    void dumpStatementsIfNeeded(const QVector<QShaderGraph::Statement> &statements, const QVector<QShaderGraph::Statement> &expected)
    {
        if (statements != expected) {
            for (int i = 0; i < qMax(statements.size(), expected.size()); i++) {
                qDebug() << "----" << i << "----";
                if (i < statements.size())
                    debugStatement("A:", statements.at(i));
                if (i < expected.size())
                    debugStatement("E:", expected.at(i));
                qDebug() << "-----------";
            }
        }
    }
}

class tst_QShaderGraphLoader : public QObject
{
    Q_OBJECT
private slots:
    void shouldManipulateLoaderMembers();
    void shouldLoadFromJsonStream_data();
    void shouldLoadFromJsonStream();
};

void tst_QShaderGraphLoader::shouldManipulateLoaderMembers()
{
    // GIVEN
    auto loader = QShaderGraphLoader();

    // THEN (default state)
    QCOMPARE(loader.status(), QShaderGraphLoader::Null);
    QVERIFY(!loader.device());
    QVERIFY(loader.graph().nodes().isEmpty());
    QVERIFY(loader.graph().edges().isEmpty());
    QVERIFY(loader.prototypes().isEmpty());

    // WHEN
    auto device1 = createBuffer(QByteArray("..........."), QIODevice::NotOpen);
    loader.setDevice(device1.data());

    // THEN
    QCOMPARE(loader.status(), QShaderGraphLoader::Error);
    QCOMPARE(loader.device(), device1.data());
    QVERIFY(loader.graph().nodes().isEmpty());
    QVERIFY(loader.graph().edges().isEmpty());

    // WHEN
    auto device2 = createBuffer(QByteArray("..........."), QIODevice::ReadOnly);
    loader.setDevice(device2.data());

    // THEN
    QCOMPARE(loader.status(), QShaderGraphLoader::Waiting);
    QCOMPARE(loader.device(), device2.data());
    QVERIFY(loader.graph().nodes().isEmpty());
    QVERIFY(loader.graph().edges().isEmpty());


    // WHEN
    const auto prototypes = [this]{
        auto res = QHash<QString, QShaderNode>();
        res.insert("foo", createNode({}));
        return res;
    }();
    loader.setPrototypes(prototypes);

    // THEN
    QCOMPARE(loader.prototypes().size(), prototypes.size());
    QVERIFY(loader.prototypes().contains("foo"));
    QCOMPARE(loader.prototypes().value("foo").uuid(), prototypes.value("foo").uuid());
}

void tst_QShaderGraphLoader::shouldLoadFromJsonStream_data()
{
    QTest::addColumn<QBufferPointer>("device");
    QTest::addColumn<PrototypeHash>("prototypes");
    QTest::addColumn<QShaderGraph>("graph");
    QTest::addColumn<QShaderGraphLoader::Status>("status");

    QTest::newRow("empty") << createBuffer("", QIODevice::ReadOnly) << PrototypeHash()
                           << QShaderGraph() << QShaderGraphLoader::Error;

    const auto smallJson = "{"
                           "    \"nodes\": ["
                           "        {"
                           "            \"uuid\": \"{00000000-0000-0000-0000-000000000001}\","
                           "            \"type\": \"MyInput\","
                           "            \"layers\": [\"foo\", \"bar\"]"
                           "        },"
                           "        {"
                           "            \"uuid\": \"{00000000-0000-0000-0000-000000000002}\","
                           "            \"type\": \"MyOutput\""
                           "        },"
                           "        {"
                           "            \"uuid\": \"{00000000-0000-0000-0000-000000000003}\","
                           "            \"type\": \"MyFunction\""
                           "        }"
                           "    ],"
                           "    \"edges\": ["
                           "        {"
                           "            \"sourceUuid\": \"{00000000-0000-0000-0000-000000000001}\","
                           "            \"sourcePort\": \"input\","
                           "            \"targetUuid\": \"{00000000-0000-0000-0000-000000000003}\","
                           "            \"targetPort\": \"functionInput\","
                           "            \"layers\": [\"bar\", \"baz\"]"
                           "        },"
                           "        {"
                           "            \"sourceUuid\": \"{00000000-0000-0000-0000-000000000003}\","
                           "            \"sourcePort\": \"functionOutput\","
                           "            \"targetUuid\": \"{00000000-0000-0000-0000-000000000002}\","
                           "            \"targetPort\": \"output\""
                           "        }"
                           "    ]"
                           "}";

    const auto smallProtos = [this]{
        auto protos = PrototypeHash();

        auto input = createNode({
            createPort(QShaderNodePort::Output, "input")
        });
        protos.insert("MyInput", input);

        auto output = createNode({
            createPort(QShaderNodePort::Input, "output")
        });
        protos.insert("MyOutput", output);

        auto function = createNode({
            createPort(QShaderNodePort::Input, "functionInput"),
            createPort(QShaderNodePort::Output, "functionOutput")
        });
        protos.insert("MyFunction", function);
        return protos;
    }();

    const auto smallGraph = [this]{
        auto graph = QShaderGraph();

        auto input = createNode({
            createPort(QShaderNodePort::Output, "input")
        }, {"foo", "bar"});
        input.setUuid(QUuid("{00000000-0000-0000-0000-000000000001}"));
        auto output = createNode({
            createPort(QShaderNodePort::Input, "output")
        });
        output.setUuid(QUuid("{00000000-0000-0000-0000-000000000002}"));
        auto function = createNode({
            createPort(QShaderNodePort::Input, "functionInput"),
            createPort(QShaderNodePort::Output, "functionOutput")
        });
        function.setUuid(QUuid("{00000000-0000-0000-0000-000000000003}"));

        graph.addNode(input);
        graph.addNode(output);
        graph.addNode(function);
        graph.addEdge(createEdge(input.uuid(), "input", function.uuid(), "functionInput", {"bar", "baz"}));
        graph.addEdge(createEdge(function.uuid(), "functionOutput", output.uuid(), "output"));

        return graph;
    }();

    QTest::newRow("TwoNodesOneEdge") << createBuffer(smallJson) << smallProtos << smallGraph << QShaderGraphLoader::Ready;
    QTest::newRow("NotOpen") << createBuffer(smallJson, QIODevice::NotOpen) << smallProtos << QShaderGraph() << QShaderGraphLoader::Error;
    QTest::newRow("NoPrototype") << createBuffer(smallJson) << PrototypeHash() << QShaderGraph() << QShaderGraphLoader::Error;

    const auto complexJson = "{"
                             "    \"nodes\": ["
                             "        {"
                             "            \"uuid\": \"{00000000-0000-0000-0000-000000000001}\","
                             "            \"type\": \"inputValue\","
                             "            \"parameters\": {"
                             "                \"name\": \"worldPosition\","
                             "                \"qualifier\": {"
                             "                    \"type\": \"QShaderLanguage::StorageQualifier\","
                             "                    \"value\": \"QShaderLanguage::Input\""
                             "                },"
                             "                \"type\": {"
                             "                    \"type\": \"QShaderLanguage::VariableType\","
                             "                    \"value\": \"QShaderLanguage::Vec3\""
                             "                }"
                             "            }"
                             "        },"
                             "        {"
                             "            \"uuid\": \"{00000000-0000-0000-0000-000000000002}\","
                             "            \"type\": \"texture\""
                             "        },"
                             "        {"
                             "            \"uuid\": \"{00000000-0000-0000-0000-000000000003}\","
                             "            \"type\": \"texCoord\""
                             "        },"
                             "        {"
                             "            \"uuid\": \"{00000000-0000-0000-0000-000000000004}\","
                             "            \"type\": \"inputValue\""
                             "        },"
                             "        {"
                             "            \"uuid\": \"{00000000-0000-0000-0000-000000000005}\","
                             "            \"type\": \"exposure\""
                             "        },"
                             "        {"
                             "            \"uuid\": \"{00000000-0000-0000-0000-000000000006}\","
                             "            \"type\": \"fragColor\""
                             "        },"
                             "        {"
                             "            \"uuid\": \"{00000000-0000-0000-0000-000000000007}\","
                             "            \"type\": \"sampleTexture\""
                             "        },"
                             "        {"
                             "            \"uuid\": \"{00000000-0000-0000-0000-000000000008}\","
                             "            \"type\": \"lightModel\""
                             "        },"
                             "        {"
                             "            \"uuid\": \"{00000000-0000-0000-0000-000000000009}\","
                             "            \"type\": \"exposureFunction\""
                             "        }"
                             "    ],"
                             "    \"edges\": ["
                             "        {"
                             "            \"sourceUuid\": \"{00000000-0000-0000-0000-000000000002}\","
                             "            \"sourcePort\": \"texture\","
                             "            \"targetUuid\": \"{00000000-0000-0000-0000-000000000007}\","
                             "            \"targetPort\": \"sampler\""
                             "        },"
                             "        {"
                             "            \"sourceUuid\": \"{00000000-0000-0000-0000-000000000003}\","
                             "            \"sourcePort\": \"texCoord\","
                             "            \"targetUuid\": \"{00000000-0000-0000-0000-000000000007}\","
                             "            \"targetPort\": \"coord\""
                             "        },"
                             "        {"
                             "            \"sourceUuid\": \"{00000000-0000-0000-0000-000000000001}\","
                             "            \"sourcePort\": \"value\","
                             "            \"targetUuid\": \"{00000000-0000-0000-0000-000000000008}\","
                             "            \"targetPort\": \"position\""
                             "        },"
                             "        {"
                             "            \"sourceUuid\": \"{00000000-0000-0000-0000-000000000007}\","
                             "            \"sourcePort\": \"color\","
                             "            \"targetUuid\": \"{00000000-0000-0000-0000-000000000008}\","
                             "            \"targetPort\": \"baseColor\""
                             "        },"
                             "        {"
                             "            \"sourceUuid\": \"{00000000-0000-0000-0000-000000000004}\","
                             "            \"sourcePort\": \"value\","
                             "            \"targetUuid\": \"{00000000-0000-0000-0000-000000000008}\","
                             "            \"targetPort\": \"lightIntensity\""
                             "        },"
                             "        {"
                             "            \"sourceUuid\": \"{00000000-0000-0000-0000-000000000008}\","
                             "            \"sourcePort\": \"outputColor\","
                             "            \"targetUuid\": \"{00000000-0000-0000-0000-000000000009}\","
                             "            \"targetPort\": \"inputColor\""
                             "        },"
                             "        {"
                             "            \"sourceUuid\": \"{00000000-0000-0000-0000-000000000005}\","
                             "            \"sourcePort\": \"exposure\","
                             "            \"targetUuid\": \"{00000000-0000-0000-0000-000000000009}\","
                             "            \"targetPort\": \"exposure\""
                             "        },"
                             "        {"
                             "            \"sourceUuid\": \"{00000000-0000-0000-0000-000000000009}\","
                             "            \"sourcePort\": \"outputColor\","
                             "            \"targetUuid\": \"{00000000-0000-0000-0000-000000000006}\","
                             "            \"targetPort\": \"fragColor\""
                             "        }"
                             "    ]"
                             "}";

    const auto complexProtos = [this]{
        const auto openGLES2 = createFormat(QShaderFormat::OpenGLES, 2, 0);
        const auto openGL3 = createFormat(QShaderFormat::OpenGLCoreProfile, 3, 0);

        auto protos = PrototypeHash();

        auto inputValue = createNode({
            createPort(QShaderNodePort::Output, "value")
        });
        inputValue.setParameter("name", "defaultName");
        inputValue.setParameter("qualifier", QVariant::fromValue<QShaderLanguage::StorageQualifier>(QShaderLanguage::Uniform));
        inputValue.setParameter("type", QVariant::fromValue<QShaderLanguage::VariableType>(QShaderLanguage::Float));
        inputValue.addRule(openGLES2, QShaderNode::Rule("highp $type $value = $name;",
                                                        QByteArrayList() << "$qualifier highp $type $name;"));
        inputValue.addRule(openGL3, QShaderNode::Rule("$type $value = $name;",
                                                      QByteArrayList() << "$qualifier $type $name;"));
        protos.insert("inputValue", inputValue);

        auto texture = createNode({
            createPort(QShaderNodePort::Output, "texture")
        });
        texture.addRule(openGLES2, QShaderNode::Rule("sampler2D $texture = texture;",
                                                     QByteArrayList() << "uniform sampler2D texture;"));
        texture.addRule(openGL3, QShaderNode::Rule("sampler2D $texture = texture;",
                                                   QByteArrayList() << "uniform sampler2D texture;"));
        protos.insert("texture", texture);

        auto texCoord = createNode({
            createPort(QShaderNodePort::Output, "texCoord")
        });
        texCoord.addRule(openGLES2, QShaderNode::Rule("highp vec2 $texCoord = texCoord;",
                                                      QByteArrayList() << "varying highp vec2 texCoord;"));
        texCoord.addRule(openGL3, QShaderNode::Rule("vec2 $texCoord = texCoord;",
                                                    QByteArrayList() << "in vec2 texCoord;"));
        protos.insert("texCoord", texCoord);

        auto exposure = createNode({
            createPort(QShaderNodePort::Output, "exposure")
        });
        exposure.addRule(openGLES2, QShaderNode::Rule("highp float $exposure = exposure;",
                                                      QByteArrayList() << "uniform highp float exposure;"));
        exposure.addRule(openGL3, QShaderNode::Rule("float $exposure = exposure;",
                                                    QByteArrayList() << "uniform float exposure;"));
        protos.insert("exposure", exposure);

        auto fragColor = createNode({
            createPort(QShaderNodePort::Input, "fragColor")
        });
        fragColor.addRule(openGLES2, QShaderNode::Rule("gl_fragColor = $fragColor;"));
        fragColor.addRule(openGL3, QShaderNode::Rule("fragColor = $fragColor;",
                                                     QByteArrayList() << "out vec4 fragColor;"));
        protos.insert("fragColor", fragColor);

        auto sampleTexture = createNode({
            createPort(QShaderNodePort::Input, "sampler"),
            createPort(QShaderNodePort::Input, "coord"),
            createPort(QShaderNodePort::Output, "color")
        });
        sampleTexture.addRule(openGLES2, QShaderNode::Rule("highp vec4 $color = texture2D($sampler, $coord);"));
        sampleTexture.addRule(openGL3, QShaderNode::Rule("vec4 $color = texture2D($sampler, $coord);"));
        protos.insert("sampleTexture", sampleTexture);

        auto lightModel = createNode({
            createPort(QShaderNodePort::Input, "baseColor"),
            createPort(QShaderNodePort::Input, "position"),
            createPort(QShaderNodePort::Input, "lightIntensity"),
            createPort(QShaderNodePort::Output, "outputColor")
        });
        lightModel.addRule(openGLES2, QShaderNode::Rule("highp vec4 $outputColor = lightModel($baseColor, $position, $lightIntensity);",
                                                        QByteArrayList() << "#pragma include es2/lightmodel.frag.inc"));
        lightModel.addRule(openGL3, QShaderNode::Rule("vec4 $outputColor = lightModel($baseColor, $position, $lightIntensity);",
                                                      QByteArrayList() << "#pragma include gl3/lightmodel.frag.inc"));
        protos.insert("lightModel", lightModel);

        auto exposureFunction = createNode({
            createPort(QShaderNodePort::Input, "inputColor"),
            createPort(QShaderNodePort::Input, "exposure"),
            createPort(QShaderNodePort::Output, "outputColor")
        });
        exposureFunction.addRule(openGLES2, QShaderNode::Rule("highp vec4 $outputColor = $inputColor * pow(2.0, $exposure);"));
        exposureFunction.addRule(openGL3, QShaderNode::Rule("vec4 $outputColor = $inputColor * pow(2.0, $exposure);"));
        protos.insert("exposureFunction", exposureFunction);

        return protos;
    }();

    const auto complexGraph = createGraph();

    QTest::newRow("ComplexGraph") << createBuffer(complexJson) << complexProtos << complexGraph << QShaderGraphLoader::Ready;
}

void tst_QShaderGraphLoader::shouldLoadFromJsonStream()
{
    // GIVEN
    QFETCH(QBufferPointer, device);
    QFETCH(PrototypeHash, prototypes);

    auto loader = QShaderGraphLoader();

    // WHEN
    loader.setPrototypes(prototypes);
    loader.setDevice(device.data());
    loader.load();

    // THEN
    QFETCH(QShaderGraphLoader::Status, status);
    QCOMPARE(loader.status(), status);

    QFETCH(QShaderGraph, graph);
    const auto statements = loader.graph().createStatements({"foo", "bar", "baz"});
    const auto expected = graph.createStatements({"foo", "bar", "baz"});
    dumpStatementsIfNeeded(statements, expected);
    QCOMPARE(statements, expected);

    const auto sortedParameters = [](const QShaderNode &node) {
        auto res = node.parameterNames();
        res.sort();
        return res;
    };

    for (int i = 0; i < statements.size(); i++) {
        const auto actualNode = statements.at(i).node;
        const auto expectedNode = expected.at(i).node;

        QCOMPARE(actualNode.layers(), expectedNode.layers());
        QCOMPARE(actualNode.ports(), expectedNode.ports());
        QCOMPARE(sortedParameters(actualNode), sortedParameters(expectedNode));
        for (const auto &name : expectedNode.parameterNames()) {
            QCOMPARE(actualNode.parameter(name), expectedNode.parameter(name));
        }
        QCOMPARE(actualNode.availableFormats(), expectedNode.availableFormats());
        for (const auto &format : expectedNode.availableFormats()) {
            QCOMPARE(actualNode.rule(format), expectedNode.rule(format));
        }
    }
}

QTEST_MAIN(tst_QShaderGraphLoader)

#include "tst_qshadergraphloader.moc"
