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;
using PkmnLib.Static;

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

/// <summary>
/// Tests for the <see cref="DestinyBond"/> move script and its <see cref="DestinyBondEffect"/>.
/// Gen VII Bulbapedia behavior: "When Destiny Bond is used, the user gains the Destiny Bound status.
/// Until the user moves again, if the user faints as the direct result of an attack, the attacking
/// Pokémon also faints."
/// </summary>
public class DestinyBondTests
{
    /// <summary>
    /// Creates a mocked user whose <see cref="IPokemon.Volatile"/> is a real <see cref="ScriptSet"/> so
    /// the volatile added by the move can be inspected.
    /// </summary>
    private static (DestinyBond script, IExecutingMove move, IPokemon user, ScriptSet userVolatile) CreateTestSetup()
    {
        var script = new DestinyBond();
        var move = Substitute.For<IExecutingMove>();
        var user = Substitute.For<IPokemon>();
        var userVolatile = new ScriptSet(user);
        user.Volatile.Returns(userVolatile);
        user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
        move.User.Returns(user);
        return (script, move, user, userVolatile);
    }

    /// <summary>
    /// Creates a fainting Pokémon whose battle's <see cref="BattleChoiceQueue.LastRanChoice"/> is a move
    /// choice made by the given attacker.
    /// </summary>
    private static IPokemon CreateFaintingPokemonAttackedBy(IPokemon attacker)
    {
        var moveChoice = Substitute.For<IMoveChoice>();
        moveChoice.User.Returns(attacker);
        var queue = new BattleChoiceQueue([moveChoice]);
        queue.Dequeue();

        var battle = Substitute.For<IBattle>();
        battle.ChoiceQueue.Returns(queue);
        var battleData = Substitute.For<IPokemonBattleData>();
        battleData.Battle.Returns(battle);
        var pokemon = Substitute.For<IPokemon>();
        pokemon.BattleData.Returns(battleData);
        return pokemon;
    }

