using Theriapolis.Core;
using Theriapolis.Core.Data;
using Theriapolis.Core.Entities;
using Theriapolis.Core.Rules.Character;
using Theriapolis.Core.Rules.Reputation;
using Theriapolis.Core.Rules.Stats;
using Xunit;

namespace Theriapolis.Tests.Reputation;

/// <summary>
/// Phase 6 M2 — disposition formula correctness.
///
/// The blend is:
///   Total = CladeBias + SizeDifferential + FactionModifier + Personal
/// each layer independently testable. We synthesise small fixtures so the
/// tests don't depend on actual content.json values.
/// </summary>
public sealed class EffectiveDispositionTests
{
    private static ContentResolver LoadContent()
        => new ContentResolver(new ContentLoader(TestHelpers.DataDirectory));

    private static Character WolfPc(ContentResolver content)
    {
        var clade   = content.Clades["canidae"];
        var species = content.Species["wolf"];
        var classD  = content.Classes["fangsworn"];
        var bg      = content.Backgrounds["pack_raised"];
        var b = new CharacterBuilder()
            .WithClade(clade).WithSpecies(species)
            .WithClass(classD).WithBackground(bg)
            .WithAbilities(new AbilityScores(13, 12, 14, 10, 10, 11));
        // Pick the right number of class skills.
        int needed = classD.SkillsChoose;
        var added = new HashSet<SkillId>();
        for (int i = 0; i < classD.SkillOptions.Length && added.Count < needed; i++)
        {
            try
            {
                var sk = SkillIdExtensions.FromJson(classD.SkillOptions[i]);
                if (added.Add(sk)) b.ChooseSkill(sk);
            }
            catch (System.ArgumentException) { /* unknown skill name in content — skip */ }
        }
        return b.Build();
    }

    [Fact]
    public void DispositionLabel_BoundariesMatchThresholds()
    {
        Assert.Equal(DispositionLabel.Champion,    DispositionLabels.For( 80));
        Assert.Equal(DispositionLabel.Allied,      DispositionLabels.For( 60));
        Assert.Equal(DispositionLabel.Friendly,    DispositionLabels.For( 30));
        Assert.Equal(DispositionLabel.Favorable,   DispositionLabels.For( 10));
        Assert.Equal(DispositionLabel.Neutral,     DispositionLabels.For(  0));
        Assert.Equal(DispositionLabel.Unfriendly,  DispositionLabels.For(-10));
        Assert.Equal(DispositionLabel.Antagonistic,DispositionLabels.For(-30));
        Assert.Equal(DispositionLabel.Hostile,     DispositionLabels.For(-60));
        Assert.Equal(DispositionLabel.Nemesis,     DispositionLabels.For(-90));
    }

    [Fact]
    public void Breakdown_Sums_AllLayers()
    {
        var content = LoadContent();
        var pc      = WolfPc(content);
        var rep     = new PlayerReputation();

        // A wolf-folk PC vs a CANID_TRADITIONALIST resident: clade bias
        // for canidae is +15, no size differential (Wolf-Folk = MediumLarge,
        // generic_innkeeper is rabbit = Small → diff = +2 ⇒ -8 mod). Personal
        // and faction = 0. So effective = 15 + (-8) = 7 → Favorable.
        var template = content.Residents["generic_innkeeper"];   // Leporidae rabbit, URBAN_PROGRESSIVE
        var npc = new NpcActor(template) { Id = 1, RoleTag = "innkeeper" };

        var br = EffectiveDisposition.Breakdown(npc, pc, rep, content);
        Assert.Equal(br.CladeBias + br.SizeDifferential + br.FactionModifier + br.Personal, br.Total);
        Assert.Equal(DispositionLabels.For(br.Total), br.Label);
    }

    [Fact]
    public void Breakdown_HostileBiasProfile_ProducesNegativeTotal()
    {
        var content = LoadContent();
        var pc      = WolfPc(content);
        var rep     = new PlayerReputation();

        // Find a resident with a profile that's actively hostile to canidae
        // (THORN_COUNCIL_HARDLINER → canidae -25).
        var hostileTemplate = new ResidentTemplateDef
        {
            Id          = "test_hardliner",
            RoleTag     = "test.hardliner",
            Named       = true,
            Name        = "Test Hardliner",
            Clade       = "cervidae",
            Species     = "elk",
            BiasProfile = "THORN_COUNCIL_HARDLINER",
        };
        var npc = new NpcActor(hostileTemplate) { Id = 2, RoleTag = "test.hardliner" };

        var br = EffectiveDisposition.Breakdown(npc, pc, rep, content);
        Assert.True(br.Total < 0,
            $"Wolf-folk vs Thorn Council Hardliner should be negative; got {br.Total}");
    }

    [Fact]
    public void Personal_Disposition_OverridesNeutralBaseline()
    {
        var content = LoadContent();
        var pc      = WolfPc(content);
        var rep     = new PlayerReputation();

        var template = content.Residents["generic_innkeeper"];
        var npc = new NpcActor(template) { Id = 3, RoleTag = "village.innkeeper" };

        // Apply a +30 personal event. Effective should rise by ~30.
        var before = EffectiveDisposition.For(npc, pc, rep, content);
        rep.PersonalFor(npc.RoleTag).Apply(new RepEvent
        {
            Kind = RepEventKind.Aid, RoleTag = npc.RoleTag, Magnitude = 30,
        });
        var after  = EffectiveDisposition.For(npc, pc, rep, content);
        Assert.Equal(30, after - before);
    }

    [Fact]
    public void Faction_Standing_ContributesToHalfWeight()
    {
        var content = LoadContent();
        var pc      = WolfPc(content);
        var rep     = new PlayerReputation();

        // Use a constable template (faction = covenant_enforcers).
        var template = content.Residents["generic_constable"];
        var npc = new NpcActor(template) { Id = 4, RoleTag = "village.constable" };

        var before = EffectiveDisposition.For(npc, pc, rep, content);
        rep.Factions.Set("covenant_enforcers", 40);
        var after  = EffectiveDisposition.For(npc, pc, rep, content);

        // Direct faction affiliation contributes 0.5×; the bias profile
        // (Covenant Faithful) layers an additional 0.25× × affinity/100
        // for matching factions. For COVENANT_FAITHFUL with
        // covenant_enforcers affinity = +25, the layered weight is
        // 40 × 25/100 × 0.25 = 2.5 → rounds to 3 on top of the 20 from
        // direct affiliation. So the delta lands in the 20..23 range.
        int delta = after - before;
        Assert.InRange(delta, 20, 23);
    }

    [Fact]
    public void Score_IsClampedToRepRange()
    {
        var content = LoadContent();
        var pc      = WolfPc(content);
        var rep     = new PlayerReputation();
        var template = content.Residents["generic_innkeeper"];
        var npc = new NpcActor(template) { Id = 5, RoleTag = "any.innkeeper" };

        // Stack +200 personal points; clamp should keep it at +100.
        rep.PersonalFor(npc.RoleTag).Score = 250;   // bypass Apply to force a value past clamp
        var br = EffectiveDisposition.Breakdown(npc, pc, rep, content);
        Assert.True(br.Total <= C.REP_MAX);

        rep.PersonalFor(npc.RoleTag).Score = -250;
        br = EffectiveDisposition.Breakdown(npc, pc, rep, content);
        Assert.True(br.Total >= C.REP_MIN);
    }
}
