""" Tests for schedule service functionality. Covers get_week_schedule, get_team_schedule, get_recent_games, get_upcoming_games, and group_games_by_series — verifying the asyncio.gather parallelization and post-fetch filtering logic. """ import pytest from unittest.mock import AsyncMock, patch from services.schedule_service import ScheduleService from tests.factories import GameFactory, TeamFactory def _game(game_id, week, away_abbrev, home_abbrev, **kwargs): """Create a Game with distinct team IDs per matchup.""" return GameFactory.create( id=game_id, week=week, away_team=TeamFactory.create(id=game_id * 10, abbrev=away_abbrev), home_team=TeamFactory.create(id=game_id * 10 + 1, abbrev=home_abbrev), **kwargs, ) class TestGetWeekSchedule: """Tests for ScheduleService.get_week_schedule — the HTTP layer.""" @pytest.fixture def service(self): svc = ScheduleService() svc.get_client = AsyncMock() return svc @pytest.mark.asyncio async def test_success(self, service): """get_week_schedule returns parsed Game objects on a normal response.""" mock_client = AsyncMock() mock_client.get.return_value = { "games": [ { "id": 1, "season": 12, "week": 5, "game_num": 1, "season_type": "regular", "away_team": { "id": 10, "abbrev": "NYY", "sname": "NYY", "lname": "New York", "season": 12, }, "home_team": { "id": 11, "abbrev": "BOS", "sname": "BOS", "lname": "Boston", "season": 12, }, "away_score": 4, "home_score": 2, } ] } service.get_client.return_value = mock_client games = await service.get_week_schedule(12, 5) assert len(games) == 1 assert games[0].away_team.abbrev == "NYY" assert games[0].home_team.abbrev == "BOS" assert games[0].is_completed @pytest.mark.asyncio async def test_empty_response(self, service): """get_week_schedule returns [] when the API has no games.""" mock_client = AsyncMock() mock_client.get.return_value = {"games": []} service.get_client.return_value = mock_client games = await service.get_week_schedule(12, 99) assert games == [] @pytest.mark.asyncio async def test_api_error_returns_empty(self, service): """get_week_schedule returns [] on API error (no exception raised).""" service.get_client.side_effect = Exception("connection refused") games = await service.get_week_schedule(12, 1) assert games == [] @pytest.mark.asyncio async def test_missing_games_key(self, service): """get_week_schedule returns [] when response lacks 'games' key.""" mock_client = AsyncMock() mock_client.get.return_value = {"status": "ok"} service.get_client.return_value = mock_client games = await service.get_week_schedule(12, 1) assert games == [] class TestGetTeamSchedule: """Tests for get_team_schedule — gather + team-abbrev filter.""" @pytest.fixture def service(self): return ScheduleService() @pytest.mark.asyncio async def test_filters_by_team_case_insensitive(self, service): """get_team_schedule returns only games involving the requested team, regardless of abbreviation casing.""" week1 = [ _game(1, 1, "NYY", "BOS", away_score=3, home_score=1), _game(2, 1, "LAD", "CHC", away_score=5, home_score=2), ] week2 = [ _game(3, 2, "BOS", "NYY", away_score=2, home_score=4), ] with patch.object(service, "get_week_schedule", new_callable=AsyncMock) as mock: mock.side_effect = [week1, week2] result = await service.get_team_schedule(12, "nyy", weeks=2) assert len(result) == 2 assert all( g.away_team.abbrev == "NYY" or g.home_team.abbrev == "NYY" for g in result ) @pytest.mark.asyncio async def test_full_season_fetches_18_weeks(self, service): """When weeks is None, all 18 weeks are fetched via gather.""" with patch.object(service, "get_week_schedule", new_callable=AsyncMock) as mock: mock.return_value = [] await service.get_team_schedule(12, "NYY") assert mock.call_count == 18 @pytest.mark.asyncio async def test_limited_weeks(self, service): """When weeks=5, only 5 weeks are fetched.""" with patch.object(service, "get_week_schedule", new_callable=AsyncMock) as mock: mock.return_value = [] await service.get_team_schedule(12, "NYY", weeks=5) assert mock.call_count == 5 class TestGetRecentGames: """Tests for get_recent_games — gather + completed-only filter.""" @pytest.fixture def service(self): return ScheduleService() @pytest.mark.asyncio async def test_returns_only_completed_games(self, service): """get_recent_games filters out games without scores.""" completed = GameFactory.completed(id=1, week=10) incomplete = GameFactory.upcoming(id=2, week=10) with patch.object(service, "get_week_schedule", new_callable=AsyncMock) as mock: mock.return_value = [completed, incomplete] result = await service.get_recent_games(12, weeks_back=1) assert len(result) == 1 assert result[0].is_completed @pytest.mark.asyncio async def test_sorted_descending_by_week_and_game_num(self, service): """Recent games are sorted most-recent first.""" game_w10 = GameFactory.completed(id=1, week=10, game_num=2) game_w9 = GameFactory.completed(id=2, week=9, game_num=1) with patch.object(service, "get_week_schedule", new_callable=AsyncMock) as mock: mock.side_effect = [[game_w10], [game_w9]] result = await service.get_recent_games(12, weeks_back=2) assert result[0].week == 10 assert result[1].week == 9 @pytest.mark.asyncio async def test_skips_negative_weeks(self, service): """Weeks that would be <= 0 are excluded from fetch.""" with patch.object(service, "get_week_schedule", new_callable=AsyncMock) as mock: mock.return_value = [] await service.get_recent_games(12, weeks_back=15) # weeks_to_fetch = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] — only 10 valid weeks assert mock.call_count == 10 class TestGetUpcomingGames: """Tests for get_upcoming_games — gather all 18 weeks + incomplete filter.""" @pytest.fixture def service(self): return ScheduleService() @pytest.mark.asyncio async def test_returns_only_incomplete_games(self, service): """get_upcoming_games filters out completed games.""" completed = GameFactory.completed(id=1, week=5) upcoming = GameFactory.upcoming(id=2, week=5) with patch.object(service, "get_week_schedule", new_callable=AsyncMock) as mock: mock.return_value = [completed, upcoming] result = await service.get_upcoming_games(12) assert len(result) == 18 # 1 incomplete game per week × 18 weeks assert all(not g.is_completed for g in result) @pytest.mark.asyncio async def test_sorted_ascending_by_week_and_game_num(self, service): """Upcoming games are sorted earliest first.""" game_w3 = GameFactory.upcoming(id=1, week=3, game_num=1) game_w1 = GameFactory.upcoming(id=2, week=1, game_num=2) with patch.object(service, "get_week_schedule", new_callable=AsyncMock) as mock: def side_effect(season, week): if week == 1: return [game_w1] if week == 3: return [game_w3] return [] mock.side_effect = side_effect result = await service.get_upcoming_games(12) assert result[0].week == 1 assert result[1].week == 3 @pytest.mark.asyncio async def test_fetches_all_18_weeks(self, service): """All 18 weeks are fetched in parallel (no early exit).""" with patch.object(service, "get_week_schedule", new_callable=AsyncMock) as mock: mock.return_value = [] await service.get_upcoming_games(12) assert mock.call_count == 18 class TestGroupGamesBySeries: """Tests for group_games_by_series — synchronous grouping logic.""" @pytest.fixture def service(self): return ScheduleService() def test_groups_by_alphabetical_pairing(self, service): """Games between the same two teams are grouped under one key, with the alphabetically-first team first in the tuple.""" games = [ _game(1, 1, "NYY", "BOS", game_num=1), _game(2, 1, "BOS", "NYY", game_num=2), _game(3, 1, "LAD", "CHC", game_num=1), ] result = service.group_games_by_series(games) assert ("BOS", "NYY") in result assert len(result[("BOS", "NYY")]) == 2 assert ("CHC", "LAD") in result assert len(result[("CHC", "LAD")]) == 1 def test_sorted_by_game_num_within_series(self, service): """Games within each series are sorted by game_num.""" games = [ _game(1, 1, "NYY", "BOS", game_num=3), _game(2, 1, "NYY", "BOS", game_num=1), _game(3, 1, "NYY", "BOS", game_num=2), ] result = service.group_games_by_series(games) series = result[("BOS", "NYY")] assert [g.game_num for g in series] == [1, 2, 3] def test_empty_input(self, service): """Empty games list returns empty dict.""" assert service.group_games_by_series([]) == {}