BareGit
#include "game_service.h"

#include <chrono>
#include <cstdint>
#include <filesystem>
#include <memory>
#include <optional>
#include <string>
#include <vector>

#include <gtest/gtest.h>

#include "startup.h"

namespace
{

class TemporaryDatabase
{
public:
    /// Allocate a unique database path for one test.
    TemporaryDatabase()
            : path_(
                std::filesystem::path(testing::TempDir()) /
                ("game_service_" + std::to_string(
                    std::chrono::steady_clock::now()
                        .time_since_epoch().count()) + ".sqlite3"))
    {}

    /// Remove the database and SQLite sidecars.
    ~TemporaryDatabase()
    {
        std::error_code error;
        std::filesystem::remove(path_, error);
        std::filesystem::remove(path_.string() + "-shm", error);
        std::filesystem::remove(path_.string() + "-wal", error);
    }

    /// Return the allocated path.
    const std::filesystem::path& path() const
    {
        return path_;
    }

private:
    std::filesystem::path path_;
};

struct ServiceFixture
{
    std::unique_ptr<DataSourceInterface> data_source;
    std::int64_t administrator_id;
    std::int64_t player_id;
};

mw::E<ServiceFixture> prepareFixture(const std::filesystem::path& path)
{
    auto data_source = prepareDataSource(path);
    if(!data_source)
    {
        return std::unexpected(std::move(data_source.error()));
    }
    auto transaction = (*data_source)->beginTransaction();
    if(!transaction)
    {
        return std::unexpected(std::move(transaction.error()));
    }
    auto named = (*transaction)->updateUsername(
        1, "Administrator", "administrator");
    if(!named)
    {
        return std::unexpected(std::move(named.error()));
    }
    User player = {
        0,
        "player@example.com",
        "player@example.com",
        std::nullopt,
        UserRole::PLAYER,
        1,
        0,
        0};
    auto player_id = (*transaction)->insertUser(player);
    if(!player_id)
    {
        return std::unexpected(std::move(player_id.error()));
    }
    named = (*transaction)->updateUsername(
        *player_id, "Player", "player");
    if(!named)
    {
        return std::unexpected(std::move(named.error()));
    }
    auto committed = (*transaction)->commit();
    if(!committed)
    {
        return std::unexpected(std::move(committed.error()));
    }
    return ServiceFixture{std::move(*data_source), 1, *player_id};
}

const mw::HTTPError* httpError(const mw::Error& error)
{
    return error.as<mw::HTTPError>();
}

} // namespace

/// Verify complete game definitions mutate atomically and advance revisions.
TEST(GameServiceTest, MutatesDefinitionsAndRejectsStaleRevisions)
{
    TemporaryDatabase database;
    auto fixture = prepareFixture(database.path());
    ASSERT_TRUE(fixture) << fixture.error().msg();
    GameService service(*fixture->data_source);

    auto created = service.createGame(
        fixture->administrator_id, "demo", " Demo Game ", "A **game**.",
        GameVisibility::PUBLIC);
    ASSERT_TRUE(created) << created.error().msg();
    EXPECT_EQ(*created, "demo");

    auto integer_id = service.createField(
        fixture->administrator_id,
        "demo",
        1,
        "score",
        " Score ",
        GameFieldType::INTEGER,
        {});
    ASSERT_TRUE(integer_id) << integer_id.error().msg();
    auto string_id = service.createField(
        fixture->administrator_id,
        "demo",
        2,
        "note",
        "Note",
        GameFieldType::STRING,
        {});
    ASSERT_TRUE(string_id) << string_id.error().msg();
    auto choice_id = service.createField(
        fixture->administrator_id,
        "demo",
        3,
        "route",
        "Route",
        GameFieldType::CHOICE,
        {"Forest", "Coast"});
    ASSERT_TRUE(choice_id) << choice_id.error().msg();

    auto duplicate_choices = service.createField(
        fixture->administrator_id,
        "demo",
        4,
        "duplicate",
        "Duplicate",
        GameFieldType::CHOICE,
        {"Same", "Same"});
    ASSERT_FALSE(duplicate_choices);
    ASSERT_NE(httpError(duplicate_choices.error()), nullptr);
    EXPECT_EQ(httpError(duplicate_choices.error())->code, 409);

    auto stale = service.updateGame(
        fixture->administrator_id, "demo", 3, "Stale", "",
        GameVisibility::PUBLIC);
    ASSERT_FALSE(stale);
    ASSERT_NE(httpError(stale.error()), nullptr);
    EXPECT_EQ(httpError(stale.error())->code, 409);

    ASSERT_TRUE(service.reorderFields(
        fixture->administrator_id,
        "demo",
        4,
        {*choice_id, *string_id, *integer_id}));
    ASSERT_TRUE(service.updateField(
        fixture->administrator_id,
        "demo",
        *choice_id,
        5,
        "Path",
        {"Coast", "Forest", "Town"}));
    ASSERT_TRUE(service.updateGame(
        fixture->administrator_id,
        "demo",
        6,
        "Renamed Game",
        "New description",
        GameVisibility::INTERNAL));

    auto definition = fixture->data_source->getGameDefinition(
        "demo", GameContentScope::INCLUDE_INTERNAL);
    ASSERT_TRUE(definition);
    ASSERT_TRUE(*definition);
    EXPECT_EQ((**definition).game.display_name, "Renamed Game");
    EXPECT_EQ((**definition).game.visibility, GameVisibility::INTERNAL);
    EXPECT_EQ((**definition).game.revision, 7);
    auto public_definition = fixture->data_source->getGameDefinition(
        "demo", GameContentScope::PUBLIC_ONLY);
    ASSERT_TRUE(public_definition);
    EXPECT_FALSE(*public_definition);
    ASSERT_EQ((**definition).fields.size(), 3);
    EXPECT_EQ((**definition).fields[0].id, *choice_id);
    EXPECT_EQ((**definition).fields[0].label, "Path");
    ASSERT_EQ((**definition).fields[0].choices.size(), 3);
    EXPECT_EQ((**definition).fields[0].choices[0].value, "Coast");
    EXPECT_EQ((**definition).fields[0].choices[2].value, "Town");
}

