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

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

/// <summary>
/// Tests for the <see cref="Entrainment"/> move script.
/// Gen VII Bulbapedia behavior: "Entrainment changes the target's Ability to match the user's." It fails
/// if both Pokémon already share the same Ability, if the target's Ability cannot be changed (e.g.
/// Truant, Multitype, Stance Change, Comatose, Disguise), or if the user's Ability cannot be copied
/// (e.g. Trace, Illusion, Imposter, Power Construct).
/// </summary>
public class EntrainmentTests
{
    /// <summary>
    /// Creates a mocked ability that reports the given flags as set.
    /// </summary>
    private static IAbility CreateAbility(params string[] flags)
    {
        var ability = Substitute.For<IAbility>();
        foreach (var flag in flags)
            ability.HasFlag(new StringKey(flag)).Returns(true);
        return ability;
    }

    private static (Entrainment script, IExecutingMove move, IPokemon target, IHitData hitData) CreateTestSetup(
        IAbility? userAbility, IAbility? targetAbility)
    {
        var script = new Entrainment();
        var move = Substitute.For<IExecutingMove>();
        var target = Substitute.For<IPokemon>();
        var hitData = Substitute.For<IHitData>();
        move.GetHitData(target, 0).Returns(hitData);

        var user = Substitute.For<IPokemon>();
        user.ActiveAbility.Returns(userAbility);
        move.User.Returns(user);
        target.ActiveAbility.Returns(targetAbility);

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

    /// <summary>
    /// Bulbapedia: "Entrainment changes the target's Ability to match the user's."
    /// </summary>
    [Test]
    public void OnSecondaryEffect_DifferentAbilities_ChangesTargetAbilityToUsers()
    {
        // Arrange
        var userAbility = CreateAbility();
        var (script, move, target, hitData) = CreateTestSetup(userAbility, CreateAbility());

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

        // Assert
        target.Received(1).ChangeAbility(userAbility);
        hitData.DidNotReceive().Fail();
    }

    /// <summary>
    /// Bulbapedia: "The move fails if both Pokémon already share the same Ability."
    /// </summary>
    [Test]
    public void OnSecondaryEffect_SameAbility_FailsHit()
    {
        // Arrange
        var sharedAbility = CreateAbility();
        var (script, move, target, hitData) = CreateTestSetup(sharedAbility, sharedAbility);

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

        // Assert
        hitData.Received(1).Fail();
        target.DidNotReceiveWithAnyArgs().ChangeAbility(default!);
    }

    /// <summary>
    /// Bulbapedia: "The move fails if the user has: Trace, Forecast, Flower Gift, Zen Mode, Illusion,
    /// Imposter, [...]" — abilities that cannot be copied prevent Entrainment from working.
    /// </summary>
    [Test]
    public void OnSecondaryEffect_UserAbilityCantBeCopied_FailsHit()
    {
        // Arrange
        var userAbility = CreateAbility("cant_be_copied");
        var (script, move, target, hitData) = CreateTestSetup(userAbility, CreateAbility());

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

        // Assert
        hitData.Received(1).Fail();
        target.DidNotReceiveWithAnyArgs().ChangeAbility(default!);
    }

    /// <summary>
    /// Bulbapedia: "The move cannot affect Pokémon with these Abilities: Truant, Multitype, Stance
    /// Change, [...]" — a target whose ability cannot be changed prevents Entrainment from working.
    /// </summary>
    [Test]
    public void OnSecondaryEffect_TargetAbilityCantBeChanged_FailsHit()
    {
        // Arrange
        var targetAbility = CreateAbility("cant_be_changed");
        var (script, move, target, hitData) = CreateTestSetup(CreateAbility(), targetAbility);

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

        // Assert
        hitData.Received(1).Fail();
        target.DidNotReceiveWithAnyArgs().ChangeAbility(default!);
    }

    /// <summary>
    /// Bulbapedia: "Entrainment changes the target's Ability to match the user's" — without a user
    /// ability there is nothing to copy, so the move fails.
    /// </summary>
    [Test]
    public void OnSecondaryEffect_UserHasNoAbility_FailsHit()
    {
        // Arrange
        var (script, move, target, hitData) = CreateTestSetup(null, CreateAbility());

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

        // Assert
        hitData.Received(1).Fail();
        target.DidNotReceiveWithAnyArgs().ChangeAbility(default!);
    }

    /// <summary>
    /// Technical test: a target without an active ability cannot have it validated for replacement, so
    /// the move fails rather than throwing.
    /// </summary>
    [Test]
    public void OnSecondaryEffect_TargetHasNoAbility_FailsHit()
    {
        // Arrange
        var (script, move, target, hitData) = CreateTestSetup(CreateAbility(), null);

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

        // Assert
        hitData.Received(1).Fail();
        target.DidNotReceiveWithAnyArgs().ChangeAbility(default!);
    }
}