#include "catch.hpp"
#include "FactoryQueries.h"

#include <algorithm>
#include <climits>
#include <vector>

#include <QPoint>

#include "Blueprint.h"
#include "Building.h"
#include "BuildingConfig.h"
#include "BuildingsConfig.h"
#include "BuildingSystem.h"
#include "BuildingType.h"
#include "ConfigLoader.h"
#include "BuildingId.h"
#include "Rotation.h"
#include "ShipLayout.h"
#include "Simulation.h"
#include "SimulationTestAccess.h"
#include "SurfaceMask.h"
#include "Tick.h"
#include "TestConfig.h"

// ---------------------------------------------------------------------------
// Helpers that mirror the production implementations under test.
// ---------------------------------------------------------------------------

// Mirror of the CW / CCW offset transforms in GameWorldView::keyPressEvent.
static QPoint rotateCW(QPoint p)  { return QPoint(-p.y(),  p.x()); }
static QPoint rotateCCW(QPoint p) { return QPoint( p.y(), -p.x()); }

// Mirror of the Rotation cycling in GameWorldView (anonymous-namespace helpers).
static Rotation rotCW(Rotation r)
{
    switch (r)
    {
    case Rotation::North: return Rotation::East;
    case Rotation::East:  return Rotation::South;
    case Rotation::South: return Rotation::West;
    case Rotation::West:  return Rotation::North;
    }
    return Rotation::East;
}

static Rotation rotCCW(Rotation r)
{
    switch (r)
    {
    case Rotation::North: return Rotation::West;
    case Rotation::East:  return Rotation::North;
    case Rotation::South: return Rotation::East;
    case Rotation::West:  return Rotation::South;
    }
    return Rotation::East;
}

// Mirror of BlueprintLibrary::createBlueprintFromSelection: given per-building
// (anchor, bodyCells, type, rotation), compute Blueprint with floor-division
// bounding-box center and per-building tile offsets.
struct BuildingSpec
{
    QPoint              anchor;
    std::vector<QPoint> bodyCells;
    BuildingType        type;
    Rotation            rotation;
    std::string         recipeId;   // empty = none
    std::optional<ShipLayoutConfig> shipLayout;
};

static Blueprint buildBlueprint(const std::vector<BuildingSpec>& specs)
{
    int minX = INT_MAX, maxX = INT_MIN;
    int minY = INT_MAX, maxY = INT_MIN;
    for (const BuildingSpec& s : specs)
    {
        for (const QPoint& cell : s.bodyCells)
        {
            minX = std::min(minX, cell.x());
            maxX = std::max(maxX, cell.x());
            minY = std::min(minY, cell.y());
            maxY = std::max(maxY, cell.y());
        }
    }
    const QPoint center((minX + maxX) / 2, (minY + maxY) / 2);

    Blueprint bp;
    for (const BuildingSpec& s : specs)
    {
        BlueprintBuilding bb;
        bb.type       = s.type;
        bb.rotation   = s.rotation;
        bb.offset     = s.anchor - center;
        bb.recipeId   = s.recipeId;
        bb.shipLayout = s.shipLayout;
        bp.buildings.push_back(bb);
    }
    return bp;
}

// Apply one CW/CCW rotation to every building in the constellation, mirroring
// the corrected Q / E handling in GameWorldView::keyPressEvent.
// Each building's anchor is recomputed from the rotated body cells so that
// multi-tile footprints stay correctly aligned after rotation.
static void applyRotationCW(Blueprint& bp, const GameConfig& cfg)
{
    for (BlueprintBuilding& bb : bp.buildings)
    {
        const BuildingDef* def = nullptr;
        for (const BuildingDef& d : cfg.buildings.buildings)
        {
            if (d.type == bb.type) { def = &d; break; }
        }
        if (!def) { continue; }
        const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, bb.rotation);
        int minX = INT_MAX, minY = INT_MAX;
        for (const QPoint& cell : mask.bodyCells)
        {
            const QPoint abs = bb.offset + cell;
            minX = std::min(minX, -abs.y());
            minY = std::min(minY,  abs.x());
        }
        bb.offset   = QPoint(minX, minY);
        bb.rotation = rotCW(bb.rotation);
    }
}

static void applyRotationCCW(Blueprint& bp, const GameConfig& cfg)
{
    for (BlueprintBuilding& bb : bp.buildings)
    {
        const BuildingDef* def = nullptr;
        for (const BuildingDef& d : cfg.buildings.buildings)
        {
            if (d.type == bb.type) { def = &d; break; }
        }
        if (!def) { continue; }
        const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, bb.rotation);
        int minX = INT_MAX, minY = INT_MAX;
        for (const QPoint& cell : mask.bodyCells)
        {
            const QPoint abs = bb.offset + cell;
            minX = std::min(minX,  abs.y());
            minY = std::min(minY, -abs.x());
        }
        bb.offset   = QPoint(minX, minY);
        bb.rotation = rotCCW(bb.rotation);
    }
}

