#include "catch.hpp"

#include <cstdint>
#include <random>
#include <vector>

#include "ConfigLoader.h"
#include "FactionComponent.h"
#include "GameConfig.h"
#include "HealthComponent.h"
#include "Rotation.h"
#include "SchematicChoiceOption.h"
#include "Simulation.h"
#include "SimulationTestAccess.h"
#include "StateChecksum.h"
#include "StationBodyComponent.h"
#include "Tick.h"
#include "TestConfig.h"

namespace
{
constexpr int kScriptTicks = 2000;

// Ticks at which the scripted session destroys the enemy stations, and the ticks
// on which the resulting schematic choice is taken. A station dying triggers the
// choice generation (REQ-DEF-SCHEMATIC-DROP), which lands during that same tick,
// so the choice is applied on the tick after.
constexpr int kFirstStationKillTick  = 800;
constexpr int kFirstChoiceTick       = kFirstStationKillTick + 1;
constexpr int kSecondStationKillTick = 1400;
constexpr int kSecondChoiceTick      = kSecondStationKillTick + 1;

// Zeroes the HP of every enemy station, so the next tick processes their death.
void killEnemyStations(Simulation& sim)
{
    sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
        [](entt::entity, StationBodyComponent&, FactionComponent& faction,
           HealthComponent& health)
        {
            if (faction.isEnemy) { health.hp = 0.0f; }
        });
}

// Runs a fixed scripted session and returns the full-state checksum after every
// tick. The script places a small factory, deconstructs part of it mid-run, and
// otherwise lets waves/combat run so the RNG stream and ECS state are exercised.
// It also destroys the enemy stations twice and takes the offered schematic
// choice, so that unlock state (awarded groups, per-schematic levels, and the
// implicit recipe/item sets derived from them) is exercised as well and reaches
// the checksum via UnlockState::appendChecksum.
std::vector<std::uint64_t> runScriptedSession(unsigned int seed)
{
    Simulation sim(loadTestConfig(), seed);

    // Tick 0: a miner feeding a short belt line on the asteroid.
    SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-3, 0), Rotation::East);
    SimulationTestAccess::place(sim, BuildingType::Belt,  QPoint(-2, 0), Rotation::East);
    SimulationTestAccess::place(sim, BuildingType::Belt,  QPoint(-1, 0), Rotation::East);

    std::vector<std::uint64_t> checksums;
    checksums.reserve(kScriptTicks);

    for (int t = 0; t < kScriptTicks; ++t)
    {
        if (t == 500)
        {
            // Deconstruct the second belt mid-run to exercise the removal paths.
            SimulationTestAccess::place(sim, BuildingType::Smelter, QPoint(-3, 3), Rotation::East);
        }

        if (t == kFirstStationKillTick || t == kSecondStationKillTick)
        {
            killEnemyStations(sim);
        }

        if (t == kFirstChoiceTick)
        {
            // Guarded rather than assumed: if the test config ever stops offering
            // a group here, this script would silently stop covering unlock state.
            REQUIRE(sim.hasSchematicChoicesPending());
            SimulationTestAccess::applySchematicChoice(sim, 0);
        }

        // The second award is opportunistic — whether a group is still eligible
        // depends on what the first one granted and on the prerequisite gating.
        if (t == kSecondChoiceTick && sim.hasSchematicChoicesPending())
        {
            SimulationTestAccess::applySchematicChoice(sim, 0);
        }

        sim.tick();
        checksums.push_back(sim.computeStateChecksum());
    }

    return checksums;
}
}  // namespace

// ---------------------------------------------------------------------------
// Hasher
// ---------------------------------------------------------------------------

TEST_CASE("Hasher: identical inputs produce identical values", "[determinism]")
{
    Hasher a;
    Hasher b;
    a.append(42);
    a.append(3.5f);
    a.append(std::string("ore"));
    b.append(42);
    b.append(3.5f);
    b.append(std::string("ore"));

    REQUIRE(a.getValue() == b.getValue());
}

TEST_CASE("Hasher: differing inputs produce differing values", "[determinism]")
{
    Hasher a;
    Hasher b;
    a.append(42);
    b.append(43);

    REQUIRE(a.getValue() != b.getValue());
}

