#include "catch.hpp"

#include <random>

#include "Building.h"
#include "BuildingSystem.h"
#include "FactoryState.h"
#include "BuildingType.h"
#include "CombatSystem.h"
#include "ConfigLoader.h"
#include "EntityAdmin.h"
#include "FactionComponent.h"
#include "BeamFiredEvent.h"
#include "HealthComponent.h"
#include "HqProxyComponent.h"
#include "ModuleOwnerComponent.h"
#include "DebrisComponent.h"
#include "DebrisSystem.h"
#include "ShipSystem.h"
#include "Simulation.h"
#include "AttackBehavior.h"
#include "StationBodyComponent.h"
#include "Tick.h"
#include "WeaponComponent.h"
#include "TestConfig.h"

static const ShipDef* findCombatShip(const GameConfig& cfg)
{
    for (const ShipDef& def : cfg.ships.ships)
    {
        if (!def.defaultModules.empty())
        {
            return &def;
        }
    }
    return nullptr;
}

static entt::entity findWeaponChild(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;
}

// Helper fixture for unit tests that need ships + combat but not a full Simulation.
struct CombatFixture
{
    GameConfig cfg;
    FactoryState state = makeFactoryState(cfg);
    std::mt19937 rng;
    EntityAdmin admin;
    ShipSystem ships;
    BuildingSystem buildings;
    CombatSystem combat;

    explicit CombatFixture()
        : cfg(loadTestConfig())
        , rng(42)
        , ships(cfg, admin)
        , buildings(cfg)
        , combat(cfg)
    {
    }

    void wireEnemyTarget(entt::entity enemy, entt::entity playerTarget)
    {
        // Set the target directly on the weapon child entity. CombatSystem now
        // fires at whatever target a weapon already has (AttackExecutor would set
        // it in a full tick); setting it here drives CombatSystem in isolation.
        const entt::entity wc = findWeaponChild(admin, enemy);
        if (wc != entt::null)
        {
            admin.get<WeaponComponent>(wc).currentTarget = playerTarget;
            admin.get<WeaponComponent>(wc).cooldownTicks = 0.0f;
        }
        if (admin.hasAll<AttackBehavior>(enemy))
        {
            admin.get<AttackBehavior>(enemy).currentTarget = playerTarget;
        }
    }
};

// ---------------------------------------------------------------------------
// Ship weapon firing
// ---------------------------------------------------------------------------

TEST_CASE("CombatSystem: ship fires when cooldown=0 and target in range", "[combat]")
{
    CombatFixture f;
    const ShipDef* combatDef = findCombatShip(f.cfg);
    REQUIRE(combatDef != nullptr);

    const entt::entity enemy  = f.ships.spawn(combatDef->id, QVector2D(5.0f, 5.0f), true);
    const entt::entity player = f.ships.spawn(combatDef->id, QVector2D(4.0f, 5.0f), false);
    f.wireEnemyTarget(enemy, player);

    const float hpBefore = f.admin.get<HealthComponent>(player).hp;

    std::vector<BeamFiredEvent> events;
    f.combat.tick(0, f.admin, events);
    f.combat.applyPendingDamage(5, f.admin);

    REQUIRE(f.admin.get<HealthComponent>(player).hp < hpBefore);
    REQUIRE(events.size() >= 1);
}

TEST_CASE("CombatSystem: cooldown prevents firing before it expires", "[combat]")
{
    CombatFixture f;
    const ShipDef* combatDef = findCombatShip(f.cfg);
    REQUIRE(combatDef != nullptr);

    const entt::entity enemy  = f.ships.spawn(combatDef->id, QVector2D(5.0f, 5.0f), true);
    const entt::entity player = f.ships.spawn(combatDef->id, QVector2D(4.0f, 5.0f), false);

    f.wireEnemyTarget(enemy, player);
    {
        const entt::entity wc = findWeaponChild(f.admin, enemy);
        REQUIRE(f.admin.isValid(wc));
        f.admin.get<WeaponComponent>(wc).cooldownTicks = 3.0f;  // override to 3
    }

    auto enemyFiredIn = [&enemy](const std::vector<BeamFiredEvent>& evts)
    {
        for (const BeamFiredEvent& evt : evts)
        {
            if (evt.shooter == enemy) { return true; }
        }
        return false;
    };

    std::vector<BeamFiredEvent> events;
    f.combat.tick(0, f.admin, events);
    REQUIRE_FALSE(enemyFiredIn(events));

    f.combat.tick(1, f.admin, events);
    REQUIRE_FALSE(enemyFiredIn(events));

    f.combat.tick(2, f.admin, events);
    REQUIRE(enemyFiredIn(events));
}

TEST_CASE("CombatSystem: no fire when target is out of range", "[combat]")
{
    CombatFixture f;
    const ShipDef* combatDef = findCombatShip(f.cfg);
    REQUIRE(combatDef != nullptr);

    const entt::entity enemy  = f.ships.spawn(combatDef->id, QVector2D(0.0f,   0.0f), true);
    const entt::entity player = f.ships.spawn(combatDef->id, QVector2D(500.0f, 0.0f), false);
    f.wireEnemyTarget(enemy, player);

    std::vector<BeamFiredEvent> events;
    f.combat.tick(0, f.admin, events);
    REQUIRE(events.empty());
}

