// Copyright (C) 2017 Klaralvdalens Datakonsult AB (KDAB).
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

#include <QtTest/QTest>
#include <qbackendnodetester.h>
#include <Qt3DCore/private/qbackendnode_p.h>
#include <Qt3DRender/private/shaderbuilder_p.h>
#include <Qt3DRender/qshaderprogram.h>
#include <Qt3DRender/qshaderprogrambuilder.h>
#include "testrenderer.h"
#include <testarbiter.h>

#include <QByteArray>

Q_DECLARE_METATYPE(Qt3DRender::QShaderProgram::ShaderType)

class tst_ShaderBuilder : public Qt3DCore::QBackendNodeTester
{
    Q_OBJECT
private slots:
    void shouldHaveGlobalDefaultPrototypes()
    {
        // GIVEN

        // THEN
        QCOMPARE(Qt3DRender::Render::ShaderBuilder::getPrototypesFile(), QStringLiteral(":/prototypes/default.json"));
        QVERIFY(!Qt3DRender::Render::ShaderBuilder::getPrototypeNames().isEmpty());

        // WHEN
        Qt3DRender::Render::ShaderBuilder::setPrototypesFile(":/prototypes.json");

        // THEN
        QCOMPARE(Qt3DRender::Render::ShaderBuilder::getPrototypesFile(), QStringLiteral(":/prototypes.json"));
        auto prototypeNames = Qt3DRender::Render::ShaderBuilder::getPrototypeNames();
        prototypeNames.sort();
        const auto expectedPrototypeNames = QStringList() << "exposure"
                                                          << "exposureFunction"
                                                          << "fragColor"
                                                          << "lightIntensity"
                                                          << "lightModel"
                                                          << "sampleTexture"
                                                          << "texCoord"
                                                          << "texture"
                                                          << "worldPosition";
        QCOMPARE(prototypeNames, expectedPrototypeNames);
    }

    void shouldHaveInitialState()
    {
        // GIVEN
        Qt3DRender::Render::ShaderBuilder shaderBuilder;

        // THEN
        QVERIFY(!shaderBuilder.isEnabled());
        QVERIFY(shaderBuilder.enabledLayers().isEmpty());
        for (int i = 0; i <= Qt3DRender::QShaderProgram::Compute; i++) {
            const auto type = static_cast<Qt3DRender::QShaderProgram::ShaderType>(i);
            QCOMPARE(shaderBuilder.shaderGraph(type), QUrl());
            QCOMPARE(shaderBuilder.shaderCode(type), QByteArray());
            QVERIFY(!shaderBuilder.isShaderCodeDirty(type));
        }
    }

    void shouldHavePropertiesMirroringFromItsPeer_data()
    {
        QTest::addColumn<Qt3DRender::QShaderProgramBuilder*>("frontend");

        {
            auto frontend = new Qt3DRender::QShaderProgramBuilder;
            QTest::newRow("empty") << frontend;
        }
        {
            auto frontend = new Qt3DRender::QShaderProgramBuilder;
            auto program = new Qt3DRender::QShaderProgram(frontend);
            frontend->setShaderProgram(program);
            QTest::newRow("shaderProgram") << frontend;
        }
        {
            auto frontend = new Qt3DRender::QShaderProgramBuilder;
            frontend->setEnabledLayers({"foo", "bar"});
            QTest::newRow("enabledLayers") << frontend;
        }
        {
            auto frontend = new Qt3DRender::QShaderProgramBuilder;
            frontend->setVertexShaderGraph(QUrl::fromEncoded("qrc:/vertex.json"));
            QTest::newRow("vertex") << frontend;
        }
        {
            auto frontend = new Qt3DRender::QShaderProgramBuilder;
            frontend->setTessellationControlShaderGraph(QUrl::fromEncoded("qrc:/tesscontrol.json"));
            QTest::newRow("tessellationControl") << frontend;
        }
        {
            auto frontend = new Qt3DRender::QShaderProgramBuilder;
            frontend->setTessellationEvaluationShaderGraph(QUrl::fromEncoded("qrc:/tesseval.json"));
            QTest::newRow("tessellationEvaluation") << frontend;
        }
        {
            auto frontend = new Qt3DRender::QShaderProgramBuilder;
            frontend->setGeometryShaderGraph(QUrl::fromEncoded("qrc:/geometry.json"));
            QTest::newRow("geometry") << frontend;
        }
        {
            auto frontend = new Qt3DRender::QShaderProgramBuilder;
            frontend->setFragmentShaderGraph(QUrl::fromEncoded("qrc:/fragment.json"));
            QTest::newRow("fragment") << frontend;
        }
        {
            auto frontend = new Qt3DRender::QShaderProgramBuilder;
            frontend->setComputeShaderGraph(QUrl::fromEncoded("qrc:/compute.json"));
            QTest::newRow("compute") << frontend;
        }
    }

