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

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

/// <summary>
/// Tests for the <see cref="Autotomize"/> move script.
/// Gen VII Bulbapedia behavior: "Autotomize raises the user's Speed stat by two stages and (if the user
/// successfully changes its Speed) decreases its weight by 220 lbs. (100 kg). If the user successfully
/// changes its weight, the message '&lt;Pokémon&gt; became nimble!' is displayed. Autotomize cannot decrease
/// the user's weight below the minimum 0.2 lbs (0.1 kg); if the user's weight would drop below the minimum,
/// it becomes the minimum instead. Weight loss from Autotomize stacks".
/// </summary>
public class AutotomizeTests
{
    private static (Autotomize script, IExecutingMove move, IPokemon user, IScriptSet userVolatile, EventHook eventHook)
        CreateTestSetup(float weightInKg, bool speedRaiseSucceeds, AutotomizeEffect? existingEffect = null)
    {
        var script = new Autotomize();
        var move = Substitute.For<IExecutingMove>();

        var eventHook = new EventHook();
        var battle = Substitute.For<IBattle>();
        battle.EventHook.Returns(eventHook);
        var battleData = Substitute.For<IPokemonBattleData>();
        battleData.Battle.Returns(battle);

        var userVolatile = Substitute.For<IScriptSet>();
        userVolatile.Get<AutotomizeEffect>().Returns(existingEffect);

        var user = Substitute.For<IPokemon>();
        user.BattleData.Returns(battleData);
        user.Volatile.Returns(userVolatile);
        user.WeightInKg.Returns(weightInKg);
        user.ChangeStatBoost(Arg.Any<Statistic>(), Arg.Any<sbyte>(), Arg.Any<bool>(), Arg.Any<bool>())
            .Returns(speedRaiseSucceeds);
        move.User.Returns(user);

        return (script, move, user, userVolatile, eventHook);
    }

    private static bool ReceivedStackOrAdd(IScriptSet scriptSet) =>
        scriptSet.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "StackOrAdd");

    /// <summary>
    /// Bulbapedia: "Autotomize raises the user's Speed stat by two stages". The stat change is self-inflicted.
    /// </summary>
    [Test]
    public void OnSecondaryEffect_RaisesUserSpeedByTwoStages()
    {
        // Arrange
        var (script, move, user, _, _) = CreateTestSetup(100f, true);

        // Act
        script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0);

        // Assert
        user.Received(1).ChangeStatBoost(Statistic.Speed, 2, true, false);
    }

    /// <summary>
    /// Bulbapedia: "(if the user successfully changes its Speed) decreases its weight by 220 lbs. (100 kg).
    /// If the user successfully changes its weight, the message '&lt;Pokémon&gt; became nimble!' is displayed."
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_SpeedRaiseSucceeds_AddsWeightReductionAndShowsMessage()
    {
        // Arrange
        var (script, move, _, userVolatile, eventHook) = CreateTestSetup(100f, true);
        DialogEvent? capturedEvent = null;
        eventHook.Handler += (_, args) =>
        {
            if (args is DialogEvent dialogEvent)
                capturedEvent = dialogEvent;
        };

        // Act
        script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0);

        // Assert
        await Assert.That(ReceivedStackOrAdd(userVolatile)).IsTrue();
        await Assert.That(capturedEvent).IsNotNull();
        await Assert.That(capturedEvent!.Message).IsEqualTo("pokemon_became_nimble");
    }

    /// <summary>
    /// Bulbapedia: the weight decrease only happens "if the user successfully changes its Speed". When the
    /// Speed stat cannot be raised (already at +6), no weight reduction is applied and no message is shown.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_SpeedRaiseFails_NoWeightReductionOrMessage()
    {
        // Arrange
        var (script, move, _, userVolatile, eventHook) = CreateTestSetup(100f, false);
        var eventFired = false;
        eventHook.Handler += (_, _) => eventFired = true;

        // Act
        script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0);

        // Assert
        await Assert.That(ReceivedStackOrAdd(userVolatile)).IsFalse();
        await Assert.That(eventFired).IsFalse();
    }

    /// <summary>
    /// Bulbapedia: "Autotomize cannot decrease the user's weight below the minimum 0.2 lbs (0.1 kg); if the
    /// user's weight would drop below the minimum, it becomes the minimum instead." A first use on a Pokémon
    /// lighter than 100 kg still applies the weight reduction (the weight clamps to the minimum).
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_FirstUseOnLightPokemon_StillAppliesWeightReduction()
    {
        // Arrange - 50 kg Pokémon, no previous Autotomize use
        var (script, move, _, userVolatile, _) = CreateTestSetup(50f, true);

        // Act
        script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0);

        // Assert
        await Assert.That(ReceivedStackOrAdd(userVolatile)).IsTrue();
    }

    /// <summary>
    /// Bulbapedia: "Weight loss from Autotomize stacks, so using it multiple times will continue to decrease
    /// the user's weight accordingly until it reaches the minimum weight" and "if the user's weight would drop
    /// below the minimum, it becomes the minimum instead." A Pokémon that still weighs more than the minimum
    /// after two stacks must receive another stack (its weight drops to the 0.1 kg minimum).
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_WeightWouldDropBelowMinimum_StillReducesToMinimum()
    {
        // Arrange - the user already has two stacks and currently weighs 50 kg
        var existingEffect = new AutotomizeEffect();
        existingEffect.Stack();
        var (script, move, _, userVolatile, _) = CreateTestSetup(50f, true, existingEffect);

        // Act
        script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0);

        // Assert - the weight should still be reduced (to the minimum)
        await Assert.That(ReceivedStackOrAdd(userVolatile)).IsTrue();
    }

    /// <summary>
    /// Bulbapedia: "Autotomize cannot decrease the user's weight below the minimum 0.2 lbs (0.1 kg)".
    /// A Pokémon already at the minimum weight cannot lose more weight, so no new stack is added and no
    /// message is shown; the Speed boost itself is still applied.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_AlreadyAtMinimumWeight_NoFurtherReductionButSpeedStillRaised()
    {
        // Arrange - the user already has a stack and is at the minimum weight
        var (script, move, user, userVolatile, eventHook) = CreateTestSetup(0.1f, true, new AutotomizeEffect());
        var eventFired = false;
        eventHook.Handler += (_, _) => eventFired = true;

        // Act
        script.OnSecondaryEffect(move, Substitute.For<IPokemon>(), 0);

        // Assert
        user.Received(1).ChangeStatBoost(Statistic.Speed, 2, true, false);
        await Assert.That(ReceivedStackOrAdd(userVolatile)).IsFalse();
        await Assert.That(eventFired).IsFalse();
    }
}