BareGit
#include "data_sqlite.h"
#include "startup.h"

#include <chrono>
#include <cstdint>
#include <filesystem>
#include <memory>
#include <string>
#include <utility>

#include <gtest/gtest.h>

namespace
{

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

    /// Remove the test database and its SQLite sidecar files.
    ~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 database path.
    const std::filesystem::path& path() const
    {
        return path_;
    }

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

Card makeLooseCard(std::uint32_t number, std::string name)
{
    return {
        0,
        {std::nullopt, number},
        std::move(name),
        std::nullopt,
        std::nullopt,
        0,
        "jpg",
        std::nullopt,
        "avif",
        1,
        1,
    };
}

} // namespace

/// Verify the SQLite factory owns an opened connection.
TEST(DataSourceSQLiteTest, OpensDatabase)
{
    auto data_source = DataSourceSQLite::fromFile(":memory:");

    ASSERT_TRUE(data_source);
    EXPECT_NE(*data_source, nullptr);
}

/// Verify a fresh unmigrated connection has no application tables.
TEST(DataSourceSQLiteTest, RejectsReadsBeforeMigration)
{
    auto data_source_result = DataSourceSQLite::fromFile(":memory:");
    ASSERT_TRUE(data_source_result);
    std::unique_ptr<DataSourceSQLite> data_source =
        std::move(*data_source_result);

    EXPECT_FALSE(data_source->getSeries(
        GameContentScope::INCLUDE_INTERNAL));
    EXPECT_FALSE(data_source->getSeries(
        1, GameContentScope::INCLUDE_INTERNAL));
    EXPECT_FALSE(data_source->getGames(
        GameContentScope::INCLUDE_INTERNAL));
}

/// Verify startup prepares a fresh database for its first card-index read.
TEST(DataSourceSQLiteTest, PreparesFreshDatabaseForStartup)
{
    TemporaryDatabase database;
    auto data_source = prepareDataSource(database.path());
    ASSERT_TRUE(data_source);

    auto version = (*data_source)->getSchemaVersion();
    ASSERT_TRUE(version);
    EXPECT_EQ(*version, DB_SCHEMA_VERSION);
    auto cards = (*data_source)->getCards(
        GameContentScope::INCLUDE_INTERNAL);
    ASSERT_TRUE(cards);
    EXPECT_TRUE(cards->empty());
}

/// Verify card index reads preserve common and nullable card fields.
TEST(DataSourceSQLiteTest, ReturnsCards)
{
    TemporaryDatabase database;
    auto connection = mw::SQLite::connectFile(database.path().string());
    ASSERT_TRUE(connection);
    ASSERT_TRUE((*connection)->execute(
        "CREATE TABLE cards ("
        "id INTEGER PRIMARY KEY, "
        "creator_user_id INTEGER NOT NULL, "
        "game_short_name TEXT, "
        "card_number INTEGER NOT NULL, "
        "name TEXT NOT NULL, "
        "short_description TEXT, "
        "long_description TEXT, "
        "rarity INTEGER NOT NULL, "
        "front_extension TEXT NOT NULL, "
        "foil_extension TEXT, "
        "thumbnail_extension TEXT NOT NULL, "
        "revision INTEGER NOT NULL);"));
    ASSERT_TRUE((*connection)->execute(
        "INSERT INTO cards VALUES (2, 1, 'pkm', 7, 'Moon card', "
        "'Short', 'Long', 4, 'avif', 'webp', 'avif', 3);"));
    ASSERT_TRUE((*connection)->execute(
        "INSERT INTO cards VALUES (1, 1, NULL, 35, 'Loose card', "
        "NULL, NULL, 0, 'jpg', NULL, 'webp', 1);"));
    connection->reset();

    auto data_source = DataSourceSQLite::fromFile(database.path());
    ASSERT_TRUE(data_source);
    auto cards = (*data_source)->getCards(
        GameContentScope::INCLUDE_INTERNAL);
    ASSERT_TRUE(cards);
    ASSERT_EQ(cards->size(), 2);

    EXPECT_EQ((*cards)[0].id, 1);
    EXPECT_EQ((*cards)[0].identity.game_short_name, std::nullopt);
    EXPECT_EQ((*cards)[0].identity.card_number, 35);
    EXPECT_EQ((*cards)[0].name, "Loose card");
    EXPECT_EQ((*cards)[0].short_description, std::nullopt);
    EXPECT_EQ((*cards)[0].long_description, std::nullopt);
    EXPECT_EQ((*cards)[0].rarity, 0);
    EXPECT_EQ((*cards)[0].front_extension, "jpg");
    EXPECT_EQ((*cards)[0].foil_extension, std::nullopt);
    EXPECT_EQ((*cards)[0].thumbnail_extension, "webp");
    EXPECT_EQ((*cards)[0].revision, 1);
    EXPECT_EQ((*cards)[0].creator_user_id, 1);

    EXPECT_EQ((*cards)[1].id, 2);
    EXPECT_EQ((*cards)[1].identity.game_short_name, "pkm");
    EXPECT_EQ((*cards)[1].identity.card_number, 7);
    EXPECT_EQ((*cards)[1].name, "Moon card");
    EXPECT_EQ((*cards)[1].short_description, "Short");
    EXPECT_EQ((*cards)[1].long_description, "Long");
    EXPECT_EQ((*cards)[1].rarity, 4);
    EXPECT_EQ((*cards)[1].front_extension, "avif");
    EXPECT_EQ((*cards)[1].foil_extension, "webp");
    EXPECT_EQ((*cards)[1].thumbnail_extension, "avif");
    EXPECT_EQ((*cards)[1].revision, 3);

    auto loose_card = (*data_source)->getCard(
        {std::nullopt, 35}, GameContentScope::INCLUDE_INTERNAL);
    ASSERT_TRUE(loose_card);
    ASSERT_TRUE(*loose_card);
    EXPECT_EQ((**loose_card).id, 1);
    EXPECT_EQ((**loose_card).name, "Loose card");
}

