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

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

/// <summary>
/// Tests for the <see cref="BatonPass"/> move script.
/// Behavior is verified against the Bulbapedia page for Baton Pass (Generation VII).
/// In this codebase, volatile scripts that are transferred by Baton Pass are marked with
/// <see cref="IBatonPassException"/> (e.g. <see cref="AutotomizeEffect"/>).
/// </summary>
public class BatonPassTests
{
    /// <summary>
    /// Creates a mocked Pokémon with a real volatile <see cref="ScriptSet"/> and a real
    /// <see cref="StatBoostStatisticSet"/>.
    /// </summary>
    private static IPokemon CreateMockPokemon(out IScriptSet volatileSet)
    {
        var pokemon = Substitute.For<IPokemon>();
        pokemon.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
        var set = new ScriptSet(pokemon);
        pokemon.Volatile.Returns(set);
        pokemon.StatBoost.Returns(new StatBoostStatisticSet());
        volatileSet = set;
        return pokemon;
    }

    /// <summary>
    /// Creates a fully mocked test setup for Baton Pass tests. The Pokémon to switch in is stored in the
    /// move choice's <see cref="IMoveChoice.AdditionalData"/> under the <c>to_switch</c> key.
    /// </summary>
    private static (BatonPass script, IExecutingMove move, IPokemon user, IPokemon toSwitch, IBattleSide side,
        IScriptSet userVolatile, IScriptSet switchInVolatile) CreateTestSetup()
    {
        var script = new BatonPass();
        var move = Substitute.For<IExecutingMove>();
        var user = CreateMockPokemon(out var userVolatile);
        var toSwitch = CreateMockPokemon(out var switchInVolatile);

        var moveChoice = Substitute.For<IMoveChoice>();
        moveChoice.AdditionalData.Returns(new Dictionary<StringKey, object?> { { "to_switch", toSwitch } });
        move.MoveChoice.Returns(moveChoice);

        var side = Substitute.For<IBattleSide>();
        var battle = Substitute.For<IBattle>();
        battle.Sides.Returns(new[] { side });

        var battleData = Substitute.For<IPokemonBattleData>();
        battleData.Battle.Returns(battle);
        battleData.SideIndex.Returns((byte)0);
        battleData.Position.Returns((byte)1);
        user.BattleData.Returns(battleData);

        move.User.Returns(user);
        return (script, move, user, toSwitch, side, userVolatile, switchInVolatile);
    }

    /// <summary>
    /// Bulbapedia: "Baton Pass switches out the user and passes several effects to the Pokémon switched in
    /// its place."
    /// The user is swapped out for the chosen Pokémon on its own field position.
    /// </summary>
    [Test]
    public void OnSecondaryEffect_SwitchTargetChosen_SwitchesOutUser()
    {
        // Arrange
        var (script, move, user, toSwitch, side, _, _) = CreateTestSetup();

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

        // Assert
        side.Received(1).SwapPokemon(1, toSwitch);
    }

    /// <summary>
    /// Bulbapedia: "Baton Pass transfers all temporary stat stage increases and decreases".
    /// Each stat stage (including evasion and accuracy) is copied to the Pokémon that is switched in.
    /// </summary>
    [Test, Arguments(Statistic.Attack, (sbyte)6), Arguments(Statistic.Defense, (sbyte)-6),
     Arguments(Statistic.SpecialAttack, (sbyte)2), Arguments(Statistic.SpecialDefense, (sbyte)-1),
     Arguments(Statistic.Speed, (sbyte)3), Arguments(Statistic.Evasion, (sbyte)1),
     Arguments(Statistic.Accuracy, (sbyte)-2)]
    public async Task OnSecondaryEffect_StatBoost_TransferredToSwitchIn(Statistic stat, sbyte boost)
    {
        // Arrange
        var (script, move, user, toSwitch, _, _, _) = CreateTestSetup();
        user.StatBoost.SetStatistic(stat, boost);

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

        // Assert
        await Assert.That(toSwitch.StatBoost.GetStatistic(stat)).IsEqualTo(boost);
    }

    /// <summary>
    /// Bulbapedia: "Baton Pass transfers all temporary stat stage increases and decreases".
    /// After the stat stages are copied, the switched-in Pokémon's boosted stats are recalculated.
    /// </summary>
    [Test]
    public void OnSecondaryEffect_StatBoostsTransferred_RecalculatesSwitchInStats()
    {
        // Arrange
        var (script, move, user, toSwitch, _, _, _) = CreateTestSetup();

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

        // Assert
        toSwitch.Received(1).RecalculateBoostedStats();
    }

