using Theriapolis.Core;
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;

/// <summary>
/// Phase 7 M0 — wires four more L3 subclass features (Phase 6.5 carryover):
///   - Antler-Guard "Retaliatory Strike": melee hit on a Sentinel-Stance
///     antler-guard returns 1d8 + CON to the attacker.
///   - Stampede-Heart "Trampling Charge": +1d8 bludgeoning on the first
///     melee attack of each turn while raging.
///   - Ambush-Artist "Opening Strike": +2d6 on the first melee attack of
///     round 1 of an encounter.
///   - Body-Wright "Combat Medic": Field Repair rolls 2d8 + INT (vs the
///     base 1d8 + INT).
///
/// Combined with the four Phase-6.5 wirings (Lone Fang, Herd-Wall,
/// Pack-Forged, Blood Memory), this brings 8 of 16 L3 subclass features
/// to live runtime. The remaining 8 are scaffolded but unwired and land
/// in Phase 7 M1.
/// </summary>
public sealed class Phase7M0SubclassFeatureTests
{
    private readonly ContentResolver _content = new(new ContentLoader(TestHelpers.DataDirectory));

    // ── Antler-Guard Retaliatory Strike ──────────────────────────────────

    [Fact]
    public void AntlerGuard_RetaliatoryStrike_ReturnsDamageInSentinelStance()
    {
        var enc = MakeDuel(out var pc, out var hostile,
            pcClass: "bulwark", pcSubclass: "antler_guard");
        pc.SentinelStanceActive = true;
        int hostileHpBefore = hostile.CurrentHp;

        var attack = pc.AttackOptions[0];
        int dealt = FeatureProcessor.OnAntlerGuardHit(enc, hostile, pc, attack);

        Assert.True(dealt >= 1, "retaliatory strike must deal at least 1 damage");
        Assert.Equal(hostileHpBefore - dealt, hostile.CurrentHp);
    }

    [Fact]
    public void AntlerGuard_RetaliatoryStrike_DoesNotFireWithoutSentinelStance()
    {
        var enc = MakeDuel(out var pc, out var hostile,
            pcClass: "bulwark", pcSubclass: "antler_guard");
        // SentinelStanceActive is false by default.
        int hostileHpBefore = hostile.CurrentHp;

        int dealt = FeatureProcessor.OnAntlerGuardHit(enc, hostile, pc, pc.AttackOptions[0]);

        Assert.Equal(0, dealt);
        Assert.Equal(hostileHpBefore, hostile.CurrentHp);
    }

    [Fact]
    public void AntlerGuard_RetaliatoryStrike_DoesNotFireOnRangedHit()
    {
        var enc = MakeDuel(out var pc, out var hostile,
            pcClass: "bulwark", pcSubclass: "antler_guard");
        pc.SentinelStanceActive = true;
        int hostileHpBefore = hostile.CurrentHp;

        var rangedAttack = MakeRangedAttack();
        int dealt = FeatureProcessor.OnAntlerGuardHit(enc, hostile, pc, rangedAttack);

        Assert.Equal(0, dealt);
        Assert.Equal(hostileHpBefore, hostile.CurrentHp);
    }

    [Fact]
    public void AntlerGuard_RetaliatoryStrike_NoFire_IfNotAntlerGuard()
    {
        var enc = MakeDuel(out var pc, out var hostile,
            pcClass: "bulwark", pcSubclass: "herd_wall");
        pc.SentinelStanceActive = true;

        int dealt = FeatureProcessor.OnAntlerGuardHit(enc, hostile, pc, pc.AttackOptions[0]);
        Assert.Equal(0, dealt);
    }

    // ── Stampede-Heart Trampling Charge ──────────────────────────────────

    [Fact]
    public void StampedeHeart_TramplingCharge_AddsDamage_FirstMeleeWhileRaging()
    {
        var enc = MakeDuel(out var pc, out var hostile,
            pcClass: "feral", pcSubclass: "stampede_heart");
        pc.RageActive = true;

        int bonus = FeatureProcessor.ApplyDamageBonus(enc, pc, hostile, pc.AttackOptions[0], isCrit: false);
        // Bonus = +2 (Rage) + 1d8 (Trampling Charge). Min 2 + 1 = 3, max 2 + 8 = 10.
        Assert.True(bonus >= 3 && bonus <= 10, $"expected [3..10], got {bonus}");
        Assert.True(pc.TramplingChargeUsedThisTurn);
    }

    [Fact]
    public void StampedeHeart_TramplingCharge_OnlyFiresOncePerTurn()
    {
        var enc = MakeDuel(out var pc, out var hostile,
            pcClass: "feral", pcSubclass: "stampede_heart");
        pc.RageActive = true;
        var attack = pc.AttackOptions[0];

        int first  = FeatureProcessor.ApplyDamageBonus(enc, pc, hostile, attack, isCrit: false);
        int second = FeatureProcessor.ApplyDamageBonus(enc, pc, hostile, attack, isCrit: false);
        // First: rage (+2) + trampling (+1d8). Second: rage only (+2).
        Assert.True(first  > second);
        Assert.Equal(2, second);
    }

    [Fact]
    public void StampedeHeart_TramplingCharge_DoesNotFireWithoutRage()
    {
        var enc = MakeDuel(out var pc, out var hostile,
            pcClass: "feral", pcSubclass: "stampede_heart");
        // RageActive false by default.
        int bonus = FeatureProcessor.ApplyDamageBonus(enc, pc, hostile, pc.AttackOptions[0], isCrit: false);
        Assert.Equal(0, bonus);
    }

