#include "catch.hpp"

#include <fstream>
#include <sstream>
#include <string>

#include <memory>

#include <QDir>
#include <QFile>

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

namespace
{
std::string readFile(const std::string& path)
{
    std::ifstream stream(path, std::ios::in | std::ios::binary);
    std::ostringstream buffer;
    buffer << stream.rdbuf();
    return buffer.str();
}

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

// ---------------------------------------------------------------------------
// Command serialization
// ---------------------------------------------------------------------------

TEST_CASE("serializeCommand: plain placement", "[replay]")
{
    PlaceBuildingCommand command;
    command.type     = BuildingType::Miner;
    command.anchor   = QPoint(-3, 5);
    command.rotation = Rotation::East;

    REQUIRE(serializeCommand(command) == "place miner -3 5 E");
}

TEST_CASE("serializeCommand: placement with recipe", "[replay]")
{
    PlaceBuildingCommand command;
    command.type     = BuildingType::Miner;
    command.anchor   = QPoint(-2, 0);
    command.rotation = Rotation::North;
    command.recipeId = "mine_iron_ore";

    REQUIRE(serializeCommand(command) == "place miner -2 0 N recipe mine_iron_ore");
}

TEST_CASE("serializeCommand: placement with ship layout", "[replay]")
{
    PlaceBuildingCommand command;
    command.type     = BuildingType::Shipyard;
    command.anchor   = QPoint(-3, 0);
    command.rotation = Rotation::East;
    ShipLayoutConfig layout;
    layout.placedModules.push_back(PlacedModule{"weapon_basic", QPoint(1, 2), Rotation::South});
    command.shipLayout = layout;

    REQUIRE(serializeCommand(command)
            == "place shipyard -3 0 E layout 1 weapon_basic 1 2 S");
}

TEST_CASE("serializeCommand: splitter filters are length-prefixed", "[replay]")
{
    SetSplitterFiltersCommand command;
    command.tile    = QPoint(4, 7);
    command.filterA = { ItemType{"iron_ore"}, ItemType{"copper_ore"} };
    command.filterB = { ItemType{"coal"} };

    REQUIRE(serializeCommand(command)
            == "splitterfilters 4 7 2 iron_ore copper_ore 1 coal");
}

TEST_CASE("serializeCommand: deconstruct / rotate / schematic / clearbelt", "[replay]")
{
    DeconstructCommand deconstruct;
    deconstruct.id = 12;
    REQUIRE(serializeCommand(deconstruct) == "deconstruct 12");

    RotateInPlaceCommand rotate;
    rotate.id          = 7;
    rotate.newRotation = Rotation::West;
    REQUIRE(serializeCommand(rotate) == "rotate 7 W");

    ApplySchematicChoiceCommand schematic;
    schematic.choiceIndex = 2;
    REQUIRE(serializeCommand(schematic) == "schematic 2");

    ClearBeltTilesCommand clear;
    clear.tiles = { QPoint(1, 2), QPoint(3, 4) };
    REQUIRE(serializeCommand(clear) == "clearbelt 2 1 2 3 4");
}

// ---------------------------------------------------------------------------
// ReplayRecorder file output
// ---------------------------------------------------------------------------

TEST_CASE("ReplayRecorder writes a well-formed file", "[replay]")
{
    ReplayRecorder recorder(CONFIG_DIR, tempOutputDir());
    recorder.startNewRun(42u, 0x1122334455667788ull);

    PlaceBuildingCommand place;
    place.type     = BuildingType::Miner;
    place.anchor   = QPoint(-3, 0);
    place.rotation = Rotation::East;
    recorder.recordCommand(5, place, 0xabcdef0123456789ull);

    recorder.recordChecksum(30, 0x0ffffffffffffff0ull);

    const std::string path = recorder.getCurrentFilePath();
    REQUIRE_FALSE(path.empty());
    recorder.close();

    const std::string content = readFile(path);

    // Header.
    REQUIRE(content.find("# dota_factory replay") != std::string::npos);
    REQUIRE(content.find("version 1") != std::string::npos);
    REQUIRE(content.find("seed 42") != std::string::npos);
    REQUIRE(content.find("config_hash ") != std::string::npos);
    REQUIRE(content.find("---") != std::string::npos);

    // Initial + per-command + periodic checksums.
    REQUIRE(content.find("# checksum 0 1122334455667788") != std::string::npos);
    REQUIRE(content.find("5 place miner -3 0 E") != std::string::npos);
    REQUIRE(content.find("# checksum 5 abcdef0123456789") != std::string::npos);
    REQUIRE(content.find("# checksum 30 0ffffffffffffff0") != std::string::npos);

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

TEST_CASE("CommandManager records commands and an initial checksum on drain", "[replay]")
{
    Simulation sim(loadTestConfig(), 7u);
    CommandManager manager(sim);

    std::unique_ptr<ReplayRecorder> recorder =
        std::make_unique<ReplayRecorder>(CONFIG_DIR, tempOutputDir());
    ReplayRecorder* recorderPtr = recorder.get();
    // setRecorder opens the file and writes the header + the tick-0 checksum.
    manager.setRecorder(std::move(recorder));
    const std::string path = recorderPtr->getCurrentFilePath();
    REQUIRE_FALSE(path.empty());

    std::shared_ptr<PlaceBuildingCommand> place = std::make_shared<PlaceBuildingCommand>();
    place->type   = BuildingType::Miner;
    place->anchor = QPoint(-3, 0);
    manager.enqueue(place);
    manager.drain();

    const std::string content = readFile(path);
    REQUIRE(content.find("seed 7") != std::string::npos);
    REQUIRE(content.find("# checksum 0 ") != std::string::npos);
    // Drained at tick 0 (no ticks have run), so the command line is tagged tick 0.
    REQUIRE(content.find("0 place miner -3 0 E") != std::string::npos);

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

TEST_CASE("ReplayRecorder startNewRun rolls to a new file", "[replay]")
{
    ReplayRecorder recorder(CONFIG_DIR, tempOutputDir());

    recorder.startNewRun(1u, 0ull);
    const std::string first = recorder.getCurrentFilePath();

    recorder.startNewRun(2u, 0ull);
    const std::string second = recorder.getCurrentFilePath();

    REQUIRE(first != second);
    REQUIRE(second.find("_2.replay") != std::string::npos);
    recorder.close();
}

TEST_CASE("CommandManager rolls the replay file on a Reset command", "[replay]")
{
    Simulation sim(loadTestConfig(), 1u);
    CommandManager manager(sim);

    std::unique_ptr<ReplayRecorder> recorder =
        std::make_unique<ReplayRecorder>(CONFIG_DIR, tempOutputDir());
    ReplayRecorder* recorderPtr = recorder.get();
    manager.setRecorder(std::move(recorder));
    const std::string firstPath = recorderPtr->getCurrentFilePath();

    std::shared_ptr<ResetCommand> reset = std::make_shared<ResetCommand>();
    reset->config = std::make_shared<GameConfig>(loadTestConfig());
    reset->seed   = 999u;
    manager.enqueue(reset);
    manager.drain();

    const std::string secondPath = recorderPtr->getCurrentFilePath();
    REQUIRE(firstPath != secondPath);
    REQUIRE(secondPath.find("_999.replay") != std::string::npos);

    manager.setRecorder(nullptr);
    QFile::remove(QString::fromStdString(firstPath));
    QFile::remove(QString::fromStdString(secondPath));
}
