"""
Tests for CombatLootService.

Tests the service that orchestrates loot generation from combat,
supporting both static and procedural loot drops.
"""

import pytest
from unittest.mock import Mock, patch

from app.services.combat_loot_service import (
    CombatLootService,
    LootContext,
    get_combat_loot_service,
    DIFFICULTY_RARITY_BONUS,
    LUCK_CONVERSION_FACTOR
)
from app.models.enemy import (
    EnemyTemplate,
    EnemyDifficulty,
    LootEntry,
    LootType
)
from app.models.stats import Stats
from app.models.items import Item
from app.models.enums import ItemType, ItemRarity


class TestLootContext:
    """Test LootContext dataclass."""

    def test_default_values(self):
        """Test default context values."""
        context = LootContext()

        assert context.party_average_level == 1
        assert context.enemy_difficulty == EnemyDifficulty.EASY
        assert context.luck_stat == 8
        assert context.loot_bonus == 0.0

    def test_custom_values(self):
        """Test creating context with custom values."""
        context = LootContext(
            party_average_level=10,
            enemy_difficulty=EnemyDifficulty.HARD,
            luck_stat=15,
            loot_bonus=0.1
        )

        assert context.party_average_level == 10
        assert context.enemy_difficulty == EnemyDifficulty.HARD
        assert context.luck_stat == 15
        assert context.loot_bonus == 0.1


class TestDifficultyBonuses:
    """Test difficulty rarity bonus constants."""

    def test_easy_bonus(self):
        """Easy enemies have no bonus."""
        assert DIFFICULTY_RARITY_BONUS[EnemyDifficulty.EASY] == 0.0

    def test_medium_bonus(self):
        """Medium enemies have small bonus."""
        assert DIFFICULTY_RARITY_BONUS[EnemyDifficulty.MEDIUM] == 0.05

    def test_hard_bonus(self):
        """Hard enemies have moderate bonus."""
        assert DIFFICULTY_RARITY_BONUS[EnemyDifficulty.HARD] == 0.15

    def test_boss_bonus(self):
        """Boss enemies have large bonus."""
        assert DIFFICULTY_RARITY_BONUS[EnemyDifficulty.BOSS] == 0.30


class TestCombatLootServiceInit:
    """Test service initialization."""

    def test_init_uses_defaults(self):
        """Service should initialize with default dependencies."""
        service = CombatLootService()

        assert service.item_generator is not None
        assert service.static_loader is not None

    def test_singleton_returns_same_instance(self):
        """get_combat_loot_service should return singleton."""
        service1 = get_combat_loot_service()
        service2 = get_combat_loot_service()

        assert service1 is service2


class TestCombatLootServiceEffectiveLuck:
    """Test effective luck calculation."""

    def test_base_luck_no_bonus(self):
        """With no bonuses, effective luck equals base luck."""
        service = CombatLootService()

        entry = LootEntry(
            loot_type=LootType.PROCEDURAL,
            item_type="weapon",
            rarity_bonus=0.0
        )
        context = LootContext(
            luck_stat=8,
            enemy_difficulty=EnemyDifficulty.EASY,
            loot_bonus=0.0
        )

        effective = service._calculate_effective_luck(entry, context)

        # No bonus, so effective should equal base
        assert effective == 8

    def test_difficulty_bonus_adds_luck(self):
        """Difficulty bonus should increase effective luck."""
        service = CombatLootService()

        entry = LootEntry(
            loot_type=LootType.PROCEDURAL,
            item_type="weapon",
            rarity_bonus=0.0
        )
        context = LootContext(
            luck_stat=8,
            enemy_difficulty=EnemyDifficulty.BOSS,  # 0.30 bonus
            loot_bonus=0.0
        )

        effective = service._calculate_effective_luck(entry, context)

        # Boss bonus = 0.30 * 20 = 6 extra luck
        assert effective == 8 + 6

    def test_entry_rarity_bonus_adds_luck(self):
        """Entry rarity bonus should increase effective luck."""
        service = CombatLootService()

        entry = LootEntry(
            loot_type=LootType.PROCEDURAL,
            item_type="weapon",
            rarity_bonus=0.10  # Entry-specific bonus
        )
        context = LootContext(
            luck_stat=8,
            enemy_difficulty=EnemyDifficulty.EASY,
            loot_bonus=0.0
        )

        effective = service._calculate_effective_luck(entry, context)

        # 0.10 * 20 = 2 extra luck
        assert effective == 8 + 2

    def test_combined_bonuses(self):
        """All bonuses should stack."""
        service = CombatLootService()

        entry = LootEntry(
            loot_type=LootType.PROCEDURAL,
            item_type="weapon",
            rarity_bonus=0.10
        )
        context = LootContext(
            luck_stat=10,
            enemy_difficulty=EnemyDifficulty.HARD,  # 0.15
            loot_bonus=0.05
        )

        effective = service._calculate_effective_luck(entry, context)

        # Total bonus = 0.10 + 0.15 + 0.05 = 0.30
        # Extra luck = 0.30 * 20 = 6
        expected = 10 + 6
        assert effective == expected


