#include "catch.hpp"

#include <optional>
#include <stdexcept>
#include <vector>

#include <QPoint>
#include <QString>

#include "Blueprint.h"
#include "BlueprintSerializer.h"
#include "BuildingType.h"
#include "ItemType.h"
#include "Rotation.h"
#include "ShipLayout.h"

namespace
{

Blueprint makeBlueprintWith(const std::vector<BlueprintBuilding>& buildings,
                            const QString& name = "Test")
{
    Blueprint bp;
    bp.name      = name;
    bp.buildings = buildings;
    return bp;
}

BlueprintBuilding makeBuilding(BuildingType type,
                               Rotation     rotation,
                               QPoint       offset,
                               std::string  recipeId = "",
                               std::optional<ShipLayoutConfig> shipLayout = std::nullopt)
{
    BlueprintBuilding b;
    b.type      = type;
    b.rotation  = rotation;
    b.offset    = offset;
    b.recipeId  = std::move(recipeId);
    b.shipLayout = std::move(shipLayout);
    return b;
}

} // namespace

// ---------------------------------------------------------------------------
// Case 1 — single blueprint, one building: all fields survive round-trip
// ---------------------------------------------------------------------------

TEST_CASE("BlueprintSerializer: single blueprint round-trips all fields", "[serializer]")
{
    const BlueprintBuilding building =
        makeBuilding(BuildingType::Miner, Rotation::East, QPoint(2, -1), "mine_iron_ore");
    const Blueprint original = makeBlueprintWith({building}, "My Miner");

    const std::string toml = BlueprintSerializer::serialize({original});
    const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);

    REQUIRE(loaded.size() == 1);
    REQUIRE(loaded[0].name == "My Miner");
    REQUIRE(loaded[0].buildings.size() == 1);

    const BlueprintBuilding& b = loaded[0].buildings[0];
    REQUIRE(b.type     == BuildingType::Miner);
    REQUIRE(b.rotation == Rotation::East);
    REQUIRE(b.offset   == QPoint(2, -1));
    REQUIRE(b.recipeId == "mine_iron_ore");
}

// ---------------------------------------------------------------------------
// Case 2 — multiple blueprints: count and order preserved
// ---------------------------------------------------------------------------

TEST_CASE("BlueprintSerializer: multiple blueprints preserve count and order", "[serializer]")
{
    const Blueprint first  = makeBlueprintWith(
        {makeBuilding(BuildingType::Belt,    Rotation::North, QPoint(0, 0))}, "First");
    const Blueprint second = makeBlueprintWith(
        {makeBuilding(BuildingType::Smelter, Rotation::South, QPoint(1, 0))}, "Second");
    const Blueprint third  = makeBlueprintWith(
        {makeBuilding(BuildingType::Assembler, Rotation::West, QPoint(0, 1))}, "Third");

    const std::string toml = BlueprintSerializer::serialize({first, second, third});
    const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);

    REQUIRE(loaded.size() == 3);
    REQUIRE(loaded[0].name == "First");
    REQUIRE(loaded[1].name == "Second");
    REQUIRE(loaded[2].name == "Third");
}

// ---------------------------------------------------------------------------
// Case 3 — all four Rotation values round-trip
// ---------------------------------------------------------------------------

TEST_CASE("BlueprintSerializer: all rotations round-trip", "[serializer]")
{
    const std::vector<Rotation> rotations = {
        Rotation::North, Rotation::East, Rotation::South, Rotation::West
    };

    for (const Rotation rot : rotations)
    {
        const Blueprint bp = makeBlueprintWith(
            {makeBuilding(BuildingType::Belt, rot, QPoint(0, 0))});

        const std::string toml = BlueprintSerializer::serialize({bp});
        const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);

        REQUIRE(loaded.size() == 1);
        REQUIRE(loaded[0].buildings.size() == 1);
        REQUIRE(loaded[0].buildings[0].rotation == rot);
    }
}

// ---------------------------------------------------------------------------
// Case 5 — negative and zero offsets survive intact
// ---------------------------------------------------------------------------

