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

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

/// <summary>
/// Tests for the <see cref="Endure"/> move script and its <see cref="EndureEffect"/>.
/// Gen VII Bulbapedia behavior: "For the rest of the turn, Endure allows the user to survive any attack
/// that would cause it to faint, leaving the user with 1 HP instead." Like other protection moves, "if it
/// or other protection moves are used consecutively, its success rate decreases."
/// </summary>
public class EndureTests
{
    /// <summary>
    /// Creates a fully mocked setup for driving <see cref="Endure"/>'s inherited
    /// <see cref="ProtectionScript.OnSecondaryEffect"/>. The <c>target</c> of the secondary effect is the
    /// Pokémon using Endure itself, as the move is self-targeted.
    /// </summary>
    private static (Endure script, IExecutingMove move, IPokemon target, IHitData hitData, IScriptSet volatileSet)
        CreateProtectSetup(bool userMovesLast, float randomRoll)
    {
        var script = new Endure();
        var move = Substitute.For<IExecutingMove>();
        var target = Substitute.For<IPokemon>();
        var hitData = Substitute.For<IHitData>();
        move.GetHitData(target, 0).Returns(hitData);

        // A queue with a remaining choice means another Pokémon still has to move after the user.
        var queue = userMovesLast ? new BattleChoiceQueue([]) : new BattleChoiceQueue([Substitute.For<IMoveChoice>()]);

        var random = Substitute.For<IBattleRandom>();
        random.GetFloat().Returns(randomRoll);

        var battle = Substitute.For<IBattle>();
        battle.ChoiceQueue.Returns(queue);
        battle.Random.Returns(random);

        var battleData = Substitute.For<IPokemonBattleData>();
        battleData.Battle.Returns(battle);
        target.BattleData.Returns(battleData);

        target.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
        IScriptSet volatileSet = new ScriptSet(target);
        target.Volatile.Returns(volatileSet);

        return (script, move, target, hitData, volatileSet);
    }

    /// <summary>
    /// Bulbapedia: "For the rest of the turn, Endure allows the user to survive any attack that would
    /// cause it to faint" — using the move attaches the <see cref="EndureEffect"/> to the user.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_FirstUse_AddsEndureEffect()
    {
        // Arrange
        var (script, move, target, hitData, volatileSet) = CreateProtectSetup(false, 0.5f);

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

        // Assert
        await Assert.That(volatileSet.Contains(ScriptUtils.ResolveName<EndureEffect>())).IsTrue();
        hitData.DidNotReceive().Fail();
    }

    /// <summary>
    /// Bulbapedia: Endure "always fails if the user is the last to act in the turn" — with no remaining
    /// choices in the queue there is nothing left to protect against, and the move fails.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_UserMovesLast_FailsHit()
    {
        // Arrange
        var (script, move, target, hitData, volatileSet) = CreateProtectSetup(true, 0.0f);

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

        // Assert
        hitData.Received(1).Fail();
        await Assert.That(volatileSet.Contains(ScriptUtils.ResolveName<EndureEffect>())).IsFalse();
    }

    /// <summary>
    /// Bulbapedia: "If it or other protection moves are used consecutively, its success rate decreases."
    /// On the second consecutive use the success chance is 1/3, so a roll above that fails.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_SecondConsecutiveUseWithHighRoll_FailsHit()
    {
        // Arrange
        var (script, move, target, hitData, volatileSet) = CreateProtectSetup(false, 0.5f);
        volatileSet.Add(new ProtectionFailureScript { ProtectTurns = 1 });

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

        // Assert
        hitData.Received(1).Fail();
        await Assert.That(volatileSet.Contains(ScriptUtils.ResolveName<EndureEffect>())).IsFalse();
    }

    /// <summary>
    /// Bulbapedia: "If it or other protection moves are used consecutively, its success rate decreases."
    /// On the second consecutive use the success chance is 1/3, so a roll below that still succeeds.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_SecondConsecutiveUseWithLowRoll_AddsEndureEffect()
    {
        // Arrange
        var (script, move, target, hitData, volatileSet) = CreateProtectSetup(false, 0.2f);
        volatileSet.Add(new ProtectionFailureScript { ProtectTurns = 1 });

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

        // Assert
        await Assert.That(volatileSet.Contains(ScriptUtils.ResolveName<EndureEffect>())).IsTrue();
        hitData.DidNotReceive().Fail();
    }

    /// <summary>
    /// Bulbapedia: "Endure allows the user to survive any attack that would cause it to faint, leaving
    /// the user with 1 HP instead." Damage exceeding the user's current HP is reduced to leave 1 HP.
    /// </summary>
    [Test, Arguments(100u, 150u, 99u), Arguments(50u, 51u, 49u), Arguments(2u, 9999u, 1u)]
    public async Task ChangeIncomingDamage_LethalDamage_LeavesUserWithOneHp(uint currentHealth, uint incomingDamage,
        uint expectedDamage)
    {
        // Arrange
        var effect = new EndureEffect();
        var pokemon = Substitute.For<IPokemon>();
        pokemon.CurrentHealth.Returns(currentHealth);
        var damage = incomingDamage;

        // Act
        effect.ChangeIncomingDamage(pokemon, DamageSource.MoveDamage, ref damage);

        // Assert
        await Assert.That(damage).IsEqualTo(expectedDamage);
    }

    /// <summary>
    /// Bulbapedia: Endure lets the user "survive any attack that would cause it to faint" — a hit for
    /// exactly the user's current HP would faint it, so it too must be reduced to leave 1 HP.
    /// </summary>
    [Test]
    public async Task ChangeIncomingDamage_DamageEqualToCurrentHp_LeavesUserWithOneHp()
    {
        // Arrange
        var effect = new EndureEffect();
        var pokemon = Substitute.For<IPokemon>();
        pokemon.CurrentHealth.Returns(100u);
        var damage = 100u;

        // Act
        effect.ChangeIncomingDamage(pokemon, DamageSource.MoveDamage, ref damage);

        // Assert
        await Assert.That(damage).IsEqualTo(99u);
    }

    /// <summary>
    /// Bulbapedia: Endure only affects attacks "that would cause it to faint" — non-lethal damage passes
    /// through unchanged.
    /// </summary>
    [Test]
    public async Task ChangeIncomingDamage_NonLethalDamage_DamageUnchanged()
    {
        // Arrange
        var effect = new EndureEffect();
        var pokemon = Substitute.For<IPokemon>();
        pokemon.CurrentHealth.Returns(100u);
        var damage = 50u;

        // Act
        effect.ChangeIncomingDamage(pokemon, DamageSource.MoveDamage, ref damage);

        // Assert
        await Assert.That(damage).IsEqualTo(50u);
    }

    /// <summary>
    /// Bulbapedia: the protection lasts "for the rest of the turn" — the effect removes itself at the end
    /// of the turn.
    /// </summary>
    [Test]
    public async Task OnEndTurn_EffectRemovesItself()
    {
        // Arrange
        var owner = Substitute.For<IPokemon>();
        owner.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
        IScriptSet ownerVolatile = new ScriptSet(owner);
        var effect = new EndureEffect();
        ownerVolatile.Add(effect);

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

        // Assert
        await Assert.That(ownerVolatile.Contains(ScriptUtils.ResolveName<EndureEffect>())).IsFalse();
    }
}