#include <doctest/doctest.h>

#include "ccm/domain/Configuration.hpp"
#include "ccm/domain/DigiBattle99Card.hpp"
#include "ccm/domain/DigiBattle99SetCatalog.hpp"
#include "ccm/domain/PokemonSetCatalog.hpp"
#include "ccm/domain/YuGiOhSetCatalog.hpp"
#include "ccm/domain/Enums.hpp"
#include "ccm/domain/JapanesePokemonCard.hpp"
#include "ccm/domain/MagicCard.hpp"
#include "ccm/domain/PokemonCard.hpp"
#include "ccm/domain/YuGiOhBandaiCard.hpp"
#include "ccm/domain/YuGiOhBandaiSetCatalog.hpp"
#include "ccm/domain/YuGiOhCard.hpp"
#include "ccm/domain/Set.hpp"

#include <nlohmann/json.hpp>

using namespace ccm;

TEST_SUITE("domain enums round-trip JSON as strings") {
    TEST_CASE("Game") {
        nlohmann::json j = Game::Magic;
        CHECK(j.get<std::string>() == "Magic");
        CHECK(j.get<Game>() == Game::Magic);

        nlohmann::json j2 = "Pokemon";
        CHECK(j2.get<Game>() == Game::Pokemon);

        nlohmann::json jYgo = "YuGiOh";
        CHECK(jYgo.get<Game>() == Game::YuGiOh);

        nlohmann::json jDigi = "DigiBattle99";
        CHECK(jDigi.get<Game>() == Game::DigiBattle99);

        nlohmann::json jBandai = "YuGiOhBandai";
        CHECK(jBandai.get<Game>() == Game::YuGiOhBandai);

        nlohmann::json jJp = "JapanesePokemon";
        CHECK(jJp.get<Game>() == Game::JapanesePokemon);

        nlohmann::json j3 = Theme::Dark;
        CHECK(j3.get<std::string>() == "Dark");
        CHECK(j3.get<Theme>() == Theme::Dark);
    }

    TEST_CASE("Language and Condition") {
        nlohmann::json l = Language::Japanese;
        CHECK(l.get<std::string>() == "Japanese");
        CHECK(l.get<Language>() == Language::Japanese);

        nlohmann::json k = Language::Korean;
        CHECK(k.get<std::string>() == "Korean");
        CHECK(k.get<Language>() == Language::Korean);

        nlohmann::json sc = Language::SimplifiedChinese;
        CHECK(sc.get<std::string>() == "S-Chinese");
        CHECK(sc.get<Language>() == Language::SimplifiedChinese);

        nlohmann::json tc = Language::TraditionalChinese;
        CHECK(tc.get<std::string>() == "T-Chinese");
        CHECK(tc.get<Language>() == Language::TraditionalChinese);

        nlohmann::json legacyChinese = "Chinese";
        CHECK(legacyChinese.get<Language>() == Language::SimplifiedChinese);

        nlohmann::json c = Condition::LightPlayed;
        CHECK(c.get<std::string>() == "LightPlayed");
        CHECK(c.get<Condition>() == Condition::LightPlayed);
    }

    TEST_CASE("PokemonRegion") {
        nlohmann::json j = PokemonRegion::Asia;
        CHECK(j.get<std::string>() == "Asia");
        CHECK(j.get<PokemonRegion>() == PokemonRegion::Asia);

        nlohmann::json w = "West";
        CHECK(w.get<PokemonRegion>() == PokemonRegion::West);
    }

    TEST_CASE("allGames excludes JapanesePokemon but string mapping remains") {
        for (const auto game : allGames()) {
            CHECK(game != Game::JapanesePokemon);
        }
        CHECK(allGames().size() == 5);
        CHECK(gameFromString("JapanesePokemon") == Game::JapanesePokemon);
        CHECK(pokemonBackendGame(PokemonRegion::West) == Game::Pokemon);
        CHECK(pokemonBackendGame(PokemonRegion::Asia) == Game::JapanesePokemon);
    }

    TEST_CASE("invalid enum string throws") {
        nlohmann::json bad = "Spanglish";
        CHECK_THROWS(bad.get<Language>());
    }

    TEST_CASE("all enum values round-trip through string helpers") {
        for (const auto game : allGames()) {
            const auto name = to_string(game);
            CHECK(gameFromString(name).has_value());
            CHECK(*gameFromString(name) == game);
        }

        for (const auto language : allLanguages()) {
            const auto name = to_string(language);
            CHECK(languageFromString(name).has_value());
            CHECK(*languageFromString(name) == language);
        }

        for (const auto condition : allConditions()) {
            const auto name = to_string(condition);
            CHECK(conditionFromString(name).has_value());
            CHECK(*conditionFromString(name) == condition);
        }

        for (const auto theme : allThemes()) {
            const auto name = to_string(theme);
            CHECK(themeFromString(name).has_value());
            CHECK(*themeFromString(name) == theme);
        }
    }

    TEST_CASE("invalid enum helper inputs return nullopt") {
        CHECK_FALSE(gameFromString("magic").has_value());
        CHECK_FALSE(languageFromString("EN").has_value());
        CHECK_FALSE(conditionFromString("Near Mint").has_value());
        CHECK_FALSE(themeFromString("OLED").has_value());
    }

    TEST_CASE("invalid game, condition and theme JSON values throw") {
        nlohmann::json badGame = "YGO";
        CHECK_THROWS(badGame.get<Game>());

        nlohmann::json badCondition = "Pristine";
        CHECK_THROWS(badCondition.get<Condition>());

        nlohmann::json badTheme = "Midnight";
        CHECK_THROWS(badTheme.get<Theme>());
    }
}