TEST_CASE("BlueprintSerializer: negative and zero offsets round-trip", "[serializer]")
{
    const Blueprint bp = makeBlueprintWith({
        makeBuilding(BuildingType::Belt, Rotation::North, QPoint(0,  0)),
        makeBuilding(BuildingType::Belt, Rotation::North, QPoint(-3, -2)),
        makeBuilding(BuildingType::Belt, Rotation::North, QPoint(5,  4)),
    });

    const std::string toml = BlueprintSerializer::serialize({bp});
    const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);

    REQUIRE(loaded[0].buildings[0].offset == QPoint(0,  0));
    REQUIRE(loaded[0].buildings[1].offset == QPoint(-3, -2));
    REQUIRE(loaded[0].buildings[2].offset == QPoint(5,  4));
}

// ---------------------------------------------------------------------------
// Case 6 — empty and non-empty recipeId both round-trip correctly
// ---------------------------------------------------------------------------

TEST_CASE("BlueprintSerializer: empty and non-empty recipeId round-trip", "[serializer]")
{
    const Blueprint bp = makeBlueprintWith({
        makeBuilding(BuildingType::Miner,    Rotation::North, QPoint(0, 0), "mine_iron_ore"),
        makeBuilding(BuildingType::Assembler, Rotation::North, QPoint(3, 0), ""),
    });

    const std::string toml = BlueprintSerializer::serialize({bp});
    const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);

    REQUIRE(loaded[0].buildings[0].recipeId == "mine_iron_ore");
    REQUIRE(loaded[0].buildings[1].recipeId == "");
}

// ---------------------------------------------------------------------------
// Case 8 — empty blueprint list round-trips to empty vector
// ---------------------------------------------------------------------------

TEST_CASE("BlueprintSerializer: empty list round-trips to empty vector", "[serializer]")
{
    const std::string toml = BlueprintSerializer::serialize({});
    const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);

    REQUIRE(loaded.empty());
}

// ---------------------------------------------------------------------------
// Case 9 — malformed TOML throws std::runtime_error
// ---------------------------------------------------------------------------

TEST_CASE("BlueprintSerializer: malformed TOML throws", "[serializer]")
{
    REQUIRE_THROWS_AS(
        BlueprintSerializer::deserialize("[[blueprint\nname = 42"),
        std::runtime_error);
}

// ---------------------------------------------------------------------------
// Case 10 — unknown building type string throws std::runtime_error
// ---------------------------------------------------------------------------

TEST_CASE("BlueprintSerializer: unknown building type throws", "[serializer]")
{
    const std::string badToml =
        "[[blueprint]]\n"
        "name = \"Broken\"\n"
        "buildings = [{type = \"unicorn\", rotation = \"north\","
        " offset_x = 0, offset_y = 0, recipe_id = \"\"}]\n";

    REQUIRE_THROWS_AS(
        BlueprintSerializer::deserialize(badToml),
        std::runtime_error);
}

// ---------------------------------------------------------------------------
// Ship layout round-trip through TOML serialization
// ---------------------------------------------------------------------------

TEST_CASE("BlueprintSerializer: shipyard with modules round-trips ship layout", "[serializer]")
{
    ShipLayoutConfig layout;
    PlacedModule pmA;
    pmA.moduleId = "laser_cannon";
    pmA.position = QPoint(1, 1);
    pmA.rotation = Rotation::East;
    PlacedModule pmB;
    pmB.moduleId = "sensor_booster";
    pmB.position = QPoint(0, 1);
    pmB.rotation = Rotation::South;
    layout.placedModules = { pmA, pmB };

    const Blueprint original = makeBlueprintWith(
        { makeBuilding(BuildingType::Shipyard, Rotation::East, QPoint(0, 0),
                       "interceptor", layout) },
        "Gunship");

    const std::string toml = BlueprintSerializer::serialize({ original });
    const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);

    REQUIRE(loaded.size() == 1);
    REQUIRE(loaded[0].buildings.size() == 1);

    const BlueprintBuilding& b = loaded[0].buildings[0];
    REQUIRE(b.shipLayout.has_value());
    REQUIRE(b.shipLayout->placedModules.size() == 2);
    REQUIRE(b.shipLayout->placedModules[0].moduleId == "laser_cannon");
    REQUIRE(b.shipLayout->placedModules[0].position == QPoint(1, 1));
    REQUIRE(b.shipLayout->placedModules[0].rotation == Rotation::East);
    REQUIRE(b.shipLayout->placedModules[1].moduleId == "sensor_booster");
    REQUIRE(b.shipLayout->placedModules[1].position == QPoint(0, 1));
    REQUIRE(b.shipLayout->placedModules[1].rotation == Rotation::South);
}