/// Verify invalid persisted card identities fail the complete read.
TEST(DataSourceSQLiteTest, RejectsInvalidCards)
{
    TemporaryDatabase database;
    auto connection = mw::SQLite::connectFile(database.path().string());
    ASSERT_TRUE(connection);
    ASSERT_TRUE((*connection)->execute(
        "CREATE TABLE cards ("
        "id INTEGER, creator_user_id INTEGER, game_short_name TEXT, "
        "card_number INTEGER, "
        "name TEXT, short_description TEXT, long_description TEXT, "
        "rarity INTEGER, front_extension TEXT, foil_extension TEXT, "
        "thumbnail_extension TEXT, revision INTEGER);"));
    ASSERT_TRUE((*connection)->execute(
        "INSERT INTO cards VALUES (1, 1, NULL, -1, 'Broken', NULL, NULL, "
        "0, 'jpg', NULL, 'jpg', 1);"));
    connection->reset();

    auto data_source = DataSourceSQLite::fromFile(database.path());
    ASSERT_TRUE(data_source);
    EXPECT_FALSE((*data_source)->getCards(
        GameContentScope::INCLUDE_INTERNAL));
}

/// Verify a committed transaction persists a complete loose card.
TEST(DataSourceSQLiteTest, InsertsLooseCard)
{
    TemporaryDatabase database;
    auto data_source = prepareDataSource(database.path());
    ASSERT_TRUE(data_source);
    auto transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(transaction);

    auto exists_before = (*transaction)->looseNumberExists(42);
    ASSERT_TRUE(exists_before);
    EXPECT_FALSE(*exists_before);
    auto card_id = (*transaction)->insertCard(
        makeLooseCard(42, "New card"), {}, {});
    ASSERT_TRUE(card_id);
    EXPECT_GT(*card_id, 0);
    auto exists_after = (*transaction)->looseNumberExists(42);
    ASSERT_TRUE(exists_after);
    EXPECT_TRUE(*exists_after);
    ASSERT_TRUE((*transaction)->commit());

    auto cards = (*data_source)->getCards(
        GameContentScope::INCLUDE_INTERNAL);
    ASSERT_TRUE(cards);
    ASSERT_EQ(cards->size(), 1);
    EXPECT_EQ(cards->front().id, *card_id);
    EXPECT_EQ(cards->front().identity.card_number, 42);
    EXPECT_EQ(cards->front().name, "New card");
}

/// Verify transactional reads and updates preserve a loose-card identity.
TEST(DataSourceSQLiteTest, UpdatesLooseCard)
{
    TemporaryDatabase database;
    auto data_source = prepareDataSource(database.path());
    ASSERT_TRUE(data_source);
    auto transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(transaction);
    auto card_id = (*transaction)->insertCard(
        makeLooseCard(42, "Original"), {}, {});
    ASSERT_TRUE(card_id);
    ASSERT_TRUE((*transaction)->commit());

    transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(transaction);
    auto card = (*transaction)->getCardForUpdate(*card_id);
    ASSERT_TRUE(card);
    ASSERT_TRUE(*card);
    (**card).name = "Edited";
    (**card).rarity = 5;
    (**card).foil_extension = "webp";
    (**card).revision = 2;
    ASSERT_TRUE((*transaction)->updateCard(
        **card, {}, {}));
    ASSERT_TRUE((*transaction)->commit());

    auto stored = (*data_source)->getCard(
        {std::nullopt, 42}, GameContentScope::INCLUDE_INTERNAL);
    ASSERT_TRUE(stored);
    ASSERT_TRUE(*stored);
    EXPECT_EQ((**stored).id, *card_id);
    EXPECT_EQ((**stored).identity.card_number, 42);
    EXPECT_EQ((**stored).name, "Edited");
    EXPECT_EQ((**stored).rarity, 5);
    EXPECT_EQ((**stored).foil_extension, "webp");
    EXPECT_EQ((**stored).revision, 2);
}

