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

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

/// <summary>
/// Tests for the <see cref="Bestow"/> move script.
/// Gen VII Bulbapedia behavior: "The user transfers its held item to the target. It fails if the user has no
/// held item, the target already has a held item, or the target is behind a substitute. [...] Items given away
/// in Trainer battles return to the original Pokémon after the battle."
/// </summary>
public class BestowTests
{
    private static (Bestow script, IExecutingMove move, IPokemon user, IPokemon target, IHitData hitData)
        CreateTestSetup(IItem? userItem, IItem? targetItem)
    {
        var script = new Bestow();
        var move = Substitute.For<IExecutingMove>();
        var target = Substitute.For<IPokemon>();
        var hitData = Substitute.For<IHitData>();
        move.GetHitData(target, 0).Returns(hitData);
        target.HeldItem.Returns(targetItem);

        var user = Substitute.For<IPokemon>();
        user.HeldItem.Returns(userItem);
        user.RemoveHeldItemForBattle().Returns(userItem);
        move.User.Returns(user);

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

    /// <summary>
    /// Bulbapedia: "The user transfers its held item to the target."
    /// The target ends up holding the user's item.
    /// </summary>
    [Test]
    public void OnSecondaryEffect_TargetHasNoItem_TargetReceivesUsersItem()
    {
        // Arrange
        var userItem = Substitute.For<IItem>();
        var (script, move, _, target, _) = CreateTestSetup(userItem, null);

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

        // Assert
        _ = target.Received(1).ForceSetHeldItem(userItem);
    }

    /// <summary>
    /// Bulbapedia: "The user transfers its held item to the target."
    /// A successful transfer does not fail the move.
    /// </summary>
    [Test]
    public void OnSecondaryEffect_TargetHasNoItem_MoveDoesNotFail()
    {
        // Arrange
        var userItem = Substitute.For<IItem>();
        var (script, move, _, target, hitData) = CreateTestSetup(userItem, null);

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

        // Assert
        hitData.DidNotReceive().Fail();
    }

    /// <summary>
    /// Bulbapedia: "Items given away in Trainer battles return to the original Pokémon after the battle."
    /// The item is taken from the user through <see cref="IPokemon.RemoveHeldItemForBattle"/>, which only
    /// removes the item for the duration of the battle.
    /// </summary>
    [Test]
    public void OnSecondaryEffect_TargetHasNoItem_UserItemIsRemovedForBattleOnly()
    {
        // Arrange
        var userItem = Substitute.For<IItem>();
        var (script, move, user, target, _) = CreateTestSetup(userItem, null);

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

        // Assert
        user.Received(1).RemoveHeldItemForBattle();
    }

    /// <summary>
    /// Bulbapedia: "It fails if the user has no held item".
    /// </summary>
    [Test]
    public void OnSecondaryEffect_UserHasNoHeldItem_MoveFails()
    {
        // Arrange
        var (script, move, _, target, hitData) = CreateTestSetup(null, null);

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

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

    /// <summary>
    /// Bulbapedia: "It fails if the user has no held item".
    /// When the move fails, no item is given to the target.
    /// </summary>
    [Test]
    public void OnSecondaryEffect_UserHasNoHeldItem_TargetDoesNotReceiveAnItem()
    {
        // Arrange
        var (script, move, _, target, _) = CreateTestSetup(null, null);

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

        // Assert
        _ = target.DidNotReceive().ForceSetHeldItem(Arg.Any<IItem>());
    }

    /// <summary>
    /// Bulbapedia: "It fails if [...] the target already has a held item".
    /// </summary>
    [Test]
    public void OnSecondaryEffect_TargetAlreadyHasHeldItem_MoveFails()
    {
        // Arrange
        var (script, move, _, target, hitData) = CreateTestSetup(Substitute.For<IItem>(), Substitute.For<IItem>());

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

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

    /// <summary>
    /// Bulbapedia: "It fails if [...] the target already has a held item".
    /// The target's own held item is left untouched by the failed move.
    /// </summary>
    [Test]
    public void OnSecondaryEffect_TargetAlreadyHasHeldItem_TargetItemIsNotReplaced()
    {
        // Arrange
        var (script, move, _, target, _) = CreateTestSetup(Substitute.For<IItem>(), Substitute.For<IItem>());

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

        // Assert
        target.DidNotReceive().ForceSetHeldItem(Arg.Any<IItem>());
    }

    /// <summary>
    /// Bulbapedia: "It fails if [...] the target already has a held item".
    /// A failed Bestow must not cost the user its held item: the item is either never taken from the user, or
    /// it is given back after the failure check.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_TargetAlreadyHasHeldItem_UserKeepsItsHeldItem()
    {
        // Arrange
        var userItem = Substitute.For<IItem>();
        var (script, move, user, target, _) = CreateTestSetup(userItem, Substitute.For<IItem>());

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

        // Assert - the user still holds its item: it was either never removed, or restored after the failure.
        var removed = user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "RemoveHeldItemForBattle");
        var restored = user.ReceivedCalls().Any(c =>
            c.GetMethodInfo().Name == "ForceSetHeldItem" && ReferenceEquals(c.GetArguments()[0], userItem));
        await Assert.That(!removed || restored).IsTrue();
    }
}