#include "catch.hpp"

#include <memory>

#include "BlueprintModeExitedEvent.h"
#include "BuildModeController.h"
#include "BuilderModeExitedEvent.h"
#include "DeconstructModeChangedEvent.h"
#include "EventHandler.h"
#include "EventManager.h"

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

// Records the mode events, which are the half of this class's contract that the
// panels depend on: a mode that ends without announcing it leaves its button
// stuck highlighted.
class ModeEventSpy : public CombinedEventHandler<BuilderModeExitedEvent,
                                                 BlueprintModeExitedEvent,
                                                 DeconstructModeChangedEvent>
{
public:
    ModeEventSpy()  { registerForEvents(); }
    ~ModeEventSpy() { unregisterForEvents(); }

    int  builderExits    = 0;
    int  blueprintExits  = 0;
    int  deconstructChanges = 0;
    bool lastDeconstructActive = false;

private:
    void handleEvent(std::shared_ptr<const BuilderModeExitedEvent> /*event*/) override
    {
        ++builderExits;
    }
    void handleEvent(std::shared_ptr<const BlueprintModeExitedEvent> /*event*/) override
    {
        ++blueprintExits;
    }
    void handleEvent(std::shared_ptr<const DeconstructModeChangedEvent> event) override
    {
        ++deconstructChanges;
        lastDeconstructActive = event->active;
    }
};

static Blueprint makeBlueprint()
{
    Blueprint blueprint;
    BlueprintBuilding building;
    building.type     = BuildingType::Belt;
    building.offset   = QPoint(0, 0);
    building.rotation = Rotation::East;
    blueprint.buildings.push_back(building);
    return blueprint;
}

// ---------------------------------------------------------------------------
// Exclusivity
// ---------------------------------------------------------------------------

TEST_CASE("Only one mode is active at a time", "[buildmode]")
{
    BuildModeController controller;
    REQUIRE(controller.getMode() == BuildMode::None);

    controller.enterBuilderMode(BuildingType::Belt);
    REQUIRE(controller.isBuilderMode());
    REQUIRE_FALSE(controller.isBlueprintMode());
    REQUIRE_FALSE(controller.isDeconstructMode());

    controller.enterBlueprintMode(makeBlueprint());
    REQUIRE(controller.isBlueprintMode());
    REQUIRE_FALSE(controller.isBuilderMode());

    controller.toggleDeconstructMode();
    REQUIRE(controller.isDeconstructMode());
    REQUIRE_FALSE(controller.isBlueprintMode());
}

TEST_CASE("Entering builder mode announces that blueprint mode ended", "[buildmode]")
{
    // Regression: entering builder mode used to drop the blueprint silently, so the
    // blueprint library kept tracking an active blueprint for a mode that was over.
    BuildModeController controller;
    controller.enterBlueprintMode(makeBlueprint());

    ModeEventSpy spy;
    controller.enterBuilderMode(BuildingType::Belt);

    REQUIRE(spy.blueprintExits == 1);
}

TEST_CASE("Every mode announces its exit however it is left", "[buildmode]")
{
    // The point of routing all transitions through one place: which mode the player
    // switches to must not change what the mode they left announces.
    SECTION("builder, left for a blueprint")
    {
        BuildModeController controller;
        controller.enterBuilderMode(BuildingType::Belt);
        ModeEventSpy spy;
        controller.enterBlueprintMode(makeBlueprint());
        REQUIRE(spy.builderExits == 1);
    }
    SECTION("builder, left for deconstruct")
    {
        BuildModeController controller;
        controller.enterBuilderMode(BuildingType::Belt);
        ModeEventSpy spy;
        controller.toggleDeconstructMode();
        REQUIRE(spy.builderExits == 1);
    }
    SECTION("blueprint, left for deconstruct")
    {
        BuildModeController controller;
        controller.enterBlueprintMode(makeBlueprint());
        ModeEventSpy spy;
        controller.toggleDeconstructMode();
        REQUIRE(spy.blueprintExits == 1);
    }
    SECTION("deconstruct, left for builder")
    {
        BuildModeController controller;
        controller.toggleDeconstructMode();
        ModeEventSpy spy;
        controller.enterBuilderMode(BuildingType::Belt);
        REQUIRE(spy.deconstructChanges == 1);
        REQUIRE_FALSE(spy.lastDeconstructActive);
    }
}