/// Verify series can be created, edited, listed, and deleted transactionally.
TEST(DataSourceSQLiteTest, MutatesSeries)
{
    TemporaryDatabase database;
    auto data_source = prepareDataSource(database.path());
    ASSERT_TRUE(data_source);

    auto transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(transaction);
    ASSERT_TRUE((*transaction)->insertGame(
        {"gh", "Going Home", "", GameVisibility::PUBLIC, 1}));
    auto series_id = (*transaction)->insertSeries(
        {0, "gh", "First series", "Description"});
    ASSERT_TRUE(series_id);
    ASSERT_TRUE((*transaction)->commit());

    auto all_series = (*data_source)->getSeries(
        GameContentScope::INCLUDE_INTERNAL);
    ASSERT_TRUE(all_series);
    ASSERT_EQ(all_series->size(), 1);
    EXPECT_EQ(all_series->front().id, *series_id);
    auto series = (*data_source)->getSeries(
        *series_id, GameContentScope::INCLUDE_INTERNAL);
    ASSERT_TRUE(series);
    ASSERT_TRUE(*series);
    (**series).name = "Edited series";

    transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(transaction);
    ASSERT_TRUE((*transaction)->updateSeries(**series));
    ASSERT_TRUE((*transaction)->commit());
    series = (*data_source)->getSeries(
        *series_id, GameContentScope::INCLUDE_INTERNAL);
    ASSERT_TRUE(series);
    ASSERT_TRUE(*series);
    EXPECT_EQ((**series).name, "Edited series");

    transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(transaction);
    ASSERT_TRUE((*transaction)->deleteSeries(*series_id));
    ASSERT_TRUE((*transaction)->commit());
    series = (*data_source)->getSeries(
        *series_id, GameContentScope::INCLUDE_INTERNAL);
    ASSERT_TRUE(series);
    EXPECT_FALSE(*series);
}

TEST(DataSourceSQLiteTest, ScopesInternalGameContentAndPulls)
{
    TemporaryDatabase database;
    auto data_source = prepareDataSource(database.path());
    ASSERT_TRUE(data_source) << data_source.error().msg();

    auto transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(transaction);
    ASSERT_TRUE((*transaction)->insertGame(
        {"public", "Public", "", GameVisibility::PUBLIC, 1}));
    ASSERT_TRUE((*transaction)->insertGame(
        {"internal", "Internal", "", GameVisibility::INTERNAL, 1}));
    auto public_series = (*transaction)->insertSeries(
        {0, "public", "Public series", ""});
    auto internal_series = (*transaction)->insertSeries(
        {0, "internal", "Internal series", ""});
    ASSERT_TRUE(public_series);
    ASSERT_TRUE(internal_series);
    auto public_number = (*transaction)->allocateGameNumber("public");
    auto internal_number = (*transaction)->allocateGameNumber("internal");
    ASSERT_TRUE(public_number);
    ASSERT_TRUE(internal_number);
    Card public_card = makeLooseCard(*public_number, "Public card");
    public_card.identity.game_short_name = "public";
    public_card.rarity = 1;
    Card internal_card = makeLooseCard(*internal_number, "Internal card");
    internal_card.identity.game_short_name = "internal";
    internal_card.rarity = 1;
    auto public_id = (*transaction)->insertCard(
        public_card, {}, {*public_series});
    auto internal_id = (*transaction)->insertCard(
        internal_card, {}, {*internal_series});
    ASSERT_TRUE(public_id);
    ASSERT_TRUE(internal_id);
    ASSERT_TRUE((*transaction)->incrementHolding(1, *public_id));
    ASSERT_TRUE((*transaction)->incrementHolding(1, *internal_id));
    ASSERT_TRUE((*transaction)->commit());

    auto public_games = (*data_source)->getGames(
        GameContentScope::PUBLIC_ONLY);
    auto all_games = (*data_source)->getGames(
        GameContentScope::INCLUDE_INTERNAL);
    ASSERT_TRUE(public_games);
    ASSERT_TRUE(all_games);
    ASSERT_EQ(public_games->size(), 1);
    ASSERT_EQ(all_games->size(), 2);
    EXPECT_EQ(public_games->front().short_name, "public");
    EXPECT_EQ((*all_games)[0].visibility, GameVisibility::INTERNAL);
    EXPECT_EQ((*all_games)[1].visibility, GameVisibility::PUBLIC);

    auto public_cards = (*data_source)->getCards(
        GameContentScope::PUBLIC_ONLY);
    auto all_cards = (*data_source)->getCards(
        GameContentScope::INCLUDE_INTERNAL);
    ASSERT_TRUE(public_cards);
    ASSERT_TRUE(all_cards);
    ASSERT_EQ(public_cards->size(), 1);
    ASSERT_EQ(all_cards->size(), 2);
    EXPECT_EQ(public_cards->front().name, "Public card");
    auto hidden_card = (*data_source)->getCard(
        {"internal", *internal_number}, GameContentScope::PUBLIC_ONLY);
    ASSERT_TRUE(hidden_card);
    EXPECT_FALSE(*hidden_card);
    auto hidden_definition = (*data_source)->getGameDefinition(
        "internal", GameContentScope::PUBLIC_ONLY);
    ASSERT_TRUE(hidden_definition);
    EXPECT_FALSE(*hidden_definition);
    auto hidden_fields = (*data_source)->getCardFieldValues(
        *internal_id, GameContentScope::PUBLIC_ONLY);
    ASSERT_TRUE(hidden_fields);
    EXPECT_FALSE(*hidden_fields);

    auto public_series_list = (*data_source)->getSeries(
        GameContentScope::PUBLIC_ONLY);
    auto public_collection = (*data_source)->getCollection(
        1, GameContentScope::PUBLIC_ONLY);
    auto pool = (*data_source)->getPoolCards();
    ASSERT_TRUE(public_series_list);
    ASSERT_TRUE(public_collection);
    ASSERT_TRUE(pool);
    EXPECT_EQ(public_series_list->size(), 1);
    EXPECT_EQ(public_collection->size(), 1);
    EXPECT_EQ(pool->size(), 1);

    transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(transaction);
    auto locked_pool = (*transaction)->getPoolCardsForUpdate();
    ASSERT_TRUE(locked_pool);
    EXPECT_EQ(locked_pool->size(), 1);
    auto visible = (*transaction)->updateGame(
        "internal", "Internal", "", GameVisibility::PUBLIC, 1);
    ASSERT_TRUE(visible);
    ASSERT_TRUE(*visible);
    ASSERT_TRUE((*transaction)->commit());

    public_cards = (*data_source)->getCards(GameContentScope::PUBLIC_ONLY);
    public_collection = (*data_source)->getCollection(
        1, GameContentScope::PUBLIC_ONLY);
    pool = (*data_source)->getPoolCards();
    ASSERT_TRUE(public_cards);
    ASSERT_TRUE(public_collection);
    ASSERT_TRUE(pool);
    EXPECT_EQ(public_cards->size(), 2);
    EXPECT_EQ(public_collection->size(), 2);
    EXPECT_EQ(pool->size(), 2);
}