// Mirrors BlueprintLibrary::createBlueprintFromSelection's player-placeable filter:
// building types absent from buildings.toml (HQ, stations) or with playerPlaceable=false
// are silently excluded before the bounding-box center and offsets are computed.
static Blueprint buildBlueprintFiltered(const std::vector<BuildingSpec>& specs,
                                         const GameConfig& cfg)
{
    std::vector<BuildingSpec> filtered;
    for (const BuildingSpec& s : specs)
    {
        for (const BuildingDef& def : cfg.buildings.buildings)
        {
            if (def.type == s.type)
            {
                if (def.playerPlaceable) { filtered.push_back(s); }
                break;
            }
        }
        // If the type is not in buildings (e.g. Hq, defence stations), it is skipped.
    }
    return buildBlueprint(filtered);
}

// ---------------------------------------------------------------------------
// Offset computation
// ---------------------------------------------------------------------------

TEST_CASE("Blueprint: single 1x1 building gets zero offset", "[blueprint]")
{
    // Body = one tile at (-5, 3). Center = (-5, 3). Offset = (0, 0).
    const BuildingSpec spec{ QPoint(-5, 3), {QPoint(-5, 3)}, BuildingType::Belt, Rotation::East };
    const Blueprint bp = buildBlueprint({ spec });

    REQUIRE(bp.buildings.size() == 1);
    REQUIRE(bp.buildings[0].offset == QPoint(0, 0));
}

TEST_CASE("Blueprint: two 1x1 buildings with odd span get symmetric offsets", "[blueprint]")
{
    // Anchors at (-6, 0) and (-4, 0). bboxX = [-6, -4], center.x = -5.
    // Offsets: -6 - (-5) = -1,  -4 - (-5) = +1.
    const BuildingSpec left { QPoint(-6, 0), {QPoint(-6, 0)}, BuildingType::Belt, Rotation::East };
    const BuildingSpec right{ QPoint(-4, 0), {QPoint(-4, 0)}, BuildingType::Belt, Rotation::East };
    const Blueprint bp = buildBlueprint({ left, right });

    REQUIRE(bp.buildings[0].offset == QPoint(-1, 0));
    REQUIRE(bp.buildings[1].offset == QPoint( 1, 0));
}

TEST_CASE("Blueprint: even span uses C++ integer truncation for center", "[blueprint]")
{
    // Anchors at (-5, 0) and (-4, 0). bboxX = [-5, -4], sum = -9.
    // center.x = -9 / 2 = -4  (C++ truncates toward zero, i.e. rounds up for negatives).
    // Offsets: -5 - (-4) = -1,  -4 - (-4) = 0.
    const BuildingSpec left { QPoint(-5, 0), {QPoint(-5, 0)}, BuildingType::Belt, Rotation::East };
    const BuildingSpec right{ QPoint(-4, 0), {QPoint(-4, 0)}, BuildingType::Belt, Rotation::East };
    const Blueprint bp = buildBlueprint({ left, right });

    REQUIRE(bp.buildings[0].offset == QPoint(-1, 0));
    REQUIRE(bp.buildings[1].offset == QPoint( 0, 0));
}

TEST_CASE("Blueprint: bounding box is computed from body cells, not anchors alone", "[blueprint]")
{
    // A 2x1 building: anchor (-6, 0), body cells (-6, 0) and (-5, 0).
    // A 1x1 building: anchor (-3, 0), body cell (-3, 0).
    // All body cells: {(-6,0),(-5,0),(-3,0)}.  bboxX = [-6, -3], sum = -9.
    // center.x = -9 / 2 = -4 (C++ truncation).
    // Wide building anchor offset: -6 - (-4) = -2.
    // Small building anchor offset: -3 - (-4) = +1.
    const BuildingSpec wide{
        QPoint(-6, 0),
        { QPoint(-6, 0), QPoint(-5, 0) },
        BuildingType::Belt,
        Rotation::East
    };
    const BuildingSpec small{
        QPoint(-3, 0),
        { QPoint(-3, 0) },
        BuildingType::Belt,
        Rotation::East
    };
    const Blueprint bp = buildBlueprint({ wide, small });

    REQUIRE(bp.buildings[0].offset == QPoint(-2, 0));
    REQUIRE(bp.buildings[1].offset == QPoint( 1, 0));
}

