Files
Card-Collection-Manager-3/tests/pokemon_set_completion_tests.cpp
T
2026-07-24 08:56:53 +02:00

338 lines
12 KiB
C++

#include <doctest/doctest.h>
#include "ccm/domain/PokemonCard.hpp"
#include "ccm/domain/PokemonSetCatalog.hpp"
#include "ccm/services/ConfigService.hpp"
#include "ccm/services/PokemonSetCatalogService.hpp"
#include "ccm/services/PokemonSetCompletion.hpp"
#include "fakes/InMemoryFileSystem.hpp"
#include <nlohmann/json.hpp>
using namespace ccm;
using ccm::testing::InMemoryFileSystem;
namespace {
ConfigService makeConfig(InMemoryFileSystem& fs, const std::string& dataDir) {
Configuration c;
c.dataStorage = dataDir;
c.defaultGame = Game::Magic;
fs.writeText("/app/config.json", nlohmann::json(c).dump());
ConfigService cfg{fs, "/app/config.json", dataDir};
cfg.initialize();
return cfg;
}
PokemonCard makeOwned(PokemonRegion region, std::string setId, std::string setNo) {
PokemonCard c;
c.id = 1;
c.name = "Owned";
c.region = region;
c.set.id = std::move(setId);
c.set.name = "Set";
c.setNo = std::move(setNo);
c.language = Language::English;
return c;
}
PokemonSetCatalog westCatalog() {
PokemonSetCatalog catalog;
PokemonSetCatalogPack base;
base.setId = "base1";
base.setName = "Base";
base.cards = {
{"4", "Charizard"},
{"58", "Growlithe"},
{"59", "Arcanine"},
};
catalog.packs.push_back(std::move(base));
return catalog;
}
PokemonSetCatalog asiaCatalog() {
PokemonSetCatalog catalog;
PokemonSetCatalogPack pmcg1;
pmcg1.setId = "PMCG1";
pmcg1.setName = "Expansion Pack";
pmcg1.cards = {
{"001", "Charmander"},
{"002", "Charmeleon"},
{"006", "Charizard"},
};
catalog.packs.push_back(std::move(pmcg1));
return catalog;
}
} // namespace
TEST_SUITE("computePokemonSetCompletion") {
TEST_CASE("west pack with owned card appears") {
const auto west = westCatalog();
const auto asia = asiaCatalog();
std::vector<PokemonCard> collection{
makeOwned(PokemonRegion::West, "base1", "4"),
};
const auto rows = computePokemonSetCompletion(collection, west, asia);
REQUIRE(rows.size() == 1);
CHECK(rows[0].region == PokemonRegion::West);
CHECK(rows[0].setId == "base1");
CHECK(rows[0].ownedUnique == 1);
CHECK(rows[0].total == 3);
CHECK(rows[0].percent() == 33);
}
TEST_CASE("asia and west do not cross-count") {
const auto west = westCatalog();
const auto asia = asiaCatalog();
// Same collector-looking number, different region/set.
PokemonCard westCard = makeOwned(PokemonRegion::West, "base1", "4");
PokemonCard asiaCard = makeOwned(PokemonRegion::Asia, "PMCG1", "006");
asiaCard.id = 2;
const auto rows = computePokemonSetCompletion({westCard, asiaCard}, west, asia);
REQUIRE(rows.size() == 2);
CHECK(rows[0].setId == "base1");
CHECK(rows[0].ownedUnique == 1);
CHECK(rows[1].setId == "PMCG1");
CHECK(rows[1].ownedUnique == 1);
}
TEST_CASE("orders packs by releaseDate then setName") {
PokemonSetCatalog west;
PokemonSetCatalogPack newer;
newer.setId = "sv01";
newer.setName = "Scarlet & Violet";
newer.cards = {{"1", "Sprigatito"}};
PokemonSetCatalogPack older;
older.setId = "base1";
older.setName = "Zoo Set"; // would sort after Scarlet by name
older.cards = {{"4", "Charizard"}};
west.packs.push_back(std::move(newer));
west.packs.push_back(std::move(older));
PokemonSetCatalog emptyAsia;
PokemonCard base = makeOwned(PokemonRegion::West, "base1", "4");
base.set.releaseDate = "1999/01/09";
PokemonCard sv = makeOwned(PokemonRegion::West, "sv01", "1");
sv.id = 2;
sv.set.releaseDate = "2023/03/31";
const auto rows = computePokemonSetCompletion({base, sv}, west, emptyAsia);
REQUIRE(rows.size() == 2);
CHECK(rows[0].setId == "base1");
CHECK(rows[0].releaseDate == "1999/01/09");
CHECK(rows[1].setId == "sv01");
CHECK(rows[1].releaseDate == "2023/03/31");
}
TEST_CASE("region filter isolates catalogs") {
const auto west = westCatalog();
const auto asia = asiaCatalog();
PokemonCard westCard = makeOwned(PokemonRegion::West, "base1", "4");
PokemonCard asiaCard = makeOwned(PokemonRegion::Asia, "PMCG1", "001");
asiaCard.id = 2;
const auto westOnly = computePokemonSetCompletion(
{westCard, asiaCard}, west, asia, PokemonRegion::West);
REQUIRE(westOnly.size() == 1);
CHECK(westOnly[0].setId == "base1");
const auto asiaOnly = computePokemonSetCompletion(
{westCard, asiaCard}, west, asia, PokemonRegion::Asia);
REQUIRE(asiaOnly.size() == 1);
CHECK(asiaOnly[0].setId == "PMCG1");
}
TEST_CASE("normalizes west 4/102 to 4") {
const auto west = westCatalog();
PokemonSetCatalog emptyAsia;
std::vector<PokemonCard> collection{
makeOwned(PokemonRegion::West, "base1", "4/102"),
};
const auto rows = computePokemonSetCompletion(collection, west, emptyAsia);
REQUIRE(rows.size() == 1);
CHECK(rows[0].ownedUnique == 1);
}
TEST_CASE("legacy pokemontcg West set id matches TCGdex catalog pack") {
PokemonSetCatalog west;
PokemonSetCatalogPack pack;
pack.setId = "sv01";
pack.setName = "Scarlet & Violet";
pack.cards = {{"6", "Charizard"}};
west.packs.push_back(std::move(pack));
PokemonSetCatalog emptyAsia;
std::vector<PokemonCard> collection{
makeOwned(PokemonRegion::West, "sv1", "6"),
};
const auto rows = computePokemonSetCompletion(collection, west, emptyAsia);
REQUIRE(rows.size() == 1);
CHECK(rows[0].setId == "sv01");
CHECK(rows[0].ownedUnique == 1);
const auto checklist = pokemonChecklistForSet(
collection, west, emptyAsia, PokemonRegion::West, "sv01");
REQUIRE(checklist.size() == 1);
CHECK(checklist[0].owned);
}
TEST_CASE("amount does not inflate unique ownership") {
const auto west = westCatalog();
PokemonSetCatalog emptyAsia;
PokemonCard a = makeOwned(PokemonRegion::West, "base1", "4");
a.amount = 5;
PokemonCard b = makeOwned(PokemonRegion::West, "base1", "4");
b.id = 2;
PokemonCard c = makeOwned(PokemonRegion::West, "base1", "58");
c.id = 3;
const auto rows = computePokemonSetCompletion({a, b, c}, west, emptyAsia);
REQUIRE(rows.size() == 1);
CHECK(rows[0].ownedUnique == 2);
}
TEST_CASE("language filter hides packs with no matching language") {
const auto west = westCatalog();
PokemonSetCatalog emptyAsia;
PokemonCard en = makeOwned(PokemonRegion::West, "base1", "4");
en.language = Language::English;
CHECK(computePokemonSetCompletion({en}, west, emptyAsia, std::nullopt,
Language::German)
.empty());
REQUIRE(computePokemonSetCompletion({en}, west, emptyAsia, std::nullopt,
Language::English)
.size() == 1);
}
TEST_CASE("empty catalog yields no rows") {
PokemonSetCatalog empty;
std::vector<PokemonCard> collection{
makeOwned(PokemonRegion::West, "base1", "4"),
};
CHECK(computePokemonSetCompletion(collection, empty, empty).empty());
}
}
TEST_SUITE("pokemonChecklistForSet") {
TEST_CASE("marks owned west cards") {
const auto west = westCatalog();
PokemonSetCatalog emptyAsia;
std::vector<PokemonCard> collection{
makeOwned(PokemonRegion::West, "base1", "58"),
};
const auto list = pokemonChecklistForSet(collection, west, emptyAsia,
PokemonRegion::West, "base1");
REQUIRE(list.size() == 3);
CHECK(list[0].setNo == "4");
CHECK(list[0].owned == false);
CHECK(list[1].setNo == "58");
CHECK(list[1].owned == true);
CHECK(list[2].setNo == "59");
CHECK(list[2].owned == false);
}
TEST_CASE("orders unpadded set numbers numerically") {
PokemonSetCatalog west;
PokemonSetCatalogPack pack;
pack.setId = "base1";
pack.setName = "Base";
// Insert out of order / in lex-favoring order to prove we re-sort.
pack.cards = {
{"100", "Lightning Energy"},
{"1", "Alakazam"},
{"10", "Mewtwo"},
{"2", "Blastoise"},
};
west.packs.push_back(std::move(pack));
PokemonSetCatalog emptyAsia;
const auto list = pokemonChecklistForSet({}, west, emptyAsia,
PokemonRegion::West, "base1");
REQUIRE(list.size() == 4);
CHECK(list[0].setNo == "1");
CHECK(list[1].setNo == "2");
CHECK(list[2].setNo == "10");
CHECK(list[3].setNo == "100");
}
TEST_CASE("asia card does not mark west checklist") {
const auto west = westCatalog();
const auto asia = asiaCatalog();
std::vector<PokemonCard> collection{
makeOwned(PokemonRegion::Asia, "PMCG1", "006"),
};
const auto list = pokemonChecklistForSet(collection, west, asia,
PokemonRegion::West, "base1");
REQUIRE(list.size() == 3);
CHECK(list[0].owned == false);
CHECK(list[1].owned == false);
CHECK(list[2].owned == false);
}
}
TEST_SUITE("pokemonLanguagesInCollection") {
TEST_CASE("region filter scopes languages") {
PokemonCard westEn = makeOwned(PokemonRegion::West, "base1", "4");
westEn.language = Language::English;
PokemonCard asiaJp = makeOwned(PokemonRegion::Asia, "PMCG1", "001");
asiaJp.id = 2;
asiaJp.language = Language::Japanese;
const auto all = pokemonLanguagesInCollection({westEn, asiaJp});
REQUIRE(all.size() == 2);
CHECK(all[0] == Language::English);
CHECK(all[1] == Language::Japanese);
const auto westOnly =
pokemonLanguagesInCollection({westEn, asiaJp}, PokemonRegion::West);
REQUIRE(westOnly.size() == 1);
CHECK(westOnly[0] == Language::English);
}
}
TEST_SUITE("pokemonRegionsInCollection") {
TEST_CASE("reports regions with matching catalog packs") {
const auto west = westCatalog();
const auto asia = asiaCatalog();
PokemonCard westCard = makeOwned(PokemonRegion::West, "base1", "4");
PokemonCard asiaCard = makeOwned(PokemonRegion::Asia, "PMCG1", "001");
asiaCard.id = 2;
const auto regions =
pokemonRegionsInCollection({westCard, asiaCard}, west, asia);
REQUIRE(regions.size() == 2);
CHECK(regions[0] == PokemonRegion::West);
CHECK(regions[1] == PokemonRegion::Asia);
}
}
TEST_SUITE("PokemonSetCatalogService") {
TEST_CASE("save then load round-trips for west and asia paths") {
InMemoryFileSystem fs;
auto config = makeConfig(fs, "/data");
PokemonSetCatalogService store{fs, config, [](Game) { return "pokemon"; }};
CHECK_FALSE(store.exists(PokemonRegion::West));
CHECK_FALSE(store.exists(PokemonRegion::Asia));
CHECK(store.load(PokemonRegion::West).isErr());
const auto west = westCatalog();
const auto asia = asiaCatalog();
REQUIRE(store.save(PokemonRegion::West, west).isOk());
REQUIRE(store.save(PokemonRegion::Asia, asia).isOk());
CHECK(store.exists(PokemonRegion::West));
CHECK(store.exists(PokemonRegion::Asia));
const auto loadedWest = store.load(PokemonRegion::West);
REQUIRE(loadedWest.isOk());
CHECK(loadedWest.value() == west);
const auto loadedAsia = store.load(PokemonRegion::Asia);
REQUIRE(loadedAsia.isOk());
CHECK(loadedAsia.value() == asia);
CHECK(fs.exists("/data/pokemon/set-catalog-west.json"));
CHECK(fs.exists("/data/pokemon/set-catalog-asia.json"));
}
}