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

#include <random>

#include "Building.h"
#include "BuildingSystem.h"
#include "BuildingType.h"
#include "ConfigLoader.h"
#include "EntityAdmin.h"
#include "FactionComponent.h"
#include "HealthComponent.h"
#include "HqProxyComponent.h"
#include "ModuleOwnerComponent.h"
#include "Rotation.h"
#include "ShipIdentityComponent.h"
#include "ShipSystem.h"
#include "StationBodyComponent.h"
#include "WeaponComponent.h"
#include "ModulesConfig.h"
#include "RecipesConfig.h"
#include "SchematicChoiceOption.h"
#include "ShipsConfig.h"
#include "Simulation.h"
#include "SimulationTestAccess.h"
#include "Tick.h"
#include "ThreatCostCalculator.h"
#include "WaveSystem.h"
#include "TestConfig.h"

// ---------------------------------------------------------------------------
// Threat accumulation
// ---------------------------------------------------------------------------

TEST_CASE("WaveSystem: threat accumulates at boss wave counter rate", "[wave]")
{
    const GameConfig cfg = loadTestConfig();
    std::mt19937 rng(42);
    WaveSystem ws(cfg, rng);

    // threat_rate_formula = "x", boss wave counter starts at 1 → rate = 1 threat/s.
    // After 1 second: threat ≈ 1.0.
    const int ticks1s = static_cast<int>(secondsToTicks(1.0));
    for (int i = 0; i < ticks1s; ++i)
    {
        ws.tickThreatAccumulation();
    }

    REQUIRE(ws.getThreatLevel() == Approx(1.0));
}

TEST_CASE("WaveSystem: threatAccumulationRate matches the rate formula outside quiet windows",
          "[wave]")
{
    const GameConfig cfg = loadTestConfig();
    std::mt19937 rng(42);
    WaveSystem ws(cfg, rng);

    // threat_rate_formula = "x", boss wave counter starts at 1 → rate = 1 threat/s.
    REQUIRE(ws.getThreatAccumulationRate() == Approx(1.0));
}

TEST_CASE("WaveSystem: threatAccumulationRate is 0 during a quiet window", "[wave]")
{
    GameConfig cfg = loadTestConfig();
    // Start with the boss countdown already at the pre-boss quiet threshold.
    cfg.world.waves.bossCountdownSeconds = cfg.world.waves.bossQuietBeforeSeconds;
    std::mt19937 rng(42);
    WaveSystem ws(cfg, rng);

    REQUIRE(ws.getThreatAccumulationRate() == Approx(0.0));

    const double before = ws.getThreatLevel();
    for (int i = 0; i < static_cast<int>(secondsToTicks(1.0)); ++i)
    {
        ws.tickThreatAccumulation();
    }
    REQUIRE(ws.getThreatLevel() == Approx(before));
}

TEST_CASE("WaveSystem: generation starts at 0 and increments on station destruction", "[wave]")
{
    const GameConfig cfg = loadTestConfig();
    std::mt19937 rng(42);
    WaveSystem ws(cfg, rng);

    REQUIRE(ws.getGeneration() == 0);
    ws.onEnemyStationsDestroyed();
    REQUIRE(ws.getGeneration() == 1);
    ws.onEnemyStationsDestroyed();
    REQUIRE(ws.getGeneration() == 2);
}

// ---------------------------------------------------------------------------
// Pre-placed structures
// ---------------------------------------------------------------------------

TEST_CASE("WaveSystem: Simulation pre-places HQ + 2 player + 2 enemy stations", "[wave]")
{
    const Simulation sim(loadTestConfig(), 42);

    // HQ is still a Building (for belt integration).
    int hqCount = 0;
    for (const Building& b : getAllBuildings(sim.getFactoryState()))
    {
        if (b.type == BuildingType::Hq) { ++hqCount; }
    }

    // Stations are ECS entities.
    int playerCount = 0;
    int enemyCount  = 0;
    sim.getAdmin().forEach<StationBodyComponent, FactionComponent>(
        [&](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f)
        {
            if (f.isEnemy) { ++enemyCount; }
            else           { ++playerCount; }
        });

    REQUIRE(hqCount     == 1);
    REQUIRE(playerCount == 2);
    REQUIRE(enemyCount  == 2);
}