TEST_CASE("Blueprint: 2-D bounding box with buildings on both axes", "[blueprint]")
{
    // Four 1x1 buildings at corners of a 4x2 rectangle (-7,0),(-5,0),(-7,2),(-5,2).
    // bboxX = [-7, -5], center.x = -6.  bboxY = [0, 2], center.y = 1.
    // Offsets: (-1,-1), (1,-1), (-1,1), (1,1).
    const auto belt = BuildingType::Belt;
    const auto east = Rotation::East;
    std::vector<BuildingSpec> specs = {
        { QPoint(-7, 0), {QPoint(-7, 0)}, belt, east },
        { QPoint(-5, 0), {QPoint(-5, 0)}, belt, east },
        { QPoint(-7, 2), {QPoint(-7, 2)}, belt, east },
        { QPoint(-5, 2), {QPoint(-5, 2)}, belt, east },
    };
    const Blueprint bp = buildBlueprint(specs);

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

// ---------------------------------------------------------------------------
// Offset rotation math
// ---------------------------------------------------------------------------

TEST_CASE("Blueprint: CW rotation in screen space maps right→down, down→left", "[blueprint]")
{
    // Screen space has Y growing downward.  CW means: right→down, down→left, left→up, up→right.
    REQUIRE(rotateCW(QPoint( 1,  0)) == QPoint( 0,  1));
    REQUIRE(rotateCW(QPoint( 0,  1)) == QPoint(-1,  0));
    REQUIRE(rotateCW(QPoint(-1,  0)) == QPoint( 0, -1));
    REQUIRE(rotateCW(QPoint( 0, -1)) == QPoint( 1,  0));
}

TEST_CASE("Blueprint: CCW rotation is the inverse of CW rotation", "[blueprint]")
{
    REQUIRE(rotateCCW(QPoint( 1,  0)) == QPoint( 0, -1));
    REQUIRE(rotateCCW(QPoint( 0, -1)) == QPoint(-1,  0));
    REQUIRE(rotateCCW(QPoint(-1,  0)) == QPoint( 0,  1));
    REQUIRE(rotateCCW(QPoint( 0,  1)) == QPoint( 1,  0));
}

TEST_CASE("Blueprint: four CW rotations restore any offset to its original", "[blueprint]")
{
    const QPoint original(-3, 5);
    QPoint p = original;
    for (int i = 0; i < 4; ++i) { p = rotateCW(p); }
    REQUIRE(p == original);
}

TEST_CASE("Blueprint: CW followed by CCW is identity", "[blueprint]")
{
    const QPoint original(2, -7);
    REQUIRE(rotateCCW(rotateCW(original)) == original);
    REQUIRE(rotateCW(rotateCCW(original)) == original);
}

TEST_CASE("Blueprint: non-axis-aligned offset rotates correctly", "[blueprint]")
{
    // (2, 3) → CW → (-3, 2) → CW → (-2, -3) → CW → (3, -2) → CW → (2, 3)
    QPoint p(2, 3);
    p = rotateCW(p);  REQUIRE(p == QPoint(-3,  2));
    p = rotateCW(p);  REQUIRE(p == QPoint(-2, -3));
    p = rotateCW(p);  REQUIRE(p == QPoint( 3, -2));
    p = rotateCW(p);  REQUIRE(p == QPoint( 2,  3));
}

// ---------------------------------------------------------------------------
// Constellation rotation: offsets AND building rotations both update
// ---------------------------------------------------------------------------

TEST_CASE("Blueprint: CW constellation rotation updates offset and building rotation", "[blueprint]")
{
    const GameConfig cfg = loadTestConfig();
    // Building one tile to the right, facing East.
    Blueprint bp;
    bp.name = "test";
    BlueprintBuilding bb;
    bb.type     = BuildingType::Belt;
    bb.rotation = Rotation::East;
    bb.offset   = QPoint(1, 0);
    bp.buildings.push_back(bb);

    applyRotationCW(bp, cfg);

    // Offset: right → down.
    REQUIRE(bp.buildings[0].offset == QPoint(0, 1));
    // Building rotation: East → South.
    REQUIRE(bp.buildings[0].rotation == Rotation::South);
}

TEST_CASE("Blueprint: CCW constellation rotation updates offset and building rotation", "[blueprint]")
{
    const GameConfig cfg = loadTestConfig();
    Blueprint bp;
    bp.name = "test";
    BlueprintBuilding bb;
    bb.type     = BuildingType::Belt;
    bb.rotation = Rotation::East;
    bb.offset   = QPoint(1, 0);
    bp.buildings.push_back(bb);

    applyRotationCCW(bp, cfg);

    // Offset: right → up.
    REQUIRE(bp.buildings[0].offset == QPoint(0, -1));
    // Building rotation: East → North.
    REQUIRE(bp.buildings[0].rotation == Rotation::North);
}

TEST_CASE("Blueprint: four CW rotations restore offset and building rotation", "[blueprint]")
{
    const GameConfig cfg = loadTestConfig();
    Blueprint bp;
    bp.name = "test";
    BlueprintBuilding bb;
    bb.type     = BuildingType::Belt;
    bb.rotation = Rotation::East;
    bb.offset   = QPoint(2, -1);
    bp.buildings.push_back(bb);

    const QPoint   originalOffset   = bb.offset;
    const Rotation originalRotation = bb.rotation;

    for (int i = 0; i < 4; ++i) { applyRotationCW(bp, cfg); }

    REQUIRE(bp.buildings[0].offset   == originalOffset);
    REQUIRE(bp.buildings[0].rotation == originalRotation);
}

TEST_CASE("Blueprint: multi-building constellation rotates symmetrically CW", "[blueprint]")
{
    const GameConfig cfg = loadTestConfig();
    // Two buildings left and right of center; after CW they should be above and below.
    Blueprint bp;
    bp.name = "test";
    BlueprintBuilding left, right;
    left.type  = right.type  = BuildingType::Belt;
    left.rotation = right.rotation = Rotation::East;
    left.offset  = QPoint(-1, 0);
    right.offset = QPoint( 1, 0);
    bp.buildings = { left, right };

    applyRotationCW(bp, cfg);

    // left  (-1, 0) → CW → (0, -1)  (above center)
    // right ( 1, 0) → CW → (0,  1)  (below center)
    REQUIRE(bp.buildings[0].offset == QPoint(0, -1));
    REQUIRE(bp.buildings[1].offset == QPoint(0,  1));
    REQUIRE(bp.buildings[0].rotation == Rotation::South);
    REQUIRE(bp.buildings[1].rotation == Rotation::South);
}

// ---------------------------------------------------------------------------
// Regression: multi-tile building rotation (the anchor-offset-only bug)
// ---------------------------------------------------------------------------

TEST_CASE("Blueprint: CW rotation keeps belt adjacent to miner output port", "[blueprint]")
{
    const GameConfig cfg = loadTestConfig();

    // East miner: anchor (0,0), body cells (0,0),(1,0),(0,1).
    // Output port indicator '>' at (1,1) → port tile (1,1), direction East.
    // Belt placed at (1,1) — the port exit tile.
    // Constellation body cells: {(0,0),(1,0),(0,1),(1,1)}.
    // Integer center: ((0+1)/2, (0+1)/2) = (0,0).
    // Miner offset (0,0), belt offset (1,1).
    Blueprint bp;
    bp.name = "test";
    BlueprintBuilding miner;
    miner.type     = BuildingType::Miner;
    miner.rotation = Rotation::East;
    miner.offset   = QPoint(0, 0);
    BlueprintBuilding belt;
    belt.type     = BuildingType::Belt;
    belt.rotation = Rotation::East;
    belt.offset   = QPoint(1, 1);   // port exit tile of East miner relative to its anchor
    bp.buildings  = { miner, belt };

    applyRotationCW(bp, cfg);

    // Miner body cells (0,0),(1,0),(0,1) rotated CW: (0,0),(0,1),(-1,0).
    // New miner anchor = min(-1..0, 0..1) = (-1, 0).
    REQUIRE(bp.buildings[0].offset   == QPoint(-1, 0));
    REQUIRE(bp.buildings[0].rotation == Rotation::South);

    // Belt body cell (1,1) rotated CW: (-1, 1). New belt anchor = (-1, 1).
    REQUIRE(bp.buildings[1].offset   == QPoint(-1, 1));
    REQUIRE(bp.buildings[1].rotation == Rotation::South);

    // South miner port tile relative to its anchor is (0,1) (the 'v' indicator in ["AA","vA"]).
    // Miner anchor in constellation = (-1,0). Port exit = (-1,0)+(0,1) = (-1,1) = belt anchor. ✓
    REQUIRE(bp.buildings[1].offset == bp.buildings[0].offset + QPoint(0, 1));
}

TEST_CASE("Blueprint: CCW rotation keeps belt adjacent to miner output port", "[blueprint]")
{
    const GameConfig cfg = loadTestConfig();

    Blueprint bp;
    bp.name = "test";
    BlueprintBuilding miner;
    miner.type     = BuildingType::Miner;
    miner.rotation = Rotation::East;
    miner.offset   = QPoint(0, 0);
    BlueprintBuilding belt;
    belt.type     = BuildingType::Belt;
    belt.rotation = Rotation::East;
    belt.offset   = QPoint(1, 1);
    bp.buildings  = { miner, belt };

    applyRotationCCW(bp, cfg);

    // Miner body cells (0,0),(1,0),(0,1) rotated CCW: (0,0),(0,-1),(1,0).
    // New miner anchor = min(0..1, -1..0) = (0, -1).
    REQUIRE(bp.buildings[0].offset   == QPoint(0, -1));
    REQUIRE(bp.buildings[0].rotation == Rotation::North);

    // Belt body cell (1,1) rotated CCW: (1,-1). New belt anchor = (1,-1).
    REQUIRE(bp.buildings[1].offset   == QPoint(1, -1));
    REQUIRE(bp.buildings[1].rotation == Rotation::North);

    // North miner port tile relative to its anchor is (1,0) (the '^' indicator in ["A^","AA"]).
    // Miner anchor in constellation = (0,-1). Port exit = (0,-1)+(1,0) = (1,-1) = belt anchor. ✓
    REQUIRE(bp.buildings[1].offset == bp.buildings[0].offset + QPoint(1, 0));
}

// ---------------------------------------------------------------------------
// Player-placeable filter in blueprint creation
// ---------------------------------------------------------------------------

TEST_CASE("Blueprint creation: non-player-placeable building alone yields empty blueprint",
          "[blueprint]")
{
    const GameConfig cfg = loadTestConfig();

    // Hq has no entry in buildings.toml, so it is treated as non-player-placeable.
    const BuildingSpec hq{ QPoint(-5, 0), {QPoint(-5, 0)}, BuildingType::Hq, Rotation::East };
    const Blueprint bp = buildBlueprintFiltered({ hq }, cfg);

    REQUIRE(bp.buildings.empty());
}

TEST_CASE("Blueprint creation: mixed selection keeps only player-placeable buildings",
          "[blueprint]")
{
    const GameConfig cfg = loadTestConfig();

    const BuildingSpec belt{ QPoint(-5, 0), {QPoint(-5, 0)}, BuildingType::Belt, Rotation::East };
    const BuildingSpec hq  { QPoint(-3, 0), {QPoint(-3, 0)}, BuildingType::Hq,   Rotation::East };
    const Blueprint bp = buildBlueprintFiltered({ belt, hq }, cfg);

    REQUIRE(bp.buildings.size() == 1);
    REQUIRE(bp.buildings[0].type == BuildingType::Belt);
}

TEST_CASE("Blueprint creation: bounding box ignores non-player-placeable buildings",
          "[blueprint]")
{
    const GameConfig cfg = loadTestConfig();

    // Belt at (-5, 0).  HQ at (-3, 0) — excluded from the blueprint.
    // If HQ were included: bboxX = [-5, -3], center.x = -4, belt offset = -1.
    // With HQ excluded:   bboxX = [-5, -5], center.x = -5, belt offset =  0.
    const BuildingSpec belt{ QPoint(-5, 0), {QPoint(-5, 0)}, BuildingType::Belt, Rotation::East };
    const BuildingSpec hq  { QPoint(-3, 0), {QPoint(-3, 0)}, BuildingType::Hq,   Rotation::East };
    const Blueprint bp = buildBlueprintFiltered({ belt, hq }, cfg);

    REQUIRE(bp.buildings.size() == 1);
    REQUIRE(bp.buildings[0].offset == QPoint(0, 0));
}

// ---------------------------------------------------------------------------
// Simulation-level: blueprint placement places buildings at correct tiles
// ---------------------------------------------------------------------------

TEST_CASE("Blueprint placement: buildings land at anchor + offset from cursor", "[blueprint]")
{
    // Simulate placing a two-belt blueprint with offsets (-1, 0) and (+1, 0)
    // at cursor tile (-5, 0).  Expected anchors: (-6, 0) and (-4, 0).
    // (Belt surface_mask ["A>"] — body at relative (0,0), port at (1,0).)
    Simulation sim(loadTestConfig());

    const QPoint cursor(-5, 0);
    const QPoint offsetA(-1, 0);
    const QPoint offsetB( 1, 0);

    const BuildingId idA = SimulationTestAccess::place(sim,
        BuildingType::Belt, cursor + offsetA, Rotation::East).value();
    const BuildingId idB = SimulationTestAccess::place(sim,
        BuildingType::Belt, cursor + offsetB, Rotation::East).value();

    REQUIRE(idA != kInvalidBuildingId);
    REQUIRE(idB != kInvalidBuildingId);
    REQUIRE(isTileOccupied(sim.getFactoryState(), cursor + offsetA));   // (-6, 0)
    REQUIRE(isTileOccupied(sim.getFactoryState(), cursor + offsetB));   // (-4, 0)
    REQUIRE_FALSE(isTileOccupied(sim.getFactoryState(), cursor));       // center not occupied
}

TEST_CASE("Blueprint placement: cost is deducted for each building in sequence", "[blueprint]")
{
    Simulation sim(loadTestConfig());

    // Find belt cost from config (belt cost = 2 in test config).
    int beltCost = 0;
    for (const BuildingDef& def : sim.getConfig().buildings.buildings)
    {
        if (def.type == BuildingType::Belt) { beltCost = def.cost; break; }
    }
    REQUIRE(beltCost > 0);

    const int startBlocks = sim.getBuildingBlocksStock();
    REQUIRE(startBlocks >= 2 * beltCost);   // test config has enough starting blocks

    SimulationTestAccess::place(sim,BuildingType::Belt, QPoint(-6, 0), Rotation::East);
    REQUIRE(sim.getBuildingBlocksStock() == startBlocks - beltCost);

    SimulationTestAccess::place(sim,BuildingType::Belt, QPoint(-4, 0), Rotation::East);
    REQUIRE(sim.getBuildingBlocksStock() == startBlocks - 2 * beltCost);
}

TEST_CASE("Blueprint placement: insufficient blocks returns kInvalidBuildingId and deducts nothing",
          "[blueprint]")
{
    Simulation sim(loadTestConfig());

    // Find miner cost (15 in test config) — expensive enough to exhaust a small stock.
    int minerCost = 0;
    for (const BuildingDef& def : sim.getConfig().buildings.buildings)
    {
        if (def.type == BuildingType::Miner) { minerCost = def.cost; break; }
    }
    REQUIRE(minerCost > 0);

    // Drain the stock by placing miners until we no longer have enough.
    // Non-overlapping columns: miner body is 2 wide, so step by 2.
    int col = -2;
    while (sim.getBuildingBlocksStock() >= minerCost)
    {
        SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(col, 0), Rotation::East);
        col -= 2;
    }

    const int blocksBeforeAttempt = sim.getBuildingBlocksStock();
    const std::optional<BuildingId> id = SimulationTestAccess::place(sim,
        BuildingType::Miner, QPoint(col - 2, 0), Rotation::East);

    // Placement must fail and leave the stock unchanged.
    REQUIRE_FALSE(id.has_value());
    REQUIRE(sim.getBuildingBlocksStock() == blocksBeforeAttempt);
}

