#include "catch.hpp"

#include <memory>
#include <string>

#include <QDir>
#include <QFile>

#include "Command.h"
#include "CommandManager.h"
#include "CommandSerializer.h"
#include "ConfigLoader.h"
#include "GameConfig.h"
#include "ReplayPlayer.h"
#include "ReplayReader.h"
#include "ReplayRecorder.h"
#include "Rotation.h"
#include "Simulation.h"
#include "TestConfig.h"

namespace
{
std::string tempOutputDir()
{
    return (QDir::tempPath() + "/dota_factory_replay_playback_test").toStdString();
}

std::shared_ptr<PlaceBuildingCommand> place(BuildingType type, QPoint anchor)
{
    std::shared_ptr<PlaceBuildingCommand> command = std::make_shared<PlaceBuildingCommand>();
    command->type   = type;
    command->anchor = anchor;
    return command;
}

void requireRoundTrip(const Command& command)
{
    const std::string text = serializeCommand(command);
    const std::shared_ptr<Command> parsed = parseCommand(text);
    REQUIRE(parsed != nullptr);
    REQUIRE(serializeCommand(*parsed) == text);
}
}  // namespace

// ---------------------------------------------------------------------------
// Round-trip
// ---------------------------------------------------------------------------

TEST_CASE("parseCommand inverts serializeCommand", "[replay]")
{
    PlaceBuildingCommand placeCommand;
    placeCommand.type     = BuildingType::Shipyard;
    placeCommand.anchor   = QPoint(-3, 2);
    placeCommand.rotation = Rotation::West;
    placeCommand.recipeId = "some_ship";
    ShipLayoutConfig layout;
    layout.placedModules.push_back(PlacedModule{"weapon_basic", QPoint(1, 0), Rotation::North});
    placeCommand.shipLayout = layout;

    const std::string text = serializeCommand(placeCommand);
    const std::shared_ptr<Command> parsed = parseCommand(text);
    REQUIRE(parsed != nullptr);
    REQUIRE(serializeCommand(*parsed) == text);
}

TEST_CASE("parseCommand round-trips every command verb", "[replay]")
{
    SetSplitterFiltersCommand filters;
    filters.tile    = QPoint(3, 9);
    filters.filterA = { ItemType{"iron_ore"} };
    filters.filterB = { ItemType{"coal"}, ItemType{"copper_ore"} };

    ClearBeltTilesCommand clear;
    clear.tiles = { QPoint(0, 0), QPoint(-1, 4) };

    DeconstructCommand deconstruct;
    deconstruct.id = 5;

    for (const Command* command : { static_cast<const Command*>(&filters),
                                    static_cast<const Command*>(&clear),
                                    static_cast<const Command*>(&deconstruct) })
    {
        const std::string text = serializeCommand(*command);
        const std::shared_ptr<Command> parsed = parseCommand(text);
        REQUIRE(parsed != nullptr);
        REQUIRE(serializeCommand(*parsed) == text);
    }
}

TEST_CASE("parseCommand rejects malformed input", "[replay]")
{
    REQUIRE(parseCommand("") == nullptr);
    REQUIRE(parseCommand("place not_a_type 0 0 E") == nullptr);
    REQUIRE(parseCommand("place miner 0 0 E bogus") == nullptr);
    REQUIRE(parseCommand("nonsense 1 2 3") == nullptr);
}

TEST_CASE("every command verb round-trips through serialize/parse", "[replay]")
{
    SetRecipeCommand setRecipe;
    setRecipe.id       = 4;
    setRecipe.recipeId = "smelt_iron";
    requireRoundTrip(setRecipe);

    SetShipLayoutCommand setLayout;
    setLayout.id = 9;
    setLayout.layout.placedModules.push_back(PlacedModule{"weapon_basic", QPoint(0, 1), Rotation::East});
    setLayout.layout.placedModules.push_back(PlacedModule{"armor", QPoint(2, -1), Rotation::South});
    requireRoundTrip(setLayout);

    SetSiteSplitterFiltersCommand siteFilters;
    siteFilters.id      = 11;
    siteFilters.filterA = { ItemType{"iron_ore"} };
    siteFilters.filterB = {};
    requireRoundTrip(siteFilters);

    RotateInPlaceCommand rotate;
    rotate.id          = 3;
    rotate.newRotation = Rotation::South;
    requireRoundTrip(rotate);

    ApplySchematicChoiceCommand schematic;
    schematic.choiceIndex = 1;
    requireRoundTrip(schematic);

    PlaceBuildingCommand placeWithFilters;
    placeWithFilters.type               = BuildingType::Splitter;
    placeWithFilters.anchor             = QPoint(2, 2);
    placeWithFilters.hasSplitterFilters = true;
    placeWithFilters.splitterFilterA    = { ItemType{"iron_ore"}, ItemType{"coal"} };
    placeWithFilters.splitterFilterB    = { ItemType{"copper_ore"} };
    requireRoundTrip(placeWithFilters);
}

// ---------------------------------------------------------------------------
// Record -> read -> replay equivalence
// ---------------------------------------------------------------------------