// ---------------------------------------------------------------------------
// Station weapon firing
// ---------------------------------------------------------------------------

TEST_CASE("CombatSystem: player station fires at enemy ship in range", "[combat]")
{
    Simulation sim(loadTestConfig(), 42);

    // Find the player station entity via ECS.
    entt::entity stationEntity = entt::null;
    QVector2D stationCenter;
    sim.getAdmin().forEach<StationBodyComponent, FactionComponent>(
        [&](entt::entity e, const StationBodyComponent& sb, const FactionComponent& f)
        {
            if (!f.isEnemy && stationEntity == entt::null)
            {
                stationEntity = e;
                stationCenter = QVector2D(
                    sb.anchor.x() + sb.footprint.width()  / 2.0f,
                    sb.anchor.y() + sb.footprint.height() / 2.0f);
            }
        });
    REQUIRE(sim.getAdmin().isValid(stationEntity));

    const ShipDef* combatDef = findCombatShip(sim.getConfig());
    REQUIRE(combatDef != nullptr);

    const entt::entity enemyShip = sim.getShips().spawn(
        combatDef->id,
        QVector2D(stationCenter.x() + 1.0f, stationCenter.y()),
        /*isEnemy=*/true);

    sim.tick();

    const std::vector<BeamFiredEvent> events = sim.drainBeamFiredEvents();
    bool stationFired = false;
    for (const BeamFiredEvent& evt : events)
    {
        if (evt.shooter == stationEntity) { stationFired = true; }
    }
    REQUIRE(stationFired);
}

TEST_CASE("CombatSystem: enemy station fires at player ship in range", "[combat]")
{
    Simulation sim(loadTestConfig(), 42);

    entt::entity stationEntity = entt::null;
    QVector2D stationCenter;
    sim.getAdmin().forEach<StationBodyComponent, FactionComponent>(
        [&](entt::entity e, const StationBodyComponent& sb, const FactionComponent& f)
        {
            if (f.isEnemy && stationEntity == entt::null)
            {
                stationEntity = e;
                stationCenter = QVector2D(
                    sb.anchor.x() + sb.footprint.width()  / 2.0f,
                    sb.anchor.y() + sb.footprint.height() / 2.0f);
            }
        });
    REQUIRE(sim.getAdmin().isValid(stationEntity));

    const ShipDef* combatDef = findCombatShip(sim.getConfig());
    REQUIRE(combatDef != nullptr);

    sim.getShips().spawn(
        combatDef->id,
        QVector2D(stationCenter.x() - 1.0f, stationCenter.y()),
        /*isEnemy=*/false);

    sim.tick();

    const std::vector<BeamFiredEvent> events = sim.drainBeamFiredEvents();
    bool stationFired = false;
    for (const BeamFiredEvent& evt : events)
    {
        if (evt.shooter == stationEntity) { stationFired = true; }
    }
    REQUIRE(stationFired);
}

TEST_CASE("CombatSystem: player ship fires at enemy station in range", "[combat]")
{
    Simulation sim(loadTestConfig(), 42);

    entt::entity stationEntity = entt::null;
    QVector2D stationCenter;
    sim.getAdmin().forEach<StationBodyComponent, FactionComponent>(
        [&](entt::entity e, const StationBodyComponent& sb, const FactionComponent& f)
        {
            if (f.isEnemy && stationEntity == entt::null)
            {
                stationEntity = e;
                stationCenter = QVector2D(
                    sb.anchor.x() + sb.footprint.width()  / 2.0f,
                    sb.anchor.y() + sb.footprint.height() / 2.0f);
            }
        });
    REQUIRE(sim.getAdmin().isValid(stationEntity));

    const ShipDef* combatDef = findCombatShip(sim.getConfig());
    REQUIRE(combatDef != nullptr);

    const entt::entity playerShip = sim.getShips().spawn(
        combatDef->id,
        QVector2D(stationCenter.x() - 1.0f, stationCenter.y()),
        /*isEnemy=*/false);

    sim.tick();

    const std::vector<BeamFiredEvent> events = sim.drainBeamFiredEvents();
    bool playerFiredAtStation = false;
    for (const BeamFiredEvent& evt : events)
    {
        if (evt.shooter == playerShip && evt.target == stationEntity)
        {
            playerFiredAtStation = true;
        }
    }
    REQUIRE(playerFiredAtStation);
}

// ---------------------------------------------------------------------------
// Deferred damage timing
// ---------------------------------------------------------------------------

