using PkmnLib.Dynamic.Libraries;
using PkmnLib.Dynamic.Models;
using PkmnLib.Dynamic.Models.Choices;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Dynamic.ScriptHandling.Registry;
using PkmnLib.Plugin.Gen7.Common;
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="Encore"/> move script and its <see cref="EncoreEffect"/>.
/// Gen VII Bulbapedia behavior: "Encore temporarily prevents the target from using any move except the
/// last used move." It fails if the target hasn't used a move yet or its last move was Struggle, and
/// from Generation V onward the duration is "exactly 3 turns".
/// </summary>
public class EncoreTests
{
    /// <summary>
    /// Creates a mocked move choice whose chosen move has the given name, optionally carrying the
    /// <see cref="MoveFlags.CantRepeat"/> flag.
    /// </summary>
    private static IMoveChoice CreateMoveChoice(string moveName, bool cantRepeat = false)
    {
        var moveData = Substitute.For<IMoveData>();
        moveData.Name.Returns(new StringKey(moveName));
        moveData.HasFlag(MoveFlags.CantRepeat).Returns(cantRepeat);
        var learnedMove = Substitute.For<ILearnedMove>();
        learnedMove.MoveData.Returns(moveData);
        var choice = Substitute.For<IMoveChoice>();
        choice.ChosenMove.Returns(learnedMove);
        return choice;
    }

