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

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

/// <summary>
/// Tests for the <see cref="Defog"/> move script.
/// Gen VII Bulbapedia behavior: "Defog lowers the target's evasion by one stage" and clears field effects:
/// "It removes Spikes, Toxic Spikes, Stealth Rock, and Sticky Web from both sides, while Light Screen,
/// Reflect, Safeguard, Mist, and Aurora Veil are cleared from the target's side only."
/// </summary>
public class DefogTests
{
    /// <summary>
    /// Creates a fully mocked test setup where the target is on side 1 and the user on side 0.
    /// </summary>
    private static (Defog script, IExecutingMove move, IPokemon target, IScriptSet targetSideScripts, IScriptSet
        userSideScripts) CreateTestSetup()
    {
        var script = new Defog();
        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 battle = Substitute.For<IBattle>();
        battle.Sides.Returns(new[] { userSide, targetSide });

        var user = Substitute.For<IPokemon>();
        var userBattleData = Substitute.For<IPokemonBattleData>();
        userBattleData.Battle.Returns(battle);
        userBattleData.SideIndex.Returns((byte)0);
        user.BattleData.Returns(userBattleData);
        move.User.Returns(user);

        var target = Substitute.For<IPokemon>();
        var targetBattleData = Substitute.For<IPokemonBattleData>();
        targetBattleData.Battle.Returns(battle);
        targetBattleData.SideIndex.Returns((byte)1);
        target.BattleData.Returns(targetBattleData);

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

    /// <summary>
    /// Bulbapedia: "Light Screen, Reflect, Safeguard, Mist, and Aurora Veil are cleared from the target's
    /// side only." The screen and protection effects (<see cref="LightScreenEffect"/>,
    /// <see cref="ReflectEffect"/>, <see cref="SafeguardEffect"/>, <see cref="MistEffect"/>) are removed
    /// from the target's side.
    /// </summary>
    [Test, Arguments("light_screen"), Arguments("reflect"), Arguments("safe_guard"), Arguments("mist")]
    public async Task OnSecondaryEffect_ScreenOnTargetSide_RemovesEffectFromTargetSide(string scriptName)
    {
        // Arrange
        var (script, move, target, targetSideScripts, _) = CreateTestSetup();

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

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

    /// <summary>
    /// Bulbapedia: "It removes Spikes, Toxic Spikes, Stealth Rock, and Sticky Web" — the entry hazards
    /// (<see cref="SpikesEffect"/>, <see cref="ToxicSpikesEffect"/>, <see cref="StealthRockEffect"/>,
    /// <see cref="StickyWebEffect"/>) are removed from the target's side.
    /// </summary>
    [Test, Arguments("spikes"), Arguments("toxic_spikes"), Arguments("stealth_rock"), Arguments("sticky_web")]
    public async Task OnSecondaryEffect_HazardOnTargetSide_RemovesHazardFromTargetSide(string scriptName)
    {
        // Arrange
        var (script, move, target, targetSideScripts, _) = CreateTestSetup();

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

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

    /// <summary>
    /// Bulbapedia (Generations VI and VII): "It removes Spikes, Toxic Spikes, Stealth Rock, and Sticky Web
    /// from both sides" — the entry hazards must also be removed from the user's own side.
    /// </summary>
    [Test, Arguments("spikes"), Arguments("toxic_spikes"), Arguments("stealth_rock"), Arguments("sticky_web")]
    public async Task OnSecondaryEffect_HazardOnUserSide_RemovesHazardFromUserSide(string scriptName)
    {
        // Arrange
        var (script, move, target, _, userSideScripts) = CreateTestSetup();

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

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

    /// <summary>
    /// Bulbapedia (Generations VI and VII): "Light Screen, Reflect, Safeguard, Mist, and Aurora Veil are
    /// cleared from the target's side only." The <see cref="AuroraVeilEffect"/> must be removed as well.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_AuroraVeilOnTargetSide_RemovesAuroraVeilFromTargetSide()
    {
        // Arrange
        var (script, move, target, targetSideScripts, _) = CreateTestSetup();

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

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

    /// <summary>
    /// Bulbapedia: "Defog lowers the target's evasion by one stage". Neither the script nor the move data
    /// applies the evasion drop, so the target's evasion is never lowered.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_Always_LowersTargetEvasionByOneStage()
    {
        // Arrange
        var (script, move, target, _, _) = CreateTestSetup();

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

        // Assert
        await Assert.That(target.ReceivedCalls().Any(c =>
            c.GetMethodInfo().Name == "ChangeStatBoost" && (Statistic)c.GetArguments()[0]! == Statistic.Evasion &&
            (sbyte)c.GetArguments()[1]! == -1)).IsTrue();
    }

    /// <summary>
    /// Technical test: without battle data (outside of battle) the secondary effect does nothing and does
    /// not throw.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_TargetHasNoBattleData_DoesNotThrow()
    {
        // Arrange
        var script = new Defog();
        var move = Substitute.For<IExecutingMove>();
        var target = Substitute.For<IPokemon>();
        target.BattleData.Returns((IPokemonBattleData?)null);

        // Act & Assert
        await Assert.That(() => script.OnSecondaryEffect(move, target, 0)).ThrowsNothing();
    }
}