#include <algorithm>

#include "catch.hpp"

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

// Zeros the HP of both enemy defence stations and advances one tick so that
// tickDeathsAndLoot fires, triggering the push and schematic choices.
static 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();
}

// Kills enemy stations and applies the first schematic choice (index 0).
static void killEnemyStationsAndApply(Simulation& sim)
{
    killEnemyStations(sim);
    if (sim.hasSchematicChoicesPending())
    {
        SimulationTestAccess::applySchematicChoice(sim, 0);
    }
}

// Destroys station sets until recipeId is unlocked or maxDestructions is reached.
// Applies schematic choice 0 after each destruction. Returns true if unlocked.
static bool awaitRecipeUnlock(Simulation& sim, const std::string& recipeId,
                              int maxDestructions = 150)
{
    for (int i = 0; i < maxDestructions; ++i)
    {
        if (sim.isRecipeUnlocked(recipeId)) { return true; }
        killEnemyStationsAndApply(sim);
    }
    return sim.isRecipeUnlocked(recipeId);
}

// ---------------------------------------------------------------------------
// ConfigLoader: unlocked_at_start and unlock-group gating on assembler recipes
// ---------------------------------------------------------------------------

TEST_CASE("RecipeSchematic: unlocked_at_start = true parsed correctly", "[recipe_schematic]")
{
    const GameConfig cfg = loadTestConfig();
    const auto it = std::find_if(cfg.recipes.recipes.begin(), cfg.recipes.recipes.end(),
        [](const RecipeDef& r) { return r.id == "premium_circuit"; });
    REQUIRE(it != cfg.recipes.recipes.end());
    CHECK(it->unlockedAtStart);
}

TEST_CASE("RecipeSchematic: a gated recipe is granted by an unlock group", "[recipe_schematic]")
{
    const GameConfig cfg = loadTestConfig();
    const bool granted = std::any_of(cfg.unlocks.groups.begin(), cfg.unlocks.groups.end(),
        [](const UnlockGroupDef& g)
        {
            return std::find(g.recipes.begin(), g.recipes.end(), "quick_circuit") != g.recipes.end();
        });
    CHECK(granted);
}

TEST_CASE("RecipeSchematic: an untagged assembler recipe has unlocked_at_start = false", "[recipe_schematic]")
{
    const GameConfig cfg = loadTestConfig();
    const auto it = std::find_if(cfg.recipes.recipes.begin(), cfg.recipes.recipes.end(),
        [](const RecipeDef& r) { return r.id == "circuit_board"; });
    REQUIRE(it != cfg.recipes.recipes.end());
    CHECK_FALSE(it->unlockedAtStart);
}

// ---------------------------------------------------------------------------
// Initial explicit lock state (REQ-LOCK-EXPLICIT)
// ---------------------------------------------------------------------------

TEST_CASE("RecipeSchematic: recipe with unlock_at_station_level = -1 is unlocked at game start",
          "[recipe_schematic]")
{
    const Simulation sim(loadTestConfig());
    REQUIRE(sim.isRecipeUnlocked("premium_circuit"));
}

TEST_CASE("RecipeSchematic: recipe with unlock_at_station_level = 0 is locked at game start",
          "[recipe_schematic]")
{
    const Simulation sim(loadTestConfig());
    REQUIRE_FALSE(sim.isRecipeUnlocked("quick_circuit"));
}

TEST_CASE("RecipeSchematic: recipe with unlock_at_station_level = 1 is locked at game start",
          "[recipe_schematic]")
{
    const Simulation sim(loadTestConfig());
    REQUIRE_FALSE(sim.isRecipeUnlocked("advanced_circuit"));
}

// ---------------------------------------------------------------------------
// Implicit unlock graph (REQ-LOCK-IMPLICIT)
// ---------------------------------------------------------------------------

TEST_CASE("RecipeSchematic: -1 recipe seeds its output item into the implicit unlock set",
          "[recipe_schematic]")
{
    // premium_circuit is not needed by any ship or module schematic, so it can
    // only reach the implicit set via the -1 recipe seed in Phase 1.
    const Simulation sim(loadTestConfig());
    REQUIRE(sim.isItemUnlocked("premium_circuit"));
}

TEST_CASE("RecipeSchematic: -1 recipe's inputs are in the implicit unlock set",
          "[recipe_schematic]")
{
    // premium_circuit takes circuit_board as input; that item was already
    // implicitly unlocked by ship schematics, so it must remain unlocked.
    const Simulation sim(loadTestConfig());
    REQUIRE(sim.isItemUnlocked("circuit_board"));
}

TEST_CASE("RecipeSchematic: locked recipe's unique input is not in the implicit unlock set",
          "[recipe_schematic]")
{
    // exotic_alloy has unlock_at_station_level = 0 (locked at start) and takes
    // exotic_ore as input. exotic_ore is only reachable through this locked
    // recipe, so it must not appear in the implicit set.
    const Simulation sim(loadTestConfig());
    REQUIRE_FALSE(sim.isItemUnlocked("exotic_ore"));
}

