"""
Integration tests for SubstitutionManager.

Tests full flow: validation → DB → state sync for all substitution types.

Author: Claude
Date: 2025-11-04
"""
import pytest
from uuid import uuid4

from app.core.substitution_manager import SubstitutionManager
from app.core.state_manager import state_manager
from app.database.operations import DatabaseOperations
from app.models.game_models import GameState, LineupPlayerState, TeamLineupState
from app.database.session import init_db


# Mark all tests in this module as integration tests
pytestmark = pytest.mark.integration


@pytest.fixture(scope="module")
async def setup_database():
    """Set up test database schema"""
    await init_db()
    yield


@pytest.fixture
async def game_with_lineups():
    """
    Create test game with full lineups in database and state.

    Returns:
        tuple: (game_id, lineup_ids, bench_ids, team_id)
    """
    db_ops = DatabaseOperations()
    game_id = uuid4()
    team_id = 1

    # Create game in DB
    await db_ops.create_game(
        game_id=game_id,
        league_id='sba',
        home_team_id=team_id,
        away_team_id=2,
        game_mode='friendly',
        visibility='public'
    )

    # Create lineup entries in DB (9 active players)
    lineup_ids = {}
    positions = ['P', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF']
    for i, position in enumerate(positions, start=1):
        lineup_id = await db_ops.add_sba_lineup_player(
            game_id=game_id,
            team_id=team_id,
            player_id=100 + i,
            position=position,
            batting_order=i,
            is_starter=True
        )
        lineup_ids[position] = lineup_id

    # Add bench players (3 substitutes)
    bench_ids = {}
    bench_positions = ['CF', 'SS', 'P']
    for i, position in enumerate(bench_positions, start=1):
        bench_id = await db_ops.add_sba_lineup_player(
            game_id=game_id,
            team_id=team_id,
            player_id=200 + i,
            position=position,
            batting_order=None,  # Not in batting order
            is_starter=False
        )
        bench_ids[position] = bench_id

    # Load lineup into state manager cache
    lineup_data = await db_ops.get_active_lineup(game_id, team_id)
    lineup_state = TeamLineupState(
        team_id=team_id,
        players=[
            LineupPlayerState(
                lineup_id=p.id,  # type: ignore[assignment]
                card_id=p.player_id,  # type: ignore[assignment]
                position=p.position,
                batting_order=p.batting_order,
                is_active=p.is_active
            )
            for p in lineup_data
        ]
    )
    state_manager.set_lineup(game_id, team_id, lineup_state)

    # Create game state with current batter
    current_batter = lineup_state.players[7]  # CF, batting 8th
    state = GameState(
        game_id=game_id,
        league_id='sba',
        home_team_id=team_id,
        away_team_id=2,
        current_batter=current_batter,
        play_count=5  # Pitcher has faced batters
    )
    state_manager.update_state(game_id, state)

    yield game_id, lineup_ids, bench_ids, team_id

    # Cleanup
    state_manager.remove_game(game_id)
    # Note: Database cleanup happens automatically in test database


class TestPinchHitIntegration:
    """Integration tests for pinch hitter substitutions"""

    async def test_pinch_hit_full_flow(self, setup_database, game_with_lineups):
        """Test complete pinch hit flow: validation → DB → state"""
        game_id, lineup_ids, bench_ids, team_id = game_with_lineups

        db_ops = DatabaseOperations()
        manager = SubstitutionManager(db_ops)

        # Get state before substitution
        state = state_manager.get_state(game_id)
        original_batter_lineup_id = state.current_batter.lineup_id

        # Execute substitution (pinch hit for CF)
        result = await manager.pinch_hit(
            game_id=game_id,
            player_out_lineup_id=lineup_ids['CF'],
            player_in_card_id=201,  # Bench CF
            team_id=team_id
        )

        # Verify result
        assert result.success
        assert result.new_lineup_id is not None
        assert result.player_out_lineup_id == lineup_ids['CF']
        assert result.player_in_card_id == 201
        assert result.new_position == 'CF'

        # Verify database updated
        lineup_data = await db_ops.get_active_lineup(game_id, team_id)
        active_lineup_ids = [p.id for p in lineup_data]
        assert result.new_lineup_id in active_lineup_ids
        assert lineup_ids['CF'] not in active_lineup_ids

        # Verify substitution metadata in DB (query directly)
        from app.database.session import AsyncSessionLocal
        from app.models.db_models import Lineup
        from sqlalchemy import select

        async with AsyncSessionLocal() as session:
            # Verify old player marked inactive
            old_result = await session.execute(
                select(Lineup).where(Lineup.id == lineup_ids['CF'])
            )
            old_player = old_result.scalar_one()
            assert not old_player.is_active

            # Verify new player active
            new_result = await session.execute(
                select(Lineup).where(Lineup.id == result.new_lineup_id)
            )
            new_player = new_result.scalar_one()
            assert new_player.is_active
            assert new_player.player_id == 201

        # Verify state updated
        state = state_manager.get_state(game_id)
        if original_batter_lineup_id == lineup_ids['CF']:
            # If we substituted for current batter, state should update
            assert state.current_batter.lineup_id == result.new_lineup_id

        # Verify lineup cache updated
        lineup_state = state_manager.get_lineup(game_id, team_id)
        old_player_state = lineup_state.get_player_by_lineup_id(lineup_ids['CF'])
        assert old_player_state is not None
        assert not old_player_state.is_active

        new_player_state = lineup_state.get_player_by_lineup_id(result.new_lineup_id)
        assert new_player_state is not None
        assert new_player_state.is_active
        assert new_player_state.card_id == 201

    async def test_pinch_hit_validation_failure(self, setup_database, game_with_lineups):
        """Test pinch hit fails validation"""
        game_id, lineup_ids, bench_ids, team_id = game_with_lineups

        db_ops = DatabaseOperations()
        manager = SubstitutionManager(db_ops)

        # Try to pinch hit with player already in game
        result = await manager.pinch_hit(
            game_id=game_id,
            player_out_lineup_id=lineup_ids['CF'],
            player_in_card_id=102,  # Catcher - already active
            team_id=team_id
        )

        # Verify validation failed
        assert not result.success
        assert result.error_code == "ALREADY_ACTIVE"
        assert "already in the game" in result.error_message.lower()

        # Verify no database changes
        lineup_data = await db_ops.get_active_lineup(game_id, team_id)
        assert lineup_ids['CF'] in [p.id for p in lineup_data]


class TestDefensiveReplacementIntegration:
    """Integration tests for defensive replacement"""

    async def test_defensive_replace_full_flow(self, setup_database, game_with_lineups):
        """Test complete defensive replacement flow"""
        game_id, lineup_ids, bench_ids, team_id = game_with_lineups

        db_ops = DatabaseOperations()
        manager = SubstitutionManager(db_ops)

        # Execute substitution (replace SS with bench SS)
        result = await manager.defensive_replace(
            game_id=game_id,
            player_out_lineup_id=lineup_ids['SS'],
            player_in_card_id=202,  # Bench SS
            new_position='SS',
            team_id=team_id
        )

        # Verify result
        assert result.success
        assert result.new_lineup_id is not None
        assert result.player_out_lineup_id == lineup_ids['SS']
        assert result.new_position == 'SS'

        # Verify database updated
        lineup_data = await db_ops.get_active_lineup(game_id, team_id)
        active_ids = [p.id for p in lineup_data]
        assert result.new_lineup_id in active_ids
        assert lineup_ids['SS'] not in active_ids

        # Verify lineup cache updated
        lineup_state = state_manager.get_lineup(game_id, team_id)
        new_player = lineup_state.get_player_by_lineup_id(result.new_lineup_id)
        assert new_player is not None
        assert new_player.position == 'SS'
        assert new_player.is_active

    async def test_defensive_replace_position_change(self, setup_database, game_with_lineups):
        """Test defensive replacement with position change"""
        game_id, lineup_ids, bench_ids, team_id = game_with_lineups

        db_ops = DatabaseOperations()
        manager = SubstitutionManager(db_ops)

        # Replace SS and move to 2B
        result = await manager.defensive_replace(
            game_id=game_id,
            player_out_lineup_id=lineup_ids['SS'],
            player_in_card_id=202,
            new_position='2B',  # Different position
            team_id=team_id
        )

        assert result.success
        assert result.new_position == '2B'

        # Verify new player has correct position in DB (query directly)
        from app.database.session import AsyncSessionLocal
        from app.models.db_models import Lineup
        from sqlalchemy import select

        async with AsyncSessionLocal() as session:
            result_query = await session.execute(
                select(Lineup).where(Lineup.id == result.new_lineup_id)
            )
            new_player = result_query.scalar_one()
            assert new_player.position == '2B'


class TestPitchingChangeIntegration:
    """Integration tests for pitching changes"""

    async def test_change_pitcher_full_flow(self, setup_database, game_with_lineups):
        """Test complete pitching change flow"""
        game_id, lineup_ids, bench_ids, team_id = game_with_lineups

        db_ops = DatabaseOperations()
        manager = SubstitutionManager(db_ops)

        # Get state and set current pitcher
        state = state_manager.get_state(game_id)
        lineup_state = state_manager.get_lineup(game_id, team_id)
        pitcher = lineup_state.get_player_by_lineup_id(lineup_ids['P'])
        state.current_pitcher = pitcher
        state_manager.update_state(game_id, state)

        # Execute pitching change
        result = await manager.change_pitcher(
            game_id=game_id,
            pitcher_out_lineup_id=lineup_ids['P'],
            pitcher_in_card_id=203,  # Bench pitcher
            team_id=team_id
        )

        # Verify result
        assert result.success
        assert result.new_lineup_id is not None
        assert result.new_position == 'P'

        # Verify database updated
        lineup_data = await db_ops.get_active_lineup(game_id, team_id)
        active_ids = [p.id for p in lineup_data]
        assert result.new_lineup_id in active_ids
        assert lineup_ids['P'] not in active_ids

        # Verify state updated (current_pitcher should change)
        state = state_manager.get_state(game_id)
        assert state.current_pitcher is not None
        assert state.current_pitcher.lineup_id == result.new_lineup_id

        # Verify lineup cache
        lineup_state = state_manager.get_lineup(game_id, team_id)
        new_pitcher = lineup_state.get_pitcher()
        assert new_pitcher is not None
        assert new_pitcher.lineup_id == result.new_lineup_id
        assert new_pitcher.card_id == 203

    async def test_change_pitcher_min_batters_validation(self, setup_database, game_with_lineups):
        """Test pitching change fails when min batters not met"""
        game_id, lineup_ids, bench_ids, team_id = game_with_lineups

        db_ops = DatabaseOperations()
        manager = SubstitutionManager(db_ops)

        # Set play_count to 0 (no batters faced)
        state = state_manager.get_state(game_id)
        state.play_count = 0
        state_manager.update_state(game_id, state)

        # Try to change pitcher
        result = await manager.change_pitcher(
            game_id=game_id,
            pitcher_out_lineup_id=lineup_ids['P'],
            pitcher_in_card_id=203,
            team_id=team_id,
            force_change=False
        )

        # Verify validation failed
        assert not result.success
        assert result.error_code == "MIN_BATTERS_NOT_MET"

    async def test_change_pitcher_force_change(self, setup_database, game_with_lineups):
        """Test force_change bypasses min batters requirement"""
        game_id, lineup_ids, bench_ids, team_id = game_with_lineups

        db_ops = DatabaseOperations()
        manager = SubstitutionManager(db_ops)

        # Set play_count to 0
        state = state_manager.get_state(game_id)
        state.play_count = 0
        state_manager.update_state(game_id, state)

        # Force change (injury/emergency)
        result = await manager.change_pitcher(
            game_id=game_id,
            pitcher_out_lineup_id=lineup_ids['P'],
            pitcher_in_card_id=203,
            team_id=team_id,
            force_change=True
        )

        # Should succeed
        assert result.success


class TestSubstitutionStateSync:
    """Tests for state synchronization after substitutions"""

    async def test_multiple_substitutions_state_sync(self, setup_database, game_with_lineups):
        """Test state stays synced after multiple substitutions"""
        game_id, lineup_ids, bench_ids, team_id = game_with_lineups

        db_ops = DatabaseOperations()
        manager = SubstitutionManager(db_ops)

        # Substitution 1: Pinch hit
        result1 = await manager.pinch_hit(
            game_id=game_id,
            player_out_lineup_id=lineup_ids['CF'],
            player_in_card_id=201,
            team_id=team_id
        )
        assert result1.success

        # Substitution 2: Defensive replacement
        result2 = await manager.defensive_replace(
            game_id=game_id,
            player_out_lineup_id=lineup_ids['SS'],
            player_in_card_id=202,
            new_position='SS',
            team_id=team_id
        )
        assert result2.success

        # Verify both changes in database
        lineup_data = await db_ops.get_active_lineup(game_id, team_id)
        active_ids = [p.id for p in lineup_data]
        assert result1.new_lineup_id in active_ids
        assert result2.new_lineup_id in active_ids
        assert lineup_ids['CF'] not in active_ids
        assert lineup_ids['SS'] not in active_ids

        # Verify both changes in state cache
        lineup_state = state_manager.get_lineup(game_id, team_id)
        player1 = lineup_state.get_player_by_lineup_id(result1.new_lineup_id)
        player2 = lineup_state.get_player_by_lineup_id(result2.new_lineup_id)
        assert player1 is not None and player1.is_active
        assert player2 is not None and player2.is_active

    async def test_substitution_batting_order_preserved(self, setup_database, game_with_lineups):
        """Test substitutions preserve batting order"""
        game_id, lineup_ids, bench_ids, team_id = game_with_lineups

        db_ops = DatabaseOperations()
        manager = SubstitutionManager(db_ops)

        # Get original batting order
        lineup_state = state_manager.get_lineup(game_id, team_id)
        cf_player = lineup_state.get_player_by_lineup_id(lineup_ids['CF'])
        original_batting_order = cf_player.batting_order

        # Pinch hit for CF
        result = await manager.pinch_hit(
            game_id=game_id,
            player_out_lineup_id=lineup_ids['CF'],
            player_in_card_id=201,
            team_id=team_id
        )

        assert result.success
        assert result.new_batting_order == original_batting_order

        # Verify in database (query directly)
        from app.database.session import AsyncSessionLocal
        from app.models.db_models import Lineup
        from sqlalchemy import select

        async with AsyncSessionLocal() as session:
            new_result = await session.execute(
                select(Lineup).where(Lineup.id == result.new_lineup_id)
            )
            new_player = new_result.scalar_one()
            assert new_player.batting_order == original_batting_order

        # Verify in state cache
        lineup_state = state_manager.get_lineup(game_id, team_id)
        new_player_state = lineup_state.get_player_by_lineup_id(result.new_lineup_id)
        assert new_player_state.batting_order == original_batting_order