TEST_SUITE("Set JSON shape stays stable") {
    TEST_CASE("uses 'releaseDate' alias") {
        Set s{"abc", "Test Set", "2024/05/01"};
        const nlohmann::json j = s;
        CHECK(j.contains("releaseDate"));
        CHECK_FALSE(j.contains("release_date"));
        CHECK(j.at("releaseDate") == "2024/05/01");

        const Set back = j.get<Set>();
        CHECK(back == s);
    }
}

TEST_SUITE("MagicCard JSON") {
    TEST_CASE("round-trips with all original field names") {
        MagicCard c;
        c.id = 42;
        c.amount = 3;
        c.name = "Lightning Bolt";
        c.set = Set{"lea", "Limited Edition Alpha", "1993/08/05"};
        c.note = "rare promo";
        c.images = {"foo+bar+0.png"};
        c.language = Language::English;
        c.condition = Condition::NearMint;
        c.foil = true;
        c.signed_ = false;
        c.altered = false;

        nlohmann::json j = c;
        CHECK(j.contains("signed"));
        CHECK(j.at("signed") == false);
        CHECK(j.at("foil") == true);

        const MagicCard back = j.get<MagicCard>();
        CHECK(back == c);
    }
}

TEST_SUITE("PokemonCard JSON") {
    TEST_CASE("uses 'setNo' and 'firstEdition' aliases") {
        PokemonCard c;
        c.id = 7;
        c.amount = 1;
        c.name = "Charizard";
        c.set = Set{"base1", "Base Set", "1999/01/09"};
        c.setNo = "4/102";
        c.note = "";
        c.images = {};
        c.language = Language::English;
        c.condition = Condition::Excellent;
        c.firstEdition = true;
        c.holo = true;
        c.signed_ = false;
        c.altered = false;
        c.region = PokemonRegion::West;

        nlohmann::json j = c;
        CHECK(j.at("setNo") == "4/102");
        CHECK(j.at("firstEdition") == true);
        CHECK(j.at("signed") == false);
        CHECK(j.at("region") == "West");

        const PokemonCard back = j.get<PokemonCard>();
        CHECK(back == c);
    }

    TEST_CASE("region Asia round-trips and missing region defaults to West") {
        PokemonCard c;
        c.id = 1;
        c.amount = 1;
        c.name = "Charmander";
        c.set = Set{"PMCG1", "Expansion Pack", "1996/10/20"};
        c.setNo = "001";
        c.language = Language::Japanese;
        c.condition = Condition::NearMint;
        c.region = PokemonRegion::Asia;

        nlohmann::json j = c;
        CHECK(j.at("region") == "Asia");
        CHECK(j.get<PokemonCard>().region == PokemonRegion::Asia);

        j.erase("region");
        const PokemonCard legacy = j.get<PokemonCard>();
        CHECK(legacy.region == PokemonRegion::West);
    }

    TEST_CASE("West load migrates legacy pokemontcg set ids to TCGdex EN") {
        nlohmann::json j = {
            {"id", 1},
            {"amount", 1},
            {"name", "Charizard"},
            {"set", {{"id", "sv1"}, {"name", "Scarlet & Violet"}, {"releaseDate", "2023/03/31"}}},
            {"setNo", "6"},
            {"note", ""},
            {"images", nlohmann::json::array()},
            {"language", "English"},
            {"condition", "NearMint"},
            {"firstEdition", false},
            {"holo", false},
            {"signed", false},
            {"altered", false},
            {"region", "West"},
        };
        const PokemonCard back = j.get<PokemonCard>();
        CHECK(back.set.id == "sv01");
    }

    TEST_CASE("Asia load does not rewrite set ids through West aliases") {
        nlohmann::json j = {
            {"id", 1},
            {"amount", 1},
            {"name", "Charmander"},
            {"set", {{"id", "sv1"}, {"name", "Keep Asia id"}, {"releaseDate", "2023/01/01"}}},
            {"setNo", "001"},
            {"note", ""},
            {"images", nlohmann::json::array()},
            {"language", "Japanese"},
            {"condition", "NearMint"},
            {"firstEdition", false},
            {"holo", false},
            {"signed", false},
            {"altered", false},
            {"region", "Asia"},
        };
        const PokemonCard back = j.get<PokemonCard>();
        CHECK(back.set.id == "sv1");
    }
}

