using PkmnLib.Dynamic.Events;
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.Moves;
using PkmnLib.Static.Utils;

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

/// <summary>
/// Tests for the <see cref="Dig"/> move script and its <see cref="DigEffect"/>.
/// Gen VII Bulbapedia behavior: the user digs underground on the first turn, becoming semi-invulnerable,
/// and attacks on the second turn. "Earthquake, Magnitude, and Fissure hit underground targets (dealing
/// double damage from Gen II onward)".
/// </summary>
public class DigTests
{
    private static (Dig script, IExecutingMove move, IPokemon user, ScriptSet userVolatile, IMoveChoice moveChoice)
        CreateTestSetup()
    {
        var script = new Dig();
        var move = Substitute.For<IExecutingMove>();
        var user = Substitute.For<IPokemon>();
        // Use a real script set so the charge volatile added by Dig can be inspected afterwards.
        var userVolatile = new ScriptSet(user);
        user.Volatile.Returns(userVolatile);
        user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
        move.User.Returns(user);

        var moveChoice = Substitute.For<IMoveChoice>();
        move.MoveChoice.Returns(moveChoice);

        var battle = Substitute.For<IBattle>();
        battle.EventHook.Returns(new EventHook());
        var battleData = Substitute.For<IPokemonBattleData>();
        battleData.Battle.Returns(battle);
        user.BattleData.Returns(battleData);

        return (script, move, user, userVolatile, moveChoice);
    }

