#include "RadiantTest.h"

#include "ieclass.h"
#include "ientity.h"
#include "irendersystemfactory.h"
#include "iselectable.h"
#include "iselection.h"
#include "ifilesystem.h"
#include "isound.h"
#include "iundo.h"
#include "ishaders.h"
#include "render/RenderableCollectionWalker.h"

#include "render/NopVolumeTest.h"
#include "string/convert.h"
#include "transformlib.h"
#include "registry/registry.h"
#include "scenelib.h"
#include "algorithm/Entity.h"
#include "algorithm/Scene.h"

namespace test
{

using EntityTest = RadiantTest;

namespace
{

// Container for an entity under test. Stores the entity and adds it to the
// global map to enable undo.
struct TestEntity
{
    IEntityNodePtr node;
    Entity* spawnArgs = nullptr;

    // Create an entity with the given class name
    static TestEntity create(const std::string& className)
    {
        TestEntity result;
        result.node = algorithm::createEntityByClassName(className);
        result.spawnArgs = &result.node->getEntity();

        // Enable undo
        scene::addNodeToContainer(result.node, GlobalMapModule().getRoot());

        return result;
    }

    // Access the spawnargs
    Entity& args() { return *spawnArgs; }
};

// Obtain entity attachments as a simple std::list
std::list<Entity::Attachment> getAttachments(const IEntityNodePtr& node)
{
    std::list<Entity::Attachment> attachments;
    if (node)
    {
        node->getEntity().forEachAttachment(
            [&](const Entity::Attachment& a) { attachments.push_back(a); }
        );
    }
    return attachments;
}

}

using StringMap = std::map<std::string, std::string>;

TEST_F(EntityTest, CannotCreateEntityWithoutClass)
{
    // Creating with a null entity class should throw an exception
    EXPECT_THROW(GlobalEntityModule().createEntity({}), std::runtime_error);
}

TEST_F(EntityTest, CreateBasicLightEntity)
{
    // Create a basic light
    auto lightCls = GlobalEntityClassManager().findClass("light");
    auto light = GlobalEntityModule().createEntity(lightCls);

    // Light has a sensible autogenerated name
    EXPECT_EQ(light->name(), "light_1");

    // Entity should have a "classname" key matching the actual entity class we
    // created
    auto clsName = light->getEntity().getKeyValue("classname");
    EXPECT_EQ(clsName, "light");

    // Entity should have an IEntityClass pointer which matches the one we
    // looked up
    EXPECT_EQ(light->getEntity().getEntityClass().get(), lightCls.get());

    // This basic light entity should have no attachments
    auto attachments = getAttachments(light);
    EXPECT_EQ(attachments.size(), 0);
}

TEST_F(EntityTest, EnumerateEntitySpawnargs)
{
    auto light = algorithm::createEntityByClassName("light");
    auto& spawnArgs = light->getEntity();

    // Visit spawnargs by key and value string
    StringMap keyValuesInit;
    spawnArgs.forEachKeyValue([&](const std::string& k, const std::string& v) {
        keyValuesInit.insert({k, v});
    });

    // Initial entity should have a name and a classname value and no other
    // properties
    EXPECT_EQ(keyValuesInit.size(), 2);
    EXPECT_EQ(keyValuesInit["name"], light->name());
    EXPECT_EQ(keyValuesInit["classname"], "light");

    // Add some new properties of our own
    spawnArgs.setKeyValue("origin", "128 256 -1024");
    spawnArgs.setKeyValue("_color", "0.5 0.5 0.5");

    // Ensure that our new properties are also enumerated
    StringMap keyValuesAll;
    spawnArgs.forEachKeyValue([&](const std::string& k, const std::string& v) {
        keyValuesAll.insert({k, v});
    });
    EXPECT_EQ(keyValuesAll.size(), 4);
    EXPECT_EQ(keyValuesAll["origin"], "128 256 -1024");
    EXPECT_EQ(keyValuesAll["_color"], "0.5 0.5 0.5");

    // Enumerate as full EntityKeyValue objects as well as strings
    StringMap keyValuesByObj;
    spawnArgs.forEachEntityKeyValue(
        [&](const std::string& k, const EntityKeyValue& v) {
            keyValuesByObj.insert({k, v.get()});
        }
    );
    EXPECT_EQ(keyValuesAll, keyValuesByObj);
}

TEST_F(EntityTest, EnumerateInheritedSpawnargs)
{
    auto light = algorithm::createEntityByClassName("atdm:light_base");
    auto& spawnArgs = light->getEntity();

    // Enumerate all keyvalues including the inherited ones
    StringMap keyValues;
    spawnArgs.forEachKeyValue(
        [&](const std::string& k, const std::string& v) {
            keyValues.insert({k, v});
        },
        true /* includeInherited */
    );

    // Check we have some inherited properties from the entitydef (including
    // spawnclass from the entitydef's own parent def)
    EXPECT_EQ(keyValues["spawnclass"], "idLight");
    EXPECT_EQ(keyValues["shouldBeOn"], "0");
    EXPECT_EQ(keyValues["AIUse"], "AIUSE_LIGHTSOURCE");
    EXPECT_EQ(keyValues["noshadows"], "0");
}

TEST_F(EntityTest, GetKeyValuePairs)
{
    auto torch = algorithm::createEntityByClassName("atdm:torch_brazier");
    auto& spawnArgs = torch->getEntity();

    using Pair = Entity::KeyValuePairs::value_type;

    // Retrieve single spawnargs as single-element lists of pairs
    auto classNamePairs = spawnArgs.getKeyValuePairs("classname");
    EXPECT_EQ(classNamePairs.size(), 1);
    EXPECT_EQ(classNamePairs[0], Pair("classname", "atdm:torch_brazier"));

    auto namePairs = spawnArgs.getKeyValuePairs("name");
    EXPECT_EQ(namePairs.size(), 1);
    EXPECT_EQ(namePairs[0], Pair("name", "atdm_torch_brazier_1"));

    // Add some spawnargs with a common prefix
    const StringMap SR_KEYS{
        {"sr_type_1", "blah"},
        {"sr_type_2", "bleh"},
        {"sR_tYpE_a", "123"},
        {"SR_type_1a", "0 123 -120"},
    };
    for (const auto& pair: SR_KEYS)
        spawnArgs.setKeyValue(pair.first, pair.second);

    // Confirm all added prefix keys are found regardless of case
    auto srPairs = spawnArgs.getKeyValuePairs("sr_type");
    EXPECT_EQ(srPairs.size(), SR_KEYS.size());
    for (const auto& pair: srPairs)
        EXPECT_EQ(SR_KEYS.at(pair.first), pair.second);
}

TEST_F(EntityTest, CopySpawnargs)
{
    auto light = algorithm::createEntityByClassName("atdm:light_base");
    auto& spawnArgs = light->getEntity();

    // Add some custom spawnargs to copy
    const StringMap EXTRA_SPAWNARGS{{"first", "1"},
                                    {"second", "two"},
                                    {"THIRD", "3333"},
                                    {"_color", "1 0 1"}};

    for (const auto& pair: EXTRA_SPAWNARGS)
        spawnArgs.setKeyValue(pair.first, pair.second);

    // Clone the entity node
    auto lightCopy = light->clone();
    Entity* clonedEnt = Node_getEntity(lightCopy);
    ASSERT_TRUE(clonedEnt);

    // Clone should have all the same spawnarg strings
    std::size_t count = 0;
    clonedEnt->forEachKeyValue([&](const std::string& k, const std::string& v) {
        EXPECT_EQ(spawnArgs.getKeyValue(k), v);
        ++count;
    });
    EXPECT_EQ(count, EXTRA_SPAWNARGS.size() + 2 /* name and classname */);

    // Clone should NOT have the same actual KeyValue object pointers, although
    // the count should be the same
    std::set<EntityKeyValue*> origPointers;
    std::set<EntityKeyValue*> copiedPointers;
    spawnArgs.forEachEntityKeyValue(
        [&](const std::string& k, EntityKeyValue& v) {
            origPointers.insert(&v);
        });
    clonedEnt->forEachEntityKeyValue(
        [&](const std::string& k, EntityKeyValue& v) {
            copiedPointers.insert(&v);
        });
    EXPECT_EQ(origPointers.size(), count);
    EXPECT_EQ(copiedPointers.size(), count);

    std::vector<EntityKeyValue*> overlap;
    std::set_intersection(origPointers.begin(), origPointers.end(),
                          copiedPointers.begin(), copiedPointers.end(),
                          std::back_inserter(overlap));
    EXPECT_EQ(overlap.size(), 0);
}

TEST_F(EntityTest, UndoRedoSpawnargValueChange)
{
    // Create entity with initial default args.
    auto entity = TestEntity::create("bucket_metal");

    // Make a simple value change
    EXPECT_EQ(entity.args().getKeyValue("name"), "bucket_metal_1");
    {
        UndoableCommand cmd("changeKeyValue");
        entity.args().setKeyValue("name", "another_bucket");
    }

    // Confirm we can undo this change
    EXPECT_EQ(entity.args().getKeyValue("name"), "another_bucket");
    GlobalUndoSystem().undo();
    EXPECT_EQ(entity.args().getKeyValue("name"), "bucket_metal_1");

    // Confirm we can redo the change
    GlobalUndoSystem().redo();
    EXPECT_EQ(entity.args().getKeyValue("name"), "another_bucket");
}

TEST_F(EntityTest, SelectEntity)
{
    auto light = algorithm::createEntityByClassName("light");

    // Confirm that setting entity node's selection status propagates to the
    // selection system
    EXPECT_EQ(GlobalSelectionSystem().countSelected(), 0);
    scene::node_cast<ISelectable>(light)->setSelected(true);
    EXPECT_EQ(GlobalSelectionSystem().countSelected(), 1);
    scene::node_cast<ISelectable>(light)->setSelected(false);
    EXPECT_EQ(GlobalSelectionSystem().countSelected(), 0);
}

TEST_F(EntityTest, DestroySelectedEntity)
{
    auto light = algorithm::createEntityByClassName("light");

    // Confirm that setting entity node's selection status propagates to the
    // selection system
    EXPECT_EQ(GlobalSelectionSystem().countSelected(), 0);
    scene::node_cast<ISelectable>(light)->setSelected(true);
    EXPECT_EQ(GlobalSelectionSystem().countSelected(), 1);

    // Destructor called here and should not crash
}

namespace
{
    // A simple RenderableCollector which just logs/stores whatever is submitted
    struct TestRenderableCollector :
        public render::RenderableCollectorBase
    {
        TestRenderableCollector(bool solid) :
            renderSolid(solid)
        {}

        bool renderSolid;

        // Count of submitted and processed renderables, and lights
        int processedNodes = 0;
        int highlightRenderables = 0;

        std::vector<const OpenGLRenderable*> highlightRenderablePtrs;

        void processNode(const scene::INodePtr& node, const VolumeTest& volume) override
        {
            RenderableCollectorBase::processNode(node, volume);
            ++processedNodes;
        }

        void addHighlightRenderable(const OpenGLRenderable& renderable,
            const Matrix4& localToWorld) override
        {
            ++highlightRenderables;
            highlightRenderablePtrs.push_back(&renderable);
        }

        bool supportsFullMaterials() const override { return true; }
    };