/// Verify dynamic fields, numbering, values, and memberships persist.
TEST(DataSourceSQLiteTest, PersistsDynamicGameCards)
{
    TemporaryDatabase database;
    auto data_source = prepareDataSource(database.path());
    ASSERT_TRUE(data_source);

    auto transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(transaction);
    ASSERT_TRUE((*transaction)->insertGame(
        {"test", "Test Game", "Description",
         GameVisibility::PUBLIC, 1}));
    auto hp_id = (*transaction)->insertGameField(
        {0, "test", "hp", "HP", GameFieldType::INTEGER, 0, {}}, {});
    auto note_id = (*transaction)->insertGameField(
        {0, "test", "note", "Note", GameFieldType::STRING, 1, {}}, {});
    auto route_id = (*transaction)->insertGameField(
        {0, "test", "route", "Route", GameFieldType::CHOICE, 2, {}},
        {{"Forest", 0}, {"Coast", 1}});
    ASSERT_TRUE(hp_id);
    ASSERT_TRUE(note_id);
    ASSERT_TRUE(route_id);
    auto series_id = (*transaction)->insertSeries(
        {0, "test", "Core set", "The first set."});
    ASSERT_TRUE(series_id);
    auto number = (*transaction)->allocateGameNumber("test");
    ASSERT_TRUE(number);
    Card card = {
        0, {"test", *number}, "Dynamic card", std::nullopt,
        std::nullopt, 2, "avif", std::nullopt, "avif", 1, 1};
    auto card_id = (*transaction)->insertCard(
        card,
        {{*hp_id, GameFieldType::INTEGER, std::int64_t{60}},
         {*note_id, GameFieldType::STRING, std::string("Near home")},
         {*route_id, GameFieldType::CHOICE, std::string("Forest")}},
        {*series_id});
    ASSERT_TRUE(card_id);
    ASSERT_TRUE((*transaction)->commit());

    auto stored = (*data_source)->getCard(
        {"test", 1}, GameContentScope::INCLUDE_INTERNAL);
    ASSERT_TRUE(stored);
    ASSERT_TRUE(*stored);
    auto values = (*data_source)->getCardFieldValues(
        *card_id, GameContentScope::INCLUDE_INTERNAL);
    ASSERT_TRUE(values);
    ASSERT_TRUE(*values);
    ASSERT_EQ((**values).definition.fields.size(), 3);
    ASSERT_EQ((**values).values.size(), 3);
    EXPECT_EQ(std::get<std::int64_t>((**values).values[0].value), 60);
    EXPECT_EQ(std::get<std::string>((**values).values[1].value), "Near home");
    EXPECT_EQ(std::get<std::string>((**values).values[2].value), "Forest");

    Card updated = **stored;
    updated.revision = 2;
    transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(transaction);
    ASSERT_TRUE((*transaction)->updateCard(
        updated,
        {{*hp_id, GameFieldType::INTEGER, std::int64_t{70}}},
        {}));
    ASSERT_TRUE((*transaction)->commit());
    values = (*data_source)->getCardFieldValues(
        *card_id, GameContentScope::INCLUDE_INTERNAL);
    ASSERT_TRUE(values);
    ASSERT_TRUE(*values);
    ASSERT_EQ((**values).values.size(), 1);
    EXPECT_EQ(std::get<std::int64_t>((**values).values[0].value), 70);

    transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(transaction);
    auto later_id = (*transaction)->insertGameField(
        {0, "test", "later", "Later", GameFieldType::STRING, 3, {}},
        {});
    ASSERT_TRUE(later_id);
    ASSERT_TRUE((*transaction)->commit());
    values = (*data_source)->getCardFieldValues(
        *card_id, GameContentScope::INCLUDE_INTERNAL);
    ASSERT_TRUE(values);
    ASSERT_TRUE(*values);
    EXPECT_EQ((**values).definition.fields.size(), 4);
    EXPECT_EQ((**values).values.size(), 1);
}

/// Verify destroying an uncommitted transaction rolls card insertion back.
TEST(DataSourceSQLiteTest, RollsBackLooseCard)
{
    TemporaryDatabase database;
    auto data_source = prepareDataSource(database.path());
    ASSERT_TRUE(data_source);
    auto transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(transaction);
    ASSERT_TRUE((*transaction)->insertCard(
        makeLooseCard(91, "Temporary card"), {}, {}));

    transaction->reset();

    auto cards = (*data_source)->getCards(
        GameContentScope::INCLUDE_INTERNAL);
    ASSERT_TRUE(cards);
    EXPECT_TRUE(cards->empty());
}

