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

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

/// <summary>
/// Tests for the Aftermath ability.
/// </summary>
public class AftermathTests
{
    /// <summary>
    /// Creates a fully mocked test setup for Aftermath tests.
    /// </summary>
    private static (Aftermath aftermath, IExecutingMove move, IPokemon target, IPokemon user, EventHook eventHook,
        IBattle battle) CreateFullTestSetup(bool isContact, uint userMaxHealth = 100)
    {
        var aftermath = new Aftermath();
        var move = Substitute.For<IExecutingMove>();
        var target = Substitute.For<IPokemon>();
        var hitData = Substitute.For<IHitData>();
        hitData.IsContact.Returns(isContact);
        move.GetHitData(target, 0).Returns(hitData);

        var eventHook = new EventHook();
        var battle = Substitute.For<IBattle>();
        battle.EventHook.Returns(eventHook);

        // Setup empty sides by default (no Damp on field)
        var side = Substitute.For<IBattleSide>();
        side.Pokemon.Returns(new List<IPokemon?>());
        battle.Sides.Returns(new[] { side });

        var battleData = Substitute.For<IPokemonBattleData>();
        battleData.Battle.Returns(battle);

        var user = Substitute.For<IPokemon>();
        user.IsUsable.Returns(true);
        user.MaxHealth.Returns(userMaxHealth);

        // Configure BattleData return value using NSubstitute's callback pattern
        // First return null to suppress auto-substitution, then configure actual value
        user.BattleData.Returns(battleData);

        move.User.Returns(user);

        return (aftermath, move, target, user, eventHook, battle);
    }

    /// <summary>
    /// Helper to extract the damage amount from a substitute's received Damage calls.
    /// </summary>
    private static uint GetDamageDealt(IPokemon user)
    {
        var damageCall = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
        return damageCall != null ? (uint)damageCall.GetArguments()[0]! : 0;
    }

    /// <summary>
    /// Helper to extract the damage source from a substitute's received Damage calls.
    /// </summary>
    private static DamageSource? GetDamageSource(IPokemon user)
    {
        var damageCall = user.ReceivedCalls().FirstOrDefault(c => c.GetMethodInfo().Name == "Damage");
        return damageCall != null ? (DamageSource)damageCall.GetArguments()[1]! : null;
    }

    /// <summary>
    /// Bulbapedia: "When a Pokémon with this Ability faints from damage caused by a move that makes contact,
    /// the attacking Pokémon takes damage equal to ¼ of its own maximum HP."
    /// This test verifies that contact moves trigger the ability and deal 1/4 max HP damage.
    /// </summary>
    [Test]
    public async Task OnFaint_ContactMove_DealsQuarterMaxHpDamage()
    {
        // Arrange
        var (aftermath, move, target, user, _, _) = CreateFullTestSetup(true, 100);

        // Act
        aftermath.OnIncomingHit(move, target, 0);
        aftermath.OnFaint(target, DamageSource.MoveDamage);

        // Assert - Should deal 25 damage (1/4 of 100)
        await Assert.That(GetDamageDealt(user)).IsEqualTo(25u);
    }

    /// <summary>
    /// Bulbapedia: "the attacking Pokémon takes damage equal to ¼ of its own maximum HP."
    /// Tests various max HP values to ensure proper integer division.
    /// </summary>
    [Test, Arguments(100u, 25u), Arguments(200u, 50u), Arguments(44u, 11u), Arguments(99u, 24u), Arguments(1u, 0u)]
    public async Task OnFaint_ContactMove_DamageCalculation(uint maxHealth, uint expectedDamage)
    {
        // Arrange
        var (aftermath, move, target, user, _, _) = CreateFullTestSetup(true, maxHealth);

        // Act
        aftermath.OnIncomingHit(move, target, 0);
        aftermath.OnFaint(target, DamageSource.MoveDamage);

        // Assert
        await Assert.That(GetDamageDealt(user)).IsEqualTo(expectedDamage);
    }

    /// <summary>
    /// Bulbapedia: Aftermath triggers "from damage caused by a move that makes contact".
    /// Non-contact moves should not trigger the ability.
    /// </summary>
    [Test]
    public async Task OnFaint_NonContactMove_DoesNotDealDamage()
    {
        // Arrange
        var (aftermath, move, target, user, _, _) = CreateFullTestSetup(false);

        // Act
        aftermath.OnIncomingHit(move, target, 0);
        aftermath.OnFaint(target, DamageSource.MoveDamage);

        // Assert - Damage should never be called
        await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage")).IsFalse();
    }

