BareGit
#include <array>
#include <cstddef>
#include <cstdint>
#include <string>
#include <filesystem>
#include <fstream>
#include <vector>

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

#include "authorization.h"
#include "card_pool.h"
#include "email_address.h"
#include "email_sender_file.h"
#include "secret_token.h"
#include "username.h"

namespace
{

Card card(std::int64_t id, std::int64_t rarity,
          std::int64_t creator_user_id = 1)
{
    Card result{};
    result.id = id;
    result.rarity = rarity;
    result.creator_user_id = creator_user_id;
    return result;
}

User user(std::int64_t id, UserRole role)
{
    return {id, "user@example.com", "user@example.com", "User", role,
            1, 0, 0};
}

TEST(EmailAddressTest, NormalizesEquivalentAsciiAddresses)
{
    auto address = normalizeEmail("  Name@Example.COM\t");
    ASSERT_TRUE(address) << address.error().msg();
    EXPECT_EQ(address->email, "Name@Example.COM");
    EXPECT_EQ(address->key, "name@example.com");
}

TEST(EmailAddressTest, RejectsInvalidDotAtomAndDomainLabels)
{
    EXPECT_FALSE(normalizeEmail("a..b@example.com"));
    EXPECT_FALSE(normalizeEmail("a@-example.com"));
    EXPECT_FALSE(normalizeEmail("a@example..com"));
    EXPECT_FALSE(normalizeEmail("a\xC3\xA9@example.com"));
}

TEST(UsernameTest, NormalizesAndFullyCaseFolds)
{
    auto composed = normalizeUsername("\xC3\xA9");
    auto decomposed = normalizeUsername("e\xCC\x81");
    ASSERT_TRUE(composed);
    ASSERT_TRUE(decomposed);
    EXPECT_EQ(composed->username, decomposed->username);

    auto sharp_s = normalizeUsername("Stra\xC3\x9F" "e");
    auto letters = normalizeUsername("STRASSE");
    ASSERT_TRUE(sharp_s);
    ASSERT_TRUE(letters);
    EXPECT_EQ(sharp_s->key, letters->key);
}

TEST(UsernameTest, EnforcesBoundariesAndControls)
{
    EXPECT_TRUE(normalizeUsername(std::string(32, 'a')));
    EXPECT_FALSE(normalizeUsername(std::string(33, 'a')));
    EXPECT_FALSE(normalizeUsername(" name"));
    EXPECT_FALSE(normalizeUsername("name\xE3\x80\x80"));
    EXPECT_FALSE(normalizeUsername("bad\nname"));
    EXPECT_FALSE(normalizeUsername(std::string("\xC3", 1)));
}

TEST(AuthorizationTest, EnforcesOwnershipAndRoleExceptions)
{
    AuthorizationService authorization;
    const Card authored = card(10, 1, 2);
    const User player = user(1, UserRole::PLAYER);
    const User creator = user(2, UserRole::CREATOR);
    const User other_creator = user(3, UserRole::CREATOR);
    const User administrator = user(4, UserRole::ADMINISTRATOR);

    EXPECT_TRUE(authorization.canViewCard(player, authored, true));
    EXPECT_FALSE(authorization.canViewCard(player, authored, false));
    EXPECT_TRUE(authorization.canViewCard(creator, authored, false));
    EXPECT_FALSE(authorization.canEditCard(other_creator, authored));
    EXPECT_TRUE(authorization.canEditCard(administrator, authored));
    EXPECT_FALSE(authorization.canSetRarity(creator));
    EXPECT_TRUE(authorization.canSetRarity(administrator));
}

TEST(AuthorizationTest, AppliesInternalGameVisibility)
{
    AuthorizationService authorization;
    const User player = user(1, UserRole::PLAYER);
    const User creator = user(2, UserRole::CREATOR);
    const User administrator = user(3, UserRole::ADMINISTRATOR);
    const Game public_game = {
        "public", "Public", "", GameVisibility::PUBLIC, 1};
    const Game internal_game = {
        "internal", "Internal", "", GameVisibility::INTERNAL, 1};

    EXPECT_EQ(
        authorization.gameContentScope(player),
        GameContentScope::PUBLIC_ONLY);
    EXPECT_EQ(
        authorization.gameContentScope(creator),
        GameContentScope::PUBLIC_ONLY);
    EXPECT_EQ(
        authorization.gameContentScope(administrator),
        GameContentScope::INCLUDE_INTERNAL);
    EXPECT_TRUE(authorization.canUseGame(creator, public_game));
    EXPECT_FALSE(authorization.canUseGame(creator, internal_game));
    EXPECT_TRUE(authorization.canUseGame(administrator, internal_game));
}

TEST(CardPoolTest, CalculatesRequiredRarityRatio)
{
    CardPoolService pool;
    const auto entries = pool.calculate({card(3, 2), card(2, 1), card(1, 0)});
    ASSERT_EQ(entries.size(), 2);
    EXPECT_EQ(entries[0].card.id, 2);
    EXPECT_DOUBLE_EQ(entries[0].scaled_weight, 1.0);
    EXPECT_DOUBLE_EQ(entries[1].scaled_weight, 0.5);
    EXPECT_NEAR(entries[0].probability, 2.0 / 3.0, 1e-15);
    EXPECT_NEAR(entries[1].probability, 1.0 / 3.0, 1e-15);
}

TEST(CardPoolTest, SelectsAtPortableRandomBoundaries)
{
    CardPoolService pool;
    const auto entries = pool.calculate({card(1, 1), card(2, 1)});
    mw::CryptoMock crypto;
    EXPECT_CALL(crypto, randomBytes(8))
        .WillOnce(testing::Return(std::vector<std::byte>(8)))
        .WillOnce(testing::Return(std::vector<std::byte>{
            std::byte{0xff}, std::byte{0xff}, std::byte{0xff},
            std::byte{0xff}, std::byte{0xff}, std::byte{0xff},
            std::byte{0xff}, std::byte{0xff}}));
    auto first = pool.select(entries, crypto);
    auto last = pool.select(entries, crypto);
    ASSERT_TRUE(first);
    ASSERT_TRUE(last);
    EXPECT_EQ(first->card.id, 1);
    EXPECT_EQ(last->card.id, 2);
}

TEST(CardPoolTest, FormatsProbabilityForCardViews)
{
    EXPECT_EQ(formatProbability(0), "0%");
    EXPECT_EQ(formatProbability(1), "100%");
    EXPECT_EQ(formatProbability(1.0 / 3.0), "33.333333%");
    EXPECT_EQ(formatProbability(0.000000001), "<0.000001%");
}

TEST(SecretTokenTest, RejectsNonCanonicalCredentials)
{
    EXPECT_FALSE(hashSecretToken(std::string(63, '0')));
    EXPECT_FALSE(hashSecretToken(std::string(64, 'A')));
    EXPECT_TRUE(hashSecretToken(std::string(64, '0')));
    EXPECT_TRUE(constantTimeEqual("secret", "secret"));
    EXPECT_FALSE(constantTimeEqual("secret", "secreu"));
    EXPECT_FALSE(constantTimeEqual("secret", "secret-long"));
}

TEST(FileEmailSenderTest, AtomicallyPublishesPrivateLatestLink)
{
    const std::filesystem::path target =
        std::filesystem::path(testing::TempDir()) /
        "card_collection_auth_link.txt";
    std::error_code ignored;
    std::filesystem::remove(target, ignored);
    auto url = mw::URL::fromStr("http://127.0.0.1/confirm/secret");
    ASSERT_TRUE(url);
    FileEmailSender sender(target);

    auto sent = sender.send({
        "person@example.com",
        std::move(*url),
        std::chrono::system_clock::now(),
    });

    ASSERT_TRUE(sent) << sent.error().msg();
    std::ifstream input(target);
    std::string contents;
    std::getline(input, contents);
    EXPECT_EQ(contents, "http://127.0.0.1/confirm/secret");
    const auto permissions = std::filesystem::status(target).permissions();
    EXPECT_EQ(
        permissions & (std::filesystem::perms::group_all |
                       std::filesystem::perms::others_all),
        std::filesystem::perms::none);
    std::filesystem::remove(target, ignored);
}

} // namespace