using Theriapolis.Core.Data;
using Theriapolis.Core.Items;
using Theriapolis.Core.Rules.Character;
using Theriapolis.Core.Rules.Combat;
using Theriapolis.Core.Rules.Stats;
using Theriapolis.Core.Util;
using Xunit;

namespace Theriapolis.Tests.Combat;

public sealed class FeatureProcessorTests
{
    private readonly ContentResolver _content = new(new ContentLoader(TestHelpers.DataDirectory));

    // ── Unarmored Defense (Feral) ──────────────────────────────────────

    [Fact]
    public void FeralUnarmoredDefense_RaisesAcWithoutBodyArmor()
    {
        var c = MakeChar("feral", "ursidae", "brown_bear", new AbilityScores(15, 14, 14, 10, 10, 8));
        // Brown bear doesn't auto-equip body armor in feral kit (hide_vest is the only one) —
        // but the kit DOES equip hide_vest. Strip it and re-check.
        var body = c.Inventory.GetEquipped(EquipSlot.Body);
        if (body is not null) c.Inventory.TryUnequip(EquipSlot.Body, out _);
        int unarmoredAc = DerivedStats.ArmorClass(c);
        // Feral CON 14 → +2; DEX 13 (after wolf-folk -1, brown_bear baseline) → varies. Just assert the floor: 10 + DEX + CON ≥ 12.
        Assert.True(unarmoredAc >= 11, $"Feral unarmored AC should be at least 11, got {unarmoredAc}");
    }

    // ── Sentinel Stance (Bulwark) ──────────────────────────────────────

    [Fact]
    public void SentinelStance_AddsTwoToAcDuringAttackResolution()
    {
        var enc = MakeMiniEncounter(out var attacker, out var target,
            attackerClass: "fangsworn", targetClass: "bulwark");
        int beforeAc = target.ArmorClass;
        target.SentinelStanceActive = true;
        int withStance = target.ArmorClass + FeatureProcessor.ApplyAcBonus(target);
        Assert.Equal(beforeAc + 2, withStance);
    }

    [Fact]
    public void ToggleSentinelStance_FlipsFlagAndLogs()
    {
        var enc = MakeMiniEncounter(out _, out var bulwark, attackerClass: "feral", targetClass: "bulwark");
        Assert.False(bulwark.SentinelStanceActive);
        bool ok = FeatureProcessor.ToggleSentinelStance(enc, bulwark);
        Assert.True(ok);
        Assert.True(bulwark.SentinelStanceActive);
        FeatureProcessor.ToggleSentinelStance(enc, bulwark);
        Assert.False(bulwark.SentinelStanceActive);
    }

    // ── Feral Rage ─────────────────────────────────────────────────────

    [Fact]
    public void TryActivateRage_ConsumesUseAndSetsFlag()
    {
        var enc = MakeMiniEncounter(out var feral, out _, attackerClass: "feral", targetClass: "fangsworn");
        var c = feral.SourceCharacter!;
        int usesBefore = c.RageUsesRemaining;
        bool ok = FeatureProcessor.TryActivateRage(enc, feral);
        Assert.True(ok);
        Assert.True(feral.RageActive);
        Assert.Equal(usesBefore - 1, c.RageUsesRemaining);
    }

    [Fact]
    public void TryActivateRage_NoUsesLeft_ReturnsFalse()
    {
        var enc = MakeMiniEncounter(out var feral, out _, attackerClass: "feral", targetClass: "fangsworn");
        feral.SourceCharacter!.RageUsesRemaining = 0;
        Assert.False(FeatureProcessor.TryActivateRage(enc, feral));
    }

    [Fact]
    public void Rage_ResistsBludgeoningPiercingSlashing()
    {
        var enc = MakeMiniEncounter(out var feral, out _, attackerClass: "feral", targetClass: "fangsworn");
        feral.RageActive = true;
        Assert.True(FeatureProcessor.IsResisted(feral, DamageType.Bludgeoning));
        Assert.True(FeatureProcessor.IsResisted(feral, DamageType.Piercing));
        Assert.True(FeatureProcessor.IsResisted(feral, DamageType.Slashing));
        Assert.False(FeatureProcessor.IsResisted(feral, DamageType.Fire));
    }

