"""Tests for feat and spell extraction from Foundry VTT sheets.""" from __future__ import annotations from pathlib import Path from pf1e_simulator.abilities import ( ALERTNESS, DODGE, GREAT_FORTITUDE, IRON_WILL, LIGHTNING_REFLEXES, POINT_BLANK_SHOT, POWER_ATTACK, PRECISE_SHOT, AbilitySpec, ) from pf1e_simulator.loaders.foundry import ( _extract_features, _extract_spells, _match_feat, load_sheet, load_sheet_detailed, ) _SHEETS = Path(__file__).resolve().parent.parent / "fiches_personnages" class TestMatchFeat: """Given: a feat name string from a Foundry sheet When: _match_feat is called Then: returns the matching AbilitySpec or None.""" def test_power_attack(self) -> None: assert _match_feat("Power Attack") == POWER_ATTACK def test_iron_will(self) -> None: assert _match_feat("Iron Will") == IRON_WILL def test_great_fortitude(self) -> None: assert _match_feat("Great Fortitude") == GREAT_FORTITUDE def test_lightning_reflexes(self) -> None: assert _match_feat("Lightning Reflexes") == LIGHTNING_REFLEXES def test_alertness(self) -> None: assert _match_feat("Alertness") == ALERTNESS def test_point_blank_shot(self) -> None: assert _match_feat("Point-Blank Shot") == POINT_BLANK_SHOT def test_precise_shot(self) -> None: assert _match_feat("Precise Shot") == PRECISE_SHOT def test_dodge(self) -> None: assert _match_feat("Dodge") == DODGE def test_weapon_focus_pistol(self) -> None: result = _match_feat("Weapon Focus (Pistol)") assert result is not None assert result.name == "Weapon Focus (Pistol)" assert result.effects[0].weapon_filter == "Pistol" def test_weapon_focus_longbow(self) -> None: result = _match_feat("Weapon Focus (Longbow)") assert result is not None assert result.name == "Weapon Focus (Longbow)" assert result.effects[0].weapon_filter == "Longbow" def test_case_insensitive(self) -> None: assert _match_feat("power attack") == POWER_ATTACK assert _match_feat("IRON WILL") == IRON_WILL def test_unknown_feat_returns_none(self) -> None: assert _match_feat("Summon Good Monster") is None assert _match_feat("Sacred Summons") is None assert _match_feat("Selective Channeling") is None def test_empty_string_returns_none(self) -> None: assert _match_feat("") is None assert _match_feat(" ") is None class TestExtractFeatures: """Given: a feats section from a Foundry sheet When: _extract_features is called Then: returns a deduplicated list of matched AbilitySpecs.""" def test_empty_dict(self) -> None: assert _extract_features({}) == [] def test_none(self) -> None: assert _extract_features(None) == [] def test_no_matching_feats(self) -> None: feats = {"feats": ["Summon Good Monster", "Sacred Summons"], "traits": ["Bitter"]} assert _extract_features(feats) == [] def test_matching_feats(self) -> None: feats = {"feats": ["Power Attack", "Iron Will"], "traits": [], "racial": []} result = _extract_features(feats) assert len(result) == 2 names = {f.name for f in result} assert names == {"Power Attack", "Iron Will"} def test_deduplication(self) -> None: feats = {"feats": ["Power Attack", "Power Attack"], "traits": ["Power Attack"]} result = _extract_features(feats) assert len(result) == 1 assert result[0] == POWER_ATTACK def test_traits_and_racial_scanned(self) -> None: feats = {"feats": [], "traits": ["Alertness"], "racial": ["Dodge"]} result = _extract_features(feats) assert len(result) == 2 names = {f.name for f in result} assert names == {"Alertness", "Dodge"} def test_non_string_items_skipped(self) -> None: feats = {"feats": ["Power Attack", 42, None, True]} result = _extract_features(feats) assert len(result) == 1 assert result[0] == POWER_ATTACK def test_non_list_values_skipped(self) -> None: feats = {"feats": "Power Attack", "traits": 42, "racial": None} assert _extract_features(feats) == [] class TestExtractSpells: """Given: a spellcasting section from a Foundry sheet When: _extract_spells is called Then: returns a deduplicated list of spell names.""" def test_empty_dict(self) -> None: assert _extract_spells({}) == [] def test_none(self) -> None: assert _extract_spells(None) == [] def test_no_spells_key(self) -> None: assert _extract_spells({"spellbooks": {}}) == [] def test_extracts_spell_names(self) -> None: sc = {"spells": [ {"name": "Fireball", "level": 3}, {"name": "Magic Missile", "level": 1}, ]} assert _extract_spells(sc) == ["Fireball", "Magic Missile"] def test_deduplication(self) -> None: sc = {"spells": [ {"name": "Fireball", "level": 3}, {"name": "Fireball", "level": 3}, ]} assert _extract_spells(sc) == ["Fireball"] def test_skips_non_dict_entries(self) -> None: sc = {"spells": ["Fireball", 42, None, {"name": "Cure Light Wounds", "level": 1}]} assert _extract_spells(sc) == ["Cure Light Wounds"] def test_skips_empty_names(self) -> None: sc = {"spells": [{"name": "", "level": 1}, {"name": "Fireball", "level": 3}]} assert _extract_spells(sc) == ["Fireball"] class TestRealSheetsFeatures: """Given: the 8 real Foundry sheets When: loaded with load_sheet_detailed Then: features and spells are extracted correctly.""" def test_esha_has_three_features(self) -> None: combatant, _ = load_sheet_detailed(_SHEETS / "esha_sheet.json") names = {f.name for f in combatant.features} assert "Point-Blank Shot" in names assert "Precise Shot" in names assert "Alertness" in names assert len(combatant.features) == 3 def test_esha_has_one_spell(self) -> None: combatant, _ = load_sheet_detailed(_SHEETS / "esha_sheet.json") assert "Unseen Servant" in combatant.spells assert len(combatant.spells) == 1 def test_harvie_has_no_features_no_spells(self) -> None: combatant, _ = load_sheet_detailed(_SHEETS / "harvie_sheet.json") assert combatant.features == [] assert combatant.spells == [] def test_ierlieth_has_power_attack(self) -> None: combatant, _ = load_sheet_detailed(_SHEETS / "ierlieth_sheet.json") names = {f.name for f in combatant.features} assert "Power Attack" in names assert combatant.spells == [] def test_jeanne_has_alertness(self) -> None: combatant, _ = load_sheet_detailed(_SHEETS / "jeanne_sheet.json") names = {f.name for f in combatant.features} assert "Alertness" in names def test_tammara_has_weapon_focus_pistol(self) -> None: combatant, _ = load_sheet_detailed(_SHEETS / "tammara_sheet.json") names = {f.name for f in combatant.features} assert "Weapon Focus (Pistol)" in names assert "Point-Blank Shot" in names assert "Precise Shot" in names assert len(combatant.features) == 3 def test_tammara_has_spells(self) -> None: combatant, _ = load_sheet_detailed(_SHEETS / "tammara_sheet.json") assert len(combatant.spells) > 0 assert "Cure Light Wounds" in combatant.spells def test_oni_has_spells(self) -> None: combatant, _ = load_sheet_detailed(_SHEETS / "oni_sheet.json") assert len(combatant.spells) == 20 assert "Cure Light Wounds" in combatant.spells assert "Guidance" in combatant.spells def test_misty_has_spells(self) -> None: combatant, _ = load_sheet_detailed(_SHEETS / "misty_sheet.json") assert len(combatant.spells) == 24 def test_nairda_has_spells(self) -> None: combatant, _ = load_sheet_detailed(_SHEETS / "nairda_sheet.json") assert len(combatant.spells) == 32 def test_all_sheets_features_are_ability_specs(self) -> None: for path in sorted(_SHEETS.glob("*.json")): combatant, _ = load_sheet_detailed(path) for feat in combatant.features: assert isinstance(feat, AbilitySpec) def test_all_sheets_spells_are_strings(self) -> None: for path in sorted(_SHEETS.glob("*.json")): combatant, _ = load_sheet_detailed(path) for spell in combatant.spells: assert isinstance(spell, str) assert spell def test_load_sheet_returns_features_and_spells(self) -> None: combatant = load_sheet(_SHEETS / "esha_sheet.json") assert len(combatant.features) == 3 assert len(combatant.spells) == 1