TEST_CASE("Hasher: string concatenation does not collide", "[determinism]")
{
    Hasher a;
    Hasher b;
    a.append(std::string("ab"));
    a.append(std::string("c"));
    b.append(std::string("a"));
    b.append(std::string("bc"));

    REQUIRE(a.getValue() != b.getValue());
}

TEST_CASE("Hasher: negative and positive zero hash equally", "[determinism]")
{
    Hasher a;
    Hasher b;
    a.append(-0.0f);
    b.append(0.0f);

    REQUIRE(a.getValue() == b.getValue());
}

// ---------------------------------------------------------------------------
// RNG fingerprint
// ---------------------------------------------------------------------------

TEST_CASE("fingerprintRng: equal states match, advanced states differ", "[determinism]")
{
    std::mt19937 a(12345);
    std::mt19937 b(12345);
    REQUIRE(fingerprintRng(a) == fingerprintRng(b));

    a();  // advance one draw
    REQUIRE(fingerprintRng(a) != fingerprintRng(b));

    b();  // advance b to the same point
    REQUIRE(fingerprintRng(a) == fingerprintRng(b));
}

TEST_CASE("Simulation::rngFingerprint is stable for equal seeds", "[determinism]")
{
    const Simulation a(loadTestConfig(), 777);
    const Simulation b(loadTestConfig(), 777);

    REQUIRE(a.getRngFingerprint() == b.getRngFingerprint());
}

// ---------------------------------------------------------------------------
// Double-run determinism
// ---------------------------------------------------------------------------

TEST_CASE("Simulation: two runs from the same seed produce identical per-tick state",
          "[determinism]")
{
    const std::vector<std::uint64_t> first  = runScriptedSession(424242);
    const std::vector<std::uint64_t> second = runScriptedSession(424242);

    REQUIRE(first.size() == second.size());
    REQUIRE(first.size() == static_cast<std::size_t>(kScriptTicks));

    for (std::size_t i = 0; i < first.size(); ++i)
    {
        INFO("divergence at tick " << i);
        REQUIRE(first[i] == second[i]);
    }
}

TEST_CASE("Simulation: different seeds diverge in state checksum", "[determinism]")
{
    const std::vector<std::uint64_t> a = runScriptedSession(111);
    const std::vector<std::uint64_t> b = runScriptedSession(222);

    // The two sessions must differ at some point (the checksum is sensitive to
    // the RNG-driven divergence; a constant checksum would be a broken hash).
    REQUIRE(a != b);
}

// ---------------------------------------------------------------------------
// Unlock state coverage
// ---------------------------------------------------------------------------

TEST_CASE("Simulation: unlock state contributes to the state checksum",
          "[determinism][unlock]")
{
    // Two sessions with identical history up to the schematic choice; only one
    // takes the choice. This pins down that awarding an unlock group actually
    // reaches the checksum, which the scripted-session tests above rely on but
    // cannot show on their own: they would still pass if UnlockState were left
    // out of the fold entirely.
    Simulation taken(loadTestConfig(), 12345u);
    Simulation skipped(loadTestConfig(), 12345u);

    for (int t = 0; t < kFirstStationKillTick; ++t)
    {
        taken.tick();
        skipped.tick();
    }
    killEnemyStations(taken);
    killEnemyStations(skipped);
    taken.tick();
    skipped.tick();

    // In lockstep before the choice, so the divergence below has one cause.
    REQUIRE(taken.computeStateChecksum() == skipped.computeStateChecksum());

    REQUIRE(taken.hasSchematicChoicesPending());

    // An artifact choice bumps m_artifactCount, which is folded separately; the
    // divergence would then not be attributable to unlock state.
    REQUIRE_FALSE(taken.getPendingSchematicChoices()[0].isArtifact);

    SimulationTestAccess::applySchematicChoice(taken, 0);

    // The pending-choice list is not itself folded into the checksum, so the
    // only state that changed is the unlock bookkeeping.
    REQUIRE(taken.computeStateChecksum() != skipped.computeStateChecksum());
}