class TestCombatLootServiceStaticItems:
    """Test static item generation."""

    def test_generate_static_items_returns_items(self):
        """Should return Item instances for static entries."""
        service = CombatLootService()

        entry = LootEntry(
            loot_type=LootType.STATIC,
            item_id="health_potion_small",
            drop_chance=1.0
        )

        items = service._generate_static_items(entry, quantity=1)

        assert len(items) == 1
        assert items[0].name == "Small Health Potion"

    def test_generate_static_items_respects_quantity(self):
        """Should generate correct quantity of items."""
        service = CombatLootService()

        entry = LootEntry(
            loot_type=LootType.STATIC,
            item_id="goblin_ear",
            drop_chance=1.0
        )

        items = service._generate_static_items(entry, quantity=3)

        assert len(items) == 3
        # All should be goblin ears with unique IDs
        for item in items:
            assert "goblin_ear" in item.item_id

    def test_generate_static_items_missing_id(self):
        """Should return empty list if item_id is missing."""
        service = CombatLootService()

        entry = LootEntry(
            loot_type=LootType.STATIC,
            item_id=None,
            drop_chance=1.0
        )

        items = service._generate_static_items(entry, quantity=1)

        assert len(items) == 0


class TestCombatLootServiceProceduralItems:
    """Test procedural item generation."""

    def test_generate_procedural_items_returns_items(self):
        """Should return generated Item instances."""
        service = CombatLootService()

        entry = LootEntry(
            loot_type=LootType.PROCEDURAL,
            item_type="weapon",
            drop_chance=1.0,
            rarity_bonus=0.0
        )
        context = LootContext(party_average_level=5)

        items = service._generate_procedural_items(entry, quantity=1, context=context)

        assert len(items) == 1
        assert items[0].is_weapon()

    def test_generate_procedural_armor(self):
        """Should generate armor when item_type is armor."""
        service = CombatLootService()

        entry = LootEntry(
            loot_type=LootType.PROCEDURAL,
            item_type="armor",
            drop_chance=1.0
        )
        context = LootContext(party_average_level=5)

        items = service._generate_procedural_items(entry, quantity=1, context=context)

        assert len(items) == 1
        assert items[0].is_armor()

    def test_generate_procedural_missing_type(self):
        """Should return empty list if item_type is missing."""
        service = CombatLootService()

        entry = LootEntry(
            loot_type=LootType.PROCEDURAL,
            item_type=None,
            drop_chance=1.0
        )
        context = LootContext()

        items = service._generate_procedural_items(entry, quantity=1, context=context)

        assert len(items) == 0