TEST_CASE("BlueprintSerializer: building without ship layout round-trips with nullopt", "[serializer]")
{
    // A shipyard with no shipLayout set — no modules key should be emitted,
    // and deserialization must leave shipLayout as nullopt.
    const Blueprint original = makeBlueprintWith(
        { makeBuilding(BuildingType::Shipyard, Rotation::East, QPoint(0, 0), "interceptor") },
        "EmptyYard");

    const std::string toml = BlueprintSerializer::serialize({ original });
    const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);

    REQUIRE(loaded.size() == 1);
    REQUIRE(loaded[0].buildings.size() == 1);
    REQUIRE_FALSE(loaded[0].buildings[0].shipLayout.has_value());
}

TEST_CASE("BlueprintSerializer: shipyard with empty modules list round-trips", "[serializer]")
{
    // An explicitly empty layout (player cleared all modules) should round-trip
    // as a present but empty ShipLayoutConfig, distinct from nullopt.
    ShipLayoutConfig emptyLayout;
    const Blueprint original = makeBlueprintWith(
        { makeBuilding(BuildingType::Shipyard, Rotation::East, QPoint(0, 0),
                       "interceptor", emptyLayout) },
        "EmptyLayout");

    const std::string toml = BlueprintSerializer::serialize({ original });
    const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);

    REQUIRE(loaded.size() == 1);
    REQUIRE(loaded[0].buildings.size() == 1);
    REQUIRE(loaded[0].buildings[0].shipLayout.has_value());
    REQUIRE(loaded[0].buildings[0].shipLayout->placedModules.empty());
}

// ---------------------------------------------------------------------------
// Splitter output filters round-trip through TOML serialization
// (REQ-UI-BLUEPRINT-STORAGE).
// ---------------------------------------------------------------------------

TEST_CASE("BlueprintSerializer: splitter output filters round-trip", "[serializer]")
{
    BlueprintBuilding splitter =
        makeBuilding(BuildingType::Splitter, Rotation::East, QPoint(0, 0));
    splitter.splitterFilterA = { ItemType{"iron_ore"}, ItemType{"copper_ore"} };
    splitter.splitterFilterB = { ItemType{"scrap"} };

    const Blueprint original = makeBlueprintWith({ splitter }, "Sorter");
    const std::string toml = BlueprintSerializer::serialize({ original });
    const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);

    REQUIRE(loaded.size() == 1);
    REQUIRE(loaded[0].buildings.size() == 1);

    const BlueprintBuilding& b = loaded[0].buildings[0];
    REQUIRE(b.splitterFilterA
            == std::vector<ItemType>{ ItemType{"iron_ore"}, ItemType{"copper_ore"} });
    REQUIRE(b.splitterFilterB == std::vector<ItemType>{ ItemType{"scrap"} });
}

TEST_CASE("BlueprintSerializer: splitter with no filters round-trips empty", "[serializer]")
{
    const Blueprint original = makeBlueprintWith(
        { makeBuilding(BuildingType::Splitter, Rotation::East, QPoint(0, 0)) }, "Open");

    const std::string toml = BlueprintSerializer::serialize({ original });
    const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);

    REQUIRE(loaded.size() == 1);
    REQUIRE(loaded[0].buildings.size() == 1);
    REQUIRE(loaded[0].buildings[0].splitterFilterA.empty());
    REQUIRE(loaded[0].buildings[0].splitterFilterB.empty());
}
