""" Tests for the /compare slash command embed builder (cogs/compare.py). What: - build_compare_embed() is a pure function that takes two card-data dicts and returns a discord.Embed. - Tests verify field count, arrow directions, type-mismatch raises, and tied stats. Why: - The embed builder has no Discord I/O so it can be tested synchronously without a bot or API calls. - Correct arrow direction is critical for usability: wrong arrows would mislead players making trade/lineup decisions. """ import sys import os import pytest import discord sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) from cogs.compare import ( build_compare_embed, CompareMismatchError, _delta_arrow, _is_pitcher, ) # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- def _make_player( name: str = "Test Player", pos_1: str = "CF", rarity_value: int = 3, rarity_name: str = "All-Star", cost: int = 300, headshot: str = None, ) -> dict: """Build a minimal player dict that mirrors the API response shape.""" return { "player_id": 1, "p_name": name, "pos_1": pos_1, "cost": cost, "rarity": { "value": rarity_value, "name": rarity_name, "color": "FFD700", }, "headshot": headshot, } def _make_batting_card( running: int = 12, steal_low: int = 7, steal_high: int = 12, bunting: str = "C", hit_and_run: str = "B", ) -> dict: """Build a minimal batting card dict.""" return { "running": running, "steal_low": steal_low, "steal_high": steal_high, "steal_auto": False, "steal_jump": 0.2, "bunting": bunting, "hit_and_run": hit_and_run, "hand": "R", "offense_col": 1, } def _make_pitching_card( starter_rating: int = 7, relief_rating: int = 4, closer_rating: int = None, balk: int = 2, wild_pitch: int = 3, ) -> dict: """Build a minimal pitching card dict.""" return { "starter_rating": starter_rating, "relief_rating": relief_rating, "closer_rating": closer_rating, "balk": balk, "wild_pitch": wild_pitch, "hand": "R", } def _batter_card_data(player: dict, batting_card: dict) -> dict: return { "player": player, "battingcard": batting_card, "pitchingcard": None, } def _pitcher_card_data(player: dict, pitching_card: dict) -> dict: return { "player": player, "battingcard": None, "pitchingcard": pitching_card, } # --------------------------------------------------------------------------- # _delta_arrow unit tests # --------------------------------------------------------------------------- class TestDeltaArrow: """_delta_arrow correctly indicates which side wins.""" def test_higher_wins_when_card2_greater(self): """▲ when card2 value is higher and higher_is_better=True.""" assert _delta_arrow(10, 15) == "▲" def test_higher_wins_when_card1_greater(self): """▼ when card1 value is higher and higher_is_better=True.""" assert _delta_arrow(20, 10) == "▼" def test_tied_returns_equals(self): """═ when both values are equal.""" assert _delta_arrow(10, 10) == "═" def test_lower_is_better_arrow_flipped(self): """▲ when card2 is lower and lower_is_better (e.g. balk count).""" assert _delta_arrow(5, 2, higher_is_better=False) == "▲" def test_lower_is_better_card1_wins(self): """▼ when card1 is lower and lower_is_better.""" assert _delta_arrow(2, 5, higher_is_better=False) == "▼" def test_grade_field_a_beats_b(self): """▲ when card2 has grade A and card1 has grade B (grade_field=True).""" assert _delta_arrow("B", "A", grade_field=True) == "▲" def test_grade_field_b_beats_c(self): """▼ when card1 has grade B and card2 has grade C.""" assert _delta_arrow("B", "C", grade_field=True) == "▼" def test_grade_field_tie(self): """═ when both cards have the same grade.""" assert _delta_arrow("C", "C", grade_field=True) == "═" def test_none_returns_equals(self): """═ when either value is None (missing stat).""" assert _delta_arrow(None, 10) == "═" assert _delta_arrow(10, None) == "═" # --------------------------------------------------------------------------- # _is_pitcher # --------------------------------------------------------------------------- class TestIsPitcher: """_is_pitcher correctly classifies positions.""" def test_sp_is_pitcher(self): assert _is_pitcher({"pos_1": "SP"}) is True def test_rp_is_pitcher(self): assert _is_pitcher({"pos_1": "RP"}) is True def test_cf_is_batter(self): assert _is_pitcher({"pos_1": "CF"}) is False def test_dh_is_batter(self): assert _is_pitcher({"pos_1": "DH"}) is False def test_lowercase_sp(self): """pos_1 comparison is case-insensitive.""" assert _is_pitcher({"pos_1": "sp"}) is True # --------------------------------------------------------------------------- # build_compare_embed — batter path # --------------------------------------------------------------------------- class TestBuildCompareEmbedBatters: """build_compare_embed works correctly for two batter cards.""" def setup_method(self): """Create two distinct batter cards for comparison.""" self.player1 = _make_player("Mike Trout", pos_1="CF", cost=500, rarity_value=5) self.player2 = _make_player("Joe Batter", pos_1="LF", cost=200, rarity_value=2) self.bc1 = _make_batting_card( running=15, steal_low=9, steal_high=14, bunting="B", hit_and_run="A" ) self.bc2 = _make_batting_card( running=10, steal_low=5, steal_high=10, bunting="C", hit_and_run="C" ) self.card1 = _batter_card_data(self.player1, self.bc1) self.card2 = _batter_card_data(self.player2, self.bc2) def test_embed_is_discord_embed(self): """Return value is a discord.Embed instance.""" embed = build_compare_embed(self.card1, self.card2, "Mike Trout", "Joe Batter") assert isinstance(embed, discord.Embed) def test_embed_title_contains_comparison(self): """Embed title identifies this as a card comparison.""" embed = build_compare_embed(self.card1, self.card2, "Mike Trout", "Joe Batter") assert "Comparison" in embed.title def test_embed_field_count(self): """ Batter embed has header row (3 fields) + 7 stat rows × 3 fields = 24 total inline fields. """ embed = build_compare_embed(self.card1, self.card2, "Mike Trout", "Joe Batter") assert len(embed.fields) == 3 + 7 * 3 # 24 def test_higher_cost_gets_down_arrow_in_center_column(self): """ card1 has higher cost (500 vs 200). The center arrow field for 'Cost (Overall)' should be '▼' (card1 wins when higher_is_better). """ embed = build_compare_embed(self.card1, self.card2, "Mike Trout", "Joe Batter") # First stat row starts at field index 3; center field of each row is idx+1 cost_arrow_field = embed.fields[4] # index 3=left, 4=center, 5=right assert cost_arrow_field.value == "▼" def test_higher_running_gets_down_arrow(self): """ card1 running=15 > card2 running=10 → center arrow for Running is ▼. Running is the 3rd stat (header row at 0..2, cost at 3..5, rarity at 6..8, running at 9..11 → center = index 10). """ embed = build_compare_embed(self.card1, self.card2, "Mike Trout", "Joe Batter") running_arrow = embed.fields[10] assert running_arrow.value == "▼" def test_better_grade_card1_bunting_b_beats_c(self): """ card1 bunting='B' beats card2 bunting='C'. Bunting is the 6th stat, center field at index 3 + 5*3 + 1 = 19. Arrow should be ▼ (card1 wins). """ embed = build_compare_embed(self.card1, self.card2, "Mike Trout", "Joe Batter") bunt_arrow = embed.fields[19] assert bunt_arrow.value == "▼" def test_type_mismatch_raises(self): """CompareMismatchError raised when one card is batter and other pitcher.""" pitcher_player = _make_player("Max Scherzer", pos_1="SP", cost=400) pitcher_card = _make_pitching_card(starter_rating=9) card_p = _pitcher_card_data(pitcher_player, pitcher_card) with pytest.raises(CompareMismatchError, match="Card types differ"): build_compare_embed(self.card1, card_p, "Mike Trout", "Max Scherzer") # --------------------------------------------------------------------------- # build_compare_embed — pitcher path # --------------------------------------------------------------------------- class TestBuildCompareEmbedPitchers: """build_compare_embed works correctly for two pitcher cards.""" def setup_method(self): self.player1 = _make_player( "Max Scherzer", pos_1="SP", cost=450, rarity_value=4 ) self.player2 = _make_player("Bullpen Bob", pos_1="RP", cost=150, rarity_value=2) self.pc1 = _make_pitching_card( starter_rating=9, relief_rating=5, closer_rating=None, balk=1, wild_pitch=2 ) self.pc2 = _make_pitching_card( starter_rating=3, relief_rating=8, closer_rating=6, balk=3, wild_pitch=5 ) self.card1 = _pitcher_card_data(self.player1, self.pc1) self.card2 = _pitcher_card_data(self.player2, self.pc2) def test_embed_field_count_pitchers(self): """ Pitcher embed has header row (3 fields) + 7 stat rows × 3 fields = 24. """ embed = build_compare_embed( self.card1, self.card2, "Max Scherzer", "Bullpen Bob" ) assert len(embed.fields) == 3 + 7 * 3 # 24 def test_description_labels_pitchers(self): """Embed description identifies card type as Pitchers.""" embed = build_compare_embed( self.card1, self.card2, "Max Scherzer", "Bullpen Bob" ) assert "Pitchers" in embed.description def test_starter_rating_card1_wins(self): """ card1 starter_rating=9 > card2 starter_rating=3 → arrow ▼ (card1 wins). Starter Rating is 3rd stat, center field at index 3 + 2*3 + 1 = 10. """ embed = build_compare_embed( self.card1, self.card2, "Max Scherzer", "Bullpen Bob" ) starter_arrow = embed.fields[10] assert starter_arrow.value == "▼" def test_relief_rating_card2_wins(self): """ card2 relief_rating=8 > card1 relief_rating=5 → arrow ▲ (card2 wins). Relief Rating is 4th stat, center field at index 3 + 3*3 + 1 = 13. """ embed = build_compare_embed( self.card1, self.card2, "Max Scherzer", "Bullpen Bob" ) relief_arrow = embed.fields[13] assert relief_arrow.value == "▲" def test_lower_balk_is_better(self): """ card1 balk=1 < card2 balk=3 → lower is better → arrow ▼ (card1 wins). Balk is 6th stat, center field at index 3 + 5*3 + 1 = 19. """ embed = build_compare_embed( self.card1, self.card2, "Max Scherzer", "Bullpen Bob" ) balk_arrow = embed.fields[19] assert balk_arrow.value == "▼" def test_tied_stat_shows_equals(self): """═ when both pitchers have the same starter_rating.""" pc_tied = _make_pitching_card(starter_rating=9) card_tied = _pitcher_card_data(self.player2, pc_tied) embed = build_compare_embed( self.card1, card_tied, "Max Scherzer", "Bullpen Bob" ) starter_arrow = embed.fields[10] assert starter_arrow.value == "═" def test_type_mismatch_pitcher_vs_batter_raises(self): """CompareMismatchError raised when pitcher compared to batter.""" batter_player = _make_player("Speedy Guy", pos_1="CF") batter_card_data = _batter_card_data(batter_player, _make_batting_card()) with pytest.raises(CompareMismatchError): build_compare_embed( self.card1, batter_card_data, "Max Scherzer", "Speedy Guy" ) # --------------------------------------------------------------------------- # build_compare_embed — edge cases # --------------------------------------------------------------------------- class TestBuildCompareEmbedEdgeCases: """Edge cases: missing data, None stats, same card compared to itself.""" def test_missing_batting_card_graceful(self): """ When battingcard is None, stat values display as '—' and arrows show ═. No exception should be raised. """ player = _make_player("Player A", pos_1="1B") card1 = {"player": player, "battingcard": None, "pitchingcard": None} card2 = {"player": player, "battingcard": None, "pitchingcard": None} # Should not raise embed = build_compare_embed(card1, card2, "Player A", "Player A") assert isinstance(embed, discord.Embed) def test_same_card_all_tied(self): """Comparing a card against itself should show ═ on every arrow field.""" player = _make_player("Clone", pos_1="CF", cost=300) bc = _make_batting_card(running=12) card_data = _batter_card_data(player, bc) embed = build_compare_embed(card_data, card_data, "Clone", "Clone") # Arrow fields are at positions 4, 7, 10, 13, 16, 19, 22 (center of each row) arrow_indices = [4, 7, 10, 13, 16, 19, 22] for idx in arrow_indices: assert embed.fields[idx].value == "═", ( f"Expected ═ at field index {idx}, " f"got {embed.fields[idx].value!r} (name={embed.fields[idx].name!r})" )