class TestCombatLootServiceGenerateFromEnemy:
    """Test full loot generation from enemy templates."""

    @pytest.fixture
    def sample_enemy(self):
        """Create a sample enemy template for testing."""
        return EnemyTemplate(
            enemy_id="test_goblin",
            name="Test Goblin",
            description="A test goblin",
            base_stats=Stats(),
            abilities=["basic_attack"],
            loot_table=[
                LootEntry(
                    loot_type=LootType.STATIC,
                    item_id="goblin_ear",
                    drop_chance=1.0,  # Guaranteed drop for testing
                    quantity_min=1,
                    quantity_max=1
                )
            ],
            experience_reward=10,
            difficulty=EnemyDifficulty.EASY
        )

    def test_generate_loot_from_enemy_basic(self, sample_enemy):
        """Should generate loot from enemy loot table."""
        service = CombatLootService()
        context = LootContext()

        items = service.generate_loot_from_enemy(sample_enemy, context)

        assert len(items) == 1
        assert "goblin_ear" in items[0].item_id

    def test_generate_loot_respects_drop_chance(self):
        """Items with 0 drop chance should never drop."""
        enemy = EnemyTemplate(
            enemy_id="test_enemy",
            name="Test Enemy",
            description="Test",
            base_stats=Stats(),
            abilities=[],
            loot_table=[
                LootEntry(
                    loot_type=LootType.STATIC,
                    item_id="rare_item",
                    drop_chance=0.0,  # Never drops
                )
            ],
            difficulty=EnemyDifficulty.EASY
        )
        service = CombatLootService()
        context = LootContext()

        # Run multiple times to ensure it never drops
        for _ in range(10):
            items = service.generate_loot_from_enemy(enemy, context)
            assert len(items) == 0

    def test_generate_loot_multiple_entries(self):
        """Should process all loot table entries."""
        enemy = EnemyTemplate(
            enemy_id="test_enemy",
            name="Test Enemy",
            description="Test",
            base_stats=Stats(),
            abilities=[],
            loot_table=[
                LootEntry(
                    loot_type=LootType.STATIC,
                    item_id="goblin_ear",
                    drop_chance=1.0,
                ),
                LootEntry(
                    loot_type=LootType.STATIC,
                    item_id="health_potion_small",
                    drop_chance=1.0,
                )
            ],
            difficulty=EnemyDifficulty.EASY
        )
        service = CombatLootService()
        context = LootContext()

        items = service.generate_loot_from_enemy(enemy, context)

        assert len(items) == 2


class TestCombatLootServiceBossLoot:
    """Test boss loot generation."""

    @pytest.fixture
    def boss_enemy(self):
        """Create a boss enemy template for testing."""
        return EnemyTemplate(
            enemy_id="test_boss",
            name="Test Boss",
            description="A test boss",
            base_stats=Stats(strength=20, constitution=20),
            abilities=["basic_attack"],
            loot_table=[
                LootEntry(
                    loot_type=LootType.STATIC,
                    item_id="goblin_chieftain_token",
                    drop_chance=1.0,
                )
            ],
            experience_reward=100,
            difficulty=EnemyDifficulty.BOSS
        )

    def test_generate_boss_loot_includes_guaranteed_drops(self, boss_enemy):
        """Boss loot should include guaranteed equipment drops."""
        service = CombatLootService()
        context = LootContext(party_average_level=10)

        items = service.generate_boss_loot(boss_enemy, context, guaranteed_drops=1)

        # Should have at least the loot table drop + guaranteed drop
        assert len(items) >= 2

    def test_generate_boss_loot_non_boss_skips_guaranteed(self):
        """Non-boss enemies shouldn't get guaranteed drops."""
        enemy = EnemyTemplate(
            enemy_id="test_enemy",
            name="Test Enemy",
            description="Test",
            base_stats=Stats(),
            abilities=[],
            loot_table=[
                LootEntry(
                    loot_type=LootType.STATIC,
                    item_id="goblin_ear",
                    drop_chance=1.0,
                )
            ],
            difficulty=EnemyDifficulty.EASY  # Not a boss
        )
        service = CombatLootService()
        context = LootContext()

        items = service.generate_boss_loot(enemy, context, guaranteed_drops=2)

        # Should only have the one loot table drop
        assert len(items) == 1
