""" Tests for the post-game Refractor Progress embed field (#147). Covers _build_refractor_progress_text() which formats tier-ups and near-threshold cards into the summary embed field value, and the updated _run_post_game_refractor_hook() return value. """ from unittest.mock import AsyncMock, MagicMock, patch from command_logic.logic_gameplay import ( _build_refractor_progress_text, _run_post_game_refractor_hook, ) # --------------------------------------------------------------------------- # _build_refractor_progress_text # --------------------------------------------------------------------------- class TestBuildRefractorProgressText: """_build_refractor_progress_text formats tier-ups and close cards.""" async def test_returns_none_when_no_tier_ups_and_no_close_cards(self): """Returns None when evaluate-game had no tier-ups and refractor/cards returns empty. Caller uses None to skip adding the field to the embed entirely. """ with patch( "command_logic.logic_gameplay.db_get", new_callable=AsyncMock ) as mock_get: mock_get.return_value = {"items": [], "count": 0} result = await _build_refractor_progress_text( evo_result={"tier_ups": []}, winning_team_id=1, losing_team_id=2, ) assert result is None async def test_returns_none_when_evo_result_is_none(self): """Returns None gracefully when the hook returned None (e.g. on API failure). Near-threshold fetch still runs; returns None when that also yields nothing. """ with patch( "command_logic.logic_gameplay.db_get", new_callable=AsyncMock ) as mock_get: mock_get.return_value = None result = await _build_refractor_progress_text( evo_result=None, winning_team_id=1, losing_team_id=2, ) assert result is None async def test_tier_up_shows_player_name_and_tier_name(self): """Tier-ups are formatted as '⬆ **Name** → Tier Name'. The tier name comes from TIER_NAMES (e.g. new_tier=1 → 'Base Chrome'). """ with patch( "command_logic.logic_gameplay.db_get", new_callable=AsyncMock ) as mock_get: mock_get.return_value = None result = await _build_refractor_progress_text( evo_result={ "tier_ups": [ { "player_id": 10, "player_name": "Mike Trout", "new_tier": 1, } ] }, winning_team_id=1, losing_team_id=2, ) assert result is not None assert "⬆" in result assert "Mike Trout" in result assert "Base Chrome" in result async def test_multiple_tier_ups_each_on_own_line(self): """Each tier-up gets its own line in the output.""" with patch( "command_logic.logic_gameplay.db_get", new_callable=AsyncMock ) as mock_get: mock_get.return_value = None result = await _build_refractor_progress_text( evo_result={ "tier_ups": [ {"player_id": 10, "player_name": "Mike Trout", "new_tier": 1}, { "player_id": 11, "player_name": "Shohei Ohtani", "new_tier": 2, }, ] }, winning_team_id=1, losing_team_id=2, ) assert result is not None assert "Mike Trout" in result assert "Shohei Ohtani" in result assert "Base Chrome" in result assert "Refractor" in result async def test_near_threshold_card_shows_percentage(self): """Near-threshold cards appear as '◈ Name (pct%)'. The percentage is current_value / next_threshold rounded to nearest integer. """ with patch( "command_logic.logic_gameplay.db_get", new_callable=AsyncMock ) as mock_get: mock_get.return_value = { "items": [ { "player_name": "Sandy Koufax", "current_value": 120, "next_threshold": 149, } ], "count": 1, } result = await _build_refractor_progress_text( evo_result=None, winning_team_id=1, losing_team_id=2, ) assert result is not None assert "◈" in result assert "Sandy Koufax" in result assert "81%" in result # 120/149 = ~80.5% → 81% async def test_near_threshold_fetch_queried_for_both_teams(self): """refractor/cards is called once per team with progress=close.""" with patch( "command_logic.logic_gameplay.db_get", new_callable=AsyncMock ) as mock_get: mock_get.return_value = None await _build_refractor_progress_text( evo_result=None, winning_team_id=3, losing_team_id=7, ) team_ids_queried = [] for call in mock_get.call_args_list: params = dict(call.kwargs.get("params", [])) if "team_id" in params: team_ids_queried.append(params["team_id"]) assert 3 in team_ids_queried assert 7 in team_ids_queried async def test_near_threshold_api_failure_is_non_fatal(self): """An exception during the near-threshold fetch does not propagate. Tier-ups are still shown; close cards silently dropped. """ with patch( "command_logic.logic_gameplay.db_get", new_callable=AsyncMock ) as mock_get: mock_get.side_effect = RuntimeError("API down") result = await _build_refractor_progress_text( evo_result={ "tier_ups": [ {"player_id": 10, "player_name": "Mike Trout", "new_tier": 1} ] }, winning_team_id=1, losing_team_id=2, ) assert result is not None assert "Mike Trout" in result async def test_close_cards_capped_at_five(self): """At most 5 near-threshold entries are included across both teams.""" many_cards = [ {"player_name": f"Player {i}", "current_value": 90, "next_threshold": 100} for i in range(10) ] with patch( "command_logic.logic_gameplay.db_get", new_callable=AsyncMock ) as mock_get: mock_get.return_value = {"items": many_cards, "count": len(many_cards)} result = await _build_refractor_progress_text( evo_result=None, winning_team_id=1, losing_team_id=2, ) assert result is not None assert result.count("◈") <= 5 # --------------------------------------------------------------------------- # _run_post_game_refractor_hook return value # --------------------------------------------------------------------------- class TestRefractorHookReturnValue: """_run_post_game_refractor_hook returns evo_result on success, None on failure.""" async def test_returns_evo_result_when_successful(self): """The evaluate-game response dict is returned so complete_game can use it.""" evo_response = { "tier_ups": [{"player_id": 1, "player_name": "Babe Ruth", "new_tier": 2}] } def _side_effect(url, *args, **kwargs): if url.startswith("season-stats"): return None return evo_response with ( patch( "command_logic.logic_gameplay.db_post", new_callable=AsyncMock ) as mock_post, patch( "command_logic.logic_gameplay._trigger_variant_renders", new_callable=AsyncMock, return_value={}, ), patch( "command_logic.logic_gameplay.notify_tier_completion", new_callable=AsyncMock, ), ): mock_post.side_effect = _side_effect result = await _run_post_game_refractor_hook(42, MagicMock()) assert result == evo_response async def test_returns_none_on_exception(self): """Hook returns None when an exception occurs (game result is unaffected).""" with patch( "command_logic.logic_gameplay.db_post", new_callable=AsyncMock ) as mock_post: mock_post.side_effect = RuntimeError("db unreachable") result = await _run_post_game_refractor_hook(42, MagicMock()) assert result is None async def test_returns_evo_result_when_no_tier_ups(self): """Returns the full evo_result even when tier_ups is empty or absent.""" evo_response = {"tier_ups": []} with patch( "command_logic.logic_gameplay.db_post", new_callable=AsyncMock ) as mock_post: mock_post.return_value = evo_response result = await _run_post_game_refractor_hook(42, MagicMock()) assert result == evo_response