TEST_CASE("Simulation: tryPlaceBuilding rejects terrain-invalid placement and charges nothing",
          "[blueprint]")
{
    Simulation sim(loadTestConfig());
    const int startBlocks = sim.getBuildingBlocksStock();

    // A miner is all-asteroid; placing it in space (x >= 0) violates the terrain
    // rule, so it must be rejected without consuming building blocks.
    const std::optional<BuildingId> id =
        SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(0, 0), Rotation::East);

    REQUIRE_FALSE(id.has_value());
    REQUIRE(sim.getBuildingBlocksStock() == startBlocks);
    REQUIRE(getAllSites(sim.getFactoryState()).empty());
}

TEST_CASE("Simulation: tryPlaceBuilding accepts a valid asteroid spot, occupies tiles, charges cost",
          "[blueprint]")
{
    Simulation sim(loadTestConfig());

    int minerCost = 0;
    for (const BuildingDef& def : sim.getConfig().buildings.buildings)
    {
        if (def.type == BuildingType::Miner) { minerCost = def.cost; break; }
    }
    REQUIRE(minerCost > 0);
    const int startBlocks = sim.getBuildingBlocksStock();

    // Miner mask ["AA","A>"] East at (-3,0) → all-asteroid body at
    // (-3,0),(-2,0),(-3,1); a valid spot.
    const BuildingId id =
        SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-3, 0), Rotation::East).value();

    REQUIRE(id != kInvalidBuildingId);
    REQUIRE(sim.getBuildingBlocksStock() == startBlocks - minerCost);
    REQUIRE(isTileOccupied(sim.getFactoryState(), QPoint(-3, 0)));
    REQUIRE(isTileOccupied(sim.getFactoryState(), QPoint(-2, 0)));
    REQUIRE(isTileOccupied(sim.getFactoryState(), QPoint(-3, 1)));
    // The output-port tile (1,1)+anchor = (-2,1) is not a body cell.
    REQUIRE_FALSE(isTileOccupied(sim.getFactoryState(), QPoint(-2, 1)));
}

