using Theriapolis.Core;
using Theriapolis.Core.Data;
using Theriapolis.Core.Entities;
using Theriapolis.Core.Items;
using Theriapolis.Core.Rules.Character;
using Theriapolis.Core.Rules.Dialogue;
using Theriapolis.Core.Rules.Quests;
using Theriapolis.Core.Rules.Reputation;
using Theriapolis.Core.Rules.Stats;
using Theriapolis.Core.Time;
using Theriapolis.Core.Util;
using Theriapolis.Core.World.Settlements;
using Xunit;

namespace Theriapolis.Tests.ActI;

/// <summary>
/// Phase 6 M6 — Act I content end-to-end. Drives the dialogue runner +
/// quest engine through the ship-point sequence:
///   1. Player arrives in Millhaven → arrival quest auto-starts.
///   2. Player talks to the magistrate → flag set, plot items handed
///      over, arrival quest advances.
///   3. Player talks to Asha → Old Howl quest starts, stone given.
///   4. Player returns to Asha → Old Howl quest completes.
///   5. Player talks to Lacroix and interrogates him → climax resolved
///      with intel branch; Maw sigil obtained.
///   6. Final inventory matches the design's ship-point checklist:
///      journal, formula, names list, Maw sigil, Howl-stone.
///
/// The test is content-driven: no mocks; loads the real JSON from
/// Content/Data/. If the dialogue or quest authoring drifts from the
/// engine's expectations, this test catches it.
/// </summary>
public sealed class ActIIntegrationTests : IClassFixture<WorldCache>
{
    private const ulong TestSeed = 0xCAFEBABEUL;
    private readonly WorldCache _cache;
    public ActIIntegrationTests(WorldCache c) => _cache = c;

    private static Character WolfFangsworn(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"];
        var added = new HashSet<SkillId>();
        for (int i = 0; i < classD.SkillOptions.Length && added.Count < classD.SkillsChoose; i++)
        {
            try
            {
                var sk = SkillIdExtensions.FromJson(classD.SkillOptions[i]);
                if (added.Add(sk)) b.ChooseSkill(sk);
            }
            catch (System.ArgumentException) { }
        }
        return b.Build();
    }

    private static NpcActor MakeNamedResident(ContentResolver content, string roleTag, int id, int settlementId)
    {
        var template = content.ResidentsByRoleTag[roleTag];
        return new NpcActor(template)
        {
            Id               = id,
            Position         = new Vec2(0, 0),
            RoleTag          = roleTag,
            HomeSettlementId = settlementId,
        };
    }

    private static void RunDialogueTree(DialogueRunner runner, params int[] optionIndices)
    {
        foreach (var idx in optionIndices)
        {
            var result = runner.ChooseOption(idx);
            if (result.ClosedAfter || runner.IsOver) return;
        }
    }

