using System.Diagnostics;
using Theriapolis.Core;
using Theriapolis.Core.Data;
using Theriapolis.Core.Dungeons;
using Theriapolis.Core.Tactical;
using Theriapolis.Core.World;
using Xunit;

namespace Theriapolis.Tests.Dungeons;

/// <summary>
/// Phase 7 M1 — engine-level tests for the dungeon generator. These use the
/// authored M0 vertical-slice content (5 imperium + 3 mine + 2 cave
/// templates, 2 layouts) and assert the engine's contracts:
///   - Determinism: same (seed, poi) → byte-identical Dungeon.
///   - Reachability: every Room reachable from Entrance via Connections.
///   - Scale: room count stays within the layout's declared band.
///   - Budget: generation completes in &lt; 400ms even under retry-fallback.
/// </summary>
public sealed class DungeonGeneratorTests
{
    private readonly ContentResolver _content = new(new ContentLoader(TestHelpers.DataDirectory));

    // ── Determinism ──────────────────────────────────────────────────────

    [Fact]
    public void Generate_SameSeedAndPoi_ProducesIdenticalDungeon()
    {
        const ulong seed = 0xCAFE12345UL;
        const int   poi  = 7;

        var a = DungeonGenerator.Generate(seed, poi, PoiType.ImperiumRuin, _content);
        var b = DungeonGenerator.Generate(seed, poi, PoiType.ImperiumRuin, _content);

        Assert.Equal(a.PoiId,        b.PoiId);
        Assert.Equal(a.Type,         b.Type);
        Assert.Equal(a.W,            b.W);
        Assert.Equal(a.H,            b.H);
        Assert.Equal(a.EntranceTile, b.EntranceTile);
        Assert.Equal(a.Rooms.Length, b.Rooms.Length);
        for (int i = 0; i < a.Rooms.Length; i++)
        {
            Assert.Equal(a.Rooms[i].TemplateId, b.Rooms[i].TemplateId);
            Assert.Equal(a.Rooms[i].AabbX,      b.Rooms[i].AabbX);
            Assert.Equal(a.Rooms[i].AabbY,      b.Rooms[i].AabbY);
            Assert.Equal(a.Rooms[i].Role,       b.Rooms[i].Role);
        }
        Assert.Equal(a.Connections.Length, b.Connections.Length);
        for (int i = 0; i < a.Connections.Length; i++)
            Assert.Equal(a.Connections[i], b.Connections[i]);

        // Tile array byte-identical.
        Assert.Equal(a.W * a.H, b.W * b.H);
        for (int y = 0; y < a.H; y++)
        for (int x = 0; x < a.W; x++)
        {
            Assert.Equal(a.Tiles[x, y].Surface, b.Tiles[x, y].Surface);
            Assert.Equal(a.Tiles[x, y].Deco,    b.Tiles[x, y].Deco);
        }
    }

    [Fact]
    public void Generate_DifferentSeed_ProducesDifferentDungeon()
    {
        var a = DungeonGenerator.Generate(0x1111UL, 5, PoiType.ImperiumRuin, _content);
        var b = DungeonGenerator.Generate(0x2222UL, 5, PoiType.ImperiumRuin, _content);

        // Same template count is fine, but at least *something* must differ.
        bool differs = a.W != b.W || a.H != b.H
            || a.Rooms.Length != b.Rooms.Length
            || (a.Rooms.Length == b.Rooms.Length && !RoomLayoutsMatch(a, b));
        Assert.True(differs,
            "Different worldSeeds should produce divergent layouts (room mix or geometry).");
    }

    [Fact]
    public void Generate_DifferentPoi_ProducesDifferentDungeon()
    {
        var a = DungeonGenerator.Generate(0xBEEFUL, 1, PoiType.ImperiumRuin, _content);
        var b = DungeonGenerator.Generate(0xBEEFUL, 2, PoiType.ImperiumRuin, _content);

        bool differs = a.W != b.W || a.H != b.H
            || a.Rooms.Length != b.Rooms.Length
            || (a.Rooms.Length == b.Rooms.Length && !RoomLayoutsMatch(a, b));
        Assert.True(differs,
            "Different poiIds at the same seed should produce divergent layouts.");
    }

    private static bool RoomLayoutsMatch(Dungeon a, Dungeon b)
    {
        for (int i = 0; i < a.Rooms.Length; i++)
            if (a.Rooms[i].TemplateId != b.Rooms[i].TemplateId
                || a.Rooms[i].AabbX != b.Rooms[i].AabbX
                || a.Rooms[i].AabbY != b.Rooms[i].AabbY)
                return false;
        return true;
    }

    // ── Reachability ─────────────────────────────────────────────────────

    [Fact]
    public void Generate_EveryRoom_ReachableFromEntrance()
    {
        // Sample 20 (seed, poi) pairs and assert reachability for each.
        for (int i = 0; i < 20; i++)
        {
            ulong seed = 0x1000000UL + (ulong)i;
            int   poi  = i;
            var d = DungeonGenerator.Generate(seed, poi, PoiType.ImperiumRuin, _content);
            AssertAllRoomsReachable(d);
        }
    }

    [Fact]
    public void Generate_Mine_AllRoomsReachable()
    {
        for (int i = 0; i < 10; i++)
        {
            var d = DungeonGenerator.Generate(0x70UL + (ulong)i, i, PoiType.AbandonedMine, _content);
            AssertAllRoomsReachable(d);
        }
    }