// ---------------------------------------------------------------------------
// Recipe / schematic capture and re-application
// ---------------------------------------------------------------------------

TEST_CASE("Blueprint: recipeId is stored in BlueprintBuilding", "[blueprint]")
{
    const BuildingSpec spec{
        QPoint(-5, 0), {QPoint(-5, 0)}, BuildingType::Miner, Rotation::East,
        "mine_iron_ore"
    };
    const Blueprint bp = buildBlueprint({ spec });

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

TEST_CASE("Blueprint: building with no recipe has empty recipeId", "[blueprint]")
{
    const BuildingSpec spec{
        QPoint(-5, 0), {QPoint(-5, 0)}, BuildingType::Belt, Rotation::East
        // recipeId defaults to ""
    };
    const Blueprint bp = buildBlueprint({ spec });

    REQUIRE(bp.buildings[0].recipeId.empty());
}

TEST_CASE("Blueprint placement: setRecipe on construction site stores recipe", "[blueprint]")
{
    Simulation sim(loadTestConfig());

    // Miner body cells: (0,0),(1,0),(0,1) — all at x < 0, valid for asteroid.
    const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
    REQUIRE(id != kInvalidBuildingId);

    SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_iron_ore");

    const ConstructionSite* site = findSite(sim.getFactoryState(), id);
    REQUIRE(site != nullptr);
    REQUIRE(site->recipeId == "mine_iron_ore");
}

TEST_CASE("Blueprint placement: recipe transfers to building after construction completes",
          "[blueprint]")
{
    Simulation sim(loadTestConfig());

    const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
    REQUIRE(id != kInvalidBuildingId);
    SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_copper_ore");

    // Miner construction_time_seconds = 10 → completesAt = secondsToTicks(10) = 300.
    // Run 301 ticks (0..300) to process the completion tick.
    for (int i = 0; i <= static_cast<int>(secondsToTicks(10.0)); ++i)
    {
        sim.tick();
    }

    const Building* b = findBuilding(sim.getFactoryState(), id);
    REQUIRE(b != nullptr);
    REQUIRE(b->recipeId == "mine_copper_ore");
}

// ---------------------------------------------------------------------------
// Blueprint capture from construction sites (REQ-UI-BLUEPRINT-CREATE,
// REQ-UI-BLUEPRINT-STORAGE): a still-under-construction building is a valid
// blueprint source, captured identically to an operational building.
// ---------------------------------------------------------------------------

TEST_CASE("Blueprint creation: a construction site is captured", "[blueprint]")
{
    Simulation sim(loadTestConfig());

    // Freshly placed → a ConstructionSite (not ticked to completion). A 1x1 belt keeps
    // the body-cell bounding-box centered on the anchor, so a single site → zero offset.
    const BuildingId id =
        SimulationTestAccess::place(sim, BuildingType::Belt, QPoint(-2, 0), Rotation::East).value();
    REQUIRE(id != kInvalidBuildingId);
    REQUIRE(findSite(sim.getFactoryState(), id) != nullptr);
    REQUIRE(findBuilding(sim.getFactoryState(), id) == nullptr);

    const Blueprint bp = captureBlueprintFromSelection(sim, { id });

    REQUIRE(bp.buildings.size() == 1);
    REQUIRE(bp.buildings[0].type == BuildingType::Belt);
    REQUIRE(bp.buildings[0].offset == QPoint(0, 0)); // single 1x1 building → zero offset
}

TEST_CASE("Blueprint creation: a construction site's recipe is captured", "[blueprint]")
{
    Simulation sim(loadTestConfig());

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

    const Blueprint bp = captureBlueprintFromSelection(sim, { id });

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

TEST_CASE("Blueprint creation: mixed operational building and construction site are both captured",
          "[blueprint]")
{
    Simulation sim(loadTestConfig());

    // Building A: place, configure, and tick to completion so it is operational.
    const BuildingId idA =
        SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
    REQUIRE(idA != kInvalidBuildingId);
    SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), idA, "mine_iron_ore");
    for (int i = 0; i <= static_cast<int>(secondsToTicks(10.0)); ++i) { sim.tick(); }
    REQUIRE(findBuilding(sim.getFactoryState(), idA) != nullptr);

    // Building B: place and configure, but leave as a construction site.
    const BuildingId idB =
        SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-6, 0), Rotation::East).value();
    REQUIRE(idB != kInvalidBuildingId);
    SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), idB, "mine_copper_ore");
    REQUIRE(findSite(sim.getFactoryState(), idB) != nullptr);

    const Blueprint bp = captureBlueprintFromSelection(sim, { idA, idB });

    REQUIRE(bp.buildings.size() == 2);
    REQUIRE(bp.buildings[0].type == BuildingType::Miner);
    REQUIRE(bp.buildings[1].type == BuildingType::Miner);
    // Both the operational building's and the site's configs come through.
    std::vector<std::string> recipes = { bp.buildings[0].recipeId, bp.buildings[1].recipeId };
    std::sort(recipes.begin(), recipes.end());
    REQUIRE(recipes == std::vector<std::string>{ "mine_copper_ore", "mine_iron_ore" });
    // Distinct anchors → distinct offsets.
    REQUIRE(bp.buildings[0].offset != bp.buildings[1].offset);
}

