"""
Unit tests for the DamageCalculator service.

Tests cover:
- Hit chance calculations with LUK/DEX
- Critical hit chance calculations
- Damage variance with lucky rolls
- Physical damage formula
- Magical damage formula
- Elemental split damage
- Defense mitigation with minimum guarantee
- AoE damage calculations
"""

import pytest
import random
from unittest.mock import patch

from app.models.stats import Stats
from app.models.enums import DamageType
from app.services.damage_calculator import (
    DamageCalculator,
    DamageResult,
    CombatConstants,
)


# =============================================================================
# Hit Chance Tests
# =============================================================================

class TestHitChance:
    """Tests for calculate_hit_chance()."""

    def test_base_hit_chance_with_average_stats(self):
        """Test hit chance with average LUK (8) and DEX (10)."""
        # LUK 8: miss = 10% - 4% = 6%
        hit_chance = DamageCalculator.calculate_hit_chance(
            attacker_luck=8,
            defender_dexterity=10,
        )
        assert hit_chance == pytest.approx(0.94, abs=0.001)

    def test_high_luck_reduces_miss_chance(self):
        """Test that high LUK reduces miss chance."""
        # LUK 12: miss = 10% - 6% = 4%, but capped at 5%
        hit_chance = DamageCalculator.calculate_hit_chance(
            attacker_luck=12,
            defender_dexterity=10,
        )
        assert hit_chance == pytest.approx(0.95, abs=0.001)

    def test_miss_chance_hard_cap_at_five_percent(self):
        """Test that miss chance cannot go below 5% (hard cap)."""
        # LUK 20: would be 10% - 10% = 0%, but capped at 5%
        hit_chance = DamageCalculator.calculate_hit_chance(
            attacker_luck=20,
            defender_dexterity=10,
        )
        assert hit_chance == pytest.approx(0.95, abs=0.001)

    def test_high_dex_increases_evasion(self):
        """Test that defender's high DEX increases miss chance."""
        # LUK 8, DEX 15: miss = 10% - 4% + 1.25% = 7.25%
        hit_chance = DamageCalculator.calculate_hit_chance(
            attacker_luck=8,
            defender_dexterity=15,
        )
        assert hit_chance == pytest.approx(0.9275, abs=0.001)

    def test_dex_below_ten_has_no_evasion_bonus(self):
        """Test that DEX below 10 doesn't reduce attacker's hit chance."""
        # DEX 5 should be same as DEX 10 (no negative evasion)
        hit_low_dex = DamageCalculator.calculate_hit_chance(
            attacker_luck=8,
            defender_dexterity=5,
        )
        hit_base_dex = DamageCalculator.calculate_hit_chance(
            attacker_luck=8,
            defender_dexterity=10,
        )
        assert hit_low_dex == hit_base_dex

    def test_skill_bonus_improves_hit_chance(self):
        """Test that skill bonus adds to hit chance."""
        base_hit = DamageCalculator.calculate_hit_chance(
            attacker_luck=8,
            defender_dexterity=10,
        )
        skill_hit = DamageCalculator.calculate_hit_chance(
            attacker_luck=8,
            defender_dexterity=10,
            skill_bonus=0.05,  # 5% bonus
        )
        assert skill_hit > base_hit


# =============================================================================
# Critical Hit Tests
# =============================================================================

class TestCritChance:
    """Tests for calculate_crit_chance()."""

    def test_base_crit_with_average_luck(self):
        """Test crit chance with average LUK (8)."""
        # Base 5% + LUK 8 * 0.5% = 5% + 4% = 9%
        crit_chance = DamageCalculator.calculate_crit_chance(
            attacker_luck=8,
        )
        assert crit_chance == pytest.approx(0.09, abs=0.001)

    def test_high_luck_increases_crit(self):
        """Test that high LUK increases crit chance."""
        # Base 5% + LUK 12 * 0.5% = 5% + 6% = 11%
        crit_chance = DamageCalculator.calculate_crit_chance(
            attacker_luck=12,
        )
        assert crit_chance == pytest.approx(0.11, abs=0.001)

    def test_weapon_crit_stacks_with_luck(self):
        """Test that weapon crit chance stacks with LUK bonus."""
        # Weapon 10% + LUK 12 * 0.5% = 10% + 6% = 16%
        crit_chance = DamageCalculator.calculate_crit_chance(
            attacker_luck=12,
            weapon_crit_chance=0.10,
        )
        assert crit_chance == pytest.approx(0.16, abs=0.001)

    def test_crit_chance_hard_cap_at_25_percent(self):
        """Test that crit chance is capped at 25%."""
        # Weapon 20% + LUK 20 * 0.5% = 20% + 10% = 30%, but capped at 25%
        crit_chance = DamageCalculator.calculate_crit_chance(
            attacker_luck=20,
            weapon_crit_chance=0.20,
        )
        assert crit_chance == pytest.approx(0.25, abs=0.001)

    def test_skill_bonus_adds_to_crit(self):
        """Test that skill bonus adds to crit chance."""
        base_crit = DamageCalculator.calculate_crit_chance(
            attacker_luck=8,
        )
        skill_crit = DamageCalculator.calculate_crit_chance(
            attacker_luck=8,
            skill_bonus=0.05,
        )
        assert skill_crit == base_crit + 0.05