TEST_CASE("WaveSystem: HQ has correct initial HP from config", "[wave]")
{
    const Simulation sim(loadTestConfig(), 42);

    const float expectedHp =
        static_cast<float>(sim.getConfig().stations.hq.hpFormula.evaluate(0.0));
    bool found = false;
    float actualHp = 0.0f;
    sim.getAdmin().forEach<HqProxyComponent, HealthComponent>(
        [&](entt::entity /*e*/, const HqProxyComponent& /*hq*/, const HealthComponent& h)
        {
            found    = true;
            actualHp = h.hp;
        });

    REQUIRE(found);
    REQUIRE(actualHp == Approx(expectedHp));
}

TEST_CASE("WaveSystem: HQ anchor is at asteroid right edge", "[wave]")
{
    const Simulation sim(loadTestConfig(), 42);

    for (const Building& b : getAllBuildings(sim.getFactoryState()))
    {
        if (b.type != BuildingType::Hq) { continue; }
        // Rightmost body cell must be at x = -1 (asteroid right edge).
        int maxX = std::numeric_limits<int>::min();
        for (const QPoint& cell : b.bodyCells)
        {
            if (cell.x() > maxX) { maxX = cell.x(); }
        }
        REQUIRE(maxX == -1);
    }
}

TEST_CASE("WaveSystem: player stations have weapon set", "[wave]")
{
    Simulation sim(loadTestConfig(), 42);

    int armedPlayerStations = 0;
    sim.getAdmin().forEach<WeaponComponent, ModuleOwnerComponent>(
        [&](entt::entity /*e*/, const WeaponComponent& w, const ModuleOwnerComponent& mo)
        {
            if (!sim.getAdmin().hasAll<StationBodyComponent>(mo.owner)) { return; }
            const FactionComponent& f = sim.getAdmin().get<FactionComponent>(mo.owner);
            if (!f.isEnemy)
            {
                ++armedPlayerStations;
                REQUIRE(w.damage     > 0.0f);
                REQUIRE(w.range_tiles > 0.0f);
                REQUIRE(w.fireRateHz > 0.0f);
            }
        });
    REQUIRE(armedPlayerStations == 2);
}

TEST_CASE("WaveSystem: enemy stations have weapon set", "[wave]")
{
    Simulation sim(loadTestConfig(), 42);

    int armedEnemyStations = 0;
    sim.getAdmin().forEach<WeaponComponent, ModuleOwnerComponent>(
        [&](entt::entity /*e*/, const WeaponComponent& w, const ModuleOwnerComponent& mo)
        {
            if (!sim.getAdmin().hasAll<StationBodyComponent>(mo.owner)) { return; }
            const FactionComponent& f = sim.getAdmin().get<FactionComponent>(mo.owner);
            if (f.isEnemy)
            {
                ++armedEnemyStations;
                REQUIRE(w.damage     > 0.0f);
                REQUIRE(w.range_tiles > 0.0f);
                REQUIRE(w.fireRateHz > 0.0f);
            }
        });
    REQUIRE(armedEnemyStations == 2);
}

// ---------------------------------------------------------------------------
// Wave spawning
// ---------------------------------------------------------------------------

TEST_CASE("WaveSystem: enemy ships spawn after the initial gap elapses", "[wave]")
{
    Simulation sim(loadTestConfig(), 42);

    // The maximum gap is gapMaxSeconds = 45s → 1350 ticks.
    // Run 1500 ticks to guarantee at least one wave has triggered.
    // Check each tick: enemy ships may be killed quickly by player stations,
    // so we must detect them while they are alive, not only after the loop.
    const int limit = static_cast<int>(secondsToTicks(50.0));
    bool foundEnemyShip = false;
    for (int i = 0; i < limit; ++i)
    {
        sim.tick();
        if (!foundEnemyShip)
        {
            sim.getAdmin().forEach<ShipIdentityComponent, FactionComponent>(
                [&](entt::entity /*e*/, const ShipIdentityComponent& /*si*/,
                    const FactionComponent& f)
                {
                    if (f.isEnemy) { foundEnemyShip = true; }
                });
        }
    }
    REQUIRE(foundEnemyShip);
}

TEST_CASE("WaveSystem: all ships have positive dynamic threat cost", "[wave]")
{
    const GameConfig cfg = loadTestConfig();

    for (const ShipDef& def : cfg.ships.ships)
    {
        const double cost = calculateShipThreatCost(cfg.threatCosts, cfg,
                                                       def.id, def.defaultModules);
        CHECK(cost > 0.0);
    }
}

// ---------------------------------------------------------------------------
// Push
// ---------------------------------------------------------------------------