/// Verify definition authorization, identity validation, and usage conflicts.
TEST(GameServiceTest, EnforcesAuthorizationAndDeletionRules)
{
    TemporaryDatabase database;
    auto fixture = prepareFixture(database.path());
    ASSERT_TRUE(fixture) << fixture.error().msg();
    GameService service(*fixture->data_source);

    auto forbidden = service.createGame(
        fixture->player_id, "player", "Player Game", "",
        GameVisibility::PUBLIC);
    ASSERT_FALSE(forbidden);
    ASSERT_NE(httpError(forbidden.error()), nullptr);
    EXPECT_EQ(httpError(forbidden.error())->code, 403);

    auto invalid = service.createGame(
        fixture->administrator_id, "d\xc3\xa9mo", "Invalid", "",
        GameVisibility::PUBLIC);
    ASSERT_FALSE(invalid);
    const auto* validation =
        invalid.error().as<GameDefinitionValidationError>();
    ASSERT_NE(validation, nullptr);
    EXPECT_EQ(validation->field_name, "short_name");

    ASSERT_TRUE(service.createGame(
        fixture->administrator_id, "used", "Used Game", "",
        GameVisibility::PUBLIC));
    auto choice_id = service.createField(
        fixture->administrator_id,
        "used",
        1,
        "route",
        "Route",
        GameFieldType::CHOICE,
        {"Forest", "Coast"});
    ASSERT_TRUE(choice_id) << choice_id.error().msg();

    auto transaction = fixture->data_source->beginTransaction();
    ASSERT_TRUE(transaction);
    auto number = (*transaction)->allocateGameNumber("used");
    ASSERT_TRUE(number);
    Card card = {
        0,
        {"used", *number},
        "Used card",
        std::nullopt,
        std::nullopt,
        0,
        "avif",
        std::nullopt,
        "avif",
        1,
        fixture->administrator_id};
    auto card_id = (*transaction)->insertCard(
        card,
        {{*choice_id, GameFieldType::CHOICE, std::string("Forest")}},
        {});
    ASSERT_TRUE(card_id) << card_id.error().msg();
    ASSERT_TRUE((*transaction)->commit());

    auto used_choice = service.updateField(
        fixture->administrator_id,
        "used",
        *choice_id,
        2,
        "Route",
        {"Coast"});
    ASSERT_FALSE(used_choice);
    ASSERT_NE(httpError(used_choice.error()), nullptr);
    EXPECT_EQ(httpError(used_choice.error())->code, 409);
    EXPECT_NE(httpError(used_choice.error())->msg.find("1 card"),
              std::string::npos);

    auto used_field = service.removeField(
        fixture->administrator_id, "used", *choice_id, 2);
    ASSERT_FALSE(used_field);
    ASSERT_NE(httpError(used_field.error()), nullptr);
    EXPECT_EQ(httpError(used_field.error())->code, 409);

    auto used_game = service.removeGame(
        fixture->administrator_id, "used", 2);
    ASSERT_FALSE(used_game);
    ASSERT_NE(httpError(used_game.error()), nullptr);
    EXPECT_EQ(httpError(used_game.error())->code, 409);

    transaction = fixture->data_source->beginTransaction();
    ASSERT_TRUE(transaction);
    ASSERT_TRUE((*transaction)->deleteCard(*card_id));
    ASSERT_TRUE((*transaction)->commit());
    ASSERT_TRUE(service.removeField(
        fixture->administrator_id, "used", *choice_id, 2));
    auto issued_game = service.removeGame(
        fixture->administrator_id, "used", 3);
    ASSERT_FALSE(issued_game);
    ASSERT_NE(httpError(issued_game.error()), nullptr);
    EXPECT_EQ(httpError(issued_game.error())->code, 409);

    ASSERT_TRUE(service.createGame(
        fixture->administrator_id, "empty", "Empty Game", "",
        GameVisibility::PUBLIC));
    ASSERT_TRUE(service.removeGame(
        fixture->administrator_id, "empty", 1));
    auto removed = fixture->data_source->getGameDefinition(
        "empty", GameContentScope::INCLUDE_INTERNAL);
    ASSERT_TRUE(removed);
    EXPECT_FALSE(*removed);
}