"""
Unit tests for CombatService.

Tests combat lifecycle, action execution, and reward distribution.
Uses mocked dependencies to isolate combat logic testing.
"""

import pytest
from unittest.mock import Mock, MagicMock, patch
from uuid import uuid4

from app.models.combat import Combatant, CombatEncounter
from app.models.character import Character
from app.models.stats import Stats
from app.models.enemy import EnemyTemplate, EnemyDifficulty
from app.models.enums import CombatStatus, AbilityType, DamageType
from app.models.abilities import Ability
from app.services.combat_service import (
    CombatService,
    CombatAction,
    ActionResult,
    CombatRewards,
    NotInCombatError,
    AlreadyInCombatError,
    InvalidActionError,
)


# =============================================================================
# Test Fixtures
# =============================================================================

@pytest.fixture
def mock_stats():
    """Create mock stats for testing."""
    return Stats(
        strength=12,
        dexterity=10,
        constitution=14,
        intelligence=10,
        wisdom=10,
        charisma=8,
        luck=8,
    )


@pytest.fixture
def mock_character(mock_stats):
    """Create a mock character for testing."""
    char = Mock(spec=Character)
    char.character_id = "test_char_001"
    char.name = "Test Hero"
    char.user_id = "test_user"
    char.level = 5
    char.experience = 1000
    char.gold = 100
    char.unlocked_skills = ["power_strike"]
    char.equipped = {}  # No equipment by default
    char.get_effective_stats = Mock(return_value=mock_stats)
    return char


@pytest.fixture
def mock_enemy_template():
    """Create a mock enemy template."""
    return EnemyTemplate(
        enemy_id="test_goblin",
        name="Test Goblin",
        description="A test goblin",
        base_stats=Stats(
            strength=8,
            dexterity=12,
            constitution=6,
            intelligence=6,
            wisdom=6,
            charisma=4,
            luck=8,
        ),
        abilities=["basic_attack"],
        experience_reward=15,
        gold_reward_min=2,
        gold_reward_max=8,
        difficulty=EnemyDifficulty.EASY,
        tags=["humanoid", "goblinoid"],
        base_damage=4,
    )


@pytest.fixture
def mock_combatant():
    """Create a mock player combatant."""
    return Combatant(
        combatant_id="test_char_001",
        name="Test Hero",
        is_player=True,
        current_hp=38,  # 10 + 14*2
        max_hp=38,
        current_mp=30,  # 10 + 10*2
        max_mp=30,
        stats=Stats(
            strength=12,
            dexterity=10,
            constitution=14,
            intelligence=10,
            wisdom=10,
            charisma=8,
            luck=8,
        ),
        abilities=["basic_attack", "power_strike"],
    )


@pytest.fixture
def mock_enemy_combatant():
    """Create a mock enemy combatant."""
    return Combatant(
        combatant_id="test_goblin_0",
        name="Test Goblin",
        is_player=False,
        current_hp=22,  # 10 + 6*2
        max_hp=22,
        current_mp=22,
        max_mp=22,
        stats=Stats(
            strength=8,
            dexterity=12,
            constitution=6,
            intelligence=6,
            wisdom=6,
            charisma=4,
            luck=8,
        ),
        abilities=["basic_attack"],
    )


@pytest.fixture
def mock_encounter(mock_combatant, mock_enemy_combatant):
    """Create a mock combat encounter."""
    encounter = CombatEncounter(
        encounter_id="test_encounter_001",
        combatants=[mock_combatant, mock_enemy_combatant],
    )
    encounter.initialize_combat()
    return encounter


@pytest.fixture
def mock_session(mock_encounter):
    """Create a mock game session."""
    session = Mock()
    session.session_id = "test_session_001"
    session.solo_character_id = "test_char_001"
    session.is_solo = Mock(return_value=True)
    session.is_in_combat = Mock(return_value=False)
    session.combat_encounter = None
    session.start_combat = Mock()
    session.end_combat = Mock()
    return session


# =============================================================================
# CombatAction Tests
# =============================================================================

