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

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

/// <summary>
/// Tests for the <see cref="BrickBreak"/> move script.
/// Gen VII Bulbapedia behavior: "Brick Break removes Light Screen and Reflect", from Generation IV onward
/// "from the target's side of the field, even if the target is an ally", and from Generation V onwards
/// "Brick Break can also remove Aurora Veil."
/// </summary>
public class BrickBreakTests
{
    /// <summary>
    /// Creates a mocked Pokémon whose <see cref="IPokemonBattleData.BattleSide"/> is the given side.
    /// </summary>
    private static IPokemon CreatePokemonOnSide(IBattleSide side)
    {
        var pokemon = Substitute.For<IPokemon>();
        var battleData = Substitute.For<IPokemonBattleData>();
        battleData.BattleSide.Returns(side);
        pokemon.BattleData.Returns(battleData);
        return pokemon;
    }

    /// <summary>
    /// Creates a fully mocked test setup where the user targets a Pokémon on the opposing side.
    /// </summary>
    private static (BrickBreak script, IExecutingMove move, IPokemon user, IScriptSet userSideScripts, IScriptSet
        targetSideScripts) CreateTestSetup()
    {
        var script = new BrickBreak();
        var move = Substitute.For<IExecutingMove>();

        var userSideScripts = Substitute.For<IScriptSet>();
        var userSide = Substitute.For<IBattleSide>();
        userSide.VolatileScripts.Returns(userSideScripts);
        var targetSideScripts = Substitute.For<IScriptSet>();
        var targetSide = Substitute.For<IBattleSide>();
        targetSide.VolatileScripts.Returns(targetSideScripts);

        var user = CreatePokemonOnSide(userSide);
        move.User.Returns(user);
        var target = CreatePokemonOnSide(targetSide);
        move.Targets.Returns(new IPokemon?[] { target });

        return (script, move, user, userSideScripts, targetSideScripts);
    }

    /// <summary>
    /// Bulbapedia (Generation IV onward): "Brick Break now removes Light Screen and Reflect from the target's
    /// side of the field, even if the target is an ally." The screen effects are removed from the target's
    /// side before the move hits. The script names match <see cref="ReflectEffect"/> and
    /// <see cref="LightScreenEffect"/>.
    /// </summary>
    [Test, Arguments("reflect"), Arguments("light_screen")]
    public async Task OnBeforeMove_ScreenOnTargetSide_RemovesScreenFromTargetSide(string screenScriptName)
    {
        // Arrange
        var (script, move, _, _, targetSideScripts) = CreateTestSetup();

        // Act
        script.OnBeforeMove(move);

        // Assert
        targetSideScripts.Received(1).Remove(new StringKey(screenScriptName));
        await Assert.That(targetSideScripts.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Remove")).IsTrue();
    }

    /// <summary>
    /// Bulbapedia (Generation V onwards): "Brick Break can also remove Aurora Veil."
    /// The <see cref="AuroraVeilEffect"/> is removed from the target's side as well.
    /// </summary>
    [Test]
    public async Task OnBeforeMove_AuroraVeilOnTargetSide_RemovesAuroraVeilFromTargetSide()
    {
        // Arrange
        var (script, move, _, _, targetSideScripts) = CreateTestSetup();

        // Act
        script.OnBeforeMove(move);

        // Assert
        targetSideScripts.Received(1).Remove(new StringKey("aurora_veil"));
        await Assert.That(targetSideScripts.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Remove")).IsTrue();
    }

    /// <summary>
    /// Bulbapedia (Generation IV onward): "Brick Break now removes Light Screen and Reflect from the target's
    /// side of the field". When the target is an opponent, screens on the user's own side stay up.
    /// </summary>
    [Test]
    public async Task OnBeforeMove_ScreensOnUserSide_DoesNotRemoveScreensFromUserSide()
    {
        // Arrange
        var (script, move, _, userSideScripts, _) = CreateTestSetup();

        // Act
        script.OnBeforeMove(move);

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

    /// <summary>
    /// Bulbapedia (Generation IV onward): "Brick Break now removes Light Screen and Reflect from the target's
    /// side of the field, even if the target is an ally." An ally shares the user's side, so targeting an ally
    /// removes the screens from the user's own side.
    /// </summary>
    [Test]
    public async Task OnBeforeMove_AllyTargetOnUserSide_RemovesScreensFromUserSide()
    {
        // Arrange
        var (script, move, user, userSideScripts, _) = CreateTestSetup();
        var ally = CreatePokemonOnSide(user.BattleData!.BattleSide);
        move.Targets.Returns(new IPokemon?[] { ally });

        // Act
        script.OnBeforeMove(move);

        // Assert
        await Assert.That(userSideScripts.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Remove")).IsTrue();
    }

    /// <summary>
    /// Bulbapedia: "Brick Break removes Light Screen and Reflect [...], then inflicts damage."
    /// When no screens are up there is nothing to remove; the move simply proceeds without error and the
    /// sides remain unaffected.
    /// </summary>
    [Test]
    public async Task OnBeforeMove_NoScreensPresent_LeavesSidesUnchanged()
    {
        // Arrange
        var script = new BrickBreak();
        var move = Substitute.For<IExecutingMove>();
        // Real, empty script sets so the removals (if any wrongly happen) would be observable as mutations.
        var userSideScripts = new ScriptSet(Substitute.For<IBattleSide>());
        var targetSideScripts = new ScriptSet(Substitute.For<IBattleSide>());
        var userSide = Substitute.For<IBattleSide>();
        userSide.VolatileScripts.Returns(userSideScripts);
        var targetSide = Substitute.For<IBattleSide>();
        targetSide.VolatileScripts.Returns(targetSideScripts);
        var user = CreatePokemonOnSide(userSide);
        move.User.Returns(user);
        var target = CreatePokemonOnSide(targetSide);
        move.Targets.Returns(new IPokemon?[] { target });

        // Act
        script.OnBeforeMove(move);

        // Assert
        await Assert.That(userSideScripts.Count).IsEqualTo(0);
        await Assert.That(targetSideScripts.Count).IsEqualTo(0);
    }

    /// <summary>
    /// Technical test: without any targets (e.g. outside of battle) the script does nothing and does not throw.
    /// </summary>
    [Test]
    public async Task OnBeforeMove_NoTargets_DoesNotThrow()
    {
        // Arrange
        var script = new BrickBreak();
        var move = Substitute.For<IExecutingMove>();
        var user = Substitute.For<IPokemon>();
        user.BattleData.Returns((IPokemonBattleData?)null);
        move.User.Returns(user);
        move.Targets.Returns(Array.Empty<IPokemon?>());

        // Act & Assert
        await Assert.That(() => script.OnBeforeMove(move)).ThrowsNothing();
    }
}