using PkmnLib.Dynamic.Models;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Plugin.Gen7.Scripts.Abilities;
using PkmnLib.Plugin.Gen7.Scripts.Moves;
using PkmnLib.Static.Species;
using PkmnLib.Static.Utils;

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

/// <summary>
/// Tests for the <see cref="DreamEater"/> move script.
/// Gen VII Bulbapedia behavior: "Dream Eater only works if the target is asleep; otherwise, it does
/// nothing", and "50% of the damage dealt to the target is restored to the user as HP".
/// </summary>
public class DreamEaterTests
{
    /// <summary>
    /// Creates a fully mocked test setup for Dream Eater tests.
    /// </summary>
    private static (DreamEater script, IExecutingMove move, IPokemon target, IPokemon user) CreateTestSetup(uint damage,
        bool holdsBigRoot = false, Script[]? targetScripts = null)
    {
        var script = new DreamEater();
        var move = Substitute.For<IExecutingMove>();
        var target = Substitute.For<IPokemon>();
        var hitData = Substitute.For<IHitData>();
        hitData.Damage.Returns(damage);
        move.GetHitData(target, 0).Returns(hitData);

        // RunScriptHook iterates the target's scripts; give the mock a real iterator so the
        // hook pass runs (empty unless the test attaches scripts such as Liquid Ooze).
        var containers = (targetScripts ?? []).Select(IEnumerable<ScriptContainer> (s) => new ScriptContainer(s))
            .ToArray();
        target.GetScripts().Returns(_ => new ScriptIterator(containers));

        var user = Substitute.For<IPokemon>();
        if (holdsBigRoot)
            user.HasHeldItem("big_root").Returns(true);
        move.User.Returns(user);

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

    /// <summary>
    /// Helper to extract the heal amount from the user's received Heal calls.
    /// </summary>
    private static uint? GetHealAmount(IPokemon user)
    {
        var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Heal");
        return call != null ? (uint)call.GetArguments()[0]! : null;
    }

    /// <summary>
    /// Bulbapedia: "Dream Eater only works if the target is asleep; otherwise, it does nothing."
    /// The outgoing hit is blocked when the target does not have the sleep status.
    /// </summary>
    [Test]
    public async Task BlockOutgoingHit_TargetNotAsleep_BlocksHit()
    {
        // Arrange
        var (script, move, target, _) = CreateTestSetup(100);
        target.HasStatus(new StringKey("asleep")).Returns(false);
        var block = false;

        // Act
        script.BlockOutgoingHit(move, target, 0, ref block);

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

    /// <summary>
    /// Bulbapedia: "Dream Eater only works if the target is asleep". A sleeping target can be hit.
    /// </summary>
    [Test]
    public async Task BlockOutgoingHit_TargetAsleep_DoesNotBlockHit()
    {
        // Arrange
        var (script, move, target, _) = CreateTestSetup(100);
        target.HasStatus(new StringKey("asleep")).Returns(true);
        var block = false;

        // Act
        script.BlockOutgoingHit(move, target, 0, ref block);

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

    /// <summary>
    /// Bulbapedia: "Dream Eater also affects targets with the Comatose Ability, which act as though they
    /// are asleep without the status condition."
    /// </summary>
    [Test]
    public async Task BlockOutgoingHit_TargetHasComatose_DoesNotBlockHit()
    {
        // Arrange
        var (script, move, target, _) = CreateTestSetup(100);
        target.HasStatus(new StringKey("asleep")).Returns(false);
        var comatose = Substitute.For<IAbility>();
        comatose.Name.Returns(new StringKey("comatose"));
        target.ActiveAbility.Returns(comatose);
        var block = false;

        // Act
        script.BlockOutgoingHit(move, target, 0, ref block);

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

    /// <summary>
    /// Bulbapedia: "50% of the damage dealt to the target is restored to the user as HP".
    /// </summary>
    [Test, Arguments(100u, 50u), Arguments(200u, 100u), Arguments(51u, 25u), Arguments(99u, 49u)]
    public async Task OnSecondaryEffect_DamageDealt_UserHealsHalfOfDamage(uint damage, uint expectedHeal)
    {
        // Arrange
        var (script, move, target, user) = CreateTestSetup(damage);

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

        // Assert
        await Assert.That(GetHealAmount(user)!.Value).IsEqualTo(expectedHeal);
    }

    /// <summary>
    /// Bulbapedia: "if the attack deals 1 HP of damage, 1 HP will be restored to the user."
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_DamageOfOne_HealsAtLeastOneHp()
    {
        // Arrange
        var (script, move, target, user) = CreateTestSetup(1);

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

        // Assert
        await Assert.That(GetHealAmount(user)!.Value).IsEqualTo(1u);
    }

    /// <summary>
    /// Bulbapedia: "If the user is holding a Big Root, the HP restored is increased by 30% (making the
    /// restored HP 65% of the damage dealt)."
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_UserHoldingBigRoot_HealIncreasedByThirtyPercent()
    {
        // Arrange
        var (script, move, target, user) = CreateTestSetup(100, true);

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

        // Assert - 65% of 100 damage
        await Assert.That(GetHealAmount(user)!.Value).IsEqualTo(65u);
    }

    /// <summary>
    /// Bulbapedia: "When used on a Pokémon with the Liquid Ooze Ability, the user will lose the amount of
    /// HP it would have gained instead."
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_TargetHasLiquidOoze_UserLosesHpInsteadOfHealing()
    {
        // Arrange
        var (script, move, target, user) = CreateTestSetup(100, targetScripts: [new LiquidOoze()]);

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

        // Assert - user takes the 50 HP it would have gained, and is not healed
        var damageCall = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
        await Assert.That(damageCall).IsNotNull();
        await Assert.That((uint)damageCall!.GetArguments()[0]!).IsEqualTo(50u);
        await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Heal")).IsFalse();
    }

    /// <summary>
    /// Bulbapedia: "the user will lose the amount of HP it would have gained instead." The HP loss caused
    /// by Liquid Ooze is indirect damage, not move damage.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_TargetHasLiquidOoze_UsesMiscDamageSource()
    {
        // Arrange
        var (script, move, target, user) = CreateTestSetup(100, targetScripts: [new LiquidOoze()]);

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

        // Assert
        var damageCall = user.ReceivedCalls().First(c => c.GetMethodInfo().Name == "Damage");
        await Assert.That((DamageSource)damageCall.GetArguments()[1]!).IsEqualTo(DamageSource.Misc);
    }
}