using Theriapolis.Core.Data;
using Xunit;
namespace Theriapolis.Tests.Dungeons;
///
/// Phase 7 M0 — content-load tests for the room-template + dungeon-layout
/// schema. These run on the actual Content/Data/room_templates/
/// + Content/Data/dungeon_layouts/ directories so a broken
/// authoring edit fails the build.
///
public sealed class RoomTemplateValidationTests
{
private static ContentLoader Loader() => new(TestHelpers.DataDirectory);
[Fact]
public void RoomTemplates_LoadAndValidate()
{
// M0 vertical-slice: 5 imperium + 3 mine + 2 cave = 10 templates.
// Test asserts ≥ 5 to allow content authoring growth without
// modifying this test on every drop.
var rooms = Loader().LoadRoomTemplates();
Assert.True(rooms.Length >= 10,
$"expected ≥10 room templates after Phase 7 M0 vertical slice, got {rooms.Length}");
// Every template must declare at least one role and be one of the
// five known dungeon types.
var validTypes = new HashSet(StringComparer.OrdinalIgnoreCase)
{ "imperium", "mine", "cult", "cave", "overgrown" };
foreach (var r in rooms)
{
Assert.True(validTypes.Contains(r.Type), $"room '{r.Id}' has invalid type '{r.Type}'");
Assert.NotEmpty(r.RolesEligible);
}
}
[Fact]
public void EveryRoomTemplate_HasGridMatchingFootprint()
{
var rooms = Loader().LoadRoomTemplates();
foreach (var r in rooms)
{
Assert.Equal(r.FootprintHTiles, r.Grid.Length);
for (int y = 0; y < r.Grid.Length; y++)
Assert.Equal(r.FootprintWTiles, r.Grid[y].Length);
}
}
[Fact]
public void EveryRoomTemplate_HasIntactPerimeter()
{
var rooms = Loader().LoadRoomTemplates();
foreach (var r in rooms)
{
int w = r.FootprintWTiles, h = r.FootprintHTiles;
for (int x = 0; x < w; x++)
{
Assert.True(IsPerimeterChar(r.Grid[0][x]),
$"room '{r.Id}' top perimeter ({x},0) is '{r.Grid[0][x]}'");
Assert.True(IsPerimeterChar(r.Grid[h - 1][x]),
$"room '{r.Id}' bottom perimeter ({x},{h - 1}) is '{r.Grid[h - 1][x]}'");
}
for (int y = 0; y < h; y++)
{
Assert.True(IsPerimeterChar(r.Grid[y][0]),
$"room '{r.Id}' left perimeter (0,{y}) is '{r.Grid[y][0]}'");
Assert.True(IsPerimeterChar(r.Grid[y][w - 1]),
$"room '{r.Id}' right perimeter ({w - 1},{y}) is '{r.Grid[y][w - 1]}'");
}
}
}
private static bool IsPerimeterChar(char c) => c == '#' || c == 'D' || c == 'S';
[Fact]
public void DungeonLayouts_LoadAndValidate()
{
var loader = Loader();
var rooms = loader.LoadRoomTemplates();
var loot = loader.LoadLootTables(loader.LoadItems());
var layouts = loader.LoadDungeonLayouts(rooms, loot);
// M0 vertical-slice: imperium_medium + mine_small = 2 layouts.
Assert.True(layouts.Length >= 2,
$"expected ≥2 dungeon layouts after Phase 7 M0, got {layouts.Length}");
// Every layout must declare a coherent room-count band.
foreach (var l in layouts)
{
Assert.True(l.RoomCountMin >= 1, $"layout '{l.Id}' room_count_min < 1");
Assert.True(l.RoomCountMax >= l.RoomCountMin, $"layout '{l.Id}' room_count_max < min");
}
}
[Fact]
public void EveryLayout_LootTableReferences_Resolve()
{
var loader = Loader();
var loot = loader.LoadLootTables(loader.LoadItems());
var layouts = loader.LoadDungeonLayouts(loader.LoadRoomTemplates(), loot);
var ids = new HashSet(loot.Select(t => t.Id), StringComparer.OrdinalIgnoreCase);
foreach (var l in layouts)
foreach (var (band, table) in l.LootTablePerBand)
Assert.True(ids.Contains(table),
$"layout '{l.Id}' loot_table_per_band['{band}'] = '{table}' not in loot_tables.json");
}
}