using PkmnLib.Dynamic.Models;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Plugin.Gen7.Scripts.Moves;
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
using PkmnLib.Static;
using PkmnLib.Static.Species;
using PkmnLib.Static.Utils;
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
///
/// Tests for the move script and its volatile effect script
/// .
/// Bulbapedia: "Leech Seed plants a seed on the target.", with the in-game description "A seed is planted
/// on the target. It steals some HP from the target every turn." In Generation VII the seed drains 1/8 of
/// the seeded Pokémon's maximum HP at the end of each turn and restores it to the placer; Grass-type
/// Pokémon are unaffected by Leech Seed.
///
public class LeechSeedTests
{
///
/// Creates a fully mocked test setup for the move script, with a target of the
/// given types.
///
private static (LeechSeed script, IExecutingMove move, IPokemon target, IScriptSet targetVolatile, IHitData hitData)
CreateMoveSetup(params TypeIdentifier[] targetTypes)
{
var script = new LeechSeed();
var move = Substitute.For();
var user = Substitute.For();
move.User.Returns(user);
var target = Substitute.For();
target.Types.Returns(targetTypes);
var targetVolatile = Substitute.For();
target.Volatile.Returns(targetVolatile);
var hitData = Substitute.For();
move.GetHitData(target, 0).Returns(hitData);
return (script, move, target, targetVolatile, hitData);
}
///
/// Creates a mocked seeded Pokémon and placer for tests.
///
private static (LeechSeedEffect effect, IPokemon seeded, IPokemon placer) CreateEffectSetup(uint maxHp,
uint currentHp)
{
var seeded = Substitute.For();
seeded.MaxHealth.Returns(maxHp);
seeded.CurrentHealth.Returns(currentHp);
var placer = Substitute.For();
var effect = new LeechSeedEffect(seeded, placer);
return (effect, seeded, placer);
}
///
/// 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;
}
///
/// Helper to extract the damage amount from a substitute's received Damage calls, or null when Damage
/// was never called.
///
private static uint? GetDamageAmount(IPokemon pokemon)
{
var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
return call != null ? (uint)call.GetArguments()[0]! : null;
}
///
/// Bulbapedia: "Leech Seed plants a seed on the target." — using the move places the
/// on the target.
///
[Test]
public async Task OnSecondaryEffect_NonGrassTarget_AddsLeechSeedEffect()
{
// Arrange
var (script, move, target, targetVolatile, hitData) = CreateMoveSetup(new TypeIdentifier(11, "water"));
// Act
script.OnSecondaryEffect(move, target, 0);
// Assert
targetVolatile.Received(1).Add(Arg.Any());
hitData.DidNotReceive().Fail();
await Assert.That(targetVolatile.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Add")).IsTrue();
}
///
/// Grass-type Pokémon are unaffected by Leech Seed — against a Grass-type target the move fails and no
/// seed is planted.
///
[Test]
public async Task OnSecondaryEffect_GrassTarget_MoveFailsAndNoSeedPlanted()
{
// Arrange
var (script, move, target, targetVolatile, hitData) = CreateMoveSetup(new TypeIdentifier(4, "grass"));
// Act
script.OnSecondaryEffect(move, target, 0);
// Assert
hitData.Received(1).Fail();
await Assert.That(targetVolatile.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Add")).IsFalse();
}
///
/// Grass-type Pokémon are unaffected by Leech Seed — this includes dual types where grass is the
/// secondary type.
///
[Test]
public async Task OnSecondaryEffect_DualTypeGrassTarget_MoveFails()
{
// Arrange
var (script, move, target, _, hitData) =
CreateMoveSetup(new TypeIdentifier(7, "poison"), new TypeIdentifier(4, "grass"));
// Act
script.OnSecondaryEffect(move, target, 0);
// Assert
hitData.Received(1).Fail();
await Assert.That(hitData.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Fail")).IsTrue();
}
///
/// Bulbapedia: "It steals some HP from the target every turn." — at the end of each turn the seeded
/// Pokémon loses 1/8 of its maximum HP (integer division) and the placer is healed by the same amount.
///
[Test, Arguments(80u, 10u), Arguments(100u, 12u), Arguments(1000u, 125u)]
public async Task OnEndTurn_SeededPokemon_DrainsOneEighthOfMaxHpToPlacer(uint maxHp, uint expectedDrain)
{
// Arrange
var (effect, seeded, placer) = CreateEffectSetup(maxHp, maxHp);
// Act
effect.OnEndTurn(Substitute.For(), Substitute.For());
// Assert
await Assert.That(GetDamageAmount(seeded)!.Value).IsEqualTo(expectedDrain);
await Assert.That(GetHealAmount(placer)!.Value).IsEqualTo(expectedDrain);
}
///
/// The drained amount cannot exceed the seeded Pokémon's remaining HP — with less HP left than the 1/8
/// drain, only the remaining HP is drained and transferred.
///
[Test]
public async Task OnEndTurn_SeededPokemonHpBelowDrain_DrainCappedAtRemainingHp()
{
// Arrange - 1/8 of 80 is 10, but only 4 HP remains
var (effect, seeded, placer) = CreateEffectSetup(80, 4);
// Act
effect.OnEndTurn(Substitute.For(), Substitute.For());
// Assert
await Assert.That(GetDamageAmount(seeded)!.Value).IsEqualTo(4u);
await Assert.That(GetHealAmount(placer)!.Value).IsEqualTo(4u);
}
///
/// When the seeded Pokémon has the Liquid Ooze Ability, the placer loses the drained HP instead of
/// being healed by it.
///
[Test]
public async Task OnEndTurn_SeededPokemonHasLiquidOoze_PlacerDamagedInsteadOfHealed()
{
// Arrange
var (effect, seeded, placer) = CreateEffectSetup(80, 80);
var ability = Substitute.For();
ability.Name.Returns(new StringKey("liquid_ooze"));
seeded.ActiveAbility.Returns(ability);
// Act
effect.OnEndTurn(Substitute.For(), Substitute.For());
// Assert
await Assert.That(GetDamageAmount(placer)!.Value).IsEqualTo(10u);
await Assert.That(placer.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Heal")).IsFalse();
}
///
/// The end-of-turn drain is indirect damage, not move damage — both the seeded Pokémon's HP loss and a
/// Liquid Ooze recoil use .
///
[Test]
public async Task OnEndTurn_SeededPokemon_UsesMiscDamageSource()
{
// Arrange
var (effect, seeded, _) = CreateEffectSetup(80, 80);
// Act
effect.OnEndTurn(Substitute.For(), Substitute.For());
// Assert
var call = seeded.ReceivedCalls().First(c => c.GetMethodInfo().Name == "Damage");
await Assert.That((DamageSource)call.GetArguments()[1]!).IsEqualTo(DamageSource.Misc);
}
}