"""
Unit Tests for Dice System

Tests cryptographic dice rolling system with all roll types.
"""
import pytest
from uuid import uuid4

from app.core.dice import DiceSystem
from app.core.roll_types import RollType, AbRoll, JumpRoll, FieldingRoll, D20Roll


class TestDiceSystemBasic:
    """Test basic DiceSystem functionality"""

    def test_dice_system_creation(self):
        """Test creating a DiceSystem"""
        dice = DiceSystem()
        assert dice is not None
        assert len(dice._roll_history) == 0

    def test_singleton_access(self):
        """Test singleton dice_system access"""
        from app.core.dice import dice_system
        assert dice_system is not None


class TestAbRolls:
    """Test at-bat rolls"""

    def test_roll_ab_basic(self):
        """Test basic at-bat roll"""
        dice = DiceSystem()
        roll = dice.roll_ab(league_id="sba")

        assert isinstance(roll, AbRoll)
        assert roll.roll_type == RollType.AB
        assert roll.league_id == "sba"
        assert 1 <= roll.d6_one <= 6
        assert 1 <= roll.d6_two_a <= 6
        assert 1 <= roll.d6_two_b <= 6
        assert 1 <= roll.chaos_d20 <= 20
        assert 1 <= roll.resolution_d20 <= 20
        assert roll.d6_two_total == roll.d6_two_a + roll.d6_two_b

    def test_roll_ab_with_game_id(self):
        """Test at-bat roll with game_id"""
        dice = DiceSystem()
        game_id = uuid4()
        roll = dice.roll_ab(league_id="pd", game_id=game_id)

        assert roll.game_id == game_id
        assert roll.league_id == "pd"

    def test_roll_ab_adds_to_history(self):
        """Test that AB rolls are added to history"""
        dice = DiceSystem()
        initial_count = len(dice._roll_history)

        dice.roll_ab(league_id="sba")

        assert len(dice._roll_history) == initial_count + 1
        assert isinstance(dice._roll_history[-1], AbRoll)

    def test_roll_ab_unique_roll_ids(self):
        """Test that each roll gets unique ID"""
        dice = DiceSystem()
        roll1 = dice.roll_ab(league_id="sba")
        roll2 = dice.roll_ab(league_id="sba")

        assert roll1.roll_id != roll2.roll_id

    def test_roll_ab_wild_pitch_check_distribution(self):
        """Test that wild pitch checks occur (roll 1 on chaos_d20)"""
        dice = DiceSystem()
        found_wp_check = False

        for _ in range(100):
            roll = dice.roll_ab(league_id="sba")
            if roll.check_wild_pitch:
                found_wp_check = True
                assert roll.chaos_d20 == 1
                assert 1 <= roll.resolution_d20 <= 20
                break

        # Should find at least one in 100 rolls (probability ~99.4%)
        assert found_wp_check, "No wild pitch check found in 100 rolls"

    def test_roll_ab_passed_ball_check_distribution(self):
        """Test that passed ball checks occur (roll 2 on chaos_d20)"""
        dice = DiceSystem()
        found_pb_check = False

        for _ in range(100):
            roll = dice.roll_ab(league_id="sba")
            if roll.check_passed_ball:
                found_pb_check = True
                assert roll.chaos_d20 == 2
                assert 1 <= roll.resolution_d20 <= 20
                break

        assert found_pb_check, "No passed ball check found in 100 rolls"


class TestJumpRolls:
    """Test jump rolls"""

    def test_roll_jump_basic(self):
        """Test basic jump roll"""
        dice = DiceSystem()
        roll = dice.roll_jump(league_id="sba")

        assert isinstance(roll, JumpRoll)
        assert roll.roll_type == RollType.JUMP
        assert roll.league_id == "sba"
        assert 1 <= roll.check_roll <= 20

    def test_roll_jump_normal(self):
        """Test normal jump (check_roll >= 3)"""
        dice = DiceSystem()
        found_normal = False

        for _ in range(50):
            roll = dice.roll_jump(league_id="sba")
            if roll.check_roll >= 3:
                found_normal = True
                assert roll.jump_dice_a is not None
                assert roll.jump_dice_b is not None
                assert 1 <= roll.jump_dice_a <= 6
                assert 1 <= roll.jump_dice_b <= 6
                assert roll.jump_total == roll.jump_dice_a + roll.jump_dice_b
                assert roll.resolution_roll is None
                break

        assert found_normal

    def test_roll_jump_pickoff(self):
        """Test pickoff check (check_roll == 1)"""
        dice = DiceSystem()
        found_pickoff = False

        for _ in range(100):
            roll = dice.roll_jump(league_id="sba")
            if roll.check_roll == 1:
                found_pickoff = True
                assert roll.is_pickoff_check
                assert not roll.is_balk_check
                assert roll.resolution_roll is not None
                assert 1 <= roll.resolution_roll <= 20
                assert roll.jump_dice_a is None
                assert roll.jump_dice_b is None
                break

        assert found_pickoff

    def test_roll_jump_balk(self):
        """Test balk check (check_roll == 2)"""
        dice = DiceSystem()
        found_balk = False

        for _ in range(100):
            roll = dice.roll_jump(league_id="sba")
            if roll.check_roll == 2:
                found_balk = True
                assert roll.is_balk_check
                assert not roll.is_pickoff_check
                assert roll.resolution_roll is not None
                assert roll.jump_dice_a is None
                break

        assert found_balk

    def test_roll_jump_adds_to_history(self):
        """Test that jump rolls are added to history"""
        dice = DiceSystem()
        initial_count = len(dice._roll_history)

        dice.roll_jump(league_id="pd")

        assert len(dice._roll_history) == initial_count + 1