TEST_CASE("Blueprint creation: selectionHasPlaceableBuilding sees a construction site", "[blueprint]")
{
    Simulation sim(loadTestConfig());

    REQUIRE_FALSE(selectionHasPlaceableBuilding(sim, {}));

    const BuildingId id =
        SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
    REQUIRE(id != kInvalidBuildingId);
    REQUIRE(findSite(sim.getFactoryState(), id) != nullptr);
    REQUIRE(selectionHasPlaceableBuilding(sim, { id }));
}

TEST_CASE("Blueprint placement: interceptor schematic is unlocked at game start", "[blueprint]")
{
    // "interceptor" has unlock_at_station_level = -1 in the test config.
    // This confirms the guard in placeBlueprintAtTile passes for start-unlocked schematics.
    Simulation sim(loadTestConfig());
    REQUIRE(sim.isSchematicUnlocked("interceptor"));
}

TEST_CASE("Blueprint placement: repair_ship schematic is locked at game start", "[blueprint]")
{
    // "repair_ship" has unlock_at_station_level = 0 in the test config.
    // This confirms the guard in placeBlueprintAtTile blocks locked schematics,
    // leaving the shipyard's schematic unset.
    Simulation sim(loadTestConfig());
    REQUIRE_FALSE(sim.isSchematicUnlocked("repair_ship"));
}