    void shouldHavePropertiesMirroringFromItsPeer()
    {
        // GIVEN
        QFETCH(Qt3DRender::QShaderProgramBuilder*, frontend);
        Qt3DRender::Render::ShaderBuilder backend;
        TestRenderer renderer;

        // WHEN
        backend.setRenderer(&renderer);
        simulateInitializationSync(frontend, &backend);

        // THEN
        QVERIFY(backend.isEnabled() == frontend->isEnabled());

        if (frontend->shaderProgram())
            QCOMPARE(backend.shaderProgramId(), frontend->shaderProgram()->id());
        else
            QVERIFY(backend.shaderProgramId().isNull());

        QCOMPARE(backend.enabledLayers(), frontend->enabledLayers());

        QCOMPARE(backend.shaderGraph(Qt3DRender::QShaderProgram::Vertex), frontend->vertexShaderGraph());
        QCOMPARE(backend.shaderCode(Qt3DRender::QShaderProgram::Vertex), QByteArray());
        QCOMPARE(backend.isShaderCodeDirty(Qt3DRender::QShaderProgram::Vertex), !frontend->vertexShaderGraph().isEmpty());

        QCOMPARE(backend.shaderGraph(Qt3DRender::QShaderProgram::TessellationControl), frontend->tessellationControlShaderGraph());
        QCOMPARE(backend.shaderCode(Qt3DRender::QShaderProgram::TessellationControl), QByteArray());
        QCOMPARE(backend.isShaderCodeDirty(Qt3DRender::QShaderProgram::TessellationControl), !frontend->tessellationControlShaderGraph().isEmpty());

        QCOMPARE(backend.shaderGraph(Qt3DRender::QShaderProgram::TessellationEvaluation), frontend->tessellationEvaluationShaderGraph());
        QCOMPARE(backend.shaderCode(Qt3DRender::QShaderProgram::TessellationEvaluation), QByteArray());
        QCOMPARE(backend.isShaderCodeDirty(Qt3DRender::QShaderProgram::TessellationEvaluation), !frontend->tessellationEvaluationShaderGraph().isEmpty());

        QCOMPARE(backend.shaderGraph(Qt3DRender::QShaderProgram::Geometry), frontend->geometryShaderGraph());
        QCOMPARE(backend.shaderCode(Qt3DRender::QShaderProgram::Geometry), QByteArray());
        QCOMPARE(backend.isShaderCodeDirty(Qt3DRender::QShaderProgram::Geometry), !frontend->geometryShaderGraph().isEmpty());

        QCOMPARE(backend.shaderGraph(Qt3DRender::QShaderProgram::Fragment), frontend->fragmentShaderGraph());
        QCOMPARE(backend.shaderCode(Qt3DRender::QShaderProgram::Fragment), QByteArray());
        QCOMPARE(backend.isShaderCodeDirty(Qt3DRender::QShaderProgram::Fragment), !frontend->fragmentShaderGraph().isEmpty());

        QCOMPARE(backend.shaderGraph(Qt3DRender::QShaderProgram::Compute), frontend->computeShaderGraph());
        QCOMPARE(backend.shaderCode(Qt3DRender::QShaderProgram::Compute), QByteArray());
        QCOMPARE(backend.isShaderCodeDirty(Qt3DRender::QShaderProgram::Compute), !frontend->computeShaderGraph().isEmpty());

        // WHEN
        backend.cleanup();

        // THEN
        QVERIFY(!backend.isEnabled());
        QVERIFY(backend.enabledLayers().isEmpty());
        for (int i = 0; i <= Qt3DRender::QShaderProgram::Compute; i++) {
            const auto type = static_cast<Qt3DRender::QShaderProgram::ShaderType>(i);
            QCOMPARE(backend.shaderGraph(type), QUrl());
            QCOMPARE(backend.shaderCode(type), QByteArray());
            QVERIFY(!backend.isShaderCodeDirty(type));
        }

        delete frontend;
    }

