"""Unit tests for the combatant loader feat resolution and feature/spell handling.""" from __future__ import annotations import json from typing import TYPE_CHECKING if TYPE_CHECKING: from pathlib import Path from pf1e_simulator.abilities import ( DODGE, IRON_WILL, POWER_ATTACK, toughness, weapon_focus, ) from pf1e_simulator.loaders.combatant import ( CombatantLoadError, MonsterLoadError, _resolve_feat, _resolve_features, load_combatant, load_monster, ) # ── _resolve_feat ───────────────────────────────────────────────────────────── class TestResolveFeat: def test_known_feat_returns_constant(self) -> None: assert _resolve_feat("Power Attack", 1) is POWER_ATTACK def test_known_feat_case_insensitive(self) -> None: assert _resolve_feat("power attack", 1) is POWER_ATTACK assert _resolve_feat("IRON WILL", 1) is IRON_WILL def test_toughness_uses_level(self) -> None: feat = _resolve_feat("Toughness", 5) assert feat.name == "Toughness" assert feat.category == "feat" assert feat.effects == toughness(5).effects def test_toughness_different_levels(self) -> None: low = _resolve_feat("Toughness", 1) high = _resolve_feat("Toughness", 10) assert low.effects != high.effects def test_weapon_focus_extracts_weapon(self) -> None: feat = _resolve_feat("Weapon Focus (Longsword)", 1) assert feat.effects == weapon_focus("Longsword").effects def test_weapon_focus_case_insensitive(self) -> None: feat = _resolve_feat("weapon focus (dagger)", 1) assert feat.effects == weapon_focus("dagger").effects def test_unknown_feat_returns_minimal_spec(self) -> None: feat = _resolve_feat("Lateral Thinking", 1) assert feat.name == "Lateral Thinking" assert feat.category == "feat" assert feat.effects == () def test_unknown_feat_preserves_whitespace_stripping(self) -> None: feat = _resolve_feat(" Some Feat ", 1) assert feat.name == "Some Feat" def test_dodge_and_alertness(self) -> None: assert _resolve_feat("Dodge", 1) is DODGE # ── _resolve_features ───────────────────────────────────────────────────────── class TestResolveFeatures: def test_filters_empty_and_whitespace(self) -> None: result = _resolve_features(["Power Attack", "", " ", "Dodge"], 1) assert len(result) == 2 assert result[0] is POWER_ATTACK assert result[1] is DODGE def test_filters_non_strings(self) -> None: result = _resolve_features(["Iron Will", 42, None, True], 1) # type: ignore[list-item] assert len(result) == 1 def test_empty_list(self) -> None: assert _resolve_features([], 1) == [] def test_all_unknown(self) -> None: result = _resolve_features(["Feat A", "Feat B"], 1) assert len(result) == 2 assert all(f.effects == () for f in result) # ── load_combatant features+spells ──────────────────────────────────────────── def _write(tmp_path: Path, payload: dict[str, object]) -> Path: target = tmp_path / "combatant.json" target.write_text(json.dumps(payload), encoding="utf-8") return target def _minimal_payload(**extra: object) -> dict[str, object]: base: dict[str, object] = { "name": "Tester", "level": 3, "abilities": { "str_score": 10, "dex_score": 10, "con_score": 10, "int_score": 10, "wis_score": 10, "cha_score": 10, }, "hp_max": 10, "ac": {"total": 10, "touch": 10, "flat_footed": 10}, "bab": 1, "initiative_mod": 0, "speed_land_ft": 30, "saves": {"fort": 0, "ref": 0, "will": 0}, "attacks": [], } base.update(extra) return base class TestFeatures: def test_known_feats_resolved(self, tmp_path: Path) -> None: data = _minimal_payload(features=["Power Attack", "Iron Will"]) c = load_combatant(_write(tmp_path, data)) assert len(c.features) == 2 assert c.features[0] is POWER_ATTACK assert c.features[1] is IRON_WILL def test_unknown_feat_preserved(self, tmp_path: Path) -> None: data = _minimal_payload(features=["Some Unknown Feat"]) c = load_combatant(_write(tmp_path, data)) assert len(c.features) == 1 assert c.features[0].name == "Some Unknown Feat" def test_toughness_receives_level(self, tmp_path: Path) -> None: data = _minimal_payload(level=7, features=["Toughness"]) c = load_combatant(_write(tmp_path, data)) assert c.features[0].effects == toughness(7).effects def test_level_default_one(self, tmp_path: Path) -> None: data = _minimal_payload(level=1, features=["Toughness"]) c = load_combatant(_write(tmp_path, data)) assert c.features[0].effects == toughness(1).effects def test_no_features_field(self, tmp_path: Path) -> None: data = _minimal_payload() c = load_combatant(_write(tmp_path, data)) assert c.features == [] def test_empty_features_list(self, tmp_path: Path) -> None: data = _minimal_payload(features=[]) c = load_combatant(_write(tmp_path, data)) assert c.features == [] class TestSpells: def test_spells_passed_through(self, tmp_path: Path) -> None: data = _minimal_payload(spells=["Magic Missile", "Shield"]) c = load_combatant(_write(tmp_path, data)) assert c.spells == ["Magic Missile", "Shield"] def test_no_spells_field(self, tmp_path: Path) -> None: data = _minimal_payload() c = load_combatant(_write(tmp_path, data)) assert c.spells == [] def test_empty_spells_list(self, tmp_path: Path) -> None: data = _minimal_payload(spells=[]) c = load_combatant(_write(tmp_path, data)) assert c.spells == [] class TestBackwardCompatAliases: def test_monster_load_error_is_combatant_load_error(self) -> None: assert MonsterLoadError is CombatantLoadError def test_load_monster_delegates(self, tmp_path: Path) -> None: data = _minimal_payload(name="Alias Test") c = load_monster(_write(tmp_path, data)) assert c.name == "Alias Test"