#include "catch.hpp"

#include <cmath>
#include <string>

#include <QPoint>
#include <QVector2D>

#include "AdvanceBehavior.h"
#include "AttackBehavior.h"
#include "BuildingId.h"
#include "ConfigLoader.h"
#include "DeliverScrapBehavior.h"
#include "DynamicBodyComponent.h"
#include "EntityAdmin.h"
#include "HealthComponent.h"
#include "ModuleOwnerComponent.h"
#include "RallyBehavior.h"
#include "RepairBehavior.h"
#include "RepairToolComponent.h"
#include "RetreatBehavior.h"
#include "CargoComponent.h"
#include "Rotation.h"
#include "SalvageScrapBehavior.h"
#include "SalvagerComponent.h"
#include "SelectedBehaviorComponent.h"
#include "SensorRangeComponent.h"
#include "ShipLayout.h"
#include "ShipSystem.h"
#include "Tick.h"
#include "WeaponComponent.h"
#include "TestConfig.h"

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

static entt::entity firstWeaponChild(EntityAdmin& admin, entt::entity ship)
{
    entt::entity result = entt::null;
    admin.forEach<WeaponComponent, ModuleOwnerComponent>(
        [&](entt::entity ce, const WeaponComponent&, const ModuleOwnerComponent& o)
        {
            if (o.owner == ship && result == entt::null) { result = ce; }
        });
    return result;
}

static entt::entity firstSalvageChild(EntityAdmin& admin, entt::entity ship)
{
    entt::entity result = entt::null;
    admin.forEach<SalvagerComponent, ModuleOwnerComponent>(
        [&](entt::entity ce, const SalvagerComponent&, const ModuleOwnerComponent& o)
        {
            if (o.owner == ship && result == entt::null) { result = ce; }
        });
    return result;
}

static entt::entity firstRepairChild(EntityAdmin& admin, entt::entity ship)
{
    entt::entity result = entt::null;
    admin.forEach<RepairToolComponent, ModuleOwnerComponent>(
        [&](entt::entity ce, const RepairToolComponent&, const ModuleOwnerComponent& o)
        {
            if (o.owner == ship && result == entt::null) { result = ce; }
        });
    return result;
}

static ShipLayoutConfig makeSingleModuleLayout(const std::string& moduleId)
{
    PlacedModule pm;
    pm.moduleId = moduleId;
    pm.position = QPoint(0, 0);
    pm.rotation = Rotation::East;
    ShipLayoutConfig layout;
    layout.placedModules.push_back(pm);
    return layout;
}

// ---------------------------------------------------------------------------
// Combat ship (interceptor has default_modules = [laser_cannon])
// ---------------------------------------------------------------------------

TEST_CASE("ShipSystem: interceptor spawn has weapon child and attack behavior, no cargo or repair",
          "[ship]")
{
    EntityAdmin admin;
    const GameConfig cfg = loadTestConfig();
    ShipSystem ss(cfg, admin);

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

    REQUIRE(admin.isValid(e));
    REQUIRE(admin.isValid(firstWeaponChild(admin, e)));
    REQUIRE(admin.hasAll<AttackBehavior>(e));
    // Every ship gets the baseline behaviors; a player combat ship also rallies
    // and can retreat.
    REQUIRE(admin.hasAll<AdvanceBehavior>(e));
    REQUIRE(admin.hasAll<SelectedBehaviorComponent>(e));
    REQUIRE(admin.hasAll<RallyBehavior>(e));
    REQUIRE(admin.hasAll<RetreatBehavior>(e));
    REQUIRE_FALSE(admin.isValid(firstSalvageChild(admin, e)));
    REQUIRE_FALSE(admin.isValid(firstRepairChild(admin, e)));
    REQUIRE_FALSE(admin.hasAll<RepairBehavior>(e));
    REQUIRE_FALSE(admin.hasAll<SalvageScrapBehavior>(e));
    REQUIRE_FALSE(admin.hasAll<DeliverScrapBehavior>(e));
}

TEST_CASE("ShipSystem: enemy combat ship has no rally or retreat behavior", "[ship]")
{
    EntityAdmin admin;
    const GameConfig cfg = loadTestConfig();
    ShipSystem ss(cfg, admin);

    const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f), /*isEnemy=*/true);

    REQUIRE(admin.hasAll<AttackBehavior>(e));
    REQUIRE(admin.hasAll<AdvanceBehavior>(e));
    REQUIRE_FALSE(admin.hasAll<RallyBehavior>(e));
    REQUIRE_FALSE(admin.hasAll<RetreatBehavior>(e));
}

