using PkmnLib.Dynamic.Models;
using PkmnLib.Dynamic.ScriptHandling;
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="Bide"/> move script and its <see cref="BideEffect"/> volatile.
/// Gen VII Bulbapedia behavior (base plus Gen III-VII deltas): the user idles while storing the damage it
/// receives, then "Bide will do damage equal to twice the damage received during the idling period", enduring
/// "attacks for two turns" and hitting "the last Pokémon to attack the user, even if this is an ally".
/// </summary>
public class BideTests
{
    private static (Bide script, IExecutingMove move, IPokemon user, IPokemon target, IScriptSet userVolatile, IHitData
        hitData) CreateTestSetup()
    {
        var script = new Bide();
        var move = Substitute.For<IExecutingMove>();
        var target = Substitute.For<IPokemon>();
        var hitData = Substitute.For<IHitData>();
        move.GetHitData(target, 0).Returns(hitData);

        var user = Substitute.For<IPokemon>();
        user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
        // A real script set so the volatile Bide effect can actually be added, retrieved and removed.
        var userVolatile = new ScriptSet(user);
        user.Volatile.Returns(userVolatile);
        move.User.Returns(user);

        return (script, move, user, target, userVolatile, hitData);
    }

    private static IPokemon CreateAttacker(bool onBattlefield = true)
    {
        var attacker = Substitute.For<IPokemon>();
        var battleData = Substitute.For<IPokemonBattleData>();
        battleData.IsOnBattlefield.Returns(onBattlefield);
        attacker.BattleData.Returns(battleData);
        return attacker;
    }

    /// <summary>
    /// Adds a <see cref="BideEffect"/> to the user's volatile scripts, as if Bide has already been storing
    /// energy for the given number of executed turns.
    /// </summary>
    private static BideEffect AddStoredBideEffect(IScriptSet userVolatile, IPokemon user, byte turns, uint damageTaken,
        params IPokemon[] hitBy)
    {
        var effect = new BideEffect(user)
        {
            Turns = turns,
            DamageTaken = damageTaken,
        };
        effect.HitBy.AddRange(hitBy);
        userVolatile.Add(effect);
        return effect;
    }

    /// <summary>
    /// Helper to check whether a Pokémon received any Damage call.
    /// </summary>
    private static bool ReceivedDamage(IPokemon pokemon) =>
        pokemon.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage");

    /// <summary>
    /// Helper to extract the damage amount from a Pokémon'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>
    /// Helper to extract the damage source from a Pokémon's received Damage calls.
    /// </summary>
    private static DamageSource? GetDamageSource(IPokemon pokemon)
    {
        var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
        return call != null ? (DamageSource)call.GetArguments()[1]! : null;
    }

    /// <summary>
    /// Bulbapedia: "After Bide is selected, the user will be unable to select a move for an idling period".
    /// The first use adds the <see cref="BideEffect"/> volatile to the user, which forces the Bide choice on
    /// the following turns and records incoming damage.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_FirstUse_AddsBideEffectToUserVolatile()
    {
        // Arrange
        var (script, move, _, target, userVolatile, _) = CreateTestSetup();

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

        // Assert
        await Assert.That(userVolatile.Get<BideEffect>()).IsNotNull();
    }

    /// <summary>
    /// Bulbapedia: "Bide will now only endure attacks for two turns."
    /// The turn Bide is selected begins the storing period, which spans two more turns; the stored damage is
    /// only released on the third execution, so on the second execution nothing is released yet.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_SecondExecution_DoesNotReleaseStoredDamage()
    {
        // Arrange
        var (script, move, user, target, userVolatile, _) = CreateTestSetup();
        var attacker = CreateAttacker();
        _ = AddStoredBideEffect(userVolatile, user, 1, 50, attacker);

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

        // Assert - still storing, so the attacker has not been damaged
        await Assert.That(ReceivedDamage(attacker)).IsFalse();
    }

    /// <summary>
    /// Bulbapedia: "Bide will do damage equal to twice the damage received during the idling period."
    /// </summary>
    [Test, Arguments(50u, 100u), Arguments(1u, 2u), Arguments(123u, 246u)]
    public async Task OnSecondaryEffect_Release_DealsTwiceTheStoredDamage(uint damageTaken, uint expectedDamage)
    {
        // Arrange
        var (script, move, user, target, userVolatile, _) = CreateTestSetup();
        var attacker = CreateAttacker();
        _ = AddStoredBideEffect(userVolatile, user, 2, damageTaken, attacker);

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

        // Assert
        await Assert.That(GetDamageAmount(attacker)!.Value).IsEqualTo(expectedDamage);
    }

