using Theriapolis.Core.Data;
using Theriapolis.Core.Rules.Stats;
using Xunit;

namespace Theriapolis.Tests.Data;

/// <summary>
/// End-to-end content load + cross-file integrity tests for Phase 5 JSON.
/// If a JSON edit breaks any of these, ContentValidate will also fail in CI.
/// </summary>
public sealed class ContentLoadTests
{
    private static ContentLoader Loader() => new(TestHelpers.DataDirectory);

    [Fact]
    public void AllPhase5ContentFiles_LoadCleanly()
    {
        var loader = Loader();
        var clades  = loader.LoadClades();
        var species = loader.LoadSpecies(clades);
        var classes = loader.LoadClasses();
        var subs    = loader.LoadSubclasses(classes);
        var bgs     = loader.LoadBackgrounds();
        var items   = loader.LoadItems();
        var npcs    = loader.LoadNpcTemplates(items);

        Assert.Equal(7,  clades.Length);
        Assert.True(species.Length >= 19, $"expected ≥19 species, got {species.Length}");
        Assert.Equal(8,  classes.Length);
        Assert.Equal(16, subs.Length);   // 8 classes × 2 subclasses
        Assert.Equal(12, bgs.Length);
        Assert.True(items.Length >= 30,  $"expected ≥30 items, got {items.Length}");
        Assert.True(npcs.Templates.Length >= 9, $"expected ≥9 NPC templates, got {npcs.Templates.Length}");
    }

    [Fact]
    public void EveryClass_HasLevel1FeaturesDefined()
    {
        var classes = Loader().LoadClasses();
        foreach (var c in classes)
        {
            var lv1 = Array.Find(c.LevelTable, e => e.Level == 1);
            Assert.NotNull(lv1);
            Assert.NotEmpty(lv1!.Features);
            foreach (var feat in lv1.Features)
                Assert.True(c.FeatureDefinitions.ContainsKey(feat),
                    $"Class '{c.Id}' level 1 references undefined feature '{feat}'");
        }
    }

    [Fact]
    public void EveryClass_HasFullLevelTable()
    {
        var classes = Loader().LoadClasses();
        foreach (var c in classes)
        {
            var levels = c.LevelTable.Select(e => e.Level).OrderBy(x => x).ToArray();
            Assert.Equal(20, levels.Length);
            for (int lv = 1; lv <= 20; lv++)
                Assert.Contains(lv, levels);
        }
    }

    [Fact]
    public void EveryClass_LevelTableProficiencyBonusMatchesD20()
    {
        var classes = Loader().LoadClasses();
        foreach (var c in classes)
        foreach (var entry in c.LevelTable)
            Assert.Equal(ProficiencyBonus.ForLevel(entry.Level), entry.ProficiencyBonus);
    }

    [Fact]
    public void EveryClass_HasTwoSubclasses()
    {
        var classes = Loader().LoadClasses();
        var subs    = Loader().LoadSubclasses(classes);
        foreach (var c in classes)
        {
            var matching = subs.Where(s => s.ClassId == c.Id).ToArray();
            Assert.Equal(2, matching.Length);
            // Subclass ids must match what the class declares
            foreach (var sid in c.SubclassIds)
                Assert.Contains(sid, matching.Select(s => s.Id));
        }
    }

    [Fact]
    public void EverySpecies_ReferencesARealClade()
    {
        var clades  = Loader().LoadClades();
        var species = Loader().LoadSpecies(clades);
        var cladeIds = clades.Select(c => c.Id).ToHashSet(StringComparer.OrdinalIgnoreCase);
        foreach (var sp in species)
            Assert.Contains(sp.CladeId, cladeIds);
    }