TEST_CASE("ShipSystem: setRetreatEnabled(false) suppresses player retreat behavior", "[ship]")
{
    EntityAdmin admin;
    const GameConfig cfg = loadTestConfig();
    ShipSystem ss(cfg, admin);
    ss.setRetreatEnabled(false);

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

    // Other player behaviors are unaffected; only retreat is suppressed.
    REQUIRE(admin.hasAll<AttackBehavior>(e));
    REQUIRE(admin.hasAll<RallyBehavior>(e));
    REQUIRE_FALSE(admin.hasAll<RetreatBehavior>(e));
}

TEST_CASE("ShipSystem: interceptor level 1 stats match config formulas", "[ship]")
{
    EntityAdmin admin;
    const GameConfig cfg = loadTestConfig();
    ShipSystem ss(cfg, admin);

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

    // hp_formula = "40 + 5*x" at x=1 → 45
    REQUIRE(admin.get<HealthComponent>(e).maxHp == Approx(45.0f));
    REQUIRE(admin.get<HealthComponent>(e).hp    == Approx(45.0f));
    // sensor_range_m_formula = "2000" m → 2000/10 = 200 tiles
    REQUIRE(admin.get<SensorRangeComponent>(e).value_tiles == Approx(200.0f));

    // laser_cannon: damage_formula = "2", attack_range_m_formula = "50" m → 50/10 = 5 tiles
    const entt::entity wc = firstWeaponChild(admin, e);
    REQUIRE(admin.isValid(wc));
    REQUIRE(admin.get<WeaponComponent>(wc).damage       == Approx(2.0f));
    REQUIRE(admin.get<WeaponComponent>(wc).range_tiles  == Approx(5.0f));
    REQUIRE(admin.get<WeaponComponent>(wc).cooldownTicks == Approx(0.0f));
}

TEST_CASE("ShipSystem: interceptor hp matches config value", "[ship]")
{
    EntityAdmin admin;
    const GameConfig cfg = loadTestConfig();
    ShipSystem ss(cfg, admin);

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

    // interceptor hp = 45 (plain value) in the test config
    REQUIRE(admin.get<HealthComponent>(e).maxHp == Approx(45.0f));
}

TEST_CASE("ShipSystem: interceptor maxSpeed_tpt matches config value / tileSize / kTickRateHz", "[ship]")
{
    EntityAdmin admin;
    const GameConfig cfg = loadTestConfig();
    ShipSystem ss(cfg, admin);

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

    // speed_mps = 2050 m/s in the test config; maxSpeed_tpt = 2050/(10*30)
    const float expected = 2050.0f / 10.0f / static_cast<float>(kTickRateHz);
    REQUIRE(admin.get<DynamicBodyComponent>(e).maxSpeed_tpt == Approx(expected));
}

// ---------------------------------------------------------------------------
// Salvage ship (spawned with salvager layout)
// ---------------------------------------------------------------------------

TEST_CASE("ShipSystem: salvage_ship spawn with salvage module has cargo child and behavior, no weapon",
          "[ship]")
{
    EntityAdmin admin;
    const GameConfig cfg = loadTestConfig();
    ShipSystem ss(cfg, admin);

    const ShipLayoutConfig layout = makeSingleModuleLayout("salvager");
    const entt::entity e = ss.spawn("salvage_ship", QVector2D(0.0f, 0.0f), false, layout);

    REQUIRE(admin.isValid(firstSalvageChild(admin, e)));
    REQUIRE(admin.hasAll<SalvageScrapBehavior>(e));
    REQUIRE(admin.hasAll<DeliverScrapBehavior>(e));
    REQUIRE_FALSE(admin.isValid(firstWeaponChild(admin, e)));
    REQUIRE_FALSE(admin.isValid(firstRepairChild(admin, e)));
}

