#include <doctest/doctest.h>

#include "ccm/infra/JsonSetRepository.hpp"
#include "ccm/services/ConfigService.hpp"

#include "fakes/InMemoryFileSystem.hpp"

#include <nlohmann/json.hpp>

using namespace ccm;
using ccm::testing::InMemoryFileSystem;

namespace {
std::string dirNameFn(Game g) { return g == Game::Magic ? "magic" : "pokemon"; }

ConfigService makeConfig(InMemoryFileSystem& fs, const std::string& dataDir) {
    Configuration c;
    c.dataStorage = dataDir;
    c.defaultGame = Game::Magic;
    fs.writeText("/app/config.json", nlohmann::json(c).dump());
    ConfigService cfg{fs, "/app/config.json", dataDir};
    cfg.initialize();
    return cfg;
}

class FailingSetFs final : public IFileSystem {
public:
    bool ensureOk{true};
    bool writeOk{true};
    bool readOk{true};
    std::string readPayload{"[]"};
    std::filesystem::path lastWritePath;
    std::string lastWriteBody;
    std::filesystem::path lastReadPath;

    [[nodiscard]] bool exists(const std::filesystem::path&) const override { return true; }
    [[nodiscard]] bool isDirectory(const std::filesystem::path&) const override { return true; }

    Result<void> ensureDirectory(const std::filesystem::path&) override {
        if (!ensureOk) return Result<void>::err("ensure failed");
        return Result<void>::ok();
    }
    Result<std::string> readText(const std::filesystem::path&) override {
        lastReadPath = std::filesystem::path("/tracked/read/path");
        if (!readOk) return Result<std::string>::err("read failed");
        return Result<std::string>::ok(readPayload);
    }
    Result<void> writeText(const std::filesystem::path& p, std::string_view contents) override {
        if (!writeOk) return Result<void>::err("write failed");
        lastWritePath = p;
        lastWriteBody = std::string(contents);
        return Result<void>::ok();
    }
    Result<void> copyFile(const std::filesystem::path&, const std::filesystem::path&, bool) override {
        return Result<void>::ok();
    }
    Result<void> remove(const std::filesystem::path&) override { return Result<void>::ok(); }
    Result<std::vector<std::filesystem::path>> listDirectory(const std::filesystem::path&) override {
        return Result<std::vector<std::filesystem::path>>::ok({});
    }
};
}  // namespace