    void shouldHandleEnablePropertyChange()
    {
        // GIVEN
        Qt3DRender::Render::ShaderBuilder backend;
        Qt3DRender::QShaderProgramBuilder frontend;
        TestRenderer renderer;
        backend.setRenderer(&renderer);
        simulateInitializationSync(&frontend, &backend);

        // WHEN
        frontend.setEnabled(false);
        backend.syncFromFrontEnd(&frontend, false);

        // THEN
        QVERIFY(!backend.isEnabled());
        QCOMPARE(renderer.dirtyBits(), Qt3DRender::Render::AbstractRenderer::ShadersDirty);
        renderer.resetDirty();

        // WHEN
        frontend.setEnabled(true);
        backend.syncFromFrontEnd(&frontend, false);
        // AND
        backend.cleanup();

        // THEN
        QVERIFY(!backend.isEnabled());
        QCOMPARE(renderer.dirtyBits(), Qt3DRender::Render::AbstractRenderer::ShadersDirty);
        renderer.resetDirty();
    }

    void shouldHandleShaderProgramPropertyChange()
    {
        // GIVEN
        Qt3DRender::Render::ShaderBuilder backend;
        Qt3DRender::QShaderProgramBuilder frontend;
        TestRenderer renderer;
        backend.setRenderer(&renderer);
        simulateInitializationSync(&frontend, &backend);

        // WHEN
        Qt3DRender::QShaderProgram prog;
        frontend.setShaderProgram(&prog);
        backend.syncFromFrontEnd(&frontend, false);

        // THEN
        QCOMPARE(backend.shaderProgramId(), prog.id());
        QCOMPARE(renderer.dirtyBits(), Qt3DRender::Render::AbstractRenderer::ShadersDirty);
        renderer.resetDirty();

        // WHEN
        frontend.setShaderProgram(nullptr);
        backend.syncFromFrontEnd(&frontend, false);

        // THEN
        QVERIFY(backend.shaderProgramId().isNull());
        QCOMPARE(renderer.dirtyBits(), Qt3DRender::Render::AbstractRenderer::ShadersDirty);
        renderer.resetDirty();

        // WHEN
        frontend.setShaderProgram(&prog);
        backend.syncFromFrontEnd(&frontend, false);
        // AND
        backend.cleanup();

        // THEN
        QVERIFY(backend.shaderProgramId().isNull());
        QCOMPARE(renderer.dirtyBits(), Qt3DRender::Render::AbstractRenderer::ShadersDirty);
        renderer.resetDirty();
    }

