""" Integration tests for DatabaseOperations. Tests actual database operations using the test database. These tests are slower than unit tests but verify real DB interactions. Author: Claude Date: 2025-10-22 """ import pytest from uuid import uuid4 from app.database.operations import DatabaseOperations from app.database.session import init_db, engine # Mark all tests in this module as integration tests pytestmark = pytest.mark.integration @pytest.fixture(scope="function") async def setup_database(): """ Set up test database schema. Runs once per test function (noop if tables exist). """ # Create all tables (will skip if they exist) await init_db() yield # Teardown if needed (tables persist between test runs) @pytest.fixture async def db_ops(): """Create DatabaseOperations instance for each test""" return DatabaseOperations() @pytest.fixture def sample_game_id(): """Generate a unique game ID for each test""" return uuid4() class TestDatabaseOperationsGame: """Tests for game CRUD operations""" @pytest.mark.asyncio async def test_create_game(self, setup_database, db_ops, sample_game_id): """Test creating a game in database""" game = await db_ops.create_game( game_id=sample_game_id, league_id="sba", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) assert game.id == sample_game_id assert game.league_id == "sba" assert game.status == "pending" assert game.home_team_id == 1 assert game.away_team_id == 2 @pytest.mark.asyncio async def test_create_game_with_ai(self, setup_database, db_ops, sample_game_id): """Test creating a game with AI opponent""" game = await db_ops.create_game( game_id=sample_game_id, league_id="pd", home_team_id=10, away_team_id=20, game_mode="practice", visibility="private", home_team_is_ai=False, away_team_is_ai=True, ai_difficulty="balanced" ) assert game.away_team_is_ai is True assert game.home_team_is_ai is False assert game.ai_difficulty == "balanced" @pytest.mark.asyncio async def test_get_game(self, setup_database, db_ops, sample_game_id): """Test retrieving a game from database""" # Create game await db_ops.create_game( game_id=sample_game_id, league_id="sba", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) # Retrieve game retrieved = await db_ops.get_game(sample_game_id) assert retrieved is not None assert retrieved.id == sample_game_id assert retrieved.league_id == "sba" @pytest.mark.asyncio async def test_get_game_nonexistent(self, setup_database, db_ops): """Test retrieving nonexistent game returns None""" fake_id = uuid4() game = await db_ops.get_game(fake_id) assert game is None @pytest.mark.asyncio async def test_update_game_state(self, setup_database, db_ops, sample_game_id): """Test updating game state""" # Create game await db_ops.create_game( game_id=sample_game_id, league_id="sba", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) # Update state await db_ops.update_game_state( game_id=sample_game_id, inning=5, half="bottom", home_score=3, away_score=2, status="active" ) # Verify update game = await db_ops.get_game(sample_game_id) assert game.current_inning == 5 assert game.current_half == "bottom" assert game.home_score == 3 assert game.away_score == 2 assert game.status == "active" @pytest.mark.asyncio async def test_update_game_state_nonexistent_raises_error(self, setup_database, db_ops): """Test updating nonexistent game raises error""" fake_id = uuid4() with pytest.raises(ValueError, match="not found"): await db_ops.update_game_state( game_id=fake_id, inning=1, half="top", home_score=0, away_score=0 ) class TestDatabaseOperationsLineup: """Tests for lineup operations""" @pytest.mark.asyncio async def test_add_sba_lineup_player(self, setup_database, db_ops, sample_game_id): """Test adding SBA player to lineup""" # Create game first await db_ops.create_game( game_id=sample_game_id, league_id="sba", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) # Add SBA player to lineup lineup = await db_ops.add_sba_lineup_player( game_id=sample_game_id, team_id=1, player_id=101, position="CF", batting_order=1, is_starter=True ) assert lineup.game_id == sample_game_id assert lineup.team_id == 1 assert lineup.player_id == 101 assert lineup.card_id is None assert lineup.position == "CF" assert lineup.batting_order == 1 assert lineup.is_active is True @pytest.mark.asyncio async def test_add_pd_lineup_card(self, setup_database, db_ops, sample_game_id): """Test adding PD card to lineup""" await db_ops.create_game( game_id=sample_game_id, league_id="pd", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) lineup = await db_ops.add_pd_lineup_card( game_id=sample_game_id, team_id=1, card_id=200, position="P", batting_order=None, # Pitcher, no batting order (AL rules) is_starter=True ) assert lineup.position == "P" assert lineup.batting_order is None assert lineup.card_id == 200 assert lineup.player_id is None @pytest.mark.asyncio async def test_get_active_lineup(self, setup_database, db_ops, sample_game_id): """Test retrieving active lineup for a team""" await db_ops.create_game( game_id=sample_game_id, league_id="sba", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) # Add multiple SBA players to lineup await db_ops.add_sba_lineup_player( game_id=sample_game_id, team_id=1, player_id=103, position="1B", batting_order=3 ) await db_ops.add_sba_lineup_player( game_id=sample_game_id, team_id=1, player_id=101, position="CF", batting_order=1 ) await db_ops.add_sba_lineup_player( game_id=sample_game_id, team_id=1, player_id=102, position="SS", batting_order=2 ) # Retrieve lineup lineup = await db_ops.get_active_lineup(sample_game_id, team_id=1) assert len(lineup) == 3 # Should be sorted by batting order assert lineup[0].batting_order == 1 assert lineup[1].batting_order == 2 assert lineup[2].batting_order == 3 @pytest.mark.asyncio async def test_get_active_lineup_empty(self, setup_database, db_ops, sample_game_id): """Test retrieving lineup for team with no entries""" await db_ops.create_game( game_id=sample_game_id, league_id="sba", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) lineup = await db_ops.get_active_lineup(sample_game_id, team_id=1) assert lineup == [] class TestDatabaseOperationsPlays: """Tests for play operations""" @pytest.mark.asyncio async def test_save_play(self, setup_database, db_ops, sample_game_id): """Test saving a play""" await db_ops.create_game( game_id=sample_game_id, league_id="sba", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) play_data = { "game_id": sample_game_id, "play_number": 1, "inning": 1, "half": "top", "outs_before": 0, "batting_order": 1, "result_description": "Single to left field", "pa": 1, "ab": 1, "hit": 1 } play = await db_ops.save_play(play_data) assert play.game_id == sample_game_id assert play.play_number == 1 assert play.result_description == "Single to left field" @pytest.mark.asyncio async def test_get_plays(self, setup_database, db_ops, sample_game_id): """Test retrieving plays for a game""" await db_ops.create_game( game_id=sample_game_id, league_id="sba", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) # Save multiple plays for i in range(3): await db_ops.save_play({ "game_id": sample_game_id, "play_number": i + 1, "inning": 1, "half": "top", "outs_before": i, "batting_order": i + 1, "result_description": f"Play {i+1}", "pa": 1 }) # Retrieve plays plays = await db_ops.get_plays(sample_game_id) assert len(plays) == 3 # Should be ordered by play_number assert plays[0].play_number == 1 assert plays[1].play_number == 2 assert plays[2].play_number == 3 @pytest.mark.asyncio async def test_get_plays_empty(self, setup_database, db_ops, sample_game_id): """Test retrieving plays for game with no plays""" await db_ops.create_game( game_id=sample_game_id, league_id="sba", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) plays = await db_ops.get_plays(sample_game_id) assert plays == [] class TestDatabaseOperationsRecovery: """Tests for game state recovery""" @pytest.mark.asyncio async def test_load_game_state_complete(self, setup_database, db_ops, sample_game_id): """Test loading complete game state""" # Create game await db_ops.create_game( game_id=sample_game_id, league_id="sba", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) # Add lineups await db_ops.create_lineup_entry( game_id=sample_game_id, team_id=1, card_id=101, position="CF", batting_order=1 ) # Add play await db_ops.save_play({ "game_id": sample_game_id, "play_number": 1, "inning": 1, "half": "top", "outs_before": 0, "batting_order": 1, "result_description": "Single", "pa": 1 }) # Update game state await db_ops.update_game_state( game_id=sample_game_id, inning=2, half="bottom", home_score=1, away_score=0 ) # Load complete state state = await db_ops.load_game_state(sample_game_id) assert state is not None assert state["game"]["id"] == sample_game_id assert state["game"]["current_inning"] == 2 assert state["game"]["current_half"] == "bottom" assert len(state["lineups"]) == 1 assert len(state["plays"]) == 1 @pytest.mark.asyncio async def test_load_game_state_nonexistent(self, setup_database, db_ops): """Test loading nonexistent game returns None""" fake_id = uuid4() state = await db_ops.load_game_state(fake_id) assert state is None class TestDatabaseOperationsGameSession: """Tests for game session operations""" @pytest.mark.asyncio async def test_create_game_session(self, setup_database, db_ops, sample_game_id): """Test creating a game session""" await db_ops.create_game( game_id=sample_game_id, league_id="sba", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) session = await db_ops.create_game_session(sample_game_id) assert session.game_id == sample_game_id assert session.state_snapshot is None # Initially null @pytest.mark.asyncio async def test_update_session_snapshot(self, setup_database, db_ops, sample_game_id): """Test updating session snapshot""" await db_ops.create_game( game_id=sample_game_id, league_id="sba", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) await db_ops.create_game_session(sample_game_id) snapshot = { "inning": 3, "outs": 2, "runners": [1, 3] } await db_ops.update_session_snapshot(sample_game_id, snapshot) # Note: Would need to query session to verify, but this tests no errors @pytest.mark.asyncio async def test_update_session_snapshot_nonexistent_raises_error(self, setup_database, db_ops): """Test updating nonexistent session raises error""" fake_id = uuid4() with pytest.raises(ValueError, match="not found"): await db_ops.update_session_snapshot(fake_id, {}) class TestDatabaseOperationsRoster: """Tests for roster link operations""" @pytest.mark.asyncio async def test_add_pd_roster_card(self, setup_database, db_ops, sample_game_id): """Test adding a PD card to roster""" # Create game first await db_ops.create_game( game_id=sample_game_id, league_id="pd", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) # Add roster card roster_data = await db_ops.add_pd_roster_card( game_id=sample_game_id, card_id=123, team_id=1 ) assert roster_data.id is not None assert roster_data.game_id == sample_game_id assert roster_data.card_id == 123 assert roster_data.team_id == 1 @pytest.mark.asyncio async def test_add_sba_roster_player(self, setup_database, db_ops, sample_game_id): """Test adding an SBA player to roster""" # Create game first await db_ops.create_game( game_id=sample_game_id, league_id="sba", home_team_id=10, away_team_id=20, game_mode="friendly", visibility="public" ) # Add roster player roster_data = await db_ops.add_sba_roster_player( game_id=sample_game_id, player_id=456, team_id=10 ) assert roster_data.id is not None assert roster_data.game_id == sample_game_id assert roster_data.player_id == 456 assert roster_data.team_id == 10 @pytest.mark.asyncio async def test_add_duplicate_pd_card_raises_error(self, setup_database, db_ops, sample_game_id): """Test adding duplicate PD card to roster fails""" # Create game and add card await db_ops.create_game( game_id=sample_game_id, league_id="pd", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) await db_ops.add_pd_roster_card( game_id=sample_game_id, card_id=123, team_id=1 ) # Try to add same card again - should fail with pytest.raises(ValueError, match="Could not add card to roster"): await db_ops.add_pd_roster_card( game_id=sample_game_id, card_id=123, team_id=1 ) @pytest.mark.asyncio async def test_add_duplicate_sba_player_raises_error(self, setup_database, db_ops, sample_game_id): """Test adding duplicate SBA player to roster fails""" # Create game and add player await db_ops.create_game( game_id=sample_game_id, league_id="sba", home_team_id=10, away_team_id=20, game_mode="friendly", visibility="public" ) await db_ops.add_sba_roster_player( game_id=sample_game_id, player_id=456, team_id=10 ) # Try to add same player again - should fail with pytest.raises(ValueError, match="Could not add player to roster"): await db_ops.add_sba_roster_player( game_id=sample_game_id, player_id=456, team_id=10 ) @pytest.mark.asyncio async def test_get_pd_roster_all_teams(self, setup_database, db_ops, sample_game_id): """Test getting all PD cards for a game""" # Create game and add cards for both teams await db_ops.create_game( game_id=sample_game_id, league_id="pd", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) await db_ops.add_pd_roster_card(sample_game_id, 101, 1) await db_ops.add_pd_roster_card(sample_game_id, 102, 1) await db_ops.add_pd_roster_card(sample_game_id, 201, 2) # Get all roster entries roster = await db_ops.get_pd_roster(sample_game_id) assert len(roster) == 3 card_ids = {r.card_id for r in roster} assert card_ids == {101, 102, 201} @pytest.mark.asyncio async def test_get_pd_roster_filtered_by_team(self, setup_database, db_ops, sample_game_id): """Test getting PD cards filtered by team""" # Create game and add cards for both teams await db_ops.create_game( game_id=sample_game_id, league_id="pd", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) await db_ops.add_pd_roster_card(sample_game_id, 101, 1) await db_ops.add_pd_roster_card(sample_game_id, 102, 1) await db_ops.add_pd_roster_card(sample_game_id, 201, 2) # Get team 1 roster team1_roster = await db_ops.get_pd_roster(sample_game_id, team_id=1) assert len(team1_roster) == 2 card_ids = {r.card_id for r in team1_roster} assert card_ids == {101, 102} @pytest.mark.asyncio async def test_get_sba_roster_all_teams(self, setup_database, db_ops, sample_game_id): """Test getting all SBA players for a game""" # Create game and add players for both teams await db_ops.create_game( game_id=sample_game_id, league_id="sba", home_team_id=10, away_team_id=20, game_mode="friendly", visibility="public" ) await db_ops.add_sba_roster_player(sample_game_id, 401, 10) await db_ops.add_sba_roster_player(sample_game_id, 402, 10) await db_ops.add_sba_roster_player(sample_game_id, 501, 20) # Get all roster entries roster = await db_ops.get_sba_roster(sample_game_id) assert len(roster) == 3 player_ids = {r.player_id for r in roster} assert player_ids == {401, 402, 501} @pytest.mark.asyncio async def test_get_sba_roster_filtered_by_team(self, setup_database, db_ops, sample_game_id): """Test getting SBA players filtered by team""" # Create game and add players for both teams await db_ops.create_game( game_id=sample_game_id, league_id="sba", home_team_id=10, away_team_id=20, game_mode="friendly", visibility="public" ) await db_ops.add_sba_roster_player(sample_game_id, 401, 10) await db_ops.add_sba_roster_player(sample_game_id, 402, 10) await db_ops.add_sba_roster_player(sample_game_id, 501, 20) # Get team 10 roster team10_roster = await db_ops.get_sba_roster(sample_game_id, team_id=10) assert len(team10_roster) == 2 player_ids = {r.player_id for r in team10_roster} assert player_ids == {401, 402} @pytest.mark.asyncio async def test_remove_roster_entry(self, setup_database, db_ops, sample_game_id): """Test removing a roster entry""" # Create game and add card await db_ops.create_game( game_id=sample_game_id, league_id="pd", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) roster_data = await db_ops.add_pd_roster_card( game_id=sample_game_id, card_id=123, team_id=1 ) # Remove it await db_ops.remove_roster_entry(roster_data.id) # Verify it's gone roster = await db_ops.get_pd_roster(sample_game_id) assert len(roster) == 0 @pytest.mark.asyncio async def test_remove_nonexistent_roster_entry_raises_error(self, setup_database, db_ops): """Test removing nonexistent roster entry fails""" fake_id = 999999 with pytest.raises(ValueError, match="not found"): await db_ops.remove_roster_entry(fake_id) @pytest.mark.asyncio async def test_get_empty_pd_roster(self, setup_database, db_ops, sample_game_id): """Test getting PD roster for game with no cards""" # Create game but don't add any cards await db_ops.create_game( game_id=sample_game_id, league_id="pd", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) roster = await db_ops.get_pd_roster(sample_game_id) assert len(roster) == 0 @pytest.mark.asyncio async def test_get_empty_sba_roster(self, setup_database, db_ops, sample_game_id): """Test getting SBA roster for game with no players""" # Create game but don't add any players await db_ops.create_game( game_id=sample_game_id, league_id="sba", home_team_id=10, away_team_id=20, game_mode="friendly", visibility="public" ) roster = await db_ops.get_sba_roster(sample_game_id) assert len(roster) == 0 class TestDatabaseOperationsRollback: """Tests for database rollback operations (delete_plays_after, etc.)""" @pytest.mark.asyncio async def test_delete_plays_after(self, setup_database, db_ops, sample_game_id): """Test deleting plays after a specific play number""" # Create game await db_ops.create_game( game_id=sample_game_id, league_id="sba", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) # Create lineup entries for batter, pitcher, and catcher batter = await db_ops.add_sba_lineup_player( game_id=sample_game_id, team_id=1, player_id=100, position="CF", batting_order=1, is_starter=True ) pitcher = await db_ops.add_sba_lineup_player( game_id=sample_game_id, team_id=2, player_id=200, position="P", batting_order=None, is_starter=True ) catcher = await db_ops.add_sba_lineup_player( game_id=sample_game_id, team_id=2, player_id=201, position="C", batting_order=1, is_starter=True ) # Create 5 plays for play_num in range(1, 6): await db_ops.save_play({ 'game_id': sample_game_id, 'play_number': play_num, 'inning': 1, 'half': 'top', 'outs_before': 0, 'batter_id': batter.id, 'pitcher_id': pitcher.id, 'catcher_id': catcher.id, 'dice_roll': f'10+{play_num}', 'result_description': f'Play {play_num}', 'pa': 1, 'complete': True }) # Delete plays after play 3 deleted_count = await db_ops.delete_plays_after(sample_game_id, 3) assert deleted_count == 2 # Plays 4 and 5 deleted # Verify only plays 1-3 remain remaining_plays = await db_ops.get_plays(sample_game_id) assert len(remaining_plays) == 3 assert all(p.play_number <= 3 for p in remaining_plays) @pytest.mark.asyncio async def test_delete_plays_after_with_no_plays_to_delete(self, setup_database, db_ops, sample_game_id): """Test deleting plays when none exist after the threshold""" # Create game await db_ops.create_game( game_id=sample_game_id, league_id="sba", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) # Create lineup for play batter = await db_ops.add_sba_lineup_player( game_id=sample_game_id, team_id=1, player_id=100, position="CF", batting_order=1, is_starter=True ) pitcher = await db_ops.add_sba_lineup_player( game_id=sample_game_id, team_id=2, player_id=200, position="P", batting_order=None, is_starter=True ) catcher = await db_ops.add_sba_lineup_player( game_id=sample_game_id, team_id=2, player_id=201, position="C", batting_order=1, is_starter=True ) # Create 3 plays for play_num in range(1, 4): await db_ops.save_play({ 'game_id': sample_game_id, 'play_number': play_num, 'inning': 1, 'half': 'top', 'outs_before': 0, 'batter_id': batter.id, 'pitcher_id': pitcher.id, 'catcher_id': catcher.id, 'dice_roll': f'10+{play_num}', 'result_description': f'Play {play_num}', 'pa': 1, 'complete': True }) # Delete plays after play 10 (none exist) deleted_count = await db_ops.delete_plays_after(sample_game_id, 10) assert deleted_count == 0 # Verify all 3 plays remain remaining_plays = await db_ops.get_plays(sample_game_id) assert len(remaining_plays) == 3 @pytest.mark.asyncio async def test_delete_substitutions_after(self, setup_database, db_ops, sample_game_id): """Test deleting substitutions after a specific play number""" # Create game await db_ops.create_game( game_id=sample_game_id, league_id="sba", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) # Create starter starter = await db_ops.add_sba_lineup_player( game_id=sample_game_id, team_id=1, player_id=100, position="CF", batting_order=1, is_starter=True ) # Create substitutions - need to manually set substitution fields sub1 = await db_ops.add_sba_lineup_player( game_id=sample_game_id, team_id=1, player_id=101, position="CF", batting_order=1, is_starter=False ) sub2 = await db_ops.add_sba_lineup_player( game_id=sample_game_id, team_id=1, player_id=102, position="CF", batting_order=1, is_starter=False ) sub3 = await db_ops.add_sba_lineup_player( game_id=sample_game_id, team_id=1, player_id=103, position="CF", batting_order=1, is_starter=False ) # Manually set substitution fields using SQLAlchemy from app.database.session import AsyncSessionLocal from app.models.db_models import Lineup from sqlalchemy import select, update async with AsyncSessionLocal() as session: # Update starter - mark as inactive await session.execute( update(Lineup) .where(Lineup.id == starter.id) .values(is_active=False, after_play=None) ) # Update sub1 - substituted at play 5 await session.execute( update(Lineup) .where(Lineup.id == sub1.id) .values(is_active=False, entered_inning=3, after_play=5, replacing_id=starter.id) ) # Update sub2 - substituted at play 10 await session.execute( update(Lineup) .where(Lineup.id == sub2.id) .values(is_active=False, entered_inning=5, after_play=10, replacing_id=sub1.id) ) # Update sub3 - substituted at play 15 await session.execute( update(Lineup) .where(Lineup.id == sub3.id) .values(is_active=True, entered_inning=7, after_play=15, replacing_id=sub2.id) ) await session.commit() # Delete substitutions after play 10 (>= 10, so deletes sub2 and sub3) deleted_count = await db_ops.delete_substitutions_after(sample_game_id, 10) assert deleted_count == 2 # sub2 (after play 10) and sub3 (after play 15) deleted # Verify lineup state - need to get ALL lineup entries, not just active from app.database.session import AsyncSessionLocal from app.models.db_models import Lineup from sqlalchemy import select async with AsyncSessionLocal() as session: result = await session.execute( select(Lineup) .where( Lineup.game_id == sample_game_id, Lineup.team_id == 1 ) ) all_lineup = list(result.scalars().all()) # Should have starter + 1 sub (sub1 only) assert len([p for p in all_lineup if p.after_play is not None]) == 1 # The remaining sub should be sub1 (after_play=5) remaining_sub = [p for p in all_lineup if p.after_play is not None][0] assert remaining_sub.after_play == 5 @pytest.mark.asyncio async def test_complete_rollback_scenario(self, setup_database, db_ops, sample_game_id): """Test complete rollback scenario: plays + substitutions""" # Create game await db_ops.create_game( game_id=sample_game_id, league_id="sba", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) # Create lineup batter = await db_ops.add_sba_lineup_player( game_id=sample_game_id, team_id=1, player_id=100, position="CF", batting_order=1, is_starter=True ) pitcher = await db_ops.add_sba_lineup_player( game_id=sample_game_id, team_id=2, player_id=200, position="P", batting_order=None, is_starter=True ) catcher = await db_ops.add_sba_lineup_player( game_id=sample_game_id, team_id=2, player_id=201, position="C", batting_order=1, is_starter=True ) # Create 10 plays for play_num in range(1, 11): await db_ops.save_play({ 'game_id': sample_game_id, 'play_number': play_num, 'inning': (play_num - 1) // 3 + 1, 'half': 'top' if play_num % 2 == 1 else 'bot', 'outs_before': 0, 'batter_id': batter.id, 'pitcher_id': pitcher.id, 'catcher_id': catcher.id, 'dice_roll': f'10+{play_num}', 'result_description': f'Play {play_num}', 'pa': 1, 'complete': True }) # Create substitution at play 7 sub = await db_ops.add_sba_lineup_player( game_id=sample_game_id, team_id=1, player_id=101, position="CF", batting_order=1, is_starter=False ) # Manually set substitution fields from app.database.session import AsyncSessionLocal from app.models.db_models import Lineup from sqlalchemy import update async with AsyncSessionLocal() as session: await session.execute( update(Lineup) .where(Lineup.id == sub.id) .values(is_active=True, entered_inning=3, after_play=7, replacing_id=batter.id) ) await session.commit() # Rollback to play 5 (delete everything after play 5) rollback_point = 5 plays_deleted = await db_ops.delete_plays_after(sample_game_id, rollback_point) subs_deleted = await db_ops.delete_substitutions_after(sample_game_id, rollback_point) # Verify deletions assert plays_deleted == 5 # Plays 6-10 deleted assert subs_deleted == 1 # Substitution at play 7 deleted # Verify remaining data remaining_plays = await db_ops.get_plays(sample_game_id) assert len(remaining_plays) == 5 assert max(p.play_number for p in remaining_plays) == 5