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 6.5 M2 — representative L3 subclass-feature mechanics:
///   - Lone Fang "Isolation Bonus": +2 to-hit / +1 AC when alone.
///   - Herd-Wall "Interlock Shields": +1 AC with adjacent ally.
///   - Pack-Forged "Packmate's Howl": melee hit marks target → ally
///     advantage on next attack against it.
///   - Blood Memory "Predatory Surge": melee kill while raging sets a
///     bonus-attack flag.
///
/// Other 12 subclasses' features are scaffolded (definitions loaded,
/// LevelUpScreen displays them, save round-trip works) but not yet
/// mechanically wired — content authoring for them is a follow-up
/// session per M2's plan-acknowledged 24-feature scope.
/// </summary>
public sealed class Phase65M2SubclassFeatureTests
{
    private readonly ContentResolver _content = new(new ContentLoader(TestHelpers.DataDirectory));

    // ── Lone Fang Isolation Bonus ─────────────────────────────────────────

    [Fact]
    public void LoneFang_IsolationBonus_AddsAcWhenNoAllyNearby()
    {
        // Build a Fangsworn with subclass = lone_fang. No allies on the field
        // (just the attacker and a hostile target).
        var enc = MakeEncounter(out var pc, out var hostile,
            pcClass: "fangsworn", pcSubclass: "lone_fang");
        int baseAc = pc.ArmorClass;
        // Target gets attacked; their AC is the relevant query — Lone Fang's
        // +1 AC applies *to themselves* (the Lone Fang). So instead, query
        // the AC bonus for the lone fang directly.
        int bonus = FeatureProcessor.ApplyAcBonus(pc, enc);
        Assert.Equal(1, bonus);
    }

    [Fact]
    public void LoneFang_IsolationBonus_DropsToZeroWithAdjacentAlly()
    {
        var enc = MakeEncounter(out var pc, out var hostile,
            pcClass: "fangsworn", pcSubclass: "lone_fang",
            includeAlly: true, allyPos: new Vec2(1, 0));
        int bonus = FeatureProcessor.ApplyAcBonus(pc, enc);
        Assert.Equal(0, bonus);
    }

    [Fact]
    public void LoneFang_IsolationBonus_AddsToHitWhenAlone()
    {
        var enc = MakeEncounter(out var pc, out var hostile,
            pcClass: "fangsworn", pcSubclass: "lone_fang");
        int toHit = FeatureProcessor.ApplyToHitBonus(pc, enc);
        Assert.Equal(2, toHit);
    }

    [Fact]
    public void LoneFang_NotApplied_WithoutSubclass()
    {
        var enc = MakeEncounter(out var pc, out _,
            pcClass: "fangsworn", pcSubclass: null);
        Assert.Equal(0, FeatureProcessor.ApplyAcBonus(pc, enc));
        Assert.Equal(0, FeatureProcessor.ApplyToHitBonus(pc, enc));
    }

    // ── Herd-Wall Interlock Shields ───────────────────────────────────────

    [Fact]
    public void HerdWall_InterlockShields_AddsAcWithAdjacentAlly()
    {
        var enc = MakeEncounter(out var pc, out _,
            pcClass: "bulwark", pcSubclass: "herd_wall",
            includeAlly: true, allyPos: new Vec2(1, 0));
        Assert.Equal(1, FeatureProcessor.ApplyAcBonus(pc, enc));
    }

    [Fact]
    public void HerdWall_InterlockShields_NoBonus_WhenAlone()
    {
        var enc = MakeEncounter(out var pc, out _,
            pcClass: "bulwark", pcSubclass: "herd_wall");
        Assert.Equal(0, FeatureProcessor.ApplyAcBonus(pc, enc));
    }

    [Fact]
    public void HerdWall_InterlockShields_NoBonus_WhenAllyNotAdjacent()
    {
        var enc = MakeEncounter(out var pc, out _,
            pcClass: "bulwark", pcSubclass: "herd_wall",
            includeAlly: true, allyPos: new Vec2(5, 0));
        Assert.Equal(0, FeatureProcessor.ApplyAcBonus(pc, enc));
    }

