#include #include "ccm/games/pokemonjp/JapanesePokemonCardPreviewSource.hpp" #include "ccm/games/pokemonjp/JapanesePokemonEnCatalog.hpp" #include "ccm/ports/IHttpClient.hpp" #include #include using namespace ccm; namespace { class RoutingHttpClient final : public IHttpClient { public: std::unordered_map bodies; std::string lastUrl; bool ok = true; Result get(std::string_view url) override { lastUrl = std::string(url); if (!ok) return Result::err("offline"); const auto it = bodies.find(lastUrl); if (it == bodies.end()) return Result::err("unknown url: " + lastUrl); return Result::ok(it->second); } }; JapanesePokemonEnCatalog sampleCatalog() { auto c = JapanesePokemonEnCatalog::parse(R"({ "sets": { "SV1a": {"name_en":"Triplet Beat","name_ja":"トリプレットビート"} }, "prints": [ {"set_id":"SV1a","local_id":"001","name_en":"Tropius","name_ja":"トロピウス","name_en_source":"bulbapedia"} ] })"); REQUIRE(c.isOk()); return std::move(c).value(); } } // namespace TEST_SUITE("JapanesePokemonCardPreviewSource helpers") { TEST_CASE("normalizeLocalId strips slash and whitespace") { CHECK(JapanesePokemonCardPreviewSource::normalizeLocalId(" 001/102 ") == "001"); CHECK(JapanesePokemonCardPreviewSource::normalizeLocalId("4/102") == "4"); } TEST_CASE("imageUrlFromBase appends high.png") { CHECK(JapanesePokemonCardPreviewSource::imageUrlFromBase( "https://assets.tcgdex.net/ja/SV/SV1a/001") == "https://assets.tcgdex.net/ja/SV/SV1a/001/high.png"); } TEST_CASE("buildCardUrl encodes set-local id") { CHECK(JapanesePokemonCardPreviewSource::buildCardUrl("SV1a", "001") == "https://api.tcgdex.net/v2/ja/cards/SV1a-001"); } } TEST_SUITE("JapanesePokemonCardPreviewSource::parseSetCards") { TEST_CASE("parses localId name and image") { const std::string json = R"({ "id":"SV1a", "cards":[ {"id":"SV1a-001","localId":"001","name":"トロピウス", "image":"https://assets.tcgdex.net/ja/SV/SV1a/001","rarity":"Common"} ] })"; const auto out = JapanesePokemonCardPreviewSource::parseSetCards(json); REQUIRE(out.isOk()); REQUIRE(out.value().size() == 1); CHECK(out.value()[0].localId == "001"); CHECK(out.value()[0].nameJa == "トロピウス"); CHECK(out.value()[0].imageBase == "https://assets.tcgdex.net/ja/SV/SV1a/001"); } TEST_CASE("missing cards array is Transient") { const auto out = JapanesePokemonCardPreviewSource::parseSetCards(R"({"id":"X"})"); REQUIRE(out.isErr()); CHECK(out.error().kind == PreviewLookupError::Kind::Transient); } } TEST_SUITE("JapanesePokemonCardPreviewSource::parseCardImageUrl") { TEST_CASE("returns high.png URL") { const auto out = JapanesePokemonCardPreviewSource::parseCardImageUrl( R"({"id":"SV1a-001","image":"https://assets.tcgdex.net/ja/SV/SV1a/001"})"); REQUIRE(out.isOk()); CHECK(out.value() == "https://assets.tcgdex.net/ja/SV/SV1a/001/high.png"); } TEST_CASE("null image is NotFound") { const auto out = JapanesePokemonCardPreviewSource::parseCardImageUrl( R"({"id":"PMCG1-001","image":null})"); REQUIRE(out.isErr()); CHECK(out.error().kind == PreviewLookupError::Kind::NotFound); } TEST_CASE("malformed JSON is Transient") { const auto out = JapanesePokemonCardPreviewSource::parseCardImageUrl("{bad"); REQUIRE(out.isErr()); CHECK(out.error().kind == PreviewLookupError::Kind::Transient); } } TEST_SUITE("JapanesePokemonCardPreviewSource::parsePrintVariants") { TEST_CASE("matches English catalog name") { const std::string body = R"({ "id":"SV1a", "cards":[ {"localId":"001","name":"トロピウス","rarity":"Common"}, {"localId":"002","name":"other","rarity":"Common"} ] })"; const auto catalog = sampleCatalog(); const auto out = JapanesePokemonCardPreviewSource::parsePrintVariants( body, "SV1a", "Tropius", catalog); REQUIRE(out.isOk()); REQUIRE(out.value().size() == 1); CHECK(out.value()[0].setNo == "001"); } TEST_CASE("matches Japanese name without catalog") { const std::string body = R"({ "id":"SV1a", "cards":[{"localId":"001","name":"トロピウス"}] })"; JapanesePokemonEnCatalog empty; const auto out = JapanesePokemonCardPreviewSource::parsePrintVariants( body, "SV1a", "トロピウス", empty); REQUIRE(out.isOk()); CHECK(out.value().front().setNo == "001"); } TEST_CASE("rejects stale catalog localId when name_ja disagrees with TCGdex") { // Historical seed bug: Charmander mapped to 001 (actually Bulbasaur). const auto catalog = JapanesePokemonEnCatalog::parse(R"({ "sets": {}, "prints": [ {"set_id":"PMCG1","local_id":"001","name_en":"Charmander","name_ja":"ヒトカゲ"} ] })"); REQUIRE(catalog.isOk()); const std::string body = R"({ "id":"PMCG1", "cards":[ {"localId":"001","name":"フシギダネ","rarity":"Common"}, {"localId":"014","name":"ヒトカゲ","rarity":"Common"} ] })"; const auto out = JapanesePokemonCardPreviewSource::parsePrintVariants( body, "PMCG1", "Charmander", catalog.value()); REQUIRE(out.isErr()); } TEST_CASE("accepts corrected catalog localId for Charmander") { const auto catalog = JapanesePokemonEnCatalog::parse(R"({ "sets": {}, "prints": [ {"set_id":"PMCG1","local_id":"014","name_en":"Charmander","name_ja":"ヒトカゲ"} ] })"); REQUIRE(catalog.isOk()); const std::string body = R"({ "id":"PMCG1", "cards":[ {"localId":"001","name":"フシギダネ"}, {"localId":"014","name":"ヒトカゲ","rarity":"Common"} ] })"; const auto out = JapanesePokemonCardPreviewSource::parsePrintVariants( body, "PMCG1", "Charmander", catalog.value()); REQUIRE(out.isOk()); REQUIRE(out.value().size() == 1); CHECK(out.value()[0].setNo == "014"); } TEST_CASE("English Blastoise and Mewtwo resolve Expansion Pack localIds") { const auto catalog = JapanesePokemonEnCatalog::parse(R"({ "sets": {}, "prints": [ {"set_id":"PMCG1","local_id":"032","name_en":"Blastoise","name_ja":"カメックス"}, {"set_id":"PMCG1","local_id":"050","name_en":"Mewtwo","name_ja":"ミュウツー"} ] })"); REQUIRE(catalog.isOk()); const std::string body = R"({ "id":"PMCG1", "cards":[ {"localId":"032","name":"カメックス","rarity":"Holo Rare"}, {"localId":"050","name":"ミュウツー","rarity":"Holo Rare"} ] })"; auto blast = JapanesePokemonCardPreviewSource::parsePrintVariants( body, "PMCG1", "Blastoise", catalog.value()); REQUIRE(blast.isOk()); REQUIRE(blast.value().size() == 1); CHECK(blast.value()[0].setNo == "032"); auto mew = JapanesePokemonCardPreviewSource::parsePrintVariants( body, "PMCG1", "Mewtwo", catalog.value()); REQUIRE(mew.isOk()); REQUIRE(mew.value().size() == 1); CHECK(mew.value()[0].setNo == "050"); } TEST_CASE("English Switch resolves Expansion Pack localId 073") { const auto catalog = JapanesePokemonEnCatalog::parse(R"({ "sets": {}, "prints": [ {"set_id":"PMCG1","local_id":"073","name_en":"Switch","name_ja":"ポケモンいれかえ"} ] })"); REQUIRE(catalog.isOk()); const std::string body = R"({ "id":"PMCG1", "cards":[ {"localId":"071","name":"きずぐすり","rarity":"Common"}, {"localId":"073","name":"ポケモンいれかえ","rarity":"Common"} ] })"; const auto out = JapanesePokemonCardPreviewSource::parsePrintVariants( body, "PMCG1", "Switch", catalog.value()); REQUIRE(out.isOk()); REQUIRE(out.value().size() == 1); CHECK(out.value()[0].setNo == "073"); } } TEST_SUITE("JapanesePokemonCardPreviewSource::fetchImageUrl") { TEST_CASE("resolves via card endpoint when localId present") { RoutingHttpClient http; http.bodies[JapanesePokemonCardPreviewSource::buildCardUrl("SV1a", "001")] = R"({"id":"SV1a-001","image":"https://assets.tcgdex.net/ja/SV/SV1a/001"})"; auto catalog = sampleCatalog(); JapanesePokemonCardPreviewSource src{http, catalog}; const auto out = src.fetchImageUrl("Tropius", "SV1a", "001"); REQUIRE(out.isOk()); CHECK(out.value() == "https://assets.tcgdex.net/ja/SV/SV1a/001/high.png"); } TEST_CASE("resolves via set detail when card has no image but set row does") { RoutingHttpClient http; http.bodies[JapanesePokemonCardPreviewSource::buildCardUrl("SV1a", "001")] = R"({"id":"SV1a-001","image":null})"; http.bodies[JapanesePokemonCardPreviewSource::buildSetDetailUrl("SV1a")] = R"({ "id":"SV1a", "cards":[{"localId":"001","name":"トロピウス", "image":"https://assets.tcgdex.net/ja/SV/SV1a/001"}] })"; auto catalog = sampleCatalog(); JapanesePokemonCardPreviewSource src{http, catalog}; const auto out = src.fetchImageUrl("Tropius", "SV1a", "001"); REQUIRE(out.isOk()); CHECK(out.value().find("/high.png") != std::string::npos); } TEST_CASE("stale catalog does not bind English name to wrong localId image") { RoutingHttpClient http; http.bodies[JapanesePokemonCardPreviewSource::buildSetDetailUrl("PMCG1")] = R"({ "id":"PMCG1", "cards":[ {"localId":"001","name":"フシギダネ", "image":"https://assets.tcgdex.net/ja/PMCG/PMCG1/001"}, {"localId":"014","name":"ヒトカゲ", "image":"https://assets.tcgdex.net/ja/PMCG/PMCG1/014"} ] })"; const auto catalog = JapanesePokemonEnCatalog::parse(R"({ "sets": {}, "prints": [ {"set_id":"PMCG1","local_id":"001","name_en":"Charmander","name_ja":"ヒトカゲ"} ] })"); REQUIRE(catalog.isOk()); JapanesePokemonCardPreviewSource src{http, catalog.value()}; // Empty setNo forces name match; stale catalog must not pick Bulbasaur's art. const auto out = src.fetchImageUrl("Charmander", "PMCG1", ""); REQUIRE(out.isErr()); CHECK(out.error().kind == PreviewLookupError::Kind::NotFound); } TEST_CASE("English name resolves correct localId image via catalog") { RoutingHttpClient http; http.bodies[JapanesePokemonCardPreviewSource::buildSetDetailUrl("PMCG1")] = R"({ "id":"PMCG1", "cards":[ {"localId":"001","name":"フシギダネ", "image":"https://assets.tcgdex.net/ja/PMCG/PMCG1/001"}, {"localId":"014","name":"ヒトカゲ", "image":"https://assets.tcgdex.net/ja/PMCG/PMCG1/014"} ] })"; const auto catalog = JapanesePokemonEnCatalog::parse(R"({ "sets": {}, "prints": [ {"set_id":"PMCG1","local_id":"014","name_en":"Charmander","name_ja":"ヒトカゲ"} ] })"); REQUIRE(catalog.isOk()); JapanesePokemonCardPreviewSource src{http, catalog.value()}; const auto out = src.fetchImageUrl("Charmander", "PMCG1", ""); REQUIRE(out.isOk()); CHECK(out.value() == "https://assets.tcgdex.net/ja/PMCG/PMCG1/014/high.png"); } TEST_CASE("null image on set-specific card is NotFound without other-printing fallback") { RoutingHttpClient http; http.bodies[JapanesePokemonCardPreviewSource::buildCardUrl("PMCG1", "021")] = R"({"id":"PMCG1-021","name":"リザードン","image":null})"; http.bodies[JapanesePokemonCardPreviewSource::buildSetDetailUrl("PMCG1")] = R"({ "id":"PMCG1", "cards":[{"localId":"021","name":"リザードン"}] })"; JapanesePokemonEnCatalog empty; JapanesePokemonCardPreviewSource src{http, empty}; const auto out = src.fetchImageUrl("Charizard", "PMCG1", "021"); REQUIRE(out.isErr()); CHECK(out.error().kind == PreviewLookupError::Kind::NotFound); } TEST_CASE("catalog tcgplayer_id gap-fills when TCGdex image is null") { RoutingHttpClient http; http.bodies[JapanesePokemonCardPreviewSource::buildCardUrl("PMCG1", "021")] = R"({"id":"PMCG1-021","name":"リザードン","image":null})"; http.bodies[JapanesePokemonCardPreviewSource::buildSetDetailUrl("PMCG1")] = R"({ "id":"PMCG1", "cards":[{"localId":"021","name":"リザードン"}] })"; const auto catalog = JapanesePokemonEnCatalog::parse(R"({ "sets": {}, "prints": [ {"set_id":"PMCG1","local_id":"021","name_en":"Charizard", "name_ja":"リザードン","tcgplayer_id":"575604"} ] })"); REQUIRE(catalog.isOk()); JapanesePokemonCardPreviewSource src{http, catalog.value()}; const auto out = src.fetchImageUrl("Charizard", "PMCG1", "021"); REQUIRE(out.isOk()); CHECK(out.value() == "https://product-images.tcgplayer.com/fit-in/437x437/575604.jpg"); } TEST_CASE("HTTP failure is Transient") { RoutingHttpClient http; http.ok = false; JapanesePokemonEnCatalog empty; JapanesePokemonCardPreviewSource src{http, empty}; const auto out = src.fetchImageUrl("Tropius", "SV1a", "001"); REQUIRE(out.isErr()); CHECK(out.error().kind == PreviewLookupError::Kind::Transient); } TEST_CASE("catalog-only theme deck resolves preview from tcgplayer_id") { RoutingHttpClient http; // No TCGdex bodies: card + set detail both miss. const auto catalog = JapanesePokemonEnCatalog::parse(R"({ "sets": {"TamamushiCG":{"name_en":"Tamamushi City Gym"}}, "prints": [ {"set_id":"TamamushiCG","local_id":"021","name_en":"Celadon City Gym", "name_ja":"タマムシシティジム","tcgplayer_id":"12345"} ] })"); REQUIRE(catalog.isOk()); JapanesePokemonCardPreviewSource src{http, catalog.value()}; const auto out = src.fetchImageUrl("Celadon City Gym", "TamamushiCG", "021"); REQUIRE(out.isOk()); CHECK(out.value() == "https://product-images.tcgplayer.com/fit-in/437x437/12345.jpg"); } TEST_CASE("Tamamushi City Gym Erika uses catalog tcgplayer gap-fill") { RoutingHttpClient http; const auto catalog = JapanesePokemonEnCatalog::parse(R"({ "sets": {"TamamushiCG":{"name_en":"Tamamushi City Gym"}}, "prints": [ {"set_id":"TamamushiCG","local_id":"016","name_en":"Erika", "name_ja":"エリカ","name_en_source":"trainer-table", "tcgplayer_id":"576776"} ] })"); REQUIRE(catalog.isOk()); JapanesePokemonCardPreviewSource src{http, catalog.value()}; const auto out = src.fetchImageUrl("Erika", "TamamushiCG", "016"); REQUIRE(out.isOk()); CHECK(out.value() == "https://product-images.tcgplayer.com/fit-in/437x437/576776.jpg"); } TEST_CASE("neo catalog image_url gap-fills when TCGdex image is null") { RoutingHttpClient http; http.bodies[JapanesePokemonCardPreviewSource::buildCardUrl("neo4", "106")] = R"({"id":"neo4-106","name":"ラッキースタジアム","image":null})"; http.bodies[JapanesePokemonCardPreviewSource::buildSetDetailUrl("neo4")] = R"({ "id":"neo4", "cards":[{"localId":"106","name":"ラッキースタジアム"}] })"; const auto catalog = JapanesePokemonEnCatalog::parse(R"({ "sets": {}, "prints": [ {"set_id":"neo4","local_id":"106","name_en":"Shining Celebi", "name_ja":"輝くセレビ","name_en_source":"species-table-variant", "image_url":"https://images.pokemontcg.io/neo4/106_hires.png"} ] })"); REQUIRE(catalog.isOk()); JapanesePokemonCardPreviewSource src{http, catalog.value()}; const auto out = src.fetchImageUrl("Shining Celebi", "neo4", "106"); REQUIRE(out.isOk()); CHECK(out.value() == "https://images.pokemontcg.io/neo4/106_hires.png"); } } TEST_SUITE("JapanesePokemonCardPreviewSource::detectPrintVariants catalog-only") { TEST_CASE("English trainer name resolves when TCGdex set detail is unavailable") { RoutingHttpClient http; const auto catalog = JapanesePokemonEnCatalog::parse(R"json({ "sets": {"TamamushiCG":{"name_en":"Tamamushi City Gym"}}, "prints": [ {"set_id":"TamamushiCG","local_id":"021","name_en":"Celadon City Gym", "name_ja":"タマムシシティジム","name_en_source":"trainer-table", "image_url":"https://example.com/celadon.jpg"}, {"set_id":"TamamushiCG","local_id":"001","name_en":"Erika's Oddish", "name_ja":"エリカのナゾノクサ","name_en_source":"manual", "image_url":"https://example.com/oddish.jpg"} ] })json"); REQUIRE(catalog.isOk()); JapanesePokemonCardPreviewSource src{http, catalog.value()}; const auto out = src.detectPrintVariants("Celadon City Gym", "TamamushiCG"); REQUIRE(out.isOk()); REQUIRE(out.value().size() == 1); CHECK(out.value()[0].setNo == "021"); } TEST_CASE("detectPrintVariantsFromCatalog includes UnnumberedPromo prints without image_url") { const auto catalog = JapanesePokemonEnCatalog::parse(R"json({ "sets": {}, "prints": [ {"set_id":"UnnumberedPromo","local_id":"007","name_en":"Mewtwo (CoroCoro promo)", "name_ja":"ミュウツー","name_en_source":"manual"}, {"set_id":"UnnumberedPromo","local_id":"008","name_en":"Mewtwo (Fan Book promo)", "name_ja":"ミュウツー","name_en_source":"manual"}, {"set_id":"UnnumberedPromo","local_id":"030","name_en":"Mewtwo (WHF Special Sheet promo)", "name_ja":"ミュウツー","name_en_source":"manual", "image_url":"https://archives.bulbagarden.net/media/upload/w/w/MewtwoWHF.jpg"}, {"set_id":"UnnumberedPromo","local_id":"045","name_en":"Mewtwo Strikes Back (Jumbo)", "name_ja":"","name_en_source":"manual"} ] })json"); REQUIRE(catalog.isOk()); const auto mew = JapanesePokemonCardPreviewSource::detectPrintVariantsFromCatalog( "UnnumberedPromo", "Mewtwo", catalog.value()); REQUIRE(mew.isOk()); REQUIRE(mew.value().size() == 4); // Imaged prints first, then empty-URL identity rows. CHECK(mew.value()[0].setNo == "030"); CHECK(mew.value()[1].setNo == "007"); CHECK(mew.value()[2].setNo == "008"); CHECK(mew.value()[3].setNo == "045"); RoutingHttpClient http; JapanesePokemonCardPreviewSource src{http, catalog.value()}; const auto img = src.fetchImageUrl("Mewtwo", "UnnumberedPromo", "030"); REQUIRE(img.isOk()); CHECK(img.value() == "https://archives.bulbagarden.net/media/upload/w/w/MewtwoWHF.jpg"); } TEST_CASE("fetchImageUrl does not borrow sibling UnnumberedPromo image") { RoutingHttpClient http; const auto catalog = JapanesePokemonEnCatalog::parse(R"json({ "sets": {}, "prints": [ {"set_id":"UnnumberedPromo","local_id":"007","name_en":"Mewtwo (CoroCoro promo)", "name_ja":"ミュウツー","name_en_source":"manual"}, {"set_id":"UnnumberedPromo","local_id":"030","name_en":"Mewtwo (WHF Special Sheet promo)", "name_ja":"ミュウツー","name_en_source":"manual", "image_url":"https://archives.bulbagarden.net/media/upload/w/w/MewtwoWHF.jpg"} ] })json"); REQUIRE(catalog.isOk()); JapanesePokemonCardPreviewSource src{http, catalog.value()}; const auto empty = src.fetchImageUrl("Mewtwo", "UnnumberedPromo", "007"); REQUIRE(empty.isErr()); CHECK(empty.error().kind == PreviewLookupError::Kind::NotFound); const auto whf = src.fetchImageUrl("Mewtwo", "UnnumberedPromo", "030"); REQUIRE(whf.isOk()); CHECK(whf.value() == "https://archives.bulbagarden.net/media/upload/w/w/MewtwoWHF.jpg"); } TEST_CASE("detectPrintVariantsFromCatalog dedupes shared preview URLs") { const auto catalog = JapanesePokemonEnCatalog::parse(R"json({ "sets": {}, "prints": [ {"set_id":"UnnumberedPromo","local_id":"030","name_en":"Mewtwo (WHF Special Sheet promo)", "image_url":"https://archives.bulbagarden.net/media/upload/w/w/same.jpg"}, {"set_id":"UnnumberedPromo","local_id":"073","name_en":"Mewtwo (Song Best Collection promo)", "image_url":"https://archives.bulbagarden.net/media/upload/w/w/same.jpg"}, {"set_id":"UnnumberedPromo","local_id":"197","name_en":"Mewtwo (Wizards Promo 12)", "image_url":"https://archives.bulbagarden.net/media/upload/w/w/same.jpg"} ] })json"); REQUIRE(catalog.isOk()); const auto mew = JapanesePokemonCardPreviewSource::detectPrintVariantsFromCatalog( "UnnumberedPromo", "Mewtwo", catalog.value()); REQUIRE(mew.isOk()); REQUIRE(mew.value().size() == 1); CHECK(mew.value()[0].setNo == "030"); } TEST_CASE("detectPrintVariantsFromCatalog matches owner Pokemon English title") { const auto catalog = JapanesePokemonEnCatalog::parse(R"json({ "sets": {}, "prints": [ {"set_id":"TamamushiCG","local_id":"001","name_en":"Erika's Oddish", "name_ja":"エリカのナゾノクサ", "image_url":"https://example.com/oddish.jpg"} ] })json"); REQUIRE(catalog.isOk()); const auto out = JapanesePokemonCardPreviewSource::detectPrintVariantsFromCatalog( "TamamushiCG", "Erika's Oddish", catalog.value()); REQUIRE(out.isOk()); REQUIRE(out.value().size() == 1); CHECK(out.value()[0].setNo == "001"); } TEST_CASE("detectPrintVariantsFromCatalog matches Dark Rocket and Owner PMCG titles") { const auto catalog = JapanesePokemonEnCatalog::parse(R"json({ "sets": {}, "prints": [ {"set_id":"PMCG4","local_id":"017","name_en":"Dark Charizard", "name_ja":"わるいリザードン","name_en_source":"species-table-variant", "tcgplayer_id":"575744"}, {"set_id":"PMCG6","local_id":"042","name_en":"Rocket's Zapdos", "name_ja":"R団のサンダー","name_en_source":"species-table-variant", "tcgplayer_id":"1"}, {"set_id":"PMCG5","local_id":"002","name_en":"Erika's Oddish", "name_ja":"エリカのナゾノクサ","name_en_source":"species-table-variant", "tcgplayer_id":"2"} ] })json"); REQUIRE(catalog.isOk()); const auto dark = JapanesePokemonCardPreviewSource::detectPrintVariantsFromCatalog( "PMCG4", "Dark Charizard", catalog.value()); REQUIRE(dark.isOk()); REQUIRE(dark.value().size() == 1); CHECK(dark.value()[0].setNo == "017"); const auto rocket = JapanesePokemonCardPreviewSource::detectPrintVariantsFromCatalog( "PMCG6", "Rocket's Zapdos", catalog.value()); REQUIRE(rocket.isOk()); REQUIRE(rocket.value().size() == 1); CHECK(rocket.value()[0].setNo == "042"); const auto owner = JapanesePokemonCardPreviewSource::detectPrintVariantsFromCatalog( "PMCG5", "Erika's Oddish", catalog.value()); REQUIRE(owner.isOk()); REQUIRE(owner.value().size() == 1); CHECK(owner.value()[0].setNo == "002"); } TEST_CASE("detectPrintVariantsFromCatalog matches Light and Shining neo titles") { const auto catalog = JapanesePokemonEnCatalog::parse(R"json({ "sets": {}, "prints": [ {"set_id":"neo4","local_id":"004","name_en":"Light Sunflora", "name_ja":"軽いサンフロラ","name_en_source":"species-table-variant", "image_url":"https://example.com/sunflora.png"}, {"set_id":"neo4","local_id":"013","name_en":"Shining Celebi", "name_ja":"輝くセレビ","name_en_source":"species-table-variant", "image_url":"https://example.com/celebi.png"} ] })json"); REQUIRE(catalog.isOk()); const auto light = JapanesePokemonCardPreviewSource::detectPrintVariantsFromCatalog( "neo4", "Light Sunflora", catalog.value()); REQUIRE(light.isOk()); REQUIRE(light.value().size() == 1); CHECK(light.value()[0].setNo == "004"); const auto shining = JapanesePokemonCardPreviewSource::detectPrintVariantsFromCatalog( "neo4", "Shining Celebi", catalog.value()); REQUIRE(shining.isOk()); REQUIRE(shining.value().size() == 1); CHECK(shining.value()[0].setNo == "013"); } } TEST_SUITE("JapanesePokemonCardPreviewSource::detectVariantsBySetNoFromCatalog") { TEST_CASE("resolves English name from setId + localId") { const auto catalog = JapanesePokemonEnCatalog::parse(R"({ "sets": {}, "prints": [ {"set_id":"PMCG1","local_id":"001","name_en":"Bulbasaur","name_ja":"フシギダネ"} ] })"); REQUIRE(catalog.isOk()); const auto out = JapanesePokemonCardPreviewSource::detectVariantsBySetNoFromCatalog( "PMCG1", "001", catalog.value()); REQUIRE(out.isOk()); REQUIRE(out.value().size() == 1); CHECK(out.value()[0].name == "Bulbasaur"); CHECK(out.value()[0].setNo == "001"); } TEST_CASE("leading-zero-insensitive localId still resolves") { const auto catalog = JapanesePokemonEnCatalog::parse(R"({ "sets": {}, "prints": [ {"set_id":"PMCG1","local_id":"001","name_en":"Bulbasaur","name_ja":"フシギダネ"}, {"set_id":"PMCG1","local_id":"011","name_en":"Weedle","name_ja":"ビードル"} ] })"); REQUIRE(catalog.isOk()); const auto byOne = JapanesePokemonCardPreviewSource::detectVariantsBySetNoFromCatalog( "PMCG1", "1", catalog.value()); REQUIRE(byOne.isOk()); REQUIRE(byOne.value().size() == 1); CHECK(byOne.value()[0].name == "Bulbasaur"); CHECK(byOne.value()[0].setNo == "001"); const auto byEleven = JapanesePokemonCardPreviewSource::detectVariantsBySetNoFromCatalog( "PMCG1", "11", catalog.value()); REQUIRE(byEleven.isOk()); REQUIRE(byEleven.value().size() == 1); CHECK(byEleven.value()[0].name == "Weedle"); } TEST_CASE("unknown localId is an error") { const auto catalog = JapanesePokemonEnCatalog::parse(R"({ "sets": {}, "prints": [ {"set_id":"PMCG1","local_id":"001","name_en":"Bulbasaur","name_ja":"フシギダネ"} ] })"); REQUIRE(catalog.isOk()); const auto out = JapanesePokemonCardPreviewSource::detectVariantsBySetNoFromCatalog( "PMCG1", "999", catalog.value()); REQUIRE(out.isErr()); } }