/*
 * Copyright 2025 Rive
 */

#include "rive/animation/state_machine_input_instance.hpp"
#include "rive/animation/state_machine_instance.hpp"
#include "rive/command_queue.hpp"
#include "rive/command_server.hpp"
#include "rive/file.hpp"
#include "common/render_context_null.hpp"
#include <fstream>

namespace rive
{
bool operator==(const ViewModelEnum& left, const ViewModelEnum& right)
{
    if (left.name != right.name ||
        left.enumerants.size() != right.enumerants.size())
        return false;

    for (int i = 0; i < left.enumerants.size(); ++i)
    {
        if (left.enumerants[i] != right.enumerants[i])
            return false;
    }

    return true;
}
bool operator!=(const ViewModelEnum& left, const ViewModelEnum& right)
{
    return !(left == right);
}
bool operator==(const PropertyData& l, const PropertyData& r)
{
    return l.name == r.name && l.type == r.type;
}
bool operator==(const CommandQueue::FileListener::ViewModelPropertyData& l,
                const CommandQueue::FileListener::ViewModelPropertyData& r)
{
    return l.name == r.name && l.type == r.type && l.metaData == r.metaData;
}
bool operator!=(const CommandQueue::FileListener::ViewModelPropertyData& l,
                const CommandQueue::FileListener::ViewModelPropertyData& r)
{
    return !(l == r);
}
} // namespace rive

template <typename t, size_t arraySize>
bool operator==(const std::vector<t>& left,
                const std::array<t, arraySize>& right)
{
    if (left.size() != arraySize)
        return false;

    for (int i = 0; i < left.size(); ++i)
    {
        if (left[i] != right[i])
            return false;
    }

    return true;
}

template <typename t>
bool operator==(const std::vector<t>& left, const std::vector<t>& right)
{
    if (left.size() != right.size())
        return false;

    for (int i = 0; i < left.size(); ++i)
    {
        if (left[i] != right[i])
            return false;
    }

    return true;
}

#include "catch.hpp"

using namespace rive;

static void server_thread(rcp<CommandQueue> commandQueue)
{
    std::unique_ptr<gpu::RenderContext> nullContext =
        RenderContextNULL::MakeContext();
    CommandServer server(std::move(commandQueue), nullContext.get());
    server.serveUntilDisconnect();
}

static void wait_for_server(CommandQueue* commandQueue)
{
    std::mutex mutex;
    std::condition_variable cv;
    bool complete = false;
    std::unique_lock<std::mutex> lock(mutex);
    commandQueue->runOnce([&](CommandServer*) {
        std::unique_lock<std::mutex> serverLock(mutex);
        complete = true;
        cv.notify_one();
    });
    while (!complete)
        cv.wait(lock);
}

class TestPODStream : public RefCnt<TestPODStream>
{
public:
    static constexpr int MAG_NUMBER = 0x99;
    int m_number = MAG_NUMBER;
};

TEST_CASE("POD Stream RCP", "[PODStream]")
{
    PODStream stream;
    rcp<TestPODStream> t1 = make_rcp<TestPODStream>();
    TestPODStream* orig = t1.get();
    stream << t1;

    CHECK(t1.get() != nullptr);
    CHECK(t1.get() == orig);

    rcp<TestPODStream> t2;
    stream >> t2;
    CHECK(t2.get() == orig);
    CHECK(t2->m_number == TestPODStream::MAG_NUMBER);

    rcp<TestPODStream> t3;
    stream << t3;
    rcp<TestPODStream> t4;
    stream >> t4;
    CHECK(t4.get() == nullptr);
}

TEST_CASE("artboard management", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);

    std::ifstream stream("assets/two_artboards.riv", std::ios::binary);
    FileHandle fileHandle = commandQueue->loadFile(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}));
    commandQueue->runOnce([fileHandle](CommandServer* server) {
        CHECK(fileHandle != RIVE_NULL_HANDLE);
        CHECK(server->getFile(fileHandle) != nullptr);
    });

    ArtboardHandle artboardHandle1 =
        commandQueue->instantiateArtboardNamed(fileHandle, "One");
    ArtboardHandle artboardHandle2 =
        commandQueue->instantiateArtboardNamed(fileHandle, "Two");
    ArtboardHandle artboardHandle3 =
        commandQueue->instantiateArtboardNamed(fileHandle, "Three");
    commandQueue->runOnce([artboardHandle1, artboardHandle2, artboardHandle3](
                              CommandServer* server) {
        CHECK(artboardHandle1 != RIVE_NULL_HANDLE);
        CHECK(server->getArtboardInstance(artboardHandle1) != nullptr);
        CHECK(artboardHandle2 != RIVE_NULL_HANDLE);
        CHECK(server->getArtboardInstance(artboardHandle2) != nullptr);
        // An artboard named "Three" doesn't exist.
        CHECK(artboardHandle3 != RIVE_NULL_HANDLE);
        CHECK(server->getArtboardInstance(artboardHandle3) == nullptr);
    });

    // Deleting an invalid handle has no effect.
    commandQueue->deleteArtboard(artboardHandle3);
    commandQueue->deleteArtboard(artboardHandle2);
    commandQueue->runOnce([artboardHandle1, artboardHandle2, artboardHandle3](
                              CommandServer* server) {
        CHECK(server->getArtboardInstance(artboardHandle1) != nullptr);
        CHECK(server->getArtboardInstance(artboardHandle2) == nullptr);
        CHECK(server->getArtboardInstance(artboardHandle3) == nullptr);
    });

    // Deleting the file first should now delete the artboard as well.
    commandQueue->deleteFile(fileHandle);
    commandQueue->runOnce([fileHandle](CommandServer* server) {
        CHECK(server->getFile(fileHandle) == nullptr);
    });

    commandQueue->deleteArtboard(artboardHandle1);
    commandQueue->runOnce([artboardHandle1](CommandServer* server) {
        CHECK(server->getArtboardInstance(artboardHandle1) == nullptr);
    });

    commandQueue->disconnect();
    serverThread.join();
}

TEST_CASE("state machine management", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);

    std::ifstream stream("assets/multiple_state_machines.riv",
                         std::ios::binary);
    FileHandle fileHandle = commandQueue->loadFile(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}));
    ArtboardHandle artboardHandle =
        commandQueue->instantiateDefaultArtboard(fileHandle);
    commandQueue->runOnce([fileHandle, artboardHandle](CommandServer* server) {
        CHECK(fileHandle != RIVE_NULL_HANDLE);
        CHECK(server->getFile(fileHandle) != nullptr);
        CHECK(artboardHandle != RIVE_NULL_HANDLE);
        CHECK(server->getArtboardInstance(artboardHandle) != nullptr);
    });

    StateMachineHandle sm1 =
        commandQueue->instantiateStateMachineNamed(artboardHandle, "one");
    StateMachineHandle sm2 =
        commandQueue->instantiateStateMachineNamed(artboardHandle, "two");
    StateMachineHandle sm3 =
        commandQueue->instantiateStateMachineNamed(artboardHandle, "blahblah");
    commandQueue->runOnce([sm1, sm2, sm3](CommandServer* server) {
        CHECK(sm1 != RIVE_NULL_HANDLE);
        CHECK(server->getStateMachineInstance(sm1) != nullptr);
        CHECK(sm2 != RIVE_NULL_HANDLE);
        CHECK(server->getStateMachineInstance(sm2) != nullptr);
        // A state machine named "blahblah" doesn't exist.
        CHECK(sm3 != RIVE_NULL_HANDLE);
        CHECK(server->getStateMachineInstance(sm3) == nullptr);
    });

    commandQueue->deleteFile(fileHandle);
    commandQueue->deleteArtboard(artboardHandle);
    commandQueue->deleteStateMachine(sm1);
    commandQueue->runOnce(
        [fileHandle, artboardHandle, sm1, sm2](CommandServer* server) {
            CHECK(server->getFile(fileHandle) == nullptr);
            CHECK(server->getArtboardInstance(artboardHandle) == nullptr);
            CHECK(server->getStateMachineInstance(sm1) == nullptr);
            // Because of the dependencies, this should delete the statemachine
            // as well.
            CHECK(server->getStateMachineInstance(sm2) == nullptr);
        });

    commandQueue->deleteStateMachine(sm2);
    commandQueue->runOnce([sm2](CommandServer* server) {
        CHECK(server->getStateMachineInstance(sm2) == nullptr);
    });

    commandQueue->disconnect();
    serverThread.join();
}

TEST_CASE("default artboard & state machine", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);

    std::ifstream stream("assets/entry.riv", std::ios::binary);
    FileHandle fileHandle = commandQueue->loadFile(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}));
    ArtboardHandle artboardHandle =
        commandQueue->instantiateDefaultArtboard(fileHandle);
    StateMachineHandle smHandle =
        commandQueue->instantiateDefaultStateMachine(artboardHandle);
    commandQueue->runOnce([artboardHandle, smHandle](CommandServer* server) {
        rive::ArtboardInstance* artboard =
            server->getArtboardInstance(artboardHandle);
        REQUIRE(artboard != nullptr);
        CHECK(artboard->name() == "New Artboard");
        rive::StateMachineInstance* sm =
            server->getStateMachineInstance(smHandle);
        REQUIRE(sm != nullptr);
        CHECK(sm->name() == "State Machine 1");
    });

    // Using an empty string is the same as requesting the default.
    ArtboardHandle artboardHandle2 =
        commandQueue->instantiateArtboardNamed(fileHandle, "");
    StateMachineHandle smHandle2 =
        commandQueue->instantiateStateMachineNamed(artboardHandle2, "");
    commandQueue->runOnce([artboardHandle2, smHandle2](CommandServer* server) {
        rive::ArtboardInstance* artboard =
            server->getArtboardInstance(artboardHandle2);
        REQUIRE(artboard != nullptr);
        CHECK(artboard->name() == "New Artboard");
        rive::StateMachineInstance* sm =
            server->getStateMachineInstance(smHandle2);
        REQUIRE(sm != nullptr);
        CHECK(sm->name() == "State Machine 1");
    });

    commandQueue->disconnect();
    serverThread.join();
}

TEST_CASE("invalid handles", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);

    std::ifstream stream("assets/entry.riv", std::ios::binary);
    FileHandle goodFile = commandQueue->loadFile(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}));
    FileHandle badFile =
        commandQueue->loadFile(std::vector<uint8_t>(100 * 1024, 0));
    commandQueue->runOnce([goodFile, badFile](CommandServer* server) {
        CHECK(goodFile != RIVE_NULL_HANDLE);
        CHECK(server->getFile(goodFile) != nullptr);
        CHECK(badFile != RIVE_NULL_HANDLE);
        CHECK(server->getFile(badFile) == nullptr);
    });

    ArtboardHandle goodArtboard =
        commandQueue->instantiateArtboardNamed(goodFile, "New Artboard");
    ArtboardHandle badArtboard1 =
        commandQueue->instantiateDefaultArtboard(badFile);
    ArtboardHandle badArtboard2 =
        commandQueue->instantiateArtboardNamed(badFile, "New Artboard");
    ArtboardHandle badArtboard3 =
        commandQueue->instantiateArtboardNamed(goodFile, "blahblahblah");
    commandQueue->runOnce(
        [goodArtboard, badArtboard1, badArtboard2, badArtboard3](
            CommandServer* server) {
            CHECK(goodArtboard != RIVE_NULL_HANDLE);
            CHECK(server->getArtboardInstance(goodArtboard) != nullptr);
            CHECK(badArtboard1 != RIVE_NULL_HANDLE);
            CHECK(server->getArtboardInstance(badArtboard1) == nullptr);
            CHECK(badArtboard2 != RIVE_NULL_HANDLE);
            CHECK(server->getArtboardInstance(badArtboard2) == nullptr);
            CHECK(badArtboard3 != RIVE_NULL_HANDLE);
            CHECK(server->getArtboardInstance(badArtboard3) == nullptr);
        });

    StateMachineHandle goodSM =
        commandQueue->instantiateStateMachineNamed(goodArtboard,
                                                   "State Machine 1");
    StateMachineHandle badSM1 =
        commandQueue->instantiateStateMachineNamed(badArtboard2,
                                                   "State Machine 1");
    StateMachineHandle badSM2 =
        commandQueue->instantiateStateMachineNamed(goodArtboard,
                                                   "blahblahblah");
    StateMachineHandle badSM3 =
        commandQueue->instantiateDefaultStateMachine(badArtboard3);
    commandQueue->runOnce(
        [goodSM, badSM1, badSM2, badSM3](CommandServer* server) {
            CHECK(goodSM != RIVE_NULL_HANDLE);
            CHECK(server->getStateMachineInstance(goodSM) != nullptr);
            CHECK(badSM1 != RIVE_NULL_HANDLE);
            CHECK(server->getStateMachineInstance(badSM1) == nullptr);
            CHECK(badSM2 != RIVE_NULL_HANDLE);
            CHECK(server->getStateMachineInstance(badSM2) == nullptr);
            CHECK(badSM3 != RIVE_NULL_HANDLE);
            CHECK(server->getStateMachineInstance(badSM3) == nullptr);
        });

    commandQueue->deleteStateMachine(badSM3);
    commandQueue->deleteStateMachine(badSM2);
    commandQueue->deleteStateMachine(badSM1);
    commandQueue->deleteArtboard(badArtboard3);
    commandQueue->deleteArtboard(badArtboard2);
    commandQueue->deleteArtboard(badArtboard1);
    commandQueue->deleteFile(badFile);
    commandQueue->runOnce(
        [goodFile, goodArtboard, goodSM](CommandServer* server) {
            CHECK(server->getFile(goodFile) != nullptr);
            CHECK(server->getArtboardInstance(goodArtboard) != nullptr);
            CHECK(server->getStateMachineInstance(goodSM) != nullptr);
        });

    commandQueue->deleteStateMachine(goodSM);
    commandQueue->deleteArtboard(goodArtboard);
    commandQueue->deleteFile(goodFile);
    commandQueue->runOnce(
        [goodFile, goodArtboard, goodSM](CommandServer* server) {
            CHECK(server->getFile(goodFile) == nullptr);
            CHECK(server->getArtboardInstance(goodArtboard) == nullptr);
            CHECK(server->getStateMachineInstance(goodSM) == nullptr);
        });

    commandQueue->disconnect();
    serverThread.join();
}

TEST_CASE("draw loops", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);

    std::atomic_uint64_t frameNumber1 = 0, frameNumber2 = 0;
    std::atomic_uint64_t lastFrameNumber1, lastFrameNumber2;
    auto drawLoop1 = [&frameNumber1](DrawKey, CommandServer*) {
        ++frameNumber1;
    };
    auto drawLoop2 = [&frameNumber2](DrawKey, CommandServer*) {
        ++frameNumber2;
    };
    DrawKey loopHandle1 = commandQueue->createDrawKey();
    DrawKey loopHandle2 = commandQueue->createDrawKey();

    commandQueue->runOnce([&](CommandServer*) {
        CHECK(frameNumber1 == 0);
        CHECK(frameNumber2 == 0);
        lastFrameNumber1 = frameNumber1.load();
        lastFrameNumber2 = frameNumber2.load();
    });

    commandQueue->draw(loopHandle1, drawLoop1);
    commandQueue->draw(loopHandle2, drawLoop2);

    do
    {
        wait_for_server(commandQueue.get());
    } while (frameNumber1 == lastFrameNumber1 ||
             frameNumber2 == lastFrameNumber2);

    commandQueue->runOnce([&](CommandServer*) {
        CHECK(frameNumber1 > lastFrameNumber1);
        CHECK(frameNumber2 > lastFrameNumber2);
    });

    commandQueue->runOnce([&](CommandServer*) {
        lastFrameNumber1 = frameNumber1.load();
        lastFrameNumber2 = frameNumber2.load();
    });

    commandQueue->draw(loopHandle2, drawLoop2);

    do
    {
        wait_for_server(commandQueue.get());
    } while (frameNumber2 == lastFrameNumber2);
    commandQueue->runOnce([&](CommandServer*) {
        CHECK(frameNumber1 == lastFrameNumber1);
        CHECK(frameNumber2 > lastFrameNumber2);
    });

    commandQueue->runOnce([&](CommandServer*) {
        lastFrameNumber1 = frameNumber1.load();
        lastFrameNumber2 = frameNumber2.load();
    });

    commandQueue->draw(loopHandle1, drawLoop1);

    do
    {
        wait_for_server(commandQueue.get());
    } while (frameNumber1 == lastFrameNumber1);
    commandQueue->runOnce([&](CommandServer*) {
        CHECK(frameNumber1 > lastFrameNumber1);
        CHECK(frameNumber2 == lastFrameNumber2);
    });

    commandQueue->runOnce([&](CommandServer*) {
        lastFrameNumber1 = frameNumber1.load();
        lastFrameNumber2 = frameNumber2.load();
    });

    for (int i = 0; i < 10; ++i)
    {
        wait_for_server(commandQueue.get());
    }
    commandQueue->runOnce([&](CommandServer*) {
        CHECK(frameNumber1 == lastFrameNumber1);
        CHECK(frameNumber2 == lastFrameNumber2);
    });

    commandQueue->draw(loopHandle1, drawLoop1);
    commandQueue->draw(loopHandle2, drawLoop2);

    do
    {
        wait_for_server(commandQueue.get());
    } while (frameNumber1 == lastFrameNumber1 ||
             frameNumber2 == lastFrameNumber2);
    commandQueue->runOnce([&](CommandServer*) {
        CHECK(frameNumber1 > lastFrameNumber1);
        CHECK(frameNumber2 > lastFrameNumber2);
    });

    // Leave the draw loops running; test tearing down the command buffer with
    // active draw loops.
    commandQueue->disconnect();
    serverThread.join();
}

TEST_CASE("wait for server race condition", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);

    auto lambda = [](CommandServer*) {
        std::this_thread::sleep_for(std::chrono::microseconds(10));
    };
    auto drawLambda = [](DrawKey, CommandServer*) {
        std::this_thread::sleep_for(std::chrono::microseconds(10));
    };

    commandQueue->runOnce(lambda);

    commandQueue->draw(reinterpret_cast<DrawKey>(0), drawLambda);
    commandQueue->draw(reinterpret_cast<DrawKey>(1), drawLambda);
    commandQueue->draw(reinterpret_cast<DrawKey>(2), drawLambda);
    commandQueue->draw(reinterpret_cast<DrawKey>(3), drawLambda);
    commandQueue->draw(reinterpret_cast<DrawKey>(4), drawLambda);

    for (size_t i = 0; i < 100; ++i)

    {
        commandQueue->draw(reinterpret_cast<DrawKey>(i), drawLambda);
        commandQueue->runOnce(lambda);
    }
    for (size_t i = 0; i < 10; ++i)

    {
        wait_for_server(commandQueue.get());
    }

    commandQueue->disconnect();
    serverThread.join();
}

TEST_CASE("stopMesssages command", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::unique_ptr<gpu::RenderContext> nullContext =
        RenderContextNULL::MakeContext();
    CommandServer server(commandQueue, nullContext.get());

    int test = 0;

    commandQueue->runOnce([&](CommandServer*) { ++test; });
    commandQueue->testing_commandLoopBreak();

    for (int i = 0; i < 10; i++)
    {
        commandQueue->runOnce([&](CommandServer*) { ++test; });
        if (i == 5)
            commandQueue->testing_commandLoopBreak();
    }

    server.processCommands();

    CHECK(test == 1);
    server.processCommands();
    CHECK(test == 7);
    server.processCommands();
    CHECK(test == 11);

    commandQueue->disconnect();
}