TEST_CASE("Switching between builder types stays in builder mode", "[buildmode]")
{
    // Picking a different building is not leaving the mode, so nothing is announced.
    BuildModeController controller;
    controller.enterBuilderMode(BuildingType::Belt);

    ModeEventSpy spy;
    controller.enterBuilderMode(BuildingType::Splitter);

    REQUIRE(controller.getBuilderType() == BuildingType::Splitter);
    REQUIRE(spy.builderExits == 0);
}

TEST_CASE("Exiting a mode that is not active does nothing", "[buildmode]")
{
    BuildModeController controller;
    controller.enterBuilderMode(BuildingType::Belt);

    ModeEventSpy spy;
    controller.exitBlueprintMode();

    REQUIRE(controller.isBuilderMode());
    REQUIRE(spy.blueprintExits == 0);
    REQUIRE(spy.builderExits   == 0);
}

TEST_CASE("Deconstruct mode toggles off and announces both edges", "[buildmode]")
{
    BuildModeController controller;

    ModeEventSpy spy;
    controller.toggleDeconstructMode();
    REQUIRE(controller.isDeconstructMode());
    REQUIRE(spy.lastDeconstructActive);

    controller.toggleDeconstructMode();
    REQUIRE(controller.getMode() == BuildMode::None);
    REQUIRE_FALSE(spy.lastDeconstructActive);
    REQUIRE(spy.deconstructChanges == 2);
}

TEST_CASE("Leaving the current mode works from any of them", "[buildmode]")
{
    BuildModeController controller;
    controller.enterBuilderMode(BuildingType::Belt);
    controller.exitCurrentMode();
    REQUIRE(controller.getMode() == BuildMode::None);

    controller.enterBlueprintMode(makeBlueprint());
    controller.exitCurrentMode();
    REQUIRE(controller.getMode() == BuildMode::None);

    controller.toggleDeconstructMode();
    controller.exitCurrentMode();
    REQUIRE(controller.getMode() == BuildMode::None);
}

// ---------------------------------------------------------------------------
// State cleared on transition
// ---------------------------------------------------------------------------

TEST_CASE("Leaving builder mode drops an in-progress belt drag", "[buildmode]")
{
    // A drag surviving the mode change would place belts on the next release.
    BuildModeController controller;
    controller.enterBuilderMode(BuildingType::Belt);
    controller.beginBeltDrag(QPoint(3, 4));
    controller.setBeltDragPath({BeltPathTile{QPoint(3, 4), Rotation::East}});
    REQUIRE(controller.isDraggingBelt());

    controller.toggleDeconstructMode();

    REQUIRE_FALSE(controller.isDraggingBelt());
    REQUIRE(controller.getBeltDragPath().empty());
}

TEST_CASE("Leaving deconstruct mode drops the hovered building", "[buildmode]")
{
    BuildModeController controller;
    controller.toggleDeconstructMode();
    controller.setDeconstructHoverBuildingId(BuildingId(4));

    controller.enterBuilderMode(BuildingType::Belt);

    REQUIRE_FALSE(controller.getDeconstructHoverBuildingId().has_value());
}

TEST_CASE("Entering builder mode resets the ghost", "[buildmode]")
{
    // A fresh builder starts facing East and invalid until the first hover, rather
    // than inheriting the previous building's facing (REQ-BLD-GHOST).
    BuildModeController controller;
    controller.enterBuilderMode(BuildingType::Belt);
    controller.rotateGhost(true);
    controller.setGhostValidity(true);

    controller.enterBuilderMode(BuildingType::Splitter);

    REQUIRE(controller.getGhostRotation() == Rotation::East);
    REQUIRE_FALSE(controller.isGhostValid());
}

TEST_CASE("Cancelling a belt drag stays in builder mode", "[buildmode]")
{
    // Right-click during a drag abandons the path but keeps the belt selected
    // (REQ-BLD-BELT-DRAG).
    BuildModeController controller;
    controller.enterBuilderMode(BuildingType::Belt);
    controller.beginBeltDrag(QPoint(1, 1));

    ModeEventSpy spy;
    controller.cancelBeltDrag();

    REQUIRE(controller.isBuilderMode());
    REQUIRE_FALSE(controller.isDraggingBelt());
    REQUIRE(spy.builderExits == 0);
}

// ---------------------------------------------------------------------------
// Ghost and tunnel state
// ---------------------------------------------------------------------------