class TestCombatAction:
    """Tests for CombatAction dataclass."""

    def test_create_attack_action(self):
        """Test creating an attack action."""
        action = CombatAction(
            action_type="attack",
            target_ids=["enemy_1"],
        )

        assert action.action_type == "attack"
        assert action.target_ids == ["enemy_1"]
        assert action.ability_id is None

    def test_create_ability_action(self):
        """Test creating an ability action."""
        action = CombatAction(
            action_type="ability",
            target_ids=["enemy_1", "enemy_2"],
            ability_id="fireball",
        )

        assert action.action_type == "ability"
        assert action.ability_id == "fireball"
        assert len(action.target_ids) == 2

    def test_from_dict(self):
        """Test creating action from dictionary."""
        data = {
            "action_type": "ability",
            "target_ids": ["enemy_1"],
            "ability_id": "heal",
        }

        action = CombatAction.from_dict(data)

        assert action.action_type == "ability"
        assert action.ability_id == "heal"

    def test_to_dict(self):
        """Test serializing action to dictionary."""
        action = CombatAction(
            action_type="defend",
            target_ids=[],
        )

        data = action.to_dict()

        assert data["action_type"] == "defend"
        assert data["target_ids"] == []


# =============================================================================
# ActionResult Tests
# =============================================================================

class TestActionResult:
    """Tests for ActionResult dataclass."""

    def test_create_success_result(self):
        """Test creating a successful action result."""
        result = ActionResult(
            success=True,
            message="Attack hits for 15 damage!",
        )

        assert result.success is True
        assert "15 damage" in result.message
        assert result.combat_ended is False

    def test_to_dict(self):
        """Test serializing result to dictionary."""
        result = ActionResult(
            success=True,
            message="Victory!",
            combat_ended=True,
            combat_status=CombatStatus.VICTORY,
        )

        data = result.to_dict()

        assert data["success"] is True
        assert data["combat_ended"] is True
        assert data["combat_status"] == "victory"


# =============================================================================
# CombatRewards Tests
# =============================================================================

class TestCombatRewards:
    """Tests for CombatRewards dataclass."""

    def test_create_rewards(self):
        """Test creating combat rewards."""
        rewards = CombatRewards(
            experience=100,
            gold=50,
            items=[{"item_id": "sword", "quantity": 1}],
            level_ups=["char_1"],
        )

        assert rewards.experience == 100
        assert rewards.gold == 50
        assert len(rewards.items) == 1

    def test_to_dict(self):
        """Test serializing rewards to dictionary."""
        rewards = CombatRewards(experience=50, gold=25)
        data = rewards.to_dict()

        assert data["experience"] == 50
        assert data["gold"] == 25
        assert data["items"] == []


# =============================================================================
# Combatant Creation Tests
# =============================================================================

class TestCombatantCreation:
    """Tests for combatant creation methods."""

    def test_create_combatant_from_character(self, mock_character):
        """Test creating a combatant from a player character."""
        service = CombatService.__new__(CombatService)

        combatant = service._create_combatant_from_character(mock_character)

        assert combatant.combatant_id == mock_character.character_id
        assert combatant.name == mock_character.name
        assert combatant.is_player is True
        assert combatant.current_hp == combatant.max_hp
        assert "basic_attack" in combatant.abilities

    def test_create_combatant_from_enemy(self, mock_enemy_template):
        """Test creating a combatant from an enemy template."""
        service = CombatService.__new__(CombatService)

        combatant = service._create_combatant_from_enemy(mock_enemy_template, instance_index=0)

        assert combatant.combatant_id == "test_goblin_0"
        assert combatant.name == mock_enemy_template.name
        assert combatant.is_player is False
        assert combatant.current_hp == combatant.max_hp
        assert "basic_attack" in combatant.abilities

    def test_create_multiple_enemy_instances(self, mock_enemy_template):
        """Test creating multiple instances of same enemy."""
        service = CombatService.__new__(CombatService)

        combatant1 = service._create_combatant_from_enemy(mock_enemy_template, instance_index=0)
        combatant2 = service._create_combatant_from_enemy(mock_enemy_template, instance_index=1)
        combatant3 = service._create_combatant_from_enemy(mock_enemy_template, instance_index=2)

        # IDs should be unique
        assert combatant1.combatant_id != combatant2.combatant_id
        assert combatant2.combatant_id != combatant3.combatant_id

        # Names should be numbered
        assert "#" in combatant2.name
        assert "#" in combatant3.name


# =============================================================================
# Combat Lifecycle Tests
# =============================================================================