TEST_CASE("WaveSystem: destroying both enemy stations triggers a push", "[wave]")
{
    Simulation sim(loadTestConfig(), 42);

    // Damage both enemy stations to 0.
    sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
        [](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h)
        {
            if (f.isEnemy) { h.hp = -1.0f; }
        });

    sim.tick();

    // After push: should have 2 new enemy stations.
    int enemyCount = 0;
    sim.getAdmin().forEach<StationBodyComponent, FactionComponent>(
        [&](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f)
        {
            if (f.isEnemy) { ++enemyCount; }
        });
    REQUIRE(enemyCount == 2);
}

TEST_CASE("WaveSystem: push generates pending schematic choices", "[wave]")
{
    Simulation sim(loadTestConfig(), 42);

    sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
        [](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h)
        {
            if (f.isEnemy) { h.hp = -1.0f; }
        });

    sim.tick();

    REQUIRE(sim.hasSchematicChoicesPending());
    const std::vector<SchematicChoiceOption>& choices = sim.getPendingSchematicChoices();
    REQUIRE(choices.size() >= 1);
    REQUIRE(choices.size() <= 3);
}

TEST_CASE("WaveSystem: push schematic choices have valid ids", "[wave]")
{
    Simulation sim(loadTestConfig(), 42);

    sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
        [](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h)
        {
            if (f.isEnemy) { h.hp = -1.0f; }
        });

    sim.tick();
    const std::vector<SchematicChoiceOption>& choices = sim.getPendingSchematicChoices();
    REQUIRE_FALSE(choices.empty());

    for (const SchematicChoiceOption& opt : choices)
    {
        if (opt.isArtifact) { continue; }
        bool validGroup = false;
        for (const UnlockGroupDef& group : sim.getConfig().unlocks.groups)
        {
            if (group.id == opt.unlockGroupId) { validGroup = true; break; }
        }
        REQUIRE(validGroup);
        // Every granted item must resolve to a defined ship/module/building/recipe.
        for (const GrantedSchematic& grant : opt.grantedItems)
        {
            bool validId = false;
            for (const ShipDef& def : sim.getConfig().ships.ships)
            {
                if (def.id == grant.id) { validId = true; break; }
            }
            for (const ModuleDef& def : sim.getConfig().modules.modules)
            {
                if (def.id == grant.id) { validId = true; break; }
            }
            for (const BuildingDef& def : sim.getConfig().buildings.buildings)
            {
                if (def.id == grant.id) { validId = true; break; }
            }
            for (const RecipeDef& def : sim.getConfig().recipes.recipes)
            {
                if (def.id == grant.id) { validId = true; break; }
            }
            REQUIRE(validId);
        }
    }
}

TEST_CASE("WaveSystem: schematic choices have no duplicates", "[wave]")
{
    Simulation sim(loadTestConfig(), 42);

    sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
        [](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h)
        {
            if (f.isEnemy) { h.hp = -1.0f; }
        });

    sim.tick();
    const std::vector<SchematicChoiceOption>& choices = sim.getPendingSchematicChoices();
    std::set<std::string> ids;
    for (const SchematicChoiceOption& opt : choices)
    {
        ids.insert(opt.isArtifact ? std::string("<artifact>") : opt.unlockGroupId);
    }
    REQUIRE(ids.size() == choices.size());
}

TEST_CASE("WaveSystem: applySchematicChoice clears pending and applies", "[wave]")
{
    Simulation sim(loadTestConfig(), 42);

    sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
        [](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h)
        {
            if (f.isEnemy) { h.hp = -1.0f; }
        });

    sim.tick();
    REQUIRE(sim.hasSchematicChoicesPending());
    SimulationTestAccess::applySchematicChoice(sim, 0);
    REQUIRE_FALSE(sim.hasSchematicChoicesPending());
}

TEST_CASE("WaveSystem: push places new enemy stations further right", "[wave]")
{
    Simulation sim(loadTestConfig(), 42);

    // Record the X position of the initial enemy stations.
    int initialX = std::numeric_limits<int>::min();
    sim.getAdmin().forEach<StationBodyComponent, FactionComponent>(
        [&](entt::entity /*e*/, const StationBodyComponent& sb, const FactionComponent& f)
        {
            if (f.isEnemy && sb.anchor.x() > initialX)
            {
                initialX = sb.anchor.x();
            }
        });

    sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
        [](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h)
        {
            if (f.isEnemy) { h.hp = -1.0f; }
        });
    sim.tick();

    int newX = std::numeric_limits<int>::min();
    sim.getAdmin().forEach<StationBodyComponent, FactionComponent>(
        [&](entt::entity /*e*/, const StationBodyComponent& sb, const FactionComponent& f)
        {
            if (f.isEnemy && sb.anchor.x() > newX)
            {
                newX = sb.anchor.x();
            }
        });

    REQUIRE(newX > initialX);
}