    [Fact]
    public void ShipPoint_PlaysActIToCompletion_WithExpectedInventoryAndQuests()
    {
        var content = new ContentResolver(new ContentLoader(TestHelpers.DataDirectory));
        var pc      = WolfFangsworn(content);
        var rep     = new PlayerReputation();
        var flags   = new Dictionary<string, int>();
        var anchors = new AnchorRegistry();
        var clock   = new WorldClock();
        var world   = _cache.Get(TestSeed).World;
        anchors.RegisterAllAnchors(world);

        var actors = new ActorManager();
        actors.SpawnPlayer(new Vec2(0, 0), pc);

        var qctx = new QuestContext(content, actors, rep, flags, anchors, clock, world)
        {
            PlayerCharacter = pc,
        };
        var qengine = new QuestEngine();

        // Find Millhaven (skip if the seed didn't place it as a normal anchor — the test seed places one).
        var millhaven = world.Settlements.FirstOrDefault(s => s.Anchor is Theriapolis.Core.World.NarrativeAnchor.Millhaven);
        if (millhaven is null) return;   // anchor placement varies per seed; this run skips

        // ── Step 1: arrive in Millhaven (set the player position there). ──
        actors.Player!.Position = new Vec2(
            millhaven.TileX * C.WORLD_TILE_PIXELS,
            millhaven.TileY * C.WORLD_TILE_PIXELS);
        qengine.Tick(qctx);
        Assert.True(qengine.IsActive("main_act_i_001_arrival"));

        // ── Step 2: talk to the magistrate. ──
        var magistrate = MakeNamedResident(content, "millhaven.magistrate", 100, millhaven.Id);
        var magCtx = new DialogueContext(magistrate, pc, rep, flags, content);
        var magRunner = new DialogueRunner(content.Dialogues["millhaven_magistrate"], magCtx, TestSeed);
        // Walk: intro → "Tell me what happened" (option 0) → "I'll take them" (option 0)
        RunDialogueTree(magRunner, 0, 0);

        Assert.True(flags.GetValueOrDefault("spoke_to_millhaven_magistrate") == 1);
        Assert.Contains(pc.Inventory.Items, i => i.Def.Id == "briarstead_journal");
        Assert.Contains(pc.Inventory.Items, i => i.Def.Id == "formula_partial");
        Assert.Contains(pc.Inventory.Items, i => i.Def.Id == "names_list");

        // Quest engine should advance to "investigate" step.
        qengine.Tick(qctx);
        Assert.Equal("investigate", qengine.Get("main_act_i_001_arrival")!.CurrentStep);

        // ── Step 3: talk to Asha → start Old Howl. ──
        var asha = MakeNamedResident(content, "millhaven.grandmother_asha", 101, millhaven.Id);
        var ashaCtx1 = new DialogueContext(asha, pc, rep, flags, content);
        var ashaRunner1 = new DialogueRunner(content.Dialogues["millhaven_grandmother_asha"], ashaCtx1, TestSeed);
        // intro options: 0 (parents), 1 (lore), 2 (journal_recognised, conditional TRUE), 3 (<end>).
        // parents → wind_smell, then wind_smell → favour (option 1 the first time).
        // favour → "I'll fetch the stone" (option 0) sets asha_offered_favour and start_quest.
        RunDialogueTree(ashaRunner1, 0, 0, 1, 0);

        // First time through, "favour" branch sets asha_offered_favour and starts the quest.
        // Hand-fire the start_quest in case the runner buffered it.
        if (ashaCtx1.StartQuestRequests.Count > 0)
        {
            foreach (var qid in ashaCtx1.StartQuestRequests)
                qengine.Start(qid, qctx);
            ashaCtx1.StartQuestRequests.Clear();
        }
        Assert.True(qengine.IsActive("side_act_i_old_howl"),
            "Asha's dialogue should have started the Old Howl side quest");

        // The quest's find_stone step's on_enter should have given the stone.
        Assert.Contains(pc.Inventory.Items, i => i.Def.Id == "howl_stone");

        // ── Step 4: return to Asha with the stone. ──
        var ashaCtx2 = new DialogueContext(asha, pc, rep, flags, content);
        var ashaRunner2 = new DialogueRunner(content.Dialogues["millhaven_grandmother_asha"], ashaCtx2, TestSeed);
        // intro: now visible options include journal_recognised + parents + lore + end.
        // Re-talk: 0 (parents) → 0 (wind_smell) → 0 (favour_offer, conditional on has_flag: asha_offered_favour).
        // From favour_offer: 0 (stone returned, conditional on has_item: howl_stone) → stone_returned.
        RunDialogueTree(ashaRunner2, 0, 0, 0, 0);

        Assert.True(flags.GetValueOrDefault("asha_received_howl_stone") == 1,
            "Returning the stone must set the asha_received_howl_stone flag");
        Assert.DoesNotContain(pc.Inventory.Items, i => i.Def.Id == "howl_stone");

        qengine.Tick(qctx);
        Assert.True(qengine.IsCompleted("side_act_i_old_howl"));

        // ── Step 5: confront Lacroix and interrogate him. ──
        var lacroix = MakeNamedResident(content, "millhaven.lacroix", 102, millhaven.Id);
        var lacCtx = new DialogueContext(lacroix, pc, rep, flags, content);
        var lacRunner = new DialogueRunner(content.Dialogues["millhaven_lacroix"], lacCtx, TestSeed);
        // intro options: 0 (what_doing), 1 (accuse, conditional on has_item: briarstead_journal — TRUE), 2 (end)
        // We have the journal. Visible: 0 (what_doing), 1 (accuse), 2 (end). Pick 1 (accuse).
        // accuse options: 0 (intimidate skill check), 1 (settle here = fight), 2 (let go)
        // Pick 0 (intimidate). Skill check is deterministic; we accept either branch since
        // both lead to climax_resolved; we just need maw_sigil.
        RunDialogueTree(lacRunner, 1, 0);
        // After the skill check, the runner is at either "interrogate" or "fight".
        // From "interrogate", pick 0 (let him go) → set lacroix_climax_resolved + give maw_sigil + act_i_briarstead_searched.
        // From "fight", pick 0 (decisive blow) → same flags + maw_sigil.
        // Either way one more option pick closes:
        if (!lacRunner.IsOver) RunDialogueTree(lacRunner, 0);

        Assert.True(flags.GetValueOrDefault("lacroix_climax_resolved") == 1,
            "Lacroix encounter must resolve");
        Assert.True(flags.GetValueOrDefault("act_i_briarstead_searched") == 1,
            "Lacroix climax sets the briarstead_searched flag (chains arrival quest)");
        Assert.Contains(pc.Inventory.Items, i => i.Def.Id == "maw_sigil");

        // ── Step 6: arrival quest completes; following_dead chains in. ──
        qengine.Tick(qctx);
        Assert.True(qengine.IsCompleted("main_act_i_001_arrival"),
            "Arrival quest must complete after Briarstead is searched");
        Assert.True(qengine.IsActive("main_act_i_003_following_dead")
                 || qengine.IsCompleted("main_act_i_003_following_dead"),
            "Following the Dead must have started (and possibly completed via climax flags)");

        // ── Final ship-point inventory check. ──
        Assert.Contains(pc.Inventory.Items, i => i.Def.Id == "briarstead_journal");
        Assert.Contains(pc.Inventory.Items, i => i.Def.Id == "formula_partial");
        Assert.Contains(pc.Inventory.Items, i => i.Def.Id == "names_list");
        Assert.Contains(pc.Inventory.Items, i => i.Def.Id == "maw_sigil");
        // The howl_stone has been returned to Asha — that's correct end-state.
        Assert.DoesNotContain(pc.Inventory.Items, i => i.Def.Id == "howl_stone");
    }