    private static void AssertAllRoomsReachable(Dungeon d)
    {
        if (d.Rooms.Length == 0) return;
        var adj = new List<int>[d.Rooms.Length];
        for (int i = 0; i < d.Rooms.Length; i++) adj[i] = new List<int>();
        foreach (var c in d.Connections)
        {
            adj[c.RoomA].Add(c.RoomB);
            adj[c.RoomB].Add(c.RoomA);
        }
        var visited = new bool[d.Rooms.Length];
        var queue   = new Queue<int>();
        queue.Enqueue(0);
        visited[0] = true;
        while (queue.Count > 0)
        {
            int n = queue.Dequeue();
            foreach (int m in adj[n])
                if (!visited[m]) { visited[m] = true; queue.Enqueue(m); }
        }
        for (int i = 0; i < d.Rooms.Length; i++)
            Assert.True(visited[i], $"Room {i} ({d.Rooms[i].TemplateId}) unreachable from Room 0.");
    }

    // ── Scale ────────────────────────────────────────────────────────────

    [Fact]
    public void Generate_RoomCount_StaysWithinLayoutBand()
    {
        // imperium_medium: 6..10 rooms.
        for (int i = 0; i < 10; i++)
        {
            var d = DungeonGenerator.Generate(0xA0UL + (ulong)i, i, PoiType.ImperiumRuin, _content);
            Assert.InRange(d.Rooms.Length,
                C.DUNGEON_MED_ROOMS_MIN,
                C.DUNGEON_MED_ROOMS_MAX);
        }
    }

    [Fact]
    public void Generate_Mine_RoomCount_StaysWithinSmallBand()
    {
        // mine_small: 3..5 rooms.
        for (int i = 0; i < 10; i++)
        {
            var d = DungeonGenerator.Generate(0xB0UL + (ulong)i, i, PoiType.AbandonedMine, _content);
            Assert.InRange(d.Rooms.Length,
                C.DUNGEON_SMALL_ROOMS_MIN,
                C.DUNGEON_SMALL_ROOMS_MAX);
        }
    }

    // ── Budget ───────────────────────────────────────────────────────────

    [Fact]
    public void Generate_CompletesUnderBudget()
    {
        // Under ~400ms even with the worst-case retry-then-linear-fallback
        // for a medium imperium ruin.
        var sw = Stopwatch.StartNew();
        for (int i = 0; i < 10; i++)
        {
            DungeonGenerator.Generate(0xC0UL + (ulong)i, i, PoiType.ImperiumRuin, _content);
        }
        sw.Stop();
        Assert.True(sw.ElapsedMilliseconds < 4000,
            $"10 dungeon gens should complete in <4s (per-gen <400ms target); took {sw.ElapsedMilliseconds}ms.");
    }

    // ── Tile-array sanity ────────────────────────────────────────────────

    [Fact]
    public void Generate_TileArray_HasEntranceStairsDeco()
    {
        var d = DungeonGenerator.Generate(0x12345UL, 1, PoiType.ImperiumRuin, _content);
        var (ex, ey) = d.EntranceTile;
        Assert.InRange(ex, 0, d.W - 1);
        Assert.InRange(ey, 0, d.H - 1);
        Assert.Equal(TacticalDeco.Stairs, d.Tiles[ex, ey].Deco);
    }

    [Fact]
    public void Generate_TileArray_RoomInteriorsAreWalkable()
    {
        var d = DungeonGenerator.Generate(0x9999UL, 1, PoiType.ImperiumRuin, _content);
        // Every room's centre tile should be walkable.
        foreach (var r in d.Rooms)
        {
            int cx = r.AabbX + r.AabbW / 2;
            int cy = r.AabbY + r.AabbH / 2;
            Assert.True(d.Tiles[cx, cy].IsWalkable,
                $"Room {r.Id} ({r.TemplateId}) centre ({cx},{cy}) is not walkable: " +
                $"surface={d.Tiles[cx, cy].Surface} deco={d.Tiles[cx, cy].Deco}");
        }
    }

    [Fact]
    public void Generate_TileArray_PerimeterIsBoundedByWalls()
    {
        var d = DungeonGenerator.Generate(0xDEADUL, 3, PoiType.ImperiumRuin, _content);
        // Outer perimeter (x=0, x=W-1, y=0, y=H-1) should never be walkable
        // — those tiles are the AABB padding, never carved.
        for (int x = 0; x < d.W; x++)
        {
            Assert.False(d.Tiles[x, 0].IsWalkable,         $"top edge ({x},0) walkable");
            Assert.False(d.Tiles[x, d.H - 1].IsWalkable,    $"bottom edge ({x},{d.H - 1}) walkable");
        }
        for (int y = 0; y < d.H; y++)
        {
            Assert.False(d.Tiles[0, y].IsWalkable,          $"left edge (0,{y}) walkable");
            Assert.False(d.Tiles[d.W - 1, y].IsWalkable,    $"right edge ({d.W - 1},{y}) walkable");
        }
    }

    [Fact]
    public void Generate_RequiredRoles_AllPresent()
    {
        // imperium_medium requires entry + boss.
        var d = DungeonGenerator.Generate(0x42UL, 1, PoiType.ImperiumRuin, _content);
        Assert.Contains(d.Rooms, r => r.Role == RoomRole.Entry);
        Assert.Contains(d.Rooms, r => r.Role == RoomRole.Boss);
    }
}