TEST_SUITE("DigiBattle99Card JSON") {
    TEST_CASE("uses 'setNo' and 'firstEdition' aliases") {
        DigiBattle99Card c;
        c.id = 3;
        c.amount = 2;
        c.name = "Agumon";
        c.set = Set{"series-1-starter-set", "Series 1 Starter Set", "1999/06/01"};
        c.setNo = "ST-01";
        c.note = "starter";
        c.images = {"a.png"};
        c.language = Language::English;
        c.condition = Condition::NearMint;
        c.firstEdition = false;
        c.holo = true;
        c.signed_ = true;
        c.altered = false;

        nlohmann::json j = c;
        CHECK(j.at("setNo") == "ST-01");
        CHECK(j.at("firstEdition") == false);
        CHECK(j.at("holo") == true);
        CHECK(j.at("signed") == true);

        const DigiBattle99Card back = j.get<DigiBattle99Card>();
        CHECK(back == c);
    }
}

TEST_SUITE("DigiBattle99SetCatalog JSON") {
    TEST_CASE("round-trips packs and setNo alias") {
        DigiBattle99SetCatalog catalog;
        DigiBattle99SetCatalogPack pack;
        pack.setId = "series-1-starter-set";
        pack.setName = "Series 1 Starter Set";
        pack.cards.push_back(DigiBattle99CatalogCard{"ST-01", "Agumon"});
        pack.cards.push_back(DigiBattle99CatalogCard{"ST-126", "Agumon"});
        catalog.packs.push_back(std::move(pack));

        nlohmann::json j = catalog;
        CHECK(j.at("packs").is_array());
        CHECK(j.at("packs").at(0).at("id") == "series-1-starter-set");
        CHECK(j.at("packs").at(0).at("cards").at(0).at("setNo") == "ST-01");

        const DigiBattle99SetCatalog back = j.get<DigiBattle99SetCatalog>();
        CHECK(back == catalog);
        CHECK(back.findPack("series-1-starter-set") != nullptr);
        CHECK(back.findPack("missing") == nullptr);
    }
}