    /// <summary>
    /// Bulbapedia: "Bide will do damage equal to twice the damage received during the idling period."
    /// The released damage is dealt as move damage, not as indirect damage.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_Release_DealsMoveDamage()
    {
        // Arrange
        var (script, move, user, target, userVolatile, _) = CreateTestSetup();
        var attacker = CreateAttacker();
        _ = AddStoredBideEffect(userVolatile, user, 2, 50, attacker);

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

        // Assert
        await Assert.That(GetDamageSource(attacker)!.Value).IsEqualTo(DamageSource.MoveDamage);
    }

    /// <summary>
    /// Bulbapedia: "Bide hits the last Pokémon to attack the user, even if this is an ally."
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_Release_HitsLastPokemonThatAttackedUser()
    {
        // Arrange
        var (script, move, user, target, userVolatile, _) = CreateTestSetup();
        var firstAttacker = CreateAttacker();
        var lastAttacker = CreateAttacker();
        _ = AddStoredBideEffect(userVolatile, user, 2, 50, firstAttacker, lastAttacker);

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

        // Assert - only the last attacker is hit
        await Assert.That(ReceivedDamage(lastAttacker)).IsTrue();
        await Assert.That(ReceivedDamage(firstAttacker)).IsFalse();
    }

    /// <summary>
    /// Bulbapedia: "this attack will hit the last Pokémon to attack the user".
    /// A Pokémon that has since left the battlefield cannot be hit by the released damage.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_Release_AttackerNoLongerOnBattlefield_IsNotDamaged()
    {
        // Arrange
        var (script, move, user, target, userVolatile, _) = CreateTestSetup();
        var departedAttacker = CreateAttacker(false);
        _ = AddStoredBideEffect(userVolatile, user, 2, 50, departedAttacker);

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

        // Assert
        await Assert.That(ReceivedDamage(departedAttacker)).IsFalse();
    }

    /// <summary>
    /// Bulbapedia: "If the user is not directly attacked during the biding period, it will fail on the turn it
    /// would have released."
    /// </summary>
    [Test]
    public void OnSecondaryEffect_ReleaseWithoutBeingAttacked_MoveFails()
    {
        // Arrange
        var (script, move, user, target, userVolatile, hitData) = CreateTestSetup();
        _ = AddStoredBideEffect(userVolatile, user, 2, 0);

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

        // Assert
        hitData.Received(1).Fail();
    }

    /// <summary>
    /// Bulbapedia: "Once a Pokémon has started biding, it cannot choose to switch out until the attack is
    /// complete."
    /// Once the stored damage is released the attack is complete, so the <see cref="BideEffect"/> volatile must
    /// be removed from the user; otherwise the user would remain locked into Bide indefinitely through
    /// <see cref="BideEffect.ForceTurnSelection"/>.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_Release_RemovesBideEffectFromUser()
    {
        // Arrange
        var (script, move, user, target, userVolatile, _) = CreateTestSetup();
        var attacker = CreateAttacker();
        _ = AddStoredBideEffect(userVolatile, user, 2, 50, attacker);

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

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

    /// <summary>
    /// Bulbapedia: "Bide will do damage equal to twice the damage received during the idling period."
    /// The <see cref="BideEffect"/> volatile accumulates every chunk of damage the user takes while storing
    /// energy.
    /// </summary>
    [Test]
    public async Task BideEffect_OnDamage_AccumulatesDamageTaken()
    {
        // Arrange
        var user = Substitute.For<IPokemon>();
        var effect = new BideEffect(user);

        // Act - the user drops from 100 to 60 HP, then from 60 to 50 HP
        effect.OnDamage(user, DamageSource.MoveDamage, 100, 60);
        effect.OnDamage(user, DamageSource.MoveDamage, 60, 50);

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

    /// <summary>
    /// Bulbapedia: "Bide hits the last Pokémon to attack the user, even if this is an ally."
    /// The <see cref="BideEffect"/> volatile records every Pokémon that hits the user, in order, so the last
    /// attacker can be targeted on release.
    /// </summary>
    [Test]
    public async Task BideEffect_OnIncomingHit_RecordsAttacker()
    {
        // Arrange
        var user = Substitute.For<IPokemon>();
        var effect = new BideEffect(user);
        var incomingMove = Substitute.For<IExecutingMove>();
        var attacker = Substitute.For<IPokemon>();
        incomingMove.User.Returns(attacker);

        // Act
        effect.OnIncomingHit(incomingMove, user, 0);

        // Assert
        await Assert.That(effect.HitBy.LastOrDefault()).IsEqualTo(attacker);
    }
}