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

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

/// <summary>
/// Tests for the <see cref="BanefulBunker"/> move script and its attached <see cref="BanefulBunkerEffect"/>.
/// Behavior is verified against the Bulbapedia page for Baneful Bunker (Generation VII).
/// </summary>
public class BanefulBunkerTests
{
    /// <summary>
    /// Creates a fully mocked setup for driving <see cref="BanefulBunker"/>'s inherited
    /// <see cref="ProtectionScript.OnSecondaryEffect"/>. The <c>target</c> of the secondary effect is the
    /// Pokémon using Baneful Bunker itself, as the move is self-targeted.
    /// </summary>
    private static (BanefulBunker script, IExecutingMove move, IPokemon target, IHitData hitData, IScriptSet volatileSet
        ) CreateProtectSetup(bool userMovesLast, float randomRoll)
    {
        var script = new BanefulBunker();
        var move = Substitute.For<IExecutingMove>();
        var target = Substitute.For<IPokemon>();
        var hitData = Substitute.For<IHitData>();
        move.GetHitData(target, 0).Returns(hitData);

        // A queue with a remaining choice means another Pokémon still has to move after the user.
        var queue = userMovesLast ? new BattleChoiceQueue([]) : new BattleChoiceQueue([Substitute.For<IMoveChoice>()]);

        var random = Substitute.For<IBattleRandom>();
        random.GetFloat().Returns(randomRoll);

        var battle = Substitute.For<IBattle>();
        battle.ChoiceQueue.Returns(queue);
        battle.Random.Returns(random);

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

        // Give the mock a real script iterator (used by the volatile add hook) and a real volatile script set.
        target.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
        IScriptSet volatileSet = new ScriptSet(target);
        target.Volatile.Returns(volatileSet);

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

    /// <summary>
    /// Creates a fully mocked setup for driving <see cref="BanefulBunkerEffect.BlockIncomingHit"/>, the
    /// volatile script that <see cref="BanefulBunker"/> attaches to its user.
    /// </summary>
    private static (BanefulBunkerEffect effect, IExecutingMove move, IPokemon target, IPokemon attacker)
        CreateBlockSetup(bool isContact, bool hasProtectFlag, MoveCategory category = MoveCategory.Physical)
    {
        var effect = new BanefulBunkerEffect();
        var move = Substitute.For<IExecutingMove>();
        var target = Substitute.For<IPokemon>();
        var hitData = Substitute.For<IHitData>();
        hitData.IsContact.Returns(isContact);
        move.GetHitData(target, 0).Returns(hitData);

        var useMove = Substitute.For<IMoveData>();
        useMove.HasFlag(new StringKey("protect")).Returns(hasProtectFlag);
        useMove.Category.Returns(category);
        move.UseMove.Returns(useMove);

        var attacker = Substitute.For<IPokemon>();
        attacker.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
        move.User.Returns(attacker);

        target.BattleData.Returns(Substitute.For<IPokemonBattleData>());

        return (effect, move, target, attacker);
    }

    /// <summary>
    /// Helper to extract the status name from a Pokémon's received <see cref="IPokemon.SetStatus"/> calls.
    /// </summary>
    private static string? GetStatusSet(IPokemon pokemon)
    {
        var call = pokemon.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "SetStatus");
        return call != null ? ((StringKey)call.GetArguments()[0]!).ToString() : null;
    }

    /// <summary>
    /// Bulbapedia: "Baneful Bunker protects the user from all effects of moves that target it during the turn
    /// it is used, including damage."
    /// Using the move attaches the <see cref="BanefulBunkerEffect"/> volatile script to the user.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_FirstUse_AddsBanefulBunkerEffect()
    {
        // Arrange
        var (script, move, target, hitData, volatileSet) = CreateProtectSetup(false, 0.0f);

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

        // Assert
        await Assert.That(volatileSet.Get<BanefulBunkerEffect>()).IsNotNull();
        hitData.DidNotReceive().Fail();
    }

    /// <summary>
    /// Bulbapedia: "If the user goes last in the turn, the move will fail."
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_UserMovesLast_Fails()
    {
        // Arrange
        var (script, move, target, hitData, volatileSet) = CreateProtectSetup(true, 0.0f);

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

        // Assert
        hitData.Received(1).Fail();
        await Assert.That(volatileSet.Get<BanefulBunkerEffect>()).IsNull();
    }

    /// <summary>
    /// Bulbapedia: "The chance that Baneful Bunker will succeed also drops each time the user successfully and
    /// consecutively uses Endure, any protection move that only affects the user, Quick Guard, or Wide Guard."
    /// A successful use registers a consecutive protection turn on the <see cref="ProtectionFailureScript"/>.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_FirstUse_RegistersConsecutiveProtectionUse()
    {
        // Arrange
        var (script, move, target, _, volatileSet) = CreateProtectSetup(false, 0.0f);

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

        // Assert
        var failure = volatileSet.Get<ProtectionFailureScript>();
        await Assert.That(failure).IsNotNull();
        await Assert.That(failure!.ProtectTurns).IsEqualTo(1);
        await Assert.That(failure.UsedProtect).IsTrue();
    }

