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;

/// <summary>
/// Tests for the <see cref="DoomDesire"/> move script and its <see cref="DoomDesireEffect"/>.
/// 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."
/// </summary>
public class DoomDesireTests
{
    /// <summary>
    /// Creates a fully mocked test setup where the target is at the given position on a side whose
    /// volatile scripts are a real <see cref="ScriptSet"/>.
    /// </summary>
    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<IExecutingMove>();

        var side = Substitute.For<IBattleSide>();
        var sideScripts = new ScriptSet(side);
        side.VolatileScripts.Returns(sideScripts);
        side.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
        var battle = Substitute.For<IBattle>();
        battle.Sides.Returns(new[] { side });

        var target = Substitute.For<IPokemon>();
        var battleData = Substitute.For<IPokemonBattleData>();
        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<IHitData>();
        hitData.Damage.Returns(damage);
        move.GetHitData(target, 0).Returns(hitData);

        return (script, move, target, side, sideScripts, hitData);
    }

    /// <summary>
    /// 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 <see cref="DoomDesireEffect"/> on the target's
    /// side.
    /// </summary>
    [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<DoomDesireEffect>())).IsTrue();
    }

    /// <summary>
    /// The pending hit is registered against the target's position, so the effect knows where the damage
    /// must land two turns later.
    /// </summary>
    [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<DoomDesireEffect>(out var effect)).IsTrue();
        await Assert.That(effect!.HasTarget(0)).IsTrue();
    }

    /// <summary>
    /// 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.
    /// </summary>
    [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();
    }

    /// <summary>
    /// 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.
    /// </summary>
    [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<DoomDesireEffect>(out var effect)).IsTrue();
        var battle = Substitute.For<IBattle>();

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

    /// <summary>
    /// 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.
    /// </summary>
    [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<DoomDesireEffect>(out var effect)).IsTrue();
        var battle = Substitute.For<IBattle>();

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

    /// <summary>
    /// Once the pending strike has landed, the effect has no remaining targets and removes itself from
    /// the side.
    /// </summary>
    [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<DoomDesireEffect>(out var effect)).IsTrue();
        var battle = Substitute.For<IBattle>();

        // Act
        effect!.OnEndTurn(side, battle);
        effect.OnEndTurn(side, battle);
        effect.OnEndTurn(side, battle);

        // Assert
        await Assert.That(sideScripts.Contains(ScriptUtils.ResolveName<DoomDesireEffect>())).IsFalse();
    }

    /// <summary>
    /// Technical test: without battle data on the target (outside of battle) the script does nothing and
    /// does not throw.
    /// </summary>
    [Test]
    public async Task BlockOutgoingHit_TargetHasNoBattleData_DoesNotBlock()
    {
        // Arrange
        var script = new DoomDesire();
        var move = Substitute.For<IExecutingMove>();
        var target = Substitute.For<IPokemon>();
        target.BattleData.Returns((IPokemonBattleData?)null);
        var block = false;

        // Act
        script.BlockOutgoingHit(move, target, 0, ref block);

        // Assert
        await Assert.That(block).IsFalse();
    }
}