TEST_SUITE("YuGiOhBandaiCard JSON") {
    TEST_CASE("round-trips with setNo, rarity, holo and signed alias") {
        YuGiOhBandaiCard c;
        c.id = 14;
        c.amount = 2;
        c.name = "Dark Magician";
        c.set = Set{"ban1", "1st Generation", "1998/09/01"};
        c.setNo = "14";
        c.rarity = "Rare";
        c.note = "classic";
        c.images = {"a.png"};
        c.language = Language::Japanese;
        c.condition = Condition::NearMint;
        c.holo = true;
        c.signed_ = true;
        c.altered = false;

        nlohmann::json j = c;
        CHECK(j.at("setNo") == "14");
        CHECK(j.at("rarity") == "Rare");
        CHECK(j.at("holo") == true);
        CHECK(j.at("signed") == true);
        CHECK_FALSE(j.contains("firstEdition"));

        const YuGiOhBandaiCard back = j.get<YuGiOhBandaiCard>();
        CHECK(back == c);
    }

    TEST_CASE("missing each required key throws") {
        const nlohmann::json full = {
            {"id", 14},
            {"amount", 1},
            {"name", "Dark Magician"},
            {"set", nlohmann::json{
                {"id", "ban1"},
                {"name", "1st Generation"},
                {"releaseDate", "1998/09/01"},
            }},
            {"setNo", "14"},
            {"rarity", "Rare"},
            {"note", ""},
            {"images", nlohmann::json::array()},
            {"language", "Japanese"},
            {"condition", "NearMint"},
            {"holo", false},
            {"signed", false},
            {"altered", false},
        };

        for (const char* key : {
                 "id", "amount", "name", "set", "setNo", "rarity", "note", "images",
                 "language", "condition", "holo", "signed", "altered"}) {
            nlohmann::json partial = full;
            partial.erase(key);
            CHECK_THROWS(partial.get<YuGiOhBandaiCard>());
        }
    }
}

TEST_SUITE("YuGiOhBandaiSetCatalog JSON") {
    TEST_CASE("round-trips packs with rarity") {
        YuGiOhBandaiSetCatalog catalog;
        YuGiOhBandaiSetCatalogPack pack;
        pack.setId = "ban1";
        pack.setName = "1st Generation";
        pack.cards.push_back(YuGiOhBandaiCatalogCard{"14", "Dark Magician", "Rare"});
        pack.cards.push_back(YuGiOhBandaiCatalogCard{"9", "Blue-Eyes White Dragon", "Super Rare"});
        catalog.packs.push_back(std::move(pack));

        nlohmann::json j = catalog;
        CHECK(j.at("packs").at(0).at("id") == "ban1");
        CHECK(j.at("packs").at(0).at("cards").at(0).at("setNo") == "14");
        CHECK(j.at("packs").at(0).at("cards").at(0).at("rarity") == "Rare");

        const YuGiOhBandaiSetCatalog back = j.get<YuGiOhBandaiSetCatalog>();
        CHECK(back == catalog);
        CHECK(back.findPack("ban1") != nullptr);
        CHECK(back.findPack("missing") == nullptr);
    }
}

TEST_SUITE("YuGiOhSetCatalog JSON") {
    TEST_CASE("round-trips packs and setNo alias") {
        YuGiOhSetCatalog catalog;
        YuGiOhSetCatalogPack pack;
        pack.setId = "LOB";
        pack.setName = "Legend of Blue Eyes White Dragon";
        pack.cards.push_back(YuGiOhCatalogCard{"LOB-001", "Blue-Eyes White Dragon", "Ultra Rare"});
        pack.cards.push_back(YuGiOhCatalogCard{"LOB-EN005", "Dark Magician", "Ultra Rare"});
        catalog.packs.push_back(std::move(pack));

        nlohmann::json j = catalog;
        CHECK(j.at("packs").is_array());
        CHECK(j.at("packs").at(0).at("id") == "LOB");
        CHECK(j.at("packs").at(0).at("cards").at(0).at("setNo") == "LOB-001");
        CHECK(j.at("packs").at(0).at("cards").at(0).at("rarity") == "Ultra Rare");

        const YuGiOhSetCatalog back = j.get<YuGiOhSetCatalog>();
        CHECK(back == catalog);
        CHECK(back.findPack("LOB") != nullptr);
        CHECK(back.findPack("missing") == nullptr);
    }

    TEST_CASE("legacy catalog JSON without rarity still loads") {
        const auto j = nlohmann::json::parse(R"({
          "packs":[{"id":"LOB","name":"Legend of Blue Eyes White Dragon",
            "cards":[{"setNo":"LOB-001","name":"Blue-Eyes White Dragon"}]}]
        })");
        const YuGiOhSetCatalog back = j.get<YuGiOhSetCatalog>();
        REQUIRE(back.packs.size() == 1);
        REQUIRE(back.packs[0].cards.size() == 1);
        CHECK(back.packs[0].cards[0].rarity.empty());
    }
}

