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

#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <mw/crypto_mock.hpp>

#include "collection.h"
#include "data_sqlite.h"
#include "startup.h"
#include "user_service.h"

namespace
{

class TemporaryDatabase
{
public:
    TemporaryDatabase()
        : path_(
              std::filesystem::path(testing::TempDir()) /
              ("collection_service_" + std::to_string(
                  std::chrono::steady_clock::now()
                      .time_since_epoch().count()) + ".sqlite3"))
    {}

    ~TemporaryDatabase()
    {
        std::error_code error;
        std::filesystem::remove(path_, error);
        std::filesystem::remove(path_.string() + "-shm", error);
        std::filesystem::remove(path_.string() + "-wal", error);
    }

    const std::filesystem::path& path() const
    {
        return path_;
    }

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

class ClockMock final : public ClockInterface
{
public:
    std::chrono::system_clock::time_point now() const override
    {
        return current;
    }

    std::chrono::system_clock::time_point current{};
};

mw::E<std::int64_t> insertPlayer(
    DataSourceInterface& data_source,
    const std::string& email,
    std::uint32_t pulls = 1)
{
    auto transaction = data_source.beginTransaction();
    if(!transaction)
    {
        return std::unexpected(std::move(transaction.error()));
    }
    User user = {
        0,
        email,
        email,
        std::nullopt,
        UserRole::PLAYER,
        pulls,
        0,
        0};
    auto id = (*transaction)->insertUser(user);
    if(!id)
    {
        return std::unexpected(std::move(id.error()));
    }
    auto username = (*transaction)->updateUsername(
        *id, email.substr(0, email.find('@')), email);
    if(!username)
    {
        return std::unexpected(std::move(username.error()));
    }
    auto commit = (*transaction)->commit();
    if(!commit)
    {
        return std::unexpected(std::move(commit.error()));
    }
    return *id;
}

mw::E<std::int64_t> insertPoolCard(
    DataSourceInterface& data_source,
    std::int64_t creator_user_id,
    std::uint32_t number,
    std::int64_t rarity)
{
    auto transaction = data_source.beginTransaction();
    if(!transaction)
    {
        return std::unexpected(std::move(transaction.error()));
    }
    Card card = {
        0,
        {std::nullopt, number},
        "Pool card",
        std::nullopt,
        std::nullopt,
        rarity,
        "jpg",
        std::nullopt,
        "jpg",
        1,
        creator_user_id};
    auto id = (*transaction)->insertCard(card, {}, {});
    if(!id)
    {
        return std::unexpected(std::move(id.error()));
    }
    auto commit = (*transaction)->commit();
    if(!commit)
    {
        return std::unexpected(std::move(commit.error()));
    }
    return *id;
}

} // namespace

TEST(UserServiceTest, OnboardsAndPermanentlyPromotesPlayer)
{
    TemporaryDatabase database;
    auto data_source = prepareDataSource(database.path());
    ASSERT_TRUE(data_source);
    auto administrator = (*data_source)->getUserByEmailKey(
        "admin@example.com");
    ASSERT_TRUE(administrator);
    ASSERT_TRUE(*administrator);
    auto player_id = insertPlayer(**data_source, "player@example.com");
    ASSERT_TRUE(player_id);
    UserService users(**data_source);

    auto renamed = users.setUsername(*player_id, "Stra\xC3\x9F" "e");
    ASSERT_TRUE(renamed) << renamed.error().msg();
    EXPECT_EQ(renamed->username, "Stra\xC3\x9F" "e");
    auto promoted = users.promote((**administrator).id, *player_id);
    ASSERT_TRUE(promoted) << promoted.error().msg();
    EXPECT_EQ(promoted->role, UserRole::CREATOR);
    auto repeated = users.promote((**administrator).id, *player_id);
    ASSERT_TRUE(repeated);
    EXPECT_EQ(repeated->role, UserRole::CREATOR);
    EXPECT_FALSE(users.promote(*player_id, (**administrator).id));
}

TEST(CollectionServiceTest, AccruesCapsAndPullsAtomically)
{
    TemporaryDatabase database;
    auto data_source = prepareDataSource(database.path());
    ASSERT_TRUE(data_source);
    auto administrator = (*data_source)->getUserByEmailKey(
        "admin@example.com");
    ASSERT_TRUE(administrator);
    ASSERT_TRUE(*administrator);
    auto player_id = insertPlayer(**data_source, "collector@example.com");
    ASSERT_TRUE(player_id);
    auto card_id = insertPoolCard(
        **data_source, (**administrator).id, 42, 1);
    ASSERT_TRUE(card_id);
    ClockMock clock;
    mw::CryptoMock crypto;
    EXPECT_CALL(crypto, randomBytes(8))
        .WillOnce(testing::Return(std::vector<std::byte>(8)));
    CollectionService collection(**data_source, clock, crypto, 3);

    auto pulled = collection.pull(*player_id);
    ASSERT_TRUE(pulled) << pulled.error().msg();
    EXPECT_EQ(pulled->card.id, *card_id);
    EXPECT_EQ(pulled->quantity, 1);
    EXPECT_FALSE(collection.pull(*player_id));

    clock.current = std::chrono::system_clock::time_point(
        std::chrono::days(10));
    auto refreshed = collection.refresh(*player_id);
    ASSERT_TRUE(refreshed);
    EXPECT_EQ(refreshed->stored_pulls, 3);
    EXPECT_EQ(refreshed->pull_refresh_day, 10);
    clock.current = std::chrono::system_clock::time_point(
        std::chrono::days(9));
    refreshed = collection.refresh(*player_id);
    ASSERT_TRUE(refreshed);
    EXPECT_EQ(refreshed->stored_pulls, 3);
    EXPECT_EQ(refreshed->pull_refresh_day, 10);
}

TEST(CollectionServiceTest, EmptyPoolPreservesRefreshedPull)
{
    TemporaryDatabase database;
    auto data_source = prepareDataSource(database.path());
    ASSERT_TRUE(data_source);
    auto player_id = insertPlayer(**data_source, "empty@example.com");
    ASSERT_TRUE(player_id);
    ClockMock clock;
    mw::CryptoMock crypto;
    CollectionService collection(**data_source, clock, crypto, 3);

    EXPECT_FALSE(collection.pull(*player_id));
    auto user = (*data_source)->getUser(*player_id);
    ASSERT_TRUE(user);
    ASSERT_TRUE(*user);
    EXPECT_EQ((**user).stored_pulls, 1);
}