BareGit
#include "app.h"

#include <cstdint>
#include <filesystem>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>

#include <gtest/gtest.h>

#include "data_fake.h"
#include "public_id.h"

namespace
{

Config makeConfig(const std::string& base_url)
{
    auto parsed_url = mw::URL::fromStr(base_url);
    if(!parsed_url)
    {
        throw std::invalid_argument("Test base URL is invalid");
    }

    return {
        std::move(*parsed_url),
        mw::IPSocketInfo{"127.0.0.1", 8080},
        std::filesystem::path(CARD_COLLECTION_SOURCE_DIR) / "static",
        std::filesystem::path("cards.sqlite3"),
        std::filesystem::path("cards"),
        75,
        256,
    };
}

std::unique_ptr<DataSourceFake> emptyDataSource()
{
    return std::make_unique<DataSourceFake>();
}

std::unique_ptr<NonSecretRandom> deterministicRandom()
{
    return std::make_unique<NonSecretRandom>(1);
}

Card makeCard(std::int64_t id, std::uint64_t number, std::string name)
{
    return {
        id,
        {"pkm", number},
        std::move(name),
        std::nullopt,
        std::nullopt,
        0,
        "avif",
        std::nullopt,
        "avif",
        1,
    };
}

class AuthenticatedDataSource : public DataSourceFake
{
public:
    using DataSourceFake::DataSourceFake;

    /// Return one deterministic onboarded administrator session.
    mw::E<std::optional<SessionContext>> getSession(
        [[maybe_unused]] const TokenHash& token_hash,
        [[maybe_unused]] std::int64_t now) const override
    {
        return SessionContext{
            1,
            {1, "admin@example.com", "admin@example.com", "Admin",
             UserRole::ADMINISTRATOR, 1, 0, 0},
            std::string(64, 'c'),
            9999999999};
    }

    /// Treat configured cards as owned for focused rendering tests.
    mw::E<bool> userOwnsCard(
        [[maybe_unused]] std::int64_t user_id,
        [[maybe_unused]] std::int64_t card_id) const override
    {
        return true;
    }

    /// Return configured cards as the focused test pool.
    mw::E<std::vector<Card>> getPoolCards() const override
    {
        return getCards(GameContentScope::INCLUDE_INTERNAL);
    }
};

App::Request authenticatedRequest()
{
    App::Request request;
    request.set_header(
        "Cookie", "card_collection_session=" + std::string(64, 'a'));
    return request;
}

} // namespace

/// Verify named routes respect a nested application base path.
TEST(AppTest, BuildsNamedUrls)
{
    const App app(
        makeConfig("https://example.test/collection/"),
        emptyDataSource(),
        deterministicRandom());

    EXPECT_EQ(
        app.urlFor("card-index"),
        "https://example.test/collection/admin/cards");
    EXPECT_EQ(
        app.urlFor("card-new"),
        "https://example.test/collection/cards/new");
    EXPECT_EQ(
        app.urlFor("card", {"PKM/2"}),
        "https://example.test/collection/cards/PKM%2f2");
    EXPECT_EQ(
        app.urlFor("series-edit", {"42"}),
        "https://example.test/collection/admin/series/42/edit");
    EXPECT_EQ(
        app.urlFor("admin-games"),
        "https://example.test/collection/admin/games");
    EXPECT_EQ(
        app.urlFor("game-field-edit", {"a/b", "42"}),
        "https://example.test/collection/admin/games/a%2fb/fields/42/edit");
}

/// Verify static relative paths and ordered query values are encoded.
TEST(AppTest, BuildsStaticAndQueryUrls)
{
    const App app(
        makeConfig("https://example.test/collection/"),
        emptyDataSource(),
        deterministicRandom());

    EXPECT_EQ(
        app.urlFor(
            "static",
            {"foil/model/card.obj"},
            {{"v", "4"}, {"name", "a b"}}),
        "https://example.test/collection/static/foil/model/card.obj"
        "?v=4&name=a%20b");
}

/// Verify route programming errors are rejected.
TEST(AppTest, RejectsInvalidRoutes)
{
    const App app(
        makeConfig("https://example.test/"),
        emptyDataSource(),
        deterministicRandom());

    EXPECT_THROW(app.urlFor("missing"), std::invalid_argument);
    EXPECT_THROW(app.urlFor("card"), std::invalid_argument);
    EXPECT_THROW(app.urlFor("card-index", {"extra"}), std::invalid_argument);
    EXPECT_THROW(app.urlFor("static", {"../secret"}), std::invalid_argument);
}

/// Verify the index renders escaped fake records in natural ID order.
TEST(AppTest, RendersCardIndex)
{
    auto data_source = std::make_unique<AuthenticatedDataSource>(
        std::vector<Card>{
            makeCard(10, 10, "Tenth card"),
            makeCard(2, 2, "<script>Second card</script>"),
        },
        std::vector<Series>{},
        std::unordered_map<std::int64_t, std::vector<std::int64_t>>{},
        std::vector<GameDefinitionSnapshot>{
            {{"pkm", "Test Game", "", GameVisibility::PUBLIC, 1}, {}}});
    App app(
        makeConfig("https://example.test/collection/"),
        std::move(data_source),
        deterministicRandom());
    App::Request request = authenticatedRequest();
    App::Response response;

    app.handleCardIndex(request, response);

    EXPECT_EQ(response.status, 200);
    EXPECT_EQ(
        response.get_header_value("Content-Type"),
        "text/html; charset=utf-8");
    EXPECT_EQ(response.body.find("<script>Second card</script>"),
              std::string::npos);
    EXPECT_NE(response.body.find("&lt;script&gt;Second card&lt;/script&gt;"),
              std::string::npos);

    const std::size_t second_position = response.body.find("PKM-2");
    const std::size_t tenth_position = response.body.find("PKM-10");
    ASSERT_NE(second_position, std::string::npos);
    ASSERT_NE(tenth_position, std::string::npos);
    EXPECT_LT(second_position, tenth_position);
}

