using PkmnLib.Dynamic.Libraries;
using PkmnLib.Dynamic.Models;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Plugin.Gen7.Scripts.Moves;
using PkmnLib.Static;
using PkmnLib.Static.Libraries;
using PkmnLib.Static.Utils;
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
///
/// Tests for the move script.
/// Behavior is verified against the Bulbapedia page for Forest's Curse (Generation VII behavior).
///
public class ForestsCurseTests
{
private static readonly TypeIdentifier GrassType = new(5, "grass");
private static readonly TypeIdentifier NormalType = new(1, "normal");
private static readonly TypeIdentifier GhostType = new(8, "ghost");
///
/// Creates a fully mocked test setup for Forest's Curse tests. The target is in battle, with a mocked
/// battle library chain that can resolve the Grass type identifier.
///
private static (ForestsCurse script, IExecutingMove move, IPokemon target, IHitData hitData) CreateTestSetup(
bool grassTypeInLibrary = true, TypeIdentifier[]? targetTypes = null)
{
var script = new ForestsCurse();
var move = Substitute.For();
var target = Substitute.For();
var hitData = Substitute.For();
move.GetHitData(target, 0).Returns(hitData);
var typeLibrary = Substitute.For();
if (grassTypeInLibrary)
{
typeLibrary.TryGetTypeIdentifier(new StringKey("grass"), out Arg.Any()).Returns(x =>
{
x[1] = GrassType;
return true;
});
}
var staticLibrary = Substitute.For();
staticLibrary.Types.Returns(typeLibrary);
var library = Substitute.For();
library.StaticLibrary.Returns(staticLibrary);
var battle = Substitute.For();
battle.Library.Returns(library);
var battleData = Substitute.For();
battleData.Battle.Returns(battle);
target.BattleData.Returns(battleData);
target.Types.Returns(targetTypes ?? [NormalType]);
return (script, move, target, hitData);
}
///
/// Helper to extract the type passed to the target's call.
///
private static TypeIdentifier? GetAddedType(IPokemon target)
{
var call = target.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "AddType");
return call != null ? (TypeIdentifier)call.GetArguments()[0]! : null;
}
///
/// Bulbapedia: "Forest's Curse adds the Grass type to the target, in addition to the Pokémon's
/// original type(s)."
///
[Test]
public async Task OnSecondaryEffect_TargetInBattle_AddsGrassTypeToTarget()
{
// Arrange
var (script, move, target, _) = CreateTestSetup();
// Act
script.OnSecondaryEffect(move, target, 0);
// Assert
var addedType = GetAddedType(target);
await Assert.That(addedType).IsNotNull();
await Assert.That(addedType!.Value).IsEqualTo(GrassType);
}
///
/// Bulbapedia: the Grass type is added "in addition to the Pokémon's original type(s)".
/// The target's original types may not be replaced, so must not be used.
///
[Test]
public async Task OnSecondaryEffect_TargetInBattle_OriginalTypesNotReplaced()
{
// Arrange
var (script, move, target, _) = CreateTestSetup();
// Act
script.OnSecondaryEffect(move, target, 0);
// Assert
await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "SetTypes")).IsFalse();
}
///
/// Bulbapedia: Forest's Curse fails if the target is already Grass-type ("The move fails against
/// Grass-type targets"), so the hit must be marked as failed and no type may be added.
///
[Test]
public async Task OnSecondaryEffect_TargetAlreadyGrassType_HitFails()
{
// Arrange
var (script, move, target, hitData) = CreateTestSetup(targetTypes: [GrassType]);
// Act
script.OnSecondaryEffect(move, target, 0);
// Assert
await Assert.That(hitData.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Fail")).IsTrue();
}
///
/// If the target has no , the script cannot resolve the type library,
/// so no type may be added.
///
[Test]
public async Task OnSecondaryEffect_NullBattleData_DoesNotAddType()
{
// Arrange
var (script, move, target, _) = CreateTestSetup();
target.BattleData.Returns((IPokemonBattleData?)null);
// Act
script.OnSecondaryEffect(move, target, 0);
// Assert
await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "AddType")).IsFalse();
}
///
/// Technical test: when the type library cannot resolve the Grass type identifier, the script must
/// bail out without adding a type.
///
[Test]
public async Task OnSecondaryEffect_GrassTypeMissingFromLibrary_DoesNotAddType()
{
// Arrange
var (script, move, target, _) = CreateTestSetup(false);
// Act
script.OnSecondaryEffect(move, target, 0);
// Assert
await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "AddType")).IsFalse();
}
///
/// Bulbapedia: "If the target already has an additional type added to it by Trick-or-Treat, that type is
/// replaced with the Grass type."
/// A target whose Ghost type was added by must end up with the Grass type and
/// without the Ghost type, while keeping its original type. The mocked target uses a real
/// for its volatile scripts, so the HasHadTypeAddedEffect marker set by
/// Trick-or-Treat is visible to Forest's Curse, and reacts to and
/// with a backing type list, mirroring the real Pokemon implementation,
/// so the resulting type list is observable.
///
[Test]
public async Task OnSecondaryEffect_TargetHasTrickOrTreatGhostType_GhostReplacedByGrass()
{
// Arrange
var forestsCurse = new ForestsCurse();
var trickOrTreat = new TrickOrTreat();
var move = Substitute.For();
var target = Substitute.For();
var typeLibrary = Substitute.For();
typeLibrary.TryGetTypeIdentifier(new StringKey("grass"), out Arg.Any()).Returns(x =>
{
x[1] = GrassType;
return true;
});
typeLibrary.TryGetTypeIdentifier(new StringKey("ghost"), out Arg.Any()).Returns(x =>
{
x[1] = GhostType;
return true;
});
var staticLibrary = Substitute.For();
staticLibrary.Types.Returns(typeLibrary);
var library = Substitute.For();
library.StaticLibrary.Returns(staticLibrary);
var battle = Substitute.For();
battle.Library.Returns(library);
move.Battle.Returns(battle);
var battleData = Substitute.For();
battleData.Battle.Returns(battle);
target.BattleData.Returns(battleData);
// Real script set so the HasHadTypeAddedEffect marker added by Trick-or-Treat is visible
// to Forest's Curse.
target.GetScripts().Returns(_ => new ScriptIterator(new List>()));
IScriptSet targetVolatile = new ScriptSet(target);
target.Volatile.Returns(targetVolatile);
// Backing type list that reacts to AddType/RemoveType like the real Pokemon implementation.
var types = new List { NormalType };
target.Types.Returns(_ => types);
target.AddType(Arg.Any()).Returns(ci =>
{
var type = ci.Arg();
if (types.Contains(type))
return false;
types.Add(type);
return true;
});
target.RemoveType(Arg.Any()).Returns(ci => types.Remove(ci.Arg()));
// Trick-or-Treat first adds the Ghost type to the target.
trickOrTreat.OnSecondaryEffect(move, target, 0);
// Act
forestsCurse.OnSecondaryEffect(move, target, 0);
// Assert - the Ghost type added by Trick-or-Treat is replaced by Grass; the original type remains.
await Assert.That(types.Contains(GrassType)).IsTrue();
await Assert.That(types.Contains(GhostType)).IsFalse();
await Assert.That(types.Contains(NormalType)).IsTrue();
}
}