TEST_SUITE("JsonSetRepository") {
    TEST_CASE("save then load returns identical sets") {
        InMemoryFileSystem fs;
        auto cfg = makeConfig(fs, "/data");
        JsonSetRepository repo{fs, cfg, dirNameFn};

        const std::vector<Set> sets = {
            {"lea", "Limited Edition Alpha", "1993/08/05"},
            {"leb", "Limited Edition Beta",  "1993/10/04"},
        };
        REQUIRE(repo.save(Game::Magic, sets).isOk());

        const auto loaded = repo.load(Game::Magic);
        REQUIRE(loaded.isOk());
        CHECK(loaded.value() == sets);
    }

    TEST_CASE("load before any save reports a clear error") {
        InMemoryFileSystem fs;
        auto cfg = makeConfig(fs, "/data");
        JsonSetRepository repo{fs, cfg, dirNameFn};

        const auto loaded = repo.load(Game::Pokemon);
        CHECK(loaded.isErr());
    }

    TEST_CASE("load propagates read errors from filesystem") {
        InMemoryFileSystem configFs;
        auto cfg = makeConfig(configFs, "/data");
        FailingSetFs fs;
        fs.readOk = false;
        JsonSetRepository repo{fs, cfg, dirNameFn};

        const auto loaded = repo.load(Game::Magic);
        REQUIRE(loaded.isErr());
        CHECK(loaded.error() == "read failed");
    }

    TEST_CASE("load reports parse error for malformed sets.json") {
        InMemoryFileSystem configFs;
        auto cfg = makeConfig(configFs, "/data");
        FailingSetFs fs;
        fs.readPayload = "{bad json";
        JsonSetRepository repo{fs, cfg, dirNameFn};

        const auto loaded = repo.load(Game::Magic);
        REQUIRE(loaded.isErr());
        CHECK(loaded.error().find("sets.json parse error:") != std::string::npos);
    }

    TEST_CASE("load reports parse error for wrong JSON shape") {
        InMemoryFileSystem configFs;
        auto cfg = makeConfig(configFs, "/data");
        FailingSetFs fs;
        fs.readPayload = R"({"not":"an array"})";
        JsonSetRepository repo{fs, cfg, dirNameFn};

        const auto loaded = repo.load(Game::Magic);
        REQUIRE(loaded.isErr());
        CHECK(loaded.error().find("sets.json parse error:") != std::string::npos);
    }

    TEST_CASE("save propagates ensureDirectory and writeText failures") {
        InMemoryFileSystem configFs;
        auto cfg = makeConfig(configFs, "/data");
        FailingSetFs fs;
        JsonSetRepository repo{fs, cfg, dirNameFn};

        const std::vector<Set> sets = {{"lea", "Limited Edition Alpha", "1993/08/05"}};

        fs.ensureOk = false;
        const auto ensureFail = repo.save(Game::Magic, sets);
        REQUIRE(ensureFail.isErr());
        CHECK(ensureFail.error() == "ensure failed");

        fs.ensureOk = true;
        fs.writeOk = false;
        const auto writeFail = repo.save(Game::Magic, sets);
        REQUIRE(writeFail.isErr());
        CHECK(writeFail.error() == "write failed");
    }

    TEST_CASE("paths use region-specific filenames for Pokemon West and Asia") {
        InMemoryFileSystem fs;
        auto cfg = makeConfig(fs, "/data");
        JsonSetRepository repo{fs, cfg, dirNameFn};
        const std::vector<Set> west = {{"base1", "Base Set", "1999/01/09"}};
        const std::vector<Set> asia = {{"SV1a", "Triplet Beat", "2023/01/20"}};

        REQUIRE(repo.save(Game::Pokemon, west).isOk());
        REQUIRE(repo.save(Game::JapanesePokemon, asia).isOk());
        CHECK(fs.files().count("/data/pokemon/sets-west.json") == 1);
        CHECK(fs.files().count("/data/pokemon/sets-asia.json") == 1);
        CHECK(fs.files().count("/data/pokemon/sets.json") == 0);
    }

    TEST_CASE("load migrates legacy pokemon/sets.json to sets-west.json") {
        InMemoryFileSystem fs;
        auto cfg = makeConfig(fs, "/data");
        const std::vector<Set> sets = {{"base1", "Base Set", "1999/01/09"}};
        REQUIRE(fs.writeText("/data/pokemon/sets.json", nlohmann::json(sets).dump(2)).isOk());

        JsonSetRepository repo{fs, cfg, dirNameFn};
        const auto loaded = repo.load(Game::Pokemon);
        REQUIRE(loaded.isOk());
        CHECK(loaded.value() == sets);
        CHECK(fs.files().count("/data/pokemon/sets-west.json") == 1);
    }

    TEST_CASE("load migrates legacy pokemonjp/sets.json to sets-asia.json") {
        InMemoryFileSystem fs;
        auto cfg = makeConfig(fs, "/data");
        const std::vector<Set> sets = {{"SV1a", "Triplet Beat", "2023/01/20"}};
        REQUIRE(fs.writeText("/data/pokemonjp/sets.json", nlohmann::json(sets).dump(2)).isOk());

        JsonSetRepository repo{fs, cfg, dirNameFn};
        const auto loaded = repo.load(Game::JapanesePokemon);
        REQUIRE(loaded.isOk());
        CHECK(loaded.value() == sets);
        CHECK(fs.files().count("/data/pokemon/sets-asia.json") == 1);
    }

    TEST_CASE("load prefers new path over legacy when both exist") {
        InMemoryFileSystem fs;
        auto cfg = makeConfig(fs, "/data");
        const std::vector<Set> legacy = {{"old", "Old", "1999/01/01"}};
        const std::vector<Set> neu = {{"new", "New", "2024/01/01"}};
        REQUIRE(fs.writeText("/data/pokemon/sets.json", nlohmann::json(legacy).dump(2)).isOk());
        REQUIRE(fs.writeText("/data/pokemon/sets-west.json", nlohmann::json(neu).dump(2)).isOk());

        JsonSetRepository repo{fs, cfg, dirNameFn};
        const auto loaded = repo.load(Game::Pokemon);
        REQUIRE(loaded.isOk());
        CHECK(loaded.value() == neu);
    }
}
