#include "catch.hpp"

#include <memory>
#include <vector>

#include "DebrisSelectionChangedEvent.h"
#include "EntitySelectionChangedEvent.h"
#include "EventHandler.h"
#include "EventManager.h"
#include "SelectionChangedEvent.h"
#include "SelectionController.h"

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

// Counts the change events the controller publishes, so the tests can assert not
// just the resulting selection but that the widgets were told about it — and, for
// the categories a change did not touch, that they were not told needlessly.
class SelectionEventSpy : public CombinedEventHandler<SelectionChangedEvent,
                                                      EntitySelectionChangedEvent,
                                                      DebrisSelectionChangedEvent>
{
public:
    SelectionEventSpy()  { registerForEvents(); }
    ~SelectionEventSpy() { unregisterForEvents(); }

    int buildingEvents = 0;
    int actorEvents    = 0;
    int debrisEvents   = 0;

    void reset() { buildingEvents = 0; actorEvents = 0; debrisEvents = 0; }

private:
    void handleEvent(std::shared_ptr<const SelectionChangedEvent> /*event*/) override
    {
        ++buildingEvents;
    }
    void handleEvent(std::shared_ptr<const EntitySelectionChangedEvent> /*event*/) override
    {
        ++actorEvents;
    }
    void handleEvent(std::shared_ptr<const DebrisSelectionChangedEvent> /*event*/) override
    {
        ++debrisEvents;
    }
};

static entt::entity makeEntity(int index)
{
    return static_cast<entt::entity>(index);
}

// ---------------------------------------------------------------------------
// Category precedence (REQ-UI-SELECTION-CATEGORIES)
// ---------------------------------------------------------------------------

TEST_CASE("Selecting a building clears actors and debris", "[selection]")
{
    SelectionController controller;
    controller.selectFieldObjects({makeEntity(1)}, {makeEntity(2)},
                                  SelectionMode::Replace);

    controller.selectBuildings({BuildingId(7)}, SelectionMode::Replace);

    REQUIRE(controller.getSelectedBuildings() == std::vector<BuildingId>{BuildingId(7)});
    REQUIRE(controller.getSelectedActors().empty());
    REQUIRE(controller.getSelectedDebris().empty());
}

TEST_CASE("Selecting a field object clears buildings", "[selection]")
{
    SelectionController controller;
    controller.selectBuildings({BuildingId(7)}, SelectionMode::Replace);

    controller.selectFieldObjects({makeEntity(1)}, {}, SelectionMode::Replace);

    REQUIRE(controller.getSelectedBuildings().empty());
    REQUIRE(controller.getSelectedActors() == std::vector<entt::entity>{makeEntity(1)});
}

TEST_CASE("Actors and debris coexist", "[selection]")
{
    // The one pair of categories that does not evict each other
    // (REQ-UI-MULTI-SELECT, REQ-UI-DEBRIS-MULTI-SELECT).
    SelectionController controller;
    controller.selectFieldObjects({makeEntity(1)}, {makeEntity(2)},
                                  SelectionMode::Replace);

    REQUIRE(controller.getSelectedActors() == std::vector<entt::entity>{makeEntity(1)});
    REQUIRE(controller.getSelectedDebris() == std::vector<entt::entity>{makeEntity(2)});
}

TEST_CASE("A plain click on an actor drops any selected debris", "[selection]")
{
    // A point click passes only the category it hit, so Replace with an empty
    // debris list is what "this actor and nothing else" means
    // (REQ-UI-ENTITY-CLICK-SELECT).
    SelectionController controller;
    controller.selectFieldObjects({}, {makeEntity(2)}, SelectionMode::Replace);

    controller.selectFieldObjects({makeEntity(1)}, {}, SelectionMode::Replace);

    REQUIRE(controller.getSelectedActors() == std::vector<entt::entity>{makeEntity(1)});
    REQUIRE(controller.getSelectedDebris().empty());
}

// ---------------------------------------------------------------------------
// Replace / Toggle / Add
// ---------------------------------------------------------------------------

TEST_CASE("Replace makes the hit the whole selection", "[selection]")
{
    SelectionController controller;
    controller.selectBuildings({BuildingId(1), BuildingId(2)}, SelectionMode::Replace);

    controller.selectBuildings({BuildingId(3)}, SelectionMode::Replace);

    REQUIRE(controller.getSelectedBuildings() == std::vector<BuildingId>{BuildingId(3)});
}

TEST_CASE("Toggle adds a building that was not selected", "[selection]")
{
    SelectionController controller;
    controller.selectBuildings({BuildingId(1)}, SelectionMode::Replace);

    controller.selectBuildings({BuildingId(2)}, SelectionMode::Toggle);

    REQUIRE(controller.getSelectedBuildings()
            == std::vector<BuildingId>{BuildingId(1), BuildingId(2)});
}

TEST_CASE("Toggle removes a building that was already selected", "[selection]")
{
    // Ctrl+clicking a selected building deselects it, leaving the rest alone.
    SelectionController controller;
    controller.selectBuildings({BuildingId(1), BuildingId(2), BuildingId(3)},
                               SelectionMode::Replace);

    controller.selectBuildings({BuildingId(2)}, SelectionMode::Toggle);

    REQUIRE(controller.getSelectedBuildings()
            == std::vector<BuildingId>{BuildingId(1), BuildingId(3)});
}