// ---------------------------------------------------------------------------
// Ship layout capture and re-application via blueprints
// ---------------------------------------------------------------------------

TEST_CASE("Blueprint: shipLayout is captured in BlueprintBuilding", "[blueprint]")
{
    // sensor_booster has surface_mask ["O"] — fits a single buildable cell.
    // The interceptor layout ["XOX","OOO","XOX"] has a buildable cell at (1, 0).
    ShipLayoutConfig layout;
    PlacedModule pm;
    pm.moduleId = "sensor_booster";
    pm.position = QPoint(1, 0);
    pm.rotation = Rotation::North;
    layout.placedModules.push_back(pm);

    const BuildingSpec spec{
        QPoint(-5, 0), {QPoint(-5, 0)}, BuildingType::Shipyard, Rotation::East,
        "interceptor", layout
    };
    const Blueprint bp = buildBlueprint({ spec });

    REQUIRE(bp.buildings.size() == 1);
    REQUIRE(bp.buildings[0].shipLayout.has_value());
    REQUIRE(bp.buildings[0].shipLayout->placedModules.size() == 1);
    REQUIRE(bp.buildings[0].shipLayout->placedModules[0].moduleId == "sensor_booster");
    REQUIRE(bp.buildings[0].shipLayout->placedModules[0].position == QPoint(1, 0));
    REQUIRE(bp.buildings[0].shipLayout->placedModules[0].rotation == Rotation::North);
}