TEST_CASE("RecipeSchematic: locked recipe's output item is not in the implicit unlock set",
          "[recipe_schematic]")
{
    // exotic_alloy is produced only by the locked recipe of the same name and
    // is not needed by any schematic, so neither the item nor the recipe should
    // be unlocked at game start.
    const Simulation sim(loadTestConfig());
    REQUIRE_FALSE(sim.isItemUnlocked("exotic_alloy"));
    REQUIRE_FALSE(sim.isRecipeUnlocked("exotic_alloy"));
}

TEST_CASE("RecipeSchematic: normal implicit unlock is unaffected for untagged assembler recipes",
          "[recipe_schematic]")
{
    // circuit_board carries no unlock_at_station_level and is needed by ships
    // that start unlocked, so it must still be implicitly unlocked.
    const Simulation sim(loadTestConfig());
    REQUIRE(sim.isRecipeUnlocked("circuit_board"));
}

// ---------------------------------------------------------------------------
// Drop pool and station destruction (REQ-DEF-SCHEMATIC-DROP)
// ---------------------------------------------------------------------------

TEST_CASE("RecipeSchematic: eligible recipe schematic is eventually awarded on station destruction",
          "[recipe_schematic]")
{
    // quick_circuit has unlock_at_station_level = 0 and produces circuit_board
    // (already implicitly unlocked), so it is eligible from the first station
    // destruction. With up to 150 trials it must be awarded at least once.
    Simulation sim(loadTestConfig());
    REQUIRE(awaitRecipeUnlock(sim, "quick_circuit"));
}

TEST_CASE("RecipeSchematic: an implicitly-gated recipe with no unlock group is never awarded",
          "[recipe_schematic]")
{
    // exotic_alloy is in no unlock group and its output/inputs are unreachable
    // via the item graph, so it can never be dropped or implicitly unlocked.
    Simulation sim(loadTestConfig());
    for (int i = 0; i < 50; ++i)
    {
        killEnemyStationsAndApply(sim);
    }
    REQUIRE_FALSE(sim.isRecipeUnlocked("exotic_alloy"));
}

TEST_CASE("RecipeSchematic: recipe with level > destroyed station level is not awarded",
          "[recipe_schematic]")
{
    // advanced_circuit has unlock_at_station_level = 1. Destroying a single
    // level-0 station set must not award it regardless of the RNG outcome.
    Simulation sim(loadTestConfig());
    killEnemyStationsAndApply(sim);
    REQUIRE_FALSE(sim.isRecipeUnlocked("advanced_circuit"));
}

TEST_CASE("RecipeSchematic: recipe with higher level is awarded once eligible station level is reached",
          "[recipe_schematic]")
{
    // After enough destructions to pass station level 1, advanced_circuit must
    // eventually be awarded.
    Simulation sim(loadTestConfig());
    REQUIRE(awaitRecipeUnlock(sim, "advanced_circuit", 300));
}

TEST_CASE("RecipeSchematic: awarded recipe schematic stays unlocked and is not awarded again",
          "[recipe_schematic]")
{
    Simulation sim(loadTestConfig());
    awaitRecipeUnlock(sim, "quick_circuit");
    REQUIRE(sim.isRecipeUnlocked("quick_circuit"));

    // Destroy 30 more station sets; the recipe is no longer in the pool.
    for (int i = 0; i < 30; ++i)
    {
        killEnemyStationsAndApply(sim);
    }

    REQUIRE(sim.isRecipeUnlocked("quick_circuit"));
}

TEST_CASE("RecipeSchematic: recipe schematic can appear in pending choices",
          "[recipe_schematic]")
{
    Simulation sim(loadTestConfig());

    bool foundRecipeChoice = false;
    for (int i = 0; i < 150 && !foundRecipeChoice; ++i)
    {
        killEnemyStations(sim);
        if (sim.hasSchematicChoicesPending())
        {
            for (const SchematicChoiceOption& opt : sim.getPendingSchematicChoices())
            {
                for (const GrantedSchematic& grant : opt.grantedItems)
                {
                    if (grant.type == SchematicType::Recipe) { foundRecipeChoice = true; break; }
                }
                if (foundRecipeChoice) { break; }
            }
            SimulationTestAccess::applySchematicChoice(sim, 0);
        }
    }
    CHECK(foundRecipeChoice);
}

// ---------------------------------------------------------------------------
// reset() restores initial lock state (REQ-LOCK-EXPLICIT, REQ-CFG-RELOAD)
// ---------------------------------------------------------------------------

TEST_CASE("RecipeSchematic: reset re-locks a previously awarded recipe schematic",
          "[recipe_schematic]")
{
    Simulation sim(loadTestConfig());
    awaitRecipeUnlock(sim, "quick_circuit");
    REQUIRE(sim.isRecipeUnlocked("quick_circuit"));

    sim.reset();

    REQUIRE_FALSE(sim.isRecipeUnlocked("quick_circuit"));
}