    [Theory]
    [InlineData("wolf",       1, 0,  1, 0,  1,  0)]
    [InlineData("fox",        0, 1,  1, 0,  1,  0)]
    [InlineData("coyote",     0, 0,  1, 0,  1,  1)]
    [InlineData("lion",       1, 1,  0, 0,  0,  1)]
    [InlineData("leopard",    0, 2,  0, 0,  0,  1)]
    [InlineData("housecat",   0, 1,  0, 1,  0,  1)]
    [InlineData("ferret",     0, 1,  0, 1,  0,  1)]
    [InlineData("badger",     0, 1,  1, 1,  0,  0)]
    [InlineData("wolverine",  1, 1,  0, 1,  0,  0)]
    [InlineData("brown_bear", 1,-1,  2, 0,  0,  0)]
    [InlineData("polar_bear", 0,-1,  2, 0,  1,  0)]
    [InlineData("elk",        1, 1,  0, 0,  1,  0)]
    [InlineData("deer",       0, 2,  0, 0,  1,  0)]
    [InlineData("moose",      0, 1,  1, 0,  1,  0)]
    [InlineData("rabbit",    -1, 2,  0, 0,  1,  0)]
    [InlineData("hare",      -1, 2,  1, 0,  0,  0)]
    [InlineData("bull",       2, 0,  1, 0,  0,  0)]
    [InlineData("sheep",      1, 0,  1, 0,  1,  0)]
    [InlineData("goat",       1, 0,  1, 0,  1,  0)]
    [InlineData("bison",      1, 0,  2, 0,  0,  0)]
    public void Clade_Plus_Species_AbilityMods_MatchQuickRefTable(
        string speciesId, int str, int dex, int con, int @int, int wis, int cha)
    {
        var loader = Loader();
        var clades  = loader.LoadClades();
        var species = loader.LoadSpecies(clades);

        var sp = species.Single(s => s.Id == speciesId);
        var cl = clades.Single(c => c.Id == sp.CladeId);

        int Sum(string ability) =>
            (cl.AbilityMods.TryGetValue(ability, out var c) ? c : 0) +
            (sp.AbilityMods.TryGetValue(ability, out var s) ? s : 0);

        Assert.Equal(str,  Sum("STR"));
        Assert.Equal(dex,  Sum("DEX"));
        Assert.Equal(con,  Sum("CON"));
        Assert.Equal(@int, Sum("INT"));
        Assert.Equal(wis,  Sum("WIS"));
        Assert.Equal(cha,  Sum("CHA"));
    }

    [Fact]
    public void EveryWeapon_HasDamageAndType()
    {
        var items = Loader().LoadItems();
        foreach (var i in items.Where(i => i.Kind == "weapon"))
        {
            Assert.False(string.IsNullOrWhiteSpace(i.Damage), $"weapon '{i.Id}' missing damage");
            Assert.False(string.IsNullOrWhiteSpace(i.DamageType), $"weapon '{i.Id}' missing damage_type");
        }
    }

    [Fact]
    public void EveryArmor_HasPositiveAcBase()
    {
        var items = Loader().LoadItems();
        foreach (var i in items.Where(i => i.Kind == "armor"))
            Assert.True(i.AcBase > 0, $"armor '{i.Id}' has non-positive ac_base");
    }

    [Fact]
    public void NpcZoneTable_HasOneEntryPerZone()
    {
        var items = Loader().LoadItems();
        var npcs  = Loader().LoadNpcTemplates(items);
        int expected = Theriapolis.Core.C.DANGER_ZONE_MAX - Theriapolis.Core.C.DANGER_ZONE_MIN + 1;

        foreach (var (kind, byZone) in npcs.SpawnKindToTemplateByZone)
            Assert.Equal(expected, byZone.Length);
    }

    [Fact]
    public void NpcZoneTable_AllReferencedTemplatesExist()
    {
        var items = Loader().LoadItems();
        var npcs  = Loader().LoadNpcTemplates(items);
        var ids = npcs.Templates.Select(t => t.Id).ToHashSet();
        foreach (var (_, byZone) in npcs.SpawnKindToTemplateByZone)
        foreach (var tid in byZone)
            Assert.Contains(tid, ids);
    }

    [Fact]
    public void EveryNpcTemplate_HasPositiveHpAndAc()
    {
        var items = Loader().LoadItems();
        var npcs  = Loader().LoadNpcTemplates(items);
        foreach (var t in npcs.Templates)
        {
            Assert.True(t.Hp > 0, $"NPC '{t.Id}' has non-positive HP");
            Assert.True(t.Ac > 0, $"NPC '{t.Id}' has non-positive AC");
        }
    }
}
