""" Tests for Evolution Tier Completion Notification embeds. These tests verify that: 1. Tier-up embeds are correctly formatted for tiers 1-3 (title, description, color). 2. Tier 4 (Fully Evolved) embeds include the special title, description, and note field. 3. Multiple tier-up events each produce a separate embed. 4. An empty tier-up list results in no channel sends. The channel interaction is mocked because we are testing the embed content, not Discord network I/O. Notification failure must never affect game flow, so the non-fatal path is also exercised. """ import pytest from unittest.mock import AsyncMock import discord from helpers.evolution_notifs import build_tier_up_embed, notify_tier_completion # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- def make_tier_up( player_name="Mike Trout", old_tier=1, new_tier=2, track_name="Batter", current_value=150, ): """Return a minimal tier_up dict matching the expected shape.""" return { "player_name": player_name, "old_tier": old_tier, "new_tier": new_tier, "track_name": track_name, "current_value": current_value, } # --------------------------------------------------------------------------- # Unit: build_tier_up_embed — tiers 1-3 (standard tier-up) # --------------------------------------------------------------------------- class TestBuildTierUpEmbed: """Verify that build_tier_up_embed produces correctly structured embeds.""" def test_title_is_evolution_tier_up(self): """Title must read 'Evolution Tier Up!' for any non-max tier.""" tier_up = make_tier_up(new_tier=2) embed = build_tier_up_embed(tier_up) assert embed.title == "Evolution Tier Up!" def test_description_contains_player_name(self): """Description must contain the player's name.""" tier_up = make_tier_up(player_name="Mike Trout", new_tier=2) embed = build_tier_up_embed(tier_up) assert "Mike Trout" in embed.description def test_description_contains_new_tier_name(self): """Description must include the human-readable tier name for the new tier.""" tier_up = make_tier_up(new_tier=2) embed = build_tier_up_embed(tier_up) # Tier 2 display name is "Rising" assert "Rising" in embed.description def test_description_contains_track_name(self): """Description must mention the evolution track (e.g., 'Batter').""" tier_up = make_tier_up(track_name="Batter", new_tier=2) embed = build_tier_up_embed(tier_up) assert "Batter" in embed.description def test_tier1_color_is_green(self): """Tier 1 uses green (0x2ecc71).""" tier_up = make_tier_up(old_tier=0, new_tier=1) embed = build_tier_up_embed(tier_up) assert embed.color.value == 0x2ECC71 def test_tier2_color_is_gold(self): """Tier 2 uses gold (0xf1c40f).""" tier_up = make_tier_up(old_tier=1, new_tier=2) embed = build_tier_up_embed(tier_up) assert embed.color.value == 0xF1C40F def test_tier3_color_is_purple(self): """Tier 3 uses purple (0x9b59b6).""" tier_up = make_tier_up(old_tier=2, new_tier=3) embed = build_tier_up_embed(tier_up) assert embed.color.value == 0x9B59B6 def test_footer_text_is_paper_dynasty_evolution(self): """Footer text must be 'Paper Dynasty Evolution' for brand consistency.""" tier_up = make_tier_up(new_tier=2) embed = build_tier_up_embed(tier_up) assert embed.footer.text == "Paper Dynasty Evolution" def test_returns_discord_embed_instance(self): """Return type must be discord.Embed so it can be sent directly.""" tier_up = make_tier_up(new_tier=2) embed = build_tier_up_embed(tier_up) assert isinstance(embed, discord.Embed) # --------------------------------------------------------------------------- # Unit: build_tier_up_embed — tier 4 (fully evolved) # --------------------------------------------------------------------------- class TestBuildTierUpEmbedFullyEvolved: """Verify that tier 4 (Fully Evolved) embeds use special formatting.""" def test_title_is_fully_evolved(self): """Tier 4 title must be 'FULLY EVOLVED!' to emphasise max achievement.""" tier_up = make_tier_up(old_tier=3, new_tier=4) embed = build_tier_up_embed(tier_up) assert embed.title == "FULLY EVOLVED!" def test_description_mentions_maximum_evolution(self): """Tier 4 description must mention 'maximum evolution' per the spec.""" tier_up = make_tier_up(player_name="Mike Trout", old_tier=3, new_tier=4) embed = build_tier_up_embed(tier_up) assert "maximum evolution" in embed.description.lower() def test_description_contains_player_name(self): """Player name must appear in the tier 4 description.""" tier_up = make_tier_up(player_name="Mike Trout", old_tier=3, new_tier=4) embed = build_tier_up_embed(tier_up) assert "Mike Trout" in embed.description def test_description_contains_track_name(self): """Track name must appear in the tier 4 description.""" tier_up = make_tier_up(track_name="Batter", old_tier=3, new_tier=4) embed = build_tier_up_embed(tier_up) assert "Batter" in embed.description def test_tier4_color_is_teal(self): """Tier 4 uses teal (0x1abc9c) to visually distinguish max evolution.""" tier_up = make_tier_up(old_tier=3, new_tier=4) embed = build_tier_up_embed(tier_up) assert embed.color.value == 0x1ABC9C def test_note_field_present(self): """Tier 4 must include a note field about future rating boosts.""" tier_up = make_tier_up(old_tier=3, new_tier=4) embed = build_tier_up_embed(tier_up) field_names = [f.name for f in embed.fields] assert any( "rating" in name.lower() or "boost" in name.lower() or "note" in name.lower() for name in field_names ), "Expected a field mentioning rating boosts for tier 4 embed" def test_note_field_value_mentions_future_update(self): """The note field value must reference the future rating boost update.""" tier_up = make_tier_up(old_tier=3, new_tier=4) embed = build_tier_up_embed(tier_up) note_field = next( ( f for f in embed.fields if "rating" in f.name.lower() or "boost" in f.name.lower() or "note" in f.name.lower() ), None, ) assert note_field is not None assert ( "future" in note_field.value.lower() or "update" in note_field.value.lower() ) def test_footer_text_is_paper_dynasty_evolution(self): """Footer must remain 'Paper Dynasty Evolution' for tier 4 as well.""" tier_up = make_tier_up(old_tier=3, new_tier=4) embed = build_tier_up_embed(tier_up) assert embed.footer.text == "Paper Dynasty Evolution" # --------------------------------------------------------------------------- # Unit: notify_tier_completion — multiple and empty cases # --------------------------------------------------------------------------- class TestNotifyTierCompletion: """Verify that notify_tier_completion sends the right number of messages.""" @pytest.mark.asyncio async def test_single_tier_up_sends_one_message(self): """A single tier-up event sends exactly one embed to the channel.""" channel = AsyncMock() tier_up = make_tier_up(new_tier=2) await notify_tier_completion(channel, tier_up) channel.send.assert_called_once() @pytest.mark.asyncio async def test_sends_embed_not_plain_text(self): """The channel.send call must use the embed= keyword, not content=.""" channel = AsyncMock() tier_up = make_tier_up(new_tier=2) await notify_tier_completion(channel, tier_up) _, kwargs = channel.send.call_args assert ( "embed" in kwargs ), "notify_tier_completion must send an embed, not plain text" @pytest.mark.asyncio async def test_embed_type_is_discord_embed(self): """The embed passed to channel.send must be a discord.Embed instance.""" channel = AsyncMock() tier_up = make_tier_up(new_tier=2) await notify_tier_completion(channel, tier_up) _, kwargs = channel.send.call_args assert isinstance(kwargs["embed"], discord.Embed) @pytest.mark.asyncio async def test_notification_failure_does_not_raise(self): """If channel.send raises, notify_tier_completion must swallow it so game flow is unaffected.""" channel = AsyncMock() channel.send.side_effect = Exception("Discord API unavailable") tier_up = make_tier_up(new_tier=2) # Should not raise await notify_tier_completion(channel, tier_up) @pytest.mark.asyncio async def test_multiple_tier_ups_caller_sends_multiple_embeds(self): """ Callers are responsible for iterating tier-up events; each call to notify_tier_completion sends a separate embed. This test simulates three consecutive calls (3 events) and asserts 3 sends occurred. """ channel = AsyncMock() events = [ make_tier_up(player_name="Mike Trout", new_tier=2), make_tier_up(player_name="Aaron Judge", new_tier=1), make_tier_up(player_name="Shohei Ohtani", new_tier=3), ] for event in events: await notify_tier_completion(channel, event) assert ( channel.send.call_count == 3 ), "Each tier-up event must produce its own embed (no batching)" @pytest.mark.asyncio async def test_no_tier_ups_means_no_sends(self): """ When the caller has an empty list of tier-up events and simply does not call notify_tier_completion, zero sends happen. This explicitly guards against any accidental unconditional send. """ channel = AsyncMock() tier_up_events = [] for event in tier_up_events: await notify_tier_completion(channel, event) channel.send.assert_not_called()