""" Unit tests for EnemyTemplate model and EnemyLoader service. Tests enemy loading, serialization, and filtering functionality. """ import pytest from pathlib import Path from app.models.enemy import EnemyTemplate, EnemyDifficulty, LootEntry from app.models.stats import Stats from app.services.enemy_loader import EnemyLoader # ============================================================================= # EnemyTemplate Model Tests # ============================================================================= class TestEnemyTemplate: """Tests for EnemyTemplate dataclass.""" def test_create_basic_enemy(self): """Test creating an enemy with minimal attributes.""" enemy = EnemyTemplate( enemy_id="test_enemy", name="Test Enemy", description="A test enemy", base_stats=Stats(strength=10, constitution=8), ) assert enemy.enemy_id == "test_enemy" assert enemy.name == "Test Enemy" assert enemy.base_stats.strength == 10 assert enemy.difficulty == EnemyDifficulty.EASY # Default def test_enemy_with_full_attributes(self): """Test creating an enemy with all attributes.""" loot = [ LootEntry(item_id="sword", drop_chance=0.5), LootEntry(item_id="gold", drop_chance=1.0, quantity_min=5, quantity_max=10), ] enemy = EnemyTemplate( enemy_id="goblin_boss", name="Goblin Boss", description="A fearsome goblin leader", base_stats=Stats(strength=14, dexterity=12, constitution=12), abilities=["basic_attack", "power_strike"], loot_table=loot, experience_reward=100, gold_reward_min=20, gold_reward_max=50, difficulty=EnemyDifficulty.HARD, tags=["humanoid", "goblinoid", "boss"], base_damage=12, crit_chance=0.15, flee_chance=0.25, ) assert enemy.enemy_id == "goblin_boss" assert enemy.experience_reward == 100 assert enemy.difficulty == EnemyDifficulty.HARD assert len(enemy.loot_table) == 2 assert len(enemy.abilities) == 2 assert "boss" in enemy.tags def test_is_boss(self): """Test boss detection.""" easy_enemy = EnemyTemplate( enemy_id="minion", name="Minion", description="", base_stats=Stats(), difficulty=EnemyDifficulty.EASY, ) boss_enemy = EnemyTemplate( enemy_id="boss", name="Boss", description="", base_stats=Stats(), difficulty=EnemyDifficulty.BOSS, ) assert not easy_enemy.is_boss() assert boss_enemy.is_boss() def test_has_tag(self): """Test tag checking.""" enemy = EnemyTemplate( enemy_id="zombie", name="Zombie", description="", base_stats=Stats(), tags=["undead", "slow", "Humanoid"], # Mixed case ) assert enemy.has_tag("undead") assert enemy.has_tag("UNDEAD") # Case insensitive assert enemy.has_tag("humanoid") assert not enemy.has_tag("beast") def test_get_gold_reward(self): """Test gold reward generation.""" enemy = EnemyTemplate( enemy_id="test", name="Test", description="", base_stats=Stats(), gold_reward_min=10, gold_reward_max=20, ) # Run multiple times to check range for _ in range(50): gold = enemy.get_gold_reward() assert 10 <= gold <= 20 def test_roll_loot_empty_table(self): """Test loot rolling with empty table.""" enemy = EnemyTemplate( enemy_id="test", name="Test", description="", base_stats=Stats(), loot_table=[], ) drops = enemy.roll_loot() assert drops == [] def test_roll_loot_guaranteed_drop(self): """Test loot rolling with guaranteed drop.""" enemy = EnemyTemplate( enemy_id="test", name="Test", description="", base_stats=Stats(), loot_table=[ LootEntry(item_id="guaranteed_item", drop_chance=1.0), ], ) drops = enemy.roll_loot() assert len(drops) == 1 assert drops[0]["item_id"] == "guaranteed_item" def test_serialization_round_trip(self): """Test that to_dict/from_dict preserves data.""" original = EnemyTemplate( enemy_id="test_enemy", name="Test Enemy", description="A test description", base_stats=Stats(strength=15, dexterity=12, luck=10), abilities=["attack", "defend"], loot_table=[ LootEntry(item_id="sword", drop_chance=0.5), ], experience_reward=50, gold_reward_min=10, gold_reward_max=25, difficulty=EnemyDifficulty.MEDIUM, tags=["humanoid", "test"], base_damage=8, crit_chance=0.10, flee_chance=0.40, ) # Serialize and deserialize data = original.to_dict() restored = EnemyTemplate.from_dict(data) # Verify all fields match assert restored.enemy_id == original.enemy_id assert restored.name == original.name assert restored.description == original.description assert restored.base_stats.strength == original.base_stats.strength assert restored.base_stats.luck == original.base_stats.luck assert restored.abilities == original.abilities assert len(restored.loot_table) == len(original.loot_table) assert restored.experience_reward == original.experience_reward assert restored.gold_reward_min == original.gold_reward_min assert restored.gold_reward_max == original.gold_reward_max assert restored.difficulty == original.difficulty assert restored.tags == original.tags assert restored.base_damage == original.base_damage assert restored.crit_chance == pytest.approx(original.crit_chance) assert restored.flee_chance == pytest.approx(original.flee_chance) class TestLootEntry: """Tests for LootEntry dataclass.""" def test_create_loot_entry(self): """Test creating a loot entry.""" entry = LootEntry( item_id="gold_coin", drop_chance=0.75, quantity_min=5, quantity_max=15, ) assert entry.item_id == "gold_coin" assert entry.drop_chance == 0.75 assert entry.quantity_min == 5 assert entry.quantity_max == 15 def test_loot_entry_defaults(self): """Test loot entry default values.""" entry = LootEntry(item_id="item") assert entry.drop_chance == 0.1 assert entry.quantity_min == 1 assert entry.quantity_max == 1 # ============================================================================= # EnemyLoader Service Tests # ============================================================================= class TestEnemyLoader: """Tests for EnemyLoader service.""" @pytest.fixture def loader(self): """Create an enemy loader with the actual data directory.""" return EnemyLoader() def test_load_goblin(self, loader): """Test loading the goblin enemy.""" enemy = loader.load_enemy("goblin") assert enemy is not None assert enemy.enemy_id == "goblin" assert enemy.name == "Goblin Scout" assert enemy.difficulty == EnemyDifficulty.EASY assert "humanoid" in enemy.tags assert "goblinoid" in enemy.tags def test_load_goblin_shaman(self, loader): """Test loading the goblin shaman.""" enemy = loader.load_enemy("goblin_shaman") assert enemy is not None assert enemy.enemy_id == "goblin_shaman" assert enemy.base_stats.intelligence == 12 # Caster stats assert "caster" in enemy.tags def test_load_dire_wolf(self, loader): """Test loading the dire wolf.""" enemy = loader.load_enemy("dire_wolf") assert enemy is not None assert enemy.difficulty == EnemyDifficulty.MEDIUM assert "beast" in enemy.tags assert enemy.base_stats.strength == 14 def test_load_bandit(self, loader): """Test loading the bandit.""" enemy = loader.load_enemy("bandit") assert enemy is not None assert enemy.difficulty == EnemyDifficulty.MEDIUM assert "rogue" in enemy.tags assert enemy.crit_chance == 0.12 def test_load_skeleton_warrior(self, loader): """Test loading the skeleton warrior.""" enemy = loader.load_enemy("skeleton_warrior") assert enemy is not None assert "undead" in enemy.tags assert "fearless" in enemy.tags def test_load_orc_berserker(self, loader): """Test loading the orc berserker.""" enemy = loader.load_enemy("orc_berserker") assert enemy is not None assert enemy.difficulty == EnemyDifficulty.HARD assert enemy.base_stats.strength == 18 assert enemy.base_damage == 15 def test_load_nonexistent_enemy(self, loader): """Test loading an enemy that doesn't exist.""" enemy = loader.load_enemy("nonexistent_enemy_12345") assert enemy is None def test_load_all_enemies(self, loader): """Test loading all enemies.""" enemies = loader.load_all_enemies() # Should have at least our 6 sample enemies assert len(enemies) >= 6 assert "goblin" in enemies assert "goblin_shaman" in enemies assert "dire_wolf" in enemies assert "bandit" in enemies assert "skeleton_warrior" in enemies assert "orc_berserker" in enemies def test_get_enemies_by_difficulty(self, loader): """Test filtering enemies by difficulty.""" loader.load_all_enemies() # Ensure loaded easy_enemies = loader.get_enemies_by_difficulty(EnemyDifficulty.EASY) medium_enemies = loader.get_enemies_by_difficulty(EnemyDifficulty.MEDIUM) hard_enemies = loader.get_enemies_by_difficulty(EnemyDifficulty.HARD) # Check we got enemies in each category assert len(easy_enemies) >= 2 # goblin, goblin_shaman assert len(medium_enemies) >= 3 # dire_wolf, bandit, skeleton_warrior assert len(hard_enemies) >= 1 # orc_berserker # Verify difficulty is correct for enemy in easy_enemies: assert enemy.difficulty == EnemyDifficulty.EASY def test_get_enemies_by_tag(self, loader): """Test filtering enemies by tag.""" loader.load_all_enemies() humanoids = loader.get_enemies_by_tag("humanoid") undead = loader.get_enemies_by_tag("undead") beasts = loader.get_enemies_by_tag("beast") # Verify results assert len(humanoids) >= 3 # goblin, goblin_shaman, bandit, orc assert len(undead) >= 1 # skeleton_warrior assert len(beasts) >= 1 # dire_wolf # Verify tags for enemy in humanoids: assert enemy.has_tag("humanoid") def test_get_random_enemies(self, loader): """Test random enemy selection.""" loader.load_all_enemies() # Get 3 random enemies random_enemies = loader.get_random_enemies(count=3) assert len(random_enemies) == 3 # All should be EnemyTemplate instances for enemy in random_enemies: assert isinstance(enemy, EnemyTemplate) def test_get_random_enemies_with_filters(self, loader): """Test random selection with difficulty filter.""" loader.load_all_enemies() # Get only easy enemies easy_enemies = loader.get_random_enemies( count=5, difficulty=EnemyDifficulty.EASY, ) # All returned enemies should be easy for enemy in easy_enemies: assert enemy.difficulty == EnemyDifficulty.EASY def test_cache_behavior(self, loader): """Test that caching works correctly.""" # Load an enemy twice enemy1 = loader.load_enemy("goblin") enemy2 = loader.load_enemy("goblin") # Should be the same object (cached) assert enemy1 is enemy2 # Clear cache loader.clear_cache() # Load again enemy3 = loader.load_enemy("goblin") # Should be a new object assert enemy3 is not enemy1 assert enemy3.enemy_id == enemy1.enemy_id # ============================================================================= # EnemyDifficulty Enum Tests # ============================================================================= class TestEnemyDifficulty: """Tests for EnemyDifficulty enum.""" def test_difficulty_values(self): """Test difficulty enum values.""" assert EnemyDifficulty.EASY.value == "easy" assert EnemyDifficulty.MEDIUM.value == "medium" assert EnemyDifficulty.HARD.value == "hard" assert EnemyDifficulty.BOSS.value == "boss" def test_difficulty_from_string(self): """Test creating difficulty from string.""" assert EnemyDifficulty("easy") == EnemyDifficulty.EASY assert EnemyDifficulty("hard") == EnemyDifficulty.HARD