    /// <summary>
    /// Creates a fully mocked test setup where the target's volatile scripts are a real
    /// <see cref="ScriptSet"/> and its last used move is the given one (or none).
    /// </summary>
    private static (Encore script, IExecutingMove move, IPokemon target, IScriptSet targetVolatile, IHitData hitData)
        CreateTestSetup(string? lastUsedMove, bool lastMoveIsReplacement = false, bool lastMoveCantRepeat = false)
    {
        var script = new Encore();
        var move = Substitute.For<IExecutingMove>();

        // Struggle is recognized through the misc library's replacement choice check, not by name.
        var miscLibrary = Substitute.For<IMiscLibrary>();
        var library = Substitute.For<IDynamicLibrary>();
        library.MiscLibrary.Returns(miscLibrary);
        var battle = Substitute.For<IBattle>();
        battle.Library.Returns(library);

        var target = Substitute.For<IPokemon>();
        target.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
        IScriptSet targetVolatile = new ScriptSet(target);
        target.Volatile.Returns(targetVolatile);
        var targetBattleData = Substitute.For<IPokemonBattleData>();
        // Create the choice before the Returns call; configuring substitutes inside Returns is not allowed.
        var lastMoveChoice = lastUsedMove == null ? null : CreateMoveChoice(lastUsedMove, lastMoveCantRepeat);
        targetBattleData.LastMoveChoice.Returns(lastMoveChoice);
        targetBattleData.Battle.Returns(battle);
        target.BattleData.Returns(targetBattleData);
        if (lastMoveChoice != null)
            miscLibrary.IsReplacementChoice(lastMoveChoice).Returns(lastMoveIsReplacement);

        var hitData = Substitute.For<IHitData>();
        move.GetHitData(target, 0).Returns(hitData);

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

    /// <summary>
    /// Bulbapedia: "Encore temporarily prevents the target from using any move except the last used
    /// move." The target gains the <see cref="EncoreEffect"/> volatile script.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_TargetUsedMove_AddsEncoreEffectToTarget()
    {
        // Arrange
        var (script, move, target, targetVolatile, hitData) = CreateTestSetup("tackle");

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

        // Assert
        await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName<EncoreEffect>())).IsTrue();
        hitData.DidNotReceive().Fail();
    }

    /// <summary>
    /// Bulbapedia: "It also fails if the foe hasn't used a move yet." Without a last move choice on the
    /// target, the hit fails and no encore is applied.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_TargetHasNotUsedMove_FailsHit()
    {
        // Arrange
        var (script, move, target, targetVolatile, hitData) = CreateTestSetup(null);

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

        // Assert
        hitData.Received(1).Fail();
        await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName<EncoreEffect>())).IsFalse();
    }

    /// <summary>
    /// Bulbapedia: Encore fails if the target's last move was Struggle. A last move that is a replacement
    /// choice (per <see cref="IMiscLibrary.IsReplacementChoice"/>) cannot be encored.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_TargetLastUsedStruggle_FailsHit()
    {
        // Arrange
        var (script, move, target, targetVolatile, hitData) = CreateTestSetup("struggle", true);

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

        // Assert
        hitData.Received(1).Fail();
        await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName<EncoreEffect>())).IsFalse();
    }

    /// <summary>
    /// Bulbapedia: Encore fails "if the opponent's last move was Transform, Mimic, Sketch, Mirror Move,
    /// Encore itself, Struggle, or a move with no PP remaining." These moves carry the
    /// <see cref="MoveFlags.CantRepeat"/> flag in the move data, which makes Encore fail.
    /// </summary>
    [Test, Arguments("encore"), Arguments("transform"), Arguments("mimic"), Arguments("sketch"),
     Arguments("mirror_move")]
    public async Task OnSecondaryEffect_TargetLastUsedUnrepeatableMove_FailsHit(string lastUsedMove)
    {
        // Arrange
        var (script, move, target, targetVolatile, hitData) = CreateTestSetup(lastUsedMove, lastMoveCantRepeat: true);

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

        // Assert
        hitData.Received(1).Fail();
        await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName<EncoreEffect>())).IsFalse();
    }

    /// <summary>
    /// Bulbapedia: Encore fails "if the opponent's last move was Transform, Mimic, Sketch, Mirror Move,
    /// Encore itself" — integration check: the real Gen7 move data marks exactly these moves with the
    /// <see cref="MoveFlags.CantRepeat"/> flag.
    /// </summary>
    [Test, Arguments("encore"), Arguments("transform"), Arguments("mimic"), Arguments("sketch"),
     Arguments("mirror_move")]
    public async Task MoveData_UnrepeatableMove_HasCantRepeatFlag(string moveName)
    {
        // Arrange
        var library = LibraryHelpers.LoadLibrary();

        // Act
        var found = library.StaticLibrary.Moves.TryGet(new StringKey(moveName), out var moveData);

        // Assert
        await Assert.That(found).IsTrue();
        await Assert.That(moveData!.HasFlag(MoveFlags.CantRepeat)).IsTrue();
    }

    /// <summary>
    /// Technical test: without battle data on the target (outside of battle) the effect does nothing and
    /// does not throw.
    /// </summary>
    [Test]
    public async Task OnSecondaryEffect_TargetHasNoBattleData_DoesNothing()
    {
        // Arrange
        var (script, move, target, targetVolatile, hitData) = CreateTestSetup("tackle");
        target.BattleData.Returns((IPokemonBattleData?)null);

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

        // Assert
        hitData.DidNotReceive().Fail();
        await Assert.That(targetVolatile.Contains(ScriptUtils.ResolveName<EncoreEffect>())).IsFalse();
    }

    /// <summary>
    /// Bulbapedia: "Duration standardized to exactly 3 turns" (Generation V onward). After two
    /// end-of-turn ticks the effect is still active.
    /// </summary>
    [Test]
    public async Task OnEndTurn_TwoTurnsPassed_EffectStillActive()
    {
        // Arrange
        var owner = Substitute.For<IPokemon>();
        owner.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
        IScriptSet ownerVolatile = new ScriptSet(owner);
        var effect = new EncoreEffect(owner, new StringKey("tackle"), 3);
        ownerVolatile.Add(effect);

        // Act
        for (var i = 0; i < 2; i++)
            effect.OnEndTurn(owner, Substitute.For<IBattle>());

        // Assert
        await Assert.That(ownerVolatile.Contains(ScriptUtils.ResolveName<EncoreEffect>())).IsTrue();
    }

    /// <summary>
    /// Bulbapedia: "Duration standardized to exactly 3 turns" (Generation V onward). After the third
    /// end-of-turn tick the effect removes itself.
    /// </summary>
    [Test]
    public async Task OnEndTurn_ThreeTurnsPassed_EffectRemovesItself()
    {
        // Arrange
        var owner = Substitute.For<IPokemon>();
        owner.GetScripts().Returns(_ => new ScriptIterator(Array.Empty<IEnumerable<ScriptContainer>>()));
        IScriptSet ownerVolatile = new ScriptSet(owner);
        var effect = new EncoreEffect(owner, new StringKey("tackle"), 3);
        ownerVolatile.Add(effect);

        // Act
        for (var i = 0; i < 3; i++)
            effect.OnEndTurn(owner, Substitute.For<IBattle>());

        // Assert
        await Assert.That(ownerVolatile.Contains(ScriptUtils.ResolveName<EncoreEffect>())).IsFalse();
    }
}