TEST_CASE("draw happens once per poll", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::unique_ptr<gpu::RenderContext> nullContext =
        RenderContextNULL::MakeContext();
    CommandServer server(commandQueue, nullContext.get());
    int test = 0;

    auto drawLamda = [&test](DrawKey, CommandServer*) { ++test; };

    commandQueue->draw(0, drawLamda);
    commandQueue->draw(0, drawLamda);
    commandQueue->draw(0, drawLamda);
    commandQueue->draw(0, drawLamda);
    commandQueue->draw(0, drawLamda);

    server.processCommands();

    CHECK(test == 1);
    commandQueue->draw(0, drawLamda);
    server.processCommands();
    CHECK(test == 2);
    server.processCommands();
    CHECK(test == 2);

    commandQueue->disconnect();
}

TEST_CASE("disconnect", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::unique_ptr<gpu::RenderContext> nullContext =
        RenderContextNULL::MakeContext();
    CommandServer server(commandQueue, nullContext.get());
    CHECK(!server.getWasDisconnected());

    CHECK(server.processCommands());
    commandQueue->disconnect();
    CHECK(!server.processCommands());
}

TEST_CASE("global asset set / remove", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::unique_ptr<gpu::RenderContext> nullContext =
        RenderContextNULL::MakeContext();

    CommandServer server(commandQueue, nullContext.get());

    std::ifstream imageStream("assets/batdude.png", std::ios::binary);

    auto imageHandle = commandQueue->decodeImage(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(imageStream), {}));

    commandQueue->addGlobalImageAsset("image", imageHandle);

    commandQueue->runOnce([&](CommandServer* server) {
        CHECK(server->testing_globalImageNamed("image") == imageHandle);
    });

    commandQueue->removeGlobalImageAsset("image");
    commandQueue->removeGlobalFontAsset("font");

    commandQueue->runOnce([&](CommandServer* server) {
        CHECK(!server->testing_globalImageContains("image"));
    });

    // should not add nullptr or invalid values to maps
    commandQueue->addGlobalImageAsset("image", nullptr);

    commandQueue->runOnce([&](CommandServer* server) {
        CHECK(!server->testing_globalImageContains("image"));
    });

    auto badImageHandle =
        commandQueue->decodeImage(std::vector<uint8_t>(1024, 0));

    commandQueue->addGlobalImageAsset("image", badImageHandle);

    commandQueue->runOnce([&](CommandServer* server) {
        CHECK(!server->testing_globalImageContains("image"));
    });

    commandQueue->removeGlobalImageAsset("blah");
    commandQueue->removeGlobalFontAsset("blah");
    commandQueue->removeGlobalAudioAsset("blah");

    commandQueue->addGlobalImageAsset("image", imageHandle);

    commandQueue->runOnce([&](CommandServer* server) {
        CHECK(server->testing_globalImageNamed("image") == imageHandle);
    });

    commandQueue->deleteImage(imageHandle);

    // These should be removed automatically
    commandQueue->runOnce([&](CommandServer* server) {
        CHECK(!server->testing_globalImageContains("image"));
    });

#ifdef WITH_RIVE_AUDIO
    std::ifstream audioStream("assets/audio/what.wav", std::ios::binary);

    auto audioHandle = commandQueue->decodeAudio(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(audioStream), {}));

    commandQueue->addGlobalAudioAsset("audio", audioHandle);

    commandQueue->runOnce([&](CommandServer* server) {
        CHECK(server->testing_globalAudioNamed("audio") == audioHandle);
    });

    commandQueue->removeGlobalAudioAsset("audio");

    commandQueue->runOnce([&](CommandServer* server) {
        CHECK(!server->testing_globalAudioContains("audio"));
    });

    // should not add nullptr or invalid values to maps
    commandQueue->addGlobalAudioAsset("audio", nullptr);

    commandQueue->runOnce([&](CommandServer* server) {
        CHECK(!server->testing_globalAudioContains("audio"));
    });

    auto badAudioHandle =
        commandQueue->decodeAudio(std::vector<uint8_t>(1024, 0));

    commandQueue->addGlobalAudioAsset("audio", badAudioHandle);

    commandQueue->runOnce([&](CommandServer* server) {
        CHECK(!server->testing_globalAudioContains("audio"));
    });

    commandQueue->addGlobalAudioAsset("audio", audioHandle);

    commandQueue->runOnce([&](CommandServer* server) {
        CHECK(server->testing_globalAudioNamed("audio") == audioHandle);
    });

    commandQueue->deleteAudio(audioHandle);

    // These should be removed automatically
    commandQueue->runOnce([&](CommandServer* server) {
        CHECK(!server->testing_globalImageContains("image"));
        CHECK(!server->testing_globalFontContains("font"));
        CHECK(!server->testing_globalAudioContains("audio"));
    });
#endif

#ifdef WITH_RIVE_TEXT
    std::ifstream fontStream("assets/fonts/OpenSans-Italic.ttf",
                             std::ios::binary);

    auto fontHandle = commandQueue->decodeFont(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(fontStream), {}));

    commandQueue->addGlobalFontAsset("font", fontHandle);

    commandQueue->runOnce([&](CommandServer* server) {
        CHECK(server->testing_globalFontNamed("font") == fontHandle);
    });

    commandQueue->removeGlobalFontAsset("font");

    commandQueue->runOnce([&](CommandServer* server) {
        CHECK(!server->testing_globalFontContains("font"));
    });

    // should not add nullptr or invalid values to maps
    commandQueue->addGlobalFontAsset("font", nullptr);

    commandQueue->runOnce([&](CommandServer* server) {
        CHECK(!server->testing_globalFontContains("font"));
    });

    auto badFontHandle =
        commandQueue->decodeFont(std::vector<uint8_t>(1024, 0));

    commandQueue->addGlobalFontAsset("font", badFontHandle);

    commandQueue->runOnce([&](CommandServer* server) {
        CHECK(!server->testing_globalFontContains("font"));
    });

    commandQueue->addGlobalFontAsset("font", fontHandle);

    commandQueue->runOnce([&](CommandServer* server) {
        CHECK(server->testing_globalFontNamed("font") == fontHandle);
    });

    commandQueue->deleteFont(fontHandle);

    // These should be removed automatically
    commandQueue->runOnce([&](CommandServer* server) {
        CHECK(!server->testing_globalFontContains("font"));
    });
#endif

    server.processCommands();
    commandQueue->disconnect();
}

TEST_CASE("View Models", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);

    std::ifstream stream("assets/data_bind_test_cmdq.riv", std::ios::binary);
    FileHandle fileHandle = commandQueue->loadFile(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}));

    auto bviewModel =
        commandQueue->instantiateBlankViewModelInstance(fileHandle, "Test All");
    auto dviewModel =
        commandQueue->instantiateDefaultViewModelInstance(fileHandle,
                                                          "Test All");
    auto nviewModel =
        commandQueue->instantiateViewModelInstanceNamed(fileHandle,
                                                        "Test All",
                                                        "Test Alternate");
    auto neviewModel =
        commandQueue->referenceNestedViewModelInstance(bviewModel,
                                                       "Test Nested");
    commandQueue->insertViewModelInstanceListViewModel(bviewModel,
                                                       "Test List",
                                                       neviewModel,
                                                       0);
    auto listViewModel =
        commandQueue->referenceListViewModelInstance(bviewModel,
                                                     "Test List",
                                                     0);
    commandQueue->runOnce([bviewModel,
                           dviewModel,
                           nviewModel,
                           neviewModel,
                           listViewModel](CommandServer* server) {
        CHECK(server->getViewModelInstance(bviewModel) != nullptr);
        CHECK(server->getViewModelInstance(dviewModel) != nullptr);
        CHECK(server->getViewModelInstance(nviewModel) != nullptr);
        CHECK(server->getViewModelInstance(neviewModel) != nullptr);
        CHECK(server->getViewModelInstance(listViewModel) != nullptr);
        CHECK(server->getViewModelInstance(listViewModel) ==
              server->getViewModelInstance(neviewModel));
        auto list =
            server->getViewModelInstance(bviewModel)->propertyList("Test List");
        CHECK(list != nullptr);
        CHECK(list->instanceAt(0).get() ==
              server->getViewModelInstance(listViewModel));
    });

    commandQueue->removeViewModelInstanceListViewModel(bviewModel,
                                                       "Test List",
                                                       neviewModel);

    commandQueue->runOnce([bviewModel](CommandServer* server) {
        auto list =
            server->getViewModelInstance(bviewModel)->propertyList("Test List");
        CHECK(list != nullptr);
        CHECK(list->size() == 0);
    });

    auto bbviewModel =
        commandQueue->instantiateBlankViewModelInstance(fileHandle, "Blah");
    auto bdviewModel =
        commandQueue->instantiateDefaultViewModelInstance(fileHandle, "Blah");
    auto bnviewModel =
        commandQueue->instantiateViewModelInstanceNamed(fileHandle,
                                                        "Blah",
                                                        "Blah");
    auto bnnviewModel =
        commandQueue->instantiateViewModelInstanceNamed(fileHandle,
                                                        "Blah",
                                                        "Test Alternate");
    auto bnbviewModel =
        commandQueue->instantiateViewModelInstanceNamed(fileHandle,
                                                        "Test All",
                                                        "Blah");

    commandQueue->removeViewModelInstanceListViewModel(bviewModel,
                                                       "Blah",
                                                       neviewModel);

    commandQueue->removeViewModelInstanceListViewModel(bviewModel,
                                                       "Test List",
                                                       bnbviewModel);
    auto badPathNeviewModel =
        commandQueue->referenceNestedViewModelInstance(bviewModel, "Blah");

    auto badNeviewModel =
        commandQueue->referenceNestedViewModelInstance(bnnviewModel,
                                                       "Test Nested");

    auto badListViewModel =
        commandQueue->referenceListViewModelInstance(bviewModel, "Blah", 0);

    auto badListViewModel2 =
        commandQueue->referenceListViewModelInstance(bviewModel,
                                                     "Test List",
                                                     5);

    commandQueue->runOnce([bbviewModel,
                           bdviewModel,
                           bnviewModel,
                           bnnviewModel,
                           bnbviewModel,
                           badPathNeviewModel,
                           badNeviewModel,
                           badListViewModel,
                           badListViewModel2](CommandServer* server) {
        CHECK(server->getViewModelInstance(bbviewModel) == nullptr);
        CHECK(server->getViewModelInstance(bdviewModel) == nullptr);
        CHECK(server->getViewModelInstance(bnviewModel) == nullptr);
        CHECK(server->getViewModelInstance(bnnviewModel) == nullptr);
        CHECK(server->getViewModelInstance(bnbviewModel) == nullptr);
        CHECK(server->getViewModelInstance(badPathNeviewModel) == nullptr);
        CHECK(server->getViewModelInstance(badNeviewModel) == nullptr);
        CHECK(server->getViewModelInstance(badListViewModel) == nullptr);
        CHECK(server->getViewModelInstance(badListViewModel2) == nullptr);
    });

    auto artboard =
        commandQueue->instantiateArtboardNamed(fileHandle, "Test Artboard");

    auto abviewModel =
        commandQueue->instantiateBlankViewModelInstance(fileHandle, artboard);
    auto adviewModel =
        commandQueue->instantiateDefaultViewModelInstance(fileHandle, artboard);
    auto anviewModel =
        commandQueue->instantiateViewModelInstanceNamed(fileHandle,
                                                        artboard,
                                                        "Test Alternate");

    commandQueue->runOnce(
        [abviewModel, adviewModel, anviewModel](CommandServer* server) {
            CHECK(server->getViewModelInstance(abviewModel) != nullptr);
            CHECK(server->getViewModelInstance(adviewModel) != nullptr);
            CHECK(server->getViewModelInstance(anviewModel) != nullptr);
        });

    auto badArtboard =
        commandQueue->instantiateArtboardNamed(fileHandle, "Blah");

    auto babviewModel =
        commandQueue->instantiateBlankViewModelInstance(fileHandle,
                                                        badArtboard);
    auto badviewModel =
        commandQueue->instantiateDefaultViewModelInstance(fileHandle,
                                                          badArtboard);
    auto banviewModel =
        commandQueue->instantiateViewModelInstanceNamed(fileHandle,
                                                        badArtboard,
                                                        "Test Alternate");

    commandQueue->runOnce(
        [babviewModel, badviewModel, banviewModel](CommandServer* server) {
            CHECK(server->getViewModelInstance(babviewModel) == nullptr);
            CHECK(server->getViewModelInstance(badviewModel) == nullptr);
            CHECK(server->getViewModelInstance(banviewModel) == nullptr);
        });

    commandQueue->deleteViewModelInstance(badNeviewModel);
    commandQueue->deleteViewModelInstance(bviewModel);

    commandQueue->runOnce([neviewModel](CommandServer* server) {
        CHECK(server->getViewModelInstance(neviewModel) != nullptr);
    });

    commandQueue->deleteViewModelInstance(neviewModel);
    commandQueue->runOnce([neviewModel](CommandServer* server) {
        CHECK(server->getViewModelInstance(neviewModel) == nullptr);
    });

    commandQueue->disconnect();
    serverThread.join();
}

class ViewModelListedListenerCallback : public CommandQueue::FileListener
{
public:
    virtual void onViewModelsListed(
        const FileHandle fileHandle,
        uint64_t requestId,
        std::vector<std::string> viewModelNames) override
    {
        CHECK(requestId == m_requestId);
        CHECK(m_fileHandle == fileHandle);
        CHECK(viewModelNames.size() == std::size(m_expectedViewModelNames));

        for (int i = 0; i < std::size(m_expectedViewModelNames); ++i)
        {
            CHECK(viewModelNames[i] == m_expectedViewModelNames[i]);
        }

        m_hasCallback = true;
    }

    std::array<std::string, 6> m_expectedViewModelNames = {"ListViewModel",
                                                           "Empty VM",
                                                           "Test All",
                                                           "Nested VM",
                                                           "State Transition",
                                                           "Alternate VM"};
    bool m_hasCallback = false;
    FileHandle m_fileHandle = RIVE_NULL_HANDLE;
    uint64_t m_requestId;
};

TEST_CASE("View Model Listed Listener", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);
    {
        ViewModelListedListenerCallback listener;

        std::ifstream stream("assets/data_bind_test_cmdq.riv",
                             std::ios::binary);
        FileHandle fileHandle = commandQueue->loadFile(
            std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}),
            &listener);

        listener.m_requestId = 2;
        listener.m_fileHandle = fileHandle;

        commandQueue->requestViewModelNames(fileHandle, listener.m_requestId);

        wait_for_server(commandQueue.get());

        commandQueue->processMessages();

        CHECK(listener.m_hasCallback);
    }

    {
        ViewModelListedListenerCallback listener;

        FileHandle fileHandle =
            commandQueue->loadFile(std::vector<uint8_t>(1024 * 1024, {}),
                                   &listener);

        listener.m_requestId = 2;
        listener.m_fileHandle = fileHandle;

        commandQueue->requestViewModelNames(fileHandle, listener.m_requestId);

        wait_for_server(commandQueue.get());

        commandQueue->processMessages();

        CHECK(!listener.m_hasCallback);
    }

    commandQueue->disconnect();
    serverThread.join();
}

class TestViewModelFileListener : public CommandQueue::FileListener
{
public:
    virtual void onViewModelInstanceNamesListed(
        const FileHandle handle,
        uint64_t requestId,
        std::string viewModelName,
        std::vector<std::string> instanceNames) override
    {
        CHECK(requestId == m_instanceRequestId);
        CHECK(m_handle == handle);
        CHECK(m_viewModelName == viewModelName);
        CHECK(instanceNames.size() == std::size(m_expectedInstanceNames));

        for (int i = 0; i < std::size(m_expectedInstanceNames); ++i)
        {
            CHECK(instanceNames[i] == m_expectedInstanceNames[i]);
        }

        m_hasInstanceCallback = true;
    }

    virtual void onViewModelPropertiesListed(
        const FileHandle handle,
        uint64_t requestId,
        std::string viewModelName,
        std::vector<CommandQueue::FileListener::ViewModelPropertyData>
            properties) override
    {
        CHECK(requestId == m_propertyRequestId);
        CHECK(m_handle == handle);
        CHECK(m_viewModelName == viewModelName);
        CHECK(properties.size() == std::size(m_expectedProperties));

        for (int i = 0; i < std::size(m_expectedProperties); ++i)
        {
            CHECK(properties[i] == m_expectedProperties[i]);
        }

        m_hasPropertyCallback = true;
    }

    std::array<std::string, 2> m_expectedInstanceNames = {"Test Default",
                                                          "Test Alternate"};
    std::array<CommandQueue::FileListener::ViewModelPropertyData, 10>
        m_expectedProperties = {
            CommandQueue::FileListener::ViewModelPropertyData{
                DataType::artboard,
                "Test Artboard"},
            CommandQueue::FileListener::ViewModelPropertyData{DataType::list,
                                                              "Test List"},
            CommandQueue::FileListener::ViewModelPropertyData{
                DataType::assetImage,
                "Test Image"},
            CommandQueue::FileListener::ViewModelPropertyData{DataType::number,
                                                              "Test Num"},
            CommandQueue::FileListener::ViewModelPropertyData{DataType::string,
                                                              "Test String"},
            CommandQueue::FileListener::ViewModelPropertyData{
                DataType::enumType,
                "Test Enum",
                "Test Enum Values"},
            CommandQueue::FileListener::ViewModelPropertyData{DataType::boolean,
                                                              "Test Bool"},
            CommandQueue::FileListener::ViewModelPropertyData{DataType::color,
                                                              "Test Color"},
            CommandQueue::FileListener::ViewModelPropertyData{DataType::trigger,
                                                              "Test Trigger"},
            CommandQueue::FileListener::ViewModelPropertyData{
                DataType::viewModel,
                "Test Nested",
                "Nested VM"}};

    bool m_hasInstanceCallback = false;
    bool m_hasPropertyCallback = false;
    FileHandle m_handle = RIVE_NULL_HANDLE;
    std::string m_viewModelName = "Test All";
    uint64_t m_instanceRequestId = 2;
    uint64_t m_propertyRequestId = 3;
};

TEST_CASE("View Model Listener", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);
    {
        TestViewModelFileListener listener;

        std::ifstream stream("assets/data_bind_test_cmdq.riv",
                             std::ios::binary);
        listener.m_handle = commandQueue->loadFile(
            std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}),
            &listener);

        commandQueue->requestViewModelInstanceNames(
            listener.m_handle,
            listener.m_viewModelName,
            listener.m_instanceRequestId);
        commandQueue->requestViewModelPropertyDefinitions(
            listener.m_handle,
            listener.m_viewModelName,
            listener.m_propertyRequestId);

        wait_for_server(commandQueue.get());

        commandQueue->processMessages();

        CHECK(listener.m_hasInstanceCallback);
        CHECK(listener.m_hasPropertyCallback);
    }

    {
        TestViewModelFileListener listener;

        listener.m_handle =
            commandQueue->loadFile(std::vector<uint8_t>(1024 * 1024, {}));

        commandQueue->requestViewModelInstanceNames(
            listener.m_handle,
            listener.m_viewModelName,
            listener.m_instanceRequestId);
        commandQueue->requestViewModelPropertyDefinitions(
            listener.m_handle,
            listener.m_viewModelName,
            listener.m_propertyRequestId);

        wait_for_server(commandQueue.get());

        commandQueue->processMessages();

        CHECK(!listener.m_hasInstanceCallback);
        CHECK(!listener.m_hasPropertyCallback);
    }

    commandQueue->disconnect();
    serverThread.join();
}

