using PkmnLib.Dynamic.Events;
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.Utils;

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

/// <summary>
/// Tests for the <see cref="BeakBlast"/> move script and its attached <see cref="BeakBlastEffect"/>.
/// Behavior is verified against the Bulbapedia page for Beak Blast (Generation VII).
/// </summary>
public class BeakBlastTests
{
    /// <summary>
    /// Creates a fully mocked setup for driving <see cref="BeakBlast.OnBeforeTurnStart"/>.
    /// </summary>
    private static (BeakBlast script, ITurnChoice choice, IPokemon user, IScriptSet volatileSet, EventHook eventHook)
        CreateChargeSetup(bool hasBattleData = true)
    {
        var script = new BeakBlast();
        var user = Substitute.For<IPokemon>();
        user.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
        IScriptSet volatileSet = new ScriptSet(user);
        user.Volatile.Returns(volatileSet);

        var eventHook = new EventHook();
        if (hasBattleData)
        {
            var battle = Substitute.For<IBattle>();
            battle.EventHook.Returns(eventHook);
            var battleData = Substitute.For<IPokemonBattleData>();
            battleData.Battle.Returns(battle);
            user.BattleData.Returns(battleData);
        }
        else
        {
            user.BattleData.Returns((IPokemonBattleData?)null);
        }

        var choice = Substitute.For<ITurnChoice>();
        choice.User.Returns(user);

        return (script, choice, user, volatileSet, eventHook);
    }

    /// <summary>
    /// Bulbapedia: "The user charges up at the beginning of the turn and executes Beak Blast at -3 priority."
    /// Before the turn starts, the charging <see cref="BeakBlastEffect"/> is attached to the user.
    /// </summary>
    [Test]
    public async Task OnBeforeTurnStart_UserInBattle_AddsChargeEffect()
    {
        // Arrange
        var (script, choice, _, volatileSet, _) = CreateChargeSetup();

        // Act
        script.OnBeforeTurnStart(choice);

        // Assert
        await Assert.That(volatileSet.Get<BeakBlastEffect>()).IsNotNull();
    }

    /// <summary>
    /// Bulbapedia: "Its charging message is displayed before any other moves."
    /// The charge fires a dialog event when the turn starts.
    /// </summary>
    [Test]
    public async Task OnBeforeTurnStart_UserInBattle_FiresChargingMessage()
    {
        // Arrange
        var (script, choice, user, _, eventHook) = CreateChargeSetup();
        DialogEvent? capturedEvent = null;
        eventHook.Handler += (_, args) =>
        {
            if (args is DialogEvent dialogEvent)
                capturedEvent = dialogEvent;
        };

        // Act
        script.OnBeforeTurnStart(choice);

        // Assert
        await Assert.That(capturedEvent).IsNotNull();
        await Assert.That(capturedEvent!.Message).IsEqualTo("beak_blast_charge");
        await Assert.That((IPokemon)capturedEvent.Parameters!["user"]).IsEqualTo(user);
    }

    /// <summary>
    /// Technical test: a Pokémon without <see cref="IPokemon.BattleData"/> is not in battle, so no charging
    /// phase starts.
    /// </summary>
    [Test]
    public async Task OnBeforeTurnStart_NoBattleData_DoesNotAddChargeEffect()
    {
        // Arrange
        var (script, choice, _, volatileSet, _) = CreateChargeSetup(false);

        // Act
        script.OnBeforeTurnStart(choice);

        // Assert
        await Assert.That(volatileSet.Get<BeakBlastEffect>()).IsNull();
    }

    /// <summary>
    /// Bulbapedia: "All who make contact with the user during the charging phase are burned."
    /// The charging phase ends when the move executes, so the <see cref="BeakBlastEffect"/> is removed again.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_MoveExecuted_RemovesChargeEffect()
    {
        // Arrange
        var script = new BeakBlast();
        var move = Substitute.For<IExecutingMove>();
        var target = Substitute.For<IPokemon>();
        var user = Substitute.For<IPokemon>();
        user.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
        IScriptSet volatileSet = new ScriptSet(user);
        user.Volatile.Returns(volatileSet);
        move.User.Returns(user);
        volatileSet.Add(new BeakBlastEffect());

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

        // Assert
        await Assert.That(volatileSet.Get<BeakBlastEffect>()).IsNull();
    }

    /// <summary>
    /// Creates a fully mocked setup for driving <see cref="BeakBlastEffect.OnIncomingHit"/>.
    /// </summary>
    private static (BeakBlastEffect effect, IExecutingMove move, IPokemon target, IPokemon attacker)
        CreateIncomingHitSetup(bool isContact)
    {
        var effect = new BeakBlastEffect();
        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 attacker = Substitute.For<IPokemon>();
        move.User.Returns(attacker);

        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: "All who make contact with the user during the charging phase are burned."
    /// </summary>
    [Test]
    public async Task OnIncomingHit_ContactMove_BurnsAttacker()
    {
        // Arrange
        var (effect, move, target, attacker) = CreateIncomingHitSetup(true);

        // Act
        effect.OnIncomingHit(move, target, 0);

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

    /// <summary>
    /// Bulbapedia: "All who make contact with the user during the charging phase are burned."
    /// A hit that does not make contact must not burn the attacker.
    /// </summary>
    [Test]
    public async Task OnIncomingHit_NonContactMove_DoesNotBurnAttacker()
    {
        // Arrange
        var (effect, move, target, attacker) = CreateIncomingHitSetup(false);

        // Act
        effect.OnIncomingHit(move, target, 0);

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