    void shouldHandleEnabledLayersPropertyChange()
    {
        // GIVEN
        qputenv("QT3D_DISABLE_SHADER_CACHE", "1");
        Qt3DRender::Render::ShaderBuilder backend;
        Qt3DRender::QShaderProgramBuilder frontend;
        TestRenderer renderer;
        backend.setRenderer(&renderer);
        simulateInitializationSync(&frontend, &backend);
        const auto layers = QStringList() << "foo" << "bar";

        static const std::pair<
                Qt3DRender::QShaderProgram::ShaderType,
                void (Qt3DRender::QShaderProgramBuilder::*)(const QUrl &)
                >
                shaderTypesToSetters[] = {
            {Qt3DRender::QShaderProgram::Vertex, &Qt3DRender::QShaderProgramBuilder::setVertexShaderGraph},
            {Qt3DRender::QShaderProgram::TessellationControl, &Qt3DRender::QShaderProgramBuilder::setTessellationControlShaderGraph},
            {Qt3DRender::QShaderProgram::TessellationEvaluation, &Qt3DRender::QShaderProgramBuilder::setTessellationEvaluationShaderGraph},
            {Qt3DRender::QShaderProgram::Geometry, &Qt3DRender::QShaderProgramBuilder::setGeometryShaderGraph},
            {Qt3DRender::QShaderProgram::Fragment, &Qt3DRender::QShaderProgramBuilder::setFragmentShaderGraph},
            {Qt3DRender::QShaderProgram::Compute, &Qt3DRender::QShaderProgramBuilder::setComputeShaderGraph},
        };


        for (auto it = std::begin(shaderTypesToSetters), end = std::end(shaderTypesToSetters); it != end; ++it) {
            const auto graphUrl = QUrl::fromEncoded("qrc:/input.json");
            (frontend.*(it->second))(graphUrl);
        }

        // WHEN
        frontend.setEnabledLayers(layers);
        backend.syncFromFrontEnd(&frontend, false);

        // THEN
        QCOMPARE(backend.enabledLayers(), layers);
        for (int i = 0; i <= Qt3DRender::QShaderProgram::Compute; i++) {
            const auto type = static_cast<Qt3DRender::QShaderProgram::ShaderType>(i);
            QVERIFY(backend.isShaderCodeDirty(type));
            backend.generateCode(type); // Resets the dirty flag
        }
        QCOMPARE(renderer.dirtyBits(), Qt3DRender::Render::AbstractRenderer::ShadersDirty);
        renderer.resetDirty();

        // WHEN
        frontend.setEnabledLayers(layers);
        backend.syncFromFrontEnd(&frontend, false);

        // THEN
        QCOMPARE(backend.enabledLayers(), layers);
        for (int i = 0; i <= Qt3DRender::QShaderProgram::Compute; i++) {
            const auto type = static_cast<Qt3DRender::QShaderProgram::ShaderType>(i);
            QVERIFY(!backend.isShaderCodeDirty(type));
            backend.generateCode(type); // Resets the dirty flag
        }
        QCOMPARE(renderer.dirtyBits(), 0);
        renderer.resetDirty();

        // WHEN
        frontend.setEnabledLayers(QStringList());
        backend.syncFromFrontEnd(&frontend, false);

        // THEN
        QVERIFY(backend.shaderProgramId().isNull());
        for (int i = 0; i <= Qt3DRender::QShaderProgram::Compute; i++) {
            const auto type = static_cast<Qt3DRender::QShaderProgram::ShaderType>(i);
            QVERIFY(backend.isShaderCodeDirty(type));
            backend.generateCode(type); // Resets the dirty flag
        }
        QCOMPARE(renderer.dirtyBits(), Qt3DRender::Render::AbstractRenderer::ShadersDirty);
        renderer.resetDirty();

        // WHEN
        frontend.setEnabledLayers(layers);
        backend.syncFromFrontEnd(&frontend, false);
        // AND
        backend.cleanup();

        // THEN
        QVERIFY(backend.enabledLayers().isEmpty());
        for (int i = 0; i <= Qt3DRender::QShaderProgram::Compute; i++) {
            const auto type = static_cast<Qt3DRender::QShaderProgram::ShaderType>(i);
            QVERIFY(!backend.isShaderCodeDirty(type));
            backend.generateCode(type); // Resets the dirty flag
        }
        QCOMPARE(renderer.dirtyBits(), Qt3DRender::Render::AbstractRenderer::ShadersDirty);
        renderer.resetDirty();
    }

    void shouldHandleShaderGraphPropertiesChanges_data()
    {
        QTest::addColumn<Qt3DRender::QShaderProgram::ShaderType>("type");
        QTest::addColumn<QUrl>("graphUrl");

        QTest::newRow("vertex") << Qt3DRender::QShaderProgram::Vertex
                                << QUrl::fromEncoded("qrc:/vertex.json");

        QTest::newRow("tessControl") << Qt3DRender::QShaderProgram::TessellationControl
                                     << QUrl::fromEncoded("qrc:/tesscontrol.json");

        QTest::newRow("tessEval") << Qt3DRender::QShaderProgram::TessellationEvaluation
                                  << QUrl::fromEncoded("qrc:/tesseval.json");

        QTest::newRow("geometry") << Qt3DRender::QShaderProgram::Geometry
                                  << QUrl::fromEncoded("qrc:/geometry.json");

        QTest::newRow("fragment") << Qt3DRender::QShaderProgram::Fragment
                                  << QUrl::fromEncoded("qrc:/fragment.json");

        QTest::newRow("compute") << Qt3DRender::QShaderProgram::Compute
                                 << QUrl::fromEncoded("qrc:/compute.json");
    }

    void shouldHandleShaderGraphPropertiesChanges()
    {
        // GIVEN
        QFETCH(Qt3DRender::QShaderProgram::ShaderType, type);
        QFETCH(QUrl, graphUrl);

        Qt3DRender::Render::ShaderBuilder backend;
        Qt3DRender::QShaderProgramBuilder frontend;
        TestRenderer renderer;
        backend.setRenderer(&renderer);
        simulateInitializationSync(&frontend, &backend);

        static const QHash<
                Qt3DRender::QShaderProgram::ShaderType,
                void (Qt3DRender::QShaderProgramBuilder::*)(const QUrl &)
                >
                shaderTypesToSetters = {
            {Qt3DRender::QShaderProgram::Vertex, &Qt3DRender::QShaderProgramBuilder::setVertexShaderGraph},
            {Qt3DRender::QShaderProgram::TessellationControl, &Qt3DRender::QShaderProgramBuilder::setTessellationControlShaderGraph},
            {Qt3DRender::QShaderProgram::TessellationEvaluation, &Qt3DRender::QShaderProgramBuilder::setTessellationEvaluationShaderGraph},
            {Qt3DRender::QShaderProgram::Geometry, &Qt3DRender::QShaderProgramBuilder::setGeometryShaderGraph},
            {Qt3DRender::QShaderProgram::Fragment, &Qt3DRender::QShaderProgramBuilder::setFragmentShaderGraph},
            {Qt3DRender::QShaderProgram::Compute, &Qt3DRender::QShaderProgramBuilder::setComputeShaderGraph},
        };

        // WHEN

        (frontend.*(shaderTypesToSetters[type]))(QUrl());
        backend.syncFromFrontEnd(&frontend, false);

        // THEN
        QCOMPARE(backend.shaderGraph(type), QUrl());
        QVERIFY(!backend.isShaderCodeDirty(type));
        QVERIFY(backend.shaderCode(type).isEmpty());
        QCOMPARE(renderer.dirtyBits(), 0);
        renderer.resetDirty();

        // WHEN
        (frontend.*(shaderTypesToSetters[type]))(graphUrl);
        backend.syncFromFrontEnd(&frontend, false);

        // THEN
        QCOMPARE(backend.shaderGraph(type), graphUrl);
        QVERIFY(backend.isShaderCodeDirty(type));
        QVERIFY(backend.shaderCode(type).isEmpty());
        QCOMPARE(renderer.dirtyBits(), Qt3DRender::Render::AbstractRenderer::ShadersDirty);
        renderer.resetDirty();

        // WHEN
        (frontend.*(shaderTypesToSetters[type]))(QUrl());
        backend.syncFromFrontEnd(&frontend, false);

        // THEN
        QCOMPARE(backend.shaderGraph(type), QUrl());
        QVERIFY(backend.isShaderCodeDirty(type));
        QVERIFY(backend.shaderCode(type).isEmpty());
        QCOMPARE(renderer.dirtyBits(), Qt3DRender::Render::AbstractRenderer::ShadersDirty);
        renderer.resetDirty();

        // WHEN
        (frontend.*(shaderTypesToSetters[type]))(graphUrl);
        backend.syncFromFrontEnd(&frontend, false);
        // AND
        backend.cleanup();

        // THEN
        QCOMPARE(backend.shaderGraph(type), QUrl());
        QVERIFY(!backend.isShaderCodeDirty(type));
        QVERIFY(backend.shaderCode(type).isEmpty());
        QCOMPARE(renderer.dirtyBits(), Qt3DRender::Render::AbstractRenderer::ShadersDirty);
        renderer.resetDirty();
    }

    void shouldHandleShaderCodeGeneration_data()
    {
        QTest::addColumn<Qt3DRender::QShaderProgram::ShaderType>("type");

        QTest::newRow("vertex") << Qt3DRender::QShaderProgram::Vertex;
        QTest::newRow("tessControl") << Qt3DRender::QShaderProgram::TessellationControl;
        QTest::newRow("tessEval") << Qt3DRender::QShaderProgram::TessellationEvaluation;
        QTest::newRow("geometry") << Qt3DRender::QShaderProgram::Geometry;
        QTest::newRow("fragment") << Qt3DRender::QShaderProgram::Fragment;
        QTest::newRow("compute") << Qt3DRender::QShaderProgram::Compute;
    }

