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();
}
}