using PkmnLib.Dynamic.Models; using PkmnLib.Plugin.Gen7.Scripts.Moves; using PkmnLib.Static; using PkmnLib.Static.Utils; using TUnit.Assertions.AssertConditions.Throws; namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// /// Tests for the script, which implements the secondary effect of Acid. /// Behavior is verified against the Bulbapedia page for Acid. /// Gen VII behavior: "Acid inflicts damage to all adjacent opponents" and "has a 10% chance of lowering the /// target's Special Defense stat by one stage". The 10% proc chance and the targeting are handled by the /// generic secondary effect data (chance: 10 in Moves.jsonc); this script is responsible for applying the /// stat change once the effect procs. /// public class ChangeTargetSpecialDefenseTests { private static ChangeTargetSpecialDefense CreateInitializedScript(int amount = -1) { var script = new ChangeTargetSpecialDefense(); script.OnInitialize(new Dictionary { { "amount", amount } }); return script; } /// /// Helper to extract the arguments of the ChangeStatBoost call received by a substitute target. /// private static object?[]? GetStatBoostCallArgs(IPokemon target) { var call = target.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "ChangeStatBoost"); return call?.GetArguments(); } /// /// Bulbapedia: "Acid now has a 10% chance of lowering the target's Special Defense stat by one stage instead." /// (Generation IV onwards.) When the secondary effect procs, the target's Special Defense is lowered by one stage. /// [Test] public async Task OnSecondaryEffect_TargetIsOpponent_LowersSpecialDefenseByOneStage() { // Arrange var script = CreateInitializedScript(); var move = Substitute.For(); var user = Substitute.For(); move.User.Returns(user); var target = Substitute.For(); // Act script.OnSecondaryEffect(move, target, 0); // Assert var args = GetStatBoostCallArgs(target); await Assert.That(args).IsNotNull(); await Assert.That((Statistic)args![0]!).IsEqualTo(Statistic.SpecialDefense); await Assert.That((sbyte)args[1]!).IsEqualTo((sbyte)-1); } /// /// Bulbapedia (Generation IV onwards): "lowering the target's Special Defense stat by one stage instead." /// The word "instead" marks the change from the Gen I/II Defense drop — the script must not touch /// the target's (physical) Defense stat. /// [Test] public async Task OnSecondaryEffect_TargetIsOpponent_DoesNotLowerDefense() { // Arrange var script = CreateInitializedScript(); var move = Substitute.For(); var user = Substitute.For(); move.User.Returns(user); var target = Substitute.For(); // Act script.OnSecondaryEffect(move, target, 0); // Assert - exactly one stat change, and it is not Defense var calls = target.ReceivedCalls().Where(c => c.GetMethodInfo().Name == "ChangeStatBoost").ToList(); await Assert.That(calls.Count).IsEqualTo(1); await Assert.That((Statistic)calls[0].GetArguments()[0]!).IsNotEqualTo(Statistic.Defense); } /// /// Bulbapedia: "lowering the target's Special Defense stat". /// The stat drop is inflicted on an opponent, so it must not be marked as self-inflicted /// (self-inflicted drops bypass effects that prevent stat lowering by opponents, e.g. Clear Body/Mist). /// [Test] public async Task OnSecondaryEffect_TargetIsOpponent_NotSelfInflicted() { // Arrange var script = CreateInitializedScript(); var move = Substitute.For(); var user = Substitute.For(); move.User.Returns(user); var target = Substitute.For(); // Act script.OnSecondaryEffect(move, target, 0); // Assert var args = GetStatBoostCallArgs(target); await Assert.That(args).IsNotNull(); await Assert.That((bool)args![2]!).IsFalse(); } /// /// Bulbapedia: "a 10% chance of lowering the target's Special Defense stat". /// The drop is an ordinary stat change, not a forced one — it must respect prevention effects, /// so the script may not pass force = true. /// [Test] public async Task OnSecondaryEffect_TargetIsOpponent_StatDropIsNotForced() { // Arrange var script = CreateInitializedScript(); var move = Substitute.For(); var user = Substitute.For(); move.User.Returns(user); var target = Substitute.For(); // Act script.OnSecondaryEffect(move, target, 0); // Assert var args = GetStatBoostCallArgs(target); await Assert.That(args).IsNotNull(); await Assert.That((bool)args![3]!).IsFalse(); } /// /// Technical test: the shared ChangeTargetStats base marks the stat change as self-inflicted /// when the target of the secondary effect is the move's own user. /// [Test] public async Task OnSecondaryEffect_TargetIsUser_SelfInflicted() { // Arrange var script = CreateInitializedScript(); var move = Substitute.For(); var user = Substitute.For(); move.User.Returns(user); // Act - the user is hit by its own move (e.g. redirected) script.OnSecondaryEffect(move, user, 0); // Assert var args = GetStatBoostCallArgs(user); await Assert.That(args).IsNotNull(); await Assert.That((bool)args![2]!).IsTrue(); } /// /// Technical test: the 'amount' parameter from the move data (amount: -1 for Acid in Moves.jsonc) /// is parsed by OnInitialize and passed through to the stat change unmodified. /// [Test, Arguments(-1), Arguments(-2)] public async Task OnInitialize_AmountParameter_PassedToStatBoost(int amount) { // Arrange var script = CreateInitializedScript(amount); var move = Substitute.For(); var user = Substitute.For(); move.User.Returns(user); var target = Substitute.For(); // Act script.OnSecondaryEffect(move, target, 0); // Assert var args = GetStatBoostCallArgs(target); await Assert.That(args).IsNotNull(); await Assert.That((sbyte)args![1]!).IsEqualTo((sbyte)amount); } /// /// Technical test: OnInitialize rejects a null parameter dictionary. /// [Test] public async Task OnInitialize_NullParameters_ThrowsArgumentNullException() { // Arrange var script = new ChangeTargetSpecialDefense(); // Act & Assert await Assert.That(() => script.OnInitialize(null)).ThrowsExactly(); } /// /// Technical test: OnInitialize rejects parameters without the required 'amount' entry. /// [Test] public async Task OnInitialize_MissingAmount_ThrowsArgumentException() { // Arrange var script = new ChangeTargetSpecialDefense(); // Act & Assert await Assert.That(() => script.OnInitialize(new Dictionary())) .ThrowsExactly(); } }