# =============================================================================
# Damage Variance Tests
# =============================================================================

class TestDamageVariance:
    """Tests for calculate_variance()."""

    @patch('random.random')
    @patch('random.uniform')
    def test_normal_variance_roll(self, mock_uniform, mock_random):
        """Test normal variance roll (95%-105%)."""
        # Not a lucky roll (random returns high value)
        mock_random.return_value = 0.99
        mock_uniform.return_value = 1.0

        variance = DamageCalculator.calculate_variance(attacker_luck=8)

        # Should call uniform with base variance range
        mock_uniform.assert_called_with(
            CombatConstants.BASE_VARIANCE_MIN,
            CombatConstants.BASE_VARIANCE_MAX,
        )
        assert variance == 1.0

    @patch('random.random')
    @patch('random.uniform')
    def test_lucky_variance_roll(self, mock_uniform, mock_random):
        """Test lucky variance roll (100%-110%)."""
        # Lucky roll (random returns low value)
        mock_random.return_value = 0.01
        mock_uniform.return_value = 1.08

        variance = DamageCalculator.calculate_variance(attacker_luck=8)

        # Should call uniform with lucky variance range
        mock_uniform.assert_called_with(
            CombatConstants.LUCKY_VARIANCE_MIN,
            CombatConstants.LUCKY_VARIANCE_MAX,
        )
        assert variance == 1.08

    def test_high_luck_increases_lucky_chance(self):
        """Test that high LUK increases chance for lucky roll."""
        # LUK 8: lucky chance = 5% + 2% = 7%
        # LUK 12: lucky chance = 5% + 3% = 8%
        # Run many iterations to verify probability
        lucky_count_low = 0
        lucky_count_high = 0
        iterations = 10000

        random.seed(42)  # Reproducible
        for _ in range(iterations):
            variance = DamageCalculator.calculate_variance(8)
            if variance >= 1.0:
                lucky_count_low += 1

        random.seed(42)  # Same seed
        for _ in range(iterations):
            variance = DamageCalculator.calculate_variance(12)
            if variance >= 1.0:
                lucky_count_high += 1

        # Higher LUK should have more lucky rolls
        # Note: This is a statistical test, might have some variance
        # Just verify the high LUK isn't dramatically lower
        assert lucky_count_high >= lucky_count_low * 0.9


# =============================================================================
# Defense Mitigation Tests
# =============================================================================

class TestDefenseMitigation:
    """Tests for apply_defense()."""

    def test_normal_defense_mitigation(self):
        """Test standard defense subtraction."""
        # 20 damage - 5 defense = 15 damage
        result = DamageCalculator.apply_defense(raw_damage=20, defense=5)
        assert result == 15

    def test_minimum_damage_guarantee(self):
        """Test that minimum 20% damage always goes through."""
        # 20 damage - 18 defense = 2 damage (but min is 20% of 20 = 4)
        result = DamageCalculator.apply_defense(raw_damage=20, defense=18)
        assert result == 4

    def test_defense_higher_than_damage(self):
        """Test when defense exceeds raw damage."""
        # 10 damage - 100 defense = -90, but min is 20% of 10 = 2
        result = DamageCalculator.apply_defense(raw_damage=10, defense=100)
        assert result == 2

    def test_absolute_minimum_damage_is_one(self):
        """Test that absolute minimum damage is 1."""
        # 3 damage - 100 defense = negative, but 20% of 3 = 0.6, so min is 1
        result = DamageCalculator.apply_defense(raw_damage=3, defense=100)
        assert result == 1

    def test_custom_minimum_ratio(self):
        """Test custom minimum damage ratio."""
        # 20 damage with 30% minimum = at least 6 damage
        result = DamageCalculator.apply_defense(
            raw_damage=20,
            defense=18,
            min_damage_ratio=0.30,
        )
        assert result == 6