class TestCombatLifecycle:
    """Tests for combat lifecycle methods."""

    @patch('app.services.combat_service.get_session_service')
    @patch('app.services.combat_service.get_character_service')
    @patch('app.services.combat_service.get_enemy_loader')
    def test_start_combat_success(
        self,
        mock_get_enemy_loader,
        mock_get_char_service,
        mock_get_session_service,
        mock_session,
        mock_character,
        mock_enemy_template,
    ):
        """Test starting combat successfully."""
        # Setup mocks
        mock_session_service = Mock()
        mock_session_service.get_session.return_value = mock_session
        mock_session_service.update_session = Mock()
        mock_get_session_service.return_value = mock_session_service

        mock_char_service = Mock()
        mock_char_service.get_character.return_value = mock_character
        mock_get_char_service.return_value = mock_char_service

        mock_enemy_loader = Mock()
        mock_enemy_loader.load_enemy.return_value = mock_enemy_template
        mock_get_enemy_loader.return_value = mock_enemy_loader

        # Create service and start combat
        service = CombatService()
        encounter = service.start_combat(
            session_id="test_session",
            user_id="test_user",
            enemy_ids=["test_goblin"],
        )

        assert encounter is not None
        assert encounter.status == CombatStatus.ACTIVE
        assert len(encounter.combatants) == 2  # 1 player + 1 enemy
        assert len(encounter.turn_order) == 2
        mock_session.start_combat.assert_called_once()

    @patch('app.services.combat_service.get_session_service')
    @patch('app.services.combat_service.get_character_service')
    @patch('app.services.combat_service.get_enemy_loader')
    def test_start_combat_already_in_combat(
        self,
        mock_get_enemy_loader,
        mock_get_char_service,
        mock_get_session_service,
        mock_session,
    ):
        """Test starting combat when already in combat."""
        mock_session.is_in_combat.return_value = True

        mock_session_service = Mock()
        mock_session_service.get_session.return_value = mock_session
        mock_get_session_service.return_value = mock_session_service

        service = CombatService()

        with pytest.raises(AlreadyInCombatError):
            service.start_combat(
                session_id="test_session",
                user_id="test_user",
                enemy_ids=["goblin"],
            )

    @patch('app.services.combat_service.get_session_service')
    def test_get_combat_state_not_in_combat(
        self,
        mock_get_session_service,
        mock_session,
    ):
        """Test getting combat state when not in combat."""
        mock_session.combat_encounter = None

        mock_session_service = Mock()
        mock_session_service.get_session.return_value = mock_session
        mock_get_session_service.return_value = mock_session_service

        service = CombatService()
        result = service.get_combat_state("test_session", "test_user")

        assert result is None


# =============================================================================
# Attack Execution Tests
# =============================================================================

class TestAttackExecution:
    """Tests for attack action execution."""

    def test_execute_attack_hit(self, mock_encounter, mock_combatant, mock_enemy_combatant):
        """Test executing a successful attack."""
        service = CombatService.__new__(CombatService)
        service.ability_loader = Mock()

        # Mock attacker as current combatant
        mock_encounter.turn_order = [mock_combatant.combatant_id, mock_enemy_combatant.combatant_id]
        mock_encounter.current_turn_index = 0

        result = service._execute_attack(
            mock_encounter,
            mock_combatant,
            [mock_enemy_combatant.combatant_id]
        )

        assert result.success is True
        assert len(result.damage_results) == 1
        # Damage should have been dealt (HP should be reduced)

    def test_execute_attack_no_target(self, mock_encounter, mock_combatant):
        """Test attack with auto-targeting."""
        service = CombatService.__new__(CombatService)
        service.ability_loader = Mock()

        result = service._execute_attack(
            mock_encounter,
            mock_combatant,
            []  # No targets specified
        )

        # Should auto-target and succeed
        assert result.success is True


# =============================================================================
# Defend Action Tests
# =============================================================================

class TestDefendExecution:
    """Tests for defend action execution."""

    def test_execute_defend(self, mock_encounter, mock_combatant):
        """Test executing a defend action."""
        service = CombatService.__new__(CombatService)

        initial_effects = len(mock_combatant.active_effects)

        result = service._execute_defend(mock_encounter, mock_combatant)

        assert result.success is True
        assert "defensive stance" in result.message.lower()
        assert len(result.effects_applied) == 1
        # Combatant should have a new effect
        assert len(mock_combatant.active_effects) == initial_effects + 1


# =============================================================================
# Flee Action Tests
# =============================================================================

class TestFleeExecution:
    """Tests for flee action execution."""

    def test_execute_flee_success(self, mock_encounter, mock_combatant, mock_session):
        """Test successful flee attempt."""
        service = CombatService.__new__(CombatService)

        # Force success by patching random
        with patch('random.random', return_value=0.1):  # Low roll = success
            result = service._execute_flee(
                mock_encounter,
                mock_combatant,
                mock_session,
                "test_user"
            )

        assert result.success is True
        assert result.combat_ended is True
        assert result.combat_status == CombatStatus.FLED

    def test_execute_flee_failure(self, mock_encounter, mock_combatant, mock_session):
        """Test failed flee attempt."""
        service = CombatService.__new__(CombatService)

        # Force failure by patching random
        with patch('random.random', return_value=0.9):  # High roll = failure
            result = service._execute_flee(
                mock_encounter,
                mock_combatant,
                mock_session,
                "test_user"
            )

        assert result.success is False
        assert result.combat_ended is False


