"""
Unit tests for Stats dataclass.

Tests computed properties, serialization, and basic operations.
"""

import pytest
from app.models.stats import Stats


def test_stats_default_values():
    """Test that Stats initializes with default values."""
    stats = Stats()

    assert stats.strength == 10
    assert stats.dexterity == 10
    assert stats.constitution == 10
    assert stats.intelligence == 10
    assert stats.wisdom == 10
    assert stats.charisma == 10


def test_stats_custom_values():
    """Test creating Stats with custom values."""
    stats = Stats(
        strength=15,
        dexterity=12,
        constitution=14,
        intelligence=8,
        wisdom=10,
        charisma=11,
    )

    assert stats.strength == 15
    assert stats.dexterity == 12
    assert stats.constitution == 14
    assert stats.intelligence == 8
    assert stats.wisdom == 10
    assert stats.charisma == 11


def test_hit_points_calculation():
    """Test HP calculation: 10 + (constitution × 2)."""
    stats = Stats(constitution=10)
    assert stats.hit_points == 30  # 10 + (10 × 2)

    stats = Stats(constitution=15)
    assert stats.hit_points == 40  # 10 + (15 × 2)

    stats = Stats(constitution=20)
    assert stats.hit_points == 50  # 10 + (20 × 2)


def test_mana_points_calculation():
    """Test MP calculation: 10 + (intelligence × 2)."""
    stats = Stats(intelligence=10)
    assert stats.mana_points == 30  # 10 + (10 × 2)

    stats = Stats(intelligence=15)
    assert stats.mana_points == 40  # 10 + (15 × 2)

    stats = Stats(intelligence=8)
    assert stats.mana_points == 26  # 10 + (8 × 2)


def test_defense_calculation():
    """Test defense calculation: constitution // 2."""
    stats = Stats(constitution=10)
    assert stats.defense == 5  # 10 // 2

    stats = Stats(constitution=15)
    assert stats.defense == 7  # 15 // 2

    stats = Stats(constitution=21)
    assert stats.defense == 10  # 21 // 2


def test_resistance_calculation():
    """Test resistance calculation: wisdom // 2."""
    stats = Stats(wisdom=10)
    assert stats.resistance == 5  # 10 // 2

    stats = Stats(wisdom=14)
    assert stats.resistance == 7  # 14 // 2

    stats = Stats(wisdom=9)
    assert stats.resistance == 4  # 9 // 2


def test_stats_serialization():
    """Test to_dict() serialization."""
    stats = Stats(
        strength=15,
        dexterity=12,
        constitution=14,
        intelligence=10,
        wisdom=11,
        charisma=8,
    )

    data = stats.to_dict()

    assert data["strength"] == 15
    assert data["dexterity"] == 12
    assert data["constitution"] == 14
    assert data["intelligence"] == 10
    assert data["wisdom"] == 11
    assert data["charisma"] == 8


def test_stats_deserialization():
    """Test from_dict() deserialization."""
    data = {
        "strength": 18,
        "dexterity": 14,
        "constitution": 16,
        "intelligence": 12,
        "wisdom": 10,
        "charisma": 9,
    }

    stats = Stats.from_dict(data)

    assert stats.strength == 18
    assert stats.dexterity == 14
    assert stats.constitution == 16
    assert stats.intelligence == 12
    assert stats.wisdom == 10
    assert stats.charisma == 9


def test_stats_deserialization_with_missing_values():
    """Test from_dict() with missing values (should use defaults)."""
    data = {
        "strength": 15,
        # Missing other stats
    }

    stats = Stats.from_dict(data)

    assert stats.strength == 15
    assert stats.dexterity == 10  # Default
    assert stats.constitution == 10  # Default
    assert stats.intelligence == 10  # Default
    assert stats.wisdom == 10  # Default
    assert stats.charisma == 10  # Default


def test_stats_round_trip_serialization():
    """Test that serialization and deserialization preserve data."""
    original = Stats(
        strength=20,
        dexterity=15,
        constitution=18,
        intelligence=10,
        wisdom=12,
        charisma=14,
    )

    # Serialize then deserialize
    data = original.to_dict()
    restored = Stats.from_dict(data)

    assert restored.strength == original.strength
    assert restored.dexterity == original.dexterity
    assert restored.constitution == original.constitution
    assert restored.intelligence == original.intelligence
    assert restored.wisdom == original.wisdom
    assert restored.charisma == original.charisma


def test_stats_copy():
    """Test that copy() creates an independent copy."""
    original = Stats(strength=15, dexterity=12, constitution=14)
    copy = original.copy()

    assert copy.strength == original.strength
    assert copy.dexterity == original.dexterity
    assert copy.constitution == original.constitution

    # Modify copy
    copy.strength = 20

    # Original should be unchanged
    assert original.strength == 15
    assert copy.strength == 20


def test_stats_repr():
    """Test string representation."""
    stats = Stats(strength=15, constitution=12, intelligence=10)
    repr_str = repr(stats)

    assert "STR=15" in repr_str
    assert "CON=12" in repr_str
    assert "INT=10" in repr_str
    assert "HP=" in repr_str
    assert "MP=" in repr_str


# =============================================================================
# LUK Computed Properties (Combat System Integration)
# =============================================================================