class TestViewModelInstanceListener
    : public CommandQueue::ViewModelInstanceListener
{
public:
    virtual void onViewModelDeleted(const ViewModelInstanceHandle handle,
                                    uint64_t requestId) override
    {
        CHECK(requestId == m_deleteRequestId);
        CHECK(m_handle == handle);
        m_hasDeleteCallback = true;
    }

    bool m_hasDeleteCallback = false;

    ViewModelInstanceHandle m_handle = RIVE_NULL_HANDLE;
    uint64_t m_deleteRequestId = std::rand();
};

TEST_CASE("View Model Instance Listener", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);

    TestViewModelInstanceListener bListener;
    TestViewModelInstanceListener dListener;
    TestViewModelInstanceListener nListener;

    TestViewModelInstanceListener badListener;

    TestViewModelInstanceListener aListener;
    TestViewModelInstanceListener adListener;
    TestViewModelInstanceListener anListener;
    TestViewModelInstanceListener badAListener;

    std::ifstream stream("assets/data_bind_test_cmdq.riv", std::ios::binary);
    FileHandle fileHandle = commandQueue->loadFile(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}));

    bListener.m_handle =
        commandQueue->instantiateBlankViewModelInstance(fileHandle,
                                                        "Test All",
                                                        &bListener);
    dListener.m_handle =
        commandQueue->instantiateDefaultViewModelInstance(fileHandle,
                                                          "Test All",
                                                          &dListener);
    nListener.m_handle =
        commandQueue->instantiateViewModelInstanceNamed(fileHandle,
                                                        "Test All",
                                                        "Test Alternate",
                                                        &nListener);

    badListener.m_handle =
        commandQueue->instantiateViewModelInstanceNamed(fileHandle,
                                                        "Blah",
                                                        "Blah",
                                                        &badListener);

    auto artboard =
        commandQueue->instantiateArtboardNamed(fileHandle, "Test Artboard");

    auto badArtboard =
        commandQueue->instantiateArtboardNamed(fileHandle, "Blah");

    aListener.m_handle =
        commandQueue->instantiateBlankViewModelInstance(fileHandle,
                                                        artboard,
                                                        &aListener);
    adListener.m_handle =
        commandQueue->instantiateDefaultViewModelInstance(fileHandle,
                                                          artboard,
                                                          &adListener);
    anListener.m_handle =
        commandQueue->instantiateViewModelInstanceNamed(fileHandle,
                                                        artboard,
                                                        "Test Alternate",
                                                        &anListener);

    badAListener.m_handle =
        commandQueue->instantiateViewModelInstanceNamed(fileHandle,
                                                        badArtboard,
                                                        "Test Alternate",
                                                        &badAListener);

    commandQueue->deleteViewModelInstance(bListener.m_handle,
                                          bListener.m_deleteRequestId);
    commandQueue->deleteViewModelInstance(dListener.m_handle,
                                          dListener.m_deleteRequestId);
    commandQueue->deleteViewModelInstance(nListener.m_handle,
                                          nListener.m_deleteRequestId);
    commandQueue->deleteViewModelInstance(badListener.m_handle,
                                          badListener.m_deleteRequestId);

    commandQueue->deleteViewModelInstance(aListener.m_handle,
                                          aListener.m_deleteRequestId);
    commandQueue->deleteViewModelInstance(adListener.m_handle,
                                          adListener.m_deleteRequestId);
    commandQueue->deleteViewModelInstance(anListener.m_handle,
                                          anListener.m_deleteRequestId);
    commandQueue->deleteViewModelInstance(badAListener.m_handle,
                                          badAListener.m_deleteRequestId);

    wait_for_server(commandQueue.get());

    commandQueue->processMessages();

    CHECK(bListener.m_hasDeleteCallback);
    CHECK(dListener.m_hasDeleteCallback);
    CHECK(nListener.m_hasDeleteCallback);
    CHECK(badListener.m_hasDeleteCallback);

    CHECK(aListener.m_hasDeleteCallback);
    CHECK(adListener.m_hasDeleteCallback);
    CHECK(anListener.m_hasDeleteCallback);
    CHECK(badAListener.m_hasDeleteCallback);

    commandQueue->disconnect();
    serverThread.join();
}

TEST_CASE("External Resources", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);

    rcp<RenderImage> externalImage = nullptr;
    rcp<AudioSource> externalAudio = nullptr;
    rcp<Font> externalFont = nullptr;

    commandQueue->runOnce([&externalImage, &externalAudio, &externalFont](
                              CommandServer* server) {
        std::ifstream imageStream("assets/batdude.png", std::ios::binary);
        std::vector<uint8_t> imageStreamData(
            std::istreambuf_iterator<char>(imageStream),
            {});

        std::ifstream audioStream("assets/audio/what.wav", std::ios::binary);
        std::vector<uint8_t> audioStreamData(
            std::istreambuf_iterator<char>(audioStream),
            {});

        std::ifstream fontStream("assets/fonts/OpenSans-Italic.ttf",
                                 std::ios::binary);
        std::vector<uint8_t> fontStreamData(
            std::istreambuf_iterator<char>(fontStream),
            {});

        auto factory = server->factory();

        externalImage = factory->decodeImage(imageStreamData);
        externalAudio = factory->decodeAudio(audioStreamData);
        externalFont = factory->decodeFont(fontStreamData);
    });

    wait_for_server(commandQueue.get());

    CHECK(externalImage);
    CHECK(externalAudio);
    CHECK(externalFont);

    RenderImageHandle externalImageHandle =
        commandQueue->addExternalImage(externalImage);
    AudioSourceHandle externalAudioHandle =
        commandQueue->addExternalAudio(externalAudio);
    FontHandle externalFontHandle = commandQueue->addExternalFont(externalFont);

    commandQueue->runOnce([externalImageHandle,
                           externalImage,
                           externalAudioHandle,
                           externalAudio,
                           externalFontHandle,
                           externalFont](CommandServer* server) {
        auto image = server->getImage(externalImageHandle);
        CHECK(image == externalImage.get());
        auto audio = server->getAudioSource(externalAudioHandle);
        CHECK(audio == externalAudio.get());
        auto font = server->getFont(externalFontHandle);
        CHECK(font == externalFont.get());
    });

    commandQueue->deleteImage(externalImageHandle);
    commandQueue->deleteAudio(externalAudioHandle);
    commandQueue->deleteFont(externalFontHandle);

    commandQueue->runOnce([externalImageHandle,
                           externalAudioHandle,
                           externalFontHandle](CommandServer* server) {
        auto image = server->getImage(externalImageHandle);
        CHECK(image == nullptr);
        auto audio = server->getAudioSource(externalAudioHandle);
        CHECK(audio == nullptr);
        auto font = server->getFont(externalFontHandle);
        CHECK(font == nullptr);
    });

    commandQueue->disconnect();
    serverThread.join();
}

TEST_CASE("RenderImage", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);

    std::ifstream stream("assets/batdude.png", std::ios::binary);
    RenderImageHandle imageHandle = commandQueue->decodeImage(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}));
    RenderImageHandle badImageHandle =
        commandQueue->decodeImage(std::vector<uint8_t>(1024, {}));

    commandQueue->runOnce([imageHandle, badImageHandle](CommandServer* server) {
        auto image = server->getImage(imageHandle);
        CHECK(image != nullptr);
        auto badImage = server->getImage(badImageHandle);
        CHECK(badImage == nullptr);
    });

    commandQueue->deleteImage(imageHandle);
    commandQueue->deleteImage(badImageHandle);

    commandQueue->runOnce([imageHandle, badImageHandle](CommandServer* server) {
        auto image = server->getImage(imageHandle);
        CHECK(image == nullptr);
        auto badImage = server->getImage(badImageHandle);
        CHECK(badImage == nullptr);
    });

    commandQueue->disconnect();
    serverThread.join();
}
#ifdef WITH_RIVE_AUDIO
class AudioSourceDeletedListener : public CommandQueue::AudioSourceListener
{
public:
    virtual void onAudioSourceDeleted(const AudioSourceHandle handle,
                                      uint64_t requestId)
    {
        CHECK(m_handle == handle);
        CHECK(m_requestId == requestId);
        CHECK(!m_hasCallback);
        m_hasCallback = true;
    }

    AudioSourceHandle m_handle;
    bool m_hasCallback = false;
    uint64_t m_requestId = 0x10;
};

TEST_CASE("AudioSource", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);

    std::ifstream stream("assets/audio/what.wav", std::ios::binary);

    AudioSourceDeletedListener listener;

    AudioSourceHandle audioHandle = commandQueue->decodeAudio(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}),
        &listener,
        10);
    listener.m_handle = audioHandle;
    AudioSourceHandle badAudioHandle =
        commandQueue->decodeAudio(std::vector<uint8_t>(1024, {}));

    commandQueue->runOnce([audioHandle, badAudioHandle](CommandServer* server) {
        auto audio = server->getAudioSource(audioHandle);
        CHECK(audio != nullptr);
        auto badAudio = server->getAudioSource(badAudioHandle);
        CHECK(badAudio == nullptr);
    });

    commandQueue->deleteAudio(audioHandle, listener.m_requestId);
    commandQueue->deleteAudio(badAudioHandle);

    commandQueue->runOnce([audioHandle, badAudioHandle](CommandServer* server) {
        auto audio = server->getAudioSource(audioHandle);
        CHECK(audio == nullptr);
        auto badAudio = server->getAudioSource(badAudioHandle);
        CHECK(badAudio == nullptr);
    });

    wait_for_server(commandQueue.get());

    commandQueue->processMessages();

    CHECK(listener.m_hasCallback);

    commandQueue->disconnect();
    serverThread.join();
}
#endif

#if WITH_RIVE_TEXT

class FontDeletedListener : public CommandQueue::FontListener
{
public:
    virtual void onFontDeleted(const FontHandle handle, uint64_t requestId)
    {
        CHECK(m_handle == handle);
        CHECK(m_requestId == requestId);
        CHECK(!m_hasCallback);
        m_hasCallback = true;
    }

    FontHandle m_handle;
    bool m_hasCallback = false;
    uint64_t m_requestId = 0x10;
};

TEST_CASE("Font", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);

    FontDeletedListener listener;

    std::ifstream stream("assets/fonts/OpenSans-Italic.ttf", std::ios::binary);
    FontHandle fontHandle = commandQueue->decodeFont(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}),
        &listener,
        10);
    listener.m_handle = fontHandle;
    FontHandle badFontHandle =
        commandQueue->decodeFont(std::vector<uint8_t>(1024, {}));

    commandQueue->runOnce([fontHandle, badFontHandle](CommandServer* server) {
        auto font = server->getFont(fontHandle);
        CHECK(font != nullptr);
        auto badFont = server->getFont(badFontHandle);
        CHECK(badFont == nullptr);
    });

    commandQueue->deleteFont(fontHandle, listener.m_requestId);
    commandQueue->deleteFont(badFontHandle);

    commandQueue->runOnce([fontHandle, badFontHandle](CommandServer* server) {
        auto font = server->getFont(fontHandle);
        CHECK(font == nullptr);
        auto badFont = server->getFont(badFontHandle);
        CHECK(badFont == nullptr);
    });

    wait_for_server(commandQueue.get());

    commandQueue->processMessages();

    CHECK(listener.m_hasCallback);

    commandQueue->disconnect();
    serverThread.join();
}
#endif

namespace rive
{
bool operator==(const CommandQueue::ViewModelInstanceData& l,
                const CommandQueue::ViewModelInstanceData& r)

{
    bool ret = l.metaData == r.metaData;
    switch (l.metaData.type)
    {
        case DataType::boolean:
            ret &= l.boolValue == r.boolValue;
            break;
        case DataType::number:
            ret &= l.numberValue == r.numberValue;
            break;
        case DataType::color:
            ret &= l.colorValue == r.colorValue;
            break;
        case DataType::string:
        case DataType::enumType:
            ret &= l.stringValue == r.stringValue;
            break;
        default:
            break;
    }

    return ret;
}
} // namespace rive

class ViewModelPropertyListener : public CommandQueue::ViewModelInstanceListener
{
public:
    virtual void onViewModelInstanceError(const ViewModelInstanceHandle handle,
                                          uint64_t requestId,
                                          std::string error) override
    {
        CHECK(handle == m_handle);
        CHECK(error.size());
        ++m_receivedErrors;
    }
    virtual void onViewModelDeleted(const ViewModelInstanceHandle handle,
                                    uint64_t requestId) override
    {
        CHECK(handle == m_handle);
        CHECK(!n_wasDeleted);
        n_wasDeleted = true;
    }

    virtual void onViewModelDataReceived(
        const ViewModelInstanceHandle handle,
        uint64_t requestId,
        CommandQueue::ViewModelInstanceData data) override
    {
        // the callback order should be garunteed.
        // so getting these in the order they are requested should work
        CHECK(m_expectedData.size());
        CHECK(m_expectedRequestIds.size());
        auto expectedData = m_expectedData.front();
        m_expectedData.pop_front();
        auto expectedRequestId = m_expectedRequestIds.front();
        m_expectedRequestIds.pop_front();

        CHECK(handle == m_handle);
        CHECK(data == expectedData);
        CHECK(requestId == expectedRequestId);
    }

    void pushExpectation(CommandQueue* queue, std::string name, float value)
    {
        ++m_requestIdx;
        queue->setViewModelInstanceNumber(m_handle, name, value, m_requestIdx);
        queue->runOnce([handle = m_handle, name, value](CommandServer* server) {
            auto instance = server->getViewModelInstance(handle);
            CHECK(instance != nullptr);
            auto property = instance->propertyNumber(name);
            CHECK(property != nullptr);
            CHECK(property->value() == value);
        });
        m_expectedData.push_back(
            {.metaData = {DataType::number, name}, .numberValue = value});
        m_expectedRequestIds.push_back(m_requestIdx);
        queue->requestViewModelInstanceNumber(m_handle, name, m_requestIdx);
    }

    void pushExpectation(CommandQueue* queue, std::string name, ColorInt value)
    {
        ++m_requestIdx;
        queue->setViewModelInstanceColor(m_handle, name, value, m_requestIdx);
        queue->runOnce([handle = m_handle, name, value](CommandServer* server) {
            auto instance = server->getViewModelInstance(handle);
            CHECK(instance != nullptr);
            auto property = instance->propertyColor(name);
            CHECK(property != nullptr);
            CHECK(property->value() == value);
        });
        m_expectedData.push_back(
            {.metaData = {DataType::color, name}, .colorValue = value});
        m_expectedRequestIds.push_back(m_requestIdx);
        queue->requestViewModelInstanceColor(m_handle, name, m_requestIdx);
    }

    void pushExpectation(CommandQueue* queue, std::string name, bool value)
    {
        ++m_requestIdx;
        queue->setViewModelInstanceBool(m_handle, name, value, m_requestIdx);
        queue->runOnce([handle = m_handle, name, value](CommandServer* server) {
            auto instance = server->getViewModelInstance(handle);
            CHECK(instance != nullptr);
            auto property = instance->propertyBoolean(name);
            CHECK(property != nullptr);
            CHECK(property->value() == value);
        });
        m_expectedData.push_back(
            {.metaData = {DataType::boolean, name}, .boolValue = value});
        m_expectedRequestIds.push_back(m_requestIdx);
        queue->requestViewModelInstanceBool(m_handle, name, m_requestIdx);
    }

    void pushExpectation(CommandQueue* queue,
                         std::string name,
                         ViewModelInstanceHandle value)
    {
        ++m_requestIdx;
        queue->setViewModelInstanceNestedViewModel(m_handle,
                                                   name,
                                                   value,
                                                   m_requestIdx);
        queue->runOnce([handle = m_handle, name, value](CommandServer* server) {
            auto instance = server->getViewModelInstance(handle);
            auto nested = server->getViewModelInstance(value);
            CHECK(instance != nullptr);
            CHECK(nested != nullptr);
            auto property = instance->propertyViewModel(name);
            CHECK(property != nullptr);
            CHECK(property.get() == nested);
        });

        // There is no requesting for nested view models
    }

    void pushStringExpectation(CommandQueue* queue,
                               std::string name,
                               std::string value)
    {
        ++m_requestIdx;
        queue->setViewModelInstanceString(m_handle, name, value, m_requestIdx);
        queue->runOnce([handle = m_handle, name, value](CommandServer* server) {
            auto instance = server->getViewModelInstance(handle);
            CHECK(instance != nullptr);
            auto property = instance->propertyString(name);
            CHECK(property != nullptr);
            CHECK(property->value() == value);
        });
        m_expectedData.push_back(
            {.metaData = {DataType::string, name}, .stringValue = value});
        m_expectedRequestIds.push_back(m_requestIdx);
        queue->requestViewModelInstanceString(m_handle, name, m_requestIdx);
    }

    void pushEnumExpectation(CommandQueue* queue,
                             std::string name,
                             std::string value)
    {
        ++m_requestIdx;
        queue->setViewModelInstanceEnum(m_handle, name, value, m_requestIdx);
        queue->runOnce([handle = m_handle, name, value](CommandServer* server) {
            auto instance = server->getViewModelInstance(handle);
            CHECK(instance != nullptr);
            auto property = instance->propertyEnum(name);
            CHECK(property != nullptr);
            CHECK(property->value() == value);
        });
        m_expectedData.push_back(
            {.metaData = {DataType::enumType, name}, .stringValue = value});
        m_expectedRequestIds.push_back(m_requestIdx);
        queue->requestViewModelInstanceEnum(m_handle, name, m_requestIdx);
    }

    void pushBadExpectation(CommandQueue* queue, std::string name, float value)
    {
        queue->setViewModelInstanceNumber(m_handle, name, value, m_requestIdx);
        ++m_expectedErrors;
    }

    void pushBadExpectation(CommandQueue* queue,
                            std::string name,
                            ColorInt value)
    {
        queue->setViewModelInstanceColor(m_handle, name, value, m_requestIdx);
        ++m_expectedErrors;
    }

    void pushBadExpectation(CommandQueue* queue, std::string name, bool value)
    {
        queue->setViewModelInstanceBool(m_handle, name, value, m_requestIdx);
        ++m_expectedErrors;
    }

    void pushBadExpectation(CommandQueue* queue,
                            std::string name,
                            ViewModelInstanceHandle value)
    {
        queue->setViewModelInstanceNestedViewModel(m_handle,
                                                   name,
                                                   value,
                                                   m_requestIdx);
        ++m_expectedErrors;
    }

    void pushBadStringExpectation(CommandQueue* queue,
                                  std::string name,
                                  std::string value)
    {
        queue->setViewModelInstanceString(m_handle, name, value, m_requestIdx);
        ++m_expectedErrors;
    }

    void pushBadEnumExpectation(CommandQueue* queue,
                                std::string name,
                                std::string value)
    {
        queue->setViewModelInstanceEnum(m_handle, name, value, m_requestIdx);
        ++m_expectedErrors;
    }

    // one data and id per callback
    std::deque<CommandQueue::ViewModelInstanceData> m_expectedData;
    std::deque<uint64_t> m_expectedRequestIds;
    // all of the handles should be the same
    ViewModelInstanceHandle m_handle;
    bool n_wasDeleted = false;

