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

namespace Theriapolis.Tests.Rules;

/// <summary>
/// CharacterBuilder smoke + integration: every (clade × species) pair,
/// when assigned a representative class, produces a valid level-1 character
/// with sane HP and ability totals.
/// </summary>
public sealed class CharacterBuilderTests
{
    private readonly ContentResolver _content = new(new ContentLoader(TestHelpers.DataDirectory));

    [Fact]
    public void Build_DefaultsProduceValidCharacter()
    {
        var c = MinimalBuilder("canidae", "wolf", "fangsworn", "pack_raised").Build();
        Assert.Equal("canidae",   c.Clade.Id);
        Assert.Equal("wolf",      c.Species.Id);
        Assert.Equal("fangsworn", c.ClassDef.Id);
        Assert.Equal(1, c.Level);
        Assert.Equal(0, c.Xp);
        Assert.True(c.MaxHp > 0);
        Assert.Equal(c.MaxHp, c.CurrentHp);
        Assert.True(c.SkillProficiencies.Count >= c.ClassDef.SkillsChoose);
    }

    [Fact]
    public void Build_AppliesCladeAndSpeciesAbilityMods()
    {
        // Wolf-Folk: STR+1 (species), CON+1 WIS+1 (canid clade)
        // Standard Array assigned in class priority — we use a known base for
        // the test instead of relying on auto-assignment.
        var b = MinimalBuilder("canidae", "wolf", "fangsworn", "pack_raised");
        b.BaseAbilities = new AbilityScores(10, 10, 10, 10, 10, 10);
        var c = b.Build();
        Assert.Equal(11, c.Abilities.STR);  // 10 + species
        Assert.Equal(10, c.Abilities.DEX);
        Assert.Equal(11, c.Abilities.CON);  // 10 + clade
        Assert.Equal(10, c.Abilities.INT);
        Assert.Equal(11, c.Abilities.WIS);  // 10 + clade
        Assert.Equal(10, c.Abilities.CHA);
    }

    [Fact]
    public void Build_HpUsesHitDiePlusConModifier()
    {
        var b = MinimalBuilder("canidae", "wolf", "fangsworn", "pack_raised");
        b.BaseAbilities = new AbilityScores(10, 10, 14, 10, 10, 10);  // CON 14 → +2 → +1 after canid (15→+2)
        var c = b.Build();
        // Wolf-Folk has no CON mod from species, canid +1 → final CON = 15 → +2.
        // Fangsworn d10 + 2 = 12.
        Assert.Equal(12, c.MaxHp);
    }

    [Fact]
    public void Validate_RejectsSpeciesNotInChosenClade()
    {
        var b = MinimalBuilder("canidae", "wolf", "fangsworn", "pack_raised");
        b.Species = _content.Species["lion"];  // felid species under canid clade
        bool ok = b.Validate(out var err);
        Assert.False(ok);
        Assert.Contains("clade", err.ToLowerInvariant());
    }

    [Fact]
    public void Validate_RequiresExactSkillCount()
    {
        var b = MinimalBuilder("canidae", "wolf", "fangsworn", "pack_raised");
        b.ChosenClassSkills.Clear();
        Assert.False(b.Validate(out _));
    }

    [Fact]
    public void Validate_RejectsSkillNotOfferedByClass()
    {
        var b = MinimalBuilder("canidae", "wolf", "fangsworn", "pack_raised");
        b.ChosenClassSkills.Clear();
        b.ChosenClassSkills.Add(SkillId.Athletics);
        b.ChosenClassSkills.Add(SkillId.Arcana);  // Fangsworn does not offer Arcana
        Assert.False(b.Validate(out var err));
        Assert.Contains("arcana", err.ToLowerInvariant());
    }

    [Theory]
    [InlineData("canidae",   "wolf",       "fangsworn",       "pack_raised")]
    [InlineData("felidae",   "lion",       "shadow_pelt",     "borderland_stray")]
    [InlineData("ursidae",   "brown_bear", "feral",           "coliseum_survivor")]
    [InlineData("cervidae",  "elk",        "covenant_keeper", "covenant_enforcer")]
    [InlineData("bovidae",   "bull",       "bulwark",         "herd_city_born")]
    [InlineData("leporidae", "rabbit",     "muzzle_speaker",  "warren_runner")]
    [InlineData("mustelidae","badger",     "claw_wright",     "borderland_stray")]
    [InlineData("felidae",   "housecat",   "scent_broker",    "scent_suppressed")]
    public void Build_AllRepresentativeCombosValid(
        string cladeId, string speciesId, string classId, string bgId)
    {
        var c = MinimalBuilder(cladeId, speciesId, classId, bgId).Build();
        Assert.True(c.MaxHp > 0);
        Assert.True(c.IsAlive);
    }

    [Fact]
    public void RollAbilityScores_IsDeterministicGivenSameInputs()
    {
        var a = CharacterBuilder.RollAbilityScores(0xCAFE, 1234);
        var b = CharacterBuilder.RollAbilityScores(0xCAFE, 1234);
        Assert.Equal(a.STR, b.STR);
        Assert.Equal(a.DEX, b.DEX);
        Assert.Equal(a.CON, b.CON);
        Assert.Equal(a.INT, b.INT);
        Assert.Equal(a.WIS, b.WIS);
        Assert.Equal(a.CHA, b.CHA);
    }

    [Fact]
    public void RollAbilityScores_DifferentMsProducesDifferentResults()
    {
        var a = CharacterBuilder.RollAbilityScores(0xCAFE, 1234);
        var b = CharacterBuilder.RollAbilityScores(0xCAFE, 5678);
        // At least one of six should differ across plausibly-different rolls.
        Assert.True(
            a.STR != b.STR || a.DEX != b.DEX || a.CON != b.CON ||
            a.INT != b.INT || a.WIS != b.WIS || a.CHA != b.CHA);
    }

    [Fact]
    public void Roll4d6DropLowest_ResultIs3to18()
    {
        var rng = new SeededRng(0xDEADBEEFUL);
        for (int i = 0; i < 1000; i++)
        {
            int v = CharacterBuilder.Roll4d6DropLowest(rng);
            Assert.InRange(v, 3, 18);
        }
    }

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

    private CharacterBuilder MinimalBuilder(string cladeId, string speciesId, string classId, string bgId)
    {
        var b = new CharacterBuilder
        {
            Clade        = _content.Clades[cladeId],
            Species      = _content.Species[speciesId],
            ClassDef     = _content.Classes[classId],
            Background   = _content.Backgrounds[bgId],
            BaseAbilities = new AbilityScores(15, 14, 13, 12, 10, 8),
            Name         = "Test",
        };
        // Auto-pick first N skill options
        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;
    }
}
