"""
Unit tests for Character dataclass.

Tests the critical get_effective_stats() method which combines all stat modifiers,
as well as inventory, equipment, experience, and serialization.
"""

import pytest
from app.models.character import Character
from app.models.stats import Stats
from app.models.items import Item
from app.models.effects import Effect
from app.models.skills import PlayerClass, SkillTree, SkillNode
from app.models.origins import Origin, StartingLocation, StartingBonus
from app.models.enums import ItemType, EffectType, StatType, DamageType


@pytest.fixture
def basic_player_class():
    """Create a basic player class for testing."""
    base_stats = Stats(strength=12, dexterity=10, constitution=14, intelligence=8, wisdom=10, charisma=11)

    # Create a simple skill tree
    skill_tree = SkillTree(
        tree_id="warrior_offense",
        name="Warrior Offense",
        description="Offensive combat skills",
        nodes=[
            SkillNode(
                skill_id="power_strike",
                name="Power Strike",
                description="+5 Strength",
                tier=1,
                effects={"strength": 5},
            ),
        ],
    )

    return PlayerClass(
        class_id="warrior",
        name="Warrior",
        description="Strong melee fighter",
        base_stats=base_stats,
        skill_trees=[skill_tree],
        starting_equipment=["basic_sword"],
        starting_abilities=["basic_attack"],
    )


@pytest.fixture
def basic_origin():
    """Create a basic origin for testing."""
    starting_location = StartingLocation(
        id="test_location",
        name="Test Village",
        region="Test Region",
        description="A simple test location"
    )

    starting_bonus = StartingBonus(
        trait="Test Trait",
        description="A test trait for testing",
        effect="+1 to all stats"
    )

    return Origin(
        id="test_origin",
        name="Test Origin",
        description="A test origin for character testing",
        starting_location=starting_location,
        narrative_hooks=["Test hook 1", "Test hook 2"],
        starting_bonus=starting_bonus
    )


@pytest.fixture
def basic_character(basic_player_class, basic_origin):
    """Create a basic character for testing."""
    return Character(
        character_id="char_001",
        user_id="user_001",
        name="Test Hero",
        player_class=basic_player_class,
        origin=basic_origin,
        level=1,
        experience=0,
        base_stats=basic_player_class.base_stats.copy(),
    )


def test_character_creation(basic_character):
    """Test creating a Character instance."""
    assert basic_character.character_id == "char_001"
    assert basic_character.user_id == "user_001"
    assert basic_character.name == "Test Hero"
    assert basic_character.level == 1
    assert basic_character.experience == 0
    assert basic_character.gold == 0


def test_get_effective_stats_base_only(basic_character):
    """Test get_effective_stats() with only base stats (no modifiers)."""
    effective = basic_character.get_effective_stats()

    # Should match base stats exactly
    assert effective.strength == 12
    assert effective.dexterity == 10
    assert effective.constitution == 14
    assert effective.intelligence == 8
    assert effective.wisdom == 10
    assert effective.charisma == 11


def test_get_effective_stats_with_equipment(basic_character):
    """Test get_effective_stats() with equipped items."""
    # Create a weapon with +5 strength
    weapon = Item(
        item_id="iron_sword",
        name="Iron Sword",
        item_type=ItemType.WEAPON,
        description="A sturdy iron sword",
        stat_bonuses={"strength": 5},
        damage=10,
    )

    basic_character.equipped["weapon"] = weapon
    effective = basic_character.get_effective_stats()

    # Strength should be base (12) + weapon (5) = 17
    assert effective.strength == 17
    assert effective.dexterity == 10  # Unchanged


def test_get_effective_stats_with_skill_bonuses(basic_character):
    """Test get_effective_stats() with skill tree bonuses."""
    # Unlock the "power_strike" skill which gives +5 strength
    basic_character.unlocked_skills.append("power_strike")

    effective = basic_character.get_effective_stats()

    # Strength should be base (12) + skill (5) = 17
    assert effective.strength == 17


