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

namespace Theriapolis.Tests.Dialogue;

/// <summary>
/// Phase 6 M3 — option visibility predicates: rep_at_least, has_flag,
/// has_item, ability_min, and their negations. The runner hides options
/// whose conditions fail; the visible-option iterator must skip them.
/// </summary>
public sealed class OptionConditionTests
{
    private static ContentResolver Content() =>
        new ContentResolver(new ContentLoader(TestHelpers.DataDirectory));

    private static (DialogueRunner runner, PlayerReputation rep, Dictionary<string,int> flags, Character pc)
        Setup(DialogueDef tree)
    {
        var content = Content();
        var pc = new CharacterBuilder()
            .WithClade(content.Clades["canidae"])
            .WithSpecies(content.Species["wolf"])
            .WithClass(content.Classes["fangsworn"])
            .WithBackground(content.Backgrounds["pack_raised"])
            .WithAbilities(new AbilityScores(13, 12, 14, 10, 14, 11))
            .ChooseSkill(SkillId.Athletics)
            .ChooseSkill(SkillId.Perception)
            .Build();
        var npc = new NpcActor(content.Residents["generic_innkeeper"]) { Id = 1, RoleTag = "test.npc" };
        var rep   = new PlayerReputation();
        var flags = new Dictionary<string, int>();
        var ctx   = new DialogueContext(npc, pc, rep, flags, content);
        return (new DialogueRunner(tree, ctx, 0xCAFEBABEUL), rep, flags, pc);
    }

    private static DialogueDef OneNode(params DialogueOptionDef[] opts)
        => new DialogueDef
        {
            Id   = "test_opts",
            Root = "n",
            Nodes = new[]
            {
                new DialogueNodeDef { Id = "n", Speaker = "npc", Text = "?", Options = opts },
            },
        };

    [Fact]
    public void HasFlag_HidesUntilSet()
    {
        var tree = OneNode(
            new DialogueOptionDef { Text = "Visible only after gifted",
                Conditions = new[] { new DialogueConditionDef { Kind = "has_flag", Flag = "gifted" } },
                Next = "<end>" });
        var (r, _, flags, _) = Setup(tree);
        Assert.Empty(r.VisibleOptions());
        flags["gifted"] = 1;
        Assert.Single(r.VisibleOptions());
    }

    [Fact]
    public void NotHasFlag_VisibleUntilSet()
    {
        var tree = OneNode(
            new DialogueOptionDef { Text = "Visible until done",
                Conditions = new[] { new DialogueConditionDef { Kind = "not_has_flag", Flag = "done" } },
                Next = "<end>" });
        var (r, _, flags, _) = Setup(tree);
        Assert.Single(r.VisibleOptions());
        flags["done"] = 1;
        Assert.Empty(r.VisibleOptions());
    }

    [Fact]
    public void RepAtLeast_GatesOnFactionStanding()
    {
        var tree = OneNode(
            new DialogueOptionDef { Text = "Friendly only",
                Conditions = new[] { new DialogueConditionDef { Kind = "rep_at_least", Faction = "covenant_enforcers", Value = 25 } },
                Next = "<end>" });
        var (r, rep, _, _) = Setup(tree);
        Assert.Empty(r.VisibleOptions());
        rep.Factions.Set("covenant_enforcers", 25);
        Assert.Single(r.VisibleOptions());
        rep.Factions.Set("covenant_enforcers", 24);
        Assert.Empty(r.VisibleOptions());
    }

    [Fact]
    public void RepAtLeast_NoFaction_ChecksEffectiveDisposition()
    {
        var tree = OneNode(
            new DialogueOptionDef { Text = "Personal only",
                Conditions = new[] { new DialogueConditionDef { Kind = "rep_at_least", Value = 30 } },
                Next = "<end>" });
        var (r, rep, _, _) = Setup(tree);
        Assert.Empty(r.VisibleOptions());
        // Pump personal disposition above threshold.
        rep.PersonalFor("test.npc").Apply(new RepEvent { Kind = RepEventKind.Aid, RoleTag = "test.npc", Magnitude = 60 });
        Assert.Single(r.VisibleOptions());
    }

    [Fact]
    public void AbilityMin_GatesOnAbilityMod()
    {
        var tree = OneNode(
            new DialogueOptionDef { Text = "Wise option",
                Conditions = new[] { new DialogueConditionDef { Kind = "ability_min", Ability = "WIS", Value = 2 } },
                Next = "<end>" });
        // Setup gave the PC WIS=14 → mod=+2; passes.
        var (r, _, _, _) = Setup(tree);
        Assert.Single(r.VisibleOptions());

        // Higher threshold fails.
        var tree2 = OneNode(
            new DialogueOptionDef { Text = "Sage option",
                Conditions = new[] { new DialogueConditionDef { Kind = "ability_min", Ability = "WIS", Value = 5 } },
                Next = "<end>" });
        var (r2, _, _, _) = Setup(tree2);
        Assert.Empty(r2.VisibleOptions());
    }

    [Fact]
    public void HasItem_DependsOnInventory()
    {
        var content = Content();
        var tree = OneNode(
            new DialogueOptionDef { Text = "Show me your knife",
                Conditions = new[] { new DialogueConditionDef { Kind = "has_item", Id = "fang_knife" } },
                Next = "<end>" });
        var (r, _, _, pc) = Setup(tree);
        Assert.Empty(r.VisibleOptions());
        pc.Inventory.Add(content.Items["fang_knife"]);
        Assert.Single(r.VisibleOptions());
    }

    [Fact]
    public void MultipleConditions_AreAnded()
    {
        var tree = OneNode(
            new DialogueOptionDef { Text = "Both",
                Conditions = new[]
                {
                    new DialogueConditionDef { Kind = "has_flag", Flag = "a" },
                    new DialogueConditionDef { Kind = "has_flag", Flag = "b" },
                },
                Next = "<end>" });
        var (r, _, flags, _) = Setup(tree);
        Assert.Empty(r.VisibleOptions());
        flags["a"] = 1;
        Assert.Empty(r.VisibleOptions());
        flags["b"] = 1;
        Assert.Single(r.VisibleOptions());
    }
}