    uint64_t m_requestIdx = 1;

    uint64_t m_expectedErrors = 0;
    uint64_t m_receivedErrors = 0;
};

TEST_CASE("View Model Property Set/Get", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);

    std::ifstream stream("assets/data_bind_test_cmdq.riv", std::ios::binary);
    FileHandle fileHandle = commandQueue->loadFile(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}));

    ViewModelPropertyListener tester;

    auto artboardHandle = commandQueue->instantiateDefaultArtboard(fileHandle);
    tester.m_handle =
        commandQueue->instantiateDefaultViewModelInstance(fileHandle,
                                                          artboardHandle,
                                                          &tester);
    auto blankHandle =
        commandQueue->instantiateBlankViewModelInstance(fileHandle,
                                                        "Nested VM");
    auto alternateHandle =
        commandQueue->instantiateViewModelInstanceNamed(fileHandle,
                                                        "Nested VM",
                                                        "Alternate Nested");

    tester.pushExpectation(commandQueue.get(), "Test Bool", true);
    tester.pushExpectation(commandQueue.get(), "Test Num", 10.0f);
    tester.pushExpectation(commandQueue.get(),
                           "Test Nested/Nested Number",
                           10.0f);
    tester.pushExpectation(commandQueue.get(), "Test Nested", blankHandle);
    tester.pushExpectation(commandQueue.get(),
                           "Test Nested/Nested Number",
                           10.0f);
    commandQueue->runOnce([rootHandle = tester.m_handle,
                           blankHandle](CommandServer* server) {
        auto root = server->getViewModelInstance(rootHandle);
        auto nested = server->getViewModelInstance(blankHandle);
        CHECK(root != nullptr);
        CHECK(nested != nullptr);
        auto rootProperty = root->propertyNumber("Test Nested/Nested Number");
        CHECK(rootProperty != nullptr);
        CHECK(rootProperty->value() == 10.0f);
        auto nestedProperty = nested->propertyNumber("Nested Number");
        CHECK(nestedProperty != nullptr);
        CHECK(nestedProperty->value() == 10.0f);
    });
    tester.pushExpectation(commandQueue.get(),
                           "Test Color",
                           rive::colorARGB(255, 255, 0, 0));
    tester.pushEnumExpectation(commandQueue.get(), "Test Enum", "Value 2");
    tester.pushStringExpectation(commandQueue.get(),
                                 "Test String",
                                 "Some String");

    // Images don't have a "get" equivalent so we test it with a run once
    // directly.
    std::ifstream imageStream("assets/batdude.png", std::ios::binary);
    auto imageHandle = commandQueue->decodeImage(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(imageStream), {}));
    commandQueue->setViewModelInstanceImage(tester.m_handle,
                                            "Test Image",
                                            imageHandle);

    commandQueue->runOnce(
        [imageHandle, handle = tester.m_handle](CommandServer* server) {
            auto image = server->getImage(imageHandle);
            CHECK(image != nullptr);
            auto viewModel = server->getViewModelInstance(handle);
            CHECK(viewModel != nullptr);
            auto imageProperty = viewModel->propertyImage("Test Image");
            CHECK(imageProperty != nullptr);
            CHECK(imageProperty->testing_value() == image);
        });

    // Same for artboards.
    auto bindableArtboardHandle =
        commandQueue->instantiateDefaultArtboard(fileHandle);
    commandQueue->setViewModelInstanceArtboard(tester.m_handle,
                                               "Test Artboard",
                                               bindableArtboardHandle);

    commandQueue->runOnce([bindableArtboardHandle,
                           handle = tester.m_handle](CommandServer* server) {
        auto bindableArtboard =
            server->getBindableArtboard(bindableArtboardHandle);
        CHECK(bindableArtboard != nullptr);
        auto viewModel = server->getViewModelInstance(handle);
        CHECK(viewModel != nullptr);
        auto artboardProperty = viewModel->propertyArtboard("Test Artboard");
        CHECK(artboardProperty != nullptr);
        CHECK(artboardProperty->testing_value() == bindableArtboard);
    });
    commandQueue->deleteArtboard(bindableArtboardHandle);

    auto badImageHandle =
        commandQueue->decodeImage(std::vector<uint8_t>(1024 * 1024, {}));
    commandQueue->setViewModelInstanceImage(tester.m_handle,
                                            "Test Image",
                                            badImageHandle);
    ++tester.m_expectedErrors;

    auto badArtboardHandle =
        commandQueue->instantiateArtboardNamed(fileHandle, "Blah");
    commandQueue->setViewModelInstanceArtboard(tester.m_handle,
                                               "Test Artboard",
                                               badArtboardHandle);
    ++tester.m_expectedErrors;

    commandQueue->runOnce([imageHandle,
                           badImageHandle,
                           handle = tester.m_handle](CommandServer* server) {
        auto image = server->getImage(imageHandle);
        auto badImage = server->getImage(badImageHandle);
        CHECK(image != nullptr);
        CHECK(badImage == nullptr);
        auto viewModel = server->getViewModelInstance(handle);
        CHECK(viewModel != nullptr);
        auto imageProperty = viewModel->propertyImage("Test Image");
        CHECK(imageProperty != nullptr);
        CHECK(imageProperty->testing_value() == image);
    });

    commandQueue->setViewModelInstanceImage(tester.m_handle,
                                            "Blah",
                                            imageHandle);
    // Account for bad image request.
    ++tester.m_expectedErrors;

    commandQueue->setViewModelInstanceArtboard(tester.m_handle,
                                               "Blah",
                                               artboardHandle);
    // Account for bad artboard request.
    ++tester.m_expectedErrors;

    commandQueue->runOnce(
        [imageHandle, handle = tester.m_handle](CommandServer* server) {
            auto image = server->getImage(imageHandle);
            CHECK(image != nullptr);
            auto viewModel = server->getViewModelInstance(handle);
            CHECK(viewModel != nullptr);
            auto imageProperty = viewModel->propertyImage("Test Image");
            CHECK(imageProperty != nullptr);
            CHECK(imageProperty->testing_value() == image);
        });

    // We should set / get in order as it goes through the list.
    for (int i = 0; i < 10; ++i)
    {
        tester.pushExpectation(commandQueue.get(),
                               "Test Bool",
                               static_cast<bool>(i % 2));
        tester.pushExpectation(commandQueue.get(),
                               "Test Num",
                               static_cast<float>(i));
        tester.pushExpectation(commandQueue.get(),
                               "Test Nested",
                               i % 2 ? blankHandle : alternateHandle);
        tester.pushExpectation(commandQueue.get(),
                               "Test Color",
                               rive::colorARGB(i, i, i, i));
        tester.pushEnumExpectation(commandQueue.get(),
                                   "Test Enum",
                                   i % 2 ? "Value 2" : "Value 1");
        tester.pushStringExpectation(commandQueue.get(),
                                     "Test String",
                                     std::to_string(i));
    }

    // Bad values

    commandQueue->deleteViewModelInstance(blankHandle);
    commandQueue->deleteViewModelInstance(alternateHandle);

    // Good property path bad value
    tester.pushBadEnumExpectation(commandQueue.get(), "Test Enum", "Blah");
    tester.pushBadExpectation(commandQueue.get(), "Test Nested", blankHandle);

    // Bad everything
    tester.pushBadExpectation(commandQueue.get(), "Blah", true);
    tester.pushBadExpectation(commandQueue.get(), "Blah", 10.0f);
    tester.pushBadExpectation(commandQueue.get(), "Blah", alternateHandle);
    tester.pushBadExpectation(commandQueue.get(),
                              "Blah",
                              rive::colorARGB(255, 255, 0, 0));
    tester.pushBadEnumExpectation(commandQueue.get(), "Blah", "Value 2");
    tester.pushBadStringExpectation(commandQueue.get(), "Blah", "Some String");

    // Delete the instance, the callback should continue fine and a delete
    // should be received
    commandQueue->deleteViewModelInstance(tester.m_handle);

    // Call sets on deleted handle
    tester.pushBadExpectation(commandQueue.get(), "Test Bool", true);
    tester.pushBadExpectation(commandQueue.get(), "Test Num", 10.0f);
    tester.pushBadExpectation(commandQueue.get(),
                              "Test Color",
                              rive::colorARGB(255, 255, 0, 0));
    tester.pushBadEnumExpectation(commandQueue.get(), "Test Enum", "Value 2");
    tester.pushBadStringExpectation(commandQueue.get(),
                                    "Test String",
                                    "Some String");

    wait_for_server(commandQueue.get());

    commandQueue->processMessages();

    // We expect there should be no expected values left
    CHECK(tester.m_expectedData.size() == 0);
    CHECK(tester.m_expectedRequestIds.size() == 0);

    CHECK(tester.m_expectedErrors == tester.m_receivedErrors);

    // We should have received the deleted event
    CHECK(tester.n_wasDeleted);

    commandQueue->disconnect();
    serverThread.join();
}

class ViewModelPropertySubscriptionListener
    : public CommandQueue::ViewModelInstanceListener
{
public:
    virtual void onViewModelInstanceError(const ViewModelInstanceHandle handle,
                                          uint64_t requestId,
                                          std::string error) override
    {
        CHECK(handle == m_handle);
        CHECK(error.size());
        ++m_receivedErrors;
    }

    virtual void onViewModelDeleted(const ViewModelInstanceHandle handle,
                                    uint64_t requestId) override
    {
        CHECK(handle == m_handle);
        CHECK(!n_wasDeleted);
        n_wasDeleted = true;
    }

    virtual void onViewModelDataReceived(
        const ViewModelInstanceHandle handle,
        uint64_t requestId,
        CommandQueue::ViewModelInstanceData data) override
    {
        // We only get one sub callback per value, so instead of a dequeue we
        // use a map of names to values that we expect, it should always be the
        // last value set.
        CHECK(m_expectedData.size());
        auto itr = m_expectedData.find(data.metaData.name);
        CHECK(itr != m_expectedData.end());
        auto& expectedData = itr->second;

        CHECK(handle == m_handle);
        CHECK(data == expectedData);

        ++m_receivedCallbacks;
    }

    void pushTriggerExpectation(CommandQueue* queue, std::string name)
    {
        ++m_requestIdx;
        m_expectedData[name] = {.metaData = {DataType::trigger, name}};
        queue->fireViewModelTrigger(m_handle, name, m_requestIdx);
    }

    void pushExpectation(CommandQueue* queue, std::string name, float value)
    {
        ++m_requestIdx;
        queue->setViewModelInstanceNumber(m_handle, name, value, m_requestIdx);
        m_expectedData[name] = {.metaData = {DataType::number, name},
                                .numberValue = value};
    }

    void pushExpectation(CommandQueue* queue, std::string name, ColorInt value)
    {
        ++m_requestIdx;
        queue->setViewModelInstanceColor(m_handle, name, value, m_requestIdx);
        m_expectedData[name] = {.metaData = {DataType::color, name},
                                .colorValue = value};
    }

    void pushExpectation(CommandQueue* queue, std::string name, bool value)
    {
        ++m_requestIdx;
        queue->setViewModelInstanceBool(m_handle, name, value, m_requestIdx);
        m_expectedData[name] = {.metaData = {DataType::boolean, name},
                                .boolValue = value};
    }

    void pushExpectation(CommandQueue* queue,
                         std::string name,
                         ViewModelInstanceHandle value)
    {
        ++m_requestIdx;
        queue->setViewModelInstanceNestedViewModel(m_handle,
                                                   name,
                                                   value,
                                                   m_requestIdx);
        // there is no subscription for view models, you must subscribe to the
        // nested property instead
    }

    void pushListExpectation(CommandQueue* queue,
                             std::string name,
                             ViewModelInstanceHandle value)
    {
        ++m_requestIdx;
        queue->appendViewModelInstanceListViewModel(m_handle,
                                                    name,
                                                    value,
                                                    m_requestIdx);
        // there is no value for view models list subscription callbacks
        m_expectedData[name] = {.metaData = {DataType::viewModel, name}};
    }

    void pushStringExpectation(CommandQueue* queue,
                               std::string name,
                               std::string value)
    {
        ++m_requestIdx;
        queue->setViewModelInstanceString(m_handle, name, value, m_requestIdx);
        m_expectedData[name] = {.metaData = {DataType::string, name},
                                .stringValue = value};
    }

    void pushEnumExpectation(CommandQueue* queue,
                             std::string name,
                             std::string value)
    {
        ++m_requestIdx;
        queue->setViewModelInstanceEnum(m_handle, name, value, m_requestIdx);
        m_expectedData[name] = {.metaData = {DataType::enumType, name},
                                .stringValue = value};
    }

    void pushExpectation(CommandQueue* queue,
                         std::string name,
                         RenderImageHandle value)
    {
        queue->setViewModelInstanceImage(m_handle, "Test Image", value);
        // no value for image subscriptions
        m_expectedData[name] = {.metaData = {DataType::assetImage, name}};
    }

    void pushBadExpectation(CommandQueue* queue, std::string name, float value)
    {
        queue->setViewModelInstanceNumber(m_handle, name, value, m_requestIdx);
        ++m_expectedErrors;
    }

    void pushBadExpectation(CommandQueue* queue,
                            std::string name,
                            ColorInt value)
    {
        queue->setViewModelInstanceColor(m_handle, name, value, m_requestIdx);
        ++m_expectedErrors;
    }

    void pushBadExpectation(CommandQueue* queue, std::string name, bool value)
    {
        queue->setViewModelInstanceBool(m_handle, name, value, m_requestIdx);
        ++m_expectedErrors;
    }

    void pushBadExpectation(CommandQueue* queue,
                            std::string name,
                            RenderImageHandle value)
    {
        queue->setViewModelInstanceImage(m_handle, name, value, m_requestIdx);
        ++m_expectedErrors;
    }

    void pushBadExpectation(CommandQueue* queue,
                            std::string name,
                            ViewModelInstanceHandle value)
    {
        queue->setViewModelInstanceNestedViewModel(m_handle,
                                                   name,
                                                   value,
                                                   m_requestIdx);
        ++m_expectedErrors;
    }

    void pushBadStringExpectation(CommandQueue* queue,
                                  std::string name,
                                  std::string value)
    {
        queue->setViewModelInstanceString(m_handle, name, value, m_requestIdx);
        ++m_expectedErrors;
    }

    void pushBadEnumExpectation(CommandQueue* queue,
                                std::string name,
                                std::string value)
    {
        queue->setViewModelInstanceEnum(m_handle, name, value, m_requestIdx);
        ++m_expectedErrors;
    }

    void pushBadTriggerExpectation(CommandQueue* queue, std::string name)
    {
        queue->fireViewModelTrigger(m_handle, name);
        ++m_expectedErrors;
    }

    // One data and id per callback.
    std::unordered_map<std::string, CommandQueue::ViewModelInstanceData>
        m_expectedData;
    // All of the handles should be the same.
    ViewModelInstanceHandle m_handle;
    bool n_wasDeleted = false;

    uint64_t m_requestIdx = 1;
    int m_receivedCallbacks = 0;

    size_t m_expectedErrors = 0;
    size_t m_receivedErrors = 0;
};

TEST_CASE("CommandServer::getHandleForInstance", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);

    std::ifstream stream("assets/data_bind_test_cmdq.riv", std::ios::binary);
    FileHandle fileHandle = commandQueue->loadFile(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}));

    ViewModelInstanceHandle viewModel =
        commandQueue->instantiateDefaultViewModelInstance(fileHandle,
                                                          "Test All");

    commandQueue->runOnce([viewModelHandle = viewModel](CommandServer* server) {
        auto viewModel = server->getViewModelInstance(viewModelHandle);
        CHECK(server->getHandleForInstance(viewModel) == viewModelHandle);
    });

    wait_for_server(commandQueue.get());

    commandQueue->disconnect();
    serverThread.join();
}

TEST_CASE("Set Artboard Size / Reset Artboard Size", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);

    std::ifstream stream("assets/data_bind_test_cmdq.riv", std::ios::binary);
    FileHandle fileHandle = commandQueue->loadFile(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}));

    auto artboardHandle = commandQueue->instantiateDefaultArtboard(fileHandle);

    rive::ArtboardHandle InvalidHandle =
        reinterpret_cast<rive::ArtboardHandle>(0xFF);

    commandQueue->setArtboardSize(artboardHandle, 1000, 1000);

    commandQueue->runOnce(
        [artboardHandle = artboardHandle](CommandServer* server) {
            auto artboard = server->getArtboardInstance(artboardHandle);
            CHECK(artboard->width() == 1000.f);
            CHECK(artboard->height() == 1000.f);
        });

    // We should be deviding the width and height by scale since `align`
    // multplies by scale.
    commandQueue->setArtboardSize(artboardHandle, 1000, 1000, 2);

    commandQueue->runOnce(
        [artboardHandle = artboardHandle](CommandServer* server) {
            auto artboard = server->getArtboardInstance(artboardHandle);
            CHECK(artboard->width() == 500.f);
            CHECK(artboard->height() == 500.f);
        });

    commandQueue->resetArtboardSize(artboardHandle);

    commandQueue->runOnce(
        [artboardHandle = artboardHandle](CommandServer* server) {
            auto artboard = server->getArtboardInstance(artboardHandle);
            CHECK(artboard->width() == artboard->originalWidth());
            CHECK(artboard->height() == artboard->originalHeight());
        });

    commandQueue->setArtboardSize(InvalidHandle, 10, 10);
    commandQueue->resetArtboardSize(InvalidHandle);

    wait_for_server(commandQueue.get());

    commandQueue->disconnect();
    serverThread.join();
}