    /// <summary>
    /// Bulbapedia: "The user excavates underground, becoming immune to most moves during the initial turn".
    /// On the first turn the move is prevented from executing.
    /// </summary>
    [Test]
    public async Task PreventMove_FirstUse_PreventsMoveForChargeTurn()
    {
        // Arrange
        var (script, move, _, _, _) = CreateTestSetup();
        var prevent = false;

        // Act
        script.PreventMove(move, ref prevent);

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

    /// <summary>
    /// Bulbapedia: the user gains "a semi-invulnerable state" on the initial turn. The charge turn
    /// attaches the <see cref="DigEffect"/> volatile script to the user.
    /// </summary>
    [Test]
    public async Task PreventMove_FirstUse_AddsDigEffectToUser()
    {
        // Arrange
        var (script, move, _, userVolatile, _) = CreateTestSetup();
        var prevent = false;

        // Act
        script.PreventMove(move, ref prevent);

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

    /// <summary>
    /// The charge turn is marked on the <see cref="IMoveChoice"/> so the <see cref="DigEffect"/> knows
    /// this choice was the charging turn and does not remove itself for it.
    /// </summary>
    [Test]
    public async Task PreventMove_FirstUse_MarksMoveChoiceAsChargeTurn()
    {
        // Arrange
        var (script, move, _, _, moveChoice) = CreateTestSetup();
        var prevent = false;

        // Act
        script.PreventMove(move, ref prevent);

        // Assert
        moveChoice.Received(1).SetAdditionalData(new StringKey("dig_charge"), true);
        await Assert.That(prevent).IsTrue();
    }

    /// <summary>
    /// Bulbapedia: "On the following turn, Dig deals damage." When the user already has the
    /// <see cref="DigEffect"/> from the charge turn, the move is not prevented again.
    /// </summary>
    [Test]
    public async Task PreventMove_SecondTurn_DoesNotPreventMove()
    {
        // Arrange
        var (script, move, user, userVolatile, _) = CreateTestSetup();
        userVolatile.Add(new DigEffect(user));
        var prevent = false;

        // Act
        script.PreventMove(move, ref prevent);

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

    /// <summary>
    /// Bulbapedia: "On the following turn, Dig deals damage." Once the attack executes, the underground
    /// state (<see cref="DigEffect"/>) is removed from the user.
    /// </summary>
    [Test]
    public async Task OnBeforeMove_ChargeCompleted_RemovesDigEffect()
    {
        // Arrange
        var (script, move, user, userVolatile, _) = CreateTestSetup();
        userVolatile.Add(new DigEffect(user));

        // Act
        script.OnBeforeMove(move);

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

    /// <summary>
    /// Bulbapedia: while underground the user is in "a semi-invulnerable state" and is "immune to most
    /// moves". The <see cref="DigEffect"/> blocks incoming hits from moves that cannot hit underground
    /// Pokémon.
    /// </summary>
    [Test]
    public async Task BlockIncomingHit_MoveCannotHitUnderground_BlocksHit()
    {
        // Arrange
        var (script, move, user, userVolatile, _) = CreateTestSetup();
        var prevent = false;
        script.PreventMove(move, ref prevent);
        await Assert.That(userVolatile.TryGet<DigEffect>(out var effect)).IsTrue();

        var incomingMove = Substitute.For<IExecutingMove>();
        var incomingMoveData = Substitute.For<IMoveData>();
        incomingMoveData.HasFlag(new StringKey("hit_underground")).Returns(false);
        incomingMove.UseMove.Returns(incomingMoveData);

        // Act
        var block = false;
        effect!.BlockIncomingHit(incomingMove, user, 0, ref block);

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

    /// <summary>
    /// Bulbapedia: "Earthquake, Magnitude, and Fissure hit underground targets". Moves flagged as able to
    /// hit underground Pokémon are not blocked by the <see cref="DigEffect"/>.
    /// </summary>
    [Test]
    public async Task BlockIncomingHit_EarthquakeLikeMove_DoesNotBlockHit()
    {
        // Arrange
        var (script, move, user, userVolatile, _) = CreateTestSetup();
        var prevent = false;
        script.PreventMove(move, ref prevent);
        await Assert.That(userVolatile.TryGet<DigEffect>(out var effect)).IsTrue();

        var incomingMove = Substitute.For<IExecutingMove>();
        var incomingMoveData = Substitute.For<IMoveData>();
        incomingMoveData.HasFlag(new StringKey("hit_underground")).Returns(true);
        incomingMove.UseMove.Returns(incomingMoveData);

        // Act
        var block = false;
        effect!.BlockIncomingHit(incomingMove, user, 0, ref block);

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

    /// <summary>
    /// Bulbapedia: "Earthquake, Magnitude, and Fissure hit underground targets (dealing double damage from
    /// Gen II onward)". A move flagged as effective against underground Pokémon deals doubled damage to the
    /// underground user.
    /// </summary>
    [Test]
    public async Task ChangeIncomingMoveDamage_EarthquakeLikeMove_DamageIsDoubled()
    {
        // Arrange
        var (script, move, user, userVolatile, _) = CreateTestSetup();
        var prevent = false;
        script.PreventMove(move, ref prevent);
        await Assert.That(userVolatile.TryGet<DigEffect>(out var effect)).IsTrue();

        var incomingMove = Substitute.For<IExecutingMove>();
        var incomingMoveData = Substitute.For<IMoveData>();
        incomingMoveData.HasFlag(new StringKey("hit_underground")).Returns(true);
        incomingMoveData.HasFlag(new StringKey("effective_against_underground")).Returns(true);
        incomingMove.UseMove.Returns(incomingMoveData);
        uint damage = 100;

        // Act
        effect!.ChangeIncomingMoveDamage(incomingMove, user, 0, ref damage);

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

    /// <summary>
    /// Bulbapedia: only Earthquake and Magnitude deal double damage to underground targets. A move without
    /// the effective-against-underground flag deals regular damage.
    /// </summary>
    [Test]
    public async Task ChangeIncomingMoveDamage_RegularMove_DamageIsUnchanged()
    {
        // Arrange
        var (script, move, user, userVolatile, _) = CreateTestSetup();
        var prevent = false;
        script.PreventMove(move, ref prevent);
        await Assert.That(userVolatile.TryGet<DigEffect>(out var effect)).IsTrue();

        var incomingMove = Substitute.For<IExecutingMove>();
        var incomingMoveData = Substitute.For<IMoveData>();
        incomingMoveData.HasFlag(new StringKey("effective_against_underground")).Returns(false);
        incomingMove.UseMove.Returns(incomingMoveData);
        uint damage = 100;

        // Act
        effect!.ChangeIncomingMoveDamage(incomingMove, user, 0, ref damage);

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

    /// <summary>
    /// Bulbapedia: "If disrupted, PP isn't consumed and the move doesn't count as the last used" — when
    /// the user's executed choice was not the Dig charge (i.e. the move was disrupted or replaced), the
    /// underground state is removed.
    /// </summary>
    [Test]
    public async Task OnAfterMoveChoice_ChoiceIsNotDigCharge_RemovesDigEffect()
    {
        // Arrange
        var user = Substitute.For<IPokemon>();
        var userVolatile = new ScriptSet(user);
        user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
        var effect = new DigEffect(user);
        userVolatile.Add(effect);
        var choice = Substitute.For<IMoveChoice>();
        choice.AdditionalData.Returns((Dictionary<StringKey, object?>?)null);

        // Act
        effect.OnAfterMoveChoice(choice);

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

    /// <summary>
    /// The charging turn itself carries the "dig_charge" marker; the <see cref="DigEffect"/> must survive
    /// that turn so the user stays underground until the attack turn.
    /// </summary>
    [Test]
    public async Task OnAfterMoveChoice_ChoiceIsDigCharge_KeepsDigEffect()
    {
        // Arrange
        var user = Substitute.For<IPokemon>();
        var userVolatile = new ScriptSet(user);
        user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
        var effect = new DigEffect(user);
        userVolatile.Add(effect);
        var choice = Substitute.For<IMoveChoice>();
        choice.AdditionalData.Returns(new Dictionary<StringKey, object?> { { "dig_charge", true } });

        // Act
        effect.OnAfterMoveChoice(choice);

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