/// Verify game sequence numbers increase and survive committed transactions.
TEST(DataSourceSQLiteTest, AllocatesGameNumbers)
{
    TemporaryDatabase database;
    auto data_source = prepareDataSource(database.path());
    ASSERT_TRUE(data_source);
    auto transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(transaction);
    ASSERT_TRUE((*transaction)->insertGame(
        {"gh", "Going Home", "", GameVisibility::PUBLIC, 1}));
    auto first = (*transaction)->allocateGameNumber("gh");
    auto second = (*transaction)->allocateGameNumber("gh");
    ASSERT_TRUE(first);
    ASSERT_TRUE(second);
    EXPECT_EQ(*first, 1);
    EXPECT_EQ(*second, 2);
    ASSERT_TRUE((*transaction)->commit());

    auto next_transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(next_transaction);
    auto third = (*next_transaction)->allocateGameNumber("gh");
    ASSERT_TRUE(third);
    EXPECT_EQ(*third, 3);
}

/// Verify the loose-card unique index rejects a repeated number.
TEST(DataSourceSQLiteTest, RejectsDuplicateLooseNumber)
{
    TemporaryDatabase database;
    auto data_source = prepareDataSource(database.path());
    ASSERT_TRUE(data_source);
    auto first_transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(first_transaction);
    ASSERT_TRUE((*first_transaction)->insertCard(
        makeLooseCard(8, "First"), {}, {}));
    ASSERT_TRUE((*first_transaction)->commit());

    auto second_transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(second_transaction);
    auto exists = (*second_transaction)->looseNumberExists(8);
    ASSERT_TRUE(exists);
    EXPECT_TRUE(*exists);
    EXPECT_FALSE((*second_transaction)->insertCard(
        makeLooseCard(8, "Duplicate"), {}, {}));
}

/// Verify administrator metadata is immutable and exactly one row is kept.
TEST(DataSourceSQLiteTest, ReconcilesImmutableAdministrator)
{
    TemporaryDatabase database;
    auto data_source = prepareDataSource(
        database.path(), "Admin@Example.com", "admin@example.com",
        100, 0);
    ASSERT_TRUE(data_source) << data_source.error().msg();

    auto administrator = (*data_source)->getUserByEmailKey(
        "admin@example.com");
    ASSERT_TRUE(administrator);
    ASSERT_TRUE(*administrator);
    EXPECT_EQ((**administrator).email, "Admin@Example.com");
    EXPECT_EQ((**administrator).role, UserRole::ADMINISTRATOR);
    EXPECT_EQ((**administrator).stored_pulls, 1);

    auto same = (*data_source)->reconcileAdministrator(
        "admin@example.com", "admin@example.com", 200, 1);
    ASSERT_TRUE(same);
    EXPECT_EQ(same->id, (**administrator).id);
    EXPECT_FALSE((*data_source)->reconcileAdministrator(
        "other@example.com", "other@example.com", 200, 1));

    auto transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(transaction);
    User second_administrator = {
        0,
        "second@example.com",
        "second@example.com",
        std::nullopt,
        UserRole::ADMINISTRATOR,
        1,
        0,
        100};
    EXPECT_FALSE((*transaction)->insertUser(second_administrator));
}