TEST_CASE("ShipSystem: salvage_ship cargo capacity matches config", "[ship]")
{
    EntityAdmin admin;
    const GameConfig cfg = loadTestConfig();
    ShipSystem ss(cfg, admin);

    const ShipLayoutConfig layout = makeSingleModuleLayout("salvager");
    const entt::entity e = ss.spawn("salvage_ship", QVector2D(0.0f, 0.0f), false, layout);

    // salvager: cargo_capacity_formula = "10", collection_range_m_formula = "500" m → 500/10 = 50 tiles
    const entt::entity sc = firstSalvageChild(admin, e);
    REQUIRE(admin.isValid(sc));
    // Cargo capacity is now a ship-level pool (REQ-MOD-CARGO-CAPACITY).
    REQUIRE(admin.get<CargoComponent>(e).maxCapacity == 10);
    REQUIRE(admin.get<CargoComponent>(e).current     == 0);
    REQUIRE_FALSE(admin.get<DeliverScrapBehavior>(e).deliveryBay.has_value());
    REQUIRE_FALSE(admin.get<SalvageScrapBehavior>(e).debrisTarget.has_value());
    REQUIRE(admin.get<SalvageScrapBehavior>(e).maxCollectionRange_tiles == Approx(50.0f));
}

// ---------------------------------------------------------------------------
// Repair ship (spawned with repair_tool layout)
// ---------------------------------------------------------------------------

TEST_CASE("ShipSystem: repair_ship spawn with repair module has repair child and behavior, no weapon",
          "[ship]")
{
    EntityAdmin admin;
    const GameConfig cfg = loadTestConfig();
    ShipSystem ss(cfg, admin);

    const ShipLayoutConfig layout = makeSingleModuleLayout("repair_tool");
    const entt::entity e = ss.spawn("repair_ship", QVector2D(0.0f, 0.0f), false, layout);

    REQUIRE(admin.isValid(firstRepairChild(admin, e)));
    REQUIRE(admin.hasAll<RepairBehavior>(e));
    REQUIRE_FALSE(admin.isValid(firstWeaponChild(admin, e)));
    REQUIRE_FALSE(admin.isValid(firstSalvageChild(admin, e)));
}

TEST_CASE("ShipSystem: repair_ship level 1 repair stats match config formulas", "[ship]")
{
    EntityAdmin admin;
    const GameConfig cfg = loadTestConfig();
    ShipSystem ss(cfg, admin);

    const ShipLayoutConfig layout = makeSingleModuleLayout("repair_tool");
    const entt::entity e = ss.spawn("repair_ship", QVector2D(0.0f, 0.0f), false, layout);

    const entt::entity rc = firstRepairChild(admin, e);
    REQUIRE(admin.isValid(rc));
    // repair_rate_hz_formula = "1" cycle/s → interval = kTickRateHz ticks
    REQUIRE(admin.get<RepairToolComponent>(rc).repairIntervalTicks == kTickRateHz);
    // repair_amount_hp_formula = "5 + x" at x=1 → 6 HP per cycle
    REQUIRE(admin.get<RepairToolComponent>(rc).repairAmountHp == Approx(6.0f));
    REQUIRE(admin.get<RepairToolComponent>(rc).cooldownTicksRemaining == 0);
    // repair_range_m_formula = "800" m → 800/10 = 80 tiles
    REQUIRE(admin.get<RepairToolComponent>(rc).range_tiles == Approx(80.0f));
    REQUIRE(admin.get<RepairBehavior>(e).maxRepairRange_tiles == Approx(80.0f));
}

// ---------------------------------------------------------------------------
// Entity ids and removal
// ---------------------------------------------------------------------------

TEST_CASE("ShipSystem: spawned ships are valid entities", "[ship]")
{
    EntityAdmin admin;
    const GameConfig cfg = loadTestConfig();
    ShipSystem ss(cfg, admin);

    const entt::entity e1 = ss.spawn("interceptor", QVector2D(0.0f, 0.0f));
    const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
    const entt::entity e2 = ss.spawn("salvage_ship", QVector2D(1.0f, 0.0f), false, salvageLayout);

    REQUIRE(admin.isValid(e1));
    REQUIRE(admin.isValid(e2));
    REQUIRE(e1 != e2);
}

TEST_CASE("ShipSystem: despawn removes the ship and its weapon children", "[ship]")
{
    EntityAdmin admin;
    const GameConfig cfg = loadTestConfig();
    ShipSystem ss(cfg, admin);

    const entt::entity e  = ss.spawn("interceptor", QVector2D(0.0f, 0.0f));
    const entt::entity wc = firstWeaponChild(admin, e);
    REQUIRE(admin.isValid(e));
    REQUIRE(admin.isValid(wc));

    ss.despawn(e);
    REQUIRE_FALSE(admin.isValid(e));
    REQUIRE_FALSE(admin.isValid(wc));
}
