#include "catch.hpp"

#include <QRectF>
#include <QSize>
#include <QVector2D>

#include <algorithm>

#include "DespawnAtComponent.h"
#include "EntityAdmin.h"
#include "EntityHitTest.h"
#include "DebrisComponent.h"
#include "DebrisSystem.h"

namespace
{
bool contains(const std::vector<entt::entity>& v, entt::entity e)
{
    return std::find(v.begin(), v.end(), e) != v.end();
}
} // namespace

// ---------------------------------------------------------------------------
// Spawn
// ---------------------------------------------------------------------------

TEST_CASE("DebrisSystem: spawn returns a valid entity with correct debris data", "[debris]")
{
    EntityAdmin admin;
    DebrisSystem ss(admin);

    const entt::entity e = ss.spawn(QVector2D(3.0f, 4.0f), 5, 100);

    REQUIRE(admin.isValid(e));
    REQUIRE(admin.get<DebrisComponent>(e).amount == 5);
    REQUIRE(admin.get<DespawnAtComponent>(e).tick == 100);
}

// ---------------------------------------------------------------------------
// Despawn timing
// ---------------------------------------------------------------------------

TEST_CASE("DebrisSystem: debris still present one tick before despawnAt", "[debris]")
{
    EntityAdmin admin;
    DebrisSystem ss(admin);

    const entt::entity e = ss.spawn(QVector2D(0.0f, 0.0f), 1, 50);

    ss.tickDespawn(49);
    REQUIRE(admin.isValid(e));
}

TEST_CASE("DebrisSystem: debris removed at despawnAt tick", "[debris]")
{
    EntityAdmin admin;
    DebrisSystem ss(admin);

    const entt::entity e = ss.spawn(QVector2D(0.0f, 0.0f), 1, 50);

    ss.tickDespawn(50);
    REQUIRE_FALSE(admin.isValid(e));
}

// ---------------------------------------------------------------------------
// Selective removal
// ---------------------------------------------------------------------------

TEST_CASE("DebrisSystem: tickDespawn removes only expired debris", "[debris]")
{
    EntityAdmin admin;
    DebrisSystem ss(admin);

    const entt::entity earlyE = ss.spawn(QVector2D(0.0f, 0.0f), 1, 30);
    const entt::entity lateE  = ss.spawn(QVector2D(1.0f, 0.0f), 2, 60);

    ss.tickDespawn(30);

    REQUIRE_FALSE(admin.isValid(earlyE));
    REQUIRE(admin.isValid(lateE));
}

// ---------------------------------------------------------------------------
// Consume
// ---------------------------------------------------------------------------

TEST_CASE("DebrisSystem: consume returns amount and destroys entity", "[debris]")
{
    EntityAdmin admin;
    DebrisSystem ss(admin);

    const entt::entity e = ss.spawn(QVector2D(0.0f, 0.0f), 7, 100);

    const std::optional<int> amount = ss.consume(e);
    REQUIRE(amount.has_value());
    REQUIRE(*amount == 7);
    REQUIRE_FALSE(admin.isValid(e));
}

TEST_CASE("DebrisSystem: consume returns nullopt for invalid entity", "[debris]")
{
    EntityAdmin admin;
    DebrisSystem ss(admin);

    const std::optional<int> amount = ss.consume(entt::null);
    REQUIRE_FALSE(amount.has_value());
}

// ---------------------------------------------------------------------------
// collectOne
// ---------------------------------------------------------------------------

TEST_CASE("DebrisSystem: collectOne depletes one scrap and keeps the debris until empty", "[debris]")
{
    EntityAdmin admin;
    DebrisSystem ss(admin);

    const entt::entity e = ss.spawn(QVector2D(0.0f, 0.0f), 3, 100);

    REQUIRE(collectOne(admin, e));
    REQUIRE(admin.isValid(e));
    REQUIRE(admin.get<DebrisComponent>(e).amount == 2);

    REQUIRE(collectOne(admin, e));
    REQUIRE(admin.isValid(e));
    REQUIRE(admin.get<DebrisComponent>(e).amount == 1);

    // Final unit collected: the debris is removed once depleted.
    REQUIRE(collectOne(admin, e));
    REQUIRE_FALSE(admin.isValid(e));
}