def test_crit_bonus_calculation():
    """Test crit bonus calculation: luck * 0.5%."""
    stats = Stats(luck=8)
    assert stats.crit_bonus == pytest.approx(0.04, abs=0.001)  # 4%

    stats = Stats(luck=12)
    assert stats.crit_bonus == pytest.approx(0.06, abs=0.001)  # 6%

    stats = Stats(luck=0)
    assert stats.crit_bonus == pytest.approx(0.0, abs=0.001)  # 0%


def test_hit_bonus_calculation():
    """Test hit bonus (miss reduction): luck * 0.5%."""
    stats = Stats(luck=8)
    assert stats.hit_bonus == pytest.approx(0.04, abs=0.001)  # 4%

    stats = Stats(luck=12)
    assert stats.hit_bonus == pytest.approx(0.06, abs=0.001)  # 6%

    stats = Stats(luck=20)
    assert stats.hit_bonus == pytest.approx(0.10, abs=0.001)  # 10%


def test_lucky_roll_chance_calculation():
    """Test lucky roll chance: 5% + (luck * 0.25%)."""
    stats = Stats(luck=8)
    # 5% + (8 * 0.25%) = 5% + 2% = 7%
    assert stats.lucky_roll_chance == pytest.approx(0.07, abs=0.001)

    stats = Stats(luck=12)
    # 5% + (12 * 0.25%) = 5% + 3% = 8%
    assert stats.lucky_roll_chance == pytest.approx(0.08, abs=0.001)

    stats = Stats(luck=0)
    # 5% + (0 * 0.25%) = 5%
    assert stats.lucky_roll_chance == pytest.approx(0.05, abs=0.001)


def test_repr_includes_combat_bonuses():
    """Test that repr includes LUK-based combat bonuses."""
    stats = Stats(luck=10)
    repr_str = repr(stats)

    assert "CRIT_BONUS=" in repr_str
    assert "HIT_BONUS=" in repr_str


# =============================================================================
# Equipment Bonus Fields (Task 2.5)
# =============================================================================

def test_bonus_fields_default_to_zero():
    """Test that equipment bonus fields default to zero."""
    stats = Stats()

    assert stats.damage_bonus == 0
    assert stats.defense_bonus == 0
    assert stats.resistance_bonus == 0


def test_damage_property_with_no_bonus():
    """Test damage calculation: int(strength * 0.75) + damage_bonus with no bonus."""
    stats = Stats(strength=10)
    # int(10 * 0.75) = 7, no bonus
    assert stats.damage == 7

    stats = Stats(strength=14)
    # int(14 * 0.75) = 10, no bonus
    assert stats.damage == 10


def test_damage_property_with_bonus():
    """Test damage calculation includes damage_bonus from weapons."""
    stats = Stats(strength=10, damage_bonus=15)
    # int(10 * 0.75) + 15 = 7 + 15 = 22
    assert stats.damage == 22

    stats = Stats(strength=14, damage_bonus=8)
    # int(14 * 0.75) + 8 = 10 + 8 = 18
    assert stats.damage == 18


def test_defense_property_with_bonus():
    """Test defense calculation includes defense_bonus from armor."""
    stats = Stats(constitution=10, defense_bonus=10)
    # (10 // 2) + 10 = 5 + 10 = 15
    assert stats.defense == 15

    stats = Stats(constitution=20, defense_bonus=5)
    # (20 // 2) + 5 = 10 + 5 = 15
    assert stats.defense == 15


def test_resistance_property_with_bonus():
    """Test resistance calculation includes resistance_bonus from armor."""
    stats = Stats(wisdom=10, resistance_bonus=8)
    # (10 // 2) + 8 = 5 + 8 = 13
    assert stats.resistance == 13

    stats = Stats(wisdom=14, resistance_bonus=3)
    # (14 // 2) + 3 = 7 + 3 = 10
    assert stats.resistance == 10


def test_bonus_fields_serialization():
    """Test that bonus fields are included in to_dict()."""
    stats = Stats(
        strength=15,
        damage_bonus=12,
        defense_bonus=8,
        resistance_bonus=5,
    )

    data = stats.to_dict()

    assert data["damage_bonus"] == 12
    assert data["defense_bonus"] == 8
    assert data["resistance_bonus"] == 5


def test_bonus_fields_deserialization():
    """Test that bonus fields are restored from from_dict()."""
    data = {
        "strength": 15,
        "damage_bonus": 12,
        "defense_bonus": 8,
        "resistance_bonus": 5,
    }

    stats = Stats.from_dict(data)

    assert stats.damage_bonus == 12
    assert stats.defense_bonus == 8
    assert stats.resistance_bonus == 5


def test_bonus_fields_deserialization_defaults():
    """Test that missing bonus fields default to zero on deserialization."""
    data = {
        "strength": 15,
        # No bonus fields
    }

    stats = Stats.from_dict(data)

    assert stats.damage_bonus == 0
    assert stats.defense_bonus == 0
    assert stats.resistance_bonus == 0


def test_copy_includes_bonus_fields():
    """Test that copy() preserves bonus fields."""
    original = Stats(
        strength=15,
        damage_bonus=10,
        defense_bonus=8,
        resistance_bonus=5,
    )
    copy = original.copy()

    assert copy.damage_bonus == 10
    assert copy.defense_bonus == 8
    assert copy.resistance_bonus == 5

    # Verify independence
    copy.damage_bonus = 20
    assert original.damage_bonus == 10
    assert copy.damage_bonus == 20


def test_repr_includes_damage():
    """Test that repr includes the damage computed property."""
    stats = Stats(strength=10, damage_bonus=15)
    repr_str = repr(stats)

    assert "DMG=" in repr_str