    /// <summary>
    /// Bulbapedia: Aftermath triggers when the Pokémon "faints from damage".
    /// If the faint is not from move damage, the ability should not trigger.
    /// </summary>
    [Test]
    public async Task OnFaint_NotFromMoveDamage_DoesNotTrigger()
    {
        // Arrange
        var (aftermath, move, target, user, _, _) = CreateFullTestSetup(true);

        aftermath.OnIncomingHit(move, target, 0);

        // Act - Faint from non-move damage source
        aftermath.OnFaint(target, DamageSource.Misc);

        // Assert - Damage should never be called
        await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage")).IsFalse();
    }

    /// <summary>
    /// Bulbapedia: "the attacking Pokémon takes damage".
    /// If the attacker has already fainted (IsUsable = false), no damage should be dealt.
    /// </summary>
    [Test]
    public async Task OnFaint_AttackerNotUsable_DoesNotDealDamage()
    {
        // Arrange
        var aftermath = new Aftermath();
        var move = Substitute.For<IExecutingMove>();
        var target = Substitute.For<IPokemon>();
        var hitData = Substitute.For<IHitData>();
        hitData.IsContact.Returns(true);
        move.GetHitData(target, 0).Returns(hitData);

        var user = Substitute.For<IPokemon>();
        user.IsUsable.Returns(false); // Attacker already fainted
        move.User.Returns(user);

        // Act
        aftermath.OnIncomingHit(move, target, 0);
        aftermath.OnFaint(target, DamageSource.MoveDamage);

        // Assert - Damage should never be called because IsUsable check fails
        await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage")).IsFalse();
    }

    /// <summary>
    /// Bulbapedia: "the attacking Pokémon takes damage".
    /// If the attacker has no battle data, no damage should be dealt.
    /// </summary>
    [Test]
    public async Task OnFaint_AttackerHasNoBattleData_DoesNotDealDamage()
    {
        // Arrange
        var aftermath = new Aftermath();
        var move = Substitute.For<IExecutingMove>();
        var target = Substitute.For<IPokemon>();
        var hitData = Substitute.For<IHitData>();
        hitData.IsContact.Returns(true);
        move.GetHitData(target, 0).Returns(hitData);

        var user = Substitute.For<IPokemon>();
        user.IsUsable.Returns(true);
        user.BattleData.Returns((IPokemonBattleData?)null);
        move.User.Returns(user);

        // Act
        aftermath.OnIncomingHit(move, target, 0);
        aftermath.OnFaint(target, DamageSource.MoveDamage);

        // Assert - Damage should never be called because BattleData is null
        await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage")).IsFalse();
    }

    /// <summary>
    /// Technical test: Verifies OnFaint handles the case where no attack was received.
    /// </summary>
    [Test]
    public void OnFaint_NoLastAttackStored_DoesNotThrow()
    {
        // Arrange
        var aftermath = new Aftermath();
        var target = Substitute.For<IPokemon>();

        // Act & Assert - Should not throw when _lastAttack is null
        aftermath.OnFaint(target, DamageSource.MoveDamage);
    }

    /// <summary>
    /// Bulbapedia: Aftermath triggers on "a move that makes contact".
    /// When multiple hits occur, only the last hit determines whether Aftermath triggers.
    /// This tests that a contact hit after a non-contact hit will trigger.
    /// </summary>
    [Test]
    public async Task OnIncomingHit_MultipleHits_LastContactHitTriggers()
    {
        // Arrange
        var aftermath = new Aftermath();
        var target = Substitute.For<IPokemon>();
        var battle = Substitute.For<IBattle>();
        var eventHook = new EventHook();
        battle.EventHook.Returns(eventHook);

        // Setup empty sides (no Damp on field)
        var side = Substitute.For<IBattleSide>();
        side.Pokemon.Returns(new List<IPokemon?>());
        battle.Sides.Returns(new[] { side });

        var battleData = Substitute.For<IPokemonBattleData>();
        battleData.Battle.Returns(battle);

        // First hit - non-contact
        var move1 = Substitute.For<IExecutingMove>();
        var hitData1 = Substitute.For<IHitData>();
        hitData1.IsContact.Returns(false);
        move1.GetHitData(target, 0).Returns(hitData1);

        // Second hit - contact (should be stored and trigger)
        var move2 = Substitute.For<IExecutingMove>();
        var hitData2 = Substitute.For<IHitData>();
        hitData2.IsContact.Returns(true);
        move2.GetHitData(target, 0).Returns(hitData2);
        var user = Substitute.For<IPokemon>();
        user.IsUsable.Returns(true);
        user.BattleData.Returns(battleData);
        user.MaxHealth.Returns(100u);
        move2.User.Returns(user);

        // Act
        aftermath.OnIncomingHit(move1, target, 0);
        aftermath.OnIncomingHit(move2, target, 0);
        aftermath.OnFaint(target, DamageSource.MoveDamage);

        // Assert - Should deal 25 damage from the last contact hit
        await Assert.That(GetDamageDealt(user)).IsEqualTo(25u);
    }