TEST_CASE("View Model Property Subscriptions", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::unique_ptr<gpu::RenderContext> nullContext =
        RenderContextNULL::MakeContext();
    // The subscriptions happens once at the end of processCommands, so it's not
    // order dependent, that makes it more difficult to test. To get around
    // this, we just make the server on the same thread. Different tests will be
    // used for testing async.
    CommandServer server(commandQueue, nullContext.get());

    std::ifstream stream("assets/data_bind_test_cmdq.riv", std::ios::binary);
    FileHandle fileHandle = commandQueue->loadFile(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}));

    ViewModelPropertySubscriptionListener tester;

    auto artboardHandle = commandQueue->instantiateDefaultArtboard(fileHandle);
    tester.m_handle =
        commandQueue->instantiateDefaultViewModelInstance(fileHandle,
                                                          artboardHandle,
                                                          &tester);

    commandQueue->subscribeToViewModelProperty(tester.m_handle,
                                               "Test Nested/Nested Number",
                                               DataType::number);
    commandQueue->subscribeToViewModelProperty(tester.m_handle,
                                               "Test Bool",
                                               DataType::boolean);
    commandQueue->subscribeToViewModelProperty(tester.m_handle,
                                               "Test Num",
                                               DataType::number);
    commandQueue->subscribeToViewModelProperty(tester.m_handle,
                                               "Test Color",
                                               DataType::color);
    commandQueue->subscribeToViewModelProperty(tester.m_handle,
                                               "Test Enum",
                                               DataType::enumType);
    commandQueue->subscribeToViewModelProperty(tester.m_handle,
                                               "Test String",
                                               DataType::string);
    commandQueue->subscribeToViewModelProperty(tester.m_handle,
                                               "Test Trigger",
                                               DataType::trigger);
    commandQueue->subscribeToViewModelProperty(tester.m_handle,
                                               "Test List",
                                               DataType::list);
    commandQueue->subscribeToViewModelProperty(tester.m_handle,
                                               "Test Image",
                                               DataType::assetImage);

    commandQueue->subscribeToViewModelProperty(tester.m_handle,
                                               "Bad property",
                                               DataType::assetImage);
    ++tester.m_expectedErrors;

    // bad type
    commandQueue->subscribeToViewModelProperty(tester.m_handle,
                                               "Test Image",
                                               DataType::integer);
    ++tester.m_expectedErrors;

    commandQueue->runOnce([](CommandServer* server) {
        auto subs = server->testing_getSubsciptions();
        CHECK(subs.size() == 9);
    });

    auto blankHandle =
        commandQueue->instantiateBlankViewModelInstance(fileHandle,
                                                        "Nested VM");
    auto alternateHandle =
        commandQueue->instantiateViewModelInstanceNamed(fileHandle,
                                                        "Nested VM",
                                                        "Alternate Nested");

    tester.pushExpectation(commandQueue.get(), "Test Bool", true);
    tester.pushExpectation(commandQueue.get(), "Test Num", 10.0f);
    tester.pushExpectation(commandQueue.get(),
                           "Test Nested/Nested Number",
                           10.0f);
    tester.pushExpectation(commandQueue.get(), "Test Nested", blankHandle);
    tester.pushExpectation(commandQueue.get(),
                           "Test Nested/Nested Number",
                           10.0f);

    tester.pushExpectation(commandQueue.get(),
                           "Test Color",
                           rive::colorARGB(255, 255, 0, 0));
    tester.pushEnumExpectation(commandQueue.get(), "Test Enum", "Value 2");
    tester.pushStringExpectation(commandQueue.get(),
                                 "Test String",
                                 "Some String");

    tester.pushTriggerExpectation(commandQueue.get(), "Test Trigger");

    std::ifstream imageStream("assets/batdude.png", std::ios::binary);
    auto imageHandle = commandQueue->decodeImage(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(imageStream), {}));
    tester.pushExpectation(commandQueue.get(), "Test Image", imageHandle);

    for (int i = 0; i < 10; ++i)
    {
        tester.pushExpectation(commandQueue.get(),
                               "Test Bool",
                               static_cast<bool>(i % 2));
        tester.pushExpectation(commandQueue.get(),
                               "Test Num",
                               static_cast<float>(i));
        tester.pushExpectation(commandQueue.get(),
                               "Test Nested",
                               i % 2 ? blankHandle : alternateHandle);
        tester.pushExpectation(commandQueue.get(),
                               "Test Color",
                               rive::colorARGB(i, i, i, i));
        tester.pushEnumExpectation(commandQueue.get(),
                                   "Test Enum",
                                   i % 2 ? "Value 2" : "Value 1");
        tester.pushStringExpectation(commandQueue.get(),
                                     "Test String",
                                     std::to_string(i));
    }

    // Bad values

    auto badImageHandle =
        commandQueue->decodeImage(std::vector<uint8_t>(1024 * 1024, {}));

    tester.pushBadExpectation(commandQueue.get(), "Test Image", badImageHandle);

    commandQueue->deleteViewModelInstance(blankHandle);
    commandQueue->deleteViewModelInstance(alternateHandle);

    // Good property path bad value
    tester.pushBadEnumExpectation(commandQueue.get(), "Test Enum", "Blah");
    tester.pushBadExpectation(commandQueue.get(), "Test Nested", blankHandle);

    // Bad everything
    tester.pushBadExpectation(commandQueue.get(), "Blah", true);
    tester.pushBadExpectation(commandQueue.get(), "Blah", 10.0f);
    tester.pushBadExpectation(commandQueue.get(), "Blah", alternateHandle);
    tester.pushBadExpectation(commandQueue.get(),
                              "Blah",
                              rive::colorARGB(255, 255, 0, 0));
    tester.pushBadEnumExpectation(commandQueue.get(), "Blah", "Value 2");
    tester.pushBadStringExpectation(commandQueue.get(), "Blah", "Some String");

    tester.pushBadTriggerExpectation(commandQueue.get(), "Blah");

    // Bad handle for trigger test.
    commandQueue->fireViewModelTrigger(0, "Blah");

    server.processCommands();
    commandQueue->processMessages();

    // Once the handle is delted subscriptions will stop, so we do this after
    // processMessages.
    commandQueue->deleteViewModelInstance(tester.m_handle);

    // Call sets on deleted handle
    tester.pushBadExpectation(commandQueue.get(), "Test Bool", true);
    tester.pushBadExpectation(commandQueue.get(), "Test Num", 10.0f);
    tester.pushBadExpectation(commandQueue.get(),
                              "Test Color",
                              rive::colorARGB(255, 255, 0, 0));
    tester.pushBadEnumExpectation(commandQueue.get(), "Test Enum", "Value 2");
    tester.pushBadStringExpectation(commandQueue.get(),
                                    "Test String",
                                    "Some String");
    server.processCommands();
    commandQueue->processMessages();

    // We should have received the deleted event.
    CHECK(tester.n_wasDeleted);
    // We should have received the same number of callbacks as entries in the
    // map.
    CHECK(tester.m_receivedCallbacks == tester.m_expectedData.size());

    commandQueue->unsubscribeToViewModelProperty(tester.m_handle,
                                                 "Test Nested/Nested Number",
                                                 DataType::number);
    commandQueue->unsubscribeToViewModelProperty(tester.m_handle,
                                                 "Test Bool",
                                                 DataType::boolean);
    commandQueue->unsubscribeToViewModelProperty(tester.m_handle,
                                                 "Test Num",
                                                 DataType::number);
    commandQueue->unsubscribeToViewModelProperty(tester.m_handle,
                                                 "Test Color",
                                                 DataType::color);
    commandQueue->unsubscribeToViewModelProperty(tester.m_handle,
                                                 "Test Enum",
                                                 DataType::enumType);
    commandQueue->unsubscribeToViewModelProperty(tester.m_handle,
                                                 "Test String",
                                                 DataType::string);
    commandQueue->unsubscribeToViewModelProperty(tester.m_handle,
                                                 "Test Trigger",
                                                 DataType::trigger);
    commandQueue->unsubscribeToViewModelProperty(tester.m_handle,
                                                 "Test List",
                                                 DataType::list);
    commandQueue->unsubscribeToViewModelProperty(tester.m_handle,
                                                 "Test Image",
                                                 DataType::assetImage);
    // Unsub something that doesn't exist.
    commandQueue->unsubscribeToViewModelProperty(tester.m_handle,
                                                 "Blah",
                                                 DataType::boolean);
    // Note we don't increment tester.m_expectedErrors because unsubing
    // something invalid is ok to do, we just ignore it.

    commandQueue->runOnce([](CommandServer* server) {
        auto subs = server->testing_getSubsciptions();
        CHECK(subs.empty());
    });

    server.processCommands();
    commandQueue->processMessages();

    CHECK(tester.m_receivedErrors == tester.m_expectedErrors);

    commandQueue->disconnect();
}

class AsyncSubListener : public CommandQueue::ViewModelInstanceListener
{
public:
    virtual void onViewModelDataReceived(
        const ViewModelInstanceHandle handle,
        uint64_t requestId,
        CommandQueue::ViewModelInstanceData data) override
    {
        CHECK(handle == m_handle);
        CHECK(!m_hasCallback);
        m_hasCallback = true;
        // we are just expecting the one sub so the value should be correct
        CHECK(data.numberValue == 10);
    }

    ViewModelInstanceHandle m_handle;
    bool m_hasCallback = false;
};

// The above tests are to check that all subscription types work and that values
// come through correctly This is just checking to make sure subscriptions work
// while the server is in a seperate thread but we don't care about the exact
// callback happening since that is tested above

TEST_CASE("View Model Property Async Subscriptions", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);

    std::ifstream stream("assets/data_bind_test_cmdq.riv", std::ios::binary);
    FileHandle fileHandle = commandQueue->loadFile(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}));

    AsyncSubListener tester;

    auto artboardHandle = commandQueue->instantiateDefaultArtboard(fileHandle);
    tester.m_handle =
        commandQueue->instantiateDefaultViewModelInstance(fileHandle,
                                                          artboardHandle,
                                                          &tester);

    commandQueue->subscribeToViewModelProperty(tester.m_handle,
                                               "Test Num",
                                               DataType::number);

    commandQueue->setViewModelInstanceNumber(tester.m_handle, "Test Num", 10);

    commandQueue->runOnce([](CommandServer* server) {
        auto subs = server->testing_getSubsciptions();
        CHECK(subs.size() == 1);
    });

    commandQueue->testing_commandLoopBreak();

    wait_for_server(commandQueue.get());

    commandQueue->processMessages();

    CHECK(tester.m_hasCallback);

    commandQueue->unsubscribeToViewModelProperty(tester.m_handle,
                                                 "Test Num",
                                                 DataType::number);

    commandQueue->runOnce([](CommandServer* server) {
        auto subs = server->testing_getSubsciptions();
        CHECK(subs.empty());
    });

    commandQueue->disconnect();
    serverThread.join();
}

class ListViewModelPropertyListener
    : public CommandQueue::ViewModelInstanceListener
{
public:
    ListViewModelPropertyListener(rcp<CommandQueue> queue) :
        m_queue(std::move(queue))
    {}

    virtual void onViewModelListSizeReceived(
        const ViewModelInstanceHandle handle,
        uint64_t requestId,
        std::string path,
        size_t size) override
    {
        CHECK(handle == m_handle);
        CHECK(path == m_path);
        CHECK(size == m_expectedSize);
        CHECK(requestId == m_requestIdx);
        m_receivedSize = true;
    }

    virtual void onViewModelDeleted(const ViewModelInstanceHandle handle,
                                    uint64_t requestId) override
    {
        CHECK(handle == m_handle);
        CHECK(!n_wasDeleted);
        n_wasDeleted = true;
    }

    void pushRequestExpectation(std::string path, size_t expectedSize)
    {
        m_path = path;
        m_expectedSize = expectedSize;
        ++m_requestIdx;
        m_receivedSize = false;
        m_queue->requestViewModelInstanceListSize(m_handle,
                                                  m_path,
                                                  m_requestIdx);

        wait_for_server(m_queue.get());
        m_queue->processMessages();
        CHECK(m_receivedSize);
    }

    void pushExpectation(std::string name,
                         ViewModelInstanceHandle expectedValueAtIndex1,
                         ViewModelInstanceHandle expectedValueAtIndex2,
                         int index,
                         int index2)
    {
        m_queue->swapViewModelInstanceListValues(m_handle, name, index, index2);
        m_queue->runOnce([handle = m_handle,
                          name,
                          expectedValueAtIndex1,
                          expectedValueAtIndex2,
                          index,
                          index2](CommandServer* server) {
            auto instance = server->getViewModelInstance(handle);
            CHECK(instance != nullptr);
            auto value1Instance =
                server->getViewModelInstance(expectedValueAtIndex1);
            CHECK(value1Instance != nullptr);
            auto value2Instance =
                server->getViewModelInstance(expectedValueAtIndex2);
            CHECK(value2Instance != nullptr);
            auto property = instance->propertyList(name);
            CHECK(property != nullptr);
            CHECK(property->instanceAt(index).get() == value1Instance);
            CHECK(property->instanceAt(index2).get() == value2Instance);
        });
    }

    void pushExpectation(std::string name,
                         ViewModelInstanceHandle value,
                         int index)
    {
        m_queue->insertViewModelInstanceListViewModel(m_handle,
                                                      name,
                                                      value,
                                                      index);
        m_queue->runOnce(
            [handle = m_handle, name, value, index](CommandServer* server) {
                auto instance = server->getViewModelInstance(handle);
                CHECK(instance != nullptr);
                auto property = instance->propertyList(name);
                CHECK(property != nullptr);
                auto valueModel = server->getViewModelInstance(value);
                CHECK(property->instanceAt(index).get() == valueModel);
            });
    }

    void pushExpectation(std::string name, ViewModelInstanceHandle value)
    {
        m_queue->appendViewModelInstanceListViewModel(m_handle, name, value);
        m_queue->runOnce([handle = m_handle, name, value](
                             CommandServer* server) {
            auto instance = server->getViewModelInstance(handle);
            CHECK(instance != nullptr);
            auto property = instance->propertyList(name);
            CHECK(property != nullptr);
            auto valueModel = server->getViewModelInstance(value);
            CHECK(property->instanceAt(static_cast<int>(property->size()) - 1)
                      .get() == valueModel);
        });
    }

    void pushBadExpectation(std::string name,
                            ViewModelInstanceHandle value,
                            int index)
    {
        m_queue->insertViewModelInstanceListViewModel(m_handle,
                                                      name,
                                                      value,
                                                      index);
    }

    void pushBadExpectation(std::string name, ViewModelInstanceHandle value)
    {
        m_queue->appendViewModelInstanceListViewModel(m_handle, name, value);
    }

    void pushBadExpectation(std::string name, int index, int index2)
    {
        m_queue->swapViewModelInstanceListValues(m_handle, name, index, index2);
    }

    void pushBadRequestExpectation(std::string name)
    {
        m_receivedSize = false;
        m_queue->requestViewModelInstanceListSize(m_handle, name);

        wait_for_server(m_queue.get());
        m_queue->processMessages();
        CHECK(!m_receivedSize);
    }

    rcp<CommandQueue> m_queue;
    ViewModelInstanceHandle m_handle;
    bool n_wasDeleted = false;
    // size callback test values

    std::string m_path;
    size_t m_expectedSize;
    uint64_t m_requestIdx = 1;
    bool m_receivedSize = false;
};

TEST_CASE("List View Model Property Set/Get", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);

    std::ifstream stream("assets/data_bind_test_cmdq.riv", std::ios::binary);
    FileHandle fileHandle = commandQueue->loadFile(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}));

    ListViewModelPropertyListener tester(commandQueue);
    auto artboardHandle = commandQueue->instantiateDefaultArtboard(fileHandle);
    tester.m_handle =
        commandQueue->instantiateDefaultViewModelInstance(fileHandle,
                                                          artboardHandle,
                                                          &tester);
    auto blankHandle =
        commandQueue->instantiateBlankViewModelInstance(fileHandle,
                                                        "Nested VM");
    auto alternateHandle =
        commandQueue->instantiateViewModelInstanceNamed(fileHandle,
                                                        "Nested VM",
                                                        "Alternate Nested");
    tester.pushExpectation("Test List", blankHandle);
    tester.pushExpectation("Test List", alternateHandle);
    tester.pushExpectation("Test List", alternateHandle, blankHandle, 2, 3);
    tester.pushRequestExpectation("Test List", 4);
    tester.pushExpectation("Test List", blankHandle, 0);
    tester.pushExpectation("Test List", alternateHandle, 0);
    tester.pushExpectation("Test List", blankHandle, alternateHandle, 0, 1);
    tester.pushRequestExpectation("Test List", 6);

    auto badBlankHandle =
        commandQueue->instantiateBlankViewModelInstance(fileHandle, "blah");
    auto badAlternateHandle =
        commandQueue->instantiateViewModelInstanceNamed(fileHandle,
                                                        "Nested VM",
                                                        "blah");
    tester.pushBadExpectation("Test List", badBlankHandle);
    tester.pushBadExpectation("Test List", badAlternateHandle);
    tester.pushBadExpectation("Test List", badBlankHandle, 0);
    tester.pushBadExpectation("Test List", badAlternateHandle, 0);

    tester.pushBadExpectation("blah", blankHandle);
    tester.pushBadExpectation("blah", alternateHandle);
    tester.pushBadExpectation("blah", blankHandle, 0);
    tester.pushBadExpectation("blah", alternateHandle, 0);

    tester.pushBadExpectation("Test List", 10, 1);
    tester.pushBadExpectation("Test List", 0, 10);
    tester.pushBadExpectation("Blah", 0, 1);
    tester.pushBadExpectation("Blah", 10, 1);
    tester.pushBadExpectation("Blah", 0, 10);

    tester.pushRequestExpectation("Test List", 6);

    tester.pushBadRequestExpectation("Blah");

    commandQueue->disconnect();
    serverThread.join();
}

class TestFileErrorListener : public CommandQueue::FileListener
{
public:
    virtual void onFileError(const FileHandle handle,
                             uint64_t requestId,
                             std::string error) override
    {
        CHECK(handle == m_handle);
        CHECK(error.size());
        ++m_receivedErrors;
    }

    FileHandle m_handle;
    size_t m_receivedErrors = 0;
};

TEST_CASE("file Error Messages", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);

    TestFileErrorListener fileListener;

    std::ifstream stream("assets/data_bind_test_cmdq.riv", std::ios::binary);
    fileListener.m_handle = commandQueue->loadFile(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}),
        &fileListener);

    commandQueue->instantiateArtboardNamed(fileListener.m_handle, "Blah");
    commandQueue->instantiateViewModelInstanceNamed(fileListener.m_handle,
                                                    "Test All",
                                                    "blah");
    commandQueue->instantiateViewModelInstanceNamed(fileListener.m_handle,
                                                    "blah",
                                                    "blah");
    commandQueue->instantiateViewModelInstanceNamed(fileListener.m_handle,
                                                    nullptr,
                                                    "blah");

    commandQueue->instantiateDefaultViewModelInstance(fileListener.m_handle,
                                                      "Blah");
    commandQueue->instantiateDefaultViewModelInstance(fileListener.m_handle,
                                                      nullptr);

    commandQueue->instantiateBlankViewModelInstance(fileListener.m_handle,
                                                    "Blah");
    commandQueue->instantiateBlankViewModelInstance(fileListener.m_handle,
                                                    nullptr);

    wait_for_server(commandQueue.get());
    commandQueue->processMessages();

    CHECK(fileListener.m_receivedErrors == 8);

    TestFileErrorListener badFileListener;
    badFileListener.m_handle =
        commandQueue->loadFile(std::vector<uint8_t>(100 * 1024, 0),
                               &badFileListener);

    commandQueue->instantiateDefaultArtboard(badFileListener.m_handle);
    commandQueue->instantiateDefaultViewModelInstance(badFileListener.m_handle,
                                                      nullptr);

    wait_for_server(commandQueue.get());
    commandQueue->processMessages();
    CHECK(badFileListener.m_receivedErrors == 3);

    commandQueue->disconnect();
    serverThread.join();
}

class TestFileListener : public CommandQueue::FileListener
{
public:
    virtual void onArtboardsListed(
        const FileHandle handle,
        uint64_t requestId,
        std::vector<std::string> artboardNames) override
    {
        CHECK(requestId == m_requestId);
        CHECK(handle == m_handle);
        CHECK(artboardNames.size() == m_artboardNames.size());
        for (auto i = 0; i < artboardNames.size(); ++i)
        {
            CHECK(artboardNames[i] == m_artboardNames[i]);
        }

        m_hasCallback = true;
    }

    uint64_t m_requestId;
    FileHandle m_handle;
    std::vector<std::string> m_artboardNames;
    bool m_hasCallback = false;
};

TEST_CASE("listArtboard", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);

    TestFileListener fileListener;

    std::ifstream stream("assets/entry.riv", std::ios::binary);
    FileHandle goodFile = commandQueue->loadFile(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}),
        &fileListener);

    fileListener.m_artboardNames = {"New Artboard", "New Artboard"};
    fileListener.m_handle = goodFile;

    fileListener.m_requestId = 0x40;
    commandQueue->requestArtboardNames(goodFile, fileListener.m_requestId);

    wait_for_server(commandQueue.get());

    commandQueue->processMessages();

    CHECK(fileListener.m_hasCallback);

    FileHandle badFile =
        commandQueue->loadFile(std::vector<uint8_t>(100 * 1024, 0));
    fileListener.m_handle = goodFile;
    fileListener.m_hasCallback = false;

    commandQueue->requestArtboardNames(badFile);

    wait_for_server(commandQueue.get());

    CHECK(!fileListener.m_hasCallback);

    commandQueue->processMessages();

    commandQueue->disconnect();
    serverThread.join();
}