# =============================================================================
# Physical Damage Tests
# =============================================================================

class TestPhysicalDamage:
    """Tests for calculate_physical_damage()."""

    def test_basic_physical_damage_formula(self):
        """Test the basic physical damage formula."""
        # Formula: (Weapon + STR * 0.75) * Variance - DEF
        attacker = Stats(strength=14, luck=0)  # LUK 0 to ensure no miss
        defender = Stats(constitution=10, dexterity=10)  # DEF = 5

        # Mock to ensure no miss and no crit, variance = 1.0
        with patch.object(DamageCalculator, 'calculate_hit_chance', return_value=1.0):
            with patch.object(DamageCalculator, 'calculate_variance', return_value=1.0):
                with patch.object(DamageCalculator, 'calculate_crit_chance', return_value=0.0):
                    result = DamageCalculator.calculate_physical_damage(
                        attacker_stats=attacker,
                        defender_stats=defender,
                        weapon_damage=8,
                    )

        # 8 + (14 * 0.75) = 8 + 10.5 = 18.5 -> 18 - 5 = 13
        assert result.total_damage == 13
        assert result.is_miss is False
        assert result.is_critical is False
        assert result.damage_type == DamageType.PHYSICAL

    def test_physical_damage_miss(self):
        """Test that misses deal zero damage."""
        attacker = Stats(strength=14, luck=0)
        defender = Stats(dexterity=30)  # Very high DEX

        # Force a miss
        with patch('random.random', return_value=0.99):
            result = DamageCalculator.calculate_physical_damage(
                attacker_stats=attacker,
                defender_stats=defender,
                weapon_damage=8,
            )

        assert result.is_miss is True
        assert result.total_damage == 0
        assert "missed" in result.message.lower()

    def test_physical_damage_critical_hit(self):
        """Test critical hit doubles damage."""
        attacker = Stats(strength=14, luck=20)  # High LUK for crit
        defender = Stats(constitution=10, dexterity=10)

        # Force hit and crit
        with patch('random.random', side_effect=[0.01, 0.01]):  # Hit, then crit
            with patch.object(DamageCalculator, 'calculate_variance', return_value=1.0):
                result = DamageCalculator.calculate_physical_damage(
                    attacker_stats=attacker,
                    defender_stats=defender,
                    weapon_damage=8,
                    weapon_crit_multiplier=2.0,
                )

        assert result.is_critical is True
        # Base: 8 + 14*0.75 = 18.5
        # Crit applied BEFORE int conversion: 18.5 * 2 = 37
        # After DEF 5: 37 - 5 = 32
        assert result.total_damage == 32
        assert "critical" in result.message.lower()


# =============================================================================
# Magical Damage Tests
# =============================================================================

