using Theriapolis.Core;
using Theriapolis.Core.Data;
using Theriapolis.Core.Dungeons;
using Theriapolis.Core.World;
using Xunit;

namespace Theriapolis.Tests.Dungeons;

/// <summary>
/// Phase 7 M2 — populator tests. Verifies:
///   - The same (seed, poi, levelBand) → byte-identical population.
///   - Encounter slots resolve to the per-dungeon-type templates.
///   - Boss-role rooms use the type's Boss template.
///   - Container slots pre-roll loot from the layout's loot-band table.
/// </summary>
public sealed class DungeonPopulatorTests
{
    private readonly ContentResolver _content = new(new ContentLoader(TestHelpers.DataDirectory));

    private DungeonPopulation Populate(ulong seed, int poi, PoiType type, int levelBand)
    {
        var d = DungeonGenerator.Generate(seed, poi, type, _content);
        // Find the matching layout (procedural; not anchor-locked).
        DungeonLayoutDef? layout = null;
        foreach (var l in _content.DungeonLayouts.Values)
            if (string.IsNullOrEmpty(l.Anchor)
                && string.Equals(l.DungeonType, type.ToString(), System.StringComparison.OrdinalIgnoreCase))
            { layout = l; break; }
        Assert.NotNull(layout);
        return DungeonPopulator.Populate(d, layout!, _content, levelBand, seed);
    }

    [Fact]
    public void Populate_SameInputs_ProducesIdenticalPopulation()
    {
        var a = Populate(0xCAFE12345UL, 7, PoiType.ImperiumRuin, levelBand: 2);
        var b = Populate(0xCAFE12345UL, 7, PoiType.ImperiumRuin, levelBand: 2);

        Assert.Equal(a.Spawns.Length, b.Spawns.Length);
        for (int i = 0; i < a.Spawns.Length; i++)
        {
            Assert.Equal(a.Spawns[i].RoomId,      b.Spawns[i].RoomId);
            Assert.Equal(a.Spawns[i].X,           b.Spawns[i].X);
            Assert.Equal(a.Spawns[i].Y,           b.Spawns[i].Y);
            Assert.Equal(a.Spawns[i].Template.Id, b.Spawns[i].Template.Id);
            Assert.Equal(a.Spawns[i].Kind,        b.Spawns[i].Kind);
        }
        Assert.Equal(a.Containers.Length, b.Containers.Length);
        for (int i = 0; i < a.Containers.Length; i++)
        {
            Assert.Equal(a.Containers[i].TableId,    b.Containers[i].TableId);
            Assert.Equal(a.Containers[i].Drops.Length, b.Containers[i].Drops.Length);
            for (int j = 0; j < a.Containers[i].Drops.Length; j++)
                Assert.Equal(a.Containers[i].Drops[j].Def.Id, b.Containers[i].Drops[j].Def.Id);
        }
    }

    [Fact]
    public void Populate_EncounterSlots_ResolveToTypeTemplates()
    {
        var pop = Populate(0x42UL, 1, PoiType.ImperiumRuin, levelBand: 2);
        // Imperium templates: imperium_undead_thrall (PoiGuard),
        // imperium_feral_canid (WildAnimal), brigand_marauder (Brigand),
        // imperium_undead_overseer (Boss).
        var expected = new HashSet<string>
        {
            "imperium_undead_thrall", "imperium_feral_canid",
            "brigand_marauder", "imperium_undead_overseer",
        };
        foreach (var s in pop.Spawns)
            Assert.Contains(s.Template.Id, expected);
    }

    [Fact]
    public void Populate_BossRoom_GetsBossTemplate()
    {
        // Imperium medium layout requires a boss room. The boss-role room's
        // encounter slots that declare Boss kind should resolve to the
        // dungeon type's Boss template.
        for (int i = 0; i < 5; i++)
        {
            var d = DungeonGenerator.Generate(0xB05UL + (ulong)i, i, PoiType.ImperiumRuin, _content);
            var layout = _content.DungeonLayouts["imperium_medium"];
            var pop = DungeonPopulator.Populate(d, layout, _content, levelBand: 2, worldSeed: 0xB05UL + (ulong)i);

            // Find the boss room.
            int bossRoomId = -1;
            foreach (var r in d.Rooms)
                if (r.Role == RoomRole.Boss) { bossRoomId = r.Id; break; }
            Assert.NotEqual(-1, bossRoomId);

            // The boss-room's Boss-kind spawn should be the overseer.
            bool foundBoss = false;
            foreach (var s in pop.Spawns)
                if (s.RoomId == bossRoomId && s.Kind == "Boss")
                { foundBoss = true; Assert.Equal("imperium_undead_overseer", s.Template.Id); }
            Assert.True(foundBoss, "Boss room should have a Boss-kind spawn");
        }
    }

    [Fact]
    public void Populate_ContainerSlots_HaveDropsAndTable()
    {
        var pop = Populate(0xC0FFEEUL, 1, PoiType.ImperiumRuin, levelBand: 2);
        // Imperium pillar_room_cardinal + sarcophagus_chamber + boss_throne_room
        // each have a container slot, so we should see at least one.
        Assert.NotEmpty(pop.Containers);
        foreach (var c in pop.Containers)
        {
            Assert.False(string.IsNullOrEmpty(c.TableId),
                $"container in room {c.RoomId} has no table id");
        }
    }

    [Fact]
    public void Populate_AllContainerTableIds_ResolveToRealTables()
    {
        // Across multiple seeds and level bands, every populated container
        // should reference a loot table that exists in the resolver. This
        // catches band-mapping bugs (e.g. layout missing a t3 entry) and
        // confirms the resolver→populator wiring stays coherent.
        for (int band = 0; band <= 3; band++)
        for (int i = 0; i < 5; i++)
        {
            ulong seed = 0x1007UL + (ulong)i * 1000UL;
            var pop = Populate(seed, i, PoiType.ImperiumRuin, levelBand: band);
            foreach (var c in pop.Containers)
            {
                Assert.True(_content.LootTables.ContainsKey(c.TableId),
                    $"populator emitted unknown loot table '{c.TableId}' (band={band}, room={c.RoomId})");
            }
        }
    }
}
