using PkmnLib.Dynamic.Models; using PkmnLib.Plugin.Gen7.Scripts.Moves; using PkmnLib.Static; using PkmnLib.Static.Species; using PkmnLib.Static.Utils; namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// /// Tests for the move script. /// Gen VII Bulbapedia behavior: "Magnetic Flux raises the Defense and Special Defense stats of allied /// Pokémon (including the user) with the Ability Plus or Minus by one stage each." /// public class MagneticFluxTests { private static (MagneticFlux script, IExecutingMove move, IBattlePokemon user) CreateTestSetup( params IBattlePokemon?[] sidePokemon) { var script = new MagneticFlux(); var side = Substitute.For(); side.Pokemon.Returns(sidePokemon); var user = Substitute.For(); var move = Substitute.For(); user.BattleSide.Returns(side); move.User.Returns(user); return (script, move, user); } private static IBattlePokemon CreatePokemonWithAbility(string? abilityName) { var pokemon = Substitute.For(); if (abilityName is null) { pokemon.ActiveAbility.Returns((IAbility?)null); } else { var ability = Substitute.For(); ability.Name.Returns(new StringKey(abilityName)); pokemon.ActiveAbility.Returns(ability); } return pokemon; } /// /// Helper to extract the arguments of all received ChangeStatBoost calls. Received-call inspection is /// used instead of NSubstitute argument matchers because the trailing EventBatchId parameter /// cannot be bound by Arg.Any (its parameterless constructor initializes a fresh id, so it /// never equals the matcher's default value). /// private static List GetStatBoostCalls(IBattlePokemon pokemon) => pokemon.ReceivedCalls().Where(c => c.GetMethodInfo().Name == "ChangeStatBoost").Select(c => c.GetArguments()) .ToList(); /// /// Bulbapedia: "Magnetic Flux raises the Defense and Special Defense stats of allied Pokémon /// (including the user) with the Ability Plus or Minus by one stage each." An ally with Plus has both /// its Defense and its Special Defense raised by one stage. /// [Test] public async Task OnSecondaryEffect_AllyWithPlus_DefenseAndSpecialDefenseRaisedByOneStage() { // Arrange var ally = CreatePokemonWithAbility("plus"); var (script, move, _) = CreateTestSetup(ally); // Act script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert var calls = GetStatBoostCalls(ally); await Assert.That(calls.Count).IsEqualTo(2); var boostedStats = calls.Select(args => (Statistic)args[0]!).ToList(); await Assert.That(boostedStats.Contains(Statistic.Defense)).IsTrue(); await Assert.That(boostedStats.Contains(Statistic.SpecialDefense)).IsTrue(); await Assert.That(calls.All(args => (sbyte)args[1]! == 1)).IsTrue(); } /// /// Bulbapedia: the boost applies to allied Pokémon "with the Ability Plus or Minus" — an ally with /// Minus is affected just like one with Plus. /// [Test] public async Task OnSecondaryEffect_AllyWithMinus_DefenseAndSpecialDefenseRaisedByOneStage() { // Arrange var ally = CreatePokemonWithAbility("minus"); var (script, move, _) = CreateTestSetup(ally); // Act script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert var calls = GetStatBoostCalls(ally); await Assert.That(calls.Count).IsEqualTo(2); var boostedStats = calls.Select(args => (Statistic)args[0]!).ToList(); await Assert.That(boostedStats.Contains(Statistic.Defense)).IsTrue(); await Assert.That(boostedStats.Contains(Statistic.SpecialDefense)).IsTrue(); await Assert.That(calls.All(args => (sbyte)args[1]! == 1)).IsTrue(); } /// /// Bulbapedia: only allied Pokémon "with the Ability Plus or Minus" are affected — an ally with a /// different ability does not have its stats raised. /// [Test] public async Task OnSecondaryEffect_AllyWithOtherAbility_StatsNotRaised() { // Arrange var ally = CreatePokemonWithAbility("levitate"); var (script, move, _) = CreateTestSetup(ally); // Act script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert await Assert.That(GetStatBoostCalls(ally).Count).IsEqualTo(0); } /// /// Bulbapedia: only allied Pokémon "with the Ability Plus or Minus" are affected — an ally without an /// active ability does not have its stats raised, and the script does not throw. /// [Test] public async Task OnSecondaryEffect_AllyWithoutActiveAbility_StatsNotRaised() { // Arrange var ally = CreatePokemonWithAbility(null); var (script, move, _) = CreateTestSetup(ally); // Act script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert await Assert.That(GetStatBoostCalls(ally).Count).IsEqualTo(0); } /// /// Bulbapedia: the boost applies to allied Pokémon "(including the user)" — when the user itself has /// Plus, its own Defense and Special Defense are raised, and the boost counts as self-inflicted. /// [Test] public async Task OnSecondaryEffect_UserWithPlus_BoostIsSelfInflicted() { // Arrange var (script, move, user) = CreateTestSetup(); var ability = Substitute.For(); ability.Name.Returns(new StringKey("plus")); user.ActiveAbility.Returns(ability); user.BattleSide.Pokemon.Returns(new IBattlePokemon?[] { user }); // Act script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert var calls = GetStatBoostCalls(user); await Assert.That(calls.Count).IsEqualTo(2); await Assert.That(calls.All(args => (bool)args[2]!)).IsTrue(); // self-inflicted for the user itself } /// /// Bulbapedia: "Magnetic Flux raises the Defense and Special Defense stats of allied Pokémon". A boost /// applied to an ally other than the user is not self-inflicted. /// [Test] public async Task OnSecondaryEffect_AllyWithPlus_BoostIsNotSelfInflicted() { // Arrange var ally = CreatePokemonWithAbility("plus"); var (script, move, _) = CreateTestSetup(ally); // Act script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert var calls = GetStatBoostCalls(ally); await Assert.That(calls.Count).IsEqualTo(2); await Assert.That(calls.All(args => (bool)args[2]!)).IsFalse(); // not self-inflicted } /// /// Bulbapedia: the boost applies to "allied Pokémon" with Plus or Minus — when multiple eligible /// allies are on the user's side, every one of them is raised. /// [Test] public async Task OnSecondaryEffect_MultipleEligibleAllies_AllAreRaised() { // Arrange var plusAlly = CreatePokemonWithAbility("plus"); var minusAlly = CreatePokemonWithAbility("minus"); var (script, move, _) = CreateTestSetup(plusAlly, minusAlly); // Act script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert await Assert.That(GetStatBoostCalls(plusAlly).Count).IsEqualTo(2); await Assert.That(GetStatBoostCalls(minusAlly).Count).IsEqualTo(2); } /// /// Technical test: empty (null) slots on the user's side are skipped without throwing, and eligible /// allies in other slots are still raised. /// [Test] public async Task OnSecondaryEffect_EmptyPokemonSlot_IsSkipped() { // Arrange var ally = CreatePokemonWithAbility("plus"); var (script, move, _) = CreateTestSetup(null, ally); // Act script.OnSecondaryEffect(move, Substitute.For(), 0); // Assert await Assert.That(GetStatBoostCalls(ally).Count).IsEqualTo(2); } }