class TestMagicalDamage:
    """Tests for calculate_magical_damage()."""

    def test_basic_magical_damage_formula(self):
        """Test the basic magical damage formula."""
        # Formula: (Ability + INT * 0.75) * Variance - RES
        attacker = Stats(intelligence=15, luck=0)
        defender = Stats(wisdom=10, dexterity=10)  # RES = 5

        with patch.object(DamageCalculator, 'calculate_hit_chance', return_value=1.0):
            with patch.object(DamageCalculator, 'calculate_variance', return_value=1.0):
                with patch.object(DamageCalculator, 'calculate_crit_chance', return_value=0.0):
                    result = DamageCalculator.calculate_magical_damage(
                        attacker_stats=attacker,
                        defender_stats=defender,
                        ability_base_power=12,
                        damage_type=DamageType.FIRE,
                    )

        # 12 + (15 * 0.75) = 12 + 11.25 = 23.25 -> 23 - 5 = 18
        assert result.total_damage == 18
        assert result.damage_type == DamageType.FIRE
        assert result.is_miss is False

    def test_spells_can_critically_hit(self):
        """Test that spells can crit (per user requirement)."""
        attacker = Stats(intelligence=15, luck=20)
        defender = Stats(wisdom=10, dexterity=10)

        # Force hit and crit
        with patch('random.random', side_effect=[0.01, 0.01]):
            with patch.object(DamageCalculator, 'calculate_variance', return_value=1.0):
                result = DamageCalculator.calculate_magical_damage(
                    attacker_stats=attacker,
                    defender_stats=defender,
                    ability_base_power=12,
                    damage_type=DamageType.FIRE,
                    weapon_crit_multiplier=2.0,
                )

        assert result.is_critical is True
        # Base: 12 + 15*0.75 = 23.25 -> 23
        # Crit: 23 * 2 = 46
        # After RES 5: 46 - 5 = 41
        assert result.total_damage == 41

    def test_magical_damage_with_different_types(self):
        """Test that different damage types are recorded correctly."""
        attacker = Stats(intelligence=10)
        defender = Stats(wisdom=10, dexterity=10)

        with patch.object(DamageCalculator, 'calculate_hit_chance', return_value=1.0):
            with patch.object(DamageCalculator, 'calculate_variance', return_value=1.0):
                with patch.object(DamageCalculator, 'calculate_crit_chance', return_value=0.0):
                    for damage_type in [DamageType.ICE, DamageType.LIGHTNING, DamageType.HOLY]:
                        result = DamageCalculator.calculate_magical_damage(
                            attacker_stats=attacker,
                            defender_stats=defender,
                            ability_base_power=10,
                            damage_type=damage_type,
                        )
                        assert result.damage_type == damage_type


# =============================================================================
# Elemental Weapon (Split Damage) Tests
# =============================================================================

class TestElementalWeaponDamage:
    """Tests for calculate_elemental_weapon_damage()."""

    def test_split_damage_calculation(self):
        """Test 70/30 physical/fire split damage."""
        # Fire Sword: 70% physical, 30% fire
        attacker = Stats(strength=14, intelligence=8, luck=0)
        defender = Stats(constitution=10, wisdom=10, dexterity=10)  # DEF=5, RES=5

        with patch.object(DamageCalculator, 'calculate_hit_chance', return_value=1.0):
            with patch.object(DamageCalculator, 'calculate_variance', return_value=1.0):
                with patch.object(DamageCalculator, 'calculate_crit_chance', return_value=0.0):
                    result = DamageCalculator.calculate_elemental_weapon_damage(
                        attacker_stats=attacker,
                        defender_stats=defender,
                        weapon_damage=15,
                        weapon_crit_chance=0.05,
                        weapon_crit_multiplier=2.0,
                        physical_ratio=0.7,
                        elemental_ratio=0.3,
                        elemental_type=DamageType.FIRE,
                    )

        # Physical: (15 * 0.7) + (14 * 0.75 * 0.7) - 5 = 10.5 + 7.35 - 5 = 12.85 -> 12
        # Elemental: (15 * 0.3) + (8 * 0.75 * 0.3) - 5 = 4.5 + 1.8 - 5 = 1.3 -> 1
        # Total: 12 + 1 = 13 (approximately, depends on min damage)

        assert result.physical_damage > 0
        assert result.elemental_damage >= 1  # At least minimum damage
        assert result.total_damage == result.physical_damage + result.elemental_damage
        assert result.elemental_type == DamageType.FIRE

    def test_50_50_split_damage(self):
        """Test 50/50 physical/elemental split (Lightning Spear)."""
        attacker = Stats(strength=12, intelligence=12, luck=0)
        defender = Stats(constitution=10, wisdom=10, dexterity=10)

        with patch.object(DamageCalculator, 'calculate_hit_chance', return_value=1.0):
            with patch.object(DamageCalculator, 'calculate_variance', return_value=1.0):
                with patch.object(DamageCalculator, 'calculate_crit_chance', return_value=0.0):
                    result = DamageCalculator.calculate_elemental_weapon_damage(
                        attacker_stats=attacker,
                        defender_stats=defender,
                        weapon_damage=20,
                        weapon_crit_chance=0.05,
                        weapon_crit_multiplier=2.0,
                        physical_ratio=0.5,
                        elemental_ratio=0.5,
                        elemental_type=DamageType.LIGHTNING,
                    )

        # Both components should be similar (same stat values)
        assert abs(result.physical_damage - result.elemental_damage) <= 2

    def test_elemental_crit_applies_to_both_components(self):
        """Test that crit multiplier applies to both damage types."""
        attacker = Stats(strength=14, intelligence=8, luck=20)
        defender = Stats(constitution=10, wisdom=10, dexterity=10)

        # Force hit and crit
        with patch('random.random', side_effect=[0.01, 0.01]):
            with patch.object(DamageCalculator, 'calculate_variance', return_value=1.0):
                result = DamageCalculator.calculate_elemental_weapon_damage(
                    attacker_stats=attacker,
                    defender_stats=defender,
                    weapon_damage=15,
                    weapon_crit_chance=0.05,
                    weapon_crit_multiplier=2.0,
                    physical_ratio=0.7,
                    elemental_ratio=0.3,
                    elemental_type=DamageType.FIRE,
                )

        assert result.is_critical is True
        # Both components should be doubled