TEST_CASE("a recorded run replays to byte-identical state with no desync", "[replay]")
{
    const unsigned int seed = 314159u;

    // --- Record a scripted run, mimicking the frame cadence (drain, then ticks). ---
    std::string replayPath;
    std::uint64_t recordedFinalChecksum = 0;
    {
        Simulation rec(loadTestConfig(), seed);
        CommandManager manager(rec);
        std::unique_ptr<ReplayRecorder> recorder =
            std::make_unique<ReplayRecorder>(CONFIG_DIR, tempOutputDir());
        ReplayRecorder* recorderPtr = recorder.get();
        manager.setRecorder(std::move(recorder));

        // Frame at tick 0: place a miner.
        manager.enqueue(place(BuildingType::Miner, QPoint(-3, 0)));
        manager.drain();
        for (int i = 0; i < 90; ++i) { rec.tick(); manager.recordTickCheckpoint(); }

        // Frame at tick 90 (a checksum boundary): place a belt — exercises the
        // periodic-checksum-then-command ordering at one tick.
        manager.enqueue(place(BuildingType::Belt, QPoint(-2, 0)));
        manager.drain();
        for (int i = 0; i < 60; ++i) { rec.tick(); manager.recordTickCheckpoint(); }

        replayPath = recorderPtr->getCurrentFilePath();
        recordedFinalChecksum = rec.computeStateChecksum();
        manager.setRecorder(nullptr);  // close the file
    }

    // --- Read it back. ---
    const std::optional<ParsedReplay> parsed = readReplayFile(replayPath);
    REQUIRE(parsed.has_value());
    REQUIRE(parsed->header.seed == seed);
    REQUIRE(parsed->header.version == 1);
    REQUIRE_FALSE(parsed->entries.empty());

    // --- Replay it. ---
    Simulation play(loadTestConfig(), parsed->header.seed);
    ReplayPlayer player(play, parsed->entries);
    player.start();
    while (!player.isFinished())
    {
        play.tick();
        player.advanceTo(play.getCurrentTick());
    }

    REQUIRE_FALSE(player.getDesyncTick().has_value());
    REQUIRE(play.getCurrentTick() == 150);
    REQUIRE(play.computeStateChecksum() == recordedFinalChecksum);

    QFile::remove(QString::fromStdString(replayPath));
}

TEST_CASE("ReplayPlayer reports a desync when a checksum is corrupted", "[replay]")
{
    const unsigned int seed = 5u;

    std::string replayPath;
    {
        Simulation rec(loadTestConfig(), seed);
        CommandManager manager(rec);
        std::unique_ptr<ReplayRecorder> recorder =
            std::make_unique<ReplayRecorder>(CONFIG_DIR, tempOutputDir());
        ReplayRecorder* recorderPtr = recorder.get();
        manager.setRecorder(std::move(recorder));
        for (int i = 0; i < 60; ++i) { rec.tick(); manager.recordTickCheckpoint(); }
        replayPath = recorderPtr->getCurrentFilePath();
        manager.setRecorder(nullptr);
    }

    std::optional<ParsedReplay> parsed = readReplayFile(replayPath);
    REQUIRE(parsed.has_value());

    // Corrupt the periodic checksum recorded at tick 30.
    bool corrupted = false;
    for (ReplayEntry& entry : parsed->entries)
    {
        if (!entry.isCommand && entry.tick == 30)
        {
            entry.fingerprint ^= 0x1ull;
            corrupted = true;
            break;
        }
    }
    REQUIRE(corrupted);

    Simulation play(loadTestConfig(), parsed->header.seed);
    ReplayPlayer player(play, parsed->entries);
    player.start();
    while (!player.isFinished())
    {
        play.tick();
        player.advanceTo(play.getCurrentTick());
    }

    REQUIRE(player.getDesyncTick().has_value());
    REQUIRE(*player.getDesyncTick() == 30);

    QFile::remove(QString::fromStdString(replayPath));
}

TEST_CASE("a long recorded run (through waves and combat) replays with no desync", "[replay]")
{
    const unsigned int seed = 271828u;

    std::string   replayPath;
    std::uint64_t recordedFinalChecksum = 0;
    {
        Simulation rec(loadTestConfig(), seed);
        CommandManager manager(rec);
        std::unique_ptr<ReplayRecorder> recorder =
            std::make_unique<ReplayRecorder>(CONFIG_DIR, tempOutputDir());
        ReplayRecorder* recorderPtr = recorder.get();
        manager.setRecorder(std::move(recorder));

        std::shared_ptr<PlaceBuildingCommand> miner = place(BuildingType::Miner, QPoint(-3, 0));
        miner->recipeId = "mine_iron_ore";
        manager.enqueue(miner);
        manager.drain();
        for (int i = 0; i < 1500; ++i) { rec.tick(); manager.recordTickCheckpoint(); }

        manager.enqueue(place(BuildingType::Belt, QPoint(-2, 0)));
        manager.drain();
        for (int i = 0; i < 900; ++i) { rec.tick(); manager.recordTickCheckpoint(); }

        replayPath = recorderPtr->getCurrentFilePath();
        recordedFinalChecksum = rec.computeStateChecksum();
        manager.setRecorder(nullptr);
    }

    const std::optional<ParsedReplay> parsed = readReplayFile(replayPath);
    REQUIRE(parsed.has_value());

    Simulation play(loadTestConfig(), parsed->header.seed);
    ReplayPlayer player(play, parsed->entries);
    player.start();
    while (!player.isFinished())
    {
        play.tick();
        player.advanceTo(play.getCurrentTick());
    }

    REQUIRE_FALSE(player.getDesyncTick().has_value());
    REQUIRE(play.getCurrentTick() == 2400);
    REQUIRE(play.computeStateChecksum() == recordedFinalChecksum);

    QFile::remove(QString::fromStdString(replayPath));
}