TEST_CASE("RecipeSchematic: reset keeps -1 recipes unlocked and their seed items accessible",
          "[recipe_schematic]")
{
    Simulation sim(loadTestConfig());
    sim.reset();

    REQUIRE(sim.isRecipeUnlocked("premium_circuit"));
    REQUIRE(sim.isItemUnlocked("premium_circuit"));
}

// ---------------------------------------------------------------------------
// Unlock dialog: newly-unlocked recipe preview (REQ-DEF-SCHEMATIC-DROP)
// ---------------------------------------------------------------------------

TEST_CASE("RecipeSchematic: newlyUnlockedRecipeIds is sorted, deduplicated, and empty for level-ups",
          "[recipe_schematic]")
{
    Simulation sim(loadTestConfig());

    for (int i = 0; i < 100; ++i)
    {
        killEnemyStations(sim);
        if (!sim.hasSchematicChoicesPending()) { continue; }

        for (const SchematicChoiceOption& opt : sim.getPendingSchematicChoices())
        {
            // Strictly ascending display names imply sorted and deduplicated.
            for (std::size_t j = 1; j < opt.newlyUnlockedRecipeIds.size(); ++j)
            {
                CHECK(toDisplayName(opt.newlyUnlockedRecipeIds[j - 1])
                      < toDisplayName(opt.newlyUnlockedRecipeIds[j]));
            }
        }

        SimulationTestAccess::applySchematicChoice(sim, 0);
    }
}

TEST_CASE("RecipeSchematic: newlyUnlockedRecipeIds matches recipes that actually become unlocked",
          "[recipe_schematic]")
{
    Simulation sim(loadTestConfig());
    const GameConfig cfg = loadTestConfig();

    auto unlockedTrackedRecipeIds = [&]()
    {
        std::set<std::string> ids;
        for (const RecipeDef& def : cfg.recipes.recipes)
        {
            if ((def.building == BuildingType::Miner || def.building == BuildingType::Assembler)
                && sim.isRecipeUnlocked(def.id))
            {
                ids.insert(def.id);
            }
        }
        return ids;
    };

    for (int i = 0; i < 100; ++i)
    {
        killEnemyStations(sim);
        if (!sim.hasSchematicChoicesPending()) { continue; }

        const std::set<std::string> unlockedBefore = unlockedTrackedRecipeIds();
        const SchematicChoiceOption choice = sim.getPendingSchematicChoices()[0];

        SimulationTestAccess::applySchematicChoice(sim, 0);

        std::vector<std::string> expected;
        for (const RecipeDef& def : cfg.recipes.recipes)
        {
            if ((def.building == BuildingType::Miner || def.building == BuildingType::Assembler)
                && sim.isRecipeUnlocked(def.id) && unlockedBefore.count(def.id) == 0)
            {
                expected.push_back(def.id);
            }
        }
        std::sort(expected.begin(), expected.end(),
                  [](const std::string& lhs, const std::string& rhs)
                  {
                      return toDisplayName(lhs) < toDisplayName(rhs);
                  });

        REQUIRE(choice.newlyUnlockedRecipeIds == expected);
    }
}

// ---------------------------------------------------------------------------
// Owned ship/module schematics leave the drop pool (REQ-DEF-SCHEMATIC-DROP)
// ---------------------------------------------------------------------------

TEST_CASE("SchematicDrop: an owned ship schematic is never offered again",
          "[recipe_schematic]")
{
    // repair_ship has unlock_at_station_level = 0 in the test config, so it is
    // locked at start and becomes eligible once a station set is destroyed.
    Simulation sim(loadTestConfig());
    REQUIRE_FALSE(sim.isSchematicUnlocked("repair_ship"));

    bool wasUnlocked = false;
    for (int i = 0; i < 200; ++i)
    {
        killEnemyStations(sim);
        if (!sim.hasSchematicChoicesPending()) { continue; }

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

        // Locate the option granting repair_ship, if present in this drop.
        int repairShipIndex = -1;
        for (int j = 0; j < static_cast<int>(choices.size()); ++j)
        {
            for (const GrantedSchematic& grant : choices[j].grantedItems)
            {
                if (grant.id == "repair_ship") { repairShipIndex = j; break; }
            }
        }

        // Once owned, it must never appear in the pool again.
        if (sim.isSchematicUnlocked("repair_ship"))
        {
            CHECK(repairShipIndex == -1);
        }

        // Prefer picking repair_ship the first time it shows up so we exercise
        // the transition from unowned to owned; otherwise just take choice 0.
        int pick = 0;
        if (repairShipIndex >= 0 && !sim.isSchematicUnlocked("repair_ship"))
        {
            pick = repairShipIndex;
            wasUnlocked = true;
        }
        SimulationTestAccess::applySchematicChoice(sim, pick);
    }

    // Sanity: we actually unlocked it at some point, so the exclusion CHECK above
    // was meaningfully exercised.
    CHECK(wasUnlocked);
    CHECK(sim.isSchematicUnlocked("repair_ship"));
}