/// Verify challenges, sessions, limits, users, and holdings are transactional.
TEST(DataSourceSQLiteTest, PersistsMvpAccountAndCollectionState)
{
    TemporaryDatabase database;
    auto data_source = prepareDataSource(database.path());
    ASSERT_TRUE(data_source) << data_source.error().msg();
    auto administrator = (*data_source)->getUserByEmailKey(
        "admin@example.com");
    ASSERT_TRUE(administrator);
    ASSERT_TRUE(*administrator);

    auto transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(transaction);
    User player = {
        0,
        "Player@Example.com",
        "player@example.com",
        std::nullopt,
        UserRole::PLAYER,
        1,
        0,
        10};
    auto player_id = (*transaction)->insertUser(player);
    ASSERT_TRUE(player_id) << player_id.error().msg();
    ASSERT_TRUE((*transaction)->commit());

    transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(transaction);
    ASSERT_TRUE((*transaction)->updateUsername(
        *player_id, "Player", "player"));
    ASSERT_TRUE((*transaction)->promoteUser(*player_id));
    auto reservation = (*transaction)->reserveAuthenticationEmail(
        "player@example.com", 100, true, 0, 1);
    ASSERT_TRUE(reservation) << reservation.error().msg();
    EXPECT_EQ(
        reservation->status,
        AuthenticationReservationStatus::RESERVED);
    ASSERT_TRUE((*transaction)->commit());

    transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(transaction);
    reservation = (*transaction)->reserveAuthenticationEmail(
        "player@example.com", 130, true, 0, 1);
    ASSERT_TRUE(reservation);
    EXPECT_EQ(
        reservation->status,
        AuthenticationReservationStatus::EMAIL_LIMITED);
    EXPECT_EQ(reservation->retry_after, 30);
    ASSERT_TRUE((*transaction)->commit());

    transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(transaction);
    reservation = (*transaction)->reserveAuthenticationEmail(
        "new@example.com", 161, true, 0, 1);
    ASSERT_TRUE(reservation);
    EXPECT_EQ(
        reservation->status,
        AuthenticationReservationStatus::GLOBAL_LIMITED);
    ASSERT_TRUE((*transaction)->commit());

    const TokenHash challenge_hash(32, 7);
    transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(transaction);
    reservation = (*transaction)->reserveAuthenticationEmail(
        "new@example.com", 161, false, 0, 1);
    ASSERT_TRUE(reservation);
    EXPECT_EQ(
        reservation->status,
        AuthenticationReservationStatus::RESERVED);
    auto challenge_id = (*transaction)->insertAuthenticationChallenge(
        "new@example.com", "new@example.com", challenge_hash, 161, 761);
    ASSERT_TRUE(challenge_id) << challenge_id.error().msg();
    ASSERT_TRUE((*transaction)->commit());
    auto challenge = (*data_source)->getAuthenticationChallenge(
        challenge_hash, 200);
    ASSERT_TRUE(challenge);
    EXPECT_FALSE(*challenge);

    const TokenHash session_hash(32, 9);
    transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(transaction);
    ASSERT_TRUE((*transaction)->markAuthenticationChallengeDelivered(
        *challenge_id, 170));
    auto consumed = (*transaction)->consumeAuthenticationChallenge(
        challenge_hash, 200);
    ASSERT_TRUE(consumed);
    ASSERT_TRUE(*consumed);
    auto session_id = (*transaction)->insertSession(
        *player_id, session_hash, std::string(64, 'c'), 200, 400);
    ASSERT_TRUE(session_id) << session_id.error().msg();
    ASSERT_TRUE((*transaction)->commit());

    auto session = (*data_source)->getSession(session_hash, 300);
    ASSERT_TRUE(session);
    ASSERT_TRUE(*session);
    EXPECT_EQ((**session).user.id, *player_id);
    EXPECT_EQ((**session).user.role, UserRole::CREATOR);
    EXPECT_EQ((**session).csrf_token, std::string(64, 'c'));
    auto expired = (*data_source)->getSession(session_hash, 400);
    ASSERT_TRUE(expired);
    EXPECT_FALSE(*expired);

    transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(transaction);
    Card card = makeLooseCard(123, "Collectible");
    card.creator_user_id = (**administrator).id;
    auto card_id = (*transaction)->insertCard(card, {}, {});
    ASSERT_TRUE(card_id) << card_id.error().msg();
    auto quantity = (*transaction)->incrementHolding(*player_id, *card_id);
    ASSERT_TRUE(quantity);
    EXPECT_EQ(*quantity, 1);
    quantity = (*transaction)->incrementHolding(*player_id, *card_id);
    ASSERT_TRUE(quantity);
    EXPECT_EQ(*quantity, 2);
    ASSERT_TRUE((*transaction)->commit());

    auto collection = (*data_source)->getCollection(
        *player_id, GameContentScope::INCLUDE_INTERNAL);
    ASSERT_TRUE(collection);
    ASSERT_EQ(collection->size(), 1);
    EXPECT_EQ(collection->front().quantity, 2);
    transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(transaction);
    ASSERT_TRUE((*transaction)->deleteCard(*card_id));
    ASSERT_TRUE((*transaction)->commit());
    collection = (*data_source)->getCollection(
        *player_id, GameContentScope::INCLUDE_INTERNAL);
    ASSERT_TRUE(collection);
    EXPECT_TRUE(collection->empty());
}

/// Verify unreleased prototype databases fail with actionable guidance.
TEST(DataSourceSQLiteTest, RejectsObsoletePrototypeSchema)
{
    TemporaryDatabase database;
    auto connection = mw::SQLite::connectFile(database.path().string());
    ASSERT_TRUE(connection);
    ASSERT_TRUE((*connection)->execute(
        "CREATE TABLE cards(id INTEGER PRIMARY KEY);"));
    ASSERT_TRUE((*connection)->execute("PRAGMA user_version = 1;"));
    connection->reset();

    auto data_source = DataSourceSQLite::fromFile(database.path());
    ASSERT_TRUE(data_source);
    auto version = (*data_source)->getSchemaVersion();
    ASSERT_FALSE(version);
    EXPECT_NE(version.error().msg().find("delete and recreate"),
              std::string::npos);
}

TEST(DataSourceSQLiteTest, RejectsVersionOneWithoutVisibility)
{
    TemporaryDatabase database;
    auto connection = mw::SQLite::connectFile(database.path().string());
    ASSERT_TRUE(connection);
    ASSERT_TRUE((*connection)->execute(
        "CREATE TABLE users(id INTEGER PRIMARY KEY);"));
    ASSERT_TRUE((*connection)->execute(
        "CREATE TABLE games(short_name TEXT PRIMARY KEY);"));
    ASSERT_TRUE((*connection)->execute("PRAGMA user_version = 1;"));
    connection->reset();

    auto data_source = DataSourceSQLite::fromFile(database.path());
    ASSERT_TRUE(data_source);
    auto version = (*data_source)->getSchemaVersion();
    ASSERT_FALSE(version);
    EXPECT_NE(version.error().msg().find("delete and recreate"),
              std::string::npos);

    data_source->reset();
    connection = mw::SQLite::connectFile(database.path().string());
    ASSERT_TRUE(connection);
    auto columns = (*connection)->evalToValue<std::int64_t>(
        "SELECT COUNT(*) FROM pragma_table_info('games');");
    ASSERT_TRUE(columns);
    EXPECT_EQ(*columns, 1);
}