def test_get_effective_stats_with_all_modifiers(basic_character):
    """Test get_effective_stats() with equipment + skills + active effects."""
    # Add equipment: +5 strength
    weapon = Item(
        item_id="iron_sword",
        name="Iron Sword",
        item_type=ItemType.WEAPON,
        description="A sturdy iron sword",
        stat_bonuses={"strength": 5},
        damage=10,
    )
    basic_character.equipped["weapon"] = weapon

    # Unlock skill: +5 strength
    basic_character.unlocked_skills.append("power_strike")

    # Add buff effect: +3 strength
    buff = Effect(
        effect_id="str_buff",
        name="Strength Boost",
        effect_type=EffectType.BUFF,
        duration=3,
        power=3,
        stat_affected=StatType.STRENGTH,
    )

    effective = basic_character.get_effective_stats([buff])

    # Total strength: 12 (base) + 5 (weapon) + 5 (skill) + 3 (buff) = 25
    assert effective.strength == 25


def test_get_effective_stats_with_debuff(basic_character):
    """Test get_effective_stats() with a debuff."""
    debuff = Effect(
        effect_id="weakened",
        name="Weakened",
        effect_type=EffectType.DEBUFF,
        duration=2,
        power=5,
        stat_affected=StatType.STRENGTH,
    )

    effective = basic_character.get_effective_stats([debuff])

    # Strength: 12 (base) - 5 (debuff) = 7
    assert effective.strength == 7


def test_get_effective_stats_debuff_minimum(basic_character):
    """Test that debuffs cannot reduce stats below 1."""
    # Massive debuff
    debuff = Effect(
        effect_id="weakened",
        name="Weakened",
        effect_type=EffectType.DEBUFF,
        duration=2,
        power=20,  # More than base strength
        stat_affected=StatType.STRENGTH,
    )

    effective = basic_character.get_effective_stats([debuff])

    # Should be clamped at 1, not 0 or negative
    assert effective.strength == 1


def test_inventory_management(basic_character):
    """Test adding and removing items from inventory."""
    item = Item(
        item_id="potion",
        name="Health Potion",
        item_type=ItemType.CONSUMABLE,
        description="Restores 50 HP",
        value=25,
    )

    # Add item
    basic_character.add_item(item)
    assert len(basic_character.inventory) == 1
    assert basic_character.inventory[0].item_id == "potion"

    # Remove item
    removed = basic_character.remove_item("potion")
    assert removed.item_id == "potion"
    assert len(basic_character.inventory) == 0


def test_equip_and_unequip(basic_character):
    """Test equipping and unequipping items."""
    weapon = Item(
        item_id="iron_sword",
        name="Iron Sword",
        item_type=ItemType.WEAPON,
        description="A sturdy sword",
        stat_bonuses={"strength": 5},
        damage=10,
    )

    # Add to inventory first
    basic_character.add_item(weapon)
    assert len(basic_character.inventory) == 1

    # Equip weapon
    basic_character.equip_item(weapon, "weapon")
    assert "weapon" in basic_character.equipped
    assert basic_character.equipped["weapon"].item_id == "iron_sword"
    assert len(basic_character.inventory) == 0  # Removed from inventory

    # Unequip weapon
    unequipped = basic_character.unequip_item("weapon")
    assert unequipped.item_id == "iron_sword"
    assert "weapon" not in basic_character.equipped
    assert len(basic_character.inventory) == 1  # Back in inventory


def test_equip_replaces_existing(basic_character):
    """Test that equipping a new item replaces the old one."""
    weapon1 = Item(
        item_id="iron_sword",
        name="Iron Sword",
        item_type=ItemType.WEAPON,
        description="A sturdy sword",
        damage=10,
    )

    weapon2 = Item(
        item_id="steel_sword",
        name="Steel Sword",
        item_type=ItemType.WEAPON,
        description="A better sword",
        damage=15,
    )

    # Equip first weapon
    basic_character.add_item(weapon1)
    basic_character.equip_item(weapon1, "weapon")

    # Equip second weapon
    basic_character.add_item(weapon2)
    previous = basic_character.equip_item(weapon2, "weapon")

    assert previous.item_id == "iron_sword"  # Old weapon returned
    assert basic_character.equipped["weapon"].item_id == "steel_sword"
    assert len(basic_character.inventory) == 1  # Old weapon back in inventory


def test_gold_management(basic_character):
    """Test adding and removing gold."""
    assert basic_character.gold == 0

    # Add gold
    basic_character.add_gold(100)
    assert basic_character.gold == 100

    # Remove gold
    success = basic_character.remove_gold(50)
    assert success == True
    assert basic_character.gold == 50

    # Try to remove more than available
    success = basic_character.remove_gold(100)
    assert success == False
    assert basic_character.gold == 50  # Unchanged