    // ── Ambush-Artist Opening Strike ─────────────────────────────────────

    [Fact]
    public void AmbushArtist_OpeningStrike_AddsDamageInRoundOne()
    {
        var enc = MakeDuel(out var pc, out var hostile,
            pcClass: "shadow_pelt", pcSubclass: "ambush_artist");
        Assert.Equal(1, enc.RoundNumber);

        int bonus = FeatureProcessor.ApplyDamageBonus(enc, pc, hostile, pc.AttackOptions[0], isCrit: false);
        // Sneak attack 1d6 + Opening Strike 2d6 = 3d6. Range [3..18].
        Assert.True(bonus >= 3 && bonus <= 18, $"expected [3..18], got {bonus}");
        Assert.True(pc.OpeningStrikeUsed);
    }

    [Fact]
    public void AmbushArtist_OpeningStrike_OnlyFiresOncePerEncounter()
    {
        var enc = MakeDuel(out var pc, out var hostile,
            pcClass: "shadow_pelt", pcSubclass: "ambush_artist");
        var attack = pc.AttackOptions[0];

        int first = FeatureProcessor.ApplyDamageBonus(enc, pc, hostile, attack, isCrit: false);
        // Reset sneak-attack flag for cross-turn re-fire test.
        pc.SneakAttackUsedThisTurn = false;
        int second = FeatureProcessor.ApplyDamageBonus(enc, pc, hostile, attack, isCrit: false);

        // First fires opening strike (3d6 = [3..18]); second only sneak attack (1d6 = [1..6]).
        Assert.True(first > second);
        Assert.True(second >= 1 && second <= 6, $"second attack should only be sneak attack [1..6], got {second}");
    }

    // ── Body-Wright Combat Medic ─────────────────────────────────────────

    [Fact]
    public void BodyWright_FieldRepair_RollsTwoD8()
    {
        var enc = MakeDuel(out var pc, out var hostile,
            pcClass: "claw_wright", pcSubclass: "body_wright");
        // Establish the per-encounter pool the way PlayScreen would.
        FeatureProcessor.EnsureFieldRepairReady(pc.SourceCharacter!);

        // Damage the PC so the heal has somewhere to go.
        pc.CurrentHp = 1;
        bool ok = FeatureProcessor.TryFieldRepair(enc, pc, pc);
        Assert.True(ok);
        // Body-Wright heals 2d8 + INT — minimum 2 + INT, max 16 + INT. Since
        // healing clamps to MaxHp we just assert the heal exceeded the
        // 1d8 + INT base ceiling (8 + INT) at least *sometimes*. To make
        // this deterministic per our seed, assert HP gained ≥ 2 (the 2d8 floor).
        Assert.True(pc.CurrentHp >= 3,
            $"Body-Wright Combat Medic should heal ≥ 2 HP from a 2d8 roll (was at 1, now {pc.CurrentHp})");
    }

    [Fact]
    public void NonBodyWright_FieldRepair_RollsOneD8()
    {
        var enc = MakeDuel(out var pc, out var hostile,
            pcClass: "claw_wright", pcSubclass: null);
        FeatureProcessor.EnsureFieldRepairReady(pc.SourceCharacter!);

        pc.CurrentHp = 1;
        bool ok = FeatureProcessor.TryFieldRepair(enc, pc, pc);
        Assert.True(ok);
        // Non-body-wright: 1d8 + INT = [1+INT..8+INT], capped to MaxHp.
        // Just assert heal ≤ 8 + INT (we don't care about INT exactly here).
        int intMod = pc.SourceCharacter!.Abilities.ModFor(AbilityId.INT);
        Assert.True(pc.CurrentHp <= 1 + 8 + intMod,
            $"Field Repair w/o Body-Wright caps at 1d8+INT = {1 + 8 + intMod}, got {pc.CurrentHp}");
    }

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

    private Encounter MakeDuel(out Combatant pc, out Combatant hostile,
        string pcClass, string? pcSubclass)
    {
        var pcChar = MakeChar(pcClass, new AbilityScores(15, 14, 13, 12, 10, 8));
        if (pcSubclass is not null) pcChar.SubclassId = pcSubclass;
        pc = Combatant.FromCharacter(pcChar, 1, "PC", new Vec2(0, 0),
            Theriapolis.Core.Rules.Character.Allegiance.Player);

        var hostileChar = MakeChar("fangsworn", new AbilityScores(13, 12, 13, 10, 10, 8));
        hostile = Combatant.FromCharacter(hostileChar, 2, "Hostile", new Vec2(3, 0),
            Theriapolis.Core.Rules.Character.Allegiance.Hostile);

        return new Encounter(0xCAFEUL, 1, new List<Combatant> { pc, hostile });
    }

    private Theriapolis.Core.Rules.Character.Character MakeChar(string classId, AbilityScores a)
    {
        var b = new CharacterBuilder
        {
            Clade        = _content.Clades["canidae"],
            Species      = _content.Species["wolf"],
            ClassDef     = _content.Classes[classId],
            Background   = _content.Backgrounds["pack_raised"],
            BaseAbilities = a,
        };
        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 static AttackOption MakeRangedAttack() =>
        new AttackOption
        {
            Name            = "ranged-test",
            Damage          = new DamageRoll(1, 6, 0, DamageType.Piercing),
            ToHitBonus      = 0,
            // Setting RangeShortTiles>0 flips IsRanged true via the derived prop.
            RangeShortTiles = 6,
            RangeLongTiles  = 18,
        };
}