/// Verify the create-card page renders its multipart form and preview.
TEST(AppTest, RendersCardCreationForm)
{
    App app(
        makeConfig("https://example.test/collection/"),
        std::make_unique<AuthenticatedDataSource>(),
        deterministicRandom());
    App::Request request = authenticatedRequest();
    App::Response response;

    app.handleCardNew(request, response);

    EXPECT_EQ(response.status, 200);
    EXPECT_NE(response.body.find("id=\"CardForm\""), std::string::npos);
    EXPECT_NE(response.body.find("method=\"post\""), std::string::npos);
    EXPECT_NE(
        response.body.find(
            "action=\"https://example.test/collection/cards\""),
        std::string::npos);
    EXPECT_NE(response.body.find("enctype=\"multipart/form-data\""),
              std::string::npos);
    EXPECT_NE(response.body.find("name=\"front\""), std::string::npos);
    EXPECT_NE(response.body.find("name=\"foil\""), std::string::npos);
    EXPECT_NE(response.body.find("\"mode\": \"create\""),
              std::string::npos);
}

/// Verify the edit page reuses the card form with current values and actions.
TEST(AppTest, RendersCardEditForm)
{
    Card card = makeCard(2, 35, "Moon card");
    card.identity.game_short_name = std::nullopt;
    card.short_description = "A quiet night.";
    card.rarity = 4;
    auto data_source = std::make_unique<AuthenticatedDataSource>(
        std::vector<Card>{card});
    App app(
        makeConfig("https://example.test/collection/"),
        std::move(data_source),
        deterministicRandom());
    auto public_id = formatPublicId(card.identity);
    ASSERT_TRUE(public_id);
    App::Request request = authenticatedRequest();
    request.path_params.emplace("id", *public_id);
    App::Response response;

    app.handleCardEdit(request, response);

    EXPECT_EQ(response.status, 200);
    EXPECT_NE(response.body.find("Edit card"), std::string::npos);
    EXPECT_NE(response.body.find("value=\"Moon card\""),
              std::string::npos);
    EXPECT_NE(response.body.find("name=\"revision\""),
              std::string::npos);
    EXPECT_NE(response.body.find("name=\"front_action\""),
              std::string::npos);
    EXPECT_NE(response.body.find("name=\"foil_action\""),
              std::string::npos);
    EXPECT_NE(response.body.find("\"mode\": \"edit\""),
              std::string::npos);
}

/// Verify a card page renders fake metadata and read-only preview data.
TEST(AppTest, RendersCardView)
{
    Card card = makeCard(2, 2, "<script>Moon card</script>");
    card.identity.game_short_name = "test";
    card.short_description = "A quiet <night>.";
    card.long_description = "First line\nSecond line";
    card.rarity = 4;
    auto data_source = std::make_unique<AuthenticatedDataSource>(
        std::vector<Card>{card},
        std::vector<Series>{
            {7, "test", "Night Signals", "A series description"},
        },
        std::unordered_map<std::int64_t, std::vector<std::int64_t>>{
            {2, {7}},
        },
        std::vector<GameDefinitionSnapshot>{
            {{"test", "Test Game", "", GameVisibility::PUBLIC, 1}, {}}},
        std::unordered_map<std::int64_t, std::vector<GameFieldValue>>{});
    App app(
        makeConfig("https://example.test/collection/"),
        std::move(data_source),
        deterministicRandom());
    App::Request request = authenticatedRequest();
    request.path_params.emplace("id", "test-2");
    App::Response response;

    app.handleCardView(request, response);

    EXPECT_EQ(response.status, 200);
    EXPECT_NE(response.body.find("id=\"GLCanvas\""), std::string::npos);
    EXPECT_NE(response.body.find("TEST-2"), std::string::npos);
    EXPECT_NE(response.body.find("Night Signals"), std::string::npos);
    EXPECT_NE(response.body.find("Some card assets are missing"),
              std::string::npos);
    EXPECT_NE(response.body.find("\"foil_url\":"), std::string::npos);
    EXPECT_NE(response.body.find("&lt;script&gt;Moon card&lt;/script&gt;"),
              std::string::npos);
    EXPECT_EQ(response.body.find("<script>Moon card</script>"),
              std::string::npos);
}

/// Verify missing and malformed public IDs return a not-found response.
TEST(AppTest, RejectsUnknownCardView)
{
    App app(
        makeConfig("https://example.test/"),
        std::make_unique<AuthenticatedDataSource>(),
        deterministicRandom());
    App::Request request = authenticatedRequest();
    request.path_params.emplace("id", "PKM-2");
    App::Response response;

    app.handleCardView(request, response);

    EXPECT_EQ(response.status, 404);
}

/// Verify canonical game and loose public IDs parse without ambiguity.
TEST(PublicIdTest, ParsesCanonicalIds)
{
    const auto game_card = parsePublicId("pkm-42");
    ASSERT_TRUE(game_card);
    EXPECT_EQ(game_card->game_short_name, "pkm");
    EXPECT_EQ(game_card->card_number, 42);

    const auto loose_card = parsePublicId("z1");
    ASSERT_TRUE(loose_card);
    EXPECT_EQ(loose_card->game_short_name, std::nullopt);
    EXPECT_EQ(loose_card->card_number, 1261);

    EXPECT_FALSE(parsePublicId("PKM-42"));
    EXPECT_FALSE(parsePublicId("pkm-042"));
    EXPECT_FALSE(parsePublicId("pkm-0"));
    EXPECT_FALSE(parsePublicId("z-1-extra"));
}