TEST_CASE("CombatSystem: damage not applied before impact tick", "[combat]")
{
    CombatFixture f;
    const ShipDef* combatDef = findCombatShip(f.cfg);
    REQUIRE(combatDef != nullptr);

    const entt::entity enemy  = f.ships.spawn(combatDef->id, QVector2D(5.0f, 5.0f), true);
    const entt::entity player = f.ships.spawn(combatDef->id, QVector2D(4.0f, 5.0f), false);
    f.wireEnemyTarget(enemy, player);

    const float hpBefore = f.admin.get<HealthComponent>(player).hp;

    std::vector<BeamFiredEvent> events;
    f.combat.tick(0, f.admin, events);

    for (Tick t = 1; t < 5; ++t)
    {
        f.combat.applyPendingDamage(t, f.admin);
        REQUIRE(f.admin.get<HealthComponent>(player).hp == Approx(hpBefore));
    }
}

TEST_CASE("CombatSystem: damage applied exactly at impact tick", "[combat]")
{
    CombatFixture f;
    const ShipDef* combatDef = findCombatShip(f.cfg);
    REQUIRE(combatDef != nullptr);

    const entt::entity enemy  = f.ships.spawn(combatDef->id, QVector2D(5.0f, 5.0f), true);
    const entt::entity player = f.ships.spawn(combatDef->id, QVector2D(4.0f, 5.0f), false);
    f.wireEnemyTarget(enemy, player);

    const float hpBefore = f.admin.get<HealthComponent>(player).hp;

    std::vector<BeamFiredEvent> events;
    f.combat.tick(0, f.admin, events);
    f.combat.applyPendingDamage(5, f.admin);

    REQUIRE(f.admin.get<HealthComponent>(player).hp < hpBefore);
}

TEST_CASE("CombatSystem: damage silently dropped if target already dead", "[combat]")
{
    CombatFixture f;
    const ShipDef* combatDef = findCombatShip(f.cfg);
    REQUIRE(combatDef != nullptr);

    const entt::entity enemy  = f.ships.spawn(combatDef->id, QVector2D(5.0f, 5.0f), true);
    const entt::entity player = f.ships.spawn(combatDef->id, QVector2D(4.0f, 5.0f), false);
    f.wireEnemyTarget(enemy, player);

    std::vector<BeamFiredEvent> events;
    f.combat.tick(0, f.admin, events);

    f.ships.despawn(player);

    // Should not crash.
    f.combat.applyPendingDamage(5, f.admin);

    REQUIRE_FALSE(f.admin.isValid(player));
}

TEST_CASE("CombatSystem: damage still applied if shooter already dead", "[combat]")
{
    CombatFixture f;
    const ShipDef* combatDef = findCombatShip(f.cfg);
    REQUIRE(combatDef != nullptr);

    const entt::entity enemy  = f.ships.spawn(combatDef->id, QVector2D(5.0f, 5.0f), true);
    const entt::entity player = f.ships.spawn(combatDef->id, QVector2D(4.0f, 5.0f), false);
    f.wireEnemyTarget(enemy, player);

    const float hpBefore = f.admin.get<HealthComponent>(player).hp;

    std::vector<BeamFiredEvent> events;
    f.combat.tick(0, f.admin, events);

    f.ships.despawn(enemy);

    f.combat.applyPendingDamage(5, f.admin);

    REQUIRE(f.admin.get<HealthComponent>(player).hp < hpBefore);
}

// ---------------------------------------------------------------------------
// Deaths & loot (tick step 9)
// ---------------------------------------------------------------------------

TEST_CASE("CombatSystem: dead ship is removed after tick step 9", "[combat]")
{
    Simulation sim(loadTestConfig(), 42);

    const ShipDef* combatDef = findCombatShip(sim.getConfig());
    REQUIRE(combatDef != nullptr);

    const entt::entity ship = sim.getShips().spawn(combatDef->id,
        QVector2D(10.0f, 10.0f));

    sim.getAdmin().get<HealthComponent>(ship).hp = -1.0f;

    sim.tick();

    REQUIRE_FALSE(sim.getAdmin().isValid(ship));
}

TEST_CASE("CombatSystem: scrap is spawned on ship death", "[combat]")
{
    Simulation sim(loadTestConfig(), 42);

    // Scrap dropped on death is derived from the ship's as-built threat cost
    // (REQ-RES-DEBRIS-DROP): round(threat * scrap_per_threat). The interceptor's
    // threat is 59.0 and the test config sets scrap_per_threat = 1.0, so it drops
    // round(59.0 * 1.0) = 59 scrap.
    const entt::entity ship = sim.getShips().spawn("interceptor",
        QVector2D(10.0f, 10.0f));
    sim.getAdmin().get<HealthComponent>(ship).hp = -1.0f;

    sim.tick();

    const std::vector<DebrisInfo> scraps = getAllDebrisInfo(sim.getAdmin());
    REQUIRE(scraps.size() == 1);
    CHECK(sim.getAdmin().get<DebrisComponent>(scraps[0].entity).amount == 59);
}

TEST_CASE("CombatSystem: HQ death sets game over", "[combat]")
{
    Simulation sim(loadTestConfig(), 42);

    // Damage the HQ proxy entity (has HqProxy + Health).
    sim.getAdmin().forEach<HqProxyComponent, HealthComponent>(
        [](entt::entity /*e*/, const HqProxyComponent& /*hq*/, HealthComponent& h)
        {
            h.hp = -1.0f;
        });

    sim.tick();

    REQUIRE(sim.isGameOver());
}