# =============================================================================
# Enemy AI Tests
# =============================================================================

class TestEnemyAI:
    """Tests for enemy AI logic."""

    def test_choose_enemy_action(self, mock_encounter, mock_enemy_combatant):
        """Test enemy AI action selection."""
        service = CombatService.__new__(CombatService)

        action_type, targets = service._choose_enemy_action(
            mock_encounter,
            mock_enemy_combatant
        )

        # Should choose attack or ability
        assert action_type in ["attack", "ability"]
        # Should target a player
        assert len(targets) > 0

    def test_choose_enemy_targets_lowest_hp(self, mock_encounter, mock_enemy_combatant):
        """Test that enemy AI targets lowest HP player."""
        # Add another player with lower HP
        low_hp_player = Combatant(
            combatant_id="low_hp_player",
            name="Wounded Hero",
            is_player=True,
            current_hp=5,  # Very low HP
            max_hp=38,
            current_mp=30,
            max_mp=30,
            stats=Stats(),
            abilities=["basic_attack"],
        )
        mock_encounter.combatants.append(low_hp_player)

        service = CombatService.__new__(CombatService)

        _, targets = service._choose_enemy_action(
            mock_encounter,
            mock_enemy_combatant
        )

        # Should target the lowest HP player
        assert targets[0] == "low_hp_player"


# =============================================================================
# Combat End Condition Tests
# =============================================================================

class TestCombatEndConditions:
    """Tests for combat end condition checking."""

    def test_victory_when_all_enemies_dead(self, mock_encounter, mock_combatant, mock_enemy_combatant):
        """Test victory is detected when all enemies are dead."""
        # Kill the enemy
        mock_enemy_combatant.current_hp = 0

        status = mock_encounter.check_end_condition()

        assert status == CombatStatus.VICTORY

    def test_defeat_when_all_players_dead(self, mock_encounter, mock_combatant, mock_enemy_combatant):
        """Test defeat is detected when all players are dead."""
        # Kill the player
        mock_combatant.current_hp = 0

        status = mock_encounter.check_end_condition()

        assert status == CombatStatus.DEFEAT

    def test_active_when_both_alive(self, mock_encounter, mock_combatant, mock_enemy_combatant):
        """Test combat remains active when both sides have survivors."""
        # Both alive
        assert mock_combatant.current_hp > 0
        assert mock_enemy_combatant.current_hp > 0

        status = mock_encounter.check_end_condition()

        assert status == CombatStatus.ACTIVE


# =============================================================================
# Rewards Calculation Tests
# =============================================================================

class TestRewardsCalculation:
    """Tests for reward distribution."""

    def test_calculate_rewards_from_enemies(self, mock_encounter, mock_enemy_combatant):
        """Test reward calculation from defeated enemies."""
        # Mark enemy as dead
        mock_enemy_combatant.current_hp = 0

        service = CombatService.__new__(CombatService)
        service.enemy_loader = Mock()
        service.character_service = Mock()
        service.loot_service = Mock()

        # Mock enemy template for rewards
        mock_template = Mock()
        mock_template.experience_reward = 50
        mock_template.get_gold_reward.return_value = 25
        mock_template.difficulty = Mock()
        mock_template.difficulty.value = "easy"
        mock_template.is_boss.return_value = False
        service.enemy_loader.load_enemy.return_value = mock_template

        # Mock loot service to return mock items
        mock_item = Mock()
        mock_item.to_dict.return_value = {"item_id": "sword", "quantity": 1}
        service.loot_service.generate_loot_from_enemy.return_value = [mock_item]

        mock_session = Mock()
        mock_session.is_solo.return_value = True
        mock_session.solo_character_id = "test_char"

        mock_char = Mock()
        mock_char.level = 1
        mock_char.experience = 0
        mock_char.gold = 0
        service.character_service.get_character.return_value = mock_char
        service.character_service.update_character = Mock()

        rewards = service._calculate_rewards(mock_encounter, mock_session, "test_user")

        assert rewards.experience == 50
        assert rewards.gold == 25
        assert len(rewards.items) == 1
