using PkmnLib.Dynamic.Models;
using PkmnLib.Dynamic.Models.Choices;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Dynamic.ScriptHandling.Registry;
using PkmnLib.Plugin.Gen7.Scripts;
using PkmnLib.Plugin.Gen7.Scripts.Moves;
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
using PkmnLib.Static.Utils;

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

/// <summary>
/// Tests for the <see cref="Bind"/> move script and its <see cref="BindEffect"/> volatile.
/// Gen VII Bulbapedia behavior (Gen II base plus Gen V/VI deltas): "It also traps the target, preventing
/// switching and escape", "Its effect will last either 4 or 5 turns", and "The end turn damage of Bind is
/// increased from 1/16 to 1/8 of the target's maximum HP."
/// </summary>
public class BindTests
{
    /// <summary>
    /// Test helper script that modifies the Bind duration/damage through the <see cref="CustomTriggers.ModifyBind"/>
    /// custom trigger, the same way the Grip Claw and Binding Band item scripts do.
    /// </summary>
    [Script(ScriptCategory.Pokemon, "test_modify_bind_trigger")]
    private class ModifyBindTrigger : Script, IScriptCustomTrigger
    {
        private readonly int? _duration;
        private readonly float? _damagePercent;

        public ModifyBindTrigger(int? duration = null, float? damagePercent = null)
        {
            _duration = duration;
            _damagePercent = damagePercent;
        }

        public void CustomTrigger(StringKey eventName, ICustomTriggerArgs args)
        {
            if (eventName != CustomTriggers.ModifyBind || args is not CustomTriggers.ModifyBindArgs bindArgs)
                return;
            if (_duration.HasValue)
                bindArgs.Duration = _duration.Value;
            if (_damagePercent.HasValue)
                bindArgs.DamagePercent = _damagePercent.Value;
        }
    }

