using PkmnLib.Dynamic.Models;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Dynamic.ScriptHandling.Registry;
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="Curse"/> move script and the <see cref="GhostCurseEffect"/> it applies.
/// Gen VII Bulbapedia behavior — non-Ghost user: "The user's Speed stat will drop by one stage and its
/// Attack stat and Defense stat will rise by one stage each." Ghost-type user: "The user will lose half of
/// its maximum HP (rounded down) and put a curse on the target", and "A cursed Pokémon will lose ¼ of its
/// maximum HP at the end of each turn."
/// </summary>
public class CurseTests
{
    private static (Curse script, IExecutingMove move, IPokemon user, IPokemon target, IScriptSet targetVolatile)
        CreateTestSetup(bool userIsGhost, uint userMaxHealth = 100, uint userCurrentHealth = 100)
    {
        var script = new Curse();
        var library = LibraryHelpers.LoadLibrary();
        library.StaticLibrary.Types.TryGetTypeIdentifier("ghost", out var ghostType);
        library.StaticLibrary.Types.TryGetTypeIdentifier("normal", out var normalType);

        var battle = Substitute.For<IBattle>();
        battle.Library.Returns(library);
        var battleData = Substitute.For<IPokemonBattleData>();
        battleData.Battle.Returns(battle);

        var user = Substitute.For<IPokemon>();
        user.BattleData.Returns(battleData);
        user.Types.Returns(new[] { userIsGhost ? ghostType : normalType });
        user.MaxHealth.Returns(userMaxHealth);
        user.CurrentHealth.Returns(userCurrentHealth);
        var move = Substitute.For<IExecutingMove>();
        move.User.Returns(user);

        var target = Substitute.For<IPokemon>();
        // Use a real script set so the curse applied to the target can be inspected afterwards.
        var targetVolatile = new ScriptSet(target);
        target.Volatile.Returns(targetVolatile);
        target.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));

        return (script, move, user, target, targetVolatile);
    }

    /// <summary>
    /// Helper that checks whether a stat boost change was applied to the given Pokémon.
    /// </summary>
    private static bool ReceivedStatBoost(IPokemon pokemon, Statistic stat, sbyte amount) =>
        pokemon.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ChangeStatBoost" &&
                                         (Statistic)c.GetArguments()[0]! == stat &&
                                         (sbyte)c.GetArguments()[1]! == amount);

    /// <summary>
    /// Helper to extract the damage amount from a substitute's received Damage calls.
    /// </summary>
    private static uint? GetDamageAmount(IPokemon pokemon)
    {
        var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
        return call != null ? (uint)call.GetArguments()[0]! : null;
    }

    /// <summary>
    /// Bulbapedia (non-Ghost user): "The user's Speed stat will drop by one stage and its Attack stat and
    /// Defense stat will rise by one stage each."
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_NonGhostUser_LowersSpeedAndRaisesAttackAndDefense()
    {
        // Arrange
        var (script, move, user, target, _) = CreateTestSetup(false);

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

        // Assert
        await Assert.That(ReceivedStatBoost(user, Statistic.Speed, -1)).IsTrue();
        await Assert.That(ReceivedStatBoost(user, Statistic.Attack, 1)).IsTrue();
        await Assert.That(ReceivedStatBoost(user, Statistic.Defense, 1)).IsTrue();
    }

    /// <summary>
    /// Bulbapedia: only a Ghost-type user pays HP and curses the target — a non-Ghost user takes no damage
    /// and does not curse the target.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_NonGhostUser_DoesNotDamageUserOrCurseTarget()
    {
        // Arrange
        var (script, move, user, target, targetVolatile) = CreateTestSetup(false);

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

        // Assert
        await Assert.That(GetDamageAmount(user)).IsNull();
        await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName<GhostCurseEffect>())).IsFalse();
    }

    /// <summary>
    /// Bulbapedia (Ghost-type user): "The user will lose half of its maximum HP (rounded down)". At full
    /// HP that is half of its current HP as well.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_GhostUserAtFullHp_UserLosesHalfItsMaximumHp()
    {
        // Arrange
        var (script, move, user, target, _) = CreateTestSetup(true, 100, 100);

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

        // Assert
        await Assert.That(GetDamageAmount(user)!.Value).IsEqualTo(50u);
    }

    /// <summary>
    /// Bulbapedia (Ghost-type user): the user pays HP to "put a curse on the target" — the
    /// <see cref="GhostCurseEffect"/> is attached to the target.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_GhostUser_CursesTarget()
    {
        // Arrange
        var (script, move, _, target, targetVolatile) = CreateTestSetup(true);

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

        // Assert
        await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName<GhostCurseEffect>())).IsTrue();
    }

    /// <summary>
    /// Bulbapedia (Ghost-type user): "The user will lose half of its maximum HP (rounded down)" — the cost
    /// is based on maximum HP, not on the HP the user has left.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_GhostUserAtHalfHp_UserStillLosesHalfItsMaximumHp()
    {
        // Arrange
        var (script, move, user, target, _) = CreateTestSetup(true, 100, 50);

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

        // Assert
        await Assert.That(GetDamageAmount(user)!.Value).IsEqualTo(50u);
    }

    /// <summary>
    /// Bulbapedia (Ghost-type user): "If this causes the user's HP to drop to 0, the move will execute
    /// fully but cause the user to faint." — the curse is still put on the target.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_GhostUserFaintsFromHpCost_TargetIsStillCursed()
    {
        // Arrange
        var (script, move, user, target, targetVolatile) = CreateTestSetup(true);
        // The first read of CurrentHealth computes the HP cost; afterwards the user has fainted.
        user.CurrentHealth.Returns(100u, 0u);

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

        // Assert
        await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName<GhostCurseEffect>())).IsTrue();
    }

    /// <summary>
    /// Bulbapedia: "A cursed Pokémon will lose ¼ of its maximum HP at the end of each turn." At full HP
    /// that is a quarter of its current HP as well.
    /// </summary>
    [Test]
    public async Task GhostCurseEffect_OnEndTurnAtFullHp_CursedPokemonLosesQuarterOfMaximumHp()
    {
        // Arrange
        var cursed = Substitute.For<IPokemon>();
        cursed.MaxHealth.Returns(100u);
        cursed.CurrentHealth.Returns(100u);
        var effect = new GhostCurseEffect(cursed);

        // Act
        effect.OnEndTurn(Substitute.For<IScriptSource>(), Substitute.For<IBattle>());

        // Assert
        await Assert.That(GetDamageAmount(cursed)!.Value).IsEqualTo(25u);
    }

    /// <summary>
    /// Bulbapedia: "A cursed Pokémon will lose ¼ of its maximum HP at the end of each turn." — the drain
    /// is based on maximum HP, not on the HP the Pokémon has left.
    /// </summary>
    [Test]
    public async Task GhostCurseEffect_OnEndTurnAtLowHp_CursedPokemonStillLosesQuarterOfMaximumHp()
    {
        // Arrange
        var cursed = Substitute.For<IPokemon>();
        cursed.MaxHealth.Returns(100u);
        cursed.CurrentHealth.Returns(40u);
        var effect = new GhostCurseEffect(cursed);

        // Act
        effect.OnEndTurn(Substitute.For<IScriptSource>(), Substitute.For<IBattle>());

        // Assert
        await Assert.That(GetDamageAmount(cursed)!.Value).IsEqualTo(25u);
    }
}