class TestFieldingRolls:
    """Test fielding rolls"""

    def test_roll_fielding_basic(self):
        """Test basic fielding roll"""
        dice = DiceSystem()
        roll = dice.roll_fielding(position="SS", league_id="sba")

        assert isinstance(roll, FieldingRoll)
        assert roll.roll_type == RollType.FIELDING
        assert roll.position == "SS"
        assert roll.league_id == "sba"
        assert 1 <= roll.d20 <= 20
        assert 1 <= roll.d6_one <= 6
        assert 1 <= roll.d6_two <= 6
        assert 1 <= roll.d6_three <= 6
        assert 1 <= roll.d100 <= 100
        assert 3 <= roll.error_total <= 18

    def test_roll_fielding_all_positions(self):
        """Test fielding roll for all valid positions"""
        dice = DiceSystem()
        positions = ['P', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF']

        for pos in positions:
            roll = dice.roll_fielding(position=pos, league_id="sba")
            assert roll.position == pos

    def test_roll_fielding_invalid_position(self):
        """Test that invalid position raises error"""
        dice = DiceSystem()

        with pytest.raises(ValueError, match="Invalid position"):
            dice.roll_fielding(position="DH", league_id="sba")

    def test_roll_fielding_sba_rare_play(self):
        """Test SBA rare play detection (d100 == 1)"""
        dice = DiceSystem()
        found_rare = False

        # Rare play is 1%, so test many times
        for _ in range(500):
            roll = dice.roll_fielding(position="CF", league_id="sba")
            if roll.d100 == 1:
                found_rare = True
                assert roll.is_rare_play
                break

        # Note: Might occasionally fail due to randomness (0.6% chance)
        # In production, we'd mock the dice for deterministic testing

    def test_roll_fielding_pd_rare_play(self):
        """Test PD rare play detection (error_total == 5)"""
        dice = DiceSystem()
        found_rare = False

        # error_total of 5 is about 2.7% chance (3d6: 1+1+3, 1+2+2)
        for _ in range(500):
            roll = dice.roll_fielding(position="1B", league_id="pd")
            if roll.error_total == 5:
                found_rare = True
                assert roll.is_rare_play
                break

    def test_roll_fielding_adds_to_history(self):
        """Test that fielding rolls are added to history"""
        dice = DiceSystem()
        initial_count = len(dice._roll_history)

        dice.roll_fielding(position="3B", league_id="pd")

        assert len(dice._roll_history) == initial_count + 1


class TestD20Rolls:
    """Test generic d20 rolls"""

    def test_roll_d20_basic(self):
        """Test basic d20 roll"""
        dice = DiceSystem()
        roll = dice.roll_d20(league_id="sba")

        assert isinstance(roll, D20Roll)
        assert roll.roll_type == RollType.D20
        assert roll.league_id == "sba"
        assert 1 <= roll.roll <= 20

    def test_roll_d20_with_game_id(self):
        """Test d20 roll with game_id"""
        dice = DiceSystem()
        game_id = uuid4()
        roll = dice.roll_d20(league_id="pd", game_id=game_id)

        assert roll.game_id == game_id

    def test_roll_d20_adds_to_history(self):
        """Test that d20 rolls are added to history"""
        dice = DiceSystem()
        initial_count = len(dice._roll_history)

        dice.roll_d20(league_id="sba")

        assert len(dice._roll_history) == initial_count + 1

    def test_roll_d20_distribution(self):
        """Test d20 distribution is roughly uniform"""
        dice = DiceSystem()
        rolls = [dice.roll_d20(league_id="sba").roll for _ in range(1000)]

        # Count occurrences of each value
        counts = {i: rolls.count(i) for i in range(1, 21)}

        # Each value should appear roughly 50 times (1000/20)
        # Allow for variance - check all values appear at least 20 times
        for value, count in counts.items():
            assert count >= 20, f"Value {value} appeared only {count} times"


class TestRollHistory:
    """Test roll history management"""

    def test_get_roll_history_all(self):
        """Test getting all roll history"""
        dice = DiceSystem()
        dice.clear_history()

        dice.roll_ab(league_id="sba")
        dice.roll_jump(league_id="sba")
        dice.roll_fielding(position="SS", league_id="sba")

        history = dice.get_roll_history()
        assert len(history) == 3

    def test_get_roll_history_by_type(self):
        """Test filtering history by roll type"""
        dice = DiceSystem()
        dice.clear_history()

        dice.roll_ab(league_id="sba")
        dice.roll_ab(league_id="sba")
        dice.roll_jump(league_id="sba")
        dice.roll_fielding(position="SS", league_id="sba")

        ab_rolls = dice.get_roll_history(roll_type=RollType.AB)
        assert len(ab_rolls) == 2
        assert all(r.roll_type == RollType.AB for r in ab_rolls)

    def test_get_roll_history_by_game(self):
        """Test filtering history by game_id"""
        dice = DiceSystem()
        dice.clear_history()

        game1 = uuid4()
        game2 = uuid4()

        dice.roll_ab(league_id="sba", game_id=game1)
        dice.roll_ab(league_id="sba", game_id=game1)
        dice.roll_ab(league_id="sba", game_id=game2)

        game1_rolls = dice.get_roll_history(game_id=game1)
        assert len(game1_rolls) == 2
        assert all(r.game_id == game1 for r in game1_rolls)

    def test_get_roll_history_limit(self):
        """Test limit parameter"""
        dice = DiceSystem()
        dice.clear_history()

        for _ in range(10):
            dice.roll_d20(league_id="sba")

        limited = dice.get_roll_history(limit=5)
        assert len(limited) == 5

    def test_get_rolls_since(self):
        """Test getting rolls since timestamp"""
        dice = DiceSystem()
        dice.clear_history()

        game_id = uuid4()
        import pendulum
        import time

        # Roll some dice
        roll1 = dice.roll_ab(league_id="sba", game_id=game_id)

        # Use roll1's timestamp as the cutoff point
        timestamp = roll1.timestamp

        # Sleep briefly to ensure roll2 has a later timestamp
        time.sleep(0.01)

        roll2 = dice.roll_jump(league_id="sba", game_id=game_id)

        # Get rolls since timestamp (should only get roll2 because timestamp >= is used)
        # We want rolls AFTER roll1, so add a tiny offset
        recent = dice.get_rolls_since(game_id, timestamp.add(microseconds=1))
        assert len(recent) == 1
        assert recent[0].roll_type == RollType.JUMP

    def test_verify_roll(self):
        """Test roll verification"""
        dice = DiceSystem()
        roll = dice.roll_d20(league_id="sba")

        assert dice.verify_roll(roll.roll_id)
        assert not dice.verify_roll("nonexistent_id")

    def test_clear_history(self):
        """Test clearing roll history"""
        dice = DiceSystem()
        dice.roll_ab(league_id="sba")
        dice.roll_jump(league_id="sba")

        assert len(dice._roll_history) > 0

        dice.clear_history()
        assert len(dice._roll_history) == 0


class TestDistributionStats:
    """Test distribution statistics"""

    def test_get_distribution_stats(self):
        """Test getting distribution statistics"""
        dice = DiceSystem()
        dice.clear_history()

        dice.roll_ab(league_id="sba")
        dice.roll_ab(league_id="sba")
        dice.roll_jump(league_id="sba")

        stats = dice.get_distribution_stats()
        assert stats["total_rolls"] == 3
        assert stats["by_type"]["ab"] == 2
        assert stats["by_type"]["jump"] == 1

    def test_get_distribution_stats_by_type(self):
        """Test getting stats for specific roll type"""
        dice = DiceSystem()
        dice.clear_history()

        dice.roll_ab(league_id="sba")
        dice.roll_ab(league_id="sba")
        dice.roll_jump(league_id="sba")

        ab_stats = dice.get_distribution_stats(roll_type=RollType.AB)
        assert ab_stats["total_rolls"] == 2

    def test_get_stats(self):
        """Test get_stats helper method"""
        dice = DiceSystem()
        dice.clear_history()

        dice.roll_ab(league_id="sba")
        dice.roll_fielding(position="SS", league_id="sba")

        stats = dice.get_stats()
        assert stats["total_rolls"] == 2
        assert "by_type" in stats


class TestCryptographicRandomness:
    """Test that dice use cryptographic randomness"""

    def test_unique_roll_ids(self):
        """Test that roll IDs are unique"""
        dice = DiceSystem()
        roll_ids = set()

        for _ in range(100):
            roll = dice.roll_d20(league_id="sba")
            roll_ids.add(roll.roll_id)

        # All 100 should be unique
        assert len(roll_ids) == 100

    def test_roll_id_format(self):
        """Test roll ID format (hex string)"""
        dice = DiceSystem()
        roll = dice.roll_d20(league_id="sba")

        # Should be hex string (16 chars for 8 bytes)
        assert len(roll.roll_id) == 16
        assert all(c in '0123456789abcdef' for c in roll.roll_id)