# =============================================================================
# AoE Damage Tests
# =============================================================================

class TestAoEDamage:
    """Tests for calculate_aoe_damage()."""

    def test_aoe_full_damage_to_all_targets(self):
        """Test that AoE deals full damage to each target."""
        attacker = Stats(intelligence=15, luck=0)
        defenders = [
            Stats(wisdom=10, dexterity=10),
            Stats(wisdom=10, dexterity=10),
            Stats(wisdom=10, dexterity=10),
        ]

        with patch.object(DamageCalculator, 'calculate_hit_chance', return_value=1.0):
            with patch.object(DamageCalculator, 'calculate_variance', return_value=1.0):
                with patch.object(DamageCalculator, 'calculate_crit_chance', return_value=0.0):
                    results = DamageCalculator.calculate_aoe_damage(
                        attacker_stats=attacker,
                        defender_stats_list=defenders,
                        ability_base_power=20,
                        damage_type=DamageType.FIRE,
                    )

        assert len(results) == 3
        # All targets should take the same damage (same stats)
        for result in results:
            assert result.total_damage == results[0].total_damage

    def test_aoe_independent_hit_checks(self):
        """Test that each target has independent hit/miss rolls."""
        attacker = Stats(intelligence=15, luck=8)
        defenders = [
            Stats(wisdom=10, dexterity=10),
            Stats(wisdom=10, dexterity=10),
        ]

        # First target hit, second target miss
        hit_sequence = [0.5, 0.99]  # Below hit chance, above hit chance
        with patch('random.random', side_effect=hit_sequence * 2):  # Extra for crit checks
            results = DamageCalculator.calculate_aoe_damage(
                attacker_stats=attacker,
                defender_stats_list=defenders,
                ability_base_power=20,
                damage_type=DamageType.FIRE,
            )

        # At least verify we got results for both
        assert len(results) == 2

    def test_aoe_with_varying_resistance(self):
        """Test that AoE respects different resistances per target."""
        attacker = Stats(intelligence=15, luck=0)
        defenders = [
            Stats(wisdom=10, dexterity=10),  # RES = 5
            Stats(wisdom=20, dexterity=10),  # RES = 10
        ]

        with patch.object(DamageCalculator, 'calculate_hit_chance', return_value=1.0):
            with patch.object(DamageCalculator, 'calculate_variance', return_value=1.0):
                with patch.object(DamageCalculator, 'calculate_crit_chance', return_value=0.0):
                    results = DamageCalculator.calculate_aoe_damage(
                        attacker_stats=attacker,
                        defender_stats_list=defenders,
                        ability_base_power=20,
                        damage_type=DamageType.FIRE,
                    )

        # First target (lower RES) should take more damage
        assert results[0].total_damage > results[1].total_damage


# =============================================================================
# DamageResult Tests
# =============================================================================

class TestDamageResult:
    """Tests for DamageResult dataclass."""

    def test_damage_result_to_dict(self):
        """Test serialization of DamageResult."""
        result = DamageResult(
            total_damage=25,
            physical_damage=25,
            elemental_damage=0,
            damage_type=DamageType.PHYSICAL,
            is_critical=True,
            is_miss=False,
            variance_roll=1.05,
            raw_damage=30,
            message="Dealt 25 physical damage. CRITICAL HIT!",
        )

        data = result.to_dict()

        assert data["total_damage"] == 25
        assert data["physical_damage"] == 25
        assert data["damage_type"] == "physical"
        assert data["is_critical"] is True
        assert data["is_miss"] is False
        assert data["variance_roll"] == pytest.approx(1.05, abs=0.001)