    /// <summary>
    /// Bulbapedia: "Baton Pass switches out the user and passes several effects to the Pokémon switched in
    /// its place."
    /// A volatile script that is marked as transferable (<see cref="IBatonPassException"/>, such as
    /// <see cref="AutotomizeEffect"/>) is passed to the switched-in Pokémon.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_TransferableVolatile_PassedToSwitchIn()
    {
        // Arrange
        var (script, move, user, _, _, userVolatile, switchInVolatile) = CreateTestSetup();
        userVolatile.Add(new AutotomizeEffect());

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

        // Assert
        await Assert.That(switchInVolatile.Get<AutotomizeEffect>()).IsNotNull();
    }

    /// <summary>
    /// Bulbapedia: "Baton Pass also passes some volatile status conditions, namely confusion, getting pumped,
    /// escape prevention, seeding, Curse, Substitute, Ingrain, Aqua Ring" — effects outside that list are not
    /// passed. The consecutive-protection counter (<see cref="ProtectionFailureScript"/>) is not transferable,
    /// so it must not end up on the switched-in Pokémon.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_NonTransferableVolatile_NotPassedToSwitchIn()
    {
        // Arrange
        var (script, move, user, _, _, userVolatile, switchInVolatile) = CreateTestSetup();
        userVolatile.Add(new ProtectionFailureScript());

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

        // Assert
        await Assert.That(switchInVolatile.Get<ProtectionFailureScript>()).IsNull();
    }

    /// <summary>
    /// Bulbapedia: "Baton Pass switches out the user and passes several effects to the Pokémon switched in
    /// its place."
    /// A transferable volatile must be passed even when the user also has a non-transferable volatile that
    /// was added before it.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_TransferableVolatileAfterNonTransferable_StillPassedToSwitchIn()
    {
        // Arrange
        var (script, move, user, _, _, userVolatile, switchInVolatile) = CreateTestSetup();
        userVolatile.Add(new ProtectionFailureScript());
        userVolatile.Add(new AutotomizeEffect());

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

        // Assert
        await Assert.That(switchInVolatile.Get<AutotomizeEffect>()).IsNotNull();
        await Assert.That(switchInVolatile.Get<ProtectionFailureScript>()).IsNull();
    }

    /// <summary>
    /// Bulbapedia: "Baton Pass switches out the user".
    /// The user's volatile scripts leave the field with it: after the switch its volatile containers are empty.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_UserSwitchedOut_UserVolatilesCleared()
    {
        // Arrange
        var (script, move, user, _, _, userVolatile, _) = CreateTestSetup();
        userVolatile.Add(new AutotomizeEffect());

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

        // Assert
        await Assert.That(userVolatile.Get<AutotomizeEffect>()).IsNull();
    }

    /// <summary>
    /// Technical test: without any <see cref="IMoveChoice.AdditionalData"/> there is no Pokémon to switch to,
    /// so no switch happens.
    /// </summary>
    [Test]
    public void OnSecondaryEffect_NoAdditionalData_DoesNotSwitch()
    {
        // Arrange
        var (script, move, user, _, side, _, _) = CreateTestSetup();
        move.MoveChoice.AdditionalData.Returns((Dictionary<StringKey, object?>?)null);

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

        // Assert
        side.DidNotReceive().SwapPokemon(Arg.Any<byte>(), Arg.Any<IPokemon?>());
    }

    /// <summary>
    /// Technical test: if the stored switch target is not a Pokémon, no switch happens.
    /// </summary>
    [Test]
    public void OnSecondaryEffect_SwitchTargetNotAPokemon_DoesNotSwitch()
    {
        // Arrange
        var (script, move, user, _, side, _, _) = CreateTestSetup();
        move.MoveChoice.AdditionalData.Returns(new Dictionary<StringKey, object?> { { "to_switch", null } });

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

        // Assert
        side.DidNotReceive().SwapPokemon(Arg.Any<byte>(), Arg.Any<IPokemon?>());
    }

    /// <summary>
    /// Technical test: if the user has no <see cref="IPokemon.BattleData"/>, the script returns without
    /// switching and without clearing the user's volatile scripts.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_NoBattleData_DoesNotSwitch()
    {
        // Arrange
        var (script, move, user, _, side, userVolatile, _) = CreateTestSetup();
        user.BattleData.Returns((IPokemonBattleData?)null);
        userVolatile.Add(new AutotomizeEffect());

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

        // Assert
        side.DidNotReceive().SwapPokemon(Arg.Any<byte>(), Arg.Any<IPokemon?>());
        await Assert.That(userVolatile.Get<AutotomizeEffect>()).IsNotNull();
    }
}