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

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

/// <summary>
/// Tests for the <see cref="BugBite"/> move script.
/// Behavior is verified against the Bulbapedia page for Bug Bite.
/// </summary>
public class BugBiteTests
{
    private const string BerryEffectName = "test_berry_effect";

    /// <summary>
    /// Minimal <see cref="ItemScript"/> that records whether its effect was applied through
    /// <see cref="ItemScript.OnUse"/>.
    /// </summary>
    private class RecordingItemScript : ItemScript
    {
        public RecordingItemScript(IItem item) : base(item)
        {
        }

        public bool WasUsed { get; private set; }

        /// <inheritdoc />
        public override bool IsItemUsable => true;

        /// <inheritdoc />
        public override void OnUse(EventHook eventHook) => WasUsed = true;
    }

    /// <summary>
    /// Creates a fully mocked test setup for BugBite tests. The battle's <see cref="ScriptResolver"/> is a
    /// real resolver with a single registered item script constructor for <see cref="BerryEffectName"/>, so
    /// eating a Berry runs a <see cref="RecordingItemScript"/> that the test can inspect.
    /// </summary>
    private static (BugBite bugBite, IExecutingMove move, IPokemon target, IHitData hitData, List<RecordingItemScript>
        createdItemScripts) CreateTestSetup(IItem? targetHeldItem, bool canSteal = true)
    {
        var bugBite = new BugBite();

        var createdItemScripts = new List<RecordingItemScript>();
        var resolver = new ScriptResolver(new Dictionary<(ScriptCategory, StringKey), Func<Script>>(),
            new Dictionary<StringKey, Func<IItem, ItemScript>>
            {
                {
                    new StringKey(BerryEffectName), item =>
                    {
                        var script = new RecordingItemScript(item);
                        createdItemScripts.Add(script);
                        return script;
                    }
                },
            });

        var battle = Substitute.For<IBattle>();
        battle.EventHook.Returns(new EventHook());
        var dynamicLibrary = Substitute.For<IDynamicLibrary>();
        dynamicLibrary.ScriptResolver.Returns(resolver);
        battle.Library.Returns(dynamicLibrary);

        var battleData = Substitute.For<IPokemonBattleData>();
        battleData.Battle.Returns(battle);

        var user = Substitute.For<IPokemon>();
        user.BattleData.Returns(battleData);

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

        var target = Substitute.For<IPokemon>();
        target.HeldItem.Returns(targetHeldItem);
        if (targetHeldItem != null && canSteal)
        {
            target.TryStealHeldItem(out Arg.Any<IItem?>()).Returns(x =>
            {
                x[0] = targetHeldItem;
                return true;
            });
        }
        else
        {
            target.TryStealHeldItem(out Arg.Any<IItem?>()).Returns(false);
        }

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

        return (bugBite, move, target, hitData, createdItemScripts);
    }

    /// <summary>
    /// Creates a mocked Berry item whose battle effect is <see cref="BerryEffectName"/>.
    /// </summary>
    private static IItem CreateBerry()
    {
        var berry = Substitute.For<IItem>();
        berry.Category.Returns(ItemCategory.Berry);
        berry.BattleEffect.Returns(new SecondaryEffectImpl(-1, new StringKey(BerryEffectName),
            new Dictionary<StringKey, object?>()));
        return berry;
    }

    /// <summary>
    /// Bulbapedia: "If the target is holding a Berry, the user will eat the Berry and gain its effect."
    /// Eating the Berry removes it from the target via <see cref="IPokemon.ForceSetHeldItem"/>.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_TargetHoldsBerry_BerryIsRemovedFromTarget()
    {
        // Arrange
        var (bugBite, move, target, _, _) = CreateTestSetup(CreateBerry());

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

        // Assert
        await Assert.That(target.ReceivedCalls().Any(c =>
            c.GetMethodInfo().Name == "ForceSetHeldItem" && c.GetArguments()[0] == null)).IsTrue();
    }

    /// <summary>
    /// Bulbapedia: "If the target is holding a Berry, the user will eat the Berry and gain its effect."
    /// The Berry's item script is executed for the user, applying its effect.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_TargetHoldsBerry_UserGainsBerryEffect()
    {
        // Arrange
        var (bugBite, move, target, _, createdItemScripts) = CreateTestSetup(CreateBerry());

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

        // Assert - exactly one berry script was resolved and its effect was applied
        await Assert.That(createdItemScripts.Count).IsEqualTo(1);
        await Assert.That(createdItemScripts[0].WasUsed).IsTrue();
    }

    /// <summary>
    /// Bulbapedia: "If the target is holding a Berry, the user will eat the Berry and gain its effect."
    /// When the Berry is successfully eaten, the secondary effect must not be marked as failed.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_TargetHoldsBerry_HitDoesNotFail()
    {
        // Arrange
        var (bugBite, move, target, hitData, _) = CreateTestSetup(CreateBerry());

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

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

    /// <summary>
    /// Bulbapedia: "If the target is holding a Berry, the user will eat the Berry and gain its effect."
    /// A held item that is not a Berry is not eaten: the secondary effect fails and the target keeps
    /// its item.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_TargetHoldsNonBerryItem_EffectFailsAndItemIsKept()
    {
        // Arrange
        var nonBerry = Substitute.For<IItem>();
        nonBerry.Category.Returns(ItemCategory.MiscItem);
        var (bugBite, move, target, hitData, _) = CreateTestSetup(nonBerry);

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

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

    /// <summary>
    /// Bulbapedia: "If the target is holding a Berry, the user will eat the Berry and gain its effect."
    /// With no held item at all there is nothing to eat, so the secondary effect fails.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_TargetHoldsNoItem_EffectFails()
    {
        // Arrange
        var (bugBite, move, target, hitData, _) = CreateTestSetup(null);

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

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

    /// <summary>
    /// Bulbapedia: "Bug Bite will not consume the Berry of a target that has the Ability Sticky Hold."
    /// When the Berry cannot be stolen (<see cref="IPokemon.TryStealHeldItem"/> returns false, as with
    /// Sticky Hold), the target keeps its Berry and the effect fails.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_BerryCannotBeStolen_TargetKeepsBerry()
    {
        // Arrange
        var (bugBite, move, target, hitData, createdItemScripts) = CreateTestSetup(CreateBerry(), false);

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

        // Assert - the berry is not removed, its effect is not gained, and the effect fails
        await Assert.That(target.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "ForceSetHeldItem")).IsFalse();
        await Assert.That(createdItemScripts.Count).IsEqualTo(0);
        await Assert.That(hitData.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Fail")).IsTrue();
    }

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

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

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