#include #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 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 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 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 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 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 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 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")); } }