def test_can_afford(basic_character):
    """Test can_afford() method."""
    basic_character.gold = 100

    assert basic_character.can_afford(50) == True
    assert basic_character.can_afford(100) == True
    assert basic_character.can_afford(101) == False


def test_add_experience_no_level_up(basic_character):
    """Test adding experience without leveling up."""
    leveled_up = basic_character.add_experience(50)

    assert leveled_up == False
    assert basic_character.level == 1
    assert basic_character.experience == 50


def test_add_experience_with_level_up(basic_character):
    """Test adding enough experience to level up."""
    # Level 1 requires 100 XP for level 2
    leveled_up = basic_character.add_experience(100)

    assert leveled_up == True
    assert basic_character.level == 2
    assert basic_character.experience == 0  # Reset


def test_add_experience_with_overflow(basic_character):
    """Test leveling up with overflow experience."""
    # Level 1 requires 100 XP, give 150
    leveled_up = basic_character.add_experience(150)

    assert leveled_up == True
    assert basic_character.level == 2
    assert basic_character.experience == 50  # Overflow


def test_xp_calculation(basic_origin):
    """Test XP required for each level."""
    char = Character(
        character_id="test",
        user_id="user",
        name="Test",
        player_class=PlayerClass(
            class_id="test",
            name="Test",
            description="Test",
            base_stats=Stats(),
        ),
        origin=basic_origin,
    )

    # Formula: 100 * (level ^ 1.5)
    assert char._calculate_xp_for_next_level() == 100  # Level 1→2

    char.level = 2
    assert char._calculate_xp_for_next_level() == 282  # Level 2→3

    char.level = 3
    assert char._calculate_xp_for_next_level() == 519  # Level 3→4


def test_get_unlocked_abilities(basic_character):
    """Test getting abilities from class + unlocked skills."""
    # Should have starting abilities
    abilities = basic_character.get_unlocked_abilities()
    assert "basic_attack" in abilities

    # TODO: When skills unlock abilities, test that here


def test_character_serialization(basic_character):
    """Test character to_dict() serialization."""
    # Add some data
    basic_character.gold = 500
    basic_character.level = 3
    basic_character.experience = 100

    data = basic_character.to_dict()

    assert data["character_id"] == "char_001"
    assert data["user_id"] == "user_001"
    assert data["name"] == "Test Hero"
    assert data["level"] == 3
    assert data["experience"] == 100
    assert data["gold"] == 500


def test_character_deserialization(basic_player_class, basic_origin):
    """Test character from_dict() deserialization."""
    data = {
        "character_id": "char_002",
        "user_id": "user_002",
        "name": "Restored Hero",
        "player_class": basic_player_class.to_dict(),
        "origin": basic_origin.to_dict(),
        "level": 5,
        "experience": 200,
        "base_stats": Stats(strength=15).to_dict(),
        "unlocked_skills": ["power_strike"],
        "inventory": [],
        "equipped": {},
        "gold": 1000,
        "active_quests": ["quest_1"],
        "discovered_locations": ["town_1"],
    }

    char = Character.from_dict(data)

    assert char.character_id == "char_002"
    assert char.name == "Restored Hero"
    assert char.level == 5
    assert char.gold == 1000
    assert "power_strike" in char.unlocked_skills


def test_character_round_trip_serialization(basic_character):
    """Test that serialization and deserialization preserve all data."""
    # Add complex state
    basic_character.gold = 500
    basic_character.level = 3
    basic_character.unlocked_skills = ["power_strike"]

    weapon = Item(
        item_id="sword",
        name="Sword",
        item_type=ItemType.WEAPON,
        description="A sword",
        damage=10,
    )
    basic_character.equipped["weapon"] = weapon

    # Serialize and deserialize
    data = basic_character.to_dict()
    restored = Character.from_dict(data)

    assert restored.character_id == basic_character.character_id
    assert restored.name == basic_character.name
    assert restored.level == basic_character.level
    assert restored.gold == basic_character.gold
    assert restored.unlocked_skills == basic_character.unlocked_skills
    assert "weapon" in restored.equipped
    assert restored.equipped["weapon"].item_id == "sword"