    /// <summary>
    /// Bulbapedia: "When Destiny Bond is used, the user gains the Destiny Bound status." The move adds the
    /// <see cref="DestinyBondEffect"/> volatile script to the user.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_Used_AddsDestinyBondEffectToUser()
    {
        // Arrange
        var (script, move, _, userVolatile) = CreateTestSetup();
        var target = Substitute.For<IPokemon>();

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

        // Assert
        await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName<DestinyBondEffect>())).IsTrue();
    }

    /// <summary>
    /// Bulbapedia (Generation VII): Destiny Bond "will always fail if it was successfully executed on the
    /// previous turn." A user that still carries the <see cref="DestinyBondEffect"/> from the previous use
    /// fails the move. The <see cref="IScriptFailMove"/> hook runs before the effect is removed by the
    /// user's move, so the lingering effect is what marks the consecutive use.
    /// </summary>
    [Test]
    public async Task FailMove_UserStillDestinyBound_FailsMove()
    {
        // Arrange
        var (script, move, _, userVolatile) = CreateTestSetup();
        userVolatile.Add(new DestinyBondEffect());
        var fail = false;

        // Act
        script.FailMove(move, ref fail);

        // Assert
        await Assert.That(fail).IsTrue();
    }

    /// <summary>
    /// Bulbapedia (Generation VII): the move only fails "if it was successfully executed on the previous
    /// turn." Without a lingering <see cref="DestinyBondEffect"/> on the user, the move executes normally.
    /// </summary>
    [Test]
    public async Task FailMove_UserNotDestinyBound_DoesNotFailMove()
    {
        // Arrange
        var (script, move, _, _) = CreateTestSetup();
        var fail = false;

        // Act
        script.FailMove(move, ref fail);

        // Assert
        await Assert.That(fail).IsFalse();
    }

    /// <summary>
    /// Bulbapedia: "if the user faints as the direct result of an attack, the attacking Pokémon also
    /// faints." When the Destiny Bound user faints from move damage, the attacker takes damage far in
    /// excess of its HP, guaranteeing the faint.
    /// </summary>
    [Test]
    public async Task OnFaint_FaintsFromMoveDamage_AttackerIsDamagedToFaint()
    {
        // Arrange
        var effect = new DestinyBondEffect();
        var attacker = Substitute.For<IPokemon>();
        attacker.BoostedStats.Returns(new StatisticSet<uint>(100, 1, 1, 1, 1, 1));
        var pokemon = CreateFaintingPokemonAttackedBy(attacker);

        // Act
        effect.OnFaint(pokemon, DamageSource.MoveDamage);

        // Assert - the attacker is damaged by well over its max HP (10x) so it always faints
        var damageCall = attacker.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
        await Assert.That(damageCall).IsNotNull();
        await Assert.That((uint)damageCall!.GetArguments()[0]!).IsEqualTo(1000u);
    }

    /// <summary>
    /// Bulbapedia: Destiny Bond "bypasses protective abilities like Focus Sash and Sturdy". The faint
    /// damage is dealt with force, so scripts cannot reduce or prevent it.
    /// </summary>
    [Test]
    public async Task OnFaint_FaintsFromMoveDamage_DamageIsForced()
    {
        // Arrange
        var effect = new DestinyBondEffect();
        var attacker = Substitute.For<IPokemon>();
        attacker.BoostedStats.Returns(new StatisticSet<uint>(100, 1, 1, 1, 1, 1));
        var pokemon = CreateFaintingPokemonAttackedBy(attacker);

        // Act
        effect.OnFaint(pokemon, DamageSource.MoveDamage);

        // Assert - the forceDamage argument (last parameter) is true
        var damageCall = attacker.ReceivedCalls().First(c => c.GetMethodInfo().Name == "Damage");
        await Assert.That((bool)damageCall.GetArguments()[3]!).IsTrue();
    }

    /// <summary>
    /// Bulbapedia: "The move doesn't activate from indirect damage like weather or Leech Seed."
    /// Fainting from a non-move damage source does not take the attacker down.
    /// </summary>
    [Test]
    public async Task OnFaint_FaintsFromIndirectDamage_AttackerIsNotDamaged()
    {
        // Arrange
        var effect = new DestinyBondEffect();
        var attacker = Substitute.For<IPokemon>();
        attacker.BoostedStats.Returns(new StatisticSet<uint>(100, 1, 1, 1, 1, 1));
        var pokemon = CreateFaintingPokemonAttackedBy(attacker);

        // Act
        effect.OnFaint(pokemon, DamageSource.Misc);

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

    /// <summary>
    /// Bulbapedia: "Until the user moves again" — the Destiny Bound status wears off when the user makes
    /// its next move, so the effect removes itself before the user's move.
    /// </summary>
    [Test]
    public async Task OnBeforeMove_UserMovesAgain_RemovesDestinyBondEffect()
    {
        // Arrange
        var user = Substitute.For<IPokemon>();
        var userVolatile = new ScriptSet(user);
        user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
        var effect = new DestinyBondEffect();
        userVolatile.Add(effect);
        var move = Substitute.For<IExecutingMove>();

        // Act
        effect.OnBeforeMove(move);

        // Assert
        await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName<DestinyBondEffect>())).IsFalse();
    }

    /// <summary>
    /// Technical test: fainting without battle data (outside of battle) does nothing and does not throw.
    /// </summary>
    [Test]
    public async Task OnFaint_NoBattleData_DoesNotThrow()
    {
        // Arrange
        var effect = new DestinyBondEffect();
        var pokemon = Substitute.For<IPokemon>();
        pokemon.BattleData.Returns((IPokemonBattleData?)null);

        // Act & Assert
        await Assert.That(() => effect.OnFaint(pokemon, DamageSource.MoveDamage)).ThrowsNothing();
    }
}