using PkmnLib.Dynamic.Models; using PkmnLib.Dynamic.ScriptHandling; using PkmnLib.Dynamic.ScriptHandling.Registry; using PkmnLib.Plugin.Gen7.Scripts.Moves; using PkmnLib.Plugin.Gen7.Scripts.Side; using PkmnLib.Static; using PkmnLib.Static.Utils; namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// /// Tests for the move script and its side effect script /// . /// Gen VII Bulbapedia behavior: "Healing Wish causes the user to faint, then switches in the new Pokémon. /// The new Pokémon's HP is restored and it is cured of any status conditions upon being sent out.", where /// from Generation V to VII "The receiving Pokémon is now sent out at the end of the turn rather than /// immediately." /// public class HealingWishTests { /// /// Creates a fully mocked test setup for the move script, with the user at /// side 0 and the given position, and the side's volatile scripts backed by a real /// . /// private static (HealingWish script, IExecutingMove move, IPokemon user, IScriptSet sideVolatile) CreateMoveSetup( byte position = 1, bool hasUsablePartyMembers = true) { var script = new HealingWish(); var move = Substitute.For(); var side = Substitute.For(); // ScriptSet.Add runs the IScriptPreventVolatileAdd hook over the owner's scripts; give the mock a // real (empty) iterator so the hook pass runs. side.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); IScriptSet sideVolatile = new ScriptSet(side); side.VolatileScripts.Returns(sideVolatile); var party = Substitute.For(); party.IsResponsibleForIndex(Arg.Any()).Returns(true); party.HasUsablePokemonNotInField().Returns(hasUsablePartyMembers); var battle = Substitute.For(); battle.Sides.Returns(new[] { side }); battle.Parties.Returns(new[] { party }); var battleData = Substitute.For(); battleData.Battle.Returns(battle); battleData.SideIndex.Returns((byte)0); battleData.Position.Returns(position); var user = Substitute.For(); user.BattleData.Returns(battleData); move.User.Returns(user); return (script, move, user, sideVolatile); } /// /// Creates a mocked replacement Pokémon with the given maximum HP for /// tests. /// private static IPokemon CreateReplacement(uint maxHp = 200) { var pokemon = Substitute.For(); pokemon.BoostedStats.Returns(new StatisticSet(maxHp, 0, 0, 0, 0, 0)); return pokemon; } /// /// Helper to extract the heal amount from a substitute's received Heal calls, or null when Heal was /// never called. /// private static uint? GetHealAmount(IPokemon pokemon) { var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Heal"); return call != null ? (uint)call.GetArguments()[0]! : null; } /// /// Bulbapedia: "Healing Wish causes the user to faint". /// [Test] public async Task OnSecondaryEffect_UserInBattle_UserFaints() { // Arrange var (script, move, user, _) = CreateMoveSetup(); // Act script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Faint")).IsTrue(); } /// /// Bulbapedia (Generations V to VII): "The receiving Pokémon is now sent out at the end of the turn /// rather than immediately." — using the move leaves a pending on the /// user's side instead of healing anything directly. /// [Test] public async Task OnSecondaryEffect_UserInBattle_AddsHealingWishEffectToUserSide() { // Arrange var (script, move, _, sideVolatile) = CreateMoveSetup(); // Act script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert await Assert.That(sideVolatile.Contains(ScriptUtils.ResolveName())).IsTrue(); } /// /// Bulbapedia: the wish benefits the Pokémon "sent out" in the user's place — the pending effect is /// bound to the position the user fainted at, so a replacement entering that position is healed. /// [Test] public async Task OnSecondaryEffect_UserAtPosition_EffectTargetsUsersPosition() { // Arrange var (script, move, _, sideVolatile) = CreateMoveSetup(1); script.OnSecondaryEffect(move, Substitute.For(), 0); var effect = (HealingWishEffect)sideVolatile.Get(new StringKey("healing_wish"))!.Script!; var replacement = CreateReplacement(); // Act effect.OnSwitchIn(replacement, 1); // Assert await Assert.That(GetHealAmount(replacement)).IsNotNull(); } /// /// Technical test: outside of battle (no ) the hook does nothing and /// the user does not faint. /// [Test] public async Task OnSecondaryEffect_NoBattleData_UserDoesNotFaint() { // Arrange var script = new HealingWish(); var move = Substitute.For(); var user = Substitute.For(); user.BattleData.Returns((IPokemonBattleData?)null); move.User.Returns(user); // Act script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Faint")).IsFalse(); } /// /// Bulbapedia: "Healing Wish will fail if its Trainer has no other conscious Pokémon in their party". /// [Test] public async Task OnSecondaryEffect_NoOtherConsciousPartyMembers_MoveFails() { // Arrange var (script, move, _, _) = CreateMoveSetup(hasUsablePartyMembers: false); var target = Substitute.For(); var hitData = Substitute.For(); move.GetHitData(target, 0).Returns(hitData); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(hitData.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Fail")).IsTrue(); } /// /// Bulbapedia: when the move fails for lack of other conscious party members, the user does not faint /// and no pending effect is left on the side. /// [Test] public async Task OnSecondaryEffect_NoOtherConsciousPartyMembers_UserDoesNotFaint() { // Arrange var (script, move, user, sideVolatile) = CreateMoveSetup(hasUsablePartyMembers: false); var target = Substitute.For(); move.GetHitData(target, 0).Returns(Substitute.For()); // Act script.OnSecondaryEffect(move, target, 0); // Assert await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Faint")).IsFalse(); await Assert.That(sideVolatile.Contains(ScriptUtils.ResolveName())).IsFalse(); } /// /// Bulbapedia: "The new Pokémon's HP is restored ... upon being sent out." — the replacement switching /// into the wisher's position is healed by its full maximum HP. /// [Test, Arguments(200u), Arguments(1u), Arguments(999u)] public async Task OnSwitchIn_MatchingPosition_HealsReplacementToFullHp(uint maxHp) { // Arrange var effect = new HealingWishEffect(1); var replacement = CreateReplacement(maxHp); // Act effect.OnSwitchIn(replacement, 1); // Assert await Assert.That(GetHealAmount(replacement)!.Value).IsEqualTo(maxHp); } /// /// Bulbapedia: "it is cured of any status conditions upon being sent out." /// [Test] public async Task OnSwitchIn_MatchingPosition_CuresStatus() { // Arrange var effect = new HealingWishEffect(1); var replacement = CreateReplacement(); // Act effect.OnSwitchIn(replacement, 1); // Assert await Assert.That(replacement.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ClearStatus")).IsTrue(); } /// /// Bulbapedia: the wish applies to the Pokémon sent out in the user's place — a Pokémon switching in /// at a different position on the side is not healed and does not consume the effect. /// [Test] public async Task OnSwitchIn_DifferentPosition_DoesNotHealAndEffectPersists() { // Arrange var side = Substitute.For(); side.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); IScriptSet sideVolatile = new ScriptSet(side); var effect = new HealingWishEffect(1); sideVolatile.Add(effect); var otherPokemon = CreateReplacement(); // Act effect.OnSwitchIn(otherPokemon, 0); // Assert await Assert.That(GetHealAmount(otherPokemon)).IsNull(); await Assert.That(otherPokemon.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ClearStatus")).IsFalse(); await Assert.That(sideVolatile.Contains(ScriptUtils.ResolveName())).IsTrue(); } /// /// Bulbapedia (Generations V to VII): the wish is consumed by the Pokémon sent out at the position — /// unlike Generation VIII onwards there is no check on the replacement's HP or status, so the effect /// is removed from the side after the first switch-in even if the replacement was already at full /// health. /// [Test] public async Task OnSwitchIn_MatchingPosition_EffectIsRemovedFromSide() { // Arrange var side = Substitute.For(); side.GetScripts().Returns(_ => new ScriptIterator(Array.Empty>())); IScriptSet sideVolatile = new ScriptSet(side); var effect = new HealingWishEffect(1); sideVolatile.Add(effect); // Act effect.OnSwitchIn(CreateReplacement(), 1); // Assert await Assert.That(sideVolatile.Contains(ScriptUtils.ResolveName())).IsFalse(); } }