# =============================================================================
# Combat Constants Tests
# =============================================================================

class TestCombatConstants:
    """Tests for CombatConstants configuration."""

    def test_stat_scaling_factor(self):
        """Verify scaling factor is 0.75."""
        assert CombatConstants.STAT_SCALING_FACTOR == 0.75

    def test_miss_chance_hard_cap(self):
        """Verify miss chance hard cap is 5%."""
        assert CombatConstants.MIN_MISS_CHANCE == 0.05

    def test_crit_chance_cap(self):
        """Verify crit chance cap is 25%."""
        assert CombatConstants.MAX_CRIT_CHANCE == 0.25

    def test_minimum_damage_ratio(self):
        """Verify minimum damage ratio is 20%."""
        assert CombatConstants.MIN_DAMAGE_RATIO == 0.20


# =============================================================================
# Integration Tests (Full Combat Flow)
# =============================================================================

class TestCombatIntegration:
    """Integration tests for complete combat scenarios."""

    def test_vanguard_attack_scenario(self):
        """Test Vanguard (STR 14) basic attack."""
        # Vanguard: STR 14, LUK 8
        vanguard = Stats(strength=14, dexterity=10, constitution=14, luck=8)
        goblin = Stats(constitution=10, dexterity=10)  # DEF = 5

        with patch.object(DamageCalculator, 'calculate_hit_chance', return_value=1.0):
            with patch.object(DamageCalculator, 'calculate_variance', return_value=1.0):
                with patch.object(DamageCalculator, 'calculate_crit_chance', return_value=0.0):
                    result = DamageCalculator.calculate_physical_damage(
                        attacker_stats=vanguard,
                        defender_stats=goblin,
                        weapon_damage=8,  # Rusty sword
                    )

        # 8 + (14 * 0.75) = 18.5 -> 18 - 5 = 13
        assert result.total_damage == 13

    def test_arcanist_fireball_scenario(self):
        """Test Arcanist (INT 15) Fireball."""
        # Arcanist: INT 15, LUK 9
        arcanist = Stats(intelligence=15, dexterity=10, wisdom=12, luck=9)
        goblin = Stats(wisdom=10, dexterity=10)  # RES = 5

        with patch.object(DamageCalculator, 'calculate_hit_chance', return_value=1.0):
            with patch.object(DamageCalculator, 'calculate_variance', return_value=1.0):
                with patch.object(DamageCalculator, 'calculate_crit_chance', return_value=0.0):
                    result = DamageCalculator.calculate_magical_damage(
                        attacker_stats=arcanist,
                        defender_stats=goblin,
                        ability_base_power=12,  # Fireball base
                        damage_type=DamageType.FIRE,
                    )

        # 12 + (15 * 0.75) = 23.25 -> 23 - 5 = 18
        assert result.total_damage == 18

    def test_physical_vs_magical_balance(self):
        """Test that physical and magical damage are comparable."""
        # Same-tier characters should deal similar damage
        vanguard = Stats(strength=14, luck=8)  # Melee
        arcanist = Stats(intelligence=15, luck=9)  # Caster
        target = Stats(constitution=10, wisdom=10, dexterity=10)  # DEF=5, RES=5

        with patch.object(DamageCalculator, 'calculate_hit_chance', return_value=1.0):
            with patch.object(DamageCalculator, 'calculate_variance', return_value=1.0):
                with patch.object(DamageCalculator, 'calculate_crit_chance', return_value=0.0):
                    phys_result = DamageCalculator.calculate_physical_damage(
                        attacker_stats=vanguard,
                        defender_stats=target,
                        weapon_damage=8,
                    )
                    magic_result = DamageCalculator.calculate_magical_damage(
                        attacker_stats=arcanist,
                        defender_stats=target,
                        ability_base_power=12,
                        damage_type=DamageType.FIRE,
                    )

        # Mage should deal slightly more (compensates for mana cost)
        assert magic_result.total_damage >= phys_result.total_damage
        # But not drastically more (within ~50%)
        assert magic_result.total_damage <= phys_result.total_damage * 1.5
