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="Acupressure"/> move script.
/// Gen VII Bulbapedia behavior: "Acupressure chooses one of the target's stats at random and raises it by
/// two stages. It can raise either the target's Attack, Defense, Special Attack, Special Defense, Speed,
/// accuracy, or evasion stat but will not attempt to raise a stat that is already maximized, meaning that
/// the move will fail if all stats are maximized."
/// </summary>
public class AcupressureTests
{
    private static (Acupressure script, IExecutingMove move, IPokemon target, IBattleRandom random, IHitData hitData)
        CreateTestSetup()
    {
        var script = new Acupressure();
        var move = Substitute.For<IExecutingMove>();
        var target = Substitute.For<IPokemon>();
        var hitData = Substitute.For<IHitData>();
        move.GetHitData(target, 0).Returns(hitData);

        var random = Substitute.For<IBattleRandom>();
        var battle = Substitute.For<IBattle>();
        battle.Random.Returns(random);
        var battleData = Substitute.For<IPokemonBattleData>();
        battleData.Battle.Returns(battle);

        var user = Substitute.For<IPokemon>();
        user.BattleData.Returns(battleData);
        move.User.Returns(user);

        target.StatBoost.Returns(new StatBoostStatisticSet());

        return (script, move, target, random, hitData);
    }

    /// <summary>
    /// Helper to extract the stat argument of a ChangeStatBoost call received by the target.
    /// </summary>
    private static object?[]? GetStatBoostCallArgs(IPokemon target)
    {
        var call = target.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "ChangeStatBoost");
        return call?.GetArguments();
    }

    /// <summary>
    /// Bulbapedia: "Acupressure chooses one of the target's stats at random and raises it by two stages."
    /// When the random roll selects Attack, the target's Attack is raised by exactly two stages.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_RandomSelectsAttack_RaisesAttackByTwoStages()
    {
        // Arrange
        var (script, move, target, random, _) = CreateTestSetup();
        // Attack is the first entry in the pool of raisable stats (HP is excluded).
        random.GetInt(Arg.Any<int>(), Arg.Any<int>()).Returns(0);

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

        // Assert
        var args = GetStatBoostCallArgs(target);
        await Assert.That(args).IsNotNull();
        await Assert.That((Statistic)args![0]!).IsEqualTo(Statistic.Attack);
        await Assert.That((sbyte)args[1]!).IsEqualTo((sbyte)2);
    }

    /// <summary>
    /// Bulbapedia: "It can raise either the target's Attack, Defense, Special Attack, Special Defense, Speed,
    /// accuracy, or evasion stat" — HP is not one of the raisable stats. When the random roll returns the lowest
    /// value of its range, the selected stat must be Attack, not HP.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_RandomMinimumRoll_SelectsAttackNotHp()
    {
        // Arrange
        var (script, move, target, random, _) = CreateTestSetup();
        // Return the inclusive lower bound of whatever range is requested.
        random.GetInt(Arg.Any<int>(), Arg.Any<int>()).Returns(ci => ci.ArgAt<int>(0));

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

        // Assert
        var args = GetStatBoostCallArgs(target);
        await Assert.That(args).IsNotNull();
        await Assert.That((Statistic)args![0]!).IsEqualTo(Statistic.Attack);
    }

    /// <summary>
    /// Bulbapedia: "It can raise either the target's Attack, Defense, Special Attack, Special Defense, Speed,
    /// accuracy, or evasion stat". Accuracy and evasion must be selectable: when the random roll returns the
    /// highest value of its range, the selected stat should be one of the stats beyond Speed in the
    /// <see cref="Statistic"/> enum (Evasion or Accuracy).
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_RandomMaximumRoll_CanSelectAccuracyOrEvasion()
    {
        // Arrange
        var (script, move, target, random, _) = CreateTestSetup();
        // Return the highest value of whatever range is requested (upper bound is exclusive).
        random.GetInt(Arg.Any<int>(), Arg.Any<int>()).Returns(ci => ci.ArgAt<int>(1) - 1);

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

        // Assert - the highest selectable stat should be Evasion or Accuracy
        var args = GetStatBoostCallArgs(target);
        await Assert.That(args).IsNotNull();
        var stat = (Statistic)args![0]!;
        await Assert.That(stat is Statistic.Evasion or Statistic.Accuracy).IsTrue();
    }

    /// <summary>
    /// Bulbapedia: "the move will fail if all stats are maximized." All raisable stats (Attack through accuracy;
    /// HP boosts cannot be raised by any move and stay at 0) are at +6, so the move must fail without attempting
    /// a stat change.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_AllRaisableStatsMaximized_Fails()
    {
        // Arrange
        var (script, move, target, _, hitData) = CreateTestSetup();
        var statBoost = new StatBoostStatisticSet(0, 6, 6, 6, 6, 6) { Evasion = 6, Accuracy = 6 };
        target.StatBoost.Returns(statBoost);

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

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

    /// <summary>
    /// Bulbapedia: "will not attempt to raise a stat that is already maximized". With the target's Attack at +6
    /// and other stats available, Attack must never be the raised stat, regardless of what the random roll
    /// returns. The rolls cover every index of the six-stat selection pool that remains.
    /// </summary>
    [Test, Arguments(0), Arguments(1), Arguments(2), Arguments(3), Arguments(4), Arguments(5)]
    public async Task OnSecondaryEffect_StatAlreadyMaximized_NeverRaisesThatStat(int roll)
    {
        // Arrange
        var (script, move, target, random, _) = CreateTestSetup();
        var statBoost = new StatBoostStatisticSet(0, 6, 0, 0, 0, 0);
        target.StatBoost.Returns(statBoost);
        random.GetInt(Arg.Any<int>(), Arg.Any<int>()).Returns(roll);

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

        // Assert - a stat is raised, but never the maximized Attack
        var args = GetStatBoostCallArgs(target);
        await Assert.That(args).IsNotNull();
        await Assert.That((Statistic)args![0]!).IsNotEqualTo(Statistic.Attack);
    }
}