    // ── Pack-Forged Packmate's Howl ───────────────────────────────────────

    [Fact]
    public void PackForged_OnHit_MarksTarget()
    {
        var enc = MakeEncounter(out var pc, out var hostile,
            pcClass: "fangsworn", pcSubclass: "pack_forged");
        // Simulate the on-hit pathway directly: the resolver calls this on
        // a melee hit.
        FeatureProcessor.OnPackForgedHit(enc, pc, hostile, pc.AttackOptions[0]);
        Assert.True(hostile.HowlMarkRound.HasValue);
        Assert.Equal(pc.Id, hostile.HowlMarkBy);
    }

    [Fact]
    public void PackForged_OnHit_DoesNotMarkOnRanged()
    {
        var enc = MakeEncounter(out var pc, out var hostile,
            pcClass: "fangsworn", pcSubclass: "pack_forged");
        var rangedAttack = pc.AttackOptions[0] with { Name = "Bow", RangeShortTiles = 6 };
        FeatureProcessor.OnPackForgedHit(enc, pc, hostile, rangedAttack);
        Assert.False(hostile.HowlMarkRound.HasValue);
    }

    [Fact]
    public void ConsumeHowlAdvantage_FiresForAllyWithinRound()
    {
        var enc = MakeEncounter(out var pc, out var hostile,
            pcClass: "fangsworn", pcSubclass: "pack_forged",
            includeAlly: true, allyPos: new Vec2(1, 0));
        var ally = enc.Participants.Single(c =>
            c.Allegiance == Theriapolis.Core.Rules.Character.Allegiance.Allied);
        FeatureProcessor.OnPackForgedHit(enc, pc, hostile, pc.AttackOptions[0]);

        bool consumed = FeatureProcessor.ConsumeHowlAdvantage(enc, ally, hostile);
        Assert.True(consumed);
        // Mark cleared after consumption.
        Assert.False(hostile.HowlMarkRound.HasValue);
        // Second consumption returns false.
        Assert.False(FeatureProcessor.ConsumeHowlAdvantage(enc, ally, hostile));
    }

    [Fact]
    public void ConsumeHowlAdvantage_RefusesSelfMarker()
    {
        var enc = MakeEncounter(out var pc, out var hostile,
            pcClass: "fangsworn", pcSubclass: "pack_forged");
        FeatureProcessor.OnPackForgedHit(enc, pc, hostile, pc.AttackOptions[0]);
        // The marker can't consume their own howl.
        bool consumed = FeatureProcessor.ConsumeHowlAdvantage(enc, pc, hostile);
        Assert.False(consumed);
        // Mark stays in place.
        Assert.True(hostile.HowlMarkRound.HasValue);
    }

    [Fact]
    public void ConsumeHowlAdvantage_RefusesEnemyAttacker()
    {
        var enc = MakeEncounter(out var pc, out var hostile,
            pcClass: "fangsworn", pcSubclass: "pack_forged");
        FeatureProcessor.OnPackForgedHit(enc, pc, hostile, pc.AttackOptions[0]);
        // Enemy attacking same target as the mark — should not consume.
        bool consumed = FeatureProcessor.ConsumeHowlAdvantage(enc, hostile, hostile);
        Assert.False(consumed);
    }

    // ── Blood Memory Predatory Surge ──────────────────────────────────────

    [Fact]
    public void BloodMemory_OnKill_SetsPredatorySurgePending_WhenRaging()
    {
        var enc = MakeEncounter(out var pc, out var hostile,
            pcClass: "feral", pcSubclass: "blood_memory");
        pc.RageActive = true;
        FeatureProcessor.OnBloodMemoryKill(enc, pc, pc.AttackOptions[0]);
        Assert.True(pc.PredatorySurgePending);
    }