    // Collection of objects needed for rendering. Since not all tests require
    // rendering, these objects are in an auxiliary fixture created when needed
    // rather than part of the EntityTest fixture used by every test. This class
    // also implements scene::NodeVisitor enabling it to visit trees of nodes
    // for rendering.
    struct RenderFixture: public scene::NodeVisitor
    {
        RenderSystemPtr backend = GlobalRenderSystemFactory().createRenderSystem();
        render::NopVolumeTest volumeTest;
        TestRenderableCollector collector;

        // Keep track of nodes visited
        int nodesVisited = 0;

        // Construct
        RenderFixture(bool solid = false) :
            collector(solid)
        {}

        // Convenience method to set render backend and traverse a node and its
        // children for rendering
        void renderSubGraph(const scene::INodePtr& node)
        {
            node->setRenderSystem(backend);
            node->traverse(*this);
        }

        // NodeVisitor implementation
        bool pre(const scene::INodePtr& node) override
        {
            // Count the node itself
            ++nodesVisited;

            // Render the node in appropriate mode
            node->onPreRender(volumeTest);

            // Continue traversing
            return true;
        }
    };
}

TEST_F(EntityTest, LightLocalToWorldFromOrigin)
{
    auto light = algorithm::createEntityByClassName("light");

    // Initial localToWorld should be identity
    EXPECT_EQ(light->localToWorld(), Matrix4::getIdentity());

    // Set an origin
    const Vector3 ORIGIN(123, 456, -10);
    light->getEntity().setKeyValue("origin", string::to_string(ORIGIN));

    // localToParent should reflect the new origin
    auto transformNode = std::dynamic_pointer_cast<ITransformNode>(light);
    ASSERT_TRUE(transformNode);
    EXPECT_EQ(transformNode->localToParent(), Matrix4::getTranslation(ORIGIN));

    // Since there is no parent, the final localToWorld should be the same as
    // localToParent
    EXPECT_EQ(light->localToWorld(), Matrix4::getTranslation(ORIGIN));
}

TEST_F(EntityTest, LightWireframeShader)
{
    auto light = algorithm::createEntityByClassName("light");

    // Initially there is no shader because there is no rendersystem
    auto wireSh = light->getWireShader();
    EXPECT_FALSE(wireSh);

    // Set a render system
    RenderSystemPtr backend = GlobalRenderSystemFactory().createRenderSystem();
    light->setRenderSystem(backend);

    // There should be a shader now
    auto newWireSh = light->getWireShader();
    ASSERT_TRUE(newWireSh);

    // Get the name for the shader. Since this is a simple built-in wireframe
    // shader, this should be an internally-constructed name based on the entity
    // colour. Note that this colour is derived from the entity *class*, which
    // for "light" is a default green. Actual lights will be rendered with a
    // colour based on their _color key.
    EXPECT_EQ(newWireSh->getName(), "<0.000000 1.000000 0.000000>");
}

// Disabled test, since the Shader implementation currently offers no public interface
// to enumerate or inspect the submitted geometry - needs more thought
#if 0
TEST_F(EntityTest, LightVolumeColorFromColorKey)
{
    // Create a default light
    auto light = algorithm::createEntityByClassName("light");

    {
        // Render the default light
        RenderFixture rf;
        rf.renderSubGraph(light);

        // Shader should have been submitted. Since a light's default _color is
        // white, this is the shader we should get for rendering.
        EXPECT_EQ(rf.collector.renderables, 1);
        const Shader* shader = rf.collector.renderablePtrs.at(0).first;
        ASSERT_TRUE(shader);
        EXPECT_EQ(shader->getName(), "<1.000000 1.000000 1.000000>");
    }

    // Set a different colour on the light
    light->getEntity().setKeyValue("_color", "0.75 0.25 0.1");

    {
        // Re-render the light
        RenderFixture rf;
        rf.renderSubGraph(light);

        // The shader should have changed to match the new _color
        EXPECT_EQ(rf.collector.renderables, 1);
        const Shader* shader = rf.collector.renderablePtrs.at(0).first;
        ASSERT_TRUE(shader);
        EXPECT_EQ(shader->getName(), "<0.750000 0.250000 0.100000>");
    }
}

TEST_F(EntityTest, OverrideLightVolumeColour)
{
    // Create a light with an arbitrary colour
    auto light = algorithm::createEntityByClassName("light");
    light->getEntity().setKeyValue("_color", "0.25 0.55 0.9");

    // Set the "override light volume colour" key
    registry::setValue(colours::RKEY_OVERRIDE_LIGHTCOL, true);

    {
        RenderFixture rf;
        rf.renderSubGraph(light);

        // The shader should ignore the _color key and render based on the entity
        // class colour
        EXPECT_EQ(rf.collector.renderables, 1);
        const Shader* shader = rf.collector.renderablePtrs.at(0).first;
        ASSERT_TRUE(shader);
        EXPECT_EQ(shader->getName(), "<0.000000 1.000000 0.000000>");
    }

    // Unset the override key
    registry::setValue(colours::RKEY_OVERRIDE_LIGHTCOL, false);

    {
        RenderFixture rf;
        rf.renderSubGraph(light);

        // Light should be rendered with its original _color key again
        EXPECT_EQ(rf.collector.renderables, 1);
        const Shader* shader = rf.collector.renderablePtrs.at(0).first;
        ASSERT_TRUE(shader);
        EXPECT_EQ(shader->getName(), "<0.250000 0.550000 0.900000>");
    }

    // Changing the override key after deleting the light must not crash
    // (because the LightNode's CachedKey is sigc::trackable)
    light.reset();
    registry::setValue(colours::RKEY_OVERRIDE_LIGHTCOL, true);
    registry::setValue(colours::RKEY_OVERRIDE_LIGHTCOL, false);
}
#endif

TEST_F(EntityTest, FuncStaticLocalToWorld)
{
    auto funcStatic = algorithm::createEntityByClassName("func_static");
    funcStatic->getEntity().setKeyValue("model", "models/torch.lwo");
    auto& spawnArgs = funcStatic->getEntity();
    spawnArgs.setKeyValue("origin", "0 0 0");

    // Initial localToWorld should be an identity matrix
    EXPECT_EQ(funcStatic->localToWorld(), Matrix4::getIdentity());

    // Set a new origin and make sure the localToWorld reflects the
    // corresponding translation
    const Vector3 MOVED(46, -128, 4096);
    spawnArgs.setKeyValue("origin", string::to_string(MOVED));
    EXPECT_EQ(funcStatic->localToWorld(),
              Matrix4::getTranslation(MOVED));

    // Clear transformation and get back to identity
    spawnArgs.setKeyValue("origin", "0 0 0");
    EXPECT_EQ(funcStatic->localToWorld(), Matrix4::getIdentity());
}

TEST_F(EntityTest, TranslateFuncStatic)
{
    auto torch = TestEntity::create("func_static");
    torch.args().setKeyValue("origin", "0 0 0");
    torch.args().setKeyValue("model", "models/torch.lwo");

    // Set translation via the ITransformable interface
    auto transformable = scene::node_cast<ITransformable>(torch.node);
    ASSERT_TRUE(transformable);
    transformable->setTranslation(Vector3(128, 56, -64));

    // Translation does not appear in origin spawnarg until frozen
    EXPECT_EQ(torch.args().getKeyValue("origin"), "0 0 0");
    transformable->freezeTransform();
    EXPECT_EQ(torch.args().getKeyValue("origin"), "128 56 -64");
}

TEST_F(EntityTest, RotateFuncStatic)
{
    auto torch = TestEntity::create("func_static");
    torch.args().setKeyValue("origin", "0 0 0");
    torch.args().setKeyValue("model", "models/torch.lwo");

    // Set rotation via the ITransformable interface
    auto transformable = scene::node_cast<ITransformable>(torch.node);
    ASSERT_TRUE(transformable);
    transformable->setRotation(Quaternion::createForEulerXYZDegrees(Vector3(0, 0, 45)));

    // Should not appear in spawnargs until frozen
    EXPECT_EQ(torch.args().getKeyValue("rotation"), "");
    transformable->freezeTransform();
    EXPECT_EQ(torch.args().getKeyValue("rotation"),
              "0.707107 0.707107 0 -0.707107 0.707107 0 0 0 1");

    // Applying the transform should be idempotent
    transformable->freezeTransform();
    EXPECT_EQ(torch.args().getKeyValue("rotation"),
              "0.707107 0.707107 0 -0.707107 0.707107 0 0 0 1");

    // Rotation does not change origin
    EXPECT_EQ(torch.args().getKeyValue("origin"), "0 0 0");
}

TEST_F(EntityTest, RotateLight)
{
    auto light = TestEntity::create("light");
    light.args().setKeyValue("origin", "0 0 0");

    // Rotate the light via ITransformable
    auto transformable = scene::node_cast<ITransformable>(light.node);
    ASSERT_TRUE(transformable);
    transformable->setRotation(Quaternion::createForEulerXYZDegrees(Vector3(0, 0, 75)));

    // Rotation appears after freezing transform
    EXPECT_EQ(light.args().getKeyValue("rotation"), "");
    transformable->freezeTransform();
    EXPECT_EQ(light.args().getKeyValue("rotation"),
              "0.258819 0.965926 0 -0.965926 0.258819 0 0 0 1");
}

TEST_F(EntityTest, TranslateFuncStaticAfterRotation)
{
    auto torch = TestEntity::create("func_static");
    torch.args().setKeyValue("origin", "0 0 0");
    torch.args().setKeyValue("model", "models/torch.lwo");

    // Set rotation via the ITransformable interface and freeze the transform
    auto transformable = scene::node_cast<ITransformable>(torch.node);
    ASSERT_TRUE(transformable);
    transformable->setRotation(Quaternion::createForEulerXYZDegrees(Vector3(0, 0, 90)));
    transformable->freezeTransform();
    EXPECT_EQ(torch.args().getKeyValue("rotation"), "0 1 0 -1 0 0 0 0 1");

    // Now add a translation
    transformable->setTranslation(Vector3(-1200, 45, 962));
    transformable->freezeTransform();
    EXPECT_EQ(torch.args().getKeyValue("origin"), "-1200 45 962");

    // Rotation must not have changed
    EXPECT_EQ(torch.args().getKeyValue("rotation"), "0 1 0 -1 0 0 0 0 1");
}

TEST_F(EntityTest, TranslateLightAfterRotation)
{
    auto light = TestEntity::create("light");
    light.args().setKeyValue("origin", "0 0 0");

    // Set rotation via the ITransformable interface and freeze the transform
    auto transformable = scene::node_cast<ITransformable>(light.node);
    ASSERT_TRUE(transformable);
    transformable->setRotation(Quaternion::createForEulerXYZDegrees(Vector3(0, 0, 90)));
    transformable->freezeTransform();
    EXPECT_EQ(light.args().getKeyValue("rotation"), "0 1 0 -1 0 0 0 0 1");

    // Now add a translation
    transformable->setTranslation(Vector3(565.25, -450, 35.2));
    transformable->freezeTransform();
    EXPECT_EQ(light.args().getKeyValue("origin"), "565.25 -450 35.2");

    // Rotation must not have changed
    EXPECT_EQ(light.args().getKeyValue("rotation"), "0 1 0 -1 0 0 0 0 1");
}

namespace detail
{

// Returns the first render entity registered in the rendersystem, matching the given predicate
inline IRenderEntityPtr getFirstRenderEntity(std::function<bool(IRenderEntityPtr)> predicate)
{
    IRenderEntityPtr result;

    auto renderSystem = GlobalMapModule().getRoot()->getRenderSystem();
    renderSystem->foreachEntity([&](const IRenderEntityPtr& entity)
    {
        if (!result && predicate(entity))
        {
            result = entity;
        }
    });

    return result;
}

inline std::set<RendererLightPtr> getAllRenderLights()
{
    std::set<RendererLightPtr> result;

    auto renderSystem = GlobalMapModule().getRoot()->getRenderSystem();
    renderSystem->foreachLight([&](const RendererLightPtr& light)
    {
        result.insert(light);
    });

    return result;
}

inline std::set<render::IRenderableObject::Ptr> getAllObjects(IRenderEntityPtr entity)
{
    std::set<render::IRenderableObject::Ptr> result;

    AABB hugeBounds({ 0,0,0 }, { 65536, 65536, 65536 });

    entity->foreachRenderableTouchingBounds(hugeBounds, [&](const render::IRenderableObject::Ptr& object, Shader*)
    {
        result.insert(object);
    });

    return result;
}

}

TEST_F(EntityTest, ForeachAttachment)
{
    // Insert a static entity with an attached light to the scene
    auto torch = algorithm::createEntityByClassName("atdm:torch_brazier");
    scene::addNodeToContainer(torch, GlobalMapModule().getRoot());

    int attachmentCount = 0;
    torch->foreachAttachment([&](const IEntityNodePtr& attachment)
    {
        attachmentCount++;
        EXPECT_TRUE(attachment->getEntity().isOfType("light_cageflame_small"));
    });

    EXPECT_EQ(attachmentCount, 1) << "No attachment found on entity " << torch->name();
}

TEST_F(EntityTest, LightTransformedByParent)
{
    // Parent a light to another entity (this isn't currently how the attachment
    // system is implemented, but it should validate that a light node can
    // inherit the transformation of its parent).
    auto light = algorithm::createEntityByClassName("light");
    auto parentModel = algorithm::createEntityByClassName("func_static");
    parentModel->getEntity().setKeyValue("model", "models/torch.lwo");
    scene::addNodeToContainer(light, parentModel);
    scene::addNodeToContainer(parentModel, GlobalMapModule().getRoot());

    // Parenting should automatically set the parent pointer of the child
    EXPECT_EQ(light->getParent(), parentModel);

    // Set an offset for the parent model
    const Vector3 ORIGIN(1024, 512, -320);
    parentModel->getEntity().setKeyValue("origin", string::to_string(ORIGIN));

    // Parent entity should have a transform matrix corresponding to its
    // translation
    EXPECT_EQ(parentModel->localToWorld(), Matrix4::getTranslation(ORIGIN));

    // The light itself should have the same transformation as the parent (since
    // the method is localToWorld not localToParent).
    EXPECT_EQ(light->localToWorld(), Matrix4::getTranslation(ORIGIN));

    // Get the first light in the render system
    auto lights = detail::getAllRenderLights();
    EXPECT_EQ(lights.size(), 1) << "Expected 1 light registered in the render system";

    auto rLight = *lights.begin();

    EXPECT_EQ(rLight->getLightOrigin(), ORIGIN);
    EXPECT_EQ(rLight->lightAABB().origin, ORIGIN);
    EXPECT_EQ(rLight->lightAABB().extents, Vector3(320, 320, 320));
}

TEST_F(EntityTest, RenderUnselectedLightEntity)
{
    RenderFixture fixture;

    auto light = algorithm::createEntityByClassName("light");
    scene::addNodeToContainer(light, GlobalMapModule().getRoot());

    // Run the front-end collector through the scene
    render::RenderableCollectionWalker::CollectRenderablesInScene(fixture.collector, fixture.volumeTest);

    // Only the light origin diamond should be rendered
    EXPECT_EQ(fixture.collector.highlightRenderables, 0);
}

TEST_F(EntityTest, RenderSelectedLightEntity)
{
    RenderFixture fixture;

    auto light = algorithm::createEntityByClassName("light");
    scene::addNodeToContainer(light, GlobalMapModule().getRoot());

    // Select the light then render it
    Node_setSelected(light, true);

    // Run the front-end collector through the scene
    render::RenderableCollectionWalker::CollectRenderablesInScene(fixture.collector, fixture.volumeTest);

    // With the light selected, we should get the origin diamond, the radius and
    // the center vertex.
    EXPECT_EQ(fixture.collector.highlightRenderables, 2);
}

TEST_F(EntityTest, RenderLightProperties)
{
    auto light = algorithm::createEntityByClassName("light_torchflame_small");
    scene::addNodeToContainer(light, GlobalMapModule().getRoot());

    auto& spawnArgs = light->getEntity();

    // Set a non-default origin for the light
    static const Vector3 ORIGIN(-64, 128, 963);
    spawnArgs.setKeyValue("origin", string::to_string(ORIGIN));

    RenderFixture fixture;

    // Run the front-end collector through the scene
    render::RenderableCollectionWalker::CollectRenderablesInScene(fixture.collector, fixture.volumeTest);

    // Confirm properties of the registered RendererLight
    auto lights = detail::getAllRenderLights();
    EXPECT_EQ(lights.size(), 1) << "Expected 1 light registered in the render system";

    auto rLight = *lights.begin();
    EXPECT_EQ(rLight->getLightOrigin(), ORIGIN);
    EXPECT_EQ(rLight->lightAABB().origin, ORIGIN);
    // Default light properties from the entitydef
    EXPECT_EQ(rLight->lightAABB().extents, Vector3(240, 240, 240));
    ASSERT_TRUE(rLight->getShader() && rLight->getShader()->getMaterial());
    EXPECT_EQ(rLight->getShader()->getMaterial()->getName(),
              "lights/biground_torchflicker");
}

TEST_F(EntityTest, RenderEmptyFuncStatic)
{
    auto funcStatic = algorithm::createEntityByClassName("func_static");

    // Func static without a model key is empty
    RenderFixture rf;
    rf.renderSubGraph(funcStatic);
    EXPECT_EQ(rf.nodesVisited, 1);
}

TEST_F(EntityTest, RenderFuncStaticWithModel)
{
    // Create a func_static with a model key
    auto funcStatic = algorithm::createEntityByClassName("func_static");
    funcStatic->getEntity().setKeyValue("model", "models/moss_patch.ase");
    scene::addNodeToContainer(funcStatic, GlobalMapModule().getRoot());

    // Run the front-end collector through the scene
    RenderFixture fixture;
    render::RenderableCollectionWalker::CollectRenderablesInScene(fixture.collector, fixture.volumeTest);

    // The entity node itself does not render the model; it is a parent node
    // with the model as a child (e.g. as a StaticModelNode). Therefore we
    // should have called onPreRender on three nodes in total: the root, the entity and its model child.
    EXPECT_EQ(fixture.collector.processedNodes, 3);

    // Get the first render entity
    auto entity = detail::getFirstRenderEntity([&](IRenderEntityPtr candidate)
    {
        return candidate == funcStatic;
    });
    EXPECT_TRUE(entity);

    // Check the renderables attached to this entity
    auto objects = detail::getAllObjects(entity);
    EXPECT_EQ(objects.size(), 1) << "Expected one renderable object attached to the func_static";
}

TEST_F(EntityTest, RenderFuncStaticWithMultiSurfaceModel)
{
    // Create a func_static with a model key
    auto funcStatic = algorithm::createEntityByClassName("func_static");
    funcStatic->getEntity().setKeyValue("model", "models/torch.lwo");
    scene::addNodeToContainer(funcStatic, GlobalMapModule().getRoot());

    // Run the front-end collector through the scene
    RenderFixture fixture;
    render::RenderableCollectionWalker::CollectRenderablesInScene(fixture.collector, fixture.volumeTest);

    // Get the first render entity
    auto entity = detail::getFirstRenderEntity([&](IRenderEntityPtr candidate)
    {
        return candidate == funcStatic;
    });
    EXPECT_TRUE(entity);

    // Check the renderables attached to this entity
    auto objects = detail::getAllObjects(entity);

    // This torch model has 3 renderable surfaces
    EXPECT_EQ(objects.size(), 3) << "Expected one renderable object attached to the func_static";
}

TEST_F(EntityTest, EntityNodeRGBShaderParms)
{
    auto funcStatic = TestEntity::create("func_static");

    // Parms 0-3 represent the colour (RGBA)
    EXPECT_EQ(funcStatic.node->getShaderParm(0), 1.0f);
    EXPECT_EQ(funcStatic.node->getShaderParm(1), 1.0f);
    EXPECT_EQ(funcStatic.node->getShaderParm(2), 1.0f);
    EXPECT_EQ(funcStatic.node->getShaderParm(3), 1.0f);

    // Change the colour and observe the new shader parms
    funcStatic.args().setKeyValue("_color", "0.25 0.3 0.75");
    EXPECT_EQ(funcStatic.node->getShaderParm(0), 0.25f);
    EXPECT_EQ(funcStatic.node->getShaderParm(1), 0.3f);
    EXPECT_EQ(funcStatic.node->getShaderParm(2), 0.75f);
    EXPECT_EQ(funcStatic.node->getShaderParm(3), 1.0f);
}

TEST_F(EntityTest, EntityNodeGenericShaderParms)
{
    auto torch = TestEntity::create("atdm:torch_brazier");

    // Initial params should be 0
    EXPECT_EQ(torch.node->getShaderParm(4), 0.0f);
    EXPECT_EQ(torch.node->getShaderParm(5), 0.0f);
    EXPECT_EQ(torch.node->getShaderParm(8), 0.0f);

    // Set some values
    torch.args().setKeyValue("shaderParm4", "127");
    torch.args().setKeyValue("shaderParm5", "-0.5");
    torch.args().setKeyValue("shaderParm8", "10245");

    // Values should be reflected in shader parm floats
    EXPECT_EQ(torch.node->getShaderParm(4), 127.0f);
    EXPECT_EQ(torch.node->getShaderParm(5), -0.5f);
    EXPECT_EQ(torch.node->getShaderParm(8), 10245.0f);

    // Remove the spawnargs again
    torch.args().setKeyValue("shaderParm4", "");
    torch.args().setKeyValue("shaderParm5", "");
    torch.args().setKeyValue("shaderParm8", "");

    // Params should revert to their initial state
    EXPECT_EQ(torch.node->getShaderParm(4), 0.0f);
    EXPECT_EQ(torch.node->getShaderParm(5), 0.0f);
    EXPECT_EQ(torch.node->getShaderParm(8), 0.0f);
}

TEST_F(EntityTest, CreateAttachedLightEntity)
{
    // Create the torch entity which has an attached light
    auto torch = algorithm::createEntityByClassName("atdm:torch_brazier");
    ASSERT_TRUE(torch);

    // Check that the attachment spawnargs are present
    const Entity& spawnArgs = torch->getEntity();
    EXPECT_EQ(spawnArgs.getKeyValue("def_attach"), "light_cageflame_small");
    EXPECT_EQ(spawnArgs.getKeyValue("pos_attach"), "flame");
    EXPECT_EQ(spawnArgs.getKeyValue("name_attach"), "flame");

    // Spawnargs should be parsed into a single attachment
    auto attachments = getAttachments(torch);
    EXPECT_EQ(attachments.size(), 1);

    // Examine the properties of the single attachment
    Entity::Attachment attachment = attachments.front();
    EXPECT_EQ(attachment.eclass, spawnArgs.getKeyValue("def_attach"));
    EXPECT_EQ(attachment.offset, Vector3(0, 0, 10));
    EXPECT_EQ(attachment.name, spawnArgs.getKeyValue("name_attach"));
}

TEST_F(EntityTest, RenderAttachedLightEntity)
{
    auto torch = TestEntity::create("atdm:torch_brazier");

    // Confirm that def has the right model
    EXPECT_EQ(torch.args().getKeyValue("model"), "models/torch.lwo");

    RenderFixture fixture;
    render::RenderableCollectionWalker::CollectRenderablesInScene(fixture.collector, fixture.volumeTest);

    EXPECT_EQ(fixture.collector.processedNodes, 3);

    auto lights = detail::getAllRenderLights();
    EXPECT_EQ(lights.size(), 1) << "Attached light not registered";

    // The submitted light should be fully realised with a light shader
    auto rLight = *lights.begin();
    ASSERT_TRUE(rLight);
    EXPECT_TRUE(rLight->getShader());
}

TEST_F(EntityTest, AttachedLightAtCorrectPosition)
{
    const Vector3 ORIGIN(256, -128, 635);
    const Vector3 EXPECTED_OFFSET(0, 0, 10); // attach offset in def

    // Create a torch node and set a non-zero origin
    auto torch = algorithm::createEntityByClassName("atdm:torch_brazier");
    torch->getEntity().setKeyValue("origin", string::to_string(ORIGIN));
    scene::addNodeToContainer(torch, GlobalMapModule().getRoot());

    // Render the torch
    RenderFixture fixture;
    render::RenderableCollectionWalker::CollectRenderablesInScene(fixture.collector, fixture.volumeTest);

    auto lights = detail::getAllRenderLights();
    EXPECT_EQ(lights.size(), 1) << "Attached light not registered";
    auto rLight = *lights.begin();

    // Check the light source's position
    EXPECT_EQ(rLight->getLightOrigin(), ORIGIN + EXPECTED_OFFSET);
    EXPECT_EQ(rLight->lightAABB().origin, ORIGIN + EXPECTED_OFFSET);
}

TEST_F(EntityTest, ReloadDefsDoesNotChangeAttachPos)
{
    const Vector3 ORIGIN(-10, 25, 320);
    const Vector3 EXPECTED_OFFSET(0, 0, 10);

    // Create a torch node at the origin
    auto torch = TestEntity::create("atdm:torch_brazier");
    torch.args().setKeyValue("origin", string::to_string(ORIGIN));

    // Reload all entity defs
    GlobalEntityClassManager().reloadDefs();

    // Render the torch
    RenderFixture fixture;
    render::RenderableCollectionWalker::CollectRenderablesInScene(fixture.collector, fixture.volumeTest);

    auto lights = detail::getAllRenderLights();
    EXPECT_EQ(lights.size(), 1) << "Attached light not registered";
    auto rLight = *lights.begin();

    // The light source should still have the expected offset
    EXPECT_EQ(rLight->getLightOrigin(), ORIGIN + EXPECTED_OFFSET);
    EXPECT_EQ(rLight->lightAABB().origin, ORIGIN + EXPECTED_OFFSET);
}

TEST_F(EntityTest, AttachedLightMovesWithEntity)
{
    const Vector3 ORIGIN(12, -0.5, 512);
    const Vector3 EXPECTED_OFFSET(0, 0, 10); // attach offset in def

    // Create a torch node and set a non-zero origin
    auto torch = algorithm::createEntityByClassName("atdm:torch_brazier");
    torch->getEntity().setKeyValue("origin", string::to_string(ORIGIN));
    scene::addNodeToContainer(torch, GlobalMapModule().getRoot());

    // First render
    {
        RenderFixture fixture;
        render::RenderableCollectionWalker::CollectRenderablesInScene(fixture.collector, fixture.volumeTest);
    }

    // Move the torch
    const Vector3 NEW_ORIGIN = ORIGIN + Vector3(128, 512, -54);
    torch->getEntity().setKeyValue("origin", string::to_string(NEW_ORIGIN));

    // Render again to get positions
    RenderFixture fixture;
    render::RenderableCollectionWalker::CollectRenderablesInScene(fixture.collector, fixture.volumeTest);

    // Access the submitted light source
    auto lights = detail::getAllRenderLights();
    EXPECT_EQ(lights.size(), 1) << "Attached light not registered";
    auto rLight = *lights.begin();

    // Check the light source's position
    EXPECT_EQ(rLight->getLightOrigin(), NEW_ORIGIN + EXPECTED_OFFSET);
    EXPECT_EQ(rLight->lightAABB().origin, NEW_ORIGIN + EXPECTED_OFFSET);
}

TEST_F(EntityTest, CreateAIEntity)
{
    auto guard = algorithm::createEntityByClassName("atdm:ai_builder_guard");
    ASSERT_TRUE(guard);

    // Guard should have a hammer attachment
    auto attachments = getAttachments(guard);
    EXPECT_EQ(attachments.size(), 1);
    EXPECT_EQ(attachments.front().eclass, "atdm:moveable_warhammer");
    EXPECT_EQ(attachments.front().offset, Vector3(14, -6, -6));
    EXPECT_EQ(attachments.front().joint, "Spine2");
}

namespace
{

class TestEntityObserver final :
    public Entity::Observer
{
public:
    bool insertFired;
    bool changeFired;
    bool eraseFired;

    std::vector<std::pair<std::string, std::string>> insertStack;
    std::vector<std::pair<std::string, std::string>> changeStack;
    std::vector<std::pair<std::string, std::string>> eraseStack;

    TestEntityObserver()
    {
        reset();
    }

    void reset()
    {
        insertFired = false;
        insertStack.clear();
        changeFired = false;
        changeStack.clear();
        eraseFired = false;
        eraseStack.clear();
    }

    void onKeyInsert(const std::string& key, EntityKeyValue& value) override
    {
        insertFired = true;
        insertStack.emplace_back(key, value.get());
    }

    void onKeyChange(const std::string& key, const std::string& value) override
    {
        changeFired = true;
        changeStack.emplace_back(key, value);
    }

    void onKeyErase(const std::string& key, EntityKeyValue& value) override
    {
        eraseFired = true;
        eraseStack.emplace_back(key, value.get());
    }
};

inline bool stackHasKeyValuePair(const std::vector<std::pair<std::string, std::string>>& stack,
    const std::string& key, const std::string& value)
{
    auto it = std::find(stack.begin(), stack.end(), std::make_pair(key, value));
    return it != stack.end();
}

inline bool stackHasKey(const std::vector<std::pair<std::string, std::string>>& stack,
    const std::string& key)
{
    for (const auto& pair : stack)
    {
        if (pair.first == key) return true;
    }

    return false;
}

// Test observer which keeps track of invocations and last received value
class TestKeyObserver: public KeyObserver
{
public:
    int invocationCount = 0;
    std::string receivedValue;

