using PkmnLib.Dynamic.Libraries;
using PkmnLib.Dynamic.Models;
using PkmnLib.Plugin.Gen7.Scripts.Moves;
using PkmnLib.Static;
using PkmnLib.Static.Libraries;
using PkmnLib.Static.Utils;

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

/// <summary>
/// Tests for the <see cref="BurnUp"/> move script.
/// Behavior is verified against the Bulbapedia page for Burn Up.
/// </summary>
public class BurnUpTests
{
    /// <summary>
    /// Creates a fully mocked test setup for Burn Up tests. The static library's type library is a real
    /// <see cref="TypeLibrary"/> with "fire" and "water" registered. The user is a Water-type, optionally
    /// also carrying the Fire type.
    /// </summary>
    private static (BurnUp burnUp, IExecutingMove move, IPokemon user, IPokemon target, IHitData hitData, TypeIdentifier
        fireType) CreateTestSetup(bool userIsFireType, bool userIsFrozen = false)
    {
        var burnUp = new BurnUp();

        var typeLibrary = new TypeLibrary();
        var fireType = typeLibrary.RegisterType(new StringKey("fire"));
        var waterType = typeLibrary.RegisterType(new StringKey("water"));

        var staticLibrary = Substitute.For<IStaticLibrary>();
        staticLibrary.Types.Returns(typeLibrary);
        var dynamicLibrary = Substitute.For<IDynamicLibrary>();
        dynamicLibrary.StaticLibrary.Returns(staticLibrary);
        var battle = Substitute.For<IBattle>();
        battle.Library.Returns(dynamicLibrary);
        var battleData = Substitute.For<IPokemonBattleData>();
        battleData.Battle.Returns(battle);

        var user = Substitute.For<IPokemon>();
        user.BattleData.Returns(battleData);
        user.Types.Returns(userIsFireType
            ? new List<TypeIdentifier> { fireType, waterType }
            : new List<TypeIdentifier> { waterType });
        user.HasStatus(new StringKey("frozen")).Returns(userIsFrozen);

        var move = Substitute.For<IExecutingMove>();
        move.User.Returns(user);

        var target = Substitute.For<IPokemon>();
        var hitData = Substitute.For<IHitData>();
        move.GetHitData(target, 0).Returns(hitData);

        return (burnUp, move, user, target, hitData, fireType);
    }

    /// <summary>
    /// Helper that checks whether <see cref="IPokemon.RemoveType"/> was called with the given type.
    /// </summary>
    private static bool ReceivedRemoveType(IPokemon user, TypeIdentifier type) =>
        user.ReceivedCalls().Any(c =>
            c.GetMethodInfo().Name == "RemoveType" && type.Equals((TypeIdentifier)c.GetArguments()[0]!));

    /// <summary>
    /// Bulbapedia: "If used by a Fire-type Pokémon, Burn Up will thaw out the user if it is frozen, and
    /// then inflict damage to the target and cause the user to lose its Fire type."
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_FireTypeUser_UserLosesFireType()
    {
        // Arrange
        var (burnUp, move, user, target, _, fireType) = CreateTestSetup(true);

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

        // Assert
        await Assert.That(ReceivedRemoveType(user, fireType)).IsTrue();
    }

    /// <summary>
    /// Bulbapedia: "If used by a Fire-type Pokémon, Burn Up will ... inflict damage to the target".
    /// For a Fire-type user, the hit is not marked as failed.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_FireTypeUser_HitDoesNotFail()
    {
        // Arrange
        var (burnUp, move, _, target, hitData, _) = CreateTestSetup(true);

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

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

    /// <summary>
    /// Bulbapedia: "Burn Up fails if the user is not Fire-type, making the move always fail after the
    /// first use until the Pokémon regains its Fire type".
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_NonFireTypeUser_HitFails()
    {
        // Arrange
        var (burnUp, move, _, target, hitData, _) = CreateTestSetup(false);

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

        // Assert
        await Assert.That(hitData.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Fail")).IsTrue();
    }

    /// <summary>
    /// Bulbapedia: "Burn Up fails if the user is not Fire-type".
    /// A failed Burn Up does not remove any type from the user.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_NonFireTypeUser_NoTypeIsRemoved()
    {
        // Arrange
        var (burnUp, move, user, target, _, _) = CreateTestSetup(false);

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

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

    /// <summary>
    /// Bulbapedia: "If used by a Fire-type Pokémon, Burn Up will thaw out the user if it is frozen".
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_FrozenFireTypeUser_UserIsThawed()
    {
        // Arrange
        var (burnUp, move, user, target, _, _) = CreateTestSetup(true, true);

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

        // Assert
        await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ClearStatus")).IsTrue();
    }

    /// <summary>
    /// Bulbapedia: "Burn Up cannot thaw the user if the move would fail (due to the user not being
    /// Fire-type)."
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_FrozenNonFireTypeUser_UserIsNotThawed()
    {
        // Arrange
        var (burnUp, move, user, target, _, _) = CreateTestSetup(false, true);

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

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

    /// <summary>
    /// Bulbapedia: "Burn Up will thaw out the user if it is frozen".
    /// A Fire-type user that is not frozen has no status cleared.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_NonFrozenFireTypeUser_StatusIsNotCleared()
    {
        // Arrange
        var (burnUp, move, user, target, _, _) = CreateTestSetup(true, false);

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

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

    /// <summary>
    /// If the user has no <see cref="IPokemon.BattleData"/>, the script does nothing: no type is removed
    /// and the hit is not failed.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_UserHasNoBattleData_DoesNothing()
    {
        // Arrange
        var (burnUp, move, user, target, hitData, _) = CreateTestSetup(true);
        user.BattleData.Returns((IPokemonBattleData?)null);

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

        // Assert
        await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "RemoveType")).IsFalse();
        await Assert.That(hitData.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Fail")).IsFalse();
    }
}