using Theriapolis.Core;
using Theriapolis.Core.Data;
using Theriapolis.Core.Entities;
using Theriapolis.Core.Rules.Combat;
using Theriapolis.Core.Tactical;
using Theriapolis.Core.World;
using Theriapolis.Core.World.Settlements;
using Xunit;

namespace Theriapolis.Tests.Settlements;

/// <summary>
/// Phase 6 M1 — resident-instantiation correctness.
///
/// Walks the full pipeline: chunk → SettlementStamper emits Resident spawn
/// records → ResidentInstantiator resolves them → NpcActor lands inside
/// the building with the right name, bias profile, and dialogue id.
/// </summary>
public sealed class ResidentSpawnTests : IClassFixture<WorldCache>
{
    private const ulong TestSeed = 0xCAFEBABEUL;
    private readonly WorldCache _cache;
    public ResidentSpawnTests(WorldCache c) => _cache = c;

    private ContentResolver Content() => new(new ContentLoader(TestHelpers.DataDirectory));

    [Fact]
    public void NamedRoleTags_ResolveToHandAuthoredTemplates()
    {
        var content = Content();
        // Direct lookup — these IDs are referenced by Millhaven's preset.
        Assert.True(content.ResidentsByRoleTag.ContainsKey("millhaven.innkeeper"));
        Assert.True(content.ResidentsByRoleTag.ContainsKey("millhaven.constable_fenn"));
        Assert.True(content.ResidentsByRoleTag.ContainsKey("millhaven.grandmother_asha"));
        Assert.True(content.ResidentsByRoleTag.ContainsKey("thornfield.dr_venn"));

        var asha = content.ResidentsByRoleTag["millhaven.grandmother_asha"];
        Assert.Equal("Grandmother Asha", asha.Name);
        Assert.Equal("canidae",          asha.Clade);
        Assert.Equal("wolf",             asha.Species);
        Assert.Equal("CANID_TRADITIONALIST", asha.BiasProfile);
    }

    [Fact]
    public void GenericRoleTags_FallBackToGenericTemplates()
    {
        var content = Content();
        // Suffix-stripping: "anywhere.innkeeper" should resolve to the
        // generic_innkeeper template since no anywhere.* preset exists.
        var pick = ResidentInstantiator.ResolveTemplate(
            "anywhere.innkeeper", content,
            worldSeed: 1, settlementId: 1, buildingId: 0, spawnIndex: 0);
        Assert.NotNull(pick);
        Assert.False(pick!.Named);
        Assert.Equal("innkeeper", pick.RoleTag);
    }

    [Fact]
    public void ResidentInstantiator_PlacesNpcInsideBuilding()
    {
        var w = _cache.Get(TestSeed).World;
        var content = Content();

        // Find a settlement that resolves to a layout (Millhaven if anchor
        // matches, else any Tier 2-3 procedural settlement).
        var settlement = w.Settlements.FirstOrDefault(s => s.Anchor is NarrativeAnchor.Millhaven)
                      ?? w.Settlements.First(s => !s.IsPoi && s.Tier <= 3);

        SettlementStamper.EnsureBuildingsResolved(TestSeed, settlement, content.Settlements);
        Assert.NotEmpty(settlement.Buildings);

        // Pick the first building that has at least one resident slot.
        var building = settlement.Buildings.FirstOrDefault(b => b.Residents.Length > 0);
        Assert.NotNull(building);
        var slot = building!.Residents[0];

        // Spawn it through the full path (chunk render → ResidentInstantiator).
        var actors  = new ActorManager();
        var registry = new AnchorRegistry();
        registry.RegisterAllAnchors(w);

        var cc = new ChunkCoord(
            slot.SpawnX / C.TACTICAL_CHUNK_SIZE - (slot.SpawnX < 0 ? 1 : 0),
            slot.SpawnY / C.TACTICAL_CHUNK_SIZE - (slot.SpawnY < 0 ? 1 : 0));
        var chunk = TacticalChunkGen.Generate(TestSeed, cc, w, content.Settlements);

        // Find the spawn entry for this slot inside the chunk.
        int spawnIdx = -1;
        for (int i = 0; i < chunk.Spawns.Count; i++)
        {
            var s = chunk.Spawns[i];
            int wx = chunk.OriginX + s.LocalX;
            int wy = chunk.OriginY + s.LocalY;
            if (s.Kind == SpawnKind.Resident && wx == slot.SpawnX && wy == slot.SpawnY)
            {
                spawnIdx = i;
                break;
            }
        }
        Assert.NotEqual(-1, spawnIdx);

        var npc = ResidentInstantiator.Spawn(
            TestSeed, chunk, spawnIdx, chunk.Spawns[spawnIdx],
            w, content, actors, registry);
        Assert.NotNull(npc);
        Assert.Equal(slot.SpawnX, (int)npc!.Position.X);
        Assert.Equal(slot.SpawnY, (int)npc.Position.Y);
        Assert.Equal(slot.RoleTag, npc.RoleTag);
        Assert.NotEmpty(npc.DisplayName);
        Assert.NotEmpty(npc.BiasProfileId);
        Assert.True(npc.IsAlive);
    }