TEST_SUITE("PokemonSetCatalog JSON") {
    TEST_CASE("round-trips packs and setNo alias") {
        PokemonSetCatalog catalog;
        PokemonSetCatalogPack pack;
        pack.setId = "base1";
        pack.setName = "Base";
        pack.cards.push_back(PokemonCatalogCard{"4", "Charizard"});
        pack.cards.push_back(PokemonCatalogCard{"58", "Growlithe"});
        catalog.packs.push_back(std::move(pack));

        nlohmann::json j = catalog;
        CHECK(j.at("packs").is_array());
        CHECK(j.at("packs").at(0).at("id") == "base1");
        CHECK(j.at("packs").at(0).at("cards").at(0).at("setNo") == "4");

        const PokemonSetCatalog back = j.get<PokemonSetCatalog>();
        CHECK(back == catalog);
        CHECK(back.findPack("base1") != nullptr);
        CHECK(back.findPack("missing") == nullptr);
    }
}

TEST_SUITE("JapanesePokemonCard JSON") {
    TEST_CASE("uses 'setNo' and 'firstEdition' aliases") {
        JapanesePokemonCard c;
        c.id = 9;
        c.amount = 1;
        c.name = "Charmander";
        c.set = Set{"PMCG1", "Expansion Pack", "1996/10/20"};
        c.setNo = "001";
        c.note = "";
        c.images = {};
        c.language = Language::Japanese;
        c.condition = Condition::NearMint;
        c.firstEdition = true;
        c.holo = false;
        c.signed_ = false;
        c.altered = false;

        nlohmann::json j = c;
        CHECK(j.at("setNo") == "001");
        CHECK(j.at("firstEdition") == true);
        CHECK(j.at("signed") == false);
        CHECK(j.at("language") == "Japanese");

        const JapanesePokemonCard back = j.get<JapanesePokemonCard>();
        CHECK(back == c);
    }
}

TEST_SUITE("Configuration JSON matches Rust serde aliases") {
    TEST_CASE("dataStorage / defaultGame / theme keys are present") {
        Configuration cfg;
        cfg.dataStorage = "/some/path";
        cfg.defaultGame = Game::Pokemon;
        cfg.theme = Theme::Dark;

        nlohmann::json j = cfg;
        CHECK(j.at("dataStorage") == "/some/path");
        CHECK(j.at("defaultGame") == "Pokemon");
        CHECK(j.at("theme") == "Dark");

        const auto back = j.get<Configuration>();
        CHECK(back == cfg);
    }

    TEST_CASE("legacy defaultGame JapanesePokemon coerces to Pokemon") {
        nlohmann::json j = {
            {"dataStorage", "/data"},
            {"defaultGame", "JapanesePokemon"},
            {"theme", "Light"},
        };
        const auto cfg = j.get<Configuration>();
        CHECK(cfg.defaultGame == Game::Pokemon);
    }

    TEST_CASE("missing theme key defaults to Light") {
        const nlohmann::json j = {
            {"dataStorage", "/portable/data"},
            {"defaultGame", "YuGiOh"},
        };

        const auto cfg = j.get<Configuration>();
        CHECK(cfg.dataStorage == "/portable/data");
        CHECK(cfg.defaultGame == Game::YuGiOh);
        CHECK(cfg.theme == Theme::Light);
    }
}

