""" Tests for WP-12: Tier Badge on Card Embed. Verifies that get_card_embeds() prepends a tier badge to the card title when a card has Refractor tier progression, and falls back gracefully when the Refractor API is unavailable or returns no state. """ import pytest from unittest.mock import AsyncMock, patch import discord # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- def _make_card(card_id=1, player_name="Mike Trout", rarity_color="FFD700"): """Minimal card dict matching the API shape consumed by get_card_embeds.""" return { "id": card_id, "player": { "player_id": 101, "p_name": player_name, "rarity": {"name": "MVP", "value": 5, "color": rarity_color}, "cost": 500, "image": "https://example.com/card.png", "image2": None, "mlbclub": "Los Angeles Angels", "franchise": "Los Angeles Angels", "headshot": "https://example.com/headshot.jpg", "cardset": {"name": "2023 Season"}, "pos_1": "CF", "pos_2": None, "pos_3": None, "pos_4": None, "pos_5": None, "pos_6": None, "pos_7": None, "bbref_id": "troutmi01", "strat_code": "420420", "fangr_id": None, "vanity_card": None, }, "team": { "id": 10, "lname": "Paper Dynasty", "logo": "https://example.com/logo.png", "season": 7, }, } def _make_paperdex(): """Minimal paperdex response.""" return {"count": 0, "paperdex": []} # --------------------------------------------------------------------------- # Helpers to patch the async dependencies of get_card_embeds # --------------------------------------------------------------------------- def _patch_db_get(evo_response=None, paperdex_response=None): """ Return a side_effect callable that routes db_get calls to the right mock responses, so other get_card_embeds internals still behave. """ if paperdex_response is None: paperdex_response = _make_paperdex() async def _side_effect(endpoint, *args, **kwargs): if str(endpoint).startswith("refractor/cards/"): return evo_response if endpoint == "paperdex": return paperdex_response # Fallback for any other endpoint (e.g. plays/batting, plays/pitching) return None return _side_effect # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- class TestTierBadgeFormat: """Unit: tier badge string format for each tier level.""" @pytest.mark.asyncio async def test_tier_zero_no_badge(self): """T0 evolution state (current_tier=0) should produce no badge in title.""" card = _make_card() evo_state = {"current_tier": 0, "card_id": 1} with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db: mock_db.side_effect = _patch_db_get(evo_response=evo_state) embeds = await _call_get_card_embeds(card) assert embeds[0].title == "Mike Trout" @pytest.mark.asyncio async def test_tier_one_badge(self): """current_tier=1 should prefix title with [BC] (Base Chrome).""" card = _make_card() evo_state = {"current_tier": 1, "card_id": 1} with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db: mock_db.side_effect = _patch_db_get(evo_response=evo_state) embeds = await _call_get_card_embeds(card) assert embeds[0].title == "[BC] Mike Trout" @pytest.mark.asyncio async def test_tier_two_badge(self): """current_tier=2 should prefix title with [R] (Refractor).""" card = _make_card() evo_state = {"current_tier": 2, "card_id": 1} with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db: mock_db.side_effect = _patch_db_get(evo_response=evo_state) embeds = await _call_get_card_embeds(card) assert embeds[0].title == "[R] Mike Trout" @pytest.mark.asyncio async def test_tier_three_badge(self): """current_tier=3 should prefix title with [GR] (Gold Refractor).""" card = _make_card() evo_state = {"current_tier": 3, "card_id": 1} with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db: mock_db.side_effect = _patch_db_get(evo_response=evo_state) embeds = await _call_get_card_embeds(card) assert embeds[0].title == "[GR] Mike Trout" @pytest.mark.asyncio async def test_tier_four_superfractor_badge(self): """current_tier=4 (Superfractor) should prefix title with [SF].""" card = _make_card() evo_state = {"current_tier": 4, "card_id": 1} with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db: mock_db.side_effect = _patch_db_get(evo_response=evo_state) embeds = await _call_get_card_embeds(card) assert embeds[0].title == "[SF] Mike Trout" class TestTierBadgeInTitle: """Unit: badge appears correctly in the embed title.""" @pytest.mark.asyncio async def test_badge_prepended_to_player_name(self): """Badge should be prepended so title reads '[Tx] '.""" card = _make_card(player_name="Juan Soto") evo_state = {"current_tier": 2, "card_id": 1} with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db: mock_db.side_effect = _patch_db_get(evo_response=evo_state) embeds = await _call_get_card_embeds(card) assert embeds[0].title.startswith("[R] ") assert "Juan Soto" in embeds[0].title class TestFullyEvolvedBadge: """Unit: fully evolved card shows [SF] badge (Superfractor).""" @pytest.mark.asyncio async def test_fully_evolved_badge(self): """T4 card should show [SF] prefix, not [T4].""" card = _make_card() evo_state = {"current_tier": 4} with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db: mock_db.side_effect = _patch_db_get(evo_response=evo_state) embeds = await _call_get_card_embeds(card) assert embeds[0].title.startswith("[SF] ") assert "[T4]" not in embeds[0].title class TestNoBadgeGracefulFallback: """Unit: embed renders correctly when evolution state is absent or API fails.""" @pytest.mark.asyncio async def test_no_evolution_state_no_badge(self): """When evolution API returns None (404), title has no badge.""" card = _make_card() with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db: mock_db.side_effect = _patch_db_get(evo_response=None) embeds = await _call_get_card_embeds(card) assert embeds[0].title == "Mike Trout" @pytest.mark.asyncio async def test_api_exception_no_badge(self): """When evolution API raises an exception, card display is unaffected.""" card = _make_card() async def _failing_db_get(endpoint, *args, **kwargs): if str(endpoint).startswith("refractor/cards/"): raise ConnectionError("API unreachable") if endpoint == "paperdex": return _make_paperdex() return None with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db: mock_db.side_effect = _failing_db_get embeds = await _call_get_card_embeds(card) assert embeds[0].title == "Mike Trout" class TestEmbedColorUnchanged: """Unit: embed color comes from card rarity, not affected by evolution state.""" @pytest.mark.asyncio async def test_embed_color_from_rarity_with_evolution(self): """Color is still derived from rarity even when a tier badge is present.""" rarity_color = "FF0000" card = _make_card(rarity_color=rarity_color) evo_state = {"current_tier": 2} with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db: mock_db.side_effect = _patch_db_get(evo_response=evo_state) embeds = await _call_get_card_embeds(card) assert embeds[0].color == discord.Color(int(rarity_color, 16)) @pytest.mark.asyncio async def test_embed_color_from_rarity_without_evolution(self): """Color is derived from rarity when no evolution state exists.""" rarity_color = "00FF00" card = _make_card(rarity_color=rarity_color) with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db: mock_db.side_effect = _patch_db_get(evo_response=None) embeds = await _call_get_card_embeds(card) assert embeds[0].color == discord.Color(int(rarity_color, 16)) # --------------------------------------------------------------------------- # T1-7: TIER_BADGES format consistency check across modules # --------------------------------------------------------------------------- class TestTierSymbolsCompleteness: """ T1-7: Assert that TIER_SYMBOLS in cogs.refractor covers all tiers 0-4 and that helpers.main TIER_BADGES still covers tiers 1-4 for card embeds. Why: The refractor status command uses Unicode TIER_SYMBOLS for display, while card embed titles use helpers.main TIER_BADGES in bracket format. Both must cover the full tier range for their respective contexts. """ def test_tier_symbols_covers_all_tiers(self): """TIER_SYMBOLS must have entries for T0 through T4.""" from cogs.refractor import TIER_SYMBOLS for tier in range(5): assert tier in TIER_SYMBOLS, f"TIER_SYMBOLS missing tier {tier}" def test_tier_badges_covers_evolved_tiers(self): """helpers.main TIER_BADGES must have entries for T1 through T4.""" from helpers.main import TIER_BADGES for tier in range(1, 5): assert tier in TIER_BADGES, f"TIER_BADGES missing tier {tier}" def test_tier_symbols_are_unique(self): """Each tier must have a distinct symbol.""" from cogs.refractor import TIER_SYMBOLS values = list(TIER_SYMBOLS.values()) assert len(values) == len(set(values)), f"Duplicate symbols found: {values}" # --------------------------------------------------------------------------- # Helper: call get_card_embeds and return embed list # --------------------------------------------------------------------------- async def _call_get_card_embeds(card): """Import and call get_card_embeds, returning the list of embeds.""" from helpers.main import get_card_embeds result = await get_card_embeds(card) if isinstance(result, list): return result return [result]