TEST_CASE("Blueprint: building without layout has nullopt shipLayout", "[blueprint]")
{
    const BuildingSpec spec{
        QPoint(-5, 0), {QPoint(-5, 0)}, BuildingType::Shipyard, Rotation::East, "interceptor"
    };
    const Blueprint bp = buildBlueprint({ spec });

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

TEST_CASE("Blueprint placement: setShipLayout on construction site stores layout", "[blueprint]")
{
    Simulation sim(loadTestConfig());

    // Shipyard surface_mask ["AAAS>","AAAS "] with Rotation::East:
    // A-tiles at (-3,0),(-2,0),(-1,0),(-3,1),(-2,1),(-1,1) — all x < 0, valid asteroid tiles.
    // S-tile at (0,0) and (0,1) — x >= 0, valid space tiles.
    const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Shipyard, QPoint(-3, 0), Rotation::East).value();
    REQUIRE(id != kInvalidBuildingId);

    ShipLayoutConfig layout;
    PlacedModule pm;
    pm.moduleId = "laser_cannon";
    pm.position = QPoint(1, 1);
    pm.rotation = Rotation::East;
    layout.placedModules.push_back(pm);

    SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), id, layout);

    const ConstructionSite* site = findSite(sim.getFactoryState(), id);
    REQUIRE(site != nullptr);
    REQUIRE(site->shipLayout.has_value());
    REQUIRE(site->shipLayout->placedModules.size() == 1);
    REQUIRE(site->shipLayout->placedModules[0].moduleId == "laser_cannon");
}

TEST_CASE("Blueprint placement: ship layout transfers to building after construction completes",
          "[blueprint]")
{
    Simulation sim(loadTestConfig());

    const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Shipyard, QPoint(-3, 0), Rotation::East).value();
    REQUIRE(id != kInvalidBuildingId);

    ShipLayoutConfig layout;
    PlacedModule pm;
    pm.moduleId = "sensor_booster";
    pm.position = QPoint(1, 0);
    pm.rotation = Rotation::North;
    layout.placedModules.push_back(pm);

    SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), id, layout);

    // Shipyard construction_time_seconds = 30 in the test config.
    double constructionTime = 0.0;
    for (const BuildingDef& def : sim.getConfig().buildings.buildings)
    {
        if (def.type == BuildingType::Shipyard) { constructionTime = def.constructionTimeSeconds; break; }
    }
    REQUIRE(constructionTime > 0.0);

    for (int i = 0; i <= static_cast<int>(secondsToTicks(constructionTime)); ++i)
    {
        sim.tick();
    }

    const Building* b = findBuilding(sim.getFactoryState(), id);
    REQUIRE(b != nullptr);
    REQUIRE(b->shipLayout.has_value());
    REQUIRE(b->shipLayout->placedModules.size() == 1);
    REQUIRE(b->shipLayout->placedModules[0].moduleId == "sensor_booster");
    REQUIRE(b->shipLayout->placedModules[0].position == QPoint(1, 0));
    REQUIRE(b->shipLayout->placedModules[0].rotation == Rotation::North);
}