    private static (Bind script, IExecutingMove move, IPokemon user, IPokemon target, IScriptSet targetVolatile)
        CreateTestSetup(params Script[] userScripts)
    {
        var script = new Bind();
        var move = Substitute.For<IExecutingMove>();
        var target = Substitute.For<IPokemon>();
        var targetVolatile = Substitute.For<IScriptSet>();
        target.Volatile.Returns(targetVolatile);

        var user = Substitute.For<IPokemon>();
        // RunScriptHook iterates the user's scripts; give the mock a real iterator so the ModifyBind trigger
        // pass runs (empty unless the test attaches scripts such as the Grip Claw / Binding Band stand-in).
        var containers = userScripts.Select(IEnumerable<ScriptContainer> (s) => new ScriptContainer(s)).ToArray();
        user.GetScripts().Returns(_ => new ScriptIterator(containers));
        move.User.Returns(user);

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

    /// <summary>
    /// Helper to extract the <see cref="BindEffect"/> instance that was added to the target's volatile scripts.
    /// </summary>
    private static BindEffect? GetAddedEffect(IScriptSet targetVolatile) => targetVolatile.ReceivedCalls()
        .Where(c => c.GetMethodInfo().Name == "Add").Select(c => c.GetArguments()[0]).OfType<BindEffect>()
        .FirstOrDefault();

    /// <summary>
    /// Helper to extract the damage amount from the target's first received Damage call.
    /// </summary>
    private static uint? GetDamageAmount(IPokemon pokemon)
    {
        var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
        return call != null ? (uint)call.GetArguments()[0]! : null;
    }

    /// <summary>
    /// Runs end-of-turn handling on the effect repeatedly and counts how many turns dealt damage to the target.
    /// </summary>
    private static int CountEndTurnDamageTicks(BindEffect effect, IPokemon target, int maxTurns = 10)
    {
        var battle = Substitute.For<IBattle>();
        for (var i = 0; i < maxTurns; i++)
            effect.OnEndTurn(target, battle);
        return target.ReceivedCalls().Count(c => c.GetMethodInfo().Name == "Damage");
    }

    /// <summary>
    /// Bulbapedia: "It also traps the target, preventing switching and escape."
    /// Using Bind adds the <see cref="BindEffect"/> volatile to the target.
    /// </summary>
    [Test]
    public void OnSecondaryEffect_AddsBindEffectToTargetVolatile()
    {
        // Arrange
        var (script, move, _, target, targetVolatile) = CreateTestSetup();

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

        // Assert
        targetVolatile.Received(1).Add(Arg.Any<BindEffect>());
    }

    /// <summary>
    /// Bulbapedia: "The end turn damage of Bind is increased from 1/16 to 1/8 of the target's maximum HP."
    /// Tests several max HP values to ensure proper integer truncation of the 1/8 fraction.
    /// </summary>
    [Test, Arguments(160u, 20u), Arguments(100u, 12u), Arguments(15u, 1u)]
    public async Task OnSecondaryEffect_Default_EndTurnDamageIsOneEighthOfTargetMaxHp(uint maxHealth,
        uint expectedDamage)
    {
        // Arrange
        var (script, move, _, target, targetVolatile) = CreateTestSetup();
        target.MaxHealth.Returns(maxHealth);

        // Act
        script.OnSecondaryEffect(move, target, 0);
        var effect = GetAddedEffect(targetVolatile);
        await Assert.That(effect).IsNotNull();
        effect!.OnEndTurn(target, Substitute.For<IBattle>());

        // Assert
        await Assert.That(GetDamageAmount(target)!.Value).IsEqualTo(expectedDamage);
    }

    /// <summary>
    /// Bulbapedia: "Its effect will last either 4 or 5 turns."
    /// The added effect deals its end-of-turn damage for 4 or 5 turns and then ends.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_Default_EffectLastsFourOrFiveTurns()
    {
        // Arrange
        var (script, move, _, target, targetVolatile) = CreateTestSetup();
        target.MaxHealth.Returns(160u);

        // Act
        script.OnSecondaryEffect(move, target, 0);
        var effect = GetAddedEffect(targetVolatile);
        await Assert.That(effect).IsNotNull();
        var turnsWithDamage = CountEndTurnDamageTicks(effect!, target);

        // Assert
        await Assert.That(turnsWithDamage is 4 or 5).IsTrue();
    }

    /// <summary>
    /// Bulbapedia: "If the user of Bind is holding a Grip Claw, the duration will be 7 turns."
    /// The duration change flows through the <see cref="CustomTriggers.ModifyBind"/> custom trigger; a script
    /// on the user that sets the duration to 7 results in an effect lasting 7 turns.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_DurationSetToSevenByTrigger_EffectLastsSevenTurns()
    {
        // Arrange
        var (script, move, _, target, targetVolatile) = CreateTestSetup(new ModifyBindTrigger(7));
        target.MaxHealth.Returns(160u);

        // Act
        script.OnSecondaryEffect(move, target, 0);
        var effect = GetAddedEffect(targetVolatile);
        await Assert.That(effect).IsNotNull();
        var turnsWithDamage = CountEndTurnDamageTicks(effect!, target);

        // Assert
        await Assert.That(turnsWithDamage).IsEqualTo(7);
    }

    /// <summary>
    /// Bulbapedia: "If the user is holding a Binding Band, the end turn damage of Bind will increase to 1/6 of
    /// the target's maximum HP."
    /// The damage change flows through the <see cref="CustomTriggers.ModifyBind"/> custom trigger; a script on
    /// the user that sets the damage to 1/6 results in end-of-turn damage of 1/6 max HP.
    /// </summary>
    [Test, Arguments(60u, 10u), Arguments(48u, 8u)]
    public async Task OnSecondaryEffect_DamageSetToOneSixthByTrigger_EndTurnDamageIsOneSixth(uint maxHealth,
        uint expectedDamage)
    {
        // Arrange
        var (script, move, _, target, targetVolatile) = CreateTestSetup(new ModifyBindTrigger(damagePercent: 1f / 6f));
        target.MaxHealth.Returns(maxHealth);

        // Act
        script.OnSecondaryEffect(move, target, 0);
        var effect = GetAddedEffect(targetVolatile);
        await Assert.That(effect).IsNotNull();
        effect!.OnEndTurn(target, Substitute.For<IBattle>());

        // Assert
        await Assert.That(GetDamageAmount(target)!.Value).IsEqualTo(expectedDamage);
    }

    /// <summary>
    /// Bulbapedia: "It also traps the target, preventing switching and escape."
    /// While the effect is active, the target cannot switch out.
    /// </summary>
    [Test]
    public async Task BindEffect_WhileActive_PreventsSwitching()
    {
        // Arrange
        var target = Substitute.For<IPokemon>();
        var effect = new BindEffect(target, 5, 1f / 8f);
        var prevent = false;

        // Act
        effect.PreventSelfSwitch(Substitute.For<ISwitchChoice>(), ref prevent);

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

    /// <summary>
    /// Bulbapedia: "It also traps the target, preventing switching and escape."
    /// While the effect is active, the target cannot flee.
    /// </summary>
    [Test]
    public async Task BindEffect_WhileActive_PreventsRunningAway()
    {
        // Arrange
        var target = Substitute.For<IPokemon>();
        var effect = new BindEffect(target, 5, 1f / 8f);
        var prevent = false;

        // Act
        effect.PreventSelfRunAway(Substitute.For<IFleeChoice>(), ref prevent);

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

    /// <summary>
    /// Bulbapedia: "Its effect will last either 4 or 5 turns."
    /// Once the duration has run out, the target is no longer prevented from switching.
    /// </summary>
    [Test]
    public async Task BindEffect_AfterDurationExpires_NoLongerPreventsSwitching()
    {
        // Arrange
        var target = Substitute.For<IPokemon>();
        target.MaxHealth.Returns(160u);
        var effect = new BindEffect(target, 1, 1f / 8f);

        // Act - the single remaining turn elapses
        effect.OnEndTurn(target, Substitute.For<IBattle>());
        var prevent = false;
        effect.PreventSelfSwitch(Substitute.For<ISwitchChoice>(), ref prevent);

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