    TestKeyObserver()
    {
        reset();
    }

    void reset()
    {
        invocationCount = 0;
        receivedValue.clear();
    }

    // KeyObserver implementation
    void onKeyValueChanged(const std::string& newValue) override
    {
        ++invocationCount;
        receivedValue = newValue;
    }

    // Return true if the observer has been invoked at least once
    bool hasBeenInvoked() const { return invocationCount > 0; }
};

inline EntityKeyValue* findKeyValue(Entity* entity, const std::string& keyToFind)
{
    EntityKeyValue* keyValue = nullptr;

    entity->forEachEntityKeyValue([&](const std::string& key, EntityKeyValue& value)
    {
        if (!keyValue && key == keyToFind)
        {
            keyValue = &value;
        }
    });

    return keyValue;
}

inline void expectKeyValuesAreEquivalent(const std::vector<std::pair<std::string, std::string>>& stack1,
    const std::vector<std::pair<std::string, std::string>>& stack2)
{
    EXPECT_EQ(stack1.size(), stack2.size()) << "Stack1 differs from Stack 2 in size";

    for (const auto& pair : stack1)
    {
        EXPECT_TRUE(stackHasKeyValuePair(stack2, pair.first, pair.second)) <<
            "Stack 2 was missing the key value pair " << pair.first << " = " << pair.second;
    }
}

}

TEST_F(EntityTest, EntityObserverAttachDetach)
{
    auto guardNode = algorithm::createEntityByClassName("atdm:ai_builder_guard");
    auto guard = Node_getEntity(guardNode);

    TestEntityObserver observer;

    // Collect all existing key values of this entity
    auto existingKeyValues = algorithm::getAllKeyValuePairs(guard);
    EXPECT_FALSE(existingKeyValues.empty()) << "Entity doesn't have any keys";

    // On attachment, the observer gets notified about all existing keys (insert)
    guard->attachObserver(&observer);

    EXPECT_EQ(observer.insertStack.size(), existingKeyValues.size()) << "Observer didn't get notified about all keys";

    for (const auto& pair : existingKeyValues)
    {
        EXPECT_TRUE(stackHasKeyValuePair(observer.insertStack, pair.first, pair.second)) <<
            "Insert stack doesn't have the expected kv " << pair.first << " = " << pair.second;
    }

    // Everything else should be silent
    EXPECT_TRUE(observer.changeStack.empty()) << "Change stack should be clean";
    EXPECT_TRUE(observer.eraseStack.empty()) << "Erase stack should be clean";

    observer.reset();

    // On detaching the observer receives an erase call for each key value pair
    guard->detachObserver(&observer);

    EXPECT_EQ(observer.eraseStack.size(), existingKeyValues.size()) << "Observer didn't get notified about all keys";

    for (const auto& pair : existingKeyValues)
    {
        EXPECT_TRUE(stackHasKeyValuePair(observer.eraseStack, pair.first, pair.second)) <<
            "Erase stack doesn't have the expected kv " << pair.first << " = " << pair.second;
    }

    // Everything else should be silent
    EXPECT_TRUE(observer.insertStack.empty()) << "Insert stack should be clean";
    EXPECT_TRUE(observer.changeStack.empty()) << "Change stack should be clean";
}

TEST_F(EntityTest, EntityObserverKeyAddition)
{
    auto guardNode = algorithm::createEntityByClassName("atdm:ai_builder_guard");
    auto guard = Node_getEntity(guardNode);

    TestEntityObserver observer;

    // Attach and reset the observer
    guard->attachObserver(&observer);
    observer.reset();

    constexpr const char* key = "New_Unique_Key";
    constexpr const char* value = "New_Unique_Value";
    guard->setKeyValue(key, value);

    // Assert on the new key
    EXPECT_EQ(observer.insertStack.size(), 1) << "Observer didn't get notified about the new key";
    EXPECT_TRUE(stackHasKeyValuePair(observer.insertStack, key, value)) <<
            "Insert stack doesn't have the expected kv " << key << " = " << value;

    // Everything else should be silent
    EXPECT_TRUE(observer.changeStack.empty()) << "Change stack should be clean";
    EXPECT_TRUE(observer.eraseStack.empty()) << "Erase stack should be clean";

    observer.reset();

    // On detaching the observer should receive a corresponding erase for the new key
    guard->detachObserver(&observer);

    EXPECT_TRUE(stackHasKeyValuePair(observer.eraseStack, key, value)) <<
        "Insert stack doesn't have the expected kv " << key << " = " << value;
}

TEST_F(EntityTest, EntityObserverKeyRemoval)
{
    auto guardNode = algorithm::createEntityByClassName("atdm:ai_builder_guard");
    auto guard = Node_getEntity(guardNode);

    TestEntityObserver observer;

    constexpr const char* key = "New_Unique_Key";
    constexpr const char* value = "New_Unique_Value";
    guard->setKeyValue(key, value);

    // Attach and reset the observer
    guard->attachObserver(&observer);
    observer.reset();

    // Remove the key
    guard->setKeyValue(key, "");

    // Assert on the event that should have been received
    EXPECT_EQ(observer.eraseStack.size(), 1) << "Observer didn't get notified about the removed key";
    EXPECT_TRUE(stackHasKeyValuePair(observer.eraseStack, key, value)) <<
        "Erase stack doesn't have the expected kv " << key << " = " << value;

    // Everything else should be silent
    EXPECT_TRUE(observer.changeStack.empty()) << "Change stack should be clean";
    EXPECT_TRUE(observer.insertStack.empty()) << "Insert stack should be clean";

    observer.reset();

    // On detaching the observer should not receive a corresponding erase for the already removed key
    guard->detachObserver(&observer);

    EXPECT_FALSE(stackHasKeyValuePair(observer.eraseStack, key, value)) <<
        "Erase stack unexpectedly contained the kv " << key << " = " << value;
}

TEST_F(EntityTest, EntityObserverKeyChange)
{
    auto guardNode = algorithm::createEntityByClassName("atdm:ai_builder_guard");
    auto guard = Node_getEntity(guardNode);

    TestEntityObserver observer;

    // Attach and reset the observer
    guard->attachObserver(&observer);
    observer.reset();

    constexpr const char* NameKey = "name";
    constexpr const char* NewName = "Ignazius";

    EXPECT_FALSE(guard->getKeyValue(NameKey).empty()) << "Key " << NameKey << " must exist for this test";

    guard->setKeyValue(NameKey, NewName);

    // Assert on the event that should have been received
    EXPECT_EQ(observer.changeStack.size(), 1) << "Observer didn't get notified about the changed key";
    EXPECT_TRUE(stackHasKeyValuePair(observer.changeStack, NameKey, NewName)) <<
        "Erase stack doesn't have the expected kv " << NameKey << " = " << NewName;

    // Everything else should be silent
    EXPECT_TRUE(observer.insertStack.empty()) << "Insert stack should be clean";
    EXPECT_TRUE(observer.eraseStack.empty()) << "Erase stack should be clean";

    observer.reset();

    constexpr const char* EvenNewerName = "Bonifazius";
    guard->setKeyValue(NameKey, EvenNewerName);

    // Assert on the event that should have been received
    EXPECT_EQ(observer.changeStack.size(), 1) << "Observer didn't get notified about the changed key";
    EXPECT_TRUE(stackHasKeyValuePair(observer.changeStack, NameKey, EvenNewerName)) <<
        "Erase stack doesn't have the expected kv " << NameKey << " = " << EvenNewerName;

    // On detaching the observer should not receive a corresponding erase for the newer value
    guard->detachObserver(&observer);

    EXPECT_TRUE(stackHasKeyValuePair(observer.eraseStack, NameKey, EvenNewerName)) <<
        "Erase stack unexpectedly contained the kv " << NameKey << " = " << EvenNewerName;
}

TEST_F(EntityTest, EntityObserverUndoRedo)
{
    auto [guardNode, guard] = TestEntity::create("atdm:ai_builder_guard");

    constexpr const char* NewKey = "New_Unique_Key";
    constexpr const char* NewValue = "New_Unique_Value";
    guard->setKeyValue(NewKey, NewValue);

    constexpr const char* NewKey2 = "New_Unique_Key2";
    constexpr const char* NewValue2 = "New_Unique_Value2";
    constexpr const char* NameKey = "name";
    constexpr const char* NewNameValue = "Ignazius";
    auto originalName = guard->getKeyValue(NameKey);

    TestEntityObserver observer;

    // Collect all existing key values of this entity
    auto keyValuesBeforeChange = algorithm::getAllKeyValuePairs(guard);
    EXPECT_FALSE(keyValuesBeforeChange.empty()) << "Entity doesn't have any keys";

    // On attachment, the observer gets notified about all existing keys (insert)
    guard->attachObserver(&observer);

    // Perform an undoable operation. The order of additions/changes/removals
    // does actually matter, since adding/removing will push the whole spawnarg set to the undo stack
    // whereas changing a single key will only push that single value to the stack
    {
        UndoableCommand cmd("testcommand");

        // Add another key
        guard->setKeyValue(NewKey2, NewValue2);

        // Change an existing key value
        guard->setKeyValue(NameKey, NewNameValue);

        // Remove a previously existing key
        guard->setKeyValue(NewKey, "");
    }

    auto keyValuesAfterChange = algorithm::getAllKeyValuePairs(guard);

    observer.reset();

    // UNDO
    GlobalUndoSystem().undo();

    // Check that the entity has now the same state as before the change
    expectKeyValuesAreEquivalent(algorithm::getAllKeyValuePairs(guard), keyValuesBeforeChange);

    // The Undo operation spams the observer with an erase() for each existing pair,
    // and a subsequent insert() for each one imported from the undo stack
    // Note that the value attached to the erase() event might depend on the order the SpawnArgs have been
    // manipulated during the Undoable operation - if a key value got changed before a new one
    // was added to the SpawnArg set, the value passed to erase() might differ from the case where
    // these two operations were happening the other way around.
    EXPECT_EQ(observer.eraseStack.size(), keyValuesAfterChange.size()) << "All keys before undo should have been reported";

    for (const auto& pair : keyValuesAfterChange)
    {
        // Only check the key of the erase calls, not the value
        EXPECT_TRUE(stackHasKey(observer.eraseStack, pair.first)) <<
            "Erase stack doesn't have the expected key " << pair.first;
    }

    EXPECT_EQ(observer.insertStack.size(), keyValuesBeforeChange.size()) << "Not all keys got reported as re-inserted";

    for (const auto& pair : keyValuesBeforeChange)
    {
        EXPECT_TRUE(stackHasKeyValuePair(observer.insertStack, pair.first, pair.second)) <<
            "Erase stack doesn't have the expected kv " << pair.first << " = " << pair.second;
    }

    // The single key value change triggered one key value change notification
    EXPECT_EQ(observer.changeStack.size(), 1) << "Change stack should just contain the single keyvalue change";
    EXPECT_TRUE(stackHasKeyValuePair(observer.changeStack, NameKey, originalName))
        << "Change stack should just contain the single keyvalue change";

    // REDO
    observer.reset();
    GlobalUndoSystem().redo();

    // Check that the entity has now the same state as before the undo
    expectKeyValuesAreEquivalent(algorithm::getAllKeyValuePairs(guard), keyValuesAfterChange);

    // The Redo operation should behave analogous to the undo, report all key values before the change as erased
    EXPECT_EQ(observer.eraseStack.size(), keyValuesBeforeChange.size()) << "All keys before redo should have been reported";

    for (const auto& pair : keyValuesBeforeChange)
    {
        // Only check the key of the erase calls, not the value
        EXPECT_TRUE(stackHasKey(observer.eraseStack, pair.first)) <<
            "Erase stack doesn't have the expected key " << pair.first;
    }

    EXPECT_EQ(observer.insertStack.size(), keyValuesAfterChange.size()) << "Not all keys got reported as re-inserted";

    // This can be considered a bug: on redo, not even the insert() call receives the correct
    // name key value "Ignazius", instead it receives the name before the change "atdm:ai_builder_guard_1"
    // So we can only assert on the key at this point
    for (const auto& pair : keyValuesAfterChange)
    {
        EXPECT_TRUE(stackHasKey(observer.insertStack, pair.first)) <<
            "Erase stack doesn't have the expected kv " << pair.first << " = " << pair.second;
    }

    // The single key value change triggered one key value change notification
    EXPECT_EQ(observer.changeStack.size(), 1) << "Change stack should just contain the single keyvalue change";
    EXPECT_TRUE(stackHasKeyValuePair(observer.changeStack, NameKey, NewNameValue))
        << "Change stack should just contain the single keyvalue change";

    guard->detachObserver(&observer);
}

TEST_F(EntityTest, EntityObserverUndoSingleKeyValue)
{
    auto [guardNode, guard] = TestEntity::create("atdm:ai_builder_guard");

    constexpr const char* NewKey = "New_Unique_Key";
    constexpr const char* NewValue = "New_Unique_Value";
    guard->setKeyValue(NewKey, NewValue);

    TestEntityObserver observer;
    // On attachment, the observer gets notified about all existing keys (insert)
    guard->attachObserver(&observer);

    // Perform an undoable operation. In this scenario, we're only editing
    // a single key, this means the entity is not saving the entire set to the stack
    constexpr const char* SomeOtherValue = "SomeOtherValue";
    {
        UndoableCommand cmd("testcommand");
        guard->setKeyValue(NewKey, SomeOtherValue);
    }

    // UNDO
    observer.reset();
    GlobalUndoSystem().undo();

    EXPECT_EQ(guard->getKeyValue(NewKey), NewValue) << "Key value not reverted properly";

    EXPECT_EQ(observer.changeStack.size(), 1) << "Reverted key didn't get reported";
    EXPECT_TRUE(stackHasKeyValuePair(observer.changeStack, NewKey, NewValue)) <<
        "Change stack doesn't have the expected kv " << NewKey << " = " << NewValue;

    // Everything else should be silent
    EXPECT_TRUE(observer.eraseStack.empty()) << "Erase stack should be clean";
    EXPECT_TRUE(observer.insertStack.empty()) << "Insert stack should be clean";

    // REDO
    observer.reset();
    GlobalUndoSystem().redo();

    EXPECT_EQ(guard->getKeyValue(NewKey), SomeOtherValue) << "Key value not re-done properly";

    EXPECT_EQ(observer.changeStack.size(), 1) << "Reverted key didn't get reported";
    EXPECT_TRUE(stackHasKeyValuePair(observer.changeStack, NewKey, SomeOtherValue)) <<
        "Change stack doesn't have the expected kv " << NewKey << " = " << SomeOtherValue;

    // Everything else should be silent
    EXPECT_TRUE(observer.eraseStack.empty()) << "Erase stack should be clean";
    EXPECT_TRUE(observer.insertStack.empty()) << "Insert stack should be clean";

    guard->detachObserver(&observer);
}

TEST_F(EntityTest, KeyObserverAttachDetach)
{
    auto guardNode = algorithm::createEntityByClassName("atdm:ai_builder_guard");
    auto guard = Node_getEntity(guardNode);

    constexpr const char* NewKeyName = "New_Unique_Key";
    constexpr const char* NewKeyValue = "New_Unique_Value";
    guard->setKeyValue(NewKeyName, NewKeyValue);

    TestKeyObserver observer;

    EntityKeyValue* keyValue = findKeyValue(guard, NewKeyName);
    EXPECT_TRUE(keyValue != nullptr) << "Could not locate the key value";

    // On attachment, the observer gets notified about the existing value
    keyValue->attach(observer);

    EXPECT_TRUE(observer.hasBeenInvoked()) << "Observer didn't get notified on attach";
    EXPECT_EQ(observer.receivedValue, NewKeyValue) << "Observer didn't get the correct value";

    observer.reset();
    observer.receivedValue = "dummyvalue_that_should_be_overwritten";

    // On detaching the observer receives another call with an empty value
    keyValue->detach(observer);

    EXPECT_TRUE(observer.hasBeenInvoked()) << "Observer didn't get notified on attach";
    EXPECT_EQ(observer.receivedValue, "") << "Observer didn't get the expected empty value";
}

TEST_F(EntityTest, KeyObserverValueChange)
{
    auto guardNode = algorithm::createEntityByClassName("atdm:ai_builder_guard");
    auto guard = Node_getEntity(guardNode);

    constexpr const char* NewKeyName = "New_Unique_Key";
    constexpr const char* NewKeyValue = "New_Unique_Value";
    guard->setKeyValue(NewKeyName, NewKeyValue);

    TestKeyObserver observer;

    EntityKeyValue* keyValue = findKeyValue(guard, NewKeyName);
    EXPECT_TRUE(keyValue != nullptr) << "Could not locate the key value";

    keyValue->attach(observer);
    observer.reset();

    constexpr const char* SomeOtherValue = "SomeOtherValue";
    guard->setKeyValue(NewKeyName, SomeOtherValue);

    EXPECT_TRUE(observer.hasBeenInvoked()) << "Observer didn't get notified on change";
    EXPECT_EQ(observer.receivedValue, SomeOtherValue) << "Observer didn't get the correct value";

    // One more round, this time we use the assign() method
    observer.reset();
    constexpr const char* DistinguishableValue = "DistinguishableValue";
    keyValue->assign(DistinguishableValue);

    EXPECT_TRUE(observer.hasBeenInvoked()) << "Observer didn't get notified on assign";
    EXPECT_EQ(observer.receivedValue, DistinguishableValue) << "Observer didn't get the correct value";
    observer.reset();

    keyValue->detach(observer);

    EXPECT_TRUE(observer.hasBeenInvoked()) << "Observer didn't get notified on attach";
    EXPECT_EQ(observer.receivedValue, "") << "Observer didn't get the expected empty value";
}

// Check that an KeyObserver stays attached to the key value after Undo
TEST_F(EntityTest, KeyObserverAttachedAfterUndo)
{
    auto [guardNode, guard] = TestEntity::create("atdm:ai_builder_guard");

    constexpr const char* NewKeyName = "New_Unique_Key";
    constexpr const char* NewKeyValue = "New_Unique_Value";
    guard->setKeyValue(NewKeyName, NewKeyValue);

    TestKeyObserver observer;
    EntityKeyValue* keyValue = findKeyValue(guard, NewKeyName);
    EXPECT_TRUE(keyValue != nullptr) << "Could not locate the key value";

    // Monitor this new key
    keyValue->attach(observer);
    observer.reset();

    // Open an undoable transaction and change that keyvalue
    constexpr const char* SomeOtherValue = "SomeOtherValue";
    {
        UndoableCommand cmd("changeKeyValue");
        guard->setKeyValue(NewKeyName, SomeOtherValue);
    }

    EXPECT_TRUE(observer.hasBeenInvoked()) << "Observer didn't get notified on change";
    EXPECT_EQ(observer.receivedValue, SomeOtherValue) << "Observer didn't get the correct value";

    // Hit Undo to revert the changed value
    GlobalUndoSystem().undo();
    EXPECT_EQ(guard->getKeyValue(NewKeyName), NewKeyValue) << "Key is still changed after undo";

    // Reset the observer and check whether it still receives messages
    observer.reset();
    guard->setKeyValue(NewKeyName, SomeOtherValue);

    EXPECT_TRUE(observer.hasBeenInvoked()) << "Observer didn't get notified on assign";
    EXPECT_EQ(observer.receivedValue, SomeOtherValue) << "Observer didn't get the correct value";

    keyValue->detach(observer);
}

// Checks that the value changes by undo/redo commands are sent out to the KeyObservers
TEST_F(EntityTest, KeyObserverUndoRedoValueChange)
{
    auto [guardNode, guard] = TestEntity::create("atdm:ai_builder_guard");

    constexpr const char* NewKeyName = "New_Unique_Key";
    constexpr const char* NewKeyValue = "New_Unique_Value";
    guard->setKeyValue(NewKeyName, NewKeyValue);

    TestKeyObserver observer;
    EntityKeyValue* keyValue = findKeyValue(guard, NewKeyName);
    EXPECT_TRUE(keyValue != nullptr) << "Could not locate the key value";

    // Monitor this new key
    keyValue->attach(observer);
    observer.reset();

    // Open an undoable transaction and change that keyvalue
    constexpr const char* SomeOtherValue = "SomeOtherValue";
    {
        UndoableCommand cmd("changeKeyValue");
        guard->setKeyValue(NewKeyName, SomeOtherValue);
    }

    // Undo
    observer.reset();
    GlobalUndoSystem().undo();
    EXPECT_EQ(guard->getKeyValue(NewKeyName), NewKeyValue) << "Key value wasn't properly reverted";
    EXPECT_TRUE(observer.hasBeenInvoked()) << "Observer didn't get notified on undo";
    EXPECT_EQ(observer.receivedValue, NewKeyValue) << "Observer didn't get the value before change";

    // Redo
    observer.reset();
    GlobalUndoSystem().redo();

    EXPECT_EQ(guard->getKeyValue(NewKeyName), SomeOtherValue) << "Key value wasn't properly redone";
    EXPECT_TRUE(observer.hasBeenInvoked()) << "Observer didn't get notified on redo";
    EXPECT_EQ(observer.receivedValue, SomeOtherValue) << "Observer didn't get the value after change";

    keyValue->detach(observer);
}

// KeyObserver doesn't get called when a key is removed entirely from the SpawnArgs
TEST_F(EntityTest, KeyObserverKeyRemoval)
{
    auto [guardNode, guard] = TestEntity::create("atdm:ai_builder_guard");

    constexpr const char* NewKeyName = "New_Unique_Key";
    constexpr const char* NewKeyValue = "New_Unique_Value";
    guard->setKeyValue(NewKeyName, NewKeyValue);

    UndoableCommand cmd("removeKey"); // prevent the key value from going out of scope
    TestKeyObserver observer;

    EntityKeyValue* keyValue = findKeyValue(guard, NewKeyName);
    EXPECT_TRUE(keyValue != nullptr) << "Could not locate the key value";

    keyValue->attach(observer);
    observer.reset();

    // Remove the key
    guard->setKeyValue(NewKeyName, "");

    // The observer shouldn't have been notified
    EXPECT_FALSE(observer.hasBeenInvoked()) << "Observer has been notified on key remove";

    keyValue->detach(observer);
}

TEST_F(EntityTest, EntityNodeObserveKeyViaFunc)
{
    auto [entityNode, _] = TestEntity::create("atdm:ai_builder_guard");

    constexpr const char* TEST_KEY = "AnotherTestKey";

    // No need for a KeyObserver, just store the info locally and bind it via lambdas
    int invocationCount = 0;
    std::string receivedValue;

    // Observe key before creating it
    entityNode->observeKey(TEST_KEY, [&](const std::string& value) {
        ++invocationCount;
        receivedValue = value;
    });
    EXPECT_EQ(invocationCount, 1);
    EXPECT_EQ(receivedValue, "");

    // Add the key with a new value
    entityNode->getEntity().setKeyValue(TEST_KEY, "First value");
    EXPECT_EQ(invocationCount, 2);
    EXPECT_EQ(receivedValue, "First value");

    // Change the value
    entityNode->getEntity().setKeyValue(TEST_KEY, "3.1425");
    EXPECT_EQ(invocationCount, 3);
    EXPECT_EQ(receivedValue, "3.1425");

    // Remove the value
    entityNode->getEntity().setKeyValue(TEST_KEY, "");
    EXPECT_EQ(invocationCount, 4);
    EXPECT_EQ(receivedValue, "");

    // Set another value
    entityNode->getEntity().setKeyValue(TEST_KEY, "-O-O-O-");
    EXPECT_EQ(invocationCount, 5);
    EXPECT_EQ(receivedValue, "-O-O-O-");
}

TEST_F(EntityTest, EntityNodeObserveKeyAutoDisconnect)
{
    auto [entityNode, spawnArgs] = TestEntity::create("atdm:ai_builder_guard");

    constexpr const char* TEST_KEY = "AnotherTestKey";

    // Allocate observer on the heap, so we can free the memory and hopefully
    // trigger a crash if the slot is called after deletion.
    auto* observer = new TestKeyObserver();

    // Observe key before creating it
    entityNode->observeKey(TEST_KEY,
                           sigc::mem_fun(observer, &TestKeyObserver::onKeyValueChanged));
    EXPECT_EQ(observer->invocationCount, 1);
    EXPECT_EQ(observer->receivedValue, "");

    // Destroy the observer and reclaim memory
    delete observer;

    // Making a new key change should not cause a crash
    spawnArgs->setKeyValue(TEST_KEY, "whatever");
}

inline IEntityNodePtr findPlayerStartEntity()
{
    IEntityNodePtr found;

    algorithm::findFirstEntity(GlobalMapModule().getRoot(), [&](const IEntityNodePtr& entity)
    {
        if (entity->getEntity().getEntityClass()->getDeclName() == "info_player_start")
        {
            found = entity;
        }

        return found == nullptr;
    });

    return found;
}

TEST_F(EntityTest, AddPlayerStart)
{
    // Empty map, check prerequisites
    EXPECT_EQ(findPlayerStartEntity(), nullptr) << "Empty map shouldn't have a player start";

    Vector3 position(50, 30, 40);
    GlobalCommandSystem().executeCommand("PlacePlayerStart", cmd::Argument(position));

    auto playerStart = findPlayerStartEntity();
    EXPECT_TRUE(playerStart) << "Couldn't find the player start entity after placing it";

    EXPECT_EQ(playerStart->getEntity().getKeyValue("origin"), string::to_string(position)) << "Origin has the wrong value";

    EXPECT_TRUE(Node_isSelected(playerStart)) << "Player start should be selected after placement";

    // Ensure this action is undoable
    GlobalUndoSystem().undo();
    EXPECT_EQ(findPlayerStartEntity(), nullptr) << "Couldn't undo the place player start action";
}

TEST_F(EntityTest, MovePlayerStart)
{
    // Empty map, check prerequisites
    auto originalPosition = "50 30 47";
    auto playerStart = GlobalEntityModule().createEntity(
        GlobalEntityClassManager().findOrInsert("info_player_start", false)
    );
    scene::addNodeToContainer(playerStart, GlobalMapModule().getRoot());
    Node_getEntity(playerStart)->setKeyValue("origin", originalPosition);

    Vector3 position(7, 2, -4);
    GlobalCommandSystem().executeCommand("PlacePlayerStart", cmd::Argument(position));
    EXPECT_EQ(Node_getEntity(playerStart)->getKeyValue("origin"), string::to_string(position)) << "Origin didn't get updated";

    EXPECT_TRUE(Node_isSelected(playerStart)) << "Player start should be selected after placement";

    // Ensure this action is undoable
    GlobalUndoSystem().undo();
    EXPECT_EQ(Node_getEntity(playerStart)->getKeyValue("origin"), originalPosition) << "Origin change didn't get undone";
}

TEST_F(EntityTest, CreateSpeaker)
{
    std::string originalPosition = "50 30 47";

    GlobalCommandSystem().executeCommand("CreateSpeaker", {
        cmd::Argument("test/jorge"), cmd::Argument(originalPosition)
    });

    // The speaker should be in the map now
    auto speaker = std::dynamic_pointer_cast<IEntityNode>(
        algorithm::findFirstEntity(GlobalMapModule().getRoot(), [](const IEntityNodePtr& entity)
    {
        return entity->getEntity().getEntityClass()->getDeclName() == "speaker";
    }));

    EXPECT_TRUE(speaker) << "Could not locate the speaker in the map";

    // Should be at the correct position
    EXPECT_EQ(speaker->worldAABB().getOrigin(), string::convert<Vector3>(originalPosition));
    EXPECT_EQ(speaker->getEntity().getKeyValue("origin"), originalPosition);

    // Should carry the s_min/s_max/s_shader key values
    EXPECT_EQ(speaker->getEntity().getKeyValue("s_shader"), "test/jorge");

    auto soundShader = GlobalSoundManager().getSoundShader("test/jorge");
    EXPECT_TRUE(soundShader) << "Could not locate the jorge sound shader";
    EXPECT_EQ(speaker->getEntity().getKeyValue("s_mindistance"), string::to_string(soundShader->getRadii().getMin(true)));
    EXPECT_EQ(speaker->getEntity().getKeyValue("s_maxdistance"), string::to_string(soundShader->getRadii().getMax(true)));

    // The speaker should be selected
    EXPECT_TRUE(Node_isSelected(speaker)) << "Speaker should be selected";
}

// #6062: Moving a speaker should no longer remove the s_mindistance/s_maxdistance key values on the entity
// even if they are matching the values defined in the sound shader declaration
TEST_F(EntityTest, MovingSpeakerNotRemovingDistanceArgs)
{
    // Empty map, check prerequisites
    GlobalCommandSystem().executeCommand("CreateSpeaker", {
        cmd::Argument("test/jorge"), cmd::Argument("50 30 47")
    });

    auto node = algorithm::findFirstEntity(GlobalMapModule().getRoot(), [](const IEntityNodePtr& entity)
    {
        return entity->getEntity().getEntityClass()->getDeclName() == "speaker";
    });
    auto speaker = std::dynamic_pointer_cast<IEntityNode>(node);

    EXPECT_TRUE(speaker);
    EXPECT_NE(speaker->getEntity().getKeyValue("s_mindistance"), "");
    EXPECT_NE(speaker->getEntity().getKeyValue("s_maxdistance"), "");

    // Move the speaker
    GlobalSelectionSystem().setSelectedAll(false);
    Node_setSelected(speaker, true);

    auto transformable = scene::node_cast<ITransformable>(speaker);
    transformable->setType(TRANSFORM_PRIMITIVE);
    transformable->setTranslation({ -64, 0, 0 });
    transformable->freezeTransform();

    EXPECT_NE(speaker->getEntity().getKeyValue("s_mindistance"), "") << "Key value has been lost in translation";
    EXPECT_NE(speaker->getEntity().getKeyValue("s_maxdistance"), "") << "Key value has been lost in translation";
}

// #6274: Empty rotation when cloning an entity using editor_rotatable and an angle key
TEST_F(EntityTest, CloneGenericEntityRotatableAngle)
{
    auto separator = algorithm::createEntityByClassName("info_vacuumSeparator");
    separator->getEntity().setKeyValue("angle", "180.0");

    // Direction should be negative x axis
    EXPECT_TRUE(math::isNear(separator->getDirection(), Vector3(-1, 0, 0), 0.01));

    // Clone the entity node
    auto separatorCopy = std::dynamic_pointer_cast<IEntityNode>(separator->clone());

    // Confirm the direction of clone is also negative x axis
    EXPECT_TRUE(math::isNear(separatorCopy->getDirection(), Vector3(-1, 0, 0), 0.01));
    Matrix4 mat = Matrix4::getRotationAboutZ(math::Degrees(180.0));
}

// Related to #6274: Confirm that still works correctly when using rotation
TEST_F(EntityTest, CloneGenericEntityRotatableRotation)
{
    auto separator = algorithm::createEntityByClassName("info_vacuumSeparator");
    separator->getEntity().setKeyValue("rotation", "-1 0 0 0 -1 0 0 0 1");

    // Direction should be negative x axis
    EXPECT_TRUE(math::isNear(separator->getDirection(), Vector3(-1, 0, 0), 0.01));

    // Clone the entity node
    auto separatorCopy = std::dynamic_pointer_cast<IEntityNode>(separator->clone());

    // Confirm the direction of clone is also negative x axis
    EXPECT_TRUE(math::isNear(separatorCopy->getDirection(), Vector3(-1, 0, 0), 0.01));
    Matrix4 mat = Matrix4::getRotationAboutZ(math::Degrees(180.0));
}

}