#include "catch.hpp"

#include <memory>

#include "BuildingSystem.h"
#include "Command.h"
#include "CommandManager.h"
#include "ConfigLoader.h"
#include "GameConfig.h"
#include "Rotation.h"
#include "Simulation.h"
#include "SimulationTestAccess.h"
#include "Tick.h"
#include "TestConfig.h"

// The command chokepoint (Simulation::apply) must produce exactly the same state
// as driving the underlying mutators directly — that equivalence is what lets a
// recorded command stream reproduce a live run (see docs/replay_design.md).

TEST_CASE("apply(PlaceBuildingCommand) matches direct placement", "[command]")
{
    Simulation viaCommand(loadTestConfig(), 99);
    Simulation viaDirect(loadTestConfig(), 99);

    PlaceBuildingCommand command;
    command.type     = BuildingType::Miner;
    command.anchor   = QPoint(-3, 0);
    command.rotation = Rotation::East;
    viaCommand.apply(command);

    SimulationTestAccess::place(viaDirect, BuildingType::Miner, QPoint(-3, 0), Rotation::East);

    REQUIRE(viaCommand.computeStateChecksum() == viaDirect.computeStateChecksum());
}

TEST_CASE("apply(PlaceBuildingCommand) with recipe matches place-then-setRecipe", "[command]")
{
    Simulation viaCommand(loadTestConfig(), 99);
    Simulation viaDirect(loadTestConfig(), 99);

    PlaceBuildingCommand command;
    command.type     = BuildingType::Miner;
    command.anchor   = QPoint(-2, 0);
    command.rotation = Rotation::East;
    command.recipeId = "mine_iron_ore";
    viaCommand.apply(command);

    const BuildingId id =
        SimulationTestAccess::place(viaDirect, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
    SimulationTestAccess::buildings(viaDirect).setRecipe(SimulationTestAccess::state(viaDirect), id, "mine_iron_ore");

    REQUIRE(viaCommand.computeStateChecksum() == viaDirect.computeStateChecksum());
}

TEST_CASE("apply(DeconstructCommand) matches direct deconstruct", "[command]")
{
    Simulation viaCommand(loadTestConfig(), 99);
    Simulation viaDirect(loadTestConfig(), 99);

    const BuildingId idA =
        SimulationTestAccess::place(viaCommand, BuildingType::Miner, QPoint(-3, 0), Rotation::East).value();
    const BuildingId idB =
        SimulationTestAccess::place(viaDirect, BuildingType::Miner, QPoint(-3, 0), Rotation::East).value();
    REQUIRE(idA == idB);

    DeconstructCommand command;
    command.id = idA;
    viaCommand.apply(command);

    SimulationTestAccess::deconstruct(viaDirect, idB);

    REQUIRE(viaCommand.computeStateChecksum() == viaDirect.computeStateChecksum());
}

TEST_CASE("apply(CancelDeconstructionCommand) matches direct cancelDeconstruction", "[command]")
{
    Simulation viaCommand(loadTestConfig(), 99);
    Simulation viaDirect(loadTestConfig(), 99);

    const BuildingId idA =
        SimulationTestAccess::place(viaCommand, BuildingType::Miner, QPoint(-3, 0), Rotation::East).value();
    const BuildingId idB =
        SimulationTestAccess::place(viaDirect, BuildingType::Miner, QPoint(-3, 0), Rotation::East).value();
    REQUIRE(idA == idB);

    // Build the miners, then queue both for deconstruction identically.
    for (int i = 0; i < static_cast<int>(secondsToTicks(10.0)) + 1; ++i)
    {
        viaCommand.tick();
        viaDirect.tick();
    }
    SimulationTestAccess::deconstruct(viaCommand, idA);
    SimulationTestAccess::deconstruct(viaDirect, idB);

    CancelDeconstructionCommand command;
    command.id = idA;
    viaCommand.apply(command);

    SimulationTestAccess::cancelDeconstruction(viaDirect, idB);

    REQUIRE(viaCommand.computeStateChecksum() == viaDirect.computeStateChecksum());
}

TEST_CASE("CommandManager drains queued commands in FIFO order through apply", "[command]")
{
    Simulation viaManager(loadTestConfig(), 99);
    Simulation viaDirect(loadTestConfig(), 99);

    CommandManager manager(viaManager);

    std::shared_ptr<PlaceBuildingCommand> first = std::make_shared<PlaceBuildingCommand>();
    first->type   = BuildingType::Miner;
    first->anchor = QPoint(-3, 0);
    std::shared_ptr<PlaceBuildingCommand> second = std::make_shared<PlaceBuildingCommand>();
    second->type   = BuildingType::Belt;
    second->anchor = QPoint(-2, 0);

    manager.enqueue(first);
    manager.enqueue(second);
    REQUIRE(manager.hasPending());

    manager.drain();
    REQUIRE_FALSE(manager.hasPending());

    SimulationTestAccess::place(viaDirect, BuildingType::Miner, QPoint(-3, 0), Rotation::East);
    SimulationTestAccess::place(viaDirect, BuildingType::Belt,  QPoint(-2, 0), Rotation::East);

    REQUIRE(viaManager.computeStateChecksum() == viaDirect.computeStateChecksum());
}