TEST(DataSourceSQLiteTest, RejectsInvalidGameVisibility)
{
    TemporaryDatabase database;
    auto data_source = prepareDataSource(database.path());
    ASSERT_TRUE(data_source) << data_source.error().msg();
    auto transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(transaction);

    EXPECT_FALSE((*transaction)->insertGame({
        "invalid", "Invalid", "",
        static_cast<GameVisibility>(2), 1}));
}

/// Verify fresh initialization and reopening do not create any games.
TEST(DataSourceSQLiteTest, StartsAndReopensWithoutGames)
{
    TemporaryDatabase database;
    auto data_source = prepareDataSource(database.path());
    ASSERT_TRUE(data_source) << data_source.error().msg();

    auto games = (*data_source)->getGames(
        GameContentScope::INCLUDE_INTERNAL);
    ASSERT_TRUE(games);
    EXPECT_TRUE(games->empty());
    data_source->reset();

    data_source = prepareDataSource(database.path());
    ASSERT_TRUE(data_source) << data_source.error().msg();
    games = (*data_source)->getGames(
        GameContentScope::INCLUDE_INTERNAL);
    ASSERT_TRUE(games);
    EXPECT_TRUE(games->empty());
}

/// Verify failed initialization rolls back newly attempted schema objects.
TEST(DataSourceSQLiteTest, RollsBackFailedInitialization)
{
    TemporaryDatabase database;
    auto connection = mw::SQLite::connectFile(database.path().string());
    ASSERT_TRUE(connection);
    ASSERT_TRUE((*connection)->execute(
        "CREATE TABLE games(existing_marker TEXT);"));
    connection->reset();

    auto data_source = DataSourceSQLite::fromFile(database.path());
    ASSERT_TRUE(data_source);
    EXPECT_FALSE((*data_source)->migrateToLatest());
    data_source->reset();

    connection = mw::SQLite::connectFile(database.path().string());
    ASSERT_TRUE(connection);
    auto users = (*connection)->evalToValue<int>(
        "SELECT EXISTS(SELECT 1 FROM sqlite_schema "
        "WHERE type = 'table' AND name = 'users');");
    ASSERT_TRUE(users);
    EXPECT_EQ(*users, 0);
    auto marker = (*connection)->evalToValue<int>(
        "SELECT EXISTS(SELECT 1 FROM sqlite_schema "
        "WHERE type = 'table' AND name = 'games');");
    ASSERT_TRUE(marker);
    EXPECT_EQ(*marker, 1);
}

/// Verify future schema versions are rejected without modifying their data.
TEST(DataSourceSQLiteTest, RejectsFutureSchemaWithoutModification)
{
    TemporaryDatabase database;
    auto connection = mw::SQLite::connectFile(database.path().string());
    ASSERT_TRUE(connection);
    ASSERT_TRUE((*connection)->execute(
        "CREATE TABLE future_marker(value TEXT);"));
    ASSERT_TRUE((*connection)->execute(
        "INSERT INTO future_marker VALUES ('kept');"));
    ASSERT_TRUE((*connection)->execute("PRAGMA user_version = 2;"));
    connection->reset();

    auto data_source = DataSourceSQLite::fromFile(database.path());
    ASSERT_TRUE(data_source);
    EXPECT_FALSE((*data_source)->migrateToLatest());
    data_source->reset();

    connection = mw::SQLite::connectFile(database.path().string());
    ASSERT_TRUE(connection);
    auto value = (*connection)->evalToValue<std::string>(
        "SELECT value FROM future_marker;");
    ASSERT_TRUE(value);
    EXPECT_EQ(*value, "kept");
    auto version = (*connection)->evalToValue<std::int64_t>(
        "PRAGMA user_version;");
    ASSERT_TRUE(version);
    EXPECT_EQ(*version, 2);
}

/// Verify startup rejects a version-1 game missing its number sequence.
TEST(DataSourceSQLiteTest, RejectsMissingGameSequence)
{
    TemporaryDatabase database;
    auto data_source = prepareDataSource(database.path());
    ASSERT_TRUE(data_source) << data_source.error().msg();
    auto transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(transaction);
    ASSERT_TRUE((*transaction)->insertGame(
        {"test", "Test Game", "", GameVisibility::PUBLIC, 1}));
    ASSERT_TRUE((*transaction)->commit());
    data_source->reset();

    auto connection = mw::SQLite::connectFile(database.path().string());
    ASSERT_TRUE(connection);
    ASSERT_TRUE((*connection)->execute(
        "DELETE FROM game_sequences WHERE game_short_name = 'test';"));
    connection->reset();

    auto reopened = prepareDataSource(database.path());
    ASSERT_FALSE(reopened);
    EXPECT_NE(reopened.error().msg().find("missing its card-number sequence"),
              std::string::npos);
}