TEST_CASE("DebrisSystem: collectOne returns false for an invalid entity", "[debris]")
{
    EntityAdmin admin;
    DebrisSystem ss(admin);

    REQUIRE_FALSE(collectOne(admin, entt::null));
}

// ---------------------------------------------------------------------------
// getAllDebrisInfo
// ---------------------------------------------------------------------------

TEST_CASE("DebrisSystem: getAllDebrisInfo returns all spawned debris", "[debris]")
{
    EntityAdmin admin;
    DebrisSystem ss(admin);

    ss.spawn(QVector2D(1.0f, 2.0f), 3, 100);
    ss.spawn(QVector2D(4.0f, 5.0f), 6, 200);

    const std::vector<DebrisInfo> info = getAllDebrisInfo(admin);
    REQUIRE(info.size() == 2);
}

TEST_CASE("DebrisSystem: getAllDebrisInfo reports each debris entry.s remaining amount", "[debris]")
{
    EntityAdmin admin;
    DebrisSystem ss(admin);

    const entt::entity a = ss.spawn(QVector2D(1.0f, 2.0f), 3, 100);
    const entt::entity b = ss.spawn(QVector2D(4.0f, 5.0f), 6, 200);

    const std::vector<DebrisInfo> info = getAllDebrisInfo(admin);
    REQUIRE(info.size() == 2);
    for (const DebrisInfo& i : info)
    {
        if (i.entity == a) { REQUIRE(i.amount == 3); }
        else if (i.entity == b) { REQUIRE(i.amount == 6); }
        else { FAIL("unexpected debris entity"); }
    }
}

// ---------------------------------------------------------------------------
// Selection hit-testing (REQ-UI-DEBRIS-CLICK-SELECT, REQ-UI-DEBRIS-MULTI-SELECT)
// ---------------------------------------------------------------------------

TEST_CASE("debrisAtWorldPos returns the debris near a point and null when far", "[debris]")
{
    EntityAdmin admin;
    DebrisSystem ss(admin);

    const entt::entity e = ss.spawn(QVector2D(3.0f, 4.0f), 5, 100);

    // Extra parens keep Catch from decomposing the comparison, which is ambiguous
    // between Catch's expression templates and entt's entity operator==.
    REQUIRE((debrisAtWorldPos(admin, QVector2D(3.1f, 4.0f)) == e));
    REQUIRE((debrisAtWorldPos(admin, QVector2D(10.0f, 10.0f)) == entt::null));
}

TEST_CASE("debrisAtWorldPos returns the nearest of several debris", "[debris]")
{
    EntityAdmin admin;
    DebrisSystem ss(admin);

    const entt::entity near = ss.spawn(QVector2D(2.0f, 2.0f), 1, 100);
    ss.spawn(QVector2D(2.4f, 2.0f), 1, 100);

    REQUIRE((debrisAtWorldPos(admin, QVector2D(2.05f, 2.0f)) == near));
}

TEST_CASE("entityAtWorldPos never returns debris", "[debris]")
{
    EntityAdmin admin;
    DebrisSystem ss(admin);

    ss.spawn(QVector2D(3.0f, 4.0f), 5, 100);

    // Debris has no HealthComponent, so the actor hit-test ignores it entirely.
    REQUIRE((entityAtWorldPos(admin, QVector2D(3.0f, 4.0f)) == entt::null));
}

TEST_CASE("debrisInBox returns exactly the debris the box encloses", "[debris]")
{
    EntityAdmin admin;
    DebrisSystem ss(admin);

    const entt::entity inA  = ss.spawn(QVector2D(1.2f, 2.7f), 1, 100);
    const entt::entity inB  = ss.spawn(QVector2D(4.9f, 5.1f), 1, 100);
    const entt::entity outX = ss.spawn(QVector2D(10.0f, 10.0f), 1, 100);

    const std::vector<entt::entity> hit = debrisInBox(admin, QRectF(0.0, 0.0, 6.0, 6.0));

    REQUIRE(hit.size() == 2);
    REQUIRE(contains(hit, inA));
    REQUIRE(contains(hit, inB));
    REQUIRE_FALSE(contains(hit, outX));
}

