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.Side;

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

/// <summary>
/// Tests for the <see cref="DoublePowerIfTargetDamagedInTurn"/> script, which implements Assurance (and
/// the other moves whose power doubles when the target was already damaged this turn, such as Avalanche
/// and Revenge). Behavior is verified against the Bulbapedia page for Assurance:
/// "Assurance inflicts damage and will deal double damage if the target has already received damage on
/// the turn it is used."
/// </summary>
public class DoublePowerIfTargetDamagedInTurnTests
{
    /// <summary>
    /// Creates a fully mocked test setup where the target sits on side 0 whose volatile scripts are a
    /// real <see cref="ScriptSet"/>.
    /// </summary>
    private static (DoublePowerIfTargetDamagedInTurn script, IExecutingMove move, IPokemon target, ScriptSet sideScripts
        ) CreateTestSetup()
    {
        var script = new DoublePowerIfTargetDamagedInTurn();
        var move = Substitute.For<IExecutingMove>();

        var side = Substitute.For<IBattleSide>();
        var sideScripts = new ScriptSet(side);
        side.VolatileScripts.Returns(sideScripts);
        side.GetScripts().Returns(_ => new ScriptIterator(new List<IEnumerable<ScriptContainer>>()));
        var battle = Substitute.For<IBattle>();
        battle.Sides.Returns(new[] { side });

        var user = Substitute.For<IPokemon>();
        var userBattleData = Substitute.For<IPokemonBattleData>();
        userBattleData.Battle.Returns(battle);
        user.BattleData.Returns(userBattleData);
        move.User.Returns(user);

        var target = Substitute.For<IPokemon>();
        var targetBattleData = Substitute.For<IPokemonBattleData>();
        targetBattleData.Battle.Returns(battle);
        targetBattleData.SideIndex.Returns((byte)0);
        target.BattleData.Returns(targetBattleData);

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

    /// <summary>
    /// Bulbapedia: "will deal double damage if the target has already received damage on the turn it is
    /// used." Before the turn starts, a tracking script is placed on the targeted side to record which
    /// Pokémon take damage this turn.
    /// </summary>
    [Test]
    public async Task OnBeforeTurnStart_MoveChoice_AddsTrackingDataToTargetSide()
    {
        // Arrange
        var (script, move, _, sideScripts) = CreateTestSetup();
        var user = move.User;
        var choice = Substitute.For<IMoveChoice>();
        choice.User.Returns(user);
        choice.TargetSide.Returns((byte)0);

        // Act
        script.OnBeforeTurnStart(choice);

        // Assert
        await Assert.That(sideScripts.Contains(ScriptUtils.ResolveName<DoublePowerIfTargetDamagedInTurnData>()))
            .IsTrue();
    }

    /// <summary>
    /// Technical test: a non-move turn choice (e.g. a switch) does not place the tracking script.
    /// </summary>
    [Test]
    public async Task OnBeforeTurnStart_NonMoveChoice_DoesNotAddTrackingData()
    {
        // Arrange
        var (script, _, _, sideScripts) = CreateTestSetup();
        var choice = Substitute.For<ITurnChoice>();

        // Act
        script.OnBeforeTurnStart(choice);

        // Assert
        await Assert.That(sideScripts.Contains(ScriptUtils.ResolveName<DoublePowerIfTargetDamagedInTurnData>()))
            .IsFalse();
    }

    /// <summary>
    /// Bulbapedia: "will deal double damage if the target has already received damage on the turn it is
    /// used." When the tracking data recorded the target as hit this turn, the base power doubles.
    /// </summary>
    [Test]
    public async Task ChangeBasePower_TargetDamagedThisTurn_BasePowerDoubles()
    {
        // Arrange
        var (script, move, target, sideScripts) = CreateTestSetup();
        var data = new DoublePowerIfTargetDamagedInTurnData();
        sideScripts.Add(data);
        data.OnDamage(target, DamageSource.MoveDamage, 100, 80);
        ushort basePower = 60;

        // Act
        script.ChangeBasePower(move, target, 0, ref basePower);

        // Assert
        await Assert.That(basePower).IsEqualTo((ushort)120);
    }

    /// <summary>
    /// Bulbapedia: the power only doubles "if the target has already received damage on the turn it is
    /// used." An undamaged target takes regular base power.
    /// </summary>
    [Test]
    public async Task ChangeBasePower_TargetNotDamagedThisTurn_BasePowerUnchanged()
    {
        // Arrange
        var (script, move, target, sideScripts) = CreateTestSetup();
        sideScripts.Add(new DoublePowerIfTargetDamagedInTurnData());
        ushort basePower = 60;

        // Act
        script.ChangeBasePower(move, target, 0, ref basePower);

        // Assert
        await Assert.That(basePower).IsEqualTo((ushort)60);
    }

    /// <summary>
    /// Bulbapedia: only damage to the target itself counts. Damage recorded for a different Pokémon on
    /// the same side does not double the power against this target.
    /// </summary>
    [Test]
    public async Task ChangeBasePower_OtherPokemonDamagedThisTurn_BasePowerUnchanged()
    {
        // Arrange
        var (script, move, target, sideScripts) = CreateTestSetup();
        var data = new DoublePowerIfTargetDamagedInTurnData();
        sideScripts.Add(data);
        data.OnDamage(Substitute.For<IPokemon>(), DamageSource.MoveDamage, 100, 80);
        ushort basePower = 60;

        // Act
        script.ChangeBasePower(move, target, 0, ref basePower);

        // Assert
        await Assert.That(basePower).IsEqualTo((ushort)60);
    }

    /// <summary>
    /// Bulbapedia: "This includes self-inflicted and indirect damage, such as damage from a Life Orb,
    /// recoil, crash damage, or hurting itself in confusion." Any damage source recorded by the tracking
    /// data counts.
    /// </summary>
    [Test, Arguments(DamageSource.Misc), Arguments(DamageSource.Confusion)]
    public async Task ChangeBasePower_TargetTookIndirectDamage_BasePowerDoubles(DamageSource source)
    {
        // Arrange
        var (script, move, target, sideScripts) = CreateTestSetup();
        var data = new DoublePowerIfTargetDamagedInTurnData();
        sideScripts.Add(data);
        data.OnDamage(target, source, 100, 90);
        ushort basePower = 60;

        // Act
        script.ChangeBasePower(move, target, 0, ref basePower);

        // Assert
        await Assert.That(basePower).IsEqualTo((ushort)120);
    }

    /// <summary>
    /// The damage tracking only spans a single turn: at the end of the turn the tracking data removes
    /// itself, so damage from a previous turn cannot double the power.
    /// </summary>
    [Test]
    public async Task OnEndTurn_TurnEnds_TrackingDataRemovesItself()
    {
        // Arrange
        var (_, _, _, sideScripts) = CreateTestSetup();
        var data = new DoublePowerIfTargetDamagedInTurnData();
        sideScripts.Add(data);
        var battle = Substitute.For<IBattle>();

        // Act
        data.OnEndTurn(Substitute.For<IBattleSide>(), battle);

        // Assert
        await Assert.That(sideScripts.Contains(ScriptUtils.ResolveName<DoublePowerIfTargetDamagedInTurnData>()))
            .IsFalse();
    }

    /// <summary>
    /// Technical test: without battle data on the user (outside of battle) the base power is unchanged
    /// and nothing throws.
    /// </summary>
    [Test]
    public async Task ChangeBasePower_UserHasNoBattleData_BasePowerUnchanged()
    {
        // Arrange
        var script = new DoublePowerIfTargetDamagedInTurn();
        var move = Substitute.For<IExecutingMove>();
        var user = Substitute.For<IPokemon>();
        user.BattleData.Returns((IPokemonBattleData?)null);
        move.User.Returns(user);
        var target = Substitute.For<IPokemon>();
        ushort basePower = 60;

        // Act
        script.ChangeBasePower(move, target, 0, ref basePower);

        // Assert
        await Assert.That(basePower).IsEqualTo((ushort)60);
    }
}