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

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

/// <summary>
/// Tests for the <see cref="Counter"/> move script and its <see cref="CounterHelperEffect"/>.
/// Gen VII Bulbapedia behavior: Counter retaliates against the last physical move that damaged the user
/// this turn, dealing twice the damage the user received to the Pokémon that dealt it. Since Generation II
/// "Counter now affects all physical moves, dealing double damage"; it fails when the user was not hit by
/// a physical move first.
/// </summary>
public class CounterTests
{
    /// <summary>
    /// Creates a user whose volatile scripts contain a <see cref="CounterHelperEffect"/> that has recorded
    /// an incoming physical hit of the given damage by <paramref name="attacker"/>.
    /// </summary>
    private static IPokemon CreateUserHitBy(IPokemon? attacker, uint damage, bool physical = true)
    {
        var user = Substitute.For<IPokemon>();
        var userVolatile = new ScriptSet(user);
        user.Volatile.Returns(userVolatile);
        user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));

        var helper = new CounterHelperEffect();
        userVolatile.Add(helper);
        if (attacker != null)
        {
            var incomingMove = Substitute.For<IExecutingMove>();
            var useMove = Substitute.For<IMoveData>();
            useMove.Category.Returns(physical ? MoveCategory.Physical : MoveCategory.Special);
            incomingMove.UseMove.Returns(useMove);
            incomingMove.User.Returns(attacker);
            var incomingHit = Substitute.For<IHitData>();
            incomingHit.Damage.Returns(damage);
            incomingMove.GetHitData(user, 0).Returns(incomingHit);
            helper.OnIncomingHit(incomingMove, user, 0);
        }

        return user;
    }

    /// <summary>
    /// Counter watches for incoming hits over the whole turn: at the start of the turn it attaches its
    /// <see cref="CounterHelperEffect"/> to the user.
    /// </summary>
    [Test]
    public async Task OnBeforeTurnStart_Always_AddsCounterHelperEffectToUser()
    {
        // Arrange
        var script = new Counter();
        var user = Substitute.For<IPokemon>();
        var userVolatile = new ScriptSet(user);
        user.Volatile.Returns(userVolatile);
        user.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
        var choice = Substitute.For<IMoveChoice>();
        choice.User.Returns(user);

        // Act
        script.OnBeforeTurnStart(choice);

        // Assert
        await Assert.That(userVolatile.Contains(ScriptUtils.ResolveName<CounterHelperEffect>())).IsTrue();
    }

    /// <summary>
    /// Bulbapedia: Counter strikes back at the Pokémon that last hit the user with a physical move — the
    /// move is redirected to that attacker.
    /// </summary>
    [Test]
    public async Task ChangeTargets_UserWasHitByPhysicalMove_TargetsTheAttacker()
    {
        // Arrange
        var script = new Counter();
        var attacker = Substitute.For<IPokemon>();
        var user = CreateUserHitBy(attacker, 40);
        var choice = Substitute.For<IMoveChoice>();
        choice.User.Returns(user);
        IReadOnlyList<IPokemon?> targets = new IPokemon?[] { Substitute.For<IPokemon>() };

        // Act
        script.ChangeTargets(choice, ref targets);

        // Assert
        await Assert.That(targets.Single()).IsEqualTo(attacker);
        choice.DidNotReceive().Fail();
    }

    /// <summary>
    /// Bulbapedia: Counter fails when the user has not been hit by a physical move this turn.
    /// </summary>
    [Test]
    public void ChangeTargets_UserWasNotHit_Fails()
    {
        // Arrange
        var script = new Counter();
        var user = CreateUserHitBy(null, 0);
        var choice = Substitute.For<IMoveChoice>();
        choice.User.Returns(user);
        IReadOnlyList<IPokemon?> targets = new IPokemon?[] { Substitute.For<IPokemon>() };

        // Act
        script.ChangeTargets(choice, ref targets);

        // Assert
        choice.Received(1).Fail();
    }

    /// <summary>
    /// Bulbapedia (Generation II onwards): "Counter now affects all physical moves, dealing double
    /// damage." The damage dealt is twice the damage of the recorded hit.
    /// </summary>
    [Test, Arguments(40u, 80u), Arguments(1u, 2u), Arguments(123u, 246u)]
    public async Task ChangeMoveDamage_UserWasHitByPhysicalMove_DealsDoubleTheDamageTaken(uint damageTaken,
        uint expectedDamage)
    {
        // Arrange
        var script = new Counter();
        var attacker = Substitute.For<IPokemon>();
        var user = CreateUserHitBy(attacker, damageTaken);
        var move = Substitute.For<IExecutingMove>();
        move.User.Returns(user);
        uint damage = 0;

        // Act
        script.ChangeMoveDamage(move, attacker, 0, ref damage);

        // Assert
        await Assert.That(damage).IsEqualTo(expectedDamage);
    }

    /// <summary>
    /// Bulbapedia: Counter retaliates against the Pokémon that hit the user — against any other target the
    /// hit fails.
    /// </summary>
    [Test]
    public void ChangeMoveDamage_TargetIsNotTheLastAttacker_FailsHit()
    {
        // Arrange
        var script = new Counter();
        var attacker = Substitute.For<IPokemon>();
        var user = CreateUserHitBy(attacker, 40);
        var someoneElse = Substitute.For<IPokemon>();
        var move = Substitute.For<IExecutingMove>();
        move.User.Returns(user);
        var hitData = Substitute.For<IHitData>();
        move.GetHitData(someoneElse, 0).Returns(hitData);
        uint damage = 0;

        // Act
        script.ChangeMoveDamage(move, someoneElse, 0, ref damage);

        // Assert
        hitData.Received(1).Fail();
    }

    /// <summary>
    /// Bulbapedia: only physical moves can be countered — the helper records physical hits.
    /// </summary>
    [Test]
    public async Task CounterHelperEffect_PhysicalHit_RecordsAttackerAndDamage()
    {
        // Arrange
        var attacker = Substitute.For<IPokemon>();
        var user = CreateUserHitBy(attacker, 40);

        // Assert
        var helper = ((IScriptSet)user.Volatile).Get<CounterHelperEffect>();
        await Assert.That(helper!.LastHitBy).IsEqualTo(attacker);
        await Assert.That(helper.LastDamage).IsEqualTo(40u);
    }

    /// <summary>
    /// Bulbapedia (Generation IV onwards): special moves cannot be countered — the helper ignores
    /// non-physical hits, so Counter fails after a special hit.
    /// </summary>
    [Test]
    public async Task CounterHelperEffect_SpecialHit_IsNotRecorded()
    {
        // Arrange
        var attacker = Substitute.For<IPokemon>();
        var user = CreateUserHitBy(attacker, 40, false);

        // Assert
        var helper = ((IScriptSet)user.Volatile).Get<CounterHelperEffect>();
        await Assert.That(helper!.LastHitBy).IsNull();
    }
}