TEST_CASE("debrisInBox cuts within a tile, not along the tile grid", "[debris]")
{
    EntityAdmin admin;
    DebrisSystem ss(admin);

    const entt::entity inTile  = ss.spawn(QVector2D(1.8f, 2.5f), 1, 100);
    // Same tile (1,2) as the piece above, but on the far side of the box's left edge:
    // a tile-snapped box would take both (REQ-UI-MULTI-SELECT, Coverage).
    const entt::entity outTile = ss.spawn(QVector2D(1.2f, 2.5f), 1, 100);

    const std::vector<entt::entity> hit = debrisInBox(admin, QRectF(1.5, 2.0, 4.0, 4.0));

    REQUIRE(hit.size() == 1);
    REQUIRE(contains(hit, inTile));
    REQUIRE_FALSE(contains(hit, outTile));
}

TEST_CASE("actorsInBox returns living ships and stations, excluding debris and dead actors",
          "[actor]")
{
    EntityAdmin admin;

    // Two living ships inside the box: one player, one enemy.
    const entt::entity playerShip = admin.spawnShip(
        QVector2D(1.5f, 2.5f), 100.0f, 100.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 5.0f,
        "fighter", false);
    const entt::entity enemyShip = admin.spawnShip(
        QVector2D(4.2f, 5.8f), 100.0f, 100.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 5.0f,
        "raider", true);

    // A dead ship inside the box is excluded.
    const entt::entity deadShip = admin.spawnShip(
        QVector2D(3.0f, 3.0f), 0.0f, 100.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 5.0f,
        "fighter", false);

    // A ship outside the box is excluded.
    const entt::entity outsideShip = admin.spawnShip(
        QVector2D(20.0f, 20.0f), 100.0f, 100.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 5.0f,
        "fighter", false);

    // A station is included when the box overlaps any body cell.
    const std::vector<QPoint> stationCells{ QPoint(2, 2), QPoint(3, 2) };
    const entt::entity station = admin.spawnStation(
        QPoint(2, 2), QSize(2, 1), stationCells, 200.0f, 200.0f, true);

    // Debris and the HQ proxy are never actors.
    admin.spawnDebris(QVector2D(1.0f, 1.0f), 5, Tick(1000));
    admin.spawnHqProxy(QVector2D(0.5f, 0.5f), 500.0f, 500.0f);

    const std::vector<entt::entity> hit = actorsInBox(admin, QRectF(0.0, 0.0, 6.0, 6.0));

    REQUIRE(hit.size() == 3);
    REQUIRE(contains(hit, playerShip));
    REQUIRE(contains(hit, enemyShip));
    REQUIRE(contains(hit, station));
    REQUIRE_FALSE(contains(hit, deadShip));
    REQUIRE_FALSE(contains(hit, outsideShip));
}

TEST_CASE("actorsInBox takes a ship by its position and a station by its footprint",
          "[actor]")
{
    EntityAdmin admin;

    // Ship inside the tile the box only reaches into: taken, because the box contains
    // its position (REQ-UI-MULTI-SELECT, Coverage).
    const entt::entity shipInside = admin.spawnShip(
        QVector2D(3.9f, 3.9f), 100.0f, 100.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 5.0f,
        "fighter", false);
    // Same tile (3,3), outside the box: a tile-snapped box would take it too.
    const entt::entity shipOutside = admin.spawnShip(
        QVector2D(3.1f, 3.1f), 100.0f, 100.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 5.0f,
        "fighter", false);

    // The box reaches 0.5 tiles into the station's only cell, which is enough: a
    // station is covered when the box overlaps its footprint.
    const std::vector<QPoint> stationCells{ QPoint(4, 3) };
    const entt::entity station = admin.spawnStation(
        QPoint(4, 3), QSize(1, 1), stationCells, 200.0f, 200.0f, true);

    const std::vector<entt::entity> hit = actorsInBox(admin, QRectF(3.5, 3.5, 1.0, 1.0));

    REQUIRE(hit.size() == 2);
    REQUIRE(contains(hit, shipInside));
    REQUIRE(contains(hit, station));
    REQUIRE_FALSE(contains(hit, shipOutside));
}
