using PkmnLib.Dynamic.Models;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Dynamic.ScriptHandling.Registry;
using PkmnLib.Plugin.Gen7.Scripts.Moves;
using PkmnLib.Plugin.Gen7.Scripts.Side;
namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;
///
/// Tests for the move script and its .
/// Gen VII Bulbapedia behavior: "On the turn Doom Desire is selected, this attack will do nothing other
/// than state that the user has chosen Doom Desire as its destiny. Two turns later, Doom Desire will do
/// damage against the target."
///
public class DoomDesireTests
{
///
/// Creates a fully mocked test setup where the target is at the given position on a side whose
/// volatile scripts are a real .
///
private static (DoomDesire script, IExecutingMove move, IPokemon target, IBattleSide side, ScriptSet sideScripts,
IHitData hitData) CreateTestSetup(byte position = 0, uint damage = 100)
{
var script = new DoomDesire();
var move = Substitute.For();
var side = Substitute.For();
var sideScripts = new ScriptSet(side);
side.VolatileScripts.Returns(sideScripts);
side.GetScripts().Returns(_ => new ScriptIterator(new List>()));
var battle = Substitute.For();
battle.Sides.Returns(new[] { side });
var target = Substitute.For();
var battleData = Substitute.For();
battleData.Battle.Returns(battle);
battleData.SideIndex.Returns((byte)0);
battleData.Position.Returns(position);
target.BattleData.Returns(battleData);
side.Pokemon.Returns(new IPokemon?[] { target });
var hitData = Substitute.For();
hitData.Damage.Returns(damage);
move.GetHitData(target, 0).Returns(hitData);
return (script, move, target, side, sideScripts, hitData);
}
///
/// Bulbapedia: "On the turn Doom Desire is selected, this attack will do nothing" — the immediate hit
/// is blocked, and the pending damage is stored as a on the target's
/// side.
///
[Test]
public async Task BlockOutgoingHit_FirstUse_BlocksHitAndAddsSideEffect()
{
// Arrange
var (script, move, target, _, sideScripts, _) = CreateTestSetup();
var block = false;
// Act
script.BlockOutgoingHit(move, target, 0, ref block);
// Assert
await Assert.That(block).IsTrue();
await Assert.That(sideScripts.Contains(ScriptUtils.ResolveName())).IsTrue();
}
///
/// The pending hit is registered against the target's position, so the effect knows where the damage
/// must land two turns later.
///
[Test]
public async Task BlockOutgoingHit_FirstUse_RegistersTargetPosition()
{
// Arrange
var (script, move, target, _, sideScripts, _) = CreateTestSetup();
var block = false;
// Act
script.BlockOutgoingHit(move, target, 0, ref block);
// Assert
await Assert.That(sideScripts.TryGet(out var effect)).IsTrue();
await Assert.That(effect!.HasTarget(0)).IsTrue();
}
///
/// Bulbapedia: Doom Desire fails when it is already in effect against the target. The second use
/// fails the hit instead of queueing a second strike.
///
[Test]
public async Task BlockOutgoingHit_AlreadyPendingForTarget_FailsHit()
{
// Arrange
var (script, move, target, _, _, hitData) = CreateTestSetup();
var block = false;
script.BlockOutgoingHit(move, target, 0, ref block);
block = false;
// Act
script.BlockOutgoingHit(move, target, 0, ref block);
// Assert
hitData.Received(1).Fail();
await Assert.That(block).IsFalse();
}
///
/// Bulbapedia: "Two turns later, Doom Desire will do damage against the target." One end-of-turn tick
/// after selection (the selection turn itself) is not enough for the strike to land.
///
[Test]
public async Task OnEndTurn_TwoTurnTicks_DoesNotDamageTargetYet()
{
// Arrange
var (script, move, target, side, sideScripts, _) = CreateTestSetup();
var block = false;
script.BlockOutgoingHit(move, target, 0, ref block);
await Assert.That(sideScripts.TryGet(out var effect)).IsTrue();
var battle = Substitute.For();
// Act - end of the selection turn and of the first turn after it
effect!.OnEndTurn(side, battle);
effect.OnEndTurn(side, battle);
// Assert
await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage")).IsFalse();
}
///
/// Bulbapedia: "Two turns later, Doom Desire will do damage against the target." At the end of the
/// second turn after selection the stored damage is dealt to the Pokémon at the registered position.
///
[Test]
public async Task OnEndTurn_ThreeTurnTicks_DamagesTargetWithStoredDamage()
{
// Arrange
var (script, move, target, side, sideScripts, _) = CreateTestSetup(damage: 123);
var block = false;
script.BlockOutgoingHit(move, target, 0, ref block);
await Assert.That(sideScripts.TryGet(out var effect)).IsTrue();
var battle = Substitute.For();
// Act - selection turn plus the two turns after it
effect!.OnEndTurn(side, battle);
effect.OnEndTurn(side, battle);
effect.OnEndTurn(side, battle);
// Assert
var damageCall = target.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
await Assert.That(damageCall).IsNotNull();
await Assert.That((uint)damageCall!.GetArguments()[0]!).IsEqualTo(123u);
}
///
/// Once the pending strike has landed, the effect has no remaining targets and removes itself from
/// the side.
///
[Test]
public async Task OnEndTurn_StrikeLanded_EffectRemovesItself()
{
// Arrange
var (script, move, target, side, sideScripts, _) = CreateTestSetup();
var block = false;
script.BlockOutgoingHit(move, target, 0, ref block);
await Assert.That(sideScripts.TryGet(out var effect)).IsTrue();
var battle = Substitute.For();
// Act
effect!.OnEndTurn(side, battle);
effect.OnEndTurn(side, battle);
effect.OnEndTurn(side, battle);
// Assert
await Assert.That(sideScripts.Contains(ScriptUtils.ResolveName())).IsFalse();
}
///
/// Technical test: without battle data on the target (outside of battle) the script does nothing and
/// does not throw.
///
[Test]
public async Task BlockOutgoingHit_TargetHasNoBattleData_DoesNotBlock()
{
// Arrange
var script = new DoomDesire();
var move = Substitute.For();
var target = Substitute.For();
target.BattleData.Returns((IPokemonBattleData?)null);
var block = false;
// Act
script.BlockOutgoingHit(move, target, 0, ref block);
// Assert
await Assert.That(block).IsFalse();
}
}