class TestFileEnumListener : public CommandQueue::FileListener
{
public:
    virtual void onViewModelEnumsListed(const FileHandle handle,
                                        uint64_t requestId,
                                        std::vector<ViewModelEnum> enums)
    {
        CHECK(requestId == m_requestId);
        CHECK(handle == m_handle);
        CHECK(enums.size() == m_enums.size());
        for (auto i = 0; i < enums.size(); ++i)
        {
            CHECK(enums[i].name == m_enums[i].name);
            for (auto k = 0; k < enums[i].enumerants.size(); ++k)
            {
                CHECK(enums[i].enumerants[k] == m_enums[i].enumerants[k]);
            }
        }

        m_hasCallback = true;
    }

    uint64_t m_requestId;
    FileHandle m_handle;
    std::array<ViewModelEnum, 1> m_enums = {
        ViewModelEnum{"Test Enum Values", {"Value 1", "Value 2"}}};
    bool m_hasCallback = false;
};

TEST_CASE("listEnums", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);

    TestFileEnumListener fileListener;

    std::ifstream stream("assets/data_bind_test_cmdq.riv", std::ios::binary);
    FileHandle goodFile = commandQueue->loadFile(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}),
        &fileListener);

    fileListener.m_handle = goodFile;

    fileListener.m_requestId = 0x40;
    commandQueue->requestViewModelEnums(goodFile, fileListener.m_requestId);

    wait_for_server(commandQueue.get());

    commandQueue->processMessages();

    CHECK(fileListener.m_hasCallback);

    FileHandle badFile =
        commandQueue->loadFile(std::vector<uint8_t>(100 * 1024, 0));
    fileListener.m_handle = goodFile;
    fileListener.m_hasCallback = false;

    commandQueue->requestViewModelEnums(badFile);

    wait_for_server(commandQueue.get());

    CHECK(!fileListener.m_hasCallback);

    commandQueue->processMessages();

    commandQueue->disconnect();
    serverThread.join();
}

class TestRenderImageErrorListener : public CommandQueue::RenderImageListener
{
public:
    virtual void onRenderImageError(const RenderImageHandle handle,
                                    uint64_t requestId,
                                    std::string error) override
    {
        CHECK(handle == m_handle);
        CHECK(error.size());
        CHECK(!m_hasCallback);
        m_hasCallback = true;
    }

    RenderImageHandle m_handle;
    bool m_hasCallback = false;
};

class TestAudioSourceErrorListener : public CommandQueue::AudioSourceListener
{
public:
    virtual void onAudioSourceError(const AudioSourceHandle handle,
                                    uint64_t requestId,
                                    std::string error) override
    {
        CHECK(handle == m_handle);
        CHECK(error.size());
        CHECK(!m_hasCallback);
        m_hasCallback = true;
    }

    AudioSourceHandle m_handle;
    bool m_hasCallback = false;
};

class TestFontErrorListener : public CommandQueue::FontListener
{
public:
    virtual void onFontError(const FontHandle handle,
                             uint64_t requestId,
                             std::string error) override
    {
        CHECK(handle == m_handle);
        CHECK(error.size());
        CHECK(!m_hasCallback);
        m_hasCallback = true;
    }

    FontHandle m_handle;
    bool m_hasCallback = false;
};

TEST_CASE("render image / audio source / font error", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);

    TestRenderImageErrorListener imageListener;
    TestAudioSourceErrorListener audioListener;
    TestFontErrorListener fontListener;

    imageListener.m_handle =
        commandQueue->decodeImage(std::vector<uint8_t>(1024, 0),
                                  &imageListener);
    audioListener.m_handle =
        commandQueue->decodeAudio(std::vector<uint8_t>(1024, 0),
                                  &audioListener);
    fontListener.m_handle =
        commandQueue->decodeFont(std::vector<uint8_t>(1024, 0), &fontListener);

    wait_for_server(commandQueue.get());
    commandQueue->processMessages();

    CHECK(imageListener.m_hasCallback);
    CHECK(audioListener.m_hasCallback);
    CHECK(fontListener.m_hasCallback);

    commandQueue->disconnect();
    serverThread.join();
}

class TestStateMachineErrorListener : public CommandQueue::StateMachineListener
{
public:
    virtual void onStateMachineError(const StateMachineHandle handle,
                                     uint64_t requestId,
                                     std::string error) override
    {
        CHECK(handle == m_handle);
        CHECK(error.size());
        ++m_receivedErrors;
    }

    StateMachineHandle m_handle;
    size_t m_receivedErrors = 0;
};

TEST_CASE("state machine error", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);

    TestStateMachineErrorListener listener;

    std::ifstream stream("assets/entry.riv", std::ios::binary);
    FileHandle fileHandle = commandQueue->loadFile(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}));

    auto artboardHandle = commandQueue->instantiateDefaultArtboard(fileHandle);

    listener.m_handle =
        commandQueue->instantiateDefaultStateMachine(artboardHandle, &listener);
    commandQueue->bindViewModelInstance(listener.m_handle, nullptr);

    wait_for_server(commandQueue.get());
    commandQueue->processMessages();

    CHECK(listener.m_receivedErrors == 1);

    TestStateMachineErrorListener badListener;
    badListener.m_handle =
        commandQueue->instantiateDefaultStateMachine(nullptr, &badListener);

    commandQueue->advanceStateMachine(badListener.m_handle, 0);
    commandQueue->pointerDown(badListener.m_handle, {});
    commandQueue->pointerExit(badListener.m_handle, {});
    commandQueue->pointerUp(badListener.m_handle, {});
    commandQueue->pointerMove(badListener.m_handle, {});

    wait_for_server(commandQueue.get());
    commandQueue->processMessages();

    CHECK(badListener.m_receivedErrors == 5);

    commandQueue->disconnect();
    serverThread.join();
}

class TestArtboardErrorListener : public CommandQueue::ArtboardListener
{
public:
    virtual void onArtboardError(const ArtboardHandle handle,
                                 uint64_t requestId,
                                 std::string error) override
    {
        CHECK(handle == m_handle);
        CHECK(error.size());
        ++m_receivedErrors;
    }

    size_t m_receivedErrors = 0;
    ArtboardHandle m_handle;
};

TEST_CASE("artboard errors", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);

    std::ifstream stream("assets/entry.riv", std::ios::binary);
    FileHandle fileHandle = commandQueue->loadFile(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}));

    TestArtboardErrorListener artboardListener;

    artboardListener.m_handle =
        commandQueue->instantiateArtboardNamed(fileHandle,
                                               "New Artboard",
                                               &artboardListener);

    commandQueue->instantiateStateMachineNamed(artboardListener.m_handle,
                                               "Blah");

    wait_for_server(commandQueue.get());
    commandQueue->processMessages();

    CHECK(artboardListener.m_receivedErrors == 1);

    TestArtboardErrorListener badArtboardListener;
    badArtboardListener.m_handle =
        commandQueue->instantiateArtboardNamed(fileHandle,
                                               "Blah",
                                               &badArtboardListener);

    commandQueue->requestStateMachineNames(badArtboardListener.m_handle);
    commandQueue->instantiateDefaultStateMachine(badArtboardListener.m_handle);

    wait_for_server(commandQueue.get());
    commandQueue->processMessages();

    CHECK(badArtboardListener.m_receivedErrors == 2);

    commandQueue->disconnect();
    serverThread.join();
}

class TestArtboardListener : public CommandQueue::ArtboardListener
{
public:
    virtual void onStateMachinesListed(
        const ArtboardHandle handle,
        uint64_t requestId,
        std::vector<std::string> stateMachineNames) override
    {
        CHECK(requestId == m_requestId);
        CHECK(handle == m_handle);
        CHECK(stateMachineNames.size() == m_stateMachineNames.size());
        for (auto i = 0; i < stateMachineNames.size(); ++i)
        {
            CHECK(stateMachineNames[i] == m_stateMachineNames[i]);
        }

        m_hasCallback = true;
    }

    uint64_t m_requestId;
    ArtboardHandle m_handle;
    std::vector<std::string> m_stateMachineNames;
    bool m_hasCallback = false;
};

TEST_CASE("listStateMachine", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);

    std::ifstream stream("assets/entry.riv", std::ios::binary);
    FileHandle goodFile = commandQueue->loadFile(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}));

    TestArtboardListener artboardListener;

    auto artboardHandle =
        commandQueue->instantiateArtboardNamed(goodFile,
                                               "New Artboard",
                                               &artboardListener);
    artboardListener.m_stateMachineNames = {"State Machine 1"};
    artboardListener.m_handle = artboardHandle;

    artboardListener.m_requestId = 0x40;
    commandQueue->requestStateMachineNames(artboardHandle,
                                           artboardListener.m_requestId);

    wait_for_server(commandQueue.get());

    commandQueue->processMessages();

    CHECK(artboardListener.m_hasCallback);

    FileHandle badFile =
        commandQueue->loadFile(std::vector<uint8_t>(100 * 1024, 0));

    auto badArtbaord = commandQueue->instantiateDefaultArtboard(badFile);

    artboardListener.m_handle = badArtbaord;
    artboardListener.m_hasCallback = false;

    artboardListener.m_requestId = 0x40;
    commandQueue->requestStateMachineNames(badArtbaord,
                                           artboardListener.m_requestId);

    wait_for_server(commandQueue.get());

    commandQueue->processMessages();

    CHECK(!artboardListener.m_hasCallback);

    commandQueue->disconnect();
    serverThread.join();
}

class TestArtboardDefaultViewModelListener
    : public CommandQueue::ArtboardListener
{
public:
    virtual void onArtboardError(const ArtboardHandle handle,
                                 uint64_t requestId,
                                 std::string error) override
    {
        CHECK(m_hasErrorCallback == false);
        CHECK(requestId == m_requestId);
        CHECK(handle == m_handle);
        m_hasErrorCallback = true;
    }

    virtual void onDefaultViewModelInfoReceived(
        const ArtboardHandle handle,
        uint64_t requestId,
        std::string viewModelName,
        std::string viewModelInstanceName) override
    {
        CHECK(requestId == m_requestId);
        CHECK(handle == m_handle);
        CHECK(viewModelName == m_expectedViewModel);
        CHECK(viewModelInstanceName == m_expectedViewModelInstance);

        m_hasCallback = true;
    }

    uint64_t m_requestId;
    ArtboardHandle m_handle;
    std::string m_expectedViewModel = "Test All";
    std::string m_expectedViewModelInstance = "Test Default";
    bool m_hasCallback = false;
    bool m_hasErrorCallback = false;
};

TEST_CASE("requestDefaultViewModel", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);

    std::ifstream stream("assets/data_bind_test_cmdq.riv", std::ios::binary);
    FileHandle goodFile = commandQueue->loadFile(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}));

    TestArtboardDefaultViewModelListener artboardListener;

    auto artboardHandle =
        commandQueue->instantiateArtboardNamed(goodFile,
                                               "Test Artboard",
                                               &artboardListener);
    artboardListener.m_handle = artboardHandle;

    artboardListener.m_requestId = 0x40;
    commandQueue->requestDefaultViewModelInfo(artboardHandle,
                                              goodFile,
                                              artboardListener.m_requestId);

    wait_for_server(commandQueue.get());

    commandQueue->processMessages();

    CHECK(artboardListener.m_hasCallback);
    CHECK(!artboardListener.m_hasErrorCallback);

    FileHandle badFile =
        commandQueue->loadFile(std::vector<uint8_t>(100 * 1024, 0));

    auto badArtbaord = commandQueue->instantiateDefaultArtboard(badFile);

    artboardListener.m_hasCallback = false;

    commandQueue->requestDefaultViewModelInfo(badArtbaord,
                                              goodFile,
                                              artboardListener.m_requestId);
    commandQueue->requestDefaultViewModelInfo(artboardHandle,
                                              badFile,
                                              artboardListener.m_requestId);
    commandQueue->requestDefaultViewModelInfo(badArtbaord,
                                              badFile,
                                              artboardListener.m_requestId);

    wait_for_server(commandQueue.get());

    commandQueue->processMessages();

    CHECK(!artboardListener.m_hasCallback);
    CHECK(artboardListener.m_hasErrorCallback);

    std::ifstream tstream("assets/entry.riv", std::ios::binary);
    FileHandle noViewModelFile = commandQueue->loadFile(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(tstream), {}));

    TestArtboardDefaultViewModelListener noViewModelListener;
    noViewModelListener.m_handle =
        commandQueue->instantiateDefaultArtboard(noViewModelFile,
                                                 &noViewModelListener);

    commandQueue->requestDefaultViewModelInfo(noViewModelListener.m_handle,
                                              noViewModelFile,
                                              noViewModelListener.m_requestId);

    wait_for_server(commandQueue.get());

    commandQueue->processMessages();

    CHECK(artboardListener.m_hasErrorCallback);

    commandQueue->disconnect();
    serverThread.join();
}

class TestStateMachineListener : public CommandQueue::StateMachineListener
{
public:
    virtual void onStateMachineDeleted(const StateMachineHandle handle,
                                       uint64_t requestId)
    {
        CHECK(m_handle == handle);
    }

    virtual void onStateMachineSettled(const StateMachineHandle handle,
                                       uint64_t requestId)
    {
        CHECK(m_handle == handle);
        CHECK(m_requestId == requestId);
        m_hasCallbck = true;
    }

    StateMachineHandle m_handle = RIVE_NULL_HANDLE;
    uint64_t m_requestId = 0;
    bool m_hasCallbck = false;
};

TEST_CASE("bindViewModelInstance", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::unique_ptr<gpu::RenderContext> nullContext =
        RenderContextNULL::MakeContext();
    CommandServer server(commandQueue, nullContext.get());

    std::ifstream stream("assets/data_bind_test_cmdq.riv", std::ios::binary);
    FileHandle fileHandle = commandQueue->loadFile(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}));

    auto viewModelHandle =
        commandQueue->instantiateViewModelInstanceNamed(fileHandle,
                                                        "Test All",
                                                        "Test Alternate");

    auto artboard = commandQueue->instantiateDefaultArtboard(fileHandle);
    auto stateMachineHandle =
        commandQueue->instantiateDefaultStateMachine(artboard);

    commandQueue->bindViewModelInstance(stateMachineHandle, viewModelHandle);

    commandQueue->runOnce([stateMachineHandle,
                           viewModelHandle](CommandServer* server) {
        auto stateMachine = server->getStateMachineInstance(stateMachineHandle);
        CHECK(stateMachine != nullptr);
        auto viewModel = server->getViewModelInstance(viewModelHandle);
        CHECK(viewModel != nullptr);

        CHECK(stateMachine->artboard()->dataContext()->viewModelInstance() ==
              viewModel->instance());
    });

    auto badInstanceHandle =
        commandQueue->instantiateViewModelInstanceNamed(fileHandle,
                                                        "blah",
                                                        "Test Alternate");

    auto badStateMachineHandle =
        commandQueue->instantiateStateMachineNamed(artboard, "blah");

    // every combo of good / bad handles

    commandQueue->bindViewModelInstance(stateMachineHandle, badInstanceHandle);
    commandQueue->bindViewModelInstance(badStateMachineHandle, viewModelHandle);
    commandQueue->bindViewModelInstance(badStateMachineHandle,
                                        badInstanceHandle);

    server.processCommands();
    commandQueue->processMessages();

    commandQueue->disconnect();
}

TEST_CASE("advanceStateMachine", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::unique_ptr<gpu::RenderContext> nullContext =
        RenderContextNULL::MakeContext();
    CommandServer server(commandQueue, nullContext.get());

    std::ifstream stream("assets/settler.riv", std::ios::binary);
    FileHandle goodFile = commandQueue->loadFile(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}));

    auto artboardHandle = commandQueue->instantiateDefaultArtboard(goodFile);

    TestStateMachineListener stateMachineListener;
    stateMachineListener.m_handle =
        commandQueue->instantiateDefaultStateMachine(artboardHandle,
                                                     &stateMachineListener);

    commandQueue->advanceStateMachine(stateMachineListener.m_handle, 10);
    commandQueue->advanceStateMachine(stateMachineListener.m_handle,
                                      10.0,
                                      stateMachineListener.m_requestId);
    // the last advance is what actually settles the statemachine, so we store
    // that id.

    stateMachineListener.m_requestId = 0x50;
    commandQueue->advanceStateMachine(stateMachineListener.m_handle,
                                      10.0,
                                      stateMachineListener.m_requestId);

    server.processCommands();
    commandQueue->processMessages();

    CHECK(stateMachineListener.m_hasCallbck);

    TestStateMachineListener badStateMachineListener;

    badStateMachineListener.m_handle =
        commandQueue->instantiateStateMachineNamed(artboardHandle,
                                                   "blah blah",
                                                   &badStateMachineListener);
    badStateMachineListener.m_requestId = 0x51;
    commandQueue->advanceStateMachine(badStateMachineListener.m_handle,
                                      10,
                                      badStateMachineListener.m_requestId);

    server.processCommands();
    commandQueue->processMessages();

    CHECK(!badStateMachineListener.m_hasCallbck);
}

class DeleteFileListener : public CommandQueue::FileListener
{
public:
    virtual void onFileDeleted(const FileHandle handle,
                               uint64_t requestId) override
    {
        CHECK(requestId == m_requestId);
        CHECK(handle == m_handle);
        m_hasCallback = true;
    }

    uint64_t m_requestId;
    FileHandle m_handle;
    bool m_hasCallback = false;
};

class DeleteArtboardListener : public CommandQueue::ArtboardListener
{
public:
    virtual void onArtboardDeleted(const ArtboardHandle handle,
                                   uint64_t requestId) override
    {
        CHECK(requestId == m_requestId);
        CHECK(handle == m_handle);
        m_hasCallback = true;
    }

    uint64_t m_requestId;
    ArtboardHandle m_handle;
    bool m_hasCallback = false;
};

class DeleteStateMachineListener : public CommandQueue::StateMachineListener
{
public:
    virtual void onStateMachineDeleted(const StateMachineHandle handle,
                                       uint64_t requestId) override
    {
        CHECK(requestId == m_requestId);
        CHECK(handle == m_handle);
        m_hasCallback = true;
    }

    uint64_t m_requestId;
    StateMachineHandle m_handle;
    bool m_hasCallback = false;
};

class DeleteRenderImageListener : public CommandQueue::RenderImageListener
{
public:
    virtual void onRenderImageDeleted(const RenderImageHandle handle,
                                      uint64_t requestId) override
    {
        CHECK(requestId == m_requestId);
        CHECK(handle == m_handle);
        m_hasCallback = true;
    }

    uint64_t m_requestId;
    RenderImageHandle m_handle;
    bool m_hasCallback = false;
};

