""" Integration tests for Roll persistence in DatabaseOperations. Tests dice roll batch saving and retrieval using the real DiceSystem. Verifies JSONB storage and querying capabilities with production code paths. Author: Claude Date: 2025-10-23 """ import pytest import pytest_asyncio from uuid import uuid4 from app.core.dice import dice_system # Mark all tests in this module as integration tests pytestmark = pytest.mark.integration @pytest_asyncio.fixture async def sample_game(db_ops, unique_game_id): """ Create a sample game for roll testing. Uses shared fixtures from conftest.py: - db_ops: DatabaseOperations instance - unique_game_id: Unique UUID for this test """ game = await db_ops.create_game( game_id=unique_game_id, league_id="sba", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) return game class TestRollPersistenceBatch: """Tests for batch saving dice rolls""" @pytest.mark.asyncio async def test_save_single_ab_roll(self, db_ops, sample_game): """Test saving a single at-bat roll from DiceSystem""" roll = dice_system.roll_ab( league_id="sba", game_id=sample_game.id, team_id=1, player_id=101 ) await db_ops.save_rolls_batch([roll]) # Verify it was saved rolls = await db_ops.get_rolls_for_game(sample_game.id) assert len(rolls) == 1 assert rolls[0].roll_id == roll.roll_id assert rolls[0].roll_type == "ab" assert rolls[0].league_id == "sba" assert rolls[0].team_id == 1 assert rolls[0].player_id == 101 @pytest.mark.asyncio async def test_save_multiple_rolls_mixed_types(self, db_ops, sample_game): """Test saving multiple rolls of different types in one batch""" ab_roll = dice_system.roll_ab( league_id="sba", game_id=sample_game.id, team_id=1, player_id=101 ) jump_roll = dice_system.roll_jump( league_id="sba", game_id=sample_game.id, team_id=1, player_id=102 ) fielding_roll = dice_system.roll_fielding( league_id="sba", position="SS", game_id=sample_game.id, team_id=2, player_id=201 ) d20_roll = dice_system.roll_d20( league_id="sba", game_id=sample_game.id, team_id=1, player_id=103 ) await db_ops.save_rolls_batch([ab_roll, jump_roll, fielding_roll, d20_roll]) # Verify all were saved rolls = await db_ops.get_rolls_for_game(sample_game.id) assert len(rolls) == 4 roll_types = {r.roll_type for r in rolls} assert roll_types == {"ab", "jump", "fielding", "d20"} @pytest.mark.asyncio async def test_save_empty_batch(self, db_ops, sample_game): """Test that saving empty batch doesn't error""" await db_ops.save_rolls_batch([]) # Verify no rolls exist for this game rolls = await db_ops.get_rolls_for_game(sample_game.id) assert len(rolls) == 0 @pytest.mark.asyncio async def test_save_pd_league_rolls(self, db_ops): """Test saving rolls for PD league""" # Create PD game game_id = uuid4() pd_game = await db_ops.create_game( game_id=game_id, league_id="pd", home_team_id=10, away_team_id=20, game_mode="ranked", visibility="public" ) roll = dice_system.roll_ab( league_id="pd", game_id=pd_game.id, team_id=10, player_id=1001 # PD card_id ) await db_ops.save_rolls_batch([roll]) # Verify rolls = await db_ops.get_rolls_for_game(pd_game.id) assert len(rolls) == 1 assert rolls[0].league_id == "pd" class TestRollRetrieval: """Tests for querying and filtering rolls""" @pytest.mark.asyncio async def test_get_rolls_by_roll_type(self, db_ops, sample_game): """Test filtering rolls by type""" # Create multiple rolls of different types ab_roll_1 = dice_system.roll_ab( league_id="sba", game_id=sample_game.id ) ab_roll_2 = dice_system.roll_ab( league_id="sba", game_id=sample_game.id ) jump_roll = dice_system.roll_jump( league_id="sba", game_id=sample_game.id ) await db_ops.save_rolls_batch([ab_roll_1, ab_roll_2, jump_roll]) # Get only AB rolls ab_rolls = await db_ops.get_rolls_for_game(sample_game.id, roll_type="ab") assert len(ab_rolls) == 2 assert all(r.roll_type == "ab" for r in ab_rolls) # Get only jump rolls jump_rolls = await db_ops.get_rolls_for_game(sample_game.id, roll_type="jump") assert len(jump_rolls) == 1 assert jump_rolls[0].roll_type == "jump" @pytest.mark.asyncio async def test_get_rolls_by_team(self, db_ops, sample_game): """Test filtering rolls by team""" team1_roll = dice_system.roll_ab( league_id="sba", game_id=sample_game.id, team_id=1 ) team2_roll = dice_system.roll_ab( league_id="sba", game_id=sample_game.id, team_id=2 ) await db_ops.save_rolls_batch([team1_roll, team2_roll]) # Get team 1 rolls team1_rolls = await db_ops.get_rolls_for_game(sample_game.id, team_id=1) assert len(team1_rolls) == 1 assert team1_rolls[0].team_id == 1 # Get team 2 rolls team2_rolls = await db_ops.get_rolls_for_game(sample_game.id, team_id=2) assert len(team2_rolls) == 1 assert team2_rolls[0].team_id == 2 @pytest.mark.asyncio async def test_get_rolls_with_limit(self, db_ops, sample_game): """Test limiting number of returned rolls""" # Create 10 rolls rolls = [ dice_system.roll_ab(league_id="sba", game_id=sample_game.id) for _ in range(10) ] await db_ops.save_rolls_batch(rolls) # Get only 5 most recent recent_rolls = await db_ops.get_rolls_for_game(sample_game.id, limit=5) assert len(recent_rolls) == 5 @pytest.mark.asyncio async def test_get_rolls_ordered_by_timestamp(self, db_ops, sample_game): """Test that rolls are returned in descending timestamp order (most recent first)""" import time roll1 = dice_system.roll_ab(league_id="sba", game_id=sample_game.id) time.sleep(0.01) # Small delay to ensure different timestamps roll2 = dice_system.roll_ab(league_id="sba", game_id=sample_game.id) time.sleep(0.01) roll3 = dice_system.roll_ab(league_id="sba", game_id=sample_game.id) await db_ops.save_rolls_batch([roll1, roll2, roll3]) # Get all rolls rolls = await db_ops.get_rolls_for_game(sample_game.id) # Most recent first assert rolls[0].roll_id == roll3.roll_id assert rolls[1].roll_id == roll2.roll_id assert rolls[2].roll_id == roll1.roll_id class TestRollDataIntegrity: """Tests for JSONB storage and data integrity""" @pytest.mark.asyncio async def test_ab_roll_data_storage(self, db_ops, sample_game): """Test that AbRoll data is correctly stored and retrieved""" roll = dice_system.roll_ab( league_id="sba", game_id=sample_game.id, team_id=1, player_id=101 ) await db_ops.save_rolls_batch([roll]) # Retrieve and verify rolls = await db_ops.get_rolls_for_game(sample_game.id) stored_roll = rolls[0] # Verify all dice values are stored assert "d6_one" in stored_roll.roll_data assert "d6_two_a" in stored_roll.roll_data assert "d6_two_b" in stored_roll.roll_data assert "d6_two_total" in stored_roll.roll_data assert "check_d20" in stored_roll.roll_data assert "resolution_d20" in stored_roll.roll_data assert "check_wild_pitch" in stored_roll.roll_data assert "check_passed_ball" in stored_roll.roll_data # Verify derived values assert stored_roll.roll_data["d6_two_total"] == ( stored_roll.roll_data["d6_two_a"] + stored_roll.roll_data["d6_two_b"] ) @pytest.mark.asyncio async def test_jump_roll_data_storage(self, db_ops, sample_game): """Test JumpRoll data storage""" roll = dice_system.roll_jump( league_id="sba", game_id=sample_game.id, team_id=1, player_id=102 ) await db_ops.save_rolls_batch([roll]) rolls = await db_ops.get_rolls_for_game(sample_game.id) stored_roll = rolls[0] # Verify structure assert "check_roll" in stored_roll.roll_data assert "is_pickoff_check" in stored_roll.roll_data assert "is_balk_check" in stored_roll.roll_data # Check conditional fields based on check_roll if stored_roll.roll_data["is_pickoff_check"] or stored_roll.roll_data["is_balk_check"]: # Should have resolution_roll assert "resolution_roll" in stored_roll.roll_data else: # Should have jump dice assert "jump_dice_a" in stored_roll.roll_data assert "jump_dice_b" in stored_roll.roll_data assert "jump_total" in stored_roll.roll_data @pytest.mark.asyncio async def test_fielding_roll_data_storage(self, db_ops, sample_game): """Test FieldingRoll data storage""" roll = dice_system.roll_fielding( league_id="sba", position="CF", game_id=sample_game.id, team_id=2, player_id=201 ) await db_ops.save_rolls_batch([roll]) rolls = await db_ops.get_rolls_for_game(sample_game.id) stored_roll = rolls[0] # Verify all fields assert stored_roll.roll_data["position"] == "CF" assert "d20" in stored_roll.roll_data assert "d6_one" in stored_roll.roll_data assert "d6_two" in stored_roll.roll_data assert "d6_three" in stored_roll.roll_data assert "d100" in stored_roll.roll_data assert "error_total" in stored_roll.roll_data assert "is_rare_play" in stored_roll.roll_data # Verify error_total calculation assert stored_roll.roll_data["error_total"] == ( stored_roll.roll_data["d6_one"] + stored_roll.roll_data["d6_two"] + stored_roll.roll_data["d6_three"] ) @pytest.mark.asyncio async def test_d20_roll_storage(self, db_ops, sample_game): """Test simple D20Roll storage""" roll = dice_system.roll_d20( league_id="sba", game_id=sample_game.id, team_id=1, player_id=103 ) await db_ops.save_rolls_batch([roll]) rolls = await db_ops.get_rolls_for_game(sample_game.id) stored_roll = rolls[0] # Verify simple structure assert "roll" in stored_roll.roll_data assert 1 <= stored_roll.roll_data["roll"] <= 20 @pytest.mark.asyncio async def test_context_storage(self, db_ops, sample_game): """Test that context metadata is stored correctly""" context = { "inning": 3, "outs": 2, "count": "3-2", "pitcher_id": 999 } roll = dice_system.roll_ab( league_id="sba", game_id=sample_game.id, team_id=1, player_id=101, context=context ) await db_ops.save_rolls_batch([roll]) rolls = await db_ops.get_rolls_for_game(sample_game.id) stored_roll = rolls[0] # Verify context was stored assert stored_roll.context is not None assert stored_roll.context["inning"] == 3 assert stored_roll.context["outs"] == 2 assert stored_roll.context["count"] == "3-2" assert stored_roll.context["pitcher_id"] == 999 class TestRollEdgeCases: """Tests for edge cases and error handling""" @pytest.mark.asyncio async def test_get_rolls_for_nonexistent_game(self, db_ops): """Test querying rolls for a game that doesn't exist""" fake_game_id = uuid4() rolls = await db_ops.get_rolls_for_game(fake_game_id) assert len(rolls) == 0 @pytest.mark.asyncio async def test_rolls_multiple_games_isolation(self, db_ops): """Test that rolls are isolated per game""" # Create two games game1_id = uuid4() game2_id = uuid4() game1 = await db_ops.create_game( game_id=game1_id, league_id="sba", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) game2 = await db_ops.create_game( game_id=game2_id, league_id="sba", home_team_id=3, away_team_id=4, game_mode="friendly", visibility="public" ) # Add rolls to each game game1_roll = dice_system.roll_ab(league_id="sba", game_id=game1.id) game2_roll = dice_system.roll_ab(league_id="sba", game_id=game2.id) await db_ops.save_rolls_batch([game1_roll]) await db_ops.save_rolls_batch([game2_roll]) # Verify isolation game1_rolls = await db_ops.get_rolls_for_game(game1.id) assert len(game1_rolls) == 1 assert game1_rolls[0].roll_id == game1_roll.roll_id game2_rolls = await db_ops.get_rolls_for_game(game2.id) assert len(game2_rolls) == 1 assert game2_rolls[0].roll_id == game2_roll.roll_id @pytest.mark.asyncio async def test_optional_fields(self, db_ops, sample_game): """Test that optional fields can be None""" roll = dice_system.roll_d20( league_id="sba", game_id=sample_game.id # No team_id, player_id, or context ) await db_ops.save_rolls_batch([roll]) rolls = await db_ops.get_rolls_for_game(sample_game.id) stored_roll = rolls[0] assert stored_roll.team_id is None assert stored_roll.player_id is None assert stored_roll.context is None