    [Fact]
    public void BloodMemory_OnKill_DoesNothing_IfNotRaging()
    {
        var enc = MakeEncounter(out var pc, out _,
            pcClass: "feral", pcSubclass: "blood_memory");
        pc.RageActive = false;
        FeatureProcessor.OnBloodMemoryKill(enc, pc, pc.AttackOptions[0]);
        Assert.False(pc.PredatorySurgePending);
    }

    [Fact]
    public void BloodMemory_OnKill_DoesNothing_OnRangedKill()
    {
        var enc = MakeEncounter(out var pc, out _,
            pcClass: "feral", pcSubclass: "blood_memory");
        pc.RageActive = true;
        var rangedAttack = pc.AttackOptions[0] with { Name = "Bow", RangeShortTiles = 6 };
        FeatureProcessor.OnBloodMemoryKill(enc, pc, rangedAttack);
        Assert.False(pc.PredatorySurgePending);
    }

    // ── LevelUpFlow integration ───────────────────────────────────────────

    [Fact]
    public void LevelUpFlow_PostL3_PopulatesSubclassFeatures()
    {
        var c = MakeChar("fangsworn", new AbilityScores(15, 12, 13, 10, 10, 8));
        c.SubclassId = "pack_forged";
        c.Level = 3;
        var result = LevelUpFlow.Compute(c, targetLevel: 7, seed: 0xBEEF,
            takeAverage: true, subclasses: _content.Subclasses);
        // Pack-Forged's L7 feature is "coordinated_takedown".
        Assert.Contains("coordinated_takedown", result.SubclassFeaturesUnlocked);
    }

    [Fact]
    public void LevelUpFlow_NoSubclassPicked_ReturnsEmptySubclassFeatures()
    {
        var c = MakeChar("fangsworn", new AbilityScores(15, 12, 13, 10, 10, 8));
        c.Level = 6;  // post-L3 but no subclass chosen (shouldn't happen in
                      // normal flow but test the defensive path).
        var result = LevelUpFlow.Compute(c, targetLevel: 7, seed: 0xBEEF,
            takeAverage: true, subclasses: _content.Subclasses);
        Assert.Empty(result.SubclassFeaturesUnlocked);
    }

    [Fact]
    public void LevelUpFlow_NullSubclassesDict_ReturnsEmptySubclassFeatures()
    {
        var c = MakeChar("fangsworn", new AbilityScores(15, 12, 13, 10, 10, 8));
        c.SubclassId = "pack_forged";
        c.Level = 3;
        var result = LevelUpFlow.Compute(c, targetLevel: 7, seed: 0xBEEF,
            takeAverage: true, subclasses: null);
        Assert.Empty(result.SubclassFeaturesUnlocked);
    }

    [Fact]
    public void Character_ApplyLevelUp_RecordsSubclassFeaturesInLearned()
    {
        var c = MakeChar("fangsworn", new AbilityScores(15, 12, 13, 10, 10, 8));
        c.SubclassId = "pack_forged";
        c.Level = 6;
        var result = LevelUpFlow.Compute(c, 7, 0xBEEF,
            takeAverage: true, subclasses: _content.Subclasses);
        c.ApplyLevelUp(result, new LevelUpChoices());
        Assert.Contains("coordinated_takedown", c.LearnedFeatureIds);
    }

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

    private Encounter MakeEncounter(
        out Combatant pc, out Combatant hostile,
        string pcClass, string? pcSubclass,
        bool includeAlly = false,
        Vec2? allyPos = null)
    {
        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);

        var participants = new List<Combatant> { pc, hostile };
        if (includeAlly)
        {
            var allyChar = MakeChar("fangsworn", new AbilityScores(12, 12, 13, 10, 10, 8));
            var ally = Combatant.FromCharacter(allyChar, 3, "Ally",
                allyPos ?? new Vec2(1, 0),
                Theriapolis.Core.Rules.Character.Allegiance.Allied);
            participants.Add(ally);
        }

        return new Encounter(0xCAFEUL, 1, participants);
    }

    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);
    }
}
