using Theriapolis.Core;
using Theriapolis.Core.Data;
using Theriapolis.Core.Tactical;
using Theriapolis.Core.World;
using Theriapolis.Core.World.Settlements;
using Xunit;

namespace Theriapolis.Tests.Settlements;

/// <summary>
/// Phase 6 M0 — building stamp determinism + content shape tests.
///
/// The settlement-stamping path picks up content lazily on the first chunk
/// that touches each settlement; identical seeds must produce identical
/// building lists, and the stamped tile bytes must round-trip across two
/// independent generations.
/// </summary>
public sealed class BuildingStampTests : IClassFixture<WorldCache>
{
    private const ulong TestSeed = 0xCAFEBABEUL;
    private readonly WorldCache _cache;
    public BuildingStampTests(WorldCache c) => _cache = c;

    private SettlementContent LoadContent()
        => new ContentResolver(new ContentLoader(TestHelpers.DataDirectory)).Settlements;

    [Fact]
    public void ContentLoader_LoadsBuildingsAndLayouts()
    {
        var content = LoadContent();
        Assert.True(content.Buildings.Count >= 6,
            $"expected ≥ 6 building templates, got {content.Buildings.Count}");
        Assert.True(content.PresetByAnchor.Count >= 1,
            "expected at least one preset settlement layout");
        Assert.True(content.ProceduralByTier.Count >= 4,
            "expected procedural layouts for Tier 2/3/4/5");
        // Sanity: every preset must reference real building templates.
        foreach (var p in content.PresetByAnchor.Values)
        foreach (var b in p.Buildings)
            Assert.True(content.Buildings.ContainsKey(b.Template),
                $"preset '{p.Id}' references unknown template '{b.Template}'");
    }

    [Fact]
    public void Stamp_ProducesIdenticalBuildingsAcrossRuns()
    {
        var w1 = _cache.Get(TestSeed, variant: 0).World;
        var w2 = _cache.Get(TestSeed, variant: 1).World;
        var content = LoadContent();

        var s1 = w1.Settlements.First(s => !s.IsPoi && s.Tier <= 3);
        var s2 = w2.Settlements.First(s => s.Id == s1.Id);

        SettlementStamper.EnsureBuildingsResolved(TestSeed, s1, content);
        SettlementStamper.EnsureBuildingsResolved(TestSeed, s2, content);

        Assert.Equal(s1.Buildings.Count, s2.Buildings.Count);
        for (int i = 0; i < s1.Buildings.Count; i++)
        {
            Assert.Equal(s1.Buildings[i].TemplateId, s2.Buildings[i].TemplateId);
            Assert.Equal(s1.Buildings[i].MinX,       s2.Buildings[i].MinX);
            Assert.Equal(s1.Buildings[i].MinY,       s2.Buildings[i].MinY);
            Assert.Equal(s1.Buildings[i].MaxX,       s2.Buildings[i].MaxX);
            Assert.Equal(s1.Buildings[i].MaxY,       s2.Buildings[i].MaxY);
        }
    }

    [Fact]
    public void Stamp_ProducesIdenticalChunkHashWithSameContent()
    {
        var w = _cache.Get(TestSeed).World;
        var content = LoadContent();
        var s = w.Settlements.First(s => !s.IsPoi && s.Tier <= 3);
        var cc = ChunkCoord.ForWorldTile(s.TileX, s.TileY);

        var a = TacticalChunkGen.Generate(TestSeed, cc, w, content);
        // Re-resolve buildings from a fresh world to make sure the stamper
        // is idempotent (BuildingsResolved guard works).
        var w2 = _cache.Get(TestSeed, variant: 1).World;
        var b = TacticalChunkGen.Generate(TestSeed, cc, w2, content);
        Assert.Equal(a.Hash(), b.Hash());
    }

    [Fact]
    public void Stamp_ContentNullFallsBackToLegacyHash()
    {
        // Generating a chunk without content should match a chunk generated
        // with the no-content overload — i.e., the fallback path is the
        // Phase-4 placeholder behaviour, byte-for-byte.
        var w = _cache.Get(TestSeed).World;
        var s = w.Settlements.First(s => !s.IsPoi && s.Tier <= 3);
        var cc = ChunkCoord.ForWorldTile(s.TileX, s.TileY);

        var a = TacticalChunkGen.Generate(TestSeed, cc, w);
        var b = TacticalChunkGen.Generate(TestSeed, cc, w, settlementContent: null);
        Assert.Equal(a.Hash(), b.Hash());
    }

    [Fact]
    public void StampedSettlement_HasMoreBuildingTilesThanFallback()
    {
        // The whole point of M0 — content path stamps Floor tiles inside
        // building footprints, fallback only stamps Cobble. Floor count
        // diverges; this captures that.
        var w = _cache.Get(TestSeed).World;
        var content = LoadContent();
        var s = w.Settlements.First(s => !s.IsPoi && s.Tier <= 3);
        var cc = ChunkCoord.ForWorldTile(s.TileX, s.TileY);

        var withContent = TacticalChunkGen.Generate(TestSeed, cc, w, content);
        var w2 = _cache.Get(TestSeed, variant: 1).World;
        var without = TacticalChunkGen.Generate(TestSeed, cc, w2, settlementContent: null);

        int floorWith    = CountSurface(withContent, TacticalSurface.Floor);
        int floorWithout = CountSurface(without,     TacticalSurface.Floor);
        Assert.True(floorWith > floorWithout,
            $"Content-aware stamp should produce floor tiles; got {floorWith} vs {floorWithout}.");
    }