    [Fact]
    public void AllActIQuests_LoadAndValidate()
    {
        var content = new ContentResolver(new ContentLoader(TestHelpers.DataDirectory));
        Assert.True(content.Quests.ContainsKey("main_act_i_001_arrival"));
        Assert.True(content.Quests.ContainsKey("main_act_i_003_following_dead"));
        Assert.True(content.Quests.ContainsKey("side_act_i_old_howl"));
        Assert.True(content.Quests.ContainsKey("side_act_i_fence_lines"));
    }

    [Fact]
    public void AllActINamedNpcDialogues_Exist()
    {
        var content = new ContentResolver(new ContentLoader(TestHelpers.DataDirectory));
        Assert.True(content.Dialogues.ContainsKey("millhaven_magistrate"));
        Assert.True(content.Dialogues.ContainsKey("millhaven_grandmother_asha"));
        Assert.True(content.Dialogues.ContainsKey("millhaven_constable"));
        Assert.True(content.Dialogues.ContainsKey("millhaven_lacroix"));
    }

    [Fact]
    public void AllActIPlotItems_Exist()
    {
        var content = new ContentResolver(new ContentLoader(TestHelpers.DataDirectory));
        Assert.True(content.Items.ContainsKey("briarstead_journal"));
        Assert.True(content.Items.ContainsKey("formula_partial"));
        Assert.True(content.Items.ContainsKey("names_list"));
        Assert.True(content.Items.ContainsKey("maw_sigil"));
        Assert.True(content.Items.ContainsKey("howl_stone"));
    }
}
