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

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

/// <summary>
/// Tests for the <see cref="Assist"/> move script.
/// Gen VII Bulbapedia behavior: "The Assist user executes a move that is randomly selected from all the
/// eligible moves known by other Pokémon in the same party. If the same move is known by multiple Pokémon,
/// each of them counts [...]. Assist can call a move that currently has no PP or is known by a fainted
/// Pokémon. [...] If the other Pokémon in the user's party do not know any eligible moves, Assist fails."
/// </summary>
public class AssistTests
{
    private static IPokemon CreatePokemonWithMoves(params string[] moveNames)
    {
        var pokemon = Substitute.For<IPokemon>();
        var moves = moveNames.Select(name =>
        {
            var learned = Substitute.For<ILearnedMove>();
            var data = Substitute.For<IMoveData>();
            data.Name.Returns(new StringKey(name));
            learned.MoveData.Returns(data);
            return (ILearnedMove?)learned;
        }).ToArray();
        pokemon.Moves.Returns(moves);
        return pokemon;
    }

    private static (Assist script, IMoveChoice choice, IPokemon user, IBattleRandom random) CreateTestSetup(
        params IPokemon?[] otherPartyMembers)
    {
        var script = new Assist();
        var user = CreatePokemonWithMoves("tackle");

        var members = new List<IPokemon?> { user };
        members.AddRange(otherPartyMembers);

        var party = Substitute.For<IPokemonParty>();
        party.GetEnumerator().Returns(_ => members.GetEnumerator());
        var battleParty = Substitute.For<IBattleParty>();
        battleParty.Party.Returns(party);

        var random = Substitute.For<IBattleRandom>();
        var battle = Substitute.For<IBattle>();
        battle.Parties.Returns(new[] { battleParty });
        battle.Random.Returns(random);
        var battleData = Substitute.For<IPokemonBattleData>();
        battleData.Battle.Returns(battle);
        user.BattleData.Returns(battleData);

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

        return (script, choice, user, random);
    }

    /// <summary>
    /// Bulbapedia: "The Assist user executes a move that is randomly selected from all the eligible moves known
    /// by other Pokémon in the same party."
    /// </summary>
    [Test]
    public async Task ChangeMove_AllyKnowsEligibleMove_SelectsThatMove()
    {
        // Arrange
        var (script, choice, _, random) = CreateTestSetup(CreatePokemonWithMoves("growl"));
        random.GetInt(Arg.Any<int>()).Returns(0);
        StringKey moveName = "assist";

        // Act
        script.ChangeMove(choice, ref moveName);

        // Assert
        await Assert.That(moveName).IsEqualTo(new StringKey("growl"));
        choice.DidNotReceive().Fail();
    }

    /// <summary>
    /// Bulbapedia: "If the other Pokémon in the user's party do not know any eligible moves, Assist fails."
    /// The user's own moves are not eligible: with no other party members, the move fails even though the
    /// user itself knows a copyable move.
    /// </summary>
    [Test]
    public async Task ChangeMove_NoOtherPartyMembers_Fails()
    {
        // Arrange
        var (script, choice, _, _) = CreateTestSetup();
        StringKey moveName = "assist";

        // Act
        script.ChangeMove(choice, ref moveName);

        // Assert
        choice.Received(1).Fail();
        await Assert.That(moveName).IsEqualTo(new StringKey("assist"));
    }

    /// <summary>
    /// Bulbapedia lists moves that Assist cannot call (e.g. Protect, Counter, moves with a semi-invulnerable
    /// turn). An ally that only knows ineligible moves provides no options, so Assist fails.
    /// </summary>
    [Test]
    public async Task ChangeMove_AllyOnlyKnowsIneligibleMoves_Fails()
    {
        // Arrange
        var (script, choice, _, _) = CreateTestSetup(CreatePokemonWithMoves("protect", "counter", "dig"));
        StringKey moveName = "assist";

        // Act
        script.ChangeMove(choice, ref moveName);

        // Assert
        choice.Received(1).Fail();
        await Assert.That(moveName).IsEqualTo(new StringKey("assist"));
    }

    /// <summary>
    /// Bulbapedia: "If the same move is known by multiple Pokémon, each of them counts and therefore the move
    /// is more likely to be called." Two allies knowing Tackle plus one knowing Growl yields three entries in
    /// the random selection pool.
    /// </summary>
    [Test]
    public void ChangeMove_SameMoveKnownByMultipleAllies_EachCounts()
    {
        // Arrange
        var (script, choice, _, random) = CreateTestSetup(CreatePokemonWithMoves("pound"),
            CreatePokemonWithMoves("pound"), CreatePokemonWithMoves("growl"));
        random.GetInt(Arg.Any<int>()).Returns(0);
        StringKey moveName = "assist";

        // Act
        script.ChangeMove(choice, ref moveName);

        // Assert - the selection pool contains three moves (pound twice, growl once)
        random.Received(1).GetInt(3);
        choice.DidNotReceive().Fail();
    }

    /// <summary>
    /// Bulbapedia: "Assist can call a move that [...] is known by a fainted Pokémon."
    /// </summary>
    [Test]
    public async Task ChangeMove_AllyIsFainted_MoveStillEligible()
    {
        // Arrange
        var faintedAlly = CreatePokemonWithMoves("growl");
        faintedAlly.IsUsable.Returns(false);
        var (script, choice, _, random) = CreateTestSetup(faintedAlly);
        random.GetInt(Arg.Any<int>()).Returns(0);
        StringKey moveName = "assist";

        // Act
        script.ChangeMove(choice, ref moveName);

        // Assert
        await Assert.That(moveName).IsEqualTo(new StringKey("growl"));
        choice.DidNotReceive().Fail();
    }

    /// <summary>
    /// Bulbapedia: "Assist can call a move that currently has no PP".
    /// </summary>
    [Test]
    public async Task ChangeMove_AllyMoveHasNoPp_MoveStillEligible()
    {
        // Arrange
        var ally = CreatePokemonWithMoves("growl");
        ally.Moves[0]!.CurrentPp.Returns((byte)0);
        var (script, choice, _, random) = CreateTestSetup(ally);
        random.GetInt(Arg.Any<int>()).Returns(0);
        StringKey moveName = "assist";

        // Act
        script.ChangeMove(choice, ref moveName);

        // Assert
        await Assert.That(moveName).IsEqualTo(new StringKey("growl"));
        choice.DidNotReceive().Fail();
    }

    /// <summary>
    /// Technical test: outside of battle (no battle data) the script returns without failing the choice.
    /// </summary>
    [Test]
    public async Task ChangeMove_NoBattleData_DoesNothing()
    {
        // Arrange
        var script = new Assist();
        var user = Substitute.For<IPokemon>();
        user.BattleData.Returns((IPokemonBattleData?)null);
        var choice = Substitute.For<IMoveChoice>();
        choice.User.Returns(user);
        StringKey moveName = "assist";

        // Act
        script.ChangeMove(choice, ref moveName);

        // Assert
        choice.DidNotReceive().Fail();
        await Assert.That(moveName).IsEqualTo(new StringKey("assist"));
    }
}