    [Fact]
    public void Buildings_HaveDoorsAndDoorsAreWalkable()
    {
        var w = _cache.Get(TestSeed).World;
        var content = LoadContent();
        var s = w.Settlements.First(x => !x.IsPoi && x.Tier <= 3);
        SettlementStamper.EnsureBuildingsResolved(TestSeed, s, content);
        Assert.NotEmpty(s.Buildings);

        // Render a chunk that overlaps each building and confirm the door
        // tile is walkable + carries the Doorway flag.
        foreach (var b in s.Buildings)
        {
            Assert.NotEmpty(b.Doors);
            foreach (var (dx, dy) in b.Doors)
            {
                var cc = new ChunkCoord(
                    dx / C.TACTICAL_CHUNK_SIZE - (dx < 0 ? 1 : 0),
                    dy / C.TACTICAL_CHUNK_SIZE - (dy < 0 ? 1 : 0));
                var chunk = TacticalChunkGen.Generate(TestSeed, cc, w, content);
                int lx = dx - chunk.OriginX;
                int ly = dy - chunk.OriginY;
                Assert.InRange(lx, 0, C.TACTICAL_CHUNK_SIZE - 1);
                Assert.InRange(ly, 0, C.TACTICAL_CHUNK_SIZE - 1);
                ref var tile = ref chunk.Tiles[lx, ly];
                Assert.True(tile.IsWalkable, $"door at ({dx},{dy}) for building {b.TemplateId} should be walkable");
                Assert.True((tile.Flags & (byte)TacticalFlags.Doorway) != 0, "door tile must carry the Doorway flag");
                Assert.True((tile.Flags & (byte)TacticalFlags.Building) != 0, "door tile must carry the Building flag");
            }
        }
    }

    [Fact]
    public void MillhavenAnchor_GetsItsPresetLayout()
    {
        // We can't guarantee the Millhaven anchor exists at every test seed
        // (placement depends on world geometry). When it does, it should
        // resolve to the preset layout, not the procedural Tier-1 fallback.
        var w = _cache.Get(TestSeed).World;
        var content = LoadContent();
        var millhaven = w.Settlements.FirstOrDefault(
            s => s.Anchor is NarrativeAnchor.Millhaven);
        if (millhaven is null) return;

        var layout = content.ResolveFor(millhaven);
        Assert.NotNull(layout);
        Assert.Equal("preset", layout!.Kind);
        Assert.Equal("Millhaven", layout.Anchor);
    }

    [Fact]
    public void ProceduralLayouts_StampMultipleBuildings()
    {
        // Pick a non-anchor Tier 2 or 3 settlement and confirm the
        // procedural roller produced more than one building. Sanity that
        // the weighted picker doesn't collapse to zero.
        var w = _cache.Get(TestSeed).World;
        var content = LoadContent();
        var s = w.Settlements.FirstOrDefault(
            x => x.Anchor is null && !x.IsPoi && x.Tier is 2 or 3);
        if (s is null) return;

        SettlementStamper.EnsureBuildingsResolved(TestSeed, s, content);
        Assert.True(s.Buildings.Count >= 2,
            $"procedural Tier-{s.Tier} settlement should stamp ≥ 2 buildings, got {s.Buildings.Count}");
    }

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

        // Find any settlement at Tier ≤ 3 whose layout has roles.
        var s = w.Settlements.First(x => !x.IsPoi && x.Tier <= 3);
        SettlementStamper.EnsureBuildingsResolved(TestSeed, s, content);

        // Sum role count across buildings.
        int expectedRoles = s.Buildings.Sum(b => b.Residents.Length);
        if (expectedRoles == 0) return;

        // Generate every chunk overlapping any building and count Resident
        // spawn records emitted.
        int actual = 0;
        var seen = new HashSet<ChunkCoord>();
        foreach (var b in s.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);
                if (!seen.Add(cc)) continue;
                var chunk = TacticalChunkGen.Generate(TestSeed, cc, w, content);
                foreach (var sp in chunk.Spawns)
                    if (sp.Kind == SpawnKind.Resident) actual++;
            }
        }
        Assert.Equal(expectedRoles, actual);
    }

    private static int CountSurface(TacticalChunk chunk, TacticalSurface surface)
    {
        int n = 0;
        for (int y = 0; y < C.TACTICAL_CHUNK_SIZE; y++)
        for (int x = 0; x < C.TACTICAL_CHUNK_SIZE; x++)
            if (chunk.Tiles[x, y].Surface == surface) n++;
        return n;
    }
}