TEST_SUITE("YuGiOhCard JSON") {
    TEST_CASE("round-trips with setNo and rarity fields") {
        YuGiOhCard c;
        c.id = 77;
        c.amount = 2;
        c.name = "Blue-Eyes White Dragon";
        c.set = Set{"SDK-001", "Starter Deck Kaiba", "2002/03/29"};
        c.setNo = "SDK-001";
        c.rarity = "Ultra Rare";
        c.note = "classic";
        c.images = {"77+starter+blue-eyes+0.png"};
        c.language = Language::English;
        c.condition = Condition::NearMint;
        c.firstEdition = true;
        c.signed_ = false;
        c.altered = false;

        nlohmann::json j = c;
        CHECK(j.at("setNo") == "SDK-001");
        CHECK(j.at("rarity") == "Ultra Rare");
        CHECK_FALSE(j.contains("rarityCode"));
        CHECK(j.at("signed") == false);

        const YuGiOhCard back = j.get<YuGiOhCard>();
        CHECK(back == c);
    }

    TEST_CASE("legacy JSON without rarityCode key parses (domain uses rarity only)") {
        const nlohmann::json j = {
            {"id", 1},
            {"amount", 1},
            {"name", "Dark Magician"},
            {"set", nlohmann::json{
                {"id", "SDY"},
                {"name", "Starter Deck: Yugi"},
                {"releaseDate", "2002/03/29"},
            }},
            {"setNo", "SDY-006"},
            {"rarity", "Ultra Rare"},
            {"note", ""},
            {"images", nlohmann::json::array()},
            {"language", "English"},
            {"condition", "NearMint"},
            {"firstEdition", false},
            {"signed", false},
            {"altered", false},
        };

        const auto card = j.get<YuGiOhCard>();
        CHECK(card.rarity == "Ultra Rare");
        CHECK(card.setNo == "SDY-006");
        CHECK(card.set.id == "SDY");
    }

    TEST_CASE("serializes non-default flags and metadata fields") {
        YuGiOhCard c;
        c.id = 3;
        c.amount = 4;
        c.name = "Red-Eyes Black Dragon";
        c.set = Set{"lob", "Legend of Blue Eyes", "2002/03/08"};
        c.setNo = "LOB-070";
        c.rarity = "Secret Rare";
        c.note = "graded";
        c.images = {};
        c.language = Language::German;
        c.condition = Condition::Played;
        c.firstEdition = false;
        c.signed_ = true;
        c.altered = true;

        const nlohmann::json j = c;
        CHECK(j.at("firstEdition") == false);
        CHECK(j.at("signed") == true);
        CHECK(j.at("altered") == true);
        CHECK(j.at("language") == "German");
        CHECK(j.at("condition") == "Played");
        CHECK(j.at("images") == nlohmann::json::array());

        const YuGiOhCard back = j.get<YuGiOhCard>();
        CHECK(back == c);
    }

    TEST_CASE("operator== distinguishes each field") {
        YuGiOhCard base;
        base.id = 10;
        base.amount = 2;
        base.name = "Dark Magician";
        base.set = Set{"lob", "Legend of Blue Eyes", "2002/03/08"};
        base.setNo = "LOB-005";
        base.rarity = "Ultra Rare";
        base.note = "note";
        base.images = {"a.png"};
        base.language = Language::English;
        base.condition = Condition::NearMint;
        base.firstEdition = true;
        base.signed_ = false;
        base.altered = false;

        auto changed = base;
        changed.id = 11;
        CHECK_FALSE(changed == base);

        changed = base;
        changed.amount = 3;
        CHECK_FALSE(changed == base);

        changed = base;
        changed.name = "Other";
        CHECK_FALSE(changed == base);

        changed = base;
        changed.set.name = "Other Set";
        CHECK_FALSE(changed == base);

        changed = base;
        changed.setNo = "LOB-006";
        CHECK_FALSE(changed == base);

        changed = base;
        changed.rarity = "Rare";
        CHECK_FALSE(changed == base);

        changed = base;
        changed.note = "other";
        CHECK_FALSE(changed == base);

        changed = base;
        changed.images = {};
        CHECK_FALSE(changed == base);

        changed = base;
        changed.language = Language::Japanese;
        CHECK_FALSE(changed == base);

        changed = base;
        changed.condition = Condition::Played;
        CHECK_FALSE(changed == base);

        changed = base;
        changed.firstEdition = false;
        CHECK_FALSE(changed == base);

        changed = base;
        changed.signed_ = true;
        CHECK_FALSE(changed == base);

        changed = base;
        changed.altered = true;
        CHECK_FALSE(changed == base);
    }
}