    [Fact]
    public void Rage_AddsDamageBonusOnMeleeOnly()
    {
        var enc = MakeMiniEncounter(out var feral, out var target, attackerClass: "feral", targetClass: "fangsworn");
        feral.RageActive = true;
        var melee = new AttackOption { Name = "Test melee", Damage = new DamageRoll(0, 0, 0, DamageType.Slashing) };
        int bonusMelee = FeatureProcessor.ApplyDamageBonus(enc, feral, target, melee, isCrit: false);
        Assert.Equal(2, bonusMelee);

        var ranged = new AttackOption { Name = "Test ranged", Damage = new DamageRoll(0, 0, 0, DamageType.Piercing),
                                        RangeShortTiles = 8, RangeLongTiles = 16 };
        int bonusRanged = FeatureProcessor.ApplyDamageBonus(enc, feral, target, ranged, isCrit: false);
        Assert.Equal(0, bonusRanged);
    }

    // ── Sneak Attack (Shadow-Pelt) ────────────────────────────────────

    [Fact]
    public void SneakAttack_FiresOncePerTurnWithFinesseWeapon()
    {
        // Shadow-Pelt's starting kit is thorn_blade (finesse) + studded_leather.
        var enc = MakeMiniEncounter(out var rogue, out var target, attackerClass: "shadow_pelt", targetClass: "fangsworn");
        var attack = rogue.AttackOptions[0];
        Assert.False(rogue.SneakAttackUsedThisTurn);
        int firstHit  = FeatureProcessor.ApplyDamageBonus(enc, rogue, target, attack, isCrit: false);
        // After first hit, the flag should be set; second call returns no sneak bonus.
        Assert.True(rogue.SneakAttackUsedThisTurn);
        int secondHit = FeatureProcessor.ApplyDamageBonus(enc, rogue, target, attack, isCrit: false);
        Assert.True(firstHit > 0,  "first finesse hit should add Sneak Attack damage");
        // Second hit may still have other bonuses from non-rogue features but not Sneak Attack.
        // Assert second is strictly less than first (Sneak removed).
        Assert.True(secondHit < firstHit, $"second hit ({secondHit}) should be less than first ({firstHit}) — sneak attack consumed");
    }

    [Fact]
    public void OnTurnStart_ResetsSneakAttackFlag()
    {
        var enc = MakeMiniEncounter(out var rogue, out _, attackerClass: "shadow_pelt", targetClass: "fangsworn");
        rogue.SneakAttackUsedThisTurn = true;
        rogue.OnTurnStart();
        Assert.False(rogue.SneakAttackUsedThisTurn);
    }

    // ── Fangsworn Duelist ─────────────────────────────────────────────

    [Fact]
    public void Duelist_AddsTwoDamage_OneHandedWeapon()
    {
        var enc = MakeMiniEncounter(out var fang, out var target, attackerClass: "fangsworn", targetClass: "fangsworn");
        // Fangsworn starting kit: rend_sword + buckler (shield in offhand). Per Duelist spec
        // shield in off-hand is OK; only "no other weapon" matters.
        Assert.Equal("duelist", fang.SourceCharacter!.FightingStyle);
        var attack = fang.AttackOptions[0];
        int bonus = FeatureProcessor.ApplyDamageBonus(enc, fang, target, attack, isCrit: false);
        Assert.True(bonus >= 2, $"Duelist should add at least 2 damage; got {bonus}");
    }

    // ── Helpers ──────────────────────────────────────────────────────

    private Theriapolis.Core.Rules.Character.Character MakeChar(string classId, string cladeId, string speciesId, AbilityScores a)
    {
        var b = new CharacterBuilder
        {
            Clade = _content.Clades[cladeId],
            Species = _content.Species[speciesId],
            ClassDef = _content.Classes[classId],
            Background = _content.Backgrounds["pack_raised"],
            BaseAbilities = a,
        };
        // Pick the right number of skills.
        int n = b.ClassDef.SkillsChoose;
        foreach (var raw in b.ClassDef.SkillOptions)
        {
            if (b.ChosenClassSkills.Count >= n) break;
            try { b.ChosenClassSkills.Add(SkillIdExtensions.FromJson(raw)); } catch { }
        }
        return b.Build(_content.Items);
    }

    private Encounter MakeMiniEncounter(
        out Combatant attacker, out Combatant target,
        string attackerClass = "fangsworn", string targetClass = "fangsworn")
    {
        var atkChar = MakeChar(attackerClass, "canidae", "wolf", new AbilityScores(15, 14, 13, 12, 10, 8));
        var defChar = MakeChar(targetClass,    "canidae", "wolf", new AbilityScores(15, 14, 13, 12, 10, 8));
        attacker = Combatant.FromCharacter(atkChar, 1, "Attacker", new Vec2(0, 0),
            Theriapolis.Core.Rules.Character.Allegiance.Player);
        target = Combatant.FromCharacter(defChar, 2, "Target", new Vec2(1, 0),
            Theriapolis.Core.Rules.Character.Allegiance.Hostile);
        return new Encounter(0xABCDUL, 1, new[] { attacker, target });
    }
}