TEST_CASE("Add never deselects", "[selection]")
{
    // This is where a Ctrl box drag differs from a Ctrl click: dragging over
    // already-selected buildings must not toggle them off (REQ-UI-MULTI-SELECT).
    SelectionController controller;
    controller.selectBuildings({BuildingId(1), BuildingId(2)}, SelectionMode::Replace);

    controller.selectBuildings({BuildingId(2), BuildingId(3)}, SelectionMode::Add);

    REQUIRE(controller.getSelectedBuildings()
            == std::vector<BuildingId>{BuildingId(1), BuildingId(2), BuildingId(3)});
}

TEST_CASE("An additive field selection leaves the untouched category alone",
          "[selection]")
{
    // Ctrl+clicking an actor passes an empty debris list, which must mean "do not
    // touch debris" rather than "clear debris" (REQ-UI-DEBRIS-MULTI-SELECT).
    SelectionController controller;
    controller.selectFieldObjects({}, {makeEntity(5)}, SelectionMode::Replace);

    controller.selectFieldObjects({makeEntity(1)}, {}, SelectionMode::Toggle);

    REQUIRE(controller.getSelectedActors() == std::vector<entt::entity>{makeEntity(1)});
    REQUIRE(controller.getSelectedDebris() == std::vector<entt::entity>{makeEntity(5)});
}

// ---------------------------------------------------------------------------
// Membership queries used by the renderer
// ---------------------------------------------------------------------------

TEST_CASE("Membership queries answer per category", "[selection]")
{
    SelectionController controller;
    controller.selectFieldObjects({makeEntity(1)}, {makeEntity(2)},
                                  SelectionMode::Replace);

    REQUIRE(controller.isActorSelected(makeEntity(1)));
    REQUIRE_FALSE(controller.isActorSelected(makeEntity(2)));
    REQUIRE(controller.isDebrisSelected(makeEntity(2)));
    REQUIRE_FALSE(controller.isDebrisSelected(makeEntity(1)));
}

// ---------------------------------------------------------------------------
// Published events
// ---------------------------------------------------------------------------

TEST_CASE("Selecting a building announces the categories it cleared", "[selection]")
{
    SelectionController controller;
    controller.selectFieldObjects({makeEntity(1)}, {makeEntity(2)},
                                  SelectionMode::Replace);

    SelectionEventSpy spy;
    controller.selectBuildings({BuildingId(7)}, SelectionMode::Replace);

    REQUIRE(spy.buildingEvents == 1);
    REQUIRE(spy.actorEvents    == 1);
    REQUIRE(spy.debrisEvents   == 1);
}

TEST_CASE("Clearing an already-empty category announces nothing", "[selection]")
{
    // The panels re-read on every event, so a redundant one is only noise — but the
    // point-click and box paths used to disagree about this, so it is pinned.
    SelectionController controller;

    SelectionEventSpy spy;
    controller.selectBuildings({BuildingId(7)}, SelectionMode::Replace);

    REQUIRE(spy.buildingEvents == 1);
    REQUIRE(spy.actorEvents    == 0);
    REQUIRE(spy.debrisEvents   == 0);
}

TEST_CASE("Re-selecting the same building still announces it", "[selection]")
{
    // Clicking an already-selected building refreshes its panel, so the event
    // fires even though the selection did not change.
    SelectionController controller;
    controller.selectBuildings({BuildingId(7)}, SelectionMode::Replace);

    SelectionEventSpy spy;
    controller.selectBuildings({BuildingId(7)}, SelectionMode::Replace);

    REQUIRE(spy.buildingEvents == 1);
}

TEST_CASE("Clearing an empty selection announces nothing at all", "[selection]")
{
    SelectionController controller;

    SelectionEventSpy spy;
    controller.clearAll();

    REQUIRE(spy.buildingEvents == 0);
    REQUIRE(spy.actorEvents    == 0);
    REQUIRE(spy.debrisEvents   == 0);
}

TEST_CASE("Clearing a populated selection announces every populated category",
          "[selection]")
{
    SelectionController controller;
    controller.selectFieldObjects({makeEntity(1)}, {makeEntity(2)},
                                  SelectionMode::Replace);

    SelectionEventSpy spy;
    controller.clearAll();

    REQUIRE(spy.buildingEvents == 0);  // buildings were already empty
    REQUIRE(spy.actorEvents    == 1);
    REQUIRE(spy.debrisEvents   == 1);
    REQUIRE(controller.getSelectedActors().empty());
    REQUIRE(controller.getSelectedDebris().empty());
}

// ---------------------------------------------------------------------------
// Pruning despawned entities
// ---------------------------------------------------------------------------

TEST_CASE("Pruning announces only when something actually went", "[selection]")
{
    // Runs every frame, so re-announcing an unchanged selection would spam the
    // panels 60 times a second.
    SelectionController controller;
    controller.selectFieldObjects({makeEntity(1), makeEntity(2)}, {},
                                  SelectionMode::Replace);

    SelectionEventSpy spy;
    controller.setSelectedActors({makeEntity(1), makeEntity(2)});
    REQUIRE(spy.actorEvents == 0);

    controller.setSelectedActors({makeEntity(1)});
    REQUIRE(spy.actorEvents == 1);
    REQUIRE(controller.getSelectedActors() == std::vector<entt::entity>{makeEntity(1)});
}