    /// <summary>
    /// Bulbapedia: "Each time, the chance of success is divided by 3."
    /// After one consecutive protection use the chance is 1/3, so a roll above 1/3 makes the move fail.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_SecondConsecutiveUse_RollAboveOneThird_Fails()
    {
        // Arrange
        var (script, move, target, hitData, volatileSet) = CreateProtectSetup(false, 0.5f);
        volatileSet.Add(new ProtectionFailureScript { ProtectTurns = 1 });

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

        // Assert
        hitData.Received(1).Fail();
        await Assert.That(volatileSet.Get<BanefulBunkerEffect>()).IsNull();
    }

    /// <summary>
    /// Bulbapedia: "Each time, the chance of success is divided by 3."
    /// After one consecutive protection use the chance is 1/3, so a roll below 1/3 still succeeds and
    /// increments the consecutive use counter.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_SecondConsecutiveUse_RollBelowOneThird_Succeeds()
    {
        // Arrange
        var (script, move, target, hitData, volatileSet) = CreateProtectSetup(false, 0.2f);
        var failure = new ProtectionFailureScript { ProtectTurns = 1 };
        volatileSet.Add(failure);

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

        // Assert
        hitData.DidNotReceive().Fail();
        await Assert.That(volatileSet.Get<BanefulBunkerEffect>()).IsNotNull();
        await Assert.That(failure.ProtectTurns).IsEqualTo(2);
    }

    /// <summary>
    /// Bulbapedia: "Baneful Bunker protects the user from all effects of moves that target it during the turn
    /// it is used, including damage."
    /// A move that can be protected against (it has the protect flag) is blocked by the
    /// <see cref="BanefulBunkerEffect"/>.
    /// </summary>
    [Test]
    public async Task BlockIncomingHit_MoveWithProtectFlag_BlocksHit()
    {
        // Arrange
        var (effect, move, target, _) = CreateBlockSetup(false, true);
        var block = false;

        // Act
        effect.BlockIncomingHit(move, target, 0, ref block);

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

    /// <summary>
    /// Bulbapedia: "Baneful Bunker protects the user from all effects of moves that target it"; moves that
    /// bypass protection (they lack the protect flag) are not blocked by the <see cref="BanefulBunkerEffect"/>.
    /// </summary>
    [Test]
    public async Task BlockIncomingHit_MoveWithoutProtectFlag_DoesNotBlock()
    {
        // Arrange
        var (effect, move, target, _) = CreateBlockSetup(false, false);
        var block = false;

        // Act
        effect.BlockIncomingHit(move, target, 0, ref block);

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

    /// <summary>
    /// Bulbapedia: "If Baneful Bunker blocks an attack that would make contact with the user, the attacker
    /// becomes poisoned (unless they are immune)."
    /// </summary>
    [Test]
    public async Task BlockIncomingHit_BlockedContactMove_PoisonsAttacker()
    {
        // Arrange
        var (effect, move, target, attacker) = CreateBlockSetup(true, true);
        var block = false;

        // Act
        effect.BlockIncomingHit(move, target, 0, ref block);

        // Assert
        await Assert.That(GetStatusSet(attacker)).IsEqualTo("poisoned");
    }

    /// <summary>
    /// Bulbapedia: "If Baneful Bunker blocks an attack that would make contact with the user, the attacker
    /// becomes poisoned (unless they are immune)."
    /// A blocked attack that does not make contact must not poison the attacker.
    /// </summary>
    [Test]
    public async Task BlockIncomingHit_BlockedNonContactMove_DoesNotPoisonAttacker()
    {
        // Arrange
        var (effect, move, target, attacker) = CreateBlockSetup(false, true);
        var block = false;

        // Act
        effect.BlockIncomingHit(move, target, 0, ref block);

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

    /// <summary>
    /// Bulbapedia: "If Baneful Bunker blocks an attack that would make contact with the user, the attacker
    /// becomes poisoned (unless they are immune)."
    /// A contact move that bypasses the protection (it lacks the protect flag, e.g. Shadow Force) is not
    /// blocked, so the attacker must not be poisoned.
    /// </summary>
    [Test]
    public async Task BlockIncomingHit_ContactMoveThatBypassesProtection_DoesNotPoisonAttacker()
    {
        // Arrange
        var (effect, move, target, attacker) = CreateBlockSetup(true, false);
        var block = false;

        // Act
        effect.BlockIncomingHit(move, target, 0, ref block);

        // Assert - the hit was not blocked, so no poison is applied
        await Assert.That(block).IsFalse();
        await Assert.That(attacker.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "SetStatus")).IsFalse();
    }
}