// Drop-pool behaviour for unlock-group prerequisites (REQ-LOCK-PREREQ). An
// unlock group enters the drop pool only when every id in its `requires` names a
// group that has already been awarded, on top of the station-level check.
//
// Configs are built in memory (load the test config, then mutate the unlock
// groups) so each case is isolated and does not perturb the shared test config
// used by other suites.

#include <algorithm>
#include <string>
#include <utility>
#include <vector>

#include "catch.hpp"

#include "ConfigLoader.h"
#include "FactionComponent.h"
#include "GameConfig.h"
#include "HealthComponent.h"
#include "SchematicChoiceOption.h"
#include "Simulation.h"
#include "SimulationTestAccess.h"
#include "StationBodyComponent.h"
#include "UnlocksConfig.h"
#include "TestConfig.h"

namespace
{

UnlockGroupDef& findGroup(GameConfig& cfg, const std::string& id)
{
    for (UnlockGroupDef& group : cfg.unlocks.groups)
    {
        if (group.id == id) { return group; }
    }
    FAIL("unlock group not found: " + id);
    return cfg.unlocks.groups.front();
}

// Zeros the HP of both enemy defence stations and advances one tick so that
// tickDeathsAndLoot fires, triggering the push and schematic choices.
void killEnemyStations(Simulation& sim)
{
    sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
        [](entt::entity, StationBodyComponent&, FactionComponent& faction, HealthComponent& health)
        {
            if (faction.isEnemy)
            {
                health.hp = 0.0f;
            }
        });
    sim.tick();
}

void killEnemyStationsAndApply(Simulation& sim)
{
    killEnemyStations(sim);
    if (sim.hasSchematicChoicesPending())
    {
        SimulationTestAccess::applySchematicChoice(sim, 0);
    }
}

// True if any pending option grants the given schematic id.
bool optionOffered(const Simulation& sim, const std::string& id)
{
    const std::vector<SchematicChoiceOption>& options = sim.getPendingSchematicChoices();
    return std::any_of(options.begin(), options.end(),
        [&](const SchematicChoiceOption& option)
        {
            return std::any_of(option.grantedItems.begin(), option.grantedItems.end(),
                [&](const GrantedSchematic& grant) { return grant.id == id; });
        });
}

} // namespace

TEST_CASE("UnlockPrereq: a group gated behind a locked ship is withheld from the pool",
          "[unlock_prereq]")
{
    // quick_circuit (recipe group, level 0) would normally be eligible at the
    // first station destruction. Gate it behind the repair_ship group (level 0,
    // locked at game start).
    GameConfig cfg = loadTestConfig();
    findGroup(cfg, "quick_circuit").requiredGroupIds = {"repair_ship"};

    Simulation sim(std::move(cfg), 123);
    REQUIRE_FALSE(sim.isSchematicUnlocked("repair_ship"));

    killEnemyStations(sim);  // destroyed set level 0
    REQUIRE(sim.hasSchematicChoicesPending());

    // Prerequisite unmet -> the gated group must not be offered, while the
    // prerequisite group itself (the only other eligible pick) is.
    CHECK_FALSE(optionOffered(sim, "quick_circuit"));
    CHECK(optionOffered(sim, "repair_ship"));
}

TEST_CASE("UnlockPrereq: the gated group becomes eligible once its prerequisite is awarded",
          "[unlock_prereq]")
{
    GameConfig cfg = loadTestConfig();
    findGroup(cfg, "quick_circuit").requiredGroupIds = {"repair_ship"};

    Simulation sim(std::move(cfg), 123);

    // Applying choice 0 each drop unlocks repair_ship first (the only eligible
    // pick at level 0), which then opens the quick_circuit group for a later drop.
    bool unlocked = false;
    for (int i = 0; i < 150 && !unlocked; ++i)
    {
        killEnemyStationsAndApply(sim);
        unlocked = sim.isRecipeUnlocked("quick_circuit");
    }

    CHECK(sim.isSchematicUnlocked("repair_ship"));  // prerequisite got unlocked along the way
    CHECK(unlocked);
}

TEST_CASE("UnlockPrereq: a module group gated behind another module is withheld until it unlocks",
          "[unlock_prereq]")
{
    // Make laser_cannon and armor_plate (both start-unlocked in the test config)
    // lockable via new unlock groups, with armor_plate gated behind laser_cannon.
    GameConfig cfg = loadTestConfig();
    cfg.unlocks.groups.push_back(
        UnlockGroupDef{"laser_cannon", 0, {}, {}, {"laser_cannon"}, {}, {}});
    cfg.unlocks.groups.push_back(
        UnlockGroupDef{"armor_plate", 0, {"laser_cannon"}, {}, {"armor_plate"}, {}, {}});

    Simulation sim(std::move(cfg), 7);
    REQUIRE_FALSE(sim.isModuleSchematicUnlocked("laser_cannon"));
    REQUIRE_FALSE(sim.isModuleSchematicUnlocked("armor_plate"));

    killEnemyStations(sim);  // level 0
    REQUIRE(sim.hasSchematicChoicesPending());
    // armor_plate is gated behind the still-locked laser_cannon module.
    CHECK_FALSE(optionOffered(sim, "armor_plate"));

    // Drive the drops until the gated module unlocks; it can only do so after its
    // prerequisite has itself been unlocked.
    bool armorUnlocked = false;
    for (int i = 0; i < 300 && !armorUnlocked; ++i)
    {
        killEnemyStationsAndApply(sim);
        armorUnlocked = sim.isModuleSchematicUnlocked("armor_plate");
        if (armorUnlocked)
        {
            CHECK(sim.isModuleSchematicUnlocked("laser_cannon"));
        }
    }
    CHECK(armorUnlocked);
}