    /// <summary>
    /// Bulbapedia: Aftermath triggers on "a move that makes contact".
    /// When the last hit is non-contact, Aftermath should not trigger.
    /// </summary>
    [Test]
    public async Task OnIncomingHit_MultipleHits_LastNonContactDoesNotTrigger()
    {
        // Arrange
        var aftermath = new Aftermath();
        var target = Substitute.For<IPokemon>();

        // First hit - contact
        var move1 = Substitute.For<IExecutingMove>();
        var hitData1 = Substitute.For<IHitData>();
        hitData1.IsContact.Returns(true);
        move1.GetHitData(target, 0).Returns(hitData1);

        // Second hit - non-contact (overwrites, sets to null)
        var move2 = Substitute.For<IExecutingMove>();
        var hitData2 = Substitute.For<IHitData>();
        hitData2.IsContact.Returns(false);
        move2.GetHitData(target, 0).Returns(hitData2);

        // Act
        aftermath.OnIncomingHit(move1, target, 0);
        aftermath.OnIncomingHit(move2, target, 0);
        aftermath.OnFaint(target, DamageSource.MoveDamage);

        // Assert - Neither move's User should be accessed (_lastAttack is null)
        await Assert.That(move1.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "get_User")).IsFalse();
        await Assert.That(move2.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "get_User")).IsFalse();
    }

    /// <summary>
    /// Bulbapedia: "When a Pokémon with this Ability faints..."
    /// Verifies that the AbilityTriggerEvent is fired when Aftermath triggers.
    /// </summary>
    [Test]
    public async Task OnFaint_ContactMove_FiresAbilityTriggerEvent()
    {
        // Arrange
        var (aftermath, move, target, user, eventHook, _) = CreateFullTestSetup(true);
        AbilityTriggerEvent? capturedEvent = null;
        eventHook.Handler += (sender, args) =>
        {
            if (args is AbilityTriggerEvent ate)
                capturedEvent = ate;
        };

        // Act
        aftermath.OnIncomingHit(move, target, 0);
        aftermath.OnFaint(target, DamageSource.MoveDamage);

        // Assert - AbilityTriggerEvent should be fired with the target (Aftermath owner)
        await Assert.That(capturedEvent).IsNotNull();
        await Assert.That(capturedEvent!.Pokemon).IsEqualTo(target);
    }

    /// <summary>
    /// Bulbapedia: "the attacking Pokémon takes damage equal to ¼ of its own maximum HP."
    /// Verifies that the damage source is DamageSource.Misc (not MoveDamage).
    /// </summary>
    [Test]
    public async Task OnFaint_ContactMove_UsesMiscDamageSource()
    {
        // Arrange
        var (aftermath, move, target, user, _, _) = CreateFullTestSetup(true);

        // Act
        aftermath.OnIncomingHit(move, target, 0);
        aftermath.OnFaint(target, DamageSource.MoveDamage);

        // Assert
        await Assert.That(GetDamageSource(user)!.Value).IsEqualTo(DamageSource.Misc);
    }

    /// <summary>
    /// Bulbapedia: Aftermath does not activate "if a Pokémon with Damp is on the field".
    /// This test verifies that Aftermath is suppressed when any Pokémon on the field has Damp.
    /// </summary>
    [Test]
    public async Task OnFaint_DampOnField_DoesNotTrigger()
    {
        // Arrange
        var (aftermath, move, target, user, _, battle) = CreateFullTestSetup(true, 100);

        // Create a Pokemon with Damp ability on the field
        var dampPokemon = Substitute.For<IPokemon>();
        var dampAbility = Substitute.For<IAbility>();
        dampAbility.Name.Returns(new StringKey("damp"));
        dampPokemon.ActiveAbility.Returns(dampAbility);

        // Update battle sides to include the Damp Pokemon
        var side = Substitute.For<IBattleSide>();
        side.Pokemon.Returns(new List<IPokemon?> { dampPokemon });
        battle.Sides.Returns(new[] { side });

        // Act
        aftermath.OnIncomingHit(move, target, 0);
        aftermath.OnFaint(target, DamageSource.MoveDamage);

        // Assert - Damage should never be called because Damp prevents Aftermath
        await Assert.That(user.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Damage")).IsFalse();
    }
}