using System.IO;
using AcDream.App.UI;
using AcDream.App.UI.Layout;
using DatReaderWriter;
using DatReaderWriter.Options;
namespace AcDream.App.Tests.UI.Layout;
///
/// TEMPORARY #375 gate-failure probe (Campaign OP connected gate 2,
/// 2026-08-11): the user found the Configure Keyboard screen a visual mess —
/// missing button/tab captions, buttons outside the window, overlapping text —
/// while KeyboardConfigControllerTests stays green against the committed
/// fixture (the #372 structural-false-negative class again). This probe runs
/// the PRODUCTION mount path against the live DATs and dumps per-element
/// rect + caption evidence for each observation. Env-gated like the other
/// live probes so CI/dat-less runs skip it.
///
public sealed class KeyboardConfigLiveMountProbeTests
{
[Fact]
public void ProbeKeyboardLiveMount()
{
if (Environment.GetEnvironmentVariable("ACDREAM_PROBE_LIVE_MOUNT") != "1")
return;
var datDir = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR")
?? Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Documents",
"Asheron's Call");
using var dats = new DatCollection(datDir, DatAccessType.Read);
var strings = new DatStringResolver(dats);
ElementInfo? info = LayoutImporter.ImportInfos(
dats, KeyboardConfigController.LayoutId);
Assert.NotNull(info);
// Production shape A — what MountKeyboardConfig actually built at the
// gate (NO string resolver on the main Build; the template build DOES
// pass one — the asymmetry under investigation).
ImportedLayout withoutStrings = LayoutImporter.Build(
info!, _ => (0u, 0, 0), null);
// Shape B — the same build WITH the resolver every sibling mount passes.
ImportedLayout withStrings = LayoutImporter.Build(
info!, _ => (0u, 0, 0), null, null, strings.Resolve);
Console.WriteLine(
$"[kbprobe] layout root id=0x{info!.Id:X8} rect=({withoutStrings.Root.Left},{withoutStrings.Root.Top} "
+ $"{withoutStrings.Root.Width}x{withoutStrings.Root.Height}) children={withoutStrings.Root.Children.Count}");
// The window-vs-content geometry question (observation 2): the layout
// is an 800x600 retail full-SCREEN — which child is the visible framed
// panel, and what escapes ITS rect?
DumpTreeRects(withoutStrings.Root, 0, maxDepth: 3);
UiElement? windowRoot = withoutStrings.FindElement(
KeyboardConfigController.WindowRootElementId);
Console.WriteLine(windowRoot is null
? "[kbprobe] window root 0x1000001F: MISSING from flat index"
: $"[kbprobe] window root 0x1000001F rect=({windowRoot.Left},{windowRoot.Top} "
+ $"{windowRoot.Width}x{windowRoot.Height}) children={windowRoot.Children.Count} "
+ $"type={windowRoot.GetType().Name}");
// Observation 1+3 — "buttons lacked descriptive text", "no text in
// tabs": dump every UiButton/UiText caption in BOTH shapes.
DumpCaptions("without-strings", withoutStrings.Root);
DumpCaptions("with-strings", withStrings.Root);
// Observation 2 — "some buttons were outside of the window": every
// element whose screen rect escapes the root's own extent.
DumpOutOfBounds(withoutStrings.Root);
// #375 regression pins (live-DAT, env-gated — the fix evidence):
// (a) the same-layout row-template prototypes (0x1000002E header,
// 0x1000002F action row) must NOT build as live elements —
// the parked copies were the "buttons outside the window" /
// "overlapping text" halves of the gate report.
Assert.Null(withStrings.FindElement(0x1000002Eu));
Assert.Null(withStrings.FindElement(0x1000002Fu));
// (b) the resolver-passing build resolves the screen's authored
// captions — the "buttons/tabs lacked text" halves. (Production
// now passes the resolver; the without-strings shape above is
// kept only as the delta record.)
foreach ((uint id, string expected) in new[]
{
(0x1000002Au, "Defaults"),
(0x1000002Bu, "Revert"),
(0x1000002Cu, "OK"),
(0x1000002Du, "Cancel"),
})
{
UiElement? el = withStrings.FindElement(id);
UiButton button = Assert.IsType(el);
Assert.Equal(expected, button.Label);
}
// Observation 4 — "text next to the buttons was overlapping": the row
// template's authored geometry vs the synthesized 260px caption column.
foreach ((string name, uint pageId) in new[]
{
("Movement", 0x1000049Du),
("Camera", 0x1000049Fu),
("Combat", 0x100004A1u),
("UI", 0x100004A3u),
("CharacterSettings", 0x10000211u),
("Emote", 0x100004A5u),
})
{
UiElement? page = UiElement.FindDescendant(withStrings.Root, pageId);
UiElement? lb = page is null ? null : UiElement.FindDescendant(page, 0x10000025u);
Console.WriteLine(
$"[kbprobe] page {name} 0x{pageId:X8} -> {(page is null ? "MISSING" : $"({page.Left},{page.Top} {page.Width}x{page.Height})")} "
+ $"listbox -> {(lb is null ? "MISSING" : $"{lb.GetType().Name} ({lb.Left},{lb.Top} {lb.Width}x{lb.Height})")}");
if (lb is UiTemplateListBox tlb)
{
for (int i = 0; i < tlb.Templates.Count; i++)
{
(uint layoutId, uint elementId) =
(tlb.Templates[i].TemplateLayoutId, tlb.Templates[i].TemplateElementId);
ElementInfo? tInfo = LayoutImporter.ImportInfos(dats, layoutId, elementId);
if (tInfo is null)
{
Console.WriteLine($"[kbprobe] template[{i}] 0x{layoutId:X8}/0x{elementId:X8} -> IMPORT MISSING");
continue;
}
UiElement built = LayoutImporter.Build(
tInfo, _ => (0u, 0, 0), null, null, strings.Resolve).Root;
Console.WriteLine(
$"[kbprobe] template[{i}] 0x{layoutId:X8}/0x{elementId:X8} -> {built.GetType().Name} "
+ $"({built.Left},{built.Top} {built.Width}x{built.Height}) children={built.Children.Count}");
foreach (uint keyBtn in new[] { 0x10000030u, 0x10000031u, 0x10000032u })
{
UiElement? b = UiElement.FindDescendant(built, keyBtn);
if (b is not null)
Console.WriteLine(
$"[kbprobe] key-button 0x{keyBtn:X8} ({b.Left},{b.Top} {b.Width}x{b.Height})");
}
}
}
// Only the first page's templates matter for geometry (all six share
// the same authored list) — stop after one full dump.
if (lb is not null) break;
}
}
private static void DumpCaptions(string tag, UiElement root)
{
Walk(root, el =>
{
string? caption = el switch
{
UiButton b => b.Label,
UiText t => string.Join(
" / ", t.LinesProvider().Select(static l => l.Text)),
_ => null,
};
if (el is UiButton or UiText)
Console.WriteLine(
$"[kbprobe] {tag} 0x{el.EventId:X8} {el.GetType().Name} "
+ $"({el.Left},{el.Top} {el.Width}x{el.Height}) caption='{caption ?? ""}'");
});
}
private static void DumpOutOfBounds(UiElement root)
{
Walk(root, el =>
{
var p = el.ScreenPosition;
bool outside = p.X < root.Left - 0.5f || p.Y < root.Top - 0.5f
|| p.X + el.Width > root.Left + root.Width + 0.5f
|| p.Y + el.Height > root.Top + root.Height + 0.5f;
if (outside)
Console.WriteLine(
$"[kbprobe] OUT-OF-BOUNDS 0x{el.EventId:X8} {el.GetType().Name} "
+ $"screen=({p.X},{p.Y} {el.Width}x{el.Height}) rootExtent={root.Width}x{root.Height}");
});
}
private static void Walk(UiElement el, Action visit)
{
visit(el);
foreach (UiElement c in el.Children)
Walk(c, visit);
}
private static void DumpTreeRects(UiElement el, int depth, int maxDepth)
{
Console.WriteLine(
$"[kbprobe] tree {new string(' ', depth * 2)}{el.GetType().Name} "
+ $"({el.Left},{el.Top} {el.Width}x{el.Height}) children={el.Children.Count}");
if (depth >= maxDepth) return;
foreach (UiElement c in el.Children)
DumpTreeRects(c, depth + 1, maxDepth);
}
}