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

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

/// <summary>
/// Tests for the <see cref="Drain"/> script, which implements Absorb (and the other HP-draining moves).
/// Behavior is verified against the Bulbapedia page for Absorb.
/// </summary>
public class DrainTests
{
    /// <summary>
    /// Creates a fully mocked test setup for Drain tests.
    /// </summary>
    private static (Drain drain, IExecutingMove move, IPokemon target, IPokemon user) CreateTestSetup(uint damage,
        bool holdsBigRoot = false, Script[]? targetScripts = null)
    {
        var drain = new Drain();
        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 (drain, 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>
    /// Helper to extract the damage amount from the user's received Damage calls.
    /// </summary>
    private static uint? GetDamageAmount(IPokemon user)
    {
        var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
        return call != null ? (uint)call.GetArguments()[0]! : null;
    }

    /// <summary>
    /// Helper to extract the damage source from the user's received Damage calls.
    /// </summary>
    private static DamageSource? GetDamageSource(IPokemon user)
    {
        var call = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
        return call != null ? (DamageSource)call.GetArguments()[1]! : null;
    }

    /// <summary>
    /// Bulbapedia: "Absorb inflicts damage, and up to 50% of the damage dealt to the target is restored to the
    /// user as HP".
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_DamageDealt_UserHealsHalfOfDamage()
    {
        // Arrange
        var (drain, move, target, user) = CreateTestSetup(100);

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

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

    /// <summary>
    /// Bulbapedia: "up to 50% of the damage dealt to the target is restored to the user as HP".
    /// Tests various damage values to ensure proper integer truncation of the halved amount.
    /// </summary>
    [Test, Arguments(200u, 100u), Arguments(51u, 25u), Arguments(99u, 49u), Arguments(3u, 1u)]
    public async Task OnSecondaryEffect_DamageDealt_HealCalculation(uint damage, uint expectedHeal)
    {
        // Arrange
        var (drain, move, target, user) = CreateTestSetup(damage);

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

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

    /// <summary>
    /// Bulbapedia: "up to 50% of the damage dealt to the target is restored to the user as HP (but not less
    /// than 1 HP)".
    /// A move dealing 1 damage should still restore 1 HP.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_DamageOfOne_HealsAtLeastOneHp()
    {
        // Arrange
        var (drain, move, target, user) = CreateTestSetup(1);

        // Act
        drain.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 (drain, move, target, user) = CreateTestSetup(100, true);

        // Act
        drain.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 (drain, move, target, user) = CreateTestSetup(100, targetScripts: [new LiquidOoze()]);

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

        // Assert - user takes the 50 HP it would have gained, and is not healed
        await Assert.That(GetDamageAmount(user)!.Value).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 (drain, move, target, user) = CreateTestSetup(100, targetScripts: [new LiquidOoze()]);

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

        // Assert
        await Assert.That(GetDamageSource(user)!.Value).IsEqualTo(DamageSource.Misc);
    }

    /// <summary>
    /// Bulbapedia: "the user will lose the amount of HP it would have gained instead."
    /// With a Big Root held, the amount it would have gained is 65% of the damage, so that is what is lost.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_LiquidOozeAndBigRoot_UserLosesBoostedAmount()
    {
        // Arrange
        var (drain, move, target, user) = CreateTestSetup(100, true, [new LiquidOoze()]);

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

        // Assert
        await Assert.That(GetDamageAmount(user)!.Value).IsEqualTo(65u);
    }

    /// <summary>
    /// Technical test: without initialization parameters the drain modifier defaults to 50%, matching Absorb.
    /// </summary>
    [Test]
    public async Task OnInitialize_NullParameters_DrainModifierDefaultsToHalf()
    {
        // Arrange
        var drain = new Drain();

        // Act
        drain.OnInitialize(null);

        // Assert
        await Assert.That(drain.DrainModifier).IsEqualTo(0.5f);
    }

    /// <summary>
    /// Technical test: a "drain_modifier" parameter overrides the default drain fraction.
    /// </summary>
    [Test]
    public async Task OnInitialize_DrainModifierParameter_OverridesDefault()
    {
        // Arrange
        var drain = new Drain();
        var parameters = new Dictionary<StringKey, object?> { { "drain_modifier", 0.75f } };

        // Act
        drain.OnInitialize(parameters);

        // Assert
        await Assert.That(drain.DrainModifier).IsEqualTo(0.75f);
    }

    /// <summary>
    /// Bulbapedia: "up to 50% of the damage dealt to the target is restored to the user as HP".
    /// Integration check: initializing the script with Absorb's actual effect parameters from the Gen7 data
    /// results in a 50% drain.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_InitializedWithAbsorbData_HealsHalfOfDamage()
    {
        // Arrange
        var library = LibraryHelpers.LoadLibrary();
        await Assert.That(library.StaticLibrary.Moves.TryGet("absorb", out var absorb)).IsTrue();
        var (drain, move, target, user) = CreateTestSetup(100);

        // Act
        drain.OnInitialize(absorb!.SecondaryEffect!.Parameters);
        drain.OnSecondaryEffect(move, target, 0);

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