TEST_CASE("listenerDeleteCallbacks", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);

    DeleteFileListener fileListener;
    DeleteArtboardListener artboardListener;
    DeleteStateMachineListener stateMachineListener;
    DeleteRenderImageListener renderImageListener;

    std::ifstream stream("assets/entry.riv", std::ios::binary);
    FileHandle goodFile = commandQueue->loadFile(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}),
        &fileListener);

    auto artboardHandle =
        commandQueue->instantiateArtboardNamed(goodFile,
                                               "New Artboard",
                                               &artboardListener);

    auto stateMachineHandle =
        commandQueue->instantiateStateMachineNamed(artboardHandle,
                                                   "",
                                                   &stateMachineListener);

    std::ifstream imageStream("assets/batdude.png", std::ios::binary);
    auto renderImage = commandQueue->decodeImage(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}),
        &renderImageListener);

    fileListener.m_handle = goodFile;
    artboardListener.m_handle = artboardHandle;
    stateMachineListener.m_handle = stateMachineHandle;
    renderImageListener.m_handle = renderImage;

    CHECK(!fileListener.m_hasCallback);
    CHECK(!artboardListener.m_hasCallback);
    CHECK(!stateMachineListener.m_hasCallback);
    CHECK(!renderImageListener.m_hasCallback);

    wait_for_server(commandQueue.get());
    commandQueue->processMessages();

    CHECK(!fileListener.m_hasCallback);
    CHECK(!artboardListener.m_hasCallback);
    CHECK(!stateMachineListener.m_hasCallback);
    CHECK(!renderImageListener.m_hasCallback);

    stateMachineListener.m_requestId = 0x50;
    commandQueue->deleteStateMachine(stateMachineHandle,
                                     stateMachineListener.m_requestId);
    artboardListener.m_requestId = 0x51,
    commandQueue->deleteArtboard(artboardHandle, artboardListener.m_requestId);
    fileListener.m_requestId = 0x52;
    commandQueue->deleteFile(goodFile, fileListener.m_requestId);
    renderImageListener.m_requestId = 0x53;
    commandQueue->deleteImage(renderImage, renderImageListener.m_requestId);

    wait_for_server(commandQueue.get());
    commandQueue->processMessages();

    CHECK(fileListener.m_hasCallback);
    CHECK(artboardListener.m_hasCallback);
    CHECK(stateMachineListener.m_hasCallback);
    CHECK(renderImageListener.m_hasCallback);

    commandQueue->disconnect();
    serverThread.join();
}

class LoadedFileListener : public CommandQueue::FileListener
{
public:
    virtual void onFileLoaded(const FileHandle handle,
                              uint64_t requestId) override
    {
        CHECK(requestId == m_requestId);
        CHECK(handle == m_handle);
        m_hasCallback = true;
    }

    uint64_t m_requestId = 0x10;
    FileHandle m_handle = RIVE_NULL_HANDLE;
    bool m_hasCallback = false;
};

TEST_CASE("fileLoadedCallback", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);
    std::ifstream stream("assets/entry.riv", std::ios::binary);

    LoadedFileListener fileListener;

    fileListener.m_handle = commandQueue->loadFile(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}),
        &fileListener,
        fileListener.m_requestId);

    wait_for_server(commandQueue.get());
    commandQueue->processMessages();

    CHECK(fileListener.m_hasCallback);

    LoadedFileListener badFileListener;
    badFileListener.m_handle =
        commandQueue->loadFile(std::vector<uint8_t>(1024, {}),
                               &badFileListener,
                               badFileListener.m_requestId);

    wait_for_server(commandQueue.get());
    commandQueue->processMessages();

    CHECK(!badFileListener.m_hasCallback);

    commandQueue->disconnect();
    serverThread.join();
}

class DecodedImageListener : public CommandQueue::RenderImageListener
{
public:
    virtual void onRenderImageDecoded(const RenderImageHandle handle,
                                      uint64_t requestId) override
    {
        CHECK(requestId == m_requestId);
        CHECK(handle == m_handle);
        m_hasCallback = true;
    }

    uint64_t m_requestId = 0x10;
    RenderImageHandle m_handle = RIVE_NULL_HANDLE;
    bool m_hasCallback = false;
};

class DecodedAudioListener : public CommandQueue::AudioSourceListener
{
public:
    virtual void onAudioSourceDecoded(const AudioSourceHandle handle,
                                      uint64_t requestId) override
    {
        CHECK(requestId == m_requestId);
        CHECK(handle == m_handle);
        m_hasCallback = true;
    }

    uint64_t m_requestId = 0x10;
    AudioSourceHandle m_handle = RIVE_NULL_HANDLE;
    bool m_hasCallback = false;
};

class DecodedFontListener : public CommandQueue::FontListener
{
public:
    virtual void onFontDecoded(const FontHandle handle,
                               uint64_t requestId) override
    {
        CHECK(requestId == m_requestId);
        CHECK(handle == m_handle);
        m_hasCallback = true;
    }

    uint64_t m_requestId = 0x10;
    FontHandle m_handle = RIVE_NULL_HANDLE;
    bool m_hasCallback = false;
};

TEST_CASE("decodedCallbacks", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);
    std::ifstream stream("assets/entry.riv", std::ios::binary);

    DecodedImageListener imageListener;
    DecodedAudioListener audioListener;
    DecodedFontListener fontListener;

    std::ifstream imageStream("assets/batdude.png", std::ios::binary);
    imageListener.m_handle = commandQueue->decodeImage(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(imageStream), {}),
        &imageListener,
        imageListener.m_requestId);

    std::ifstream audioStream("assets/audio/what.wav", std::ios::binary);
    audioListener.m_handle = commandQueue->decodeAudio(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(audioStream), {}),
        &audioListener,
        audioListener.m_requestId);

    std::ifstream fontStream("assets/fonts/OpenSans-Italic.ttf",
                             std::ios::binary);
    fontListener.m_handle = commandQueue->decodeFont(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(fontStream), {}),
        &fontListener,
        fontListener.m_requestId);

    wait_for_server(commandQueue.get());
    commandQueue->processMessages();

    CHECK(imageListener.m_hasCallback);
    CHECK(audioListener.m_hasCallback);
    CHECK(fontListener.m_hasCallback);

    DecodedImageListener badimageListener;
    DecodedAudioListener badaudioListener;
    DecodedFontListener badfontListener;

    badimageListener.m_handle =
        commandQueue->decodeImage(std::vector<uint8_t>(1024, {}),
                                  &badimageListener,
                                  badimageListener.m_requestId);

    badaudioListener.m_handle =
        commandQueue->decodeAudio(std::vector<uint8_t>(1024, {}),
                                  &badaudioListener,
                                  badaudioListener.m_requestId);

    badfontListener.m_handle =
        commandQueue->decodeFont(std::vector<uint8_t>(1024, {}),
                                 &badfontListener,
                                 badfontListener.m_requestId);

    wait_for_server(commandQueue.get());
    commandQueue->processMessages();

    CHECK(!badimageListener.m_hasCallback);
    CHECK(!badaudioListener.m_hasCallback);
    CHECK(!badfontListener.m_hasCallback);

    commandQueue->disconnect();
    serverThread.join();
}

TEST_CASE("listenerLifeTimes", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);

    std::ifstream stream("assets/entry.riv", std::ios::binary);

    CommandQueue::FileListener fileListener;
    CommandQueue::ArtboardListener artboardListener;
    CommandQueue::StateMachineListener stateMachineListener;

    FileHandle fileHandle = commandQueue->loadFile(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}),
        &fileListener);

    auto artboardHandle =
        commandQueue->instantiateArtboardNamed(fileHandle,
                                               "New Artboard",
                                               &artboardListener);

    auto stateMachineHandle =
        commandQueue->instantiateDefaultStateMachine(artboardHandle,
                                                     &stateMachineListener);

    commandQueue->requestArtboardNames(fileHandle);
    commandQueue->requestStateMachineNames(artboardHandle);

    CHECK(commandQueue->testing_getFileListener(fileHandle));
    CHECK(commandQueue->testing_getArtboardListener(artboardHandle));
    CHECK(commandQueue->testing_getStateMachineListener(stateMachineHandle));

    wait_for_server(commandQueue.get());

    commandQueue->processMessages();

    CommandQueue::FileListener deleteFileListener;
    CommandQueue::ArtboardListener deleteArtboardListener;
    CommandQueue::StateMachineListener deleteStateMachineListener;

    FileHandle dFileHandle = commandQueue->loadFile(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}),
        &deleteFileListener);

    auto dArtboardHandle =
        commandQueue->instantiateArtboardNamed(fileHandle,
                                               "New Artboard",
                                               &deleteArtboardListener);

    auto dStateMachineHandle = commandQueue->instantiateDefaultStateMachine(
        artboardHandle,
        &deleteStateMachineListener);

    CHECK(commandQueue->testing_getFileListener(dFileHandle));
    CHECK(commandQueue->testing_getArtboardListener(dArtboardHandle));
    CHECK(commandQueue->testing_getStateMachineListener(dStateMachineHandle));

    commandQueue->deleteFile(dFileHandle);
    commandQueue->deleteArtboard(dArtboardHandle);
    commandQueue->deleteStateMachine(dStateMachineHandle);

    wait_for_server(commandQueue.get());
    commandQueue->processMessages();

    CHECK(commandQueue->testing_getFileListener(dFileHandle));
    CHECK(commandQueue->testing_getArtboardListener(dArtboardHandle));
    CHECK(commandQueue->testing_getStateMachineListener(dStateMachineHandle));

    commandQueue->disconnect();
    serverThread.join();

    FileHandle fh;

    {
        CommandQueue::FileListener listener;
        fh = commandQueue->loadFile(std::vector<uint8_t>(100 * 1024, 0),
                                    &listener);
        CHECK(commandQueue->testing_getFileListener(fh));
    }

    CHECK(!commandQueue->testing_getFileListener(fh));

    ArtboardHandle ah;
    {
        CommandQueue::ArtboardListener listener;
        ah = commandQueue->instantiateDefaultArtboard(fh, &listener);
        CHECK(commandQueue->testing_getArtboardListener(ah));
    }
    CHECK(!commandQueue->testing_getArtboardListener(ah));

    StateMachineHandle sh;
    {
        CommandQueue::StateMachineListener listener;
        sh = commandQueue->instantiateDefaultStateMachine(ah, &listener);
        CHECK(commandQueue->testing_getStateMachineListener(sh));
    }
    CHECK(!commandQueue->testing_getStateMachineListener(sh));

    CHECK(commandQueue->testing_getFileListener(fileHandle));
    CHECK(commandQueue->testing_getArtboardListener(artboardHandle));
    CHECK(commandQueue->testing_getStateMachineListener(stateMachineHandle));

    // If we move we should now point to the new listener
    CommandQueue::FileListener newFileListener = std::move(fileListener);
    auto listenerPtr = commandQueue->testing_getFileListener(fileHandle);
    CHECK(&newFileListener == listenerPtr);

    CommandQueue::ArtboardListener newArtboardListener =
        std::move(artboardListener);
    auto listenerPtr1 =
        commandQueue->testing_getArtboardListener(artboardHandle);
    CHECK(&newArtboardListener == listenerPtr1);

    CommandQueue::StateMachineListener newStateMachineListener =
        std::move(stateMachineListener);
    auto listenerPtr2 =
        commandQueue->testing_getStateMachineListener(stateMachineHandle);
    CHECK(&newStateMachineListener == listenerPtr2);

    // force unref the commandQueue to ensure it stays alive for listeners
    commandQueue.reset();

    // after this we are checking the destructors for fileListener,
    // artboardListener and stateMachineListener as they should gracefully
    // remove themselves from the commandQeueue even though the ref here is gone
}

TEST_CASE("empty test for code cove", "[CommandQueue]")
{
    CommandQueue::FileListener fileL;
    CommandQueue::ArtboardListener artboardL;
    CommandQueue::StateMachineListener statemachineL;

    std::vector<std::string> emptyVector;

    fileL.onFileDeleted(0, 0);
    fileL.onArtboardsListed(0, 0, emptyVector);

    artboardL.onArtboardDeleted(0, 0);
    artboardL.onStateMachinesListed(0, 0, emptyVector);

    statemachineL.onStateMachineDeleted(0, 0);
    statemachineL.onStateMachineSettled(0, 0);

    CHECK(true);
}

// Helps with repetition of checks in the following test
void checkStateMachineBool(rcp<CommandQueue>& commandQueue,
                           StateMachineHandle handle,
                           const char* boolName,
                           bool expectedValue)
{
    commandQueue->runOnce(
        [handle, boolName, expectedValue](CommandServer* server) {
            rive::StateMachineInstance* sm =
                server->getStateMachineInstance(handle);
            CHECK(sm->getBool(boolName)->value() == expectedValue);
        });
}

TEST_CASE("pointer input", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);

    std::ifstream stream("assets/pointer_events.riv", std::ios::binary);
    FileHandle fileHandle = commandQueue->loadFile(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}));
    ArtboardHandle artboardHandle =
        commandQueue->instantiateDefaultArtboard(fileHandle);
    StateMachineHandle smHandle =
        commandQueue->instantiateDefaultStateMachine(artboardHandle);

    commandQueue->runOnce(
        [fileHandle, artboardHandle, smHandle](CommandServer* server) {
            CHECK(fileHandle != RIVE_NULL_HANDLE);
            CHECK(server->getFile(fileHandle) != nullptr);
            CHECK(artboardHandle != RIVE_NULL_HANDLE);
            CHECK(server->getArtboardInstance(artboardHandle) != nullptr);
            CHECK(smHandle != RIVE_NULL_HANDLE);
            CHECK(server->getStateMachineInstance(smHandle) != nullptr);
        });

    // Prime for events by advancing once
    commandQueue->advanceStateMachine(smHandle, 0.0f);

    // The listener object in the bottom-left corner,
    // which toggles `isDown` when receiving down events.
    Vec2D toggleOnDownCorner(425.0f, 425.0f);
    commandQueue->pointerDown(smHandle, {.position = toggleOnDownCorner});
    checkStateMachineBool(commandQueue, smHandle, "isDown", true);
    commandQueue->pointerUp(smHandle, {.position = toggleOnDownCorner});
    checkStateMachineBool(commandQueue, smHandle, "isDown", true);
    commandQueue->pointerDown(smHandle, {.position = toggleOnDownCorner});
    checkStateMachineBool(commandQueue, smHandle, "isDown", false);

    // The listener object in the top-left corner,
    // which toggles `isDown` when receiving down or up events.
    Vec2D toggleOnDownOrUpCorner(75.0f, 75.0f);
    commandQueue->pointerDown(smHandle, {.position = toggleOnDownOrUpCorner});
    checkStateMachineBool(commandQueue, smHandle, "isDown", true);
    commandQueue->pointerUp(smHandle, {.position = toggleOnDownOrUpCorner});
    checkStateMachineBool(commandQueue, smHandle, "isDown", false);

    // The listener object in the top-right corner,
    // which toggles `isDown` when hovered.
    Vec2D toggleOnHoverCorner(425.0f, 75.0f);
    // Center, which has no pointer listener.
    Vec2D center(250.0f, 250.0f);
    commandQueue->pointerMove(smHandle, {.position = center});
    checkStateMachineBool(commandQueue, smHandle, "isDown", false);
    commandQueue->pointerMove(smHandle, {.position = toggleOnHoverCorner});
    checkStateMachineBool(commandQueue, smHandle, "isDown", true);
    commandQueue->pointerMove(smHandle, {.position = center});
    checkStateMachineBool(commandQueue, smHandle, "isDown", true);
    commandQueue->pointerMove(smHandle, {.position = toggleOnHoverCorner});
    checkStateMachineBool(commandQueue, smHandle, "isDown", false);

    // A position off of the artboard, which can be used for pointer exits.
    Vec2D offArtboard(-25.0f, -25.0f);
    commandQueue->pointerDown(smHandle, {.position = toggleOnDownOrUpCorner});
    checkStateMachineBool(commandQueue, smHandle, "isDown", true);
    // Slide off while "holding down" the pointer.
    commandQueue->pointerExit(smHandle, {.position = offArtboard});
    checkStateMachineBool(commandQueue, smHandle, "isDown", true);
    // Release the pointer while off the artboard - should not toggle
    commandQueue->pointerUp(smHandle, {.position = offArtboard});
    checkStateMachineBool(commandQueue, smHandle, "isDown", true);
    // Reset
    commandQueue->pointerUp(smHandle, {.position = toggleOnDownOrUpCorner});
    checkStateMachineBool(commandQueue, smHandle, "isDown", false);

    // New test, sliding off and back, but this time releasing back on the
    // artboard.
    commandQueue->pointerDown(smHandle, {.position = toggleOnDownOrUpCorner});
    checkStateMachineBool(commandQueue, smHandle, "isDown", true);
    commandQueue->pointerExit(smHandle, {.position = offArtboard});
    checkStateMachineBool(commandQueue, smHandle, "isDown", true);
    commandQueue->pointerMove(smHandle, {.position = toggleOnDownOrUpCorner});
    checkStateMachineBool(commandQueue, smHandle, "isDown", true);
    commandQueue->pointerUp(smHandle, {.position = toggleOnDownOrUpCorner});
    checkStateMachineBool(commandQueue, smHandle, "isDown", false);

    commandQueue->disconnect();
    serverThread.join();
}

static bool aboutEquals(const Vec2D& l, const Vec2D& r, float theta = 0.0001)
{
    auto b = l - r;
    return abs(b.x) < theta && abs(b.y) < theta;
}

TEST_CASE("pointer input translation", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::unique_ptr<gpu::RenderContext> nullContext =
        RenderContextNULL::MakeContext();
    CommandServer server(commandQueue, nullContext.get());

    std::ifstream stream("assets/data_bind_test_cmdq.riv", std::ios::binary);
    FileHandle fileHandle = commandQueue->loadFile(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}));
    ArtboardHandle artboardHandle =
        commandQueue->instantiateDefaultArtboard(fileHandle);
    StateMachineHandle smHandle =
        commandQueue->instantiateDefaultStateMachine(artboardHandle);
    // artboard is 500x500 so 50x50 should translate to 250,250

    // expected result
    Vec2D expected(250, 250);
    // Inputs
    Vec2D center(50, 50);
    Vec2D size(100, 100);

    commandQueue->runOnce([expected, center, size, smHandle](
                              CommandServer* server) {
        auto instance = server->getStateMachineInstance(smHandle);
        CHECK(instance != nullptr);
        auto translated = server->testing_cursorPosForPointerEvent(
            instance,
            {.fit = Fit::contain, .screenBounds = size, .position = center});
        CHECK(aboutEquals(translated, expected));
    });

    // expected result
    Vec2D expectedTL(125, 125);
    // Inputs
    Vec2D topLeft(25, 25);

    commandQueue->runOnce([expectedTL, topLeft, size, smHandle](
                              CommandServer* server) {
        auto instance = server->getStateMachineInstance(smHandle);
        CHECK(instance != nullptr);
        auto translated = server->testing_cursorPosForPointerEvent(
            instance,
            {.fit = Fit::contain, .screenBounds = size, .position = topLeft});
        CHECK(aboutEquals(translated, expectedTL));
    });

    // expected result
    Vec2D expectedTR(375, 375);
    // Inputs
    Vec2D topRight(75, 75);

    commandQueue->runOnce([expectedTR, topRight, size, smHandle](
                              CommandServer* server) {
        auto instance = server->getStateMachineInstance(smHandle);
        CHECK(instance != nullptr);
        auto translated = server->testing_cursorPosForPointerEvent(
            instance,
            {.fit = Fit::contain, .screenBounds = size, .position = topRight});
        CHECK(aboutEquals(translated, expectedTR));
    });

    // expected result
    Vec2D expectedBR(375, 125);
    // Inputs
    Vec2D bottomRight(75, 25);

    commandQueue->runOnce([bottomRight, expectedBR, size, smHandle](
                              CommandServer* server) {
        auto instance = server->getStateMachineInstance(smHandle);
        CHECK(instance != nullptr);
        auto translated =
            server->testing_cursorPosForPointerEvent(instance,
                                                     {.fit = Fit::contain,
                                                      .screenBounds = size,
                                                      .position = bottomRight});
        CHECK(aboutEquals(translated, expectedBR));
    });

    // expected result
    Vec2D expectedBL(125, 375);
    // Inputs
    Vec2D bottomLeft(25, 75);

    commandQueue->runOnce([bottomLeft, expectedBL, size, smHandle](
                              CommandServer* server) {
        auto instance = server->getStateMachineInstance(smHandle);
        CHECK(instance != nullptr);
        auto translated =
            server->testing_cursorPosForPointerEvent(instance,
                                                     {.fit = Fit::contain,
                                                      .screenBounds = size,
                                                      .position = bottomLeft});
        CHECK(aboutEquals(translated, expectedBL));
    });

    server.processCommands();
    commandQueue->processMessages();

    commandQueue->disconnect();
}