    [Fact]
    public void NamedResident_RegistersInAnchorRegistry()
    {
        var w = _cache.Get(TestSeed).World;
        var content = Content();
        var millhaven = w.Settlements.FirstOrDefault(s => s.Anchor is NarrativeAnchor.Millhaven);
        if (millhaven is null) return;  // anchor placement varies — skip if absent

        SettlementStamper.EnsureBuildingsResolved(TestSeed, millhaven, content.Settlements);

        var actors   = new ActorManager();
        var registry = new AnchorRegistry();
        registry.RegisterAllAnchors(w);

        // Stream every chunk overlapping each Millhaven building.
        foreach (var b in millhaven.Buildings)
        {
            int minCx = (int)Math.Floor(b.MinX / (double)C.TACTICAL_CHUNK_SIZE);
            int minCy = (int)Math.Floor(b.MinY / (double)C.TACTICAL_CHUNK_SIZE);
            int maxCx = (int)Math.Floor(b.MaxX / (double)C.TACTICAL_CHUNK_SIZE);
            int maxCy = (int)Math.Floor(b.MaxY / (double)C.TACTICAL_CHUNK_SIZE);
            for (int cy = minCy; cy <= maxCy; cy++)
            for (int cx = minCx; cx <= maxCx; cx++)
            {
                var cc    = new ChunkCoord(cx, cy);
                var chunk = TacticalChunkGen.Generate(TestSeed, cc, w, content.Settlements);
                for (int i = 0; i < chunk.Spawns.Count; i++)
                {
                    var s = chunk.Spawns[i];
                    if (s.Kind != SpawnKind.Resident) continue;
                    if (actors.FindNpcBySource(cc, i) is not null) continue;
                    ResidentInstantiator.Spawn(TestSeed, chunk, i, s, w, content, actors, registry);
                }
            }
        }

        // Anchor entry exists.
        Assert.NotNull(registry.ResolveAnchor("anchor:millhaven"));

        // The named innkeeper role must be registered.
        var innkeeperId = registry.ResolveRole("role:millhaven.innkeeper");
        Assert.NotNull(innkeeperId);

        var innkeeper = actors.Npcs.First(n => n.Id == innkeeperId.Value);
        Assert.Equal("Mara Threadwell", innkeeper.DisplayName);
        Assert.Equal("URBAN_PROGRESSIVE", innkeeper.BiasProfileId);
    }

    [Fact]
    public void GenericResidents_DoNotPolluteAnchorRegistry()
    {
        // Procedural Tier 2/3 settlements use generic role tags ("innkeeper")
        // — those should NOT register as roles (only anchor.role pairs do).
        var w = _cache.Get(TestSeed).World;
        var content = Content();
        var settlement = w.Settlements.FirstOrDefault(
            s => s.Anchor is null && !s.IsPoi && s.Tier is 2 or 3);
        if (settlement is null) return;
        SettlementStamper.EnsureBuildingsResolved(TestSeed, settlement, content.Settlements);

        var actors   = new ActorManager();
        var registry = new AnchorRegistry();

        foreach (var b in settlement.Buildings)
        foreach (var r in b.Residents)
        {
            int cx = (int)Math.Floor(r.SpawnX / (double)C.TACTICAL_CHUNK_SIZE);
            int cy = (int)Math.Floor(r.SpawnY / (double)C.TACTICAL_CHUNK_SIZE);
            var cc    = new ChunkCoord(cx, cy);
            var chunk = TacticalChunkGen.Generate(TestSeed, cc, w, content.Settlements);
            for (int i = 0; i < chunk.Spawns.Count; i++)
            {
                var s = chunk.Spawns[i];
                if (s.Kind != SpawnKind.Resident) continue;
                int wx = chunk.OriginX + s.LocalX;
                int wy = chunk.OriginY + s.LocalY;
                if (wx != r.SpawnX || wy != r.SpawnY) continue;
                if (actors.FindNpcBySource(cc, i) is not null) continue;
                ResidentInstantiator.Spawn(TestSeed, chunk, i, s, w, content, actors, registry);
            }
        }

        // No role:* entries should exist for a generic-only settlement.
        foreach (var (id, _) in registry.AllRoles)
            Assert.DoesNotContain(".", id[..(id.IndexOf(':'))]);  // sanity: prefix is "role:"
        Assert.True(registry.AllRoles.Count == 0,
            $"generic settlement should produce no named role registrations, got {registry.AllRoles.Count}");
    }

    [Fact]
    public void ResidentInstantiator_IsDeterministic()
    {
        var content = Content();
        // Generic role with multiple matching templates picks the same one
        // for the same seed/chunk/slot every time.
        var first = ResidentInstantiator.ResolveTemplate("village.shopkeeper", content,
            worldSeed: 0xCAFEBABEUL, settlementId: 5, buildingId: 2, spawnIndex: 0);
        var second = ResidentInstantiator.ResolveTemplate("village.shopkeeper", content,
            worldSeed: 0xCAFEBABEUL, settlementId: 5, buildingId: 2, spawnIndex: 0);
        Assert.NotNull(first);
        Assert.Equal(first!.Id, second!.Id);
    }
}