/// Verify database constraints reject cross-game and mistyped field values.
TEST(DataSourceSQLiteTest, EnforcesDynamicFieldInvariants)
{
    TemporaryDatabase database;
    auto data_source = prepareDataSource(database.path());
    ASSERT_TRUE(data_source) << data_source.error().msg();
    auto transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(transaction);
    ASSERT_TRUE((*transaction)->insertGame(
        {"one", "One", "", GameVisibility::PUBLIC, 1}));
    ASSERT_TRUE((*transaction)->insertGame(
        {"two", "Two", "", GameVisibility::PUBLIC, 1}));
    auto integer_id = (*transaction)->insertGameField(
        {0, "one", "score", "Score", GameFieldType::INTEGER, 0, {}},
        {});
    auto first_choice_id = (*transaction)->insertGameField(
        {0, "one", "route", "Route", GameFieldType::CHOICE, 1, {}},
        {{"Shared", 0}});
    auto second_choice_id = (*transaction)->insertGameField(
        {0, "one", "color", "Color", GameFieldType::CHOICE, 2, {}},
        {{"Other", 0}});
    ASSERT_TRUE(integer_id);
    ASSERT_TRUE(first_choice_id);
    ASSERT_TRUE(second_choice_id);
    ASSERT_TRUE((*transaction)->commit());

    transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(transaction);
    auto one_number = (*transaction)->allocateGameNumber("one");
    ASSERT_TRUE(one_number);
    Card one_card = makeLooseCard(*one_number, "One card");
    one_card.identity.game_short_name = "one";
    EXPECT_FALSE((*transaction)->insertCard(
        one_card,
        {{*integer_id, GameFieldType::STRING, std::string("5")}},
        {}));
    transaction->reset();

    transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(transaction);
    auto two_number = (*transaction)->allocateGameNumber("two");
    ASSERT_TRUE(two_number);
    Card two_card = makeLooseCard(*two_number, "Two card");
    two_card.identity.game_short_name = "two";
    EXPECT_FALSE((*transaction)->insertCard(
        two_card,
        {{*integer_id, GameFieldType::INTEGER, std::int64_t{5}}},
        {}));
    transaction->reset();

    transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(transaction);
    one_number = (*transaction)->allocateGameNumber("one");
    ASSERT_TRUE(one_number);
    one_card.identity.card_number = *one_number;
    EXPECT_FALSE((*transaction)->insertCard(
        one_card,
        {{*first_choice_id,
          GameFieldType::CHOICE,
          std::string("Other")}},
        {}));
}

/// Verify definition identities are immutable and card values cascade.
TEST(DataSourceSQLiteTest, PreservesDynamicIdentitiesAndCascadesValues)
{
    TemporaryDatabase database;
    auto data_source = prepareDataSource(database.path());
    ASSERT_TRUE(data_source) << data_source.error().msg();
    auto transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(transaction);
    ASSERT_TRUE((*transaction)->insertGame(
        {"fixed", "Fixed", "", GameVisibility::PUBLIC, 1}));
    auto field_id = (*transaction)->insertGameField(
        {0, "fixed", "score", "Score", GameFieldType::INTEGER, 0, {}},
        {});
    auto choice_id = (*transaction)->insertGameField(
        {0, "fixed", "route", "Route", GameFieldType::CHOICE, 1, {}},
        {{"Home", 0}});
    ASSERT_TRUE(field_id);
    ASSERT_TRUE(choice_id);
    auto number = (*transaction)->allocateGameNumber("fixed");
    ASSERT_TRUE(number);
    Card card = makeLooseCard(*number, "Fixed card");
    card.identity.game_short_name = "fixed";
    auto card_id = (*transaction)->insertCard(
        card,
        {{*field_id, GameFieldType::INTEGER, std::int64_t{9}}},
        {});
    ASSERT_TRUE(card_id);
    ASSERT_TRUE((*transaction)->commit());

    transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(transaction);
    auto stored = (*transaction)->getCardForUpdate(*card_id);
    ASSERT_TRUE(stored);
    ASSERT_TRUE(*stored);
    (**stored).identity.card_number = 99;
    EXPECT_FALSE((*transaction)->updateCard(
        **stored,
        {{*field_id, GameFieldType::INTEGER, std::int64_t{10}}},
        {}));
    transaction->reset();

    transaction = (*data_source)->beginTransaction();
    ASSERT_TRUE(transaction);
    ASSERT_TRUE((*transaction)->deleteCard(*card_id));
    ASSERT_TRUE((*transaction)->commit());
    data_source->reset();

    auto connection = mw::SQLite::connectFile(database.path().string());
    ASSERT_TRUE(connection);
    ASSERT_TRUE((*connection)->execute("PRAGMA foreign_keys = ON;"));
    EXPECT_FALSE((*connection)->execute(
        "UPDATE games SET short_name = 'changed' "
        "WHERE short_name = 'fixed';"));
    EXPECT_FALSE((*connection)->execute(
        "UPDATE game_fields SET key = 'changed' "
        "WHERE id = 1;"));
    EXPECT_FALSE((*connection)->execute(
        "INSERT INTO game_field_choices(field_id, value, position) "
        "VALUES (1, 'Invalid', 0);"));
    EXPECT_FALSE((*connection)->execute(
        "UPDATE game_field_choices SET value = 'Changed' "
        "WHERE field_id = 2 AND value = 'Home';"));
    auto value_count = (*connection)->evalToValue<std::int64_t>(
        "SELECT COUNT(*) FROM card_field_values;");
    ASSERT_TRUE(value_count);
    EXPECT_EQ(*value_count, 0);
}