// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PrimitiveRestartScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
//   https://www.gnu.org/licenses/gpl-3.0.txt
//   https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - DESKTOP GL_PRIMITIVE_RESTART WITH AN APPLICATION-CHOSEN INDEX.
//
// Desktop GL restarts on whatever glPrimitiveRestartIndex named; GLES and Vulkan both restart
// only on the all-ones value of the index type. DirectGLES used to THROW_EXCEPTION on the
// mismatch, and a throw out of a GL entry point unwinds a C++ exception through the C ABI and
// kills the process - which is how KHR-GL4x.geometry_shader.primitive_counter.*_rp took the whole
// conformance runner down, nine bodies at a time, losing every result in the chunk with it.
//
// So the first thing this asserts is simply that the process is still here. The second is that
// the restart actually happened: the substitution rewrites the index data so the driver restarts
// where the application asked, and the difference between "restart honoured" and "restart
// silently dropped" is a triangle strip that welds its two halves together across the gap.
//
// Needs a real context on purpose. The GPU-free suite cannot reach a backend at all, and this is
// entirely about what the backend does with the index buffer.

#include <cstddef>
#include <iterator>
#include <string>
#include <utility>
#include <vector>

#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"

#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES

namespace MGITest {
    namespace {

        constexpr GLsizei kSurface = 64;

        const char* const kVertexSource = R"(#version 420 core
layout(location = 0) in vec2 a_position;
void main()
{
    gl_Position = vec4(a_position, 0.0, 1.0);
}
)";

        const char* const kFragmentSource = R"(#version 420 core
out vec4 fragColor;
void main()
{
    fragColor = vec4(0.0, 1.0, 0.0, 1.0);
}
)";

        // Two triangles with a gap down the middle, plus two spare vertices parked at the origin.
        //
        // The spares exist so the restart index is a LEGAL vertex index: if the restart were
        // dropped the driver would still fetch a real vertex rather than read out of bounds, so
        // the negative case is defined behaviour and the test measures the restart rather than
        // whatever robust-buffer-access does.
        constexpr GLfloat kVertices[] = {
            -0.9f, -0.9f, // 0 - left triangle
            -0.1f, -0.9f, // 1
            -0.9f,  0.9f, // 2
             0.1f, -0.9f, // 3 - right triangle
             0.9f, -0.9f, // 4
             0.9f,  0.9f, // 5
             0.0f,  0.0f, // 6 - spare
             0.0f,  0.0f, // 7 - spare, and the application's restart index
        };
        constexpr GLuint kRestartIndex = 7;

        // A triangle STRIP, restarted in the middle: honoured, it is exactly the two triangles
        // above. Dropped, the strip welds vertices 2, 7 and 3 into extra triangles that spill
        // across the gap - which is what the middle probe below catches.
        constexpr GLuint kIndices[] = {0, 1, 2, kRestartIndex, 3, 4, 5};

        struct Pixel {
            GLubyte r = 0, g = 0, b = 0, a = 0;
        };

        class PrimitiveRestartScenario : public ScenarioTest {
        protected:
            void SetUp() override {
                ScenarioTest::SetUp();
                if (!Ready()) return;

                glGenVertexArrays(1, &m_vao);
                glBindVertexArray(m_vao);

                glGenBuffers(1, &m_vbo);
                glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
                glBufferData(GL_ARRAY_BUFFER, sizeof(kVertices), kVertices, GL_STATIC_DRAW);
                glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), nullptr);
                glEnableVertexAttribArray(0);

                glGenBuffers(1, &m_ebo);
                glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
                glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(kIndices), kIndices, GL_STATIC_DRAW);

                glGenTextures(1, &m_colorTexture);
                glBindTexture(GL_TEXTURE_2D, m_colorTexture);
                glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kSurface, kSurface);
                glGenFramebuffers(1, &m_fbo);
                glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
                glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_colorTexture, 0);
                ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER),
                          static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
                glViewport(0, 0, kSurface, kSurface);

                m_program = BuildProgram();
                ASSERT_NE(m_program, 0u) << "the flat-colour program did not build: " << m_buildLog;
                glUseProgram(m_program);
                DrainErrors();
            }

            void TearDown() override {
                if (!Ready()) return;
                glDisable(GL_PRIMITIVE_RESTART);
                glDisable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
                glPrimitiveRestartIndex(0);
                glUseProgram(0);
                if (m_program != 0) glDeleteProgram(m_program);
                glBindFramebuffer(GL_FRAMEBUFFER, 0);
                if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
                if (m_colorTexture != 0) glDeleteTextures(1, &m_colorTexture);
                glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
                glBindBuffer(GL_ARRAY_BUFFER, 0);
                if (m_ebo != 0) glDeleteBuffers(1, &m_ebo);
                if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
                glBindVertexArray(0);
                if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
                DrainErrors();
            }

            static void DrainErrors() {
                for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
                }
            }

            GLuint BuildProgram() {
                const GLuint vs = glCreateShader(GL_VERTEX_SHADER);
                glShaderSource(vs, 1, &kVertexSource, nullptr);
                glCompileShader(vs);
                const GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
                glShaderSource(fs, 1, &kFragmentSource, nullptr);
                glCompileShader(fs);
                const GLuint program = glCreateProgram();
                glAttachShader(program, vs);
                glAttachShader(program, fs);
                glLinkProgram(program);
                GLint linked = 0;
                glGetProgramiv(program, GL_LINK_STATUS, &linked);
                glDeleteShader(vs);
                glDeleteShader(fs);
                if (!linked) {
                    GLint length = 0;
                    glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
                    std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
                    glGetProgramInfoLog(program, length + 1, nullptr, buffer.data());
                    m_buildLog = buffer.data();
                    glDeleteProgram(program);
                    return 0;
                }
                return program;
            }

            // The whole surface, so a failure can report the three probes together rather than
            // three separate readbacks that might disagree about which draw they saw.
            std::vector<Pixel> DrawAndRead() {
                glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
                glClear(GL_COLOR_BUFFER_BIT);
                glDrawElements(GL_TRIANGLE_STRIP, static_cast<GLsizei>(std::size(kIndices)), GL_UNSIGNED_INT,
                               nullptr);
                std::vector<Pixel> pixels(static_cast<std::size_t>(kSurface) * kSurface);
                glReadPixels(0, 0, kSurface, kSurface, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
                return pixels;
            }

            static const Pixel& At(const std::vector<Pixel>& pixels, int x, int y) {
                return pixels[static_cast<std::size_t>(y) * kSurface + x];
            }

            static bool IsGreen(const Pixel& p) { return p.g > 128 && p.r < 128; }

            // NDC (-0.5, -0.5): well inside the left triangle whichever way the restart went.
            static constexpr int kLeftX = 16, kLeftY = 16;
            // NDC (0.6, -0.5): well inside the right triangle, and outside every welded one.
            static constexpr int kRightX = 51, kRightY = 16;
            // NDC (0.2, -0.5): in the gap between the two triangles, and INSIDE the triangle the
            // strip welds out of vertices 7, 3 and 4 when the restart is dropped. This is the
            // probe that distinguishes a working restart from a silently ignored one.
            static constexpr int kGapX = 38, kGapY = 16;

            GLuint m_vao = 0;
            GLuint m_vbo = 0;
            GLuint m_ebo = 0;
            GLuint m_fbo = 0;
            GLuint m_colorTexture = 0;
            GLuint m_program = 0;
            std::string m_buildLog;
        };

        // THE crash regression. Before the fix this call never returned: DirectGLES threw
        // std::runtime_error out of glDrawElements and the process died on the spot. Reaching the
        // assertion at all is most of the point.
        TEST_F(PrimitiveRestartScenario, AnArbitraryRestartIndexDrawsInsteadOfKillingTheProcess) {
            if (!Ready()) GTEST_SKIP();

            glEnable(GL_PRIMITIVE_RESTART);
            glPrimitiveRestartIndex(kRestartIndex);
            ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));

            const std::vector<Pixel> pixels = DrawAndRead();
            EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
                << "an arbitrary restart index is legal desktop GL and must raise no error";

            EXPECT_TRUE(IsGreen(At(pixels, kLeftX, kLeftY))) << "the first strip half did not render";
            EXPECT_TRUE(IsGreen(At(pixels, kRightX, kRightY))) << "the second strip half did not render";
            EXPECT_FALSE(IsGreen(At(pixels, kGapX, kGapY)))
                << "the gap between the two halves is covered, so the restart was dropped and the "
                   "strip welded across it";
        }

        // The other half of the state: an application that sets the restart index TO the fixed
        // all-ones value needs no rewriting at all, and the cap must map straight onto the
        // driver's own fixed-index restart. Same picture, different path through the backend.
        TEST_F(PrimitiveRestartScenario, TheFixedIndexValueTakesTheForwardingPath) {
            if (!Ready()) GTEST_SKIP();

            // Index 0xFFFFFFFF is not a vertex this draw uses, so the strip is the same shape.
            const GLuint fixedIndices[] = {0, 1, 2, 0xFFFFFFFFu, 3, 4, 5};
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
            glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, sizeof(fixedIndices), fixedIndices);

            glEnable(GL_PRIMITIVE_RESTART);
            glPrimitiveRestartIndex(0xFFFFFFFFu);
            ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));

            const std::vector<Pixel> pixels = DrawAndRead();
            EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
            EXPECT_TRUE(IsGreen(At(pixels, kLeftX, kLeftY)));
            EXPECT_TRUE(IsGreen(At(pixels, kRightX, kRightY)));
            EXPECT_FALSE(IsGreen(At(pixels, kGapX, kGapY)));

            // Put the buffer back for whatever runs next in this fixture.
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
            glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, sizeof(kIndices), kIndices);
            DrainErrors();
        }

        // With the cap off, the same index data is just data - nothing restarts, and the strip
        // welds across the gap. The negative control for the probe above: without it, a backend
        // that lost the whole draw would pass the test by rendering nothing in the gap.
        TEST_F(PrimitiveRestartScenario, WithoutTheCapTheStripWeldsAcrossTheGap) {
            if (!Ready()) GTEST_SKIP();

            glDisable(GL_PRIMITIVE_RESTART);
            glPrimitiveRestartIndex(kRestartIndex);
            ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));

            const std::vector<Pixel> pixels = DrawAndRead();
            EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
            EXPECT_TRUE(IsGreen(At(pixels, kLeftX, kLeftY))) << "the draw itself must still happen";
            EXPECT_TRUE(IsGreen(At(pixels, kGapX, kGapY)))
                << "with restart disabled the strip is continuous, so the gap must be covered - if "
                   "it is not, the probe above proves nothing";
        }

        // A second draw with a DIFFERENT restart index has to be rewritten again. The substitution
        // stages through one scratch buffer, so a cached or half-restored element-array binding
        // would show up here as the second draw reusing the first one's data.
        TEST_F(PrimitiveRestartScenario, ChangingTheRestartIndexBetweenDrawsIsHonoured) {
            if (!Ready()) GTEST_SKIP();

            glEnable(GL_PRIMITIVE_RESTART);
            glPrimitiveRestartIndex(kRestartIndex);
            const std::vector<Pixel> restarted = DrawAndRead();
            ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
            EXPECT_FALSE(IsGreen(At(restarted, kGapX, kGapY)));

            // 6 is the other spare vertex, and it appears nowhere in the index data - so nothing
            // restarts and the strip is continuous again, from the very same buffer.
            glPrimitiveRestartIndex(6);
            const std::vector<Pixel> notRestarted = DrawAndRead();
            ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
            EXPECT_TRUE(IsGreen(At(notRestarted, kLeftX, kLeftY)));
            EXPECT_TRUE(IsGreen(At(notRestarted, kGapX, kGapY)))
                << "the second draw restarted on an index that is not in its data";
        }

        // A NON-indexed draw has no index stream, so GL primitive restart cannot affect it - and a
        // list topology is the shape DirectVulkan has to refuse when the device lacks
        // VK_EXT_primitive_topology_list_restart. Deriving the pipeline's primitiveRestartEnable
        // from the capability bits alone conflated the two: an application that enables
        // GL_PRIMITIVE_RESTART once at init and then draws its UI with glDrawArrays(GL_TRIANGLES)
        // had every one of those draws silently dropped on such a device.
        TEST_F(PrimitiveRestartScenario, ANonIndexedListTopologyDrawIsUnaffectedByTheCap) {
            if (!Ready()) GTEST_SKIP();

            glEnable(GL_PRIMITIVE_RESTART);
            glPrimitiveRestartIndex(kRestartIndex);
            ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));

            glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
            glClear(GL_COLOR_BUFFER_BIT);
            // Vertices 0,1,2 are the left triangle; GL_TRIANGLES is a list topology.
            glDrawArrays(GL_TRIANGLES, 0, 3);
            EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));

            std::vector<Pixel> pixels(static_cast<std::size_t>(kSurface) * kSurface);
            glReadPixels(0, 0, kSurface, kSurface, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
            EXPECT_TRUE(IsGreen(At(pixels, kLeftX, kLeftY)))
                << "primitive restart has no meaning for glDrawArrays, so the draw must render "
                   "normally whatever the device supports";
            DrainErrors();
        }

        // GL 4.6 core 10.3.6 compares the fetched index, zero-extended, against the full 32-bit
        // PRIMITIVE_RESTART_INDEX. A restart index the index type cannot hold therefore matches
        // nothing and the draw restarts NOWHERE - it does not restart on the truncated value, and
        // it does not restart on the type's all-ones value either, which is what the driver's own
        // fixed-index restart would have done if it had been left enabled.
        TEST_F(PrimitiveRestartScenario, ARestartIndexTooLargeForTheIndexTypeRestartsNowhere) {
            if (!Ready()) GTEST_SKIP();

            // 16-bit indices with a restart index of 0x10007: the low half (7) IS a real index in
            // the data, so a truncating comparison would split the strip exactly where a correct
            // one leaves it whole.
            const GLushort shortIndices[] = {0, 1, 2, static_cast<GLushort>(kRestartIndex), 3, 4, 5};
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
            glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(shortIndices), shortIndices, GL_STATIC_DRAW);

            glEnable(GL_PRIMITIVE_RESTART);
            glPrimitiveRestartIndex(0x10000u + kRestartIndex);
            ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));

            glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
            glClear(GL_COLOR_BUFFER_BIT);
            glDrawElements(GL_TRIANGLE_STRIP, static_cast<GLsizei>(std::size(shortIndices)), GL_UNSIGNED_SHORT,
                           nullptr);
            EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));

            std::vector<Pixel> pixels(static_cast<std::size_t>(kSurface) * kSurface);
            glReadPixels(0, 0, kSurface, kSurface, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
            EXPECT_TRUE(IsGreen(At(pixels, kLeftX, kLeftY)));
            EXPECT_TRUE(IsGreen(At(pixels, kGapX, kGapY)))
                << "no 16-bit index can equal 0x10007, so nothing restarts and the strip is "
                   "continuous - truncating the restart index to 7 would split it here";

            glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(kIndices), kIndices, GL_STATIC_DRAW);
            DrainErrors();
        }

        // The all-ones value of an index type is an ordinary vertex index whenever the array uses
        // the type's full range, which is exactly why an application picks an arbitrary restart
        // index in the first place. Substituting the sentinel in place would either steal that
        // vertex or spuriously restart on it, so the copy widens instead - and the draw has to be
        // issued with the widened type, which is the part that is easy to forget.
        TEST_F(PrimitiveRestartScenario, AnAllOnesVertexIndexSurvivesTheSubstitution) {
            if (!Ready()) GTEST_SKIP();

            // The buffer carries the 16-bit all-ones value as an ordinary element. It sits past
            // the seven indices this draw reads, because the vertex array has only eight entries
            // and fetching index 65535 would be out of range - what is under test is that its
            // mere PRESENCE forces the widened copy, and that the draw still finds its own
            // indices at the right offsets in a copy whose element width has changed underneath
            // it. Narrowly substituting in place instead would rewrite this element to 0xFFFE.
            const GLushort shortIndices[] = {0, 1, 2, static_cast<GLushort>(kRestartIndex), 3, 4, 5, 0xFFFFu};
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
            glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(shortIndices), shortIndices, GL_STATIC_DRAW);

            glEnable(GL_PRIMITIVE_RESTART);
            glPrimitiveRestartIndex(kRestartIndex);
            ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));

            glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
            glClear(GL_COLOR_BUFFER_BIT);
            // Only the first seven indices are drawn, so the 0xFFFF element is never fetched - what
            // is under test is that its PRESENCE does not break the substitution or the offsets.
            glDrawElements(GL_TRIANGLE_STRIP, 7, GL_UNSIGNED_SHORT, nullptr);
            EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));

            std::vector<Pixel> pixels(static_cast<std::size_t>(kSurface) * kSurface);
            glReadPixels(0, 0, kSurface, kSurface, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
            EXPECT_TRUE(IsGreen(At(pixels, kLeftX, kLeftY))) << "the first strip half did not render";
            EXPECT_TRUE(IsGreen(At(pixels, kRightX, kRightY))) << "the second strip half did not render";
            EXPECT_FALSE(IsGreen(At(pixels, kGapX, kGapY)))
                << "the restart still has to happen once the copy has been widened";

            glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(kIndices), kIndices, GL_STATIC_DRAW);
            DrainErrors();
        }

    } // namespace
} // namespace MGITest
