/*
 * Copyright 2022 Rive
 */

#include "common/render_context_null.hpp"
#include "rive/renderer/rive_renderer.hpp"
#include "rive/renderer/rive_render_image.hpp"
#include "../src/rive_render_path.hpp"
#include <catch.hpp>

namespace rive::gpu
{
static RenderContext::FrameDescriptor s_frameDescriptor = {
    .renderTargetWidth = 100,
    .renderTargetHeight = 100,
};

// Ensures that clip contents get reused when we pop and push the same path(s).
TEST_CASE("clip-stack", "RiveRenderer")
{
    std::unique_ptr<RenderContext> renderContext =
        RenderContextNULL::MakeContext();
    auto renderTarget =
        renderContext->static_impl_cast<RenderContextNULL>()->makeRenderTarget(
            s_frameDescriptor.renderTargetWidth,
            s_frameDescriptor.renderTargetHeight);

    for (int i = 0; i < 3; ++i)
    {
        renderContext->beginFrame(s_frameDescriptor);

        auto pathA = renderContext->makeEmptyRenderPath();
        auto pathB = renderContext->makeEmptyRenderPath();
        auto pathC = renderContext->makeEmptyRenderPath();
        auto drawablePath = renderContext->makeEmptyRenderPath();
        pathA->cubicTo(1, 1, 1, 2, 2, 2);
        pathA->lineTo(3, 4);
        pathB->cubicTo(5, 5, 5, 6, 6, 6);
        pathB->lineTo(7, 8);
        pathC->lineTo(9, 10);
        pathC->cubicTo(11, 12, 13, 14, 15, 16);
        drawablePath->cubicTo(17, 17, 17, 18, 18, 18);
        drawablePath->lineTo(19, 20);

        auto paint = renderContext->makeRenderPaint();
        paint->color(0xffffffff);

        RiveRenderer renderer(renderContext.get());

        renderer.save();
        renderer.clipPath(pathA.get());
        renderer.drawPath(drawablePath.get(), paint.get());
        renderer.restore();
        uint32_t clipID = renderContext->getClipContentID();
        CHECK(clipID != 0);

        // Pushing the same clip sholdn't cause us to redraw the clipBuffer
        // (clipID will stay the same.)
        renderer.save();
        renderer.clipPath(pathA.get());
        renderer.drawPath(drawablePath.get(), paint.get());
        renderer.restore();
        CHECK(clipID == renderContext->getClipContentID());

        // We can't modify paths anymore once they've been pushed to the
        // context.
#if 0
        // Modifying the path will cause the clip to be regenerated.
        renderer.save();
        pathA->lineTo(21, 22);
        renderer.clipPath(pathA.get());
        renderer.drawPath(drawablePath.get(), paint.get());
        renderer.restore();
        CHECK(clipID != renderContext->getClipContentID());
        clipID = renderContext->getClipContentID();
#endif

        // Pushing the same (modified) clip sholdn't cause us to redraw the
        // clipBuffer, again.
        renderer.save();
        renderer.clipPath(pathA.get());
        renderer.drawPath(drawablePath.get(), paint.get());
        renderer.restore();
        CHECK(clipID == renderContext->getClipContentID());

        // Changing the view matrix will invalidate the clip.
        renderer.translate(.5f, 1.5f);
        renderer.save();
        renderer.clipPath(pathA.get());
        renderer.drawPath(drawablePath.get(), paint.get());
        renderer.restore();
        CHECK(clipID != renderContext->getClipContentID());
        clipID = renderContext->getClipContentID();

        // Now we should reuse the clip again on the new matrix.
        renderer.save();
        renderer.clipPath(pathA.get());
        renderer.drawPath(drawablePath.get(), paint.get());
        renderer.restore();
        CHECK(clipID == renderContext->getClipContentID());

        // Pushing and popping a nested clip will cause the clip to be
        // regenerated.
        renderer.save();
        renderer.clipPath(pathA.get());
        renderer.drawPath(drawablePath.get(), paint.get());
        CHECK(clipID == renderContext->getClipContentID());
        renderer.clipPath(pathB.get());
        renderer.drawPath(drawablePath.get(), paint.get());
        CHECK(clipID != renderContext->getClipContentID());
        clipID = renderContext->getClipContentID();
        renderer.restore();

        // Pushing both back will reuse the clip.
        renderer.save();
        renderer.clipPath(pathA.get());
        renderer.clipPath(pathB.get());
        renderer.drawPath(drawablePath.get(), paint.get());
        renderer.restore();
        CHECK(clipID == renderContext->getClipContentID());

        // Pushing a new clip and not using it will also not affect the clip
        // buffer.
        renderer.save();
        renderer.clipPath(pathA.get());
        renderer.clipPath(pathB.get());
        renderer.save();
        renderer.clipPath(pathC.get());
        renderer.restore();
        renderer.drawPath(drawablePath.get(), paint.get());
        CHECK(clipID == renderContext->getClipContentID());
        renderer.restore();

        // Now go three deep, just for fun.
        renderer.save();
        renderer.clipPath(pathA.get());
        renderer.save();
        renderer.clipPath(pathB.get());
        renderer.clipPath(pathC.get());
        renderer.drawPath(drawablePath.get(), paint.get());
        CHECK(clipID != renderContext->getClipContentID());
        clipID = renderContext->getClipContentID();
        renderer.restore();
        renderer.clipPath(pathB.get());
        renderer.clipPath(pathC.get());
        renderer.drawPath(drawablePath.get(), paint.get());
        CHECK(clipID == renderContext->getClipContentID());
        renderer.restore();

        // Adding them back in a different order will regenerate the clip.
        renderer.save();
        renderer.clipPath(pathA.get());
        renderer.clipPath(pathC.get());
        renderer.clipPath(pathB.get());
        renderer.drawPath(drawablePath.get(), paint.get());
        CHECK(clipID != renderContext->getClipContentID());
        clipID = renderContext->getClipContentID();
        renderer.restore();

        // And now the alternate order should stick.
        renderer.save();
        renderer.clipPath(pathA.get());
        renderer.clipPath(pathC.get());
        renderer.clipPath(pathB.get());
        renderer.drawPath(drawablePath.get(), paint.get());
        CHECK(clipID == renderContext->getClipContentID());
        renderer.restore();

        for (bool logicalFlush : {false, true})
        {
            if (logicalFlush)
            {
                renderContext->logicalFlush();
            }
            else
            {
                renderContext->flush({.renderTarget = renderTarget.get()});
                renderContext->beginFrame(s_frameDescriptor);
            }

            // The clip content ID gets reset after a flush.
            CHECK(renderContext->getClipContentID() == 0);

            // The clip gets cleared after a flush, so this should start over
            // with a new clip ID.
            renderer.save();
            renderer.clipPath(pathA.get());
            renderer.clipPath(pathC.get());
            renderer.clipPath(pathB.get());
            renderer.drawPath(drawablePath.get(), paint.get());
            CHECK(clipID != renderContext->getClipContentID());
            CHECK(renderContext->getClipContentID() == 3);
            clipID = renderContext->getClipContentID();
            renderer.restore();

            // And now that we're in the new flush, doing it again will reuse
            // the same clipID.
            renderer.save();
            renderer.clipPath(pathA.get());
            renderer.clipPath(pathC.get());
            renderer.clipPath(pathB.get());
            renderer.drawPath(drawablePath.get(), paint.get());
            renderer.restore();
            CHECK(clipID == renderContext->getClipContentID());

            // Draw another clip so the clipID isn't 1 anymore.
            renderer.save();
            renderer.clipPath(pathA.get());
            renderer.drawPath(drawablePath.get(), paint.get());
            renderer.restore();
            CHECK(clipID != renderContext->getClipContentID());
            clipID = renderContext->getClipContentID();
        }

        renderContext->flush({.renderTarget = renderTarget.get()});
    }
}

static RawPath& make_oval(const AABB& bounds)
{
    static RawPath path;
    path.rewind();
    path.addOval(bounds);
    return path;
};

// Ensures that a flush only invalidates the current clip once, and that we can
// start reusing it again after.
TEST_CASE("clip-flush-clip-clip", "RiveRenderer")
{
    std::unique_ptr<RenderContext> renderContext =
        RenderContextNULL::MakeContext();

    renderContext->beginFrame(s_frameDescriptor);

    auto pathA = renderContext->makeRenderPath(make_oval({0, 0, 1, 5}),
                                               FillRule::nonZero);
    auto pathB = renderContext->makeRenderPath(make_oval({0, 0, 2, 6}),
                                               FillRule::nonZero);
    auto pathC = renderContext->makeRenderPath(make_oval({0, 0, 3, 7}),
                                               FillRule::nonZero);
    auto pathD = renderContext->makeRenderPath(make_oval({0, 0, 4, 8}),
                                               FillRule::nonZero);
    auto drawablePath = renderContext->makeRenderPath(make_oval({0, 0, 9, 10}),
                                                      FillRule::nonZero);

    auto paint = renderContext->makeRenderPaint();
    paint->color(0xffffffff);

    RiveRenderer renderer(renderContext.get());

    renderer.save();
    renderer.clipPath(pathD.get());
    renderer.drawPath(drawablePath.get(), paint.get());
    uint32_t clipID = renderContext->getClipContentID();
    CHECK(clipID != 0);
    renderer.restore();

    renderer.clipPath(pathC.get());
    renderer.clipPath(pathB.get());
    renderer.clipPath(pathA.get());
    renderer.drawPath(drawablePath.get(), paint.get());
    CHECK(renderContext->getClipContentID() != clipID);
    clipID = renderContext->getClipContentID();

    // Flushing should invalidate the clip.
    renderContext->logicalFlush();
    renderer.drawPath(drawablePath.get(), paint.get());
    CHECK(renderContext->getClipContentID() != clipID);
    clipID = renderContext->getClipContentID();

    // The clip should now get reused again for subsequent draws.
    for (size_t i = 0; i < 3; ++i)
    {
        renderer.drawPath(drawablePath.get(), paint.get());
        CHECK(renderContext->getClipContentID() == clipID);
    }

    auto renderTarget =
        renderContext->static_impl_cast<RenderContextNULL>()->makeRenderTarget(
            s_frameDescriptor.renderTargetWidth,
            s_frameDescriptor.renderTargetHeight);
    renderContext->flush({.renderTarget = renderTarget.get()});
}

static RawPath& make_cusp(const IAABB& rect)
{
    static RawPath path;
    path.rewind();
    path.moveTo(rect.left, rect.top);
    path.lineTo(rect.right, rect.bottom);
    path.lineTo(rect.right, rect.top);
    path.lineTo(rect.left, rect.bottom);
    return path;
}

// Check that clip readBounds and contentBounds get tracked properly.
TEST_CASE("clip-content-read-bounds", "RiveRenderer")
{
    std::unique_ptr<RenderContext> renderContext =
        RenderContextNULL::MakeContext();

    auto renderTarget =
        renderContext->static_impl_cast<RenderContextNULL>()->makeRenderTarget(
            s_frameDescriptor.renderTargetWidth,
            s_frameDescriptor.renderTargetHeight);
    RenderContext::FrameDescriptor frameDescriptor = s_frameDescriptor;
    frameDescriptor.msaaSampleCount = 4;
    renderContext->beginFrame(frameDescriptor);

    auto cuspA = static_rcp_cast<RiveRenderPath>(
        renderContext->makeRenderPath(make_cusp({0, 0, 1, 5}),
                                      FillRule::nonZero));
    auto cuspB = static_rcp_cast<RiveRenderPath>(
        renderContext->makeRenderPath(make_cusp({-1, -1, 1, 1}),
                                      FillRule::nonZero));
    auto cuspC = static_rcp_cast<RiveRenderPath>(
        renderContext->makeRenderPath(make_cusp({0, 0, 5, 1}),
                                      FillRule::nonZero));

    auto paint = renderContext->makeRenderPaint();
    paint->color(0xffffffff);

    // Clip read bounds are affected by the view matrix.
    Mat2D xforms[] = {Mat2D(),
                      Mat2D::fromTranslate(3, 1),
                      Mat2D::fromScale(3, 2),
                      Mat2D::fromTranslate(-1, 3).scale({2, 3})};

    RiveRenderer renderer(renderContext.get());
    for (const Mat2D& m : xforms)
    {
        renderer.save();
        renderer.clipPath(cuspA.get());
        renderer.transform(m);
        renderer.drawPath(cuspB.get(), paint.get());

        uint32_t clipAID = renderContext->getClipContentID();
        REQUIRE(clipAID != 0);
        // clipA is not transformed.
        auto clipAContentBounds = cuspA->getBounds().roundOut();
        CHECK(renderContext->getClipContentBounds(clipAID) ==
              clipAContentBounds);
        CHECK(renderContext->getClipReadBounds(clipAID) ==
              m.mapBoundingBox({-1, -1, 1, 1}).roundOut());

        renderer.clipPath(cuspC.get());
        renderer.drawPath(cuspB.get(), paint.get());
        uint32_t clipCID = renderContext->getClipContentID();
        REQUIRE(clipCID != 0);
        REQUIRE(clipCID != clipAID);
        auto clipCContentBounds =
            m.mapBoundingBox(cuspC->getBounds().roundOut()).roundOut();
        CHECK(renderContext->getClipContentBounds(clipCID) ==
              clipCContentBounds);
        CHECK(renderContext->getClipReadBounds(clipCID) ==
              m.mapBoundingBox({-1, -1, 1, 1}).roundOut());

        // clipAID read bounds should have expanded from the nested clipping.
        CHECK(renderContext->getClipContentBounds(clipAID) ==
              clipAContentBounds);
        CHECK(renderContext->getClipReadBounds(clipAID) ==
              m.mapBoundingBox({-1, -1, 5, 1}).roundOut());

        // Each nested clip is read only by the one directly below it.
        auto cusp6 = static_rcp_cast<RiveRenderPath>(
            renderContext->makeRenderPath(make_cusp({0, 0, 1, 6}),
                                          FillRule::nonZero));
        auto cusp7 = static_rcp_cast<RiveRenderPath>(
            renderContext->makeRenderPath(make_cusp({0, 0, 1, 7}),
                                          FillRule::nonZero));
        auto cusp8 = static_rcp_cast<RiveRenderPath>(
            renderContext->makeRenderPath(make_cusp({0, 0, 1, 8}),
                                          FillRule::nonZero));
        auto cusp9 = static_rcp_cast<RiveRenderPath>(
            renderContext->makeRenderPath(make_cusp({0, 0, 1, 9}),
                                          FillRule::nonZero));

        renderer.clipPath(cusp9.get());
        renderer.drawPath(cuspB.get(), paint.get());
        uint32_t clip9ID = renderContext->getClipContentID();

        renderer.clipPath(cusp8.get());
        renderer.drawPath(cuspB.get(), paint.get());
        uint32_t clip8ID = renderContext->getClipContentID();

        renderer.save();
        renderer.clipPath(cusp7.get());
        renderer.drawPath(cuspB.get(), paint.get());
        uint32_t clip7ID = renderContext->getClipContentID();

        renderer.clipPath(cusp6.get());
        renderer.drawPath(cuspC.get(), paint.get());
        uint32_t clip6ID = renderContext->getClipContentID();

        // clipA bounds should not have been affected by deeper nested clips.
        CHECK(renderContext->getClipContentBounds(clipAID) ==
              clipAContentBounds);
        CHECK(renderContext->getClipReadBounds(clipAID) ==
              m.mapBoundingBox({-1, -1, 5, 1}).roundOut());

        // Each nested clip is read only by the one directly below it. Outer
        // clips are not read by the draw either.
        CHECK(renderContext->getClipContentBounds(clipCID) ==
              clipCContentBounds);
        CHECK(renderContext->getClipReadBounds(clipCID) ==
              m.mapBoundingBox({-1, -1, 1, 9}).roundOut());
        auto clip9ContentBounds =
            m.mapBoundingBox(cusp9->getBounds().roundOut()).roundOut();
        CHECK(renderContext->getClipContentBounds(clip9ID) ==
              clip9ContentBounds);
        CHECK(renderContext->getClipReadBounds(clip9ID) ==
              m.mapBoundingBox({-1, -1, 1, 8}).roundOut());
        auto clip8ContentBounds =
            m.mapBoundingBox(cusp8->getBounds().roundOut()).roundOut();
        CHECK(renderContext->getClipContentBounds(clip8ID) ==
              clip8ContentBounds);
        CHECK(renderContext->getClipReadBounds(clip8ID) ==
              m.mapBoundingBox({-1, -1, 1, 7}).roundOut());
        auto clip7ContentBounds =
            m.mapBoundingBox(cusp7->getBounds().roundOut()).roundOut();
        CHECK(renderContext->getClipContentBounds(clip7ID) ==
              clip7ContentBounds);
        CHECK(renderContext->getClipReadBounds(clip7ID) ==
              m.mapBoundingBox({-1, -1, 1, 6}).roundOut());
        auto clip6ContentBounds =
            m.mapBoundingBox(cusp6->getBounds().roundOut()).roundOut();
        CHECK(renderContext->getClipContentBounds(clip6ID) ==
              clip6ContentBounds);
        CHECK(renderContext->getClipReadBounds(clip6ID) ==
              m.mapBoundingBox({0, 0, 5, 1}).roundOut());

        // Pop back and do some more reading from clip8.
        renderer.restore();
        renderer.drawPath(cuspC.get(), paint.get());
        uint32_t secondClip8ID = renderContext->getClipContentID();

        // Since clip8 got obliterated and redrawn, this next read shouldn't
        // affect it.
        CHECK(secondClip8ID != clip8ID);
        CHECK(renderContext->getClipContentBounds(clip8ID) ==
              clip8ContentBounds);
        CHECK(renderContext->getClipReadBounds(clip8ID) ==
              m.mapBoundingBox({-1, -1, 1, 7}).roundOut());
        // ... But should affect the clip currently in the clip buffer.
        CHECK(renderContext->getClipContentBounds(secondClip8ID) ==
              clip8ContentBounds);
        CHECK(renderContext->getClipReadBounds(secondClip8ID) ==
              m.mapBoundingBox({0, 0, 5, 1}).roundOut());

        renderer.clipPath(cuspB.get());
        renderer.drawPath(cuspC.get(), paint.get());
        CHECK(renderContext->getClipContentBounds(clip8ID) ==
              clip8ContentBounds);
        CHECK(renderContext->getClipReadBounds(clip8ID) ==
              m.mapBoundingBox({-1, -1, 1, 7}).roundOut());
        CHECK(renderContext->getClipContentBounds(secondClip8ID) ==
              clip8ContentBounds);
        CHECK(renderContext->getClipReadBounds(secondClip8ID) ==
              m.mapBoundingBox({-1, -1, 5, 1}).roundOut());
        uint32_t clipBID = renderContext->getClipContentID();
        auto clipBContentBounds =
            m.mapBoundingBox(cuspB->getBounds().roundOut()).roundOut();
        CHECK(renderContext->getClipContentBounds(clipBID) ==
              clipBContentBounds);
        CHECK(renderContext->getClipReadBounds(clipBID) ==
              m.mapBoundingBox({0, 0, 5, 1}).roundOut());

        renderer.restore();

        // Clip content bounds should never change.
        CHECK(renderContext->getClipContentBounds(clipAID) ==
              clipAContentBounds);
        CHECK(renderContext->getClipContentBounds(clipCID) ==
              clipCContentBounds);
        CHECK(renderContext->getClipContentBounds(clip9ID) ==
              clip9ContentBounds);
        CHECK(renderContext->getClipContentBounds(clip8ID) ==
              clip8ContentBounds);
        CHECK(renderContext->getClipContentBounds(clip7ID) ==
              clip7ContentBounds);
        CHECK(renderContext->getClipContentBounds(clip6ID) ==
              clip6ContentBounds);
        CHECK(renderContext->getClipContentBounds(secondClip8ID) ==
              clip8ContentBounds);
        CHECK(renderContext->getClipContentBounds(clipBID) ==
              clipBContentBounds);
    }

    renderContext->flush({.renderTarget = renderTarget.get()});
}

static RawPath& make_rect(const AABB& rect)
{
    static RawPath path;
    path.rewind();
    path.addRect(rect);
    return path;
};

#define CHECK_AABB_NEARLY_EQUAL(AABB, L, T, R, B)                              \
    {                                                                          \
        CHECK(AABB.left() == Approx(L));                                       \
        CHECK(AABB.top() == Approx(T));                                        \
        CHECK(AABB.right() == Approx(R));                                      \
        CHECK(AABB.bottom() == Approx(B));                                     \
    }

// Ensures that rectangular clips are handled as "clipRects" where possible,
// instead of going to the clip buffer.
TEST_CASE("clip-rects", "RiveRenderer")
{
    std::unique_ptr<RenderContext> renderContext =
        RenderContextNULL::MakeContext();

    renderContext->beginFrame(s_frameDescriptor);

    auto rectA = renderContext->makeRenderPath(make_rect({0, 0, 1, 5}),
                                               FillRule::nonZero);
    auto rectB = renderContext->makeRenderPath(make_rect({1, -1, 2, 4}),
                                               FillRule::nonZero);
    auto rectC = renderContext->makeRenderPath(make_rect({0, 0, 3, 6}),
                                               FillRule::nonZero);
    auto ovalA = renderContext->makeRenderPath(make_oval({0, 0, 1, 4}),
                                               FillRule::nonZero);
    auto ovalB = renderContext->makeRenderPath(make_oval({0, 0, 2, 5}),
                                               FillRule::nonZero);
    auto ovalC = renderContext->makeRenderPath(make_oval({0, 0, 3, 6}),
                                               FillRule::nonZero);
    auto drawablePath = renderContext->makeRenderPath(make_oval({0, 0, 7, 8}),
                                                      FillRule::nonZero);

    auto paint = renderContext->makeRenderPaint();
    paint->color(0xffffffff);

    RiveRenderer renderer(renderContext.get());

    // A single rect is always handled as a clipRect.
    renderer.save();
    renderer.clipPath(rectA.get());
    CHECK(renderContext->getClipContentID() == 0);
    renderer.drawPath(drawablePath.get(), paint.get());
    CHECK(renderContext->getClipContentID() == 0);
    renderer.restore();

    renderer.save();

    // The nested rect is always handled as a clipRect, even if it's nested.
    renderer.clipPath(ovalA.get());
    renderer.drawPath(drawablePath.get(), paint.get());
    uint32_t clipID = renderContext->getClipContentID();
    CHECK(clipID != 0);
    CHECK(!renderer.hasClipRect());
    renderer.clipPath(rectA.get());
    renderer.drawPath(drawablePath.get(), paint.get());
    CHECK(renderContext->getClipContentID() == clipID);
    CHECK(renderer.hasClipRect());
    CHECK_AABB_NEARLY_EQUAL(renderer.getClipRect(), 0, 0, 1, 5);
    CHECK(renderer.getClipRectMatrix() == Mat2D());

    // Doubly nested rectangles get combined into a single clipRect.
    renderer.clipPath(ovalB.get());
    renderer.drawPath(drawablePath.get(), paint.get());
    CHECK(renderContext->getClipContentID() != clipID);
    clipID = renderContext->getClipContentID();
    renderer.clipPath(rectB.get());
    renderer.drawPath(drawablePath.get(), paint.get());
    CHECK(renderContext->getClipContentID() == clipID);
    CHECK(renderer.hasClipRect());
    CHECK_AABB_NEARLY_EQUAL(renderer.getClipRect(), 1, 0, 1, 4);
    CHECK(renderer.getClipRectMatrix() == Mat2D());

    // Nested rectangles don't get combined if their matrices are incompatible.
    renderer.save();
    renderer.rotate(1);
    renderer.clipPath(ovalA.get());
    renderer.drawPath(drawablePath.get(), paint.get());
    CHECK(renderContext->getClipContentID() != clipID);
    clipID = renderContext->getClipContentID();
    renderer.clipPath(rectB.get());
    renderer.drawPath(drawablePath.get(), paint.get());
    CHECK(renderContext->getClipContentID() != clipID);
    clipID = renderContext->getClipContentID();
    renderer.restore();
    CHECK(renderer.hasClipRect());
    CHECK_AABB_NEARLY_EQUAL(renderer.getClipRect(), 1, 0, 1, 4);
    CHECK(renderer.getClipRectMatrix() == Mat2D());

    // Nested rectangles DO get combined if their matrices ARE compatible.
    renderer.save();
    renderer.scale(1, 2);
    renderer.translate(3, 4);
    renderer.clipPath(ovalC.get());
    renderer.drawPath(drawablePath.get(), paint.get());
    CHECK(renderContext->getClipContentID() != clipID);
    clipID = renderContext->getClipContentID();
    renderer.clipPath(rectC.get());
    renderer.drawPath(drawablePath.get(), paint.get());
    CHECK(renderContext->getClipContentID() == clipID);
    CHECK(renderer.hasClipRect());
    CHECK_AABB_NEARLY_EQUAL(renderer.getClipRect(), 3, 8, 1, 4);
    CHECK(renderer.getClipRectMatrix() == Mat2D());
    renderer.restore();

    renderer.restore();

    renderContext->logicalFlush();

    renderer.save();

    // Scaling and transforming within the same baseline space should still
    // allow for clipRects to be combined.
    Mat2D m = Mat2D::fromRotation(1) * Mat2D{.3f, .3f, .9f, -.7f, .4f, .2f};
    renderer.transform(m);
    renderer.clipPath(rectA.get());
    renderer.drawPath(drawablePath.get(), paint.get());
    CHECK(renderContext->getClipContentID() == 0);
    CHECK(renderer.hasClipRect());
    CHECK(renderer.getClipRect() == AABB{0, 0, 1, 5});
    CHECK(renderer.getClipRectMatrix() == m);
    renderer.clipPath(rectB.get());
    renderer.drawPath(drawablePath.get(), paint.get());
    CHECK(renderContext->getClipContentID() == 0);
    renderer.translate(.9f, -.1f);
    renderer.clipPath(rectB.get());
    renderer.drawPath(drawablePath.get(), paint.get());
    CHECK(renderContext->getClipContentID() == 0);
    CHECK(renderer.hasClipRect());
    CHECK_AABB_NEARLY_EQUAL(renderer.getClipRect(), 1.9f, 0, 1, 3.9f);
    CHECK(renderer.getClipRectMatrix() == m);
    renderer.scale(10, 2);
    renderer.clipPath(rectB.get());
    renderer.drawPath(drawablePath.get(), paint.get());
    CHECK(renderContext->getClipContentID() == 0);

    // Even 90 degree rotations work!
    renderer.rotate(math::PI / 2);
    renderer.clipPath(rectB.get());
    renderer.drawPath(drawablePath.get(), paint.get());
    CHECK(renderer.hasClipRect());
    CHECK_AABB_NEARLY_EQUAL(renderer.getClipRect(), 10.9f, 1.9f, 1, 3.9f);
    CHECK(renderer.getClipRectMatrix() == m);
    CHECK(renderContext->getClipContentID() == 0);
    renderer.rotate(math::PI);
    renderer.translate(-10, 0);
    renderer.clipPath(rectB.get());
    renderer.drawPath(drawablePath.get(), paint.get());
    CHECK(renderer.hasClipRect());
    CHECK_AABB_NEARLY_EQUAL(renderer.getClipRect(), 10.9f, 15.9f, 1, 3.9f);
    CHECK(renderer.getClipRectMatrix() == m);
    CHECK(renderContext->getClipContentID() == 0);
    renderer.rotate(-math::PI / 2);
    renderer.clipPath(rectB.get());
    renderer.drawPath(drawablePath.get(), paint.get());
    CHECK(renderContext->getClipContentID() == 0);
    CHECK(renderer.hasClipRect());
    CHECK_AABB_NEARLY_EQUAL(renderer.getClipRect(), 10.9f, 15.9f, -9.1f, 3.9f);
    CHECK(renderer.getClipRectMatrix() == m);

    // And flips!
    renderer.scale(1, -1);
    renderer.clipPath(rectB.get());
    renderer.drawPath(drawablePath.get(), paint.get());
    CHECK(renderContext->getClipContentID() == 0);
    CHECK(renderer.hasClipRect());
    CHECK_AABB_NEARLY_EQUAL(renderer.getClipRect(), 10.9f, 17.9f, -9.1f, 3.9f);
    CHECK(renderer.getClipRectMatrix() == m);
    renderer.scale(-1, -1);
    renderer.translate(2, 0);
    renderer.clipPath(rectB.get());
    renderer.drawPath(drawablePath.get(), paint.get());
    CHECK(renderContext->getClipContentID() == 0);
    CHECK(renderer.hasClipRect());
    CHECK_AABB_NEARLY_EQUAL(renderer.getClipRect(), 30.9f, 17.9f, -9.1f, 3.9f);
    CHECK(renderer.getClipRectMatrix() == m);

    // ... but once the matrices become incompatible, we can't intersect with
    // the clipRect anymore.
    renderer.transform(Mat2D{1, .1f, 0, 1, 0, 0});
    renderer.clipPath(rectB.get());
    renderer.drawPath(drawablePath.get(), paint.get());
    CHECK(renderContext->getClipContentID() != 0);
    CHECK_AABB_NEARLY_EQUAL(renderer.getClipRect(), 30.9f, 17.9f, -9.1f, 3.9f);
    CHECK(renderer.getClipRectMatrix() == m);

    renderer.restore();

    auto renderTarget =
        renderContext->static_impl_cast<RenderContextNULL>()->makeRenderTarget(
            s_frameDescriptor.renderTargetWidth,
            s_frameDescriptor.renderTargetHeight);
    renderContext->flush({.renderTarget = renderTarget.get()});
}

// This test passes by not triggering assertions within RiveRenderer and
// RenderContext due to drawing empty paths.
TEST_CASE("empty-paths", "RiveRenderer")
{
    std::unique_ptr<RenderContext> renderContext =
        RenderContextNULL::MakeContext();
    renderContext->beginFrame(s_frameDescriptor);

    RiveRenderer renderer(renderContext.get());

    // Draw an empty path.
    auto paint = renderContext->makeRenderPaint();
    auto path = renderContext->makeEmptyRenderPath();
    renderer.drawPath(path.get(), paint.get());

    // Draw another empty path.
    path = renderContext->makeEmptyRenderPath();
    path->moveTo(1, 1);
    path->moveTo(2, 2);
    path->lineTo(2, 2);
    renderer.drawPath(path.get(), paint.get());

    // Draw a path with tons of vertices that definitely trigger a first-draw
    // realloc.
    path = renderContext->makeEmptyRenderPath();
    auto rand100 = []() {
        return static_cast<float>(rand()) * 100.f /
               static_cast<float>(RAND_MAX);
    };
    for (size_t i = 0; i < 100000; ++i)
    {
        path->cubicTo(rand100(),
                      rand100(),
                      rand100(),
                      rand100(),
                      rand100(),
                      rand100());
    }
    renderer.drawPath(path.get(), paint.get());

    // Draw one more empty path.
    renderer.drawPath(renderContext->makeEmptyRenderPath().get(), paint.get());

    auto renderTarget =
        renderContext->static_impl_cast<RenderContextNULL>()->makeRenderTarget(
            s_frameDescriptor.renderTargetWidth,
            s_frameDescriptor.renderTargetHeight);
    renderContext->flush({.renderTarget = renderTarget.get()});
}

// This test passes by not triggering assertions within RiveRenderer and
// RenderContext due to drawing empty paths composed of nothing but moves.
TEST_CASE("empty-paths2", "RiveRenderer")
{
    std::unique_ptr<RenderContext> renderContext =
        RenderContextNULL::MakeContext();
    renderContext->beginFrame(s_frameDescriptor);

    RiveRenderer renderer(renderContext.get());

    auto paint = renderContext->makeRenderPaint();

    // Draw path composed of nothing but moves.
    auto path = renderContext->makeEmptyRenderPath();
    path->moveTo(0, 0);
    path->moveTo(1, 2);
    path->moveTo(3, 4);
    path->moveTo(5, 6);
    renderer.drawPath(path.get(), paint.get());

    // Draw a large path (that triggers the triangulator) composed of nothing
    // but moves.
    path = renderContext->makeEmptyRenderPath();
    path->moveTo(0, 0);
    path->moveTo(1000, 2000);
    path->moveTo(3000, 4000);
    path->moveTo(5000, 6000);
    path->moveTo(7000, 8000);
    renderer.drawPath(path.get(), paint.get());

    auto renderTarget =
        renderContext->static_impl_cast<RenderContextNULL>()->makeRenderTarget(
            s_frameDescriptor.renderTargetWidth,
            s_frameDescriptor.renderTargetHeight);
    renderContext->flush({.renderTarget = renderTarget.get()});
}

TEST_CASE("IsAABB", "RiveRenderer")
{
    AABB rect;
    for (bool doClose : {true, false})
    {
        RawPath path;
        if (doClose)
        {
            path.addRect(AABB{0, 0, 10, 10}, PathDirection::ccw);
        }
        else
        {
            path.lineTo(10, 0);
            path.lineTo(10, 10);
            path.lineTo(0, 10);
        }
        CHECK(RiveRenderer::IsAABB(path, &rect));
        CHECK(rect == AABB{0, 0, 10, 10});

        path.reset();
        if (doClose)
        {
            path.addRect(AABB{1, 2, -1, 5}, PathDirection::cw);
        }
        else
        {
            path.moveTo(1, 2);
            path.lineTo(-1, 2);
            path.lineTo(-1, 5);
            path.lineTo(1, 5);
        }
        CHECK(RiveRenderer::IsAABB(path, &rect));
        CHECK(rect == AABB{-1, 2, 1, 5});

        path.reset();
        if (doClose)
        {
            path.addRect(AABB{0, 0, 10, 10}, PathDirection::ccw);
            path.addRect(AABB{1, 2, -1, 5}, PathDirection::ccw);
        }
        else
        {
            path.lineTo(10, 0);
            path.lineTo(10, 10);
            path.lineTo(0, 10);
            path.lineTo(0, 0);
            path.lineTo(10, 0);
        }
        CHECK(!RiveRenderer::IsAABB(path, &rect));
    }

    RawPath path;
    path.moveTo(1, 0);
    CHECK(!RiveRenderer::IsAABB(path, &rect));
    path.lineTo(0, 0);
    CHECK(!RiveRenderer::IsAABB(path, &rect));
    path.lineTo(0, 1);
    CHECK(!RiveRenderer::IsAABB(path, &rect));
    path.lineTo(1, 1);
    CHECK(RiveRenderer::IsAABB(path, &rect));
    path.close();
    CHECK(RiveRenderer::IsAABB(path, &rect));
    CHECK(rect == AABB{0, 0, 1, 1});

    path.reset();
    path.moveTo(0, 0);
    path.lineTo(1, 0);
    path.lineTo(1, 1);
    path.lineTo(0, 1.1f);
    path.close();
    CHECK(!RiveRenderer::IsAABB(path, &rect));

    // Accept additional verbs and points after the AABB as long as every point
    // after is equal to p0.
    path.reset();
    path.moveTo(0, 0);
    path.lineTo(1, 0);
    path.lineTo(1, 1);
    path.lineTo(0, 1);
    CHECK(RiveRenderer::IsAABB(path, &rect));
    CHECK(rect == AABB{0, 0, 1, 1});
    path.close();
    CHECK(RiveRenderer::IsAABB(path, &rect));
    CHECK(rect == AABB{0, 0, 1, 1});
    path.reset();
    path.moveTo(1, 1);
    path.lineTo(0, 1);
    path.lineTo(0, 0);
    path.lineTo(1, 0);
    CHECK(RiveRenderer::IsAABB(path, &rect));
    CHECK(rect == AABB{0, 0, 1, 1});
    path.lineTo(1, 1);
    CHECK(RiveRenderer::IsAABB(path, &rect));
    CHECK(rect == AABB{0, 0, 1, 1});
    path.close();
    CHECK(RiveRenderer::IsAABB(path, &rect));
    CHECK(rect == AABB{0, 0, 1, 1});
    path.close();
    CHECK(RiveRenderer::IsAABB(path, &rect));
    CHECK(rect == AABB{0, 0, 1, 1});
    path.cubicTo(1, 1, 1, 1, 1, 1);
    CHECK(RiveRenderer::IsAABB(path, &rect));
    CHECK(rect == AABB{0, 0, 1, 1});
    path.quadTo(1, 1, 1, 1);
    CHECK(RiveRenderer::IsAABB(path, &rect));
    CHECK(rect == AABB{0, 0, 1, 1});
}

// Check that paths with NaN vertices don't crash.
TEST_CASE("nan-render-path", "RiveRenderer")
{
    std::unique_ptr<RenderContext> renderContext =
        RenderContextNULL::MakeContext();
    RiveRenderer renderer(renderContext.get());
    auto paint = renderContext->makeRenderPaint();

    auto finitePath = renderContext->makeEmptyRenderPath();
    finitePath->moveTo(1, 2);
    finitePath->lineTo(3, 4);
    finitePath->cubicTo(5, 6, 7, 8, 9, 10);
    finitePath->cubicTo(11, 12, 13, 14, 15, 16);
    finitePath->lineTo(17, 18);

    auto nan = std::numeric_limits<float>::quiet_NaN();
    auto nanPath = renderContext->makeEmptyRenderPath();
    nanPath->moveTo(nan, nan);
    nanPath->lineTo(nan, nan);
    nanPath->cubicTo(nan, nan, nan, nan, nan, nan);
    nanPath->cubicTo(nan, nan, nan, nan, nan, nan);
    nanPath->lineTo(nan, nan);

    renderContext->beginFrame(s_frameDescriptor);

    renderer.drawPath(nanPath.get(), paint.get());

    renderer.save();
    renderer.clipPath(nanPath.get());
    renderer.drawPath(finitePath.get(), paint.get());
    renderer.restore();

    renderer.save();
    renderer.transform({nan, nan, nan, nan, nan, nan});
    renderer.drawPath(finitePath.get(), paint.get());
    renderer.clipPath(finitePath.get());
    renderer.drawPath(nanPath.get(), paint.get());
    renderer.restore();

    CHECK(renderContext->getClipContentID() == 0);

    renderer.save();
    renderer.clipPath(nanPath.get());
    renderer.drawPath(finitePath.get(), paint.get());
    // NaN paths cause an immediatedly empty clip stack that just discards
    // elements.
    CHECK(renderContext->getClipContentID() == 0);
    renderer.clipPath(finitePath.get());
    renderer.drawPath(finitePath.get(), paint.get());
    CHECK(renderContext->getClipContentID() == 0);
    renderer.restore();

    renderer.save();
    renderer.clipPath(finitePath.get());
    renderer.drawPath(finitePath.get(), paint.get());
    // Non-NaN paths update the clip content ID.
    CHECK(renderContext->getClipContentID() != 0);
    renderer.restore();

    // move(nan, nan), close() found a crash.
    auto moveNaN = renderContext->makeEmptyRenderPath();
    moveNaN->moveTo(nan, nan);
    moveNaN->close();
    renderer.drawPath(moveNaN.get(), paint.get());

    auto renderTarget =
        renderContext->static_impl_cast<RenderContextNULL>()->makeRenderTarget(
            s_frameDescriptor.renderTargetWidth,
            s_frameDescriptor.renderTargetHeight);
    renderContext->flush({.renderTarget = renderTarget.get()});
}

// Check that edge cases in stroke thickness don't crash or assert.
TEST_CASE("stroke-thickness", "RiveRenderer")
{
    std::unique_ptr<RenderContext> renderContext =
        RenderContextNULL::MakeContext();

    renderContext->beginFrame(s_frameDescriptor);

    auto path = renderContext->makeEmptyRenderPath();
    path->lineTo(100, 100);
    path->lineTo(100, 0);

    auto paint = renderContext->makeRenderPaint();
    paint->style(RenderPaintStyle::stroke);

    RiveRenderer renderer(renderContext.get());

    // Check a stroke thickness that truncates to 0 when converted to a radius
    // (i.e., when multiplied by .5).
    paint->thickness(math::bit_cast<float>(1u));
    renderer.drawPath(path.get(), paint.get());

    // Check NaN stroke thickness.
    paint->thickness(std::numeric_limits<float>::quiet_NaN());
    renderer.drawPath(path.get(), paint.get());

    // Check infinite stroke thickness.
    paint->thickness(std::numeric_limits<float>::infinity());
    renderer.drawPath(path.get(), paint.get());

    // Check -infinite stroke thickness.
    paint->thickness(-std::numeric_limits<float>::infinity());
    renderer.drawPath(path.get(), paint.get());

    auto renderTarget =
        renderContext->static_impl_cast<RenderContextNULL>()->makeRenderTarget(
            s_frameDescriptor.renderTargetWidth,
            s_frameDescriptor.renderTargetHeight);
    renderContext->flush({.renderTarget = renderTarget.get()});
}

// When AABB::height() evaluates inf - inf, the result is nan.
TEST_CASE("nan-testcase", "RiveRenderer")
{
    std::unique_ptr<RenderContext> nullContext =
        RenderContextNULL::MakeContext();
    nullContext->beginFrame({
        .renderTargetWidth = 2279,
        .renderTargetHeight = 710,
    });

    auto path = nullContext->makeEmptyRenderPath();
    path->moveTo(0, std::numeric_limits<float>::infinity());
    path->lineTo(1, std::numeric_limits<float>::infinity());

    auto paint = nullContext->makeRenderPaint();

    RiveRenderer renderer(nullContext.get());
    renderer.save();
    renderer.align(Fit::fill,
                   Alignment::center,
                   {144, 77, 885, 168},
                   {0, 0, 100, 100});
    renderer.drawPath(path.get(), paint.get());
    renderer.restore();

    auto renderTarget =
        nullContext->static_impl_cast<RenderContextNULL>()->makeRenderTarget(
            2279,
            710);
    nullContext->flush({.renderTarget = renderTarget.get()});
}

// This path was found by the fuzzer and triggered an assertion.
TEST_CASE("nan-testcase2", "RiveRenderer")
{
    std::unique_ptr<RenderContext> nullContext =
        RenderContextNULL::MakeContext();
    nullContext->beginFrame({
        .renderTargetWidth = 2279,
        .renderTargetHeight = 710,
    });

    auto inf = std::numeric_limits<float>::infinity();
    auto nan = std::numeric_limits<float>::quiet_NaN();
    auto path = nullContext->makeEmptyRenderPath();
    path->lineTo(1, -inf);
    path->moveTo(nan, -inf);

    auto paint = nullContext->makeRenderPaint();

    RiveRenderer renderer(nullContext.get());
    renderer.save();
    renderer.align(Fit::fill,
                   {0.260523f, 0.803141f},
                   {144.649948f, 77.102341f, 885.225891f, 168.455017f},
                   {0, 0, 100, 100});
    renderer.drawPath(path.get(), paint.get());
    renderer.restore();

    auto renderTarget =
        nullContext->static_impl_cast<RenderContextNULL>()->makeRenderTarget(
            2279,
            710);
    nullContext->flush({.renderTarget = renderTarget.get()});
}

// Attempting to create a gradient with invalid stops returns null.
TEST_CASE("invalid-gradient-stops", "RiveRenderer")
{
    std::vector<ColorInt> colors(100);
    std::unique_ptr<RenderContext> nullContext =
        RenderContextNULL::MakeContext();
    auto checkGradients = [&](std::vector<float> stops, bool shouldSucceed) {
        if (stops.size() > colors.size())
        {
            return false;
        }
        if (static_cast<bool>(nullContext->makeLinearGradient(0,
                                                              0,
                                                              0,
                                                              0,
                                                              colors.data(),
                                                              stops.data(),
                                                              stops.size())) !=
            shouldSucceed)
        {
            return false;
        }
        if (static_cast<bool>(nullContext->makeRadialGradient(0,
                                                              0,
                                                              0,
                                                              colors.data(),
                                                              stops.data(),
                                                              stops.size())) !=
            shouldSucceed)
        {
            return false;
        }
        return true;
    };
    CHECK(checkGradients({0, .5, 1}, true));

    auto inf = std::numeric_limits<float>::infinity();
    auto nan = std::numeric_limits<float>::quiet_NaN();

    // Empty gradients are invalid.
    CHECK(checkGradients({0}, true));
    CHECK(checkGradients({1}, true));
    CHECK(checkGradients({}, false));

    // Gradients outside 0..1 are invalid
    CHECK(checkGradients({0, .5f, 1.1f}, false));
    CHECK(checkGradients({0, .5f, inf}, false));
    CHECK(checkGradients({0, .5f, nan}, false));
    CHECK(checkGradients({-.1f, .5f, 1}, false));
    CHECK(checkGradients({-inf, .5f, 1}, false));
    CHECK(checkGradients({nan, .5f, 1}, false));

    // Unordered gradients are invalid.
    CHECK(checkGradients({0, .4F, .5F, 1}, true));
    CHECK(checkGradients({0, .5F, .4F, 1}, false));
    CHECK(checkGradients({.4f, 0, .5f, 1}, false));
    CHECK(checkGradients({0, .4f, 1, .5f}, false));
    CHECK(checkGradients({nan, .5f, 1}, false));
    CHECK(checkGradients({0, nan, 1}, false));
    CHECK(checkGradients({0, .5f, nan}, false));
}

// Drawing round caps with tiny, negative, and non-finite stroke thickness
// values should not crash.
TEST_CASE("round-cap-edge-values", "RiveRenderer")
{
    std::unique_ptr<RenderContext> renderContext =
        RenderContextNULL::MakeContext();

    renderContext->beginFrame(s_frameDescriptor);

    auto path = renderContext->makeEmptyRenderPath();
    path->moveTo(25, 25);
    path->lineTo(75, 75);

    auto emptyPath = renderContext->makeEmptyRenderPath();
    emptyPath->close();
    emptyPath->moveTo(50, 50);
    emptyPath->close();

    RiveRenderer renderer(renderContext.get());

    auto drawStrokes = [&](float thickness) {
        auto stroke = renderContext->makeRenderPaint();
        stroke->style(RenderPaintStyle::stroke);
        stroke->thickness(thickness);
        stroke->cap(StrokeCap::round);
        stroke->join(StrokeJoin::round);
        renderer.drawPath(path.get(), stroke.get());
        renderer.drawPath(emptyPath.get(), stroke.get());
    };

    for (float thickness = 1; thickness > 0; thickness /= 2)
    {
        drawStrokes(thickness);
        drawStrokes(-thickness);
    }
    drawStrokes(math::bit_cast<float>(1u));
    drawStrokes(std::numeric_limits<float>::infinity());
    drawStrokes(-std::numeric_limits<float>::infinity());
    drawStrokes(std::numeric_limits<float>::quiet_NaN());

    auto renderTarget =
        renderContext->static_impl_cast<RenderContextNULL>()->makeRenderTarget(
            s_frameDescriptor.renderTargetWidth,
            s_frameDescriptor.renderTargetHeight);
    renderContext->flush({.renderTarget = renderTarget.get()});
}

// Infinite draw bounds cast to {min32i, min32i, max32i, max32i}.
// Make sure these bounds don't overflow and cause assertions when working with
// the intersection board.
TEST_CASE("infinite-atomic-path", "RiveRenderer")
{
    std::unique_ptr<RenderContext> renderContext =
        RenderContextNULL::MakeContext();
    auto desc = s_frameDescriptor;
    desc.disableRasterOrdering = true;
    renderContext->beginFrame(desc);

    RiveRenderer renderer(renderContext.get());

    auto paint = renderContext->makeRenderPaint();
    auto path = renderContext->makeEmptyRenderPath();
    path->moveTo(-std::numeric_limits<float>::infinity(),
                 -std::numeric_limits<float>::infinity());
    path->lineTo(std::numeric_limits<float>::infinity(),
                 std::numeric_limits<float>::infinity());
    renderer.drawPath(path.get(), paint.get());

    auto renderTarget =
        renderContext->static_impl_cast<RenderContextNULL>()->makeRenderTarget(
            desc.renderTargetWidth,
            desc.renderTargetHeight);
    renderContext->flush({.renderTarget = renderTarget.get()});
}

#define filter(a) rive::ImageSampler::GetFilterOptionFromKey(a)
#define wrapx(a) rive::ImageSampler::GetWrapXOptionFromKey(a)
#define wrapy(a) rive::ImageSampler::GetWrapYOptionFromKey(a)

TEST_CASE("image-sample-option-conversions", "RiveRenderer")
{
    rive::ImageSampler defaultOptions = rive::ImageSampler::LinearClamp();
    rive::ImageSampler repeatNearestOptions = {rive::ImageWrap::repeat,
                                               rive::ImageWrap::repeat,
                                               rive::ImageFilter::nearest};

    auto defaultKey = defaultOptions.asKey();
    CHECK(defaultKey == 0);
    auto repeatNearestOptionsKey = repeatNearestOptions.asKey();
    CHECK(defaultKey != repeatNearestOptionsKey);
    CHECK(wrapx(repeatNearestOptionsKey) == rive::ImageWrap::repeat);
    CHECK(wrapy(repeatNearestOptionsKey) == rive::ImageWrap::repeat);
    CHECK(filter(repeatNearestOptionsKey) == rive::ImageFilter::nearest);

    rive::ImageSampler clampMirrorLinearOptions = {rive::ImageWrap::clamp,
                                                   rive::ImageWrap::mirror,
                                                   rive::ImageFilter::bilinear};

    auto clampMirrorLinearOptionsKey = clampMirrorLinearOptions.asKey();
    CHECK(wrapx(clampMirrorLinearOptionsKey) == rive::ImageWrap::clamp);
    CHECK(wrapy(clampMirrorLinearOptionsKey) == rive::ImageWrap::mirror);
    CHECK(filter(clampMirrorLinearOptionsKey) == rive::ImageFilter::bilinear);
    CHECK(clampMirrorLinearOptions != repeatNearestOptions);

    rive::ImageSampler repeatClampMipLinearOptions = {
        rive::ImageWrap::repeat,
        rive::ImageWrap::clamp,
        rive::ImageFilter::bilinear};

    auto repearClampMipLinearOptionsKey = repeatClampMipLinearOptions.asKey();
    CHECK(wrapx(repearClampMipLinearOptionsKey) == rive::ImageWrap::repeat);
    CHECK(wrapy(repearClampMipLinearOptionsKey) == rive::ImageWrap::clamp);
    CHECK(filter(repearClampMipLinearOptionsKey) ==
          rive::ImageFilter::bilinear);

    rive::ImageSampler clampRepeatMipNearestOptions = {
        rive::ImageWrap::clamp,
        rive::ImageWrap::repeat,
        rive::ImageFilter::bilinear};

    auto clampRepeatMipNearestOptionsKey = clampRepeatMipNearestOptions.asKey();
    CHECK(wrapx(clampRepeatMipNearestOptionsKey) == rive::ImageWrap::clamp);
    CHECK(wrapy(clampRepeatMipNearestOptionsKey) == rive::ImageWrap::repeat);
    CHECK(filter(clampRepeatMipNearestOptionsKey) ==
          rive::ImageFilter::bilinear);

    rive::ImageSampler mirrorClampMipNearestOptions = {
        rive::ImageWrap::mirror,
        rive::ImageWrap::clamp,
        rive::ImageFilter::nearest};

    auto mirrorClampMipNearestOptionsKey = mirrorClampMipNearestOptions.asKey();
    CHECK(wrapx(mirrorClampMipNearestOptionsKey) == rive::ImageWrap::mirror);
    CHECK(wrapy(mirrorClampMipNearestOptionsKey) == rive::ImageWrap::clamp);
    CHECK(filter(mirrorClampMipNearestOptionsKey) ==
          rive::ImageFilter::nearest);
}

// Ensure the renderer gracefully handles a null texture within a
// RiveRenderImage.
TEST_CASE("null-render-texture", "RiveRenderer")
{
    std::unique_ptr<RenderContext> renderContext =
        RenderContextNULL::MakeContext();
    auto desc = s_frameDescriptor;
    desc.disableRasterOrdering = true;
    renderContext->beginFrame(desc);

    RiveRenderer renderer(renderContext.get());

    class NullTextureImage : public RiveRenderImage
    {
    public:
        NullTextureImage(int width, int height) : RiveRenderImage(width, height)
        {
            // Never call resetTexture() so that our texture is null.
        }
    };
    auto nullTextureImage = make_rcp<NullTextureImage>(100, 100);
    CHECK(nullTextureImage->getTexture() == nullptr);

    renderer.drawImage(nullTextureImage.get(),
                       ImageSampler::LinearClamp(),
                       BlendMode::screen,
                       .5f);

    renderer.drawImageMesh(
        nullTextureImage.get(),
        ImageSampler::LinearClamp(),
        renderContext->makeRenderBuffer(RenderBufferType::vertex,
                                        RenderBufferFlags::none,
                                        80),
        renderContext->makeRenderBuffer(RenderBufferType::vertex,
                                        RenderBufferFlags::none,
                                        80),
        renderContext->makeRenderBuffer(RenderBufferType::index,
                                        RenderBufferFlags::none,
                                        100),
        10,
        50,
        BlendMode::colorBurn,
        .5f);

    auto renderTarget =
        renderContext->static_impl_cast<RenderContextNULL>()->makeRenderTarget(
            desc.renderTargetWidth,
            desc.renderTargetHeight);
    renderContext->flush({.renderTarget = renderTarget.get()});

    // If we don't crash, the test passed.
    CHECK(nullTextureImage->getTexture() == nullptr);
}
} // namespace rive::gpu