    void shouldHandleShaderCodeGeneration()
    {
        // GIVEN
        qputenv("QT3D_DISABLE_SHADER_CACHE", "1");
        Qt3DRender::Render::ShaderBuilder::setPrototypesFile(":/prototypes.json");
        QVERIFY(!Qt3DRender::Render::ShaderBuilder::getPrototypeNames().isEmpty());

        QFETCH(Qt3DRender::QShaderProgram::ShaderType, type);

        const auto gl3Api = []{
            auto api = Qt3DRender::GraphicsApiFilterData();
            api.m_api = Qt3DRender::QGraphicsApiFilter::OpenGL;
            api.m_profile = Qt3DRender::QGraphicsApiFilter::CoreProfile;
            api.m_major = 3;
            api.m_minor = 2;
            return api;
        }();

        const auto es2Api = []{
            auto api = Qt3DRender::GraphicsApiFilterData();
            api.m_api = Qt3DRender::QGraphicsApiFilter::OpenGLES;
            api.m_major = 2;
            api.m_minor = 0;
            return api;
        }();

        const auto readCode = [](const QString &suffix) -> QByteArray {
            const auto filePath = QStringLiteral(":/output.") + suffix;
            QFile file(filePath);
            if (!file.open(QFile::ReadOnly | QFile::Text))
                qFatal("File open failed: %s", qPrintable(filePath));
            return file.readAll();
        };

        const auto gl3Code = readCode("gl3");
        const auto es2Code = readCode("es2");

        Qt3DRender::Render::ShaderBuilder backend;

        // WHEN
        const auto graphUrl = QUrl::fromEncoded("qrc:/input.json");
        backend.setShaderGraph(type, graphUrl);

        // THEN
        QCOMPARE(backend.shaderGraph(type), graphUrl);
        QVERIFY(backend.isShaderCodeDirty(type));
        QVERIFY(backend.shaderCode(type).isEmpty());

        // WHEN
        backend.setGraphicsApi(gl3Api);
        backend.generateCode(type);

        // THEN
        QCOMPARE(backend.shaderGraph(type), graphUrl);
        QVERIFY(!backend.isShaderCodeDirty(type));
        QCOMPARE(backend.shaderCode(type), gl3Code);

        // WHEN
        backend.setGraphicsApi(es2Api);

        // THEN
        QCOMPARE(backend.shaderGraph(type), graphUrl);
        QVERIFY(backend.isShaderCodeDirty(type));
        QCOMPARE(backend.shaderCode(type), gl3Code);

        // WHEN
        backend.generateCode(type);

        // THEN
        QCOMPARE(backend.shaderGraph(type), graphUrl);
        QVERIFY(!backend.isShaderCodeDirty(type));
        QCOMPARE(backend.shaderCode(type), es2Code);
    }

    void checkCodeUpdatedNotification_data()
    {
        QTest::addColumn<Qt3DRender::QShaderProgram::ShaderType>("type");
        QTest::addColumn<Qt3DRender::QShaderProgram::ShaderType>("notificationType");

        QTest::newRow("vertex") << Qt3DRender::QShaderProgram::Vertex << Qt3DRender::QShaderProgram::Vertex;
        QTest::newRow("tessControl") << Qt3DRender::QShaderProgram::TessellationControl << Qt3DRender::QShaderProgram::TessellationControl;
        QTest::newRow("tessEval") << Qt3DRender::QShaderProgram::TessellationEvaluation << Qt3DRender::QShaderProgram::TessellationEvaluation;
        QTest::newRow("geometry") << Qt3DRender::QShaderProgram::Geometry << Qt3DRender::QShaderProgram::Geometry;
        QTest::newRow("fragment") << Qt3DRender::QShaderProgram::Fragment << Qt3DRender::QShaderProgram::Fragment;
        QTest::newRow("compute") << Qt3DRender::QShaderProgram::Compute << Qt3DRender::QShaderProgram::Compute;
    }


    void checkCodeUpdatedNotification()
    {
        // GIVEN
        qputenv("QT3D_DISABLE_SHADER_CACHE", "1");
        Qt3DRender::Render::ShaderBuilder::setPrototypesFile(":/prototypes.json");
        QVERIFY(!Qt3DRender::Render::ShaderBuilder::getPrototypeNames().isEmpty());
        QFETCH(Qt3DRender::QShaderProgram::ShaderType, type);
        QFETCH(Qt3DRender::QShaderProgram::ShaderType, notificationType);
        Q_UNUSED(notificationType);

        const auto gl3Api = []{
            auto api = Qt3DRender::GraphicsApiFilterData();
            api.m_api = Qt3DRender::QGraphicsApiFilter::OpenGL;
            api.m_profile = Qt3DRender::QGraphicsApiFilter::CoreProfile;
            api.m_major = 3;
            api.m_minor = 2;
            return api;
        }();

        const auto readCode = [](const QString &suffix) -> QByteArray {
            const auto filePath = QStringLiteral(":/output.") + suffix;
            QFile file(filePath);
            if (!file.open(QFile::ReadOnly | QFile::Text))
                qFatal("File open failed: %s", qPrintable(filePath));
            return file.readAll();
        };

        const auto gl3Code = readCode("gl3");

        Qt3DRender::Render::ShaderBuilder backend;
        TestArbiter arbiter;


        // WHEN
        const auto graphUrl = QUrl::fromEncoded("qrc:/input.json");
        backend.setShaderGraph(type, graphUrl);

        // THEN
        QCOMPARE(backend.shaderGraph(type), graphUrl);
        QVERIFY(backend.isShaderCodeDirty(type));
        QVERIFY(backend.shaderCode(type).isEmpty());

        // WHEN
        backend.setGraphicsApi(gl3Api);
        backend.generateCode(type);

        // THEN
        QCOMPARE(backend.shaderGraph(type), graphUrl);
        QVERIFY(!backend.isShaderCodeDirty(type));
        QCOMPARE(backend.shaderCode(type), gl3Code);
    }

