""" Tests for claude_client.py — JSON parsing, payload building, slot parsing, UNOWNED marker stripping, and CardEntry construction. """ import pytest from app.services.ai.claude_client import ( _parse_json, _parse_slot, _build_payload, CardEntry, CutEntry, DeckPayload, UNOWNED_RE, ) from app.models.deck import CardSlot # --------------------------------------------------------------------------- # _parse_json # --------------------------------------------------------------------------- class TestParseJson: def test_valid_json(self): text = '{"deck_name": "Test", "cards": []}' result = _parse_json(text) assert result["deck_name"] == "Test" def test_strips_markdown_fences(self): text = '''```json {"deck_name": "Test", "cards": []} ```''' result = _parse_json(text) assert result["deck_name"] == "Test" def test_strips_plain_fences(self): text = '''``` {"deck_name": "Test", "cards": []} ```''' result = _parse_json(text) assert result["deck_name"] == "Test" def test_raises_on_invalid_json(self): with pytest.raises(ValueError, match="not valid JSON"): _parse_json('{"broken: json}') def test_raises_on_empty_string(self): with pytest.raises(ValueError): _parse_json("") # --------------------------------------------------------------------------- # _parse_slot # --------------------------------------------------------------------------- class TestParseSlot: def test_valid_slots(self): assert _parse_slot("creature") == CardSlot.CREATURE assert _parse_slot("instant") == CardSlot.INSTANT assert _parse_slot("sorcery") == CardSlot.SORCERY assert _parse_slot("enchantment") == CardSlot.ENCHANTMENT assert _parse_slot("artifact") == CardSlot.ARTIFACT assert _parse_slot("planeswalker") == CardSlot.PLANESWALKER assert _parse_slot("land") == CardSlot.LAND assert _parse_slot("battle") == CardSlot.BATTLE def test_plural_aliases(self): assert _parse_slot("creatures") == CardSlot.CREATURE assert _parse_slot("instants") == CardSlot.INSTANT assert _parse_slot("sorceries") == CardSlot.SORCERY assert _parse_slot("lands") == CardSlot.LAND def test_case_insensitive(self): assert _parse_slot("CREATURE") == CardSlot.CREATURE assert _parse_slot("Land") == CardSlot.LAND def test_unknown_defaults_to_creature(self): assert _parse_slot("unknown_type") == CardSlot.CREATURE # --------------------------------------------------------------------------- # CardEntry # --------------------------------------------------------------------------- class TestCardEntry: def test_basic_card(self): entry = CardEntry({"name": "Sol Ring", "slot": "artifact", "quantity": 1}) assert entry.card_name == "Sol Ring" assert entry.slot == CardSlot.ARTIFACT assert entry.quantity == 1 assert entry.is_owned is True def test_unowned_marker_stripped(self): entry = CardEntry({"name": "Sol Ring [UNOWNED]", "slot": "artifact", "quantity": 1}) assert entry.card_name == "Sol Ring" assert entry.is_owned is False def test_unowned_marker_case_insensitive(self): entry = CardEntry({"name": "Sol Ring [unowned]", "slot": "artifact", "quantity": 1}) assert entry.card_name == "Sol Ring" assert entry.is_owned is False def test_quantity_defaults_to_1(self): entry = CardEntry({"name": "Forest", "slot": "land"}) assert entry.quantity == 1 def test_quantity_minimum_1(self): entry = CardEntry({"name": "Forest", "slot": "land", "quantity": 0}) assert entry.quantity == 1 def test_reasoning_stored(self): entry = CardEntry({"name": "Sol Ring", "slot": "artifact", "reasoning": "Best ramp"}) assert entry.reasoning == "Best ramp" # --------------------------------------------------------------------------- # _build_payload # --------------------------------------------------------------------------- class TestBuildPayload: def _card(self, name="Sol Ring", slot="artifact"): return {"name": name, "slot": slot, "quantity": 1, "reasoning": "Good card"} def test_standard_cards_key(self): data = { "deck_name": "Test Deck", "strategy_summary": "A test deck", "cards": [self._card()] } payload = _build_payload(data) assert payload.deck_name == "Test Deck" assert payload.strategy_summary == "A test deck" assert len(payload.cards) == 1 assert payload.cards[0].card_name == "Sol Ring" def test_alternative_decklist_key(self): data = { "deck_name": "Test Deck", "decklist": [self._card()] } payload = _build_payload(data) assert len(payload.cards) == 1 def test_alternative_deck_key(self): data = { "deck_name": "Test Deck", "deck": [self._card()] } payload = _build_payload(data) assert len(payload.cards) == 1 def test_cards_key_takes_priority_over_deck(self): data = { "cards": [self._card("Sol Ring")], "deck": [self._card("Black Lotus")], } payload = _build_payload(data) assert payload.cards[0].card_name == "Sol Ring" def test_default_deck_name(self): payload = _build_payload({}) assert payload.deck_name == "Untitled Deck" def test_skips_cards_without_name(self): data = {"cards": [{"slot": "artifact"}, self._card()]} payload = _build_payload(data) assert len(payload.cards) == 1 def test_cuts_parsed(self): data = { "cards": [], "cuts": [{"name": "Swamp", "reasoning": "Too slow"}] } payload = _build_payload(data) assert len(payload.cuts) == 1 assert payload.cuts[0].card_name == "Swamp" def test_empty_data(self): payload = _build_payload({}) assert payload.deck_name == "Untitled Deck" assert payload.strategy_summary == "" assert payload.cards == [] assert payload.cuts == []