TEST_CASE("Rotating the ghost cycles through the four facings", "[buildmode]")
{
    BuildModeController controller;
    controller.enterBuilderMode(BuildingType::Belt);

    controller.rotateGhost(true);
    REQUIRE(controller.getGhostRotation() == Rotation::South);
    controller.rotateGhost(true);
    REQUIRE(controller.getGhostRotation() == Rotation::West);
    controller.rotateGhost(false);
    REQUIRE(controller.getGhostRotation() == Rotation::South);
}

TEST_CASE("Tunnel mode is the tunnel entry builder type", "[buildmode]")
{
    // REQ-BLD-TUNNEL-MODE: one builder type covers both tunnel ends.
    BuildModeController controller;
    controller.enterBuilderMode(BuildingType::Belt);
    REQUIRE_FALSE(controller.isTunnelMode());

    controller.enterBuilderMode(BuildingType::TunnelEntry);
    REQUIRE(controller.isTunnelMode());
}

TEST_CASE("The effective builder type follows the resolved tunnel end", "[buildmode]")
{
    BuildModeController controller;
    controller.enterBuilderMode(BuildingType::TunnelEntry);
    REQUIRE(controller.getEffectiveBuilderType() == BuildingType::TunnelEntry);

    controller.setTunnelGhost(BuildingType::TunnelExit, QPoint(5, 5));
    REQUIRE(controller.getEffectiveBuilderType() == BuildingType::TunnelExit);
    REQUIRE(controller.getTunnelPartnerTile() == QPoint(5, 5));
}

TEST_CASE("Clearing the hover drops everything the cursor pointed at", "[buildmode]")
{
    // A cursor that leaves the world hovers nothing, so the ghost and its resolved
    // tunnel end go with it, while the mode itself stays active (REQ-BLD-GHOST).
    BuildModeController controller;
    controller.enterBuilderMode(BuildingType::TunnelEntry);
    controller.setGhostTile(QPoint(7, 2));
    controller.setGhostValidity(true);
    controller.setTunnelGhost(BuildingType::TunnelExit, QPoint(5, 2));

    controller.clearHover();

    REQUIRE(controller.isBuilderMode());
    REQUIRE_FALSE(controller.getGhostTile().has_value());
    REQUIRE_FALSE(controller.isGhostValid());
    REQUIRE(controller.getEffectiveBuilderType() == BuildingType::TunnelEntry);
    REQUIRE_FALSE(controller.getTunnelPartnerTile().has_value());
}

TEST_CASE("Clearing the hover keeps a belt drag's path", "[buildmode]")
{
    // A drag holds the button and goes on hovering wherever the cursor travels, so
    // nothing clears it short of releasing or cancelling (REQ-BLD-BELT-DRAG).
    BuildModeController controller;
    controller.enterBuilderMode(BuildingType::Belt);
    controller.beginBeltDrag(QPoint(3, 4));
    controller.setBeltDragPath({BeltPathTile{QPoint(3, 4), Rotation::East}});

    controller.clearHover();

    REQUIRE(controller.isDraggingBelt());
    REQUIRE(controller.getBeltDragPath().size() == 1);
}

TEST_CASE("Clearing the hover drops the blueprint ghost and its transfer", "[buildmode]")
{
    BuildModeController controller;
    controller.enterBlueprintMode(makeBlueprint());
    controller.setBlueprintGhostTile(QPoint(9, 9));
    controller.setHoveredGhostTransfer(true);

    controller.clearHover();

    REQUIRE(controller.isBlueprintMode());
    REQUIRE_FALSE(controller.getBlueprintGhostTile().has_value());
    REQUIRE_FALSE(controller.isHoveredGhostTransfer());
}

TEST_CASE("Clearing the hover drops the deconstruct hover", "[buildmode]")
{
    BuildModeController controller;
    controller.toggleDeconstructMode();
    controller.setDeconstructHoverBuildingId(BuildingId(4));

    controller.clearHover();

    REQUIRE(controller.isDeconstructMode());
    REQUIRE_FALSE(controller.getDeconstructHoverBuildingId().has_value());
}

TEST_CASE("A non-tunnel builder ignores any resolved tunnel end", "[buildmode]")
{
    BuildModeController controller;
    controller.enterBuilderMode(BuildingType::TunnelEntry);
    controller.setTunnelGhost(BuildingType::TunnelExit, QPoint(5, 5));

    controller.enterBuilderMode(BuildingType::Belt);

    REQUIRE(controller.getEffectiveBuilderType() == BuildingType::Belt);
    REQUIRE_FALSE(controller.getTunnelPartnerTile().has_value());
}