TEST_SUITE("Domain JSON required fields") {
    TEST_CASE("Set missing required key throws") {
        const nlohmann::json j = {
            {"id", "lea"},
            {"name", "Limited Edition Alpha"},
        };
        CHECK_THROWS(j.get<Set>());
    }

    TEST_CASE("MagicCard missing required key throws") {
        const nlohmann::json j = {
            {"id", 10},
            {"amount", 1},
            {"name", "Lightning Bolt"},
            {"set", nlohmann::json{
                {"id", "lea"},
                {"name", "Limited Edition Alpha"},
                {"releaseDate", "1993/08/05"},
            }},
            // note missing on purpose
            {"images", nlohmann::json::array()},
            {"language", "English"},
            {"condition", "NearMint"},
            {"foil", false},
            {"signed", false},
            {"altered", false},
        };
        CHECK_THROWS(j.get<MagicCard>());
    }

    TEST_CASE("PokemonCard missing required key throws") {
        const nlohmann::json j = {
            {"id", 7},
            {"amount", 1},
            {"name", "Charizard"},
            {"set", nlohmann::json{
                {"id", "base1"},
                {"name", "Base Set"},
                {"releaseDate", "1999/01/09"},
            }},
            {"setNo", "4/102"},
            {"note", ""},
            {"images", nlohmann::json::array()},
            {"language", "English"},
            {"condition", "Excellent"},
            {"firstEdition", true},
            // holo missing on purpose
            {"signed", false},
            {"altered", false},
        };
        CHECK_THROWS(j.get<PokemonCard>());
    }

    TEST_CASE("YuGiOhCard missing required key throws") {
        const nlohmann::json j = {
            {"id", 7},
            {"amount", 1},
            {"name", "Blue-Eyes White Dragon"},
            {"set", nlohmann::json{
                {"id", "sdk"},
                {"name", "Starter Deck Kaiba"},
                {"releaseDate", "2002/03/29"},
            }},
            {"setNo", "SDK-001"},
            {"note", ""},
            {"images", nlohmann::json::array()},
            {"language", "English"},
            {"condition", "NearMint"},
            {"firstEdition", true},
            // rarity missing on purpose
            {"signed", false},
            {"altered", false},
        };
        CHECK_THROWS(j.get<YuGiOhCard>());
    }

    TEST_CASE("YuGiOhCard missing each required key throws") {
        const nlohmann::json full = {
            {"id", 7},
            {"amount", 1},
            {"name", "Blue-Eyes White Dragon"},
            {"set", nlohmann::json{
                {"id", "sdk"},
                {"name", "Starter Deck Kaiba"},
                {"releaseDate", "2002/03/29"},
            }},
            {"setNo", "SDK-001"},
            {"rarity", "Ultra Rare"},
            {"note", ""},
            {"images", nlohmann::json::array()},
            {"language", "English"},
            {"condition", "NearMint"},
            {"firstEdition", true},
            {"signed", false},
            {"altered", false},
        };

        for (const char* key : {
                 "id", "amount", "name", "set", "setNo", "note", "images",
                 "language", "condition", "firstEdition", "rarity", "signed", "altered"}) {
            nlohmann::json partial = full;
            partial.erase(key);
            CHECK_THROWS(partial.get<YuGiOhCard>());
        }
    }

    TEST_CASE("MagicCard missing each required key throws") {
        const nlohmann::json full = {
            {"id", 10},
            {"amount", 1},
            {"name", "Lightning Bolt"},
            {"set", nlohmann::json{
                {"id", "lea"},
                {"name", "Limited Edition Alpha"},
                {"releaseDate", "1993/08/05"},
            }},
            {"note", ""},
            {"images", nlohmann::json::array()},
            {"language", "English"},
            {"condition", "NearMint"},
            {"foil", false},
            {"signed", false},
            {"altered", false},
        };

        for (const char* key : {"id", "amount", "name", "set", "note", "images", "language",
                               "condition", "foil", "signed", "altered"}) {
            nlohmann::json partial = full;
            partial.erase(key);
            CHECK_THROWS(partial.get<MagicCard>());
        }
    }

    TEST_CASE("PokemonCard missing each required key throws") {
        const nlohmann::json full = {
            {"id", 7},
            {"amount", 1},
            {"name", "Charizard"},
            {"set", nlohmann::json{
                {"id", "base1"},
                {"name", "Base Set"},
                {"releaseDate", "1999/01/09"},
            }},
            {"setNo", "4/102"},
            {"note", ""},
            {"images", nlohmann::json::array()},
            {"language", "English"},
            {"condition", "Excellent"},
            {"firstEdition", true},
            {"holo", true},
            {"signed", false},
            {"altered", false},
        };

        for (const char* key :
             {"id", "amount", "name", "set", "setNo", "note", "images", "language", "condition",
              "firstEdition", "holo", "signed", "altered"}) {
            nlohmann::json partial = full;
            partial.erase(key);
            CHECK_THROWS(partial.get<PokemonCard>());
        }
    }

    TEST_CASE("DigiBattle99Card missing each required key throws") {
        const nlohmann::json full = {
            {"id", 3},
            {"amount", 1},
            {"name", "Agumon"},
            {"set", nlohmann::json{
                {"id", "series-1-starter-set"},
                {"name", "Series 1 Starter Set"},
                {"releaseDate", "1999/06/01"},
            }},
            {"setNo", "ST-01"},
            {"note", ""},
            {"images", nlohmann::json::array()},
            {"language", "English"},
            {"condition", "NearMint"},
            {"firstEdition", false},
            {"holo", true},
            {"signed", false},
            {"altered", false},
        };

        for (const char* key :
             {"id", "amount", "name", "set", "setNo", "note", "images", "language", "condition",
              "firstEdition", "holo", "signed", "altered"}) {
            nlohmann::json partial = full;
            partial.erase(key);
            CHECK_THROWS(partial.get<DigiBattle99Card>());
        }
    }

    TEST_CASE("JapanesePokemonCard missing each required key throws") {
        const nlohmann::json full = {
            {"id", 9},
            {"amount", 1},
            {"name", "Charmander"},
            {"set", nlohmann::json{
                {"id", "PMCG1"},
                {"name", "Expansion Pack"},
                {"releaseDate", "1996/10/20"},
            }},
            {"setNo", "001"},
            {"note", ""},
            {"images", nlohmann::json::array()},
            {"language", "Japanese"},
            {"condition", "NearMint"},
            {"firstEdition", true},
            {"holo", false},
            {"signed", false},
            {"altered", false},
        };

        for (const char* key :
             {"id", "amount", "name", "set", "setNo", "note", "images", "language", "condition",
              "firstEdition", "holo", "signed", "altered"}) {
            nlohmann::json partial = full;
            partial.erase(key);
            CHECK_THROWS(partial.get<JapanesePokemonCard>());
        }
    }

    TEST_CASE("Configuration missing required key throws") {
        const nlohmann::json j = {
            {"defaultGame", "Magic"},
            {"theme", "Dark"},
        };
        CHECK_THROWS(j.get<Configuration>());
    }

    TEST_CASE("Configuration invalid theme value throws when present") {
        const nlohmann::json j = {
            {"dataStorage", "/portable/data"},
            {"defaultGame", "Magic"},
            {"theme", "Neon"},
        };
        CHECK_THROWS(j.get<Configuration>());
    }
}
