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 — DialogueRunner mechanics: option visibility, effect
/// application (set_flag, give_item, rep_event, open_shop), skill-check
/// branching, deterministic dice, history scrollback.
/// </summary>
public sealed class DialogueRunnerTests
{
    private static ContentResolver LoadContent()
        => new ContentResolver(new ContentLoader(TestHelpers.DataDirectory));

    private static Character WolfPc(ContentResolver content)
    {
        var b = 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, 10, 11));
        var classD = content.Classes["fangsworn"];
        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 in content */ }
        }
        return b.Build();
    }

    private static NpcActor SyntheticInnkeeper(ContentResolver content)
    {
        var template = content.Residents["generic_innkeeper"];
        return new NpcActor(template) { Id = 1, RoleTag = "test.innkeeper" };
    }

    private static (DialogueRunner runner, DialogueContext ctx) NewRunner(
        DialogueDef tree,
        ContentResolver content,
        out PlayerReputation rep,
        out Dictionary<string, int> flags)
    {
        var pc  = WolfPc(content);
        var npc = SyntheticInnkeeper(content);
        rep   = new PlayerReputation();
        flags = new Dictionary<string, int>();
        var ctx = new DialogueContext(npc, pc, rep, flags, content);
        return (new DialogueRunner(tree, ctx, worldSeed: 0xCAFEBABEUL), ctx);
    }

    private static DialogueDef SimpleTree()
        => new DialogueDef
        {
            Id   = "test_simple",
            Root = "intro",
            Nodes = new[]
            {
                new DialogueNodeDef
                {
                    Id      = "intro",
                    Speaker = "npc",
                    Text    = "Welcome.",
                    Options = new[]
                    {
                        new DialogueOptionDef
                        {
                            Text    = "Take this gift.",
                            Next    = "thanks",
                            Effects = new[] { new DialogueEffectDef { Kind = "set_flag", Flag = "gifted", Value = 1 } },
                        },
                        new DialogueOptionDef { Text = "Goodbye.", Next = "<end>" },
                    },
                },
                new DialogueNodeDef { Id = "thanks", Speaker = "npc", Text = "You shouldn't have." },
            },
        };

    private static DialogueDef TreeWithSkillCheck()
        => new DialogueDef
        {
            Id   = "test_check",
            Root = "intro",
            Nodes = new[]
            {
                new DialogueNodeDef
                {
                    Id      = "intro",
                    Speaker = "npc",
                    Text    = "Try me.",
                    Options = new[]
                    {
                        new DialogueOptionDef
                        {
                            Text          = "Persuade",
                            SkillCheck    = new DialogueSkillCheckDef { Skill = "persuasion", Dc = 5 },
                            NextOnSuccess = "yes",
                            NextOnFailure = "no",
                            EffectsOnSuccess = new[] { new DialogueEffectDef { Kind = "set_flag", Flag = "won" } },
                            EffectsOnFailure = new[] { new DialogueEffectDef { Kind = "set_flag", Flag = "lost" } },
                        },
                    },
                },
                new DialogueNodeDef { Id = "yes", Speaker = "npc", Text = "Fine." },
                new DialogueNodeDef { Id = "no",  Speaker = "npc", Text = "No way." },
            },
        };

    [Fact]
    public void StartsAtRoot_AppendsOpeningLine()
    {
        var content = LoadContent();
        var (runner, _) = NewRunner(SimpleTree(), content, out _, out _);
        Assert.False(runner.IsOver);
        Assert.Equal("intro", runner.CurrentNode.Id);
        Assert.Single(runner.History);   // the opening NPC line
    }

    [Fact]
    public void ChooseOption_AppliesEffectAndAdvances()
    {
        var content = LoadContent();
        var (runner, _) = NewRunner(SimpleTree(), content, out _, out var flags);
        var result = runner.ChooseOption(0);
        Assert.Equal("thanks", runner.CurrentNode.Id);
        Assert.True(flags.ContainsKey("gifted") && flags["gifted"] == 1);
        Assert.False(result.ClosedAfter);
    }

    [Fact]
    public void EndOption_ClosesDialogue()
    {
        var content = LoadContent();
        var (runner, _) = NewRunner(SimpleTree(), content, out _, out _);
        runner.ChooseOption(1);
        Assert.True(runner.IsOver);
    }

    [Fact]
    public void SkillCheck_BranchesOnRoll()
    {
        // DC=5 against a STR/DEX-leaning Fangsworn with 11 CHA → mod = 0,
        // so the d20 alone must clear 5. Most rolls succeed.
        var content = LoadContent();
        var (runner, _) = NewRunner(TreeWithSkillCheck(), content, out _, out var flags);
        runner.ChooseOption(0);
        bool won  = flags.ContainsKey("won");
        bool lost = flags.ContainsKey("lost");
        Assert.True(won ^ lost, "exactly one of won/lost must be set");
        Assert.True(runner.IsOver || runner.CurrentNode.Id is "yes" or "no");
    }

    [Fact]
    public void SkillCheck_IsDeterministic_ForSameSeedAndOptionIndex()
    {
        var content = LoadContent();

        // Run two independent runners on the same seed; both should pick the
        // same branch.
        var (r1, _) = NewRunner(TreeWithSkillCheck(), content, out _, out var f1);
        var (r2, _) = NewRunner(TreeWithSkillCheck(), content, out _, out var f2);
        r1.ChooseOption(0);
        r2.ChooseOption(0);
        Assert.Equal(f1.ContainsKey("won"), f2.ContainsKey("won"));
        Assert.Equal(r1.CurrentNode.Id, r2.CurrentNode.Id);
    }

    [Fact]
    public void Effect_OpenShop_SetsContextFlag()
    {
        var tree = new DialogueDef
        {
            Id = "shop_test", Root = "n",
            Nodes = new[]
            {
                new DialogueNodeDef
                {
                    Id = "n", Speaker = "npc", Text = "Hi",
                    Options = new[]
                    {
                        new DialogueOptionDef { Text = "Browse.", Effects = new[] { new DialogueEffectDef { Kind = "open_shop" } }, Next = "<end>" },
                    },
                },
            },
        };
        var content = LoadContent();
        var (runner, ctx) = NewRunner(tree, content, out _, out _);
        runner.ChooseOption(0);
        Assert.True(ctx.ShopRequested);
    }

    [Fact]
    public void Effect_RepEvent_SubmitsToReputation()
    {
        var tree = new DialogueDef
        {
            Id = "rep_test", Root = "n",
            Nodes = new[]
            {
                new DialogueNodeDef
                {
                    Id = "n", Speaker = "npc", Text = "Hi",
                    Options = new[]
                    {
                        new DialogueOptionDef
                        {
                            Text = "Insult.",
                            Effects = new[]
                            {
                                new DialogueEffectDef
                                {
                                    Kind = "rep_event",
                                    Event = new DialogueRepEventDef { Kind = "Dialogue", Magnitude = -5 },
                                },
                            },
                            Next = "<end>",
                        },
                    },
                },
            },
        };
        var content = LoadContent();
        var (runner, _) = NewRunner(tree, content, out var rep, out _);
        runner.ChooseOption(0);
        Assert.True(rep.Personal["test.innkeeper"].Score < 0);
    }
}