    void checkFileCaching()
    {
        // GIVEN
        qunsetenv("QT3D_DISABLE_SHADER_CACHE");
        QTemporaryDir cacheDir;

        if (!cacheDir.isValid()) {
            QSKIP("Unable to generate cache dir, skipping");
            return;
        }
        const auto gl3Api = []{
            auto api = Qt3DRender::GraphicsApiFilterData();
            api.m_api = Qt3DRender::QGraphicsApiFilter::OpenGL;
            api.m_profile = Qt3DRender::QGraphicsApiFilter::CoreProfile;
            api.m_major = 3;
            api.m_minor = 2;
            return api;
        }();
        const auto gl2Api = []{
            auto api = Qt3DRender::GraphicsApiFilterData();
            api.m_api = Qt3DRender::QGraphicsApiFilter::OpenGL;
            api.m_profile = Qt3DRender::QGraphicsApiFilter::NoProfile;
            api.m_major = 2;
            api.m_minor = 0;
            return api;
        }();
        Qt3DRender::Render::ShaderBuilder::setPrototypesFile(":/prototypes.json");
        qputenv("QT3D_WRITABLE_CACHE_PATH", cacheDir.path().toUtf8());

        // THEN
        QVERIFY(QDir(cacheDir.path()).entryList(QDir::Files).empty());
        QByteArray hashKey;
        {
            // WHEN
            Qt3DRender::Render::ShaderBuilder b;
            b.setGraphicsApi(gl3Api);
            const auto graphUrl = QUrl::fromEncoded("qrc:/input.json");
            b.setShaderGraph(Qt3DRender::QShaderProgram::Vertex, graphUrl);
            b.generateCode(Qt3DRender::QShaderProgram::Vertex);

            // THEN
            QCOMPARE(QDir(cacheDir.path()).entryList(QDir::Files).size(), 1);

            hashKey = b.hashKeyForShaderGraph(Qt3DRender::QShaderProgram::Vertex);
            QCOMPARE(hashKey.size(), 40);

            QCOMPARE(QDir(cacheDir.path()).entryList(QDir::Files).first(),
                     QString::fromUtf8(hashKey) + QLatin1String(".qt3d"));
        }
        {
            // WHEN
            Qt3DRender::Render::ShaderBuilder b;
            b.setGraphicsApi(gl3Api);
            const auto graphUrl = QUrl::fromEncoded("qrc:/input.json");
            b.setShaderGraph(Qt3DRender::QShaderProgram::Vertex, graphUrl);
            b.generateCode(Qt3DRender::QShaderProgram::Vertex);

            // THEN
            QCOMPARE(QDir(cacheDir.path()).entryList(QDir::Files).size(), 1);
            QCOMPARE(QDir(cacheDir.path()).entryList(QDir::Files).first(),
                     QString::fromUtf8(hashKey) + QLatin1String(".qt3d"));
        }
        {
            // WHEN
            Qt3DRender::Render::ShaderBuilder b;
            b.setGraphicsApi(gl2Api);
            const auto graphUrl = QUrl::fromEncoded("qrc:/input.json");
            b.setShaderGraph(Qt3DRender::QShaderProgram::Vertex, graphUrl);
            b.generateCode(Qt3DRender::QShaderProgram::Vertex);
            QByteArray gl2HashKey = b.hashKeyForShaderGraph(Qt3DRender::QShaderProgram::Vertex);

            // THEN
            QCOMPARE(QDir(cacheDir.path()).entryList(QDir::Files).size(), 2);
            QVERIFY(gl2HashKey != hashKey);
        }
    }

    void checkRuntimeCaching()
    {
        // GIVEN
        qunsetenv("QT3D_DISABLE_SHADER_CACHE");
        TestRenderer renderer;
        QTemporaryDir cacheDir;

        if (!cacheDir.isValid()) {
            QSKIP("Unable to generate cache dir, skipping");
            return;
        }
        const auto gl3Api = []{
            auto api = Qt3DRender::GraphicsApiFilterData();
            api.m_api = Qt3DRender::QGraphicsApiFilter::OpenGL;
            api.m_profile = Qt3DRender::QGraphicsApiFilter::CoreProfile;
            api.m_major = 3;
            api.m_minor = 2;
            return api;
        }();
        Qt3DRender::Render::ShaderBuilder::setPrototypesFile(":/prototypes.json");
        qputenv("QT3D_WRITABLE_CACHE_PATH", cacheDir.path().toUtf8());

        // THEN
        QVERIFY(QDir(cacheDir.path()).entryList(QDir::Files).empty());

        // WHEN
        Qt3DRender::Render::ShaderBuilder b;
        b.setGraphicsApi(gl3Api);
        const auto graphUrl = QUrl::fromEncoded("qrc:/input.json");
        b.setShaderGraph(Qt3DRender::QShaderProgram::Vertex, graphUrl);
        b.generateCode(Qt3DRender::QShaderProgram::Vertex);

        // THEN
        QCOMPARE(QDir(cacheDir.path()).entryList(QDir::Files).size(), 1);

        const QByteArray hashKey = b.hashKeyForShaderGraph(Qt3DRender::QShaderProgram::Vertex);
        QCOMPARE(hashKey.size(), 40);

        QCOMPARE(QDir(cacheDir.path()).entryList(QDir::Files).first(),
                 QString::fromUtf8(hashKey) + QLatin1String(".qt3d"));

        QVERIFY(!renderer.containsGeneratedShaderGraph(hashKey));

        // WHEN
        b.setRenderer(&renderer);
        b.generateCode(Qt3DRender::QShaderProgram::Vertex);

        // THEN
        QVERIFY(renderer.containsGeneratedShaderGraph(hashKey));
    }

    void checkDontUseCache()
    {
        // GIVEN
        QTemporaryDir cacheDir;

        if (!cacheDir.isValid()) {
            QSKIP("Unable to generate cache dir, skipping");
            return;
        }
        const auto gl3Api = []{
            auto api = Qt3DRender::GraphicsApiFilterData();
            api.m_api = Qt3DRender::QGraphicsApiFilter::OpenGL;
            api.m_profile = Qt3DRender::QGraphicsApiFilter::CoreProfile;
            api.m_major = 3;
            api.m_minor = 2;
            return api;
        }();
        Qt3DRender::Render::ShaderBuilder::setPrototypesFile(":/prototypes.json");

        // THEN
        QVERIFY(QDir(cacheDir.path()).entryList(QDir::Files).empty());

        // WHEN
        qputenv("QT3D_DISABLE_SHADER_CACHE", "1");
        Qt3DRender::Render::ShaderBuilder b;
        b.setGraphicsApi(gl3Api);
        const auto graphUrl = QUrl::fromEncoded("qrc:/input.json");
        b.setShaderGraph(Qt3DRender::QShaderProgram::Vertex, graphUrl);
        b.generateCode(Qt3DRender::QShaderProgram::Vertex);

        // THEN
        QCOMPARE(QDir(cacheDir.path()).entryList(QDir::Files).size(), 0);
    }

    void checkForceRebuildCache()
    {
        // GIVEN
        QTemporaryDir cacheDir;

        if (!cacheDir.isValid()) {
            QSKIP("Unable to generate cache dir, skipping");
            return;
        }
        const auto gl3Api = []{
            auto api = Qt3DRender::GraphicsApiFilterData();
            api.m_api = Qt3DRender::QGraphicsApiFilter::OpenGL;
            api.m_profile = Qt3DRender::QGraphicsApiFilter::CoreProfile;
            api.m_major = 3;
            api.m_minor = 2;
            return api;
        }();
        Qt3DRender::Render::ShaderBuilder::setPrototypesFile(":/prototypes.json");

        // THEN
        QVERIFY(QDir(cacheDir.path()).entryList(QDir::Files).empty());

        // WHEN
        qputenv("QT3D_WRITABLE_CACHE_PATH", cacheDir.path().toUtf8());
        qputenv("QT3D_DISABLE_SHADER_CACHE", "1");
        qputenv("QT3D_REBUILD_SHADER_CACHE", "1");
        Qt3DRender::Render::ShaderBuilder b;
        b.setGraphicsApi(gl3Api);
        const auto graphUrl = QUrl::fromEncoded("qrc:/input.json");
        b.setShaderGraph(Qt3DRender::QShaderProgram::Vertex, graphUrl);
        b.generateCode(Qt3DRender::QShaderProgram::Vertex);

        // THEN -> We have rebuilt the shader file (even if we don't use it)
        QCOMPARE(QDir(cacheDir.path()).entryList(QDir::Files).size(), 1);
    }
};

QTEST_MAIN(tst_ShaderBuilder)

#include "tst_shaderbuilder.moc"
