using AcDream.App.Studio;
using AcDream.App.UI;
namespace AcDream.App.Tests.Studio;
///
/// Tests for — parsing the retail UI dump JSON and
/// building a tree from it.
///
/// These tests load the real dump file from the source tree
/// (docs/research/2026-06-25-retail-ui-layout-dump.json). The test
/// skips cleanly when the file is absent (should not happen in a normal dev
/// checkout, but guards against stripped CI machines).
///
public class DumpLayoutTests
{
private static string DumpPath()
{
// Walk up from the test output directory to the solution root,
// mirroring ConformanceDats.SolutionRoot().
var dir = AppContext.BaseDirectory;
while (!string.IsNullOrEmpty(dir))
{
if (File.Exists(Path.Combine(dir, "AcDream.slnx")))
return Path.Combine(dir, "docs", "research",
"2026-06-25-retail-ui-layout-dump.json");
dir = Path.GetDirectoryName(dir);
}
// Fallback: try a relative path (won't find it but skip rather than throw)
return Path.Combine(AppContext.BaseDirectory,
"docs", "research", "2026-06-25-retail-ui-layout-dump.json");
}
private static (uint, int, int) NoTex(uint _) => (1u, 1, 1);
// ── Helpers ──────────────────────────────────────────────────────────
/// Depth-first search for an element with the given EventId.
private static UiElement? FindById(UiElement root, uint id)
{
if (root.EventId == id) return root;
foreach (var c in root.Children)
{
var found = FindById(c, id);
if (found is not null) return found;
}
return null;
}
/// Count the total elements in the tree (self + all descendants).
private static int CountAll(UiElement root)
{
int n = 1;
foreach (var c in root.Children) n += CountAll(c);
return n;
}
// ── Tests ─────────────────────────────────────────────────────────────
///
/// Loading the "inventory" slug should succeed and the returned tree should
/// contain an element with EventId == 0x100001D5 (the doll viewport node)
/// and at least 40 elements in total.
///
[Fact]
public void Load_Inventory_ReturnsTreeWithDollViewport()
{
var path = DumpPath();
if (!File.Exists(path))
return; // Skip: dump not available.
var root = DumpLayout.Load(path, "inventory", NoTex, out var err);
Assert.NotNull(root);
Assert.Null(err);
// The doll viewport element must appear somewhere in the tree.
const uint dollViewportId = 0x100001D5u;
var found = FindById(root!, dollViewportId);
Assert.NotNull(found);
// The full tree must be reasonably deep — 59 dump nodes → >= 40 elements.
int total = CountAll(root!);
Assert.True(total >= 40,
$"Expected >= 40 elements in inventory tree; got {total}");
}
///
/// Loading an unknown slug must return null and a non-empty error string.
///
[Fact]
public void Load_UnknownSlug_ReturnsNullWithError()
{
var path = DumpPath();
if (!File.Exists(path))
return; // Skip.
var root = DumpLayout.Load(path, "this_slug_does_not_exist", NoTex, out var err);
Assert.Null(root);
Assert.NotNull(err);
Assert.NotEmpty(err!);
}
///
/// The root element's Left/Top should be (0,0) (the panel's rect offset has
/// been stripped so the tree sits at the window origin), and its Width/Height
/// should match the dump panel dimensions.
///
[Fact]
public void Load_Inventory_RootAtOrigin()
{
var path = DumpPath();
if (!File.Exists(path))
return; // Skip.
var root = DumpLayout.Load(path, "inventory", NoTex, out _);
Assert.NotNull(root);
// Root always placed at (0,0) by DumpLayout (origin of the UiHost).
Assert.Equal(0f, root!.Left);
Assert.Equal(0f, root.Top);
// Width/Height come from the panel record in the dump.
Assert.True(root.Width > 0, "Root width must be > 0");
Assert.True(root.Height > 0, "Root height must be > 0");
}
///
/// Children must use parent-relative coordinates (the dump rects are absolute;
/// DumpLayout subtracts the parent rect to produce parent-local offsets).
/// Verify that at least the direct children of the root have Left/Top values
/// that are NOT equal to the absolute rect they had in the dump (since the root
/// was at x>0 in screen space but we place it at 0,0).
///
[Fact]
public void Load_Inventory_ChildrenAreParentRelative()
{
var path = DumpPath();
if (!File.Exists(path))
return; // Skip.
var root = DumpLayout.Load(path, "inventory", NoTex, out _);
Assert.NotNull(root);
// If the dump has children at absolute x>=500 but the root is at 0,
// a correct parent-relative placement will give children x < 500.
// (The inventory panel root is at absolute x=500; children in the dump
// also start at x=500 — after subtraction they should land near x=0.)
bool anyChildAtAbsoluteX = false;
foreach (var child in root!.Children)
{
if (child.Left >= 490f) // would indicate absolute not relative
{
anyChildAtAbsoluteX = true;
break;
}
}
Assert.False(anyChildAtAbsoluteX,
"Children appear to have absolute coords (Left >= 490) — " +
"DumpLayout must subtract the parent rect.");
}
///
/// Every panel slug known in the dump must load without error.
/// This is a smoke test that the JSON parse + tree build does not
/// crash on any of the 26 panels.
///
[Fact]
public void Load_AllSlugs_Succeed()
{
var path = DumpPath();
if (!File.Exists(path))
return; // Skip.
var slugs = UiDumpModel.ListSlugs(path);
Assert.True(slugs.Count >= 20,
$"Expected >= 20 panel slugs in dump; got {slugs.Count}");
foreach (var slug in slugs)
{
var root = DumpLayout.Load(path, slug, NoTex, out var err);
Assert.True(root is not null || err is not null,
$"Load('{slug}') returned both null root AND null error — one must be set.");
if (root is null)
Assert.Fail($"Slug '{slug}' failed with error: {err}");
}
}
}