"""
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
