using PkmnLib.Dynamic.Events;
using PkmnLib.Dynamic.Models;
using PkmnLib.Plugin.Gen7.Scripts.Moves;
using PkmnLib.Static;

namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves;

/// <summary>
/// Tests for the <see cref="DragonAscent"/> move script.
/// Gen VII Bulbapedia behavior: "Dragon Ascent inflicts damage and then lowers the user's Defense stat
/// and Special Defense stat by one stage each."
/// </summary>
public class DragonAscentTests
{
    private static (DragonAscent script, IExecutingMove move, IPokemon user) CreateTestSetup()
    {
        var script = new DragonAscent();
        var move = Substitute.For<IExecutingMove>();
        var user = Substitute.For<IPokemon>();
        move.User.Returns(user);
        return (script, move, user);
    }

    /// <summary>
    /// Helper checking whether the given Pokémon received a ChangeStatBoost call for the given stat with
    /// a one-stage self-inflicted, non-forced drop.
    /// </summary>
    private static bool ReceivedSelfInflictedDrop(IPokemon pokemon, Statistic stat) =>
        pokemon.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ChangeStatBoost" &&
                                         (Statistic)c.GetArguments()[0]! == stat && (sbyte)c.GetArguments()[1]! == -1 &&
                                         (bool)c.GetArguments()[2]! && !(bool)c.GetArguments()[3]!);

    /// <summary>
    /// Bulbapedia: "lowers the user's Defense stat [...] by one stage". The stat change is self-inflicted
    /// and not forced.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_MoveHits_LowersUserDefenseByOneStage()
    {
        // Arrange
        var (script, move, user) = CreateTestSetup();
        var target = Substitute.For<IPokemon>();

        // Act
        script.OnSecondaryEffect(move, target, 0);

        // Assert
        await Assert.That(ReceivedSelfInflictedDrop(user, Statistic.Defense)).IsTrue();
    }

    /// <summary>
    /// Bulbapedia: "lowers the user's [...] Special Defense stat by one stage". The stat change is
    /// self-inflicted and not forced.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_MoveHits_LowersUserSpecialDefenseByOneStage()
    {
        // Arrange
        var (script, move, user) = CreateTestSetup();
        var target = Substitute.For<IPokemon>();

        // Act
        script.OnSecondaryEffect(move, target, 0);

        // Assert
        await Assert.That(ReceivedSelfInflictedDrop(user, Statistic.SpecialDefense)).IsTrue();
    }

    /// <summary>
    /// Bulbapedia: the drop hits the user's stats, not the target's. The target's stat stages are left
    /// untouched.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_MoveHits_DoesNotChangeTargetStats()
    {
        // Arrange
        var (script, move, _) = CreateTestSetup();
        var target = Substitute.For<IPokemon>();

        // Act
        script.OnSecondaryEffect(move, target, 0);

        // Assert
        await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ChangeStatBoost")).IsFalse();
    }

    /// <summary>
    /// Both stat drops are reported under the same event batch, so they display as a single combined
    /// event.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_MoveHits_BothStatDropsShareEventBatch()
    {
        // Arrange
        var (script, move, user) = CreateTestSetup();
        var target = Substitute.For<IPokemon>();

        // Act
        script.OnSecondaryEffect(move, target, 0);

        // Assert
        var batchIds = user.ReceivedCalls().Where(c => c.GetMethodInfo().Name == "ChangeStatBoost")
            .Select(c => (EventBatchId)c.GetArguments()[4]!).Distinct().ToList();
        await Assert.That(batchIds.Count).IsEqualTo(1);
    }
}