// clang format is removing the needed space between the func names and the (
// clang-format off
#define DEFINE_TEST_CALLBACK(fun, handleType, expectedRequestId)               \
    bool m_##fun##WasCalled = false;                                           \
    virtual void fun (const handleType handle, uint64_t requestId) override    \
    {                                                                          \
        CHECK(handle == m_handle);                                            \
        CHECK(requestId == expectedRequestId);                                \
        m_##fun##WasCalled = true;                                             \
    }

#define DEFINE_TEST_CALLBACK_ONE_PARAM(fun,                                    \
                                       handleType,                             \
                                       expectedRequestId,                      \
                                       paramType,                              \
                                       param)                                  \
    bool m_##fun##WasCalled = false;                                           \
    virtual void fun (const handleType handle,                                 \
                     uint64_t requestId,                                       \
                     paramType param) override                                 \
    {                                                                          \
        CHECK(handle == m_handle);                                            \
        CHECK(requestId == expectedRequestId);                                \
        CHECK(param == m_##param);                                            \
        m_##fun##WasCalled = true;                                             \
    }

#define DEFINE_TEST_CALLBACK_TWO_PARAM(fun,                                    \
                                       handleType,                             \
                                       expectedRequestId,                      \
                                       paramType,                              \
                                       param,                                  \
                                       param2Type,                             \
                                       param2)                                 \
    bool m_##fun##WasCalled = false;                                           \
    virtual void fun (const handleType handle,                                 \
                     uint64_t requestId,                                       \
                     paramType param,                                          \
                     param2Type param2) override                               \
    {                                                                          \
        CHECK(handle == m_handle);                                            \
        CHECK(requestId == expectedRequestId);                                \
        CHECK(param == m_##param);                                            \
        CHECK(param2 == m_##param2);                                          \
        m_##fun##WasCalled = true;                                             \
    }
// clang-format on
#define CHECK_CALLBACK(obj, func) CHECK(obj.m_##func##WasCalled)

class GlobalFileListener : public CommandQueue::FileListener
{
public:
    virtual void onFileError(const FileHandle,
                             uint64_t requestId,
                             std::string error) override
    {}

    DEFINE_TEST_CALLBACK(onFileDeleted, FileHandle, 7);
    DEFINE_TEST_CALLBACK(onFileLoaded, FileHandle, 1);
    DEFINE_TEST_CALLBACK_ONE_PARAM(onArtboardsListed,
                                   FileHandle,
                                   2,
                                   std::vector<std::string>,
                                   artboardNames);
    DEFINE_TEST_CALLBACK_ONE_PARAM(onViewModelsListed,
                                   FileHandle,
                                   3,
                                   std::vector<std::string>,
                                   viewModelNames);

    DEFINE_TEST_CALLBACK_TWO_PARAM(onViewModelInstanceNamesListed,
                                   FileHandle,
                                   4,
                                   std::string,
                                   viewModelNameI,
                                   std::vector<std::string>,
                                   instanceNames);
    DEFINE_TEST_CALLBACK_TWO_PARAM(
        onViewModelPropertiesListed,
        FileHandle,
        5,
        std::string,
        viewModelNameP,
        std::vector<CommandQueue::FileListener::ViewModelPropertyData>,
        properties);
    DEFINE_TEST_CALLBACK_ONE_PARAM(onViewModelEnumsListed,
                                   FileHandle,
                                   6,
                                   std::vector<ViewModelEnum>,
                                   enums);

    FileHandle m_handle;
    std::array<std::string, 4> m_artboardNames = {"Test Artboard",
                                                  "Test Transitions",
                                                  "Test Observation",
                                                  "Artboard"};
    std::array<std::string, 6> m_viewModelNames = {"ListViewModel",
                                                   "Empty VM",
                                                   "Test All",
                                                   "Nested VM",
                                                   "State Transition",
                                                   "Alternate VM"};
    std::array<std::string, 2> m_instanceNames = {"Test Default",
                                                  "Test Alternate"};
    std::array<CommandQueue::FileListener::ViewModelPropertyData, 10>
        m_properties = {
            CommandQueue::FileListener::ViewModelPropertyData{
                DataType::artboard,
                "Test Artboard"},
            CommandQueue::FileListener::ViewModelPropertyData{DataType::list,
                                                              "Test List"},
            CommandQueue::FileListener::ViewModelPropertyData{
                DataType::assetImage,
                "Test Image"},
            CommandQueue::FileListener::ViewModelPropertyData{DataType::number,
                                                              "Test Num"},
            CommandQueue::FileListener::ViewModelPropertyData{DataType::string,
                                                              "Test String"},
            CommandQueue::FileListener::ViewModelPropertyData{
                DataType::enumType,
                "Test Enum",
                "Test Enum Values"},
            CommandQueue::FileListener::ViewModelPropertyData{DataType::boolean,
                                                              "Test Bool"},
            CommandQueue::FileListener::ViewModelPropertyData{DataType::color,
                                                              "Test Color"},
            CommandQueue::FileListener::ViewModelPropertyData{DataType::trigger,
                                                              "Test Trigger"},
            CommandQueue::FileListener::ViewModelPropertyData{
                DataType::viewModel,
                "Test Nested",
                "Nested VM"}};
    std::array<ViewModelEnum, 1> m_enums = {
        ViewModelEnum{"Test Enum Values", {"Value 1", "Value 2"}}};
    std::string m_viewModelNameI = "Test All";
    std::string m_viewModelNameP = "Test All";
};

class GlobalRenderImageListener : public CommandQueue::RenderImageListener
{
public:
    virtual void onRenderImageError(const RenderImageHandle,
                                    uint64_t requestId,
                                    std::string error) override
    {}

    DEFINE_TEST_CALLBACK(onRenderImageDeleted, RenderImageHandle, 8);

    RenderImageHandle m_handle;
};

class GlobalAudioSourceListener : public CommandQueue::AudioSourceListener
{
public:
    virtual void onAudioSourceError(const AudioSourceHandle,
                                    uint64_t requestId,
                                    std::string error) override
    {}

    DEFINE_TEST_CALLBACK(onAudioSourceDeleted, AudioSourceHandle, 9);

    AudioSourceHandle m_handle;
};

class GlobalFontListener : public CommandQueue::FontListener
{
public:
    virtual void onFontError(const FontHandle,
                             uint64_t requestId,
                             std::string error) override
    {}

    DEFINE_TEST_CALLBACK(onFontDeleted, FontHandle, 10);

    FontHandle m_handle;
};

class GlobalArtboardListener : public CommandQueue::ArtboardListener
{
public:
    virtual void onArtboardError(const ArtboardHandle,
                                 uint64_t requestId,
                                 std::string error) override
    {}

    DEFINE_TEST_CALLBACK(onArtboardDeleted, ArtboardHandle, 12);
    DEFINE_TEST_CALLBACK_ONE_PARAM(onStateMachinesListed,
                                   ArtboardHandle,
                                   11,
                                   std::vector<std::string>,
                                   stateMachineNames);
    DEFINE_TEST_CALLBACK_TWO_PARAM(onDefaultViewModelInfoReceived,
                                   ArtboardHandle,
                                   20,
                                   std::string,
                                   viewModelName,
                                   std::string,
                                   instanceName);

    ArtboardHandle m_handle;
    std::array<std::string, 1> m_stateMachineNames = {"SM"};
    std::string m_viewModelName = "Test All";
    std::string m_instanceName = "Test Default";
};

class GlobalViewModelInstanceListener
    : public CommandQueue::ViewModelInstanceListener
{
public:
    virtual void onViewModelInstanceError(const ViewModelInstanceHandle,
                                          uint64_t requestId,
                                          std::string error) override
    {}

    DEFINE_TEST_CALLBACK(onViewModelDeleted, ViewModelInstanceHandle, 15);

    DEFINE_TEST_CALLBACK_ONE_PARAM(onViewModelDataReceived,
                                   ViewModelInstanceHandle,
                                   13,
                                   CommandQueue::ViewModelInstanceData,
                                   instanceData);

    DEFINE_TEST_CALLBACK_TWO_PARAM(onViewModelListSizeReceived,
                                   ViewModelInstanceHandle,
                                   14,
                                   std::string,
                                   path,
                                   size_t,
                                   size);

    size_t m_size = 2;
    std::string m_path = "Test List";
    ViewModelInstanceHandle m_handle;
    CommandQueue::ViewModelInstanceData m_instanceData = {
        .metaData = PropertyData{DataType::boolean, "Test Bool"},
        .boolValue = true};
};

class GlobalStateMachineListener : public CommandQueue::StateMachineListener
{
public:
    virtual void onStateMachineError(const StateMachineHandle,
                                     uint64_t requestId,
                                     std::string error) override
    {}

    DEFINE_TEST_CALLBACK(onStateMachineDeleted, StateMachineHandle, 17);
    DEFINE_TEST_CALLBACK(onStateMachineSettled, StateMachineHandle, 16);

    StateMachineHandle m_handle;
};

TEST_CASE("global Listener", "[CommandQueue]")
{
    GlobalStateMachineListener globalStateMachineListener;
    GlobalViewModelInstanceListener globalViewModelInstanceListener;
    GlobalArtboardListener globalArtboardListener;
    GlobalFontListener globalFontListener;
    GlobalAudioSourceListener globalAudioSourceListener;
    GlobalRenderImageListener globalRenderImageListener;
    GlobalFileListener globalFileListener;

    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);

    commandQueue->setGlobalFileListener(&globalFileListener);
    commandQueue->setGlobalArtboardListener(&globalArtboardListener);
    commandQueue->setGlobalStateMachineListener(&globalStateMachineListener);
    commandQueue->setGlobalRenderImageListener(&globalRenderImageListener);
    commandQueue->setGlobalAudioSourceListener(&globalAudioSourceListener);
    commandQueue->setGlobalViewModelInstanceListener(
        &globalViewModelInstanceListener);
    commandQueue->setGlobalFontListener(&globalFontListener);

    std::ifstream stream("assets/data_bind_test_cmdq.riv", std::ios::binary);
    FileHandle fileHandle = commandQueue->loadFile(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}),
        nullptr,
        1);
    auto artboardHandle = commandQueue->instantiateDefaultArtboard(fileHandle);
    auto stateMachineHandle =
        commandQueue->instantiateDefaultStateMachine(artboardHandle);
    auto viewModel =
        commandQueue->instantiateDefaultViewModelInstance(fileHandle,
                                                          artboardHandle);

    std::ifstream imageStream("assets/batdude.png", std::ios::binary);
    auto renderImage = commandQueue->decodeImage(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(imageStream), {}));

    std::ifstream audioStream("assets/audio/what.wav", std::ios::binary);
    auto audioSource = commandQueue->decodeAudio(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(audioStream), {}));

    std::ifstream fontStream("assets/fonts/OpenSans-Italic.ttf",
                             std::ios::binary);
    auto font = commandQueue->decodeFont(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(fontStream), {}));

    globalFileListener.m_handle = fileHandle;
    globalArtboardListener.m_handle = artboardHandle;
    globalStateMachineListener.m_handle = stateMachineHandle;
    globalViewModelInstanceListener.m_handle = viewModel;
    globalRenderImageListener.m_handle = renderImage;
    globalAudioSourceListener.m_handle = audioSource;
    globalFontListener.m_handle = font;

    // 1 is create file or fileLoaded callback
    commandQueue->requestArtboardNames(fileHandle, 2);
    commandQueue->requestViewModelNames(fileHandle, 3);
    commandQueue->requestViewModelInstanceNames(fileHandle, "Test All", 4);
    commandQueue->requestViewModelPropertyDefinitions(fileHandle,
                                                      "Test All",
                                                      5);
    commandQueue->requestViewModelEnums(fileHandle, 6);
    commandQueue->requestStateMachineNames(artboardHandle, 11);
    commandQueue->requestDefaultViewModelInfo(artboardHandle, fileHandle, 20);
    commandQueue->requestViewModelInstanceBool(viewModel, "Test Bool", 13);
    commandQueue->requestViewModelInstanceListSize(viewModel, "Test List", 14);
    commandQueue->advanceStateMachine(stateMachineHandle, 1, 16);
    commandQueue->advanceStateMachine(stateMachineHandle, 1, 16);
    commandQueue->advanceStateMachine(stateMachineHandle, 1, 16);

    commandQueue->deleteFont(font, 10);
    commandQueue->deleteStateMachine(stateMachineHandle, 17);
    commandQueue->deleteArtboard(artboardHandle, 12);
    commandQueue->deleteViewModelInstance(viewModel, 15);
    commandQueue->deleteImage(renderImage, 8);
    commandQueue->deleteFile(fileHandle, 7);
    commandQueue->deleteAudio(audioSource, 9);

    wait_for_server(commandQueue.get());
    commandQueue->processMessages();

    CHECK_CALLBACK(globalStateMachineListener, onStateMachineDeleted);
    CHECK_CALLBACK(globalStateMachineListener, onStateMachineSettled);

    CHECK_CALLBACK(globalViewModelInstanceListener, onViewModelDeleted);
    CHECK_CALLBACK(globalViewModelInstanceListener, onViewModelDataReceived);
    CHECK_CALLBACK(globalViewModelInstanceListener,
                   onViewModelListSizeReceived);

    CHECK_CALLBACK(globalArtboardListener, onArtboardDeleted);
    CHECK_CALLBACK(globalArtboardListener, onStateMachinesListed);

    CHECK_CALLBACK(globalFileListener, onFileDeleted);
    CHECK_CALLBACK(globalFileListener, onFileLoaded);
    CHECK_CALLBACK(globalFileListener, onArtboardsListed);
    CHECK_CALLBACK(globalFileListener, onViewModelsListed);
    CHECK_CALLBACK(globalFileListener, onViewModelEnumsListed);
    CHECK_CALLBACK(globalFileListener, onViewModelPropertiesListed);
    CHECK_CALLBACK(globalFileListener, onViewModelInstanceNamesListed);

    CHECK_CALLBACK(globalFontListener, onFontDeleted);
    CHECK_CALLBACK(globalAudioSourceListener, onAudioSourceDeleted);
    CHECK_CALLBACK(globalRenderImageListener, onRenderImageDeleted);

    commandQueue->disconnect();
    serverThread.join();
}

// StateMachines depend on Artboards and Artboards depend on Files.
// So if an artboard gets deleted so should the statemachines assosiated with
// it. If a file is deleted so should artboards and therefore statemachines.

TEST_CASE("dependency lifetime management", "[CommandQueue]")
{
    auto commandQueue = make_rcp<CommandQueue>();
    std::thread serverThread(server_thread, commandQueue);

    std::ifstream stream("assets/data_bind_test_cmdq.riv", std::ios::binary);
    FileHandle fileHandle = commandQueue->loadFile(
        std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}),
        nullptr,
        1);

    auto artboardHandle = commandQueue->instantiateDefaultArtboard(fileHandle);
    auto artboardHandle2 = commandQueue->instantiateDefaultArtboard(fileHandle);
    auto artboardHandle3 = commandQueue->instantiateDefaultArtboard(fileHandle);

    auto stateMachine =
        commandQueue->instantiateDefaultStateMachine(artboardHandle);
    auto stateMachine_2 =
        commandQueue->instantiateDefaultStateMachine(artboardHandle);

    auto stateMachine2 =
        commandQueue->instantiateDefaultStateMachine(artboardHandle2);
    auto stateMachine2_2 =
        commandQueue->instantiateDefaultStateMachine(artboardHandle2);

    auto stateMachine3 =
        commandQueue->instantiateDefaultStateMachine(artboardHandle2);
    auto stateMachine3_2 =
        commandQueue->instantiateDefaultStateMachine(artboardHandle2);

    commandQueue->runOnce([&](CommandServer* server) {
        CHECK(server->getFile(fileHandle) != nullptr);
        CHECK(server->getArtboardInstance(artboardHandle) != nullptr);
        CHECK(server->getArtboardInstance(artboardHandle2) != nullptr);
        CHECK(server->getArtboardInstance(artboardHandle3) != nullptr);
        CHECK(server->getStateMachineInstance(stateMachine) != nullptr);
        CHECK(server->getStateMachineInstance(stateMachine_2) != nullptr);
        CHECK(server->getStateMachineInstance(stateMachine2) != nullptr);
        CHECK(server->getStateMachineInstance(stateMachine2_2) != nullptr);
        CHECK(server->getStateMachineInstance(stateMachine3) != nullptr);
        CHECK(server->getStateMachineInstance(stateMachine3_2) != nullptr);
    });

    commandQueue->deleteArtboard(artboardHandle);

    commandQueue->runOnce([&](CommandServer* server) {
        CHECK(server->getFile(fileHandle) != nullptr);
        CHECK(server->getArtboardInstance(artboardHandle) == nullptr);
        CHECK(server->getArtboardInstance(artboardHandle2) != nullptr);
        CHECK(server->getArtboardInstance(artboardHandle3) != nullptr);
        CHECK(server->getStateMachineInstance(stateMachine) == nullptr);
        CHECK(server->getStateMachineInstance(stateMachine_2) == nullptr);
        CHECK(server->getStateMachineInstance(stateMachine2) != nullptr);
        CHECK(server->getStateMachineInstance(stateMachine2_2) != nullptr);
        CHECK(server->getStateMachineInstance(stateMachine3) != nullptr);
        CHECK(server->getStateMachineInstance(stateMachine3_2) != nullptr);
    });

    commandQueue->deleteStateMachine(stateMachine2);

    commandQueue->runOnce([&](CommandServer* server) {
        CHECK(server->getFile(fileHandle) != nullptr);
        CHECK(server->getArtboardInstance(artboardHandle) == nullptr);
        CHECK(server->getArtboardInstance(artboardHandle2) != nullptr);
        CHECK(server->getArtboardInstance(artboardHandle3) != nullptr);
        CHECK(server->getStateMachineInstance(stateMachine) == nullptr);
        CHECK(server->getStateMachineInstance(stateMachine_2) == nullptr);
        CHECK(server->getStateMachineInstance(stateMachine2) == nullptr);
        CHECK(server->getStateMachineInstance(stateMachine2_2) != nullptr);
        CHECK(server->getStateMachineInstance(stateMachine3) != nullptr);
        CHECK(server->getStateMachineInstance(stateMachine3_2) != nullptr);
    });

    commandQueue->disconnect();
    serverThread.join();
}