#include "catch.hpp"

#include <algorithm>
#include <vector>

#include "BuildModeController.h"
#include "BuildingType.h"
#include "ControlAction.h"

// REQ-UI-CONTROLS-ACCURACY. The panel and the input handling read one table, and these
// tests are what makes that pay: the round-trip case below fails the moment a row is
// shown whose binding resolves elsewhere, which is the drift the whole design exists to
// prevent. Everything here works on action ids -- the display strings live in the ui
// target and are not what can silently go wrong.

namespace
{

ControlContext generalContext()
{
    return ControlContext();
}

ControlContext selectionContext(bool placeable = true)
{
    ControlContext context;
    context.selection                 = ControlSelection::Buildings;
    context.selectionCount            = 3;
    context.placeableBuildingSelected = placeable;
    return context;
}

ControlContext builderContext(BuildingType type)
{
    ControlContext context;
    context.mode        = BuildMode::Builder;
    context.builderType = type;
    return context;
}

ControlContext blueprintContext(bool transfer = false)
{
    ControlContext context;
    context.mode                   = BuildMode::Blueprint;
    context.hoveredGhostIsTransfer = transfer;
    return context;
}

ControlContext deconstructContext()
{
    ControlContext context;
    context.mode = BuildMode::Deconstruct;
    return context;
}

// A spread wide enough that every availability rule and every binding condition is
// exercised by the whole-table properties below.
std::vector<ControlContext> allContexts()
{
    ControlContext beltDragging = builderContext(BuildingType::Belt);
    beltDragging.draggingBelt   = true;

    ControlContext generalWithBlueprint            = generalContext();
    generalWithBlueprint.temporaryBlueprintExists  = true;

    ControlContext fieldSelection      = selectionContext(false);
    fieldSelection.selection           = ControlSelection::FieldObjects;

    return {generalContext(),
            generalWithBlueprint,
            selectionContext(),
            fieldSelection,
            builderContext(BuildingType::Belt),
            builderContext(BuildingType::Assembler),
            beltDragging,
            blueprintContext(false),
            blueprintContext(true),
            deconstructContext()};
}

std::vector<ControlAction> shownActions(const ControlContext& context)
{
    std::vector<ControlAction> actions = getContextActions(context);
    const std::vector<ControlAction> always = getAlwaysAvailableActions(context);
    actions.insert(actions.end(), always.begin(), always.end());
    return actions;
}

bool contains(const std::vector<ControlAction>& actions, ControlAction action)
{
    return std::find(actions.begin(), actions.end(), action) != actions.end();
}

}  // namespace

TEST_CASE("ControlAction: every shown row is available", "[controls]")
{
    for (const ControlContext& context : allContexts())
    {
        for (ControlAction action : shownActions(context))
        {
            REQUIRE(isControlActionAvailable(action, context));
        }
    }
}

TEST_CASE("ControlAction: no row is shown twice", "[controls]")
{
    for (const ControlContext& context : allContexts())
    {
        std::vector<ControlAction> actions = shownActions(context);
        std::vector<ControlAction> unique  = actions;
        std::sort(unique.begin(), unique.end());
        unique.erase(std::unique(unique.begin(), unique.end()), unique.end());
        REQUIRE(unique.size() == actions.size());
    }
}

// The core anti-drift property: a row's badges are rendered from its bindings, so every
// binding a row advertises must actually trigger that row's action in that same context.
TEST_CASE("ControlAction: every advertised binding resolves back to its own action",
          "[controls]")
{
    for (const ControlContext& context : allContexts())
    {
        for (ControlAction action : shownActions(context))
        {
            const std::vector<ControlBinding> bindings =
                getControlActionBindings(action, context);
            REQUIRE_FALSE(bindings.empty());

            for (const ControlBinding& binding : bindings)
            {
                if (binding.isMouse)
                {
                    REQUIRE(resolveMouseAction(binding.mouse, context) == action);
                }
                else
                {
                    REQUIRE(resolveKeyAction(binding.key, binding.modifiers, context)
                            == action);
                }
            }
        }
    }
}

// The other direction: an input that resolves to something must resolve to an action
// that is actually available there, so no input can trigger a no-op. This is weaker
// than "must be a shown row" on purpose -- a context may omit an available binding
// (Ctrl+click in the General context), which REQ-UI-CONTROLS-ACCURACY permits; what it
// may never do is act on an unavailable one.
TEST_CASE("ControlAction: every resolvable input is available", "[controls]")
{
    const std::vector<MouseBinding> mouseBindings = {
        MouseBinding::LeftClick, MouseBinding::LeftDrag, MouseBinding::CtrlLeftClick,
        MouseBinding::CtrlLeftDrag, MouseBinding::RightClick};

    for (const ControlContext& context : allContexts())
    {
        for (MouseBinding binding : mouseBindings)
        {
            const ControlAction action = resolveMouseAction(binding, context);
            if (action != ControlAction::None)
            {
                REQUIRE(isControlActionAvailable(action, context));
            }
        }

        const std::vector<int> keys = {Qt::Key_A, Qt::Key_D, Qt::Key_W, Qt::Key_S,
                                       Qt::Key_Space, Qt::Key_C, Qt::Key_V, Qt::Key_R,
                                       Qt::Key_Q, Qt::Key_Escape};
        for (int key : keys)
        {
            for (Qt::KeyboardModifiers modifiers :
                 {Qt::KeyboardModifiers(Qt::NoModifier),
                  Qt::KeyboardModifiers(Qt::ShiftModifier),
                  Qt::KeyboardModifiers(Qt::ControlModifier)})
            {
                const ControlAction action = resolveKeyAction(key, modifiers, context);
                if (action != ControlAction::None)
                {
                    REQUIRE(isControlActionAvailable(action, context));
                }
            }
        }
    }
}

TEST_CASE("ControlAction: contexts are named by mode and selection", "[controls]")
{
    REQUIRE(getControlContextKind(generalContext()) == ControlContextKind::General);
    REQUIRE(getControlContextKind(selectionContext()) == ControlContextKind::Selection);
    REQUIRE(getControlContextKind(builderContext(BuildingType::Belt))
            == ControlContextKind::Build);
    REQUIRE(getControlContextKind(blueprintContext()) == ControlContextKind::Blueprint);
    REQUIRE(getControlContextKind(deconstructContext())
            == ControlContextKind::Deconstruct);
}

// The three cases of Q, in the order REQ-UI-HOTKEYS evaluates them.
TEST_CASE("ControlAction: Q leaves the active mode, else clears, else deconstructs",
          "[controls]")
{
    REQUIRE(resolveKeyAction(Qt::Key_Q, Qt::NoModifier, builderContext(BuildingType::Belt))
            == ControlAction::ExitMode);
    REQUIRE(resolveKeyAction(Qt::Key_Q, Qt::NoModifier, blueprintContext())
            == ControlAction::ExitMode);
    REQUIRE(resolveKeyAction(Qt::Key_Q, Qt::NoModifier, deconstructContext())
            == ControlAction::ExitMode);

    // Whatever the selection holds: clearing it is not a buildings-only action.
    ControlContext fieldSelection = selectionContext(false);
    fieldSelection.selection      = ControlSelection::FieldObjects;
    REQUIRE(resolveKeyAction(Qt::Key_Q, Qt::NoModifier, selectionContext())
            == ControlAction::ClearSelection);
    REQUIRE(resolveKeyAction(Qt::Key_Q, Qt::NoModifier, fieldSelection)
            == ControlAction::ClearSelection);

    // Only with nothing to clear and no mode to leave does Q enter deconstruct mode --
    // which is what makes it two presses from a selection.
    REQUIRE(resolveKeyAction(Qt::Key_Q, Qt::NoModifier, generalContext())
            == ControlAction::EnterDeconstruct);
}

TEST_CASE("ControlAction: Ctrl distinguishes the chords, other modifiers do not",
          "[controls]")
{
    const ControlContext context = selectionContext();

    REQUIRE(resolveKeyAction(Qt::Key_C, Qt::NoModifier, context)
            == ControlAction::CopyTemporary);
    REQUIRE(resolveKeyAction(Qt::Key_C, Qt::ControlModifier, context)
            == ControlAction::CreateBlueprint);
    REQUIRE(resolveKeyAction(Qt::Key_V, Qt::ControlModifier, context)
            == ControlAction::OpenBlueprints);

    // Shift+A must still pan, as it always has -- matching modifiers exactly instead of
    // testing Ctrl alone would silently swallow the press.
    REQUIRE(resolveKeyAction(Qt::Key_A, Qt::ShiftModifier, context) == ControlAction::Move);
    REQUIRE(resolveKeyAction(Qt::Key_R, Qt::ShiftModifier, blueprintContext())
            == ControlAction::Rotate);
}

TEST_CASE("ControlAction: a transfer target turns the click into Apply settings",
          "[controls]")
{
    const ControlContext plain    = blueprintContext(false);
    const ControlContext transfer = blueprintContext(true);

    REQUIRE(resolveMouseAction(MouseBinding::LeftClick, plain) == ControlAction::Place);
    REQUIRE(resolveMouseAction(MouseBinding::LeftClick, transfer)
            == ControlAction::ApplySettings);

    REQUIRE(contains(getContextActions(plain), ControlAction::Place));
    REQUIRE_FALSE(contains(getContextActions(plain), ControlAction::ApplySettings));
    REQUIRE(contains(getContextActions(transfer), ControlAction::ApplySettings));
    REQUIRE_FALSE(contains(getContextActions(transfer), ControlAction::Place));
}

TEST_CASE("ControlAction: a belt drag takes the right mouse button from ExitMode",
          "[controls]")
{
    ControlContext dragging   = builderContext(BuildingType::Belt);
    dragging.draggingBelt     = true;
    const ControlContext idle = builderContext(BuildingType::Belt);

    REQUIRE(resolveMouseAction(MouseBinding::RightClick, idle) == ControlAction::ExitMode);
    REQUIRE(resolveMouseAction(MouseBinding::RightClick, dragging)
            == ControlAction::CancelBeltLine);

    // Q still leaves the mode outright while the drag runs, so ExitMode stays shown --
    // with its right-click badge dropped, since the button now means something else.
    REQUIRE(resolveKeyAction(Qt::Key_Q, Qt::NoModifier, dragging) == ControlAction::ExitMode);

    const std::vector<ControlBinding> idleBindings =
        getControlActionBindings(ControlAction::ExitMode, idle);
    const std::vector<ControlBinding> dragBindings =
        getControlActionBindings(ControlAction::ExitMode, dragging);
    REQUIRE(idleBindings.size() == 2);
    REQUIRE(dragBindings.size() == 1);
    REQUIRE_FALSE(dragBindings.front().isMouse);
}

TEST_CASE("ControlAction: dragging a line is offered for belts only", "[controls]")
{
    REQUIRE(resolveMouseAction(MouseBinding::LeftDrag, builderContext(BuildingType::Belt))
            == ControlAction::PlaceBeltLine);
    REQUIRE(resolveMouseAction(MouseBinding::LeftDrag,
                               builderContext(BuildingType::Assembler))
            == ControlAction::None);
    REQUIRE(resolveMouseAction(MouseBinding::LeftDrag, blueprintContext())
            == ControlAction::None);
}

TEST_CASE("ControlAction: blueprint keys are offered only where they do something",
          "[controls]")
{
    // V does nothing until C has captured something (REQ-UI-BLUEPRINT-TEMP).
    ControlContext withBlueprint                  = generalContext();
    withBlueprint.temporaryBlueprintExists        = true;
    REQUIRE_FALSE(contains(shownActions(generalContext()), ControlAction::PasteTemporary));
    REQUIRE(contains(shownActions(withBlueprint), ControlAction::PasteTemporary));
    REQUIRE(resolveKeyAction(Qt::Key_V, Qt::NoModifier, generalContext())
            == ControlAction::None);

    // C and Ctrl+C need a selection holding something placeable (REQ-UI-HOTKEYS).
    ControlContext fieldSelection = selectionContext(false);
    fieldSelection.selection      = ControlSelection::FieldObjects;
    REQUIRE(contains(getContextActions(selectionContext()), ControlAction::CopyTemporary));
    REQUIRE_FALSE(contains(getContextActions(fieldSelection), ControlAction::CopyTemporary));
    REQUIRE(resolveKeyAction(Qt::Key_C, Qt::NoModifier, fieldSelection)
            == ControlAction::None);

    // Ctrl+V opens the dialog whatever is going on (REQ-UI-HOTKEYS).
    for (const ControlContext& context : allContexts())
    {
        REQUIRE(resolveKeyAction(Qt::Key_V, Qt::ControlModifier, context)
                == ControlAction::OpenBlueprints);
    }
}

TEST_CASE("ControlAction: selection rows appear only once something is selected",
          "[controls]")
{
    const std::vector<ControlAction> general   = getContextActions(generalContext());
    const std::vector<ControlAction> selection = getContextActions(selectionContext());

    REQUIRE(contains(general, ControlAction::Select));
    REQUIRE(contains(general, ControlAction::EnterDeconstruct));
    REQUIRE_FALSE(contains(general, ControlAction::AddToSelection));
    REQUIRE_FALSE(contains(general, ControlAction::CreateBlueprint));
    REQUIRE_FALSE(contains(general, ControlAction::ClearSelection));

    REQUIRE(contains(selection, ControlAction::AddToSelection));
    REQUIRE(contains(selection, ControlAction::AddAreaToSelection));
    REQUIRE(contains(selection, ControlAction::CreateBlueprint));
    // Q clears here instead of entering deconstruct mode, and its row sits last, as the
    // row that hands the context back does in every context (REQ-UI-CONTROLS-CONTENT).
    REQUIRE(contains(selection, ControlAction::ClearSelection));
    REQUIRE_FALSE(contains(selection, ControlAction::EnterDeconstruct));
    REQUIRE(selection.back() == ControlAction::ClearSelection);
}
