using AcDream.App.Studio;
using AcDream.App.UI;
using AcDream.App.UI.Layout;
namespace AcDream.App.Tests.UI.Layout;
///
/// Unit tests for — the Attributes-tab controller that
/// binds the REAL importer-mounted header + 9-row list + footer elements (no created overlay).
/// Pure data wiring against fake layouts; no dats, no GL.
///
public class CharacterStatControllerTests
{
// ── Header labels bind to the sheet ──────────────────────────────────────
[Fact]
public void Bind_SetsNameLabel()
{
var name = new UiText();
var layout = Fake((CharacterStatController.NameId, name));
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
Assert.Equal("Studio Player", name.LinesProvider()[0].Text);
}
[Fact]
public void Bind_SetsLevelLabel()
{
var level = new UiText();
var layout = Fake((CharacterStatController.LevelId, level));
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
Assert.Equal("126", level.LinesProvider()[0].Text);
}
[Fact]
public void Bind_SetsHeritageAndPkLabels()
{
var heritage = new UiText();
var pk = new UiText();
var layout = Fake((CharacterStatController.HeritageId, heritage),
(CharacterStatController.PkStatusId, pk));
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
Assert.Equal("Aluvian Heritage", heritage.LinesProvider()[0].Text);
Assert.Equal("Non-Player Killer", pk.LinesProvider()[0].Text);
}
// ── XP meter fill ────────────────────────────────────────────────────────
[Fact]
public void Bind_SetsXpMeterFill()
{
var meter = new UiMeter();
var layout = Fake((CharacterStatController.XpMeterId, meter));
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
var fill = meter.Fill();
Assert.NotNull(fill);
Assert.True(System.MathF.Abs(fill!.Value - 0.63f) < 0.001f, $"expected ~0.63, got {fill}");
}
// ── Attribute list — 9 rows in list box 0x1000023D ───────────────────────
/// Bind adds exactly 9 UiPanel row containers to the list box.
[Fact]
public void Bind_AttributeList_Has9Rows()
{
var list = new UiPanel();
var layout = Fake((CharacterStatController.ListBoxId, list));
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
var rows = list.Children.OfType().ToList();
Assert.Equal(9, rows.Count);
}
///
/// Every row must have a right-aligned value UiText child (the last UiText in
/// the row), confirming the RightAligned path was wired.
///
[Fact]
public void Bind_AttributeList_EachRowHasRightAlignedValueLabel()
{
var list = new UiPanel();
var layout = Fake((CharacterStatController.ListBoxId, list));
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
var rows = list.Children.OfType().ToList();
Assert.Equal(9, rows.Count);
foreach (var row in rows)
{
// Last UiText in each row is the value (right-aligned).
var texts = row.Children.OfType().ToList();
Assert.True(texts.Count >= 2, "each row must have at least name + value UiText");
var valueEl = texts[^1]; // last = value
Assert.True(valueEl.RightAligned, "value label must be RightAligned");
}
}
///
/// Retail display order: Strength, Endurance, Coordination, Quickness, Focus, Self,
/// Health, Stamina, Mana (spec §1). Name is the SECOND UiText child of each row.
///
[Fact]
public void Bind_AttributeList_RowNamesInRetailOrder()
{
var list = new UiPanel();
var layout = Fake((CharacterStatController.ListBoxId, list));
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
var rows = list.Children.OfType().ToList();
Assert.Equal(9, rows.Count);
string[] expectedNames =
{
"Strength", "Endurance", "Coordination", "Quickness", "Focus", "Self",
"Health", "Stamina", "Mana",
};
for (int i = 0; i < 9; i++)
{
// Name UiText = second UiText in the row (after the icon UiText).
var texts = rows[i].Children.OfType().ToList();
Assert.True(texts.Count >= 2, $"row {i} must have name + value");
string rowName = texts[1].LinesProvider()[0].Text; // index 1 = name (0 = icon)
Assert.Equal(expectedNames[i], rowName);
}
}
///
/// Attribute row values are plain integers; vital row values are "cur/max".
/// Validates against SampleData fixture: Str=200, Quick=200, rest=10;
/// Health=5/5, Stamina=10/10, Mana=10/10.
///
[Fact]
public void Bind_AttributeList_RowValues_AttributeIntegersAndVitalsCurMax()
{
var list = new UiPanel();
var layout = Fake((CharacterStatController.ListBoxId, list));
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
var rows = list.Children.OfType().ToList();
// Helper: value text is the last UiText in the row.
string ValueOf(UiPanel row) => row.Children.OfType().ToList()[^1].LinesProvider()[0].Text;
// Attribute rows 0-5.
Assert.Equal("200", ValueOf(rows[0])); // Strength
Assert.Equal("10", ValueOf(rows[1])); // Endurance
Assert.Equal("10", ValueOf(rows[2])); // Coordination
Assert.Equal("200", ValueOf(rows[3])); // Quickness
Assert.Equal("10", ValueOf(rows[4])); // Focus
Assert.Equal("10", ValueOf(rows[5])); // Self
// Vital rows 6-8: "cur/max" format.
Assert.Equal("5/5", ValueOf(rows[6])); // Health
Assert.Equal("10/10", ValueOf(rows[7])); // Stamina
Assert.Equal("10/10", ValueOf(rows[8])); // Mana
}
///
/// When spriteResolve is provided the icon UiText must have BackgroundSprite set
/// to the correct DataID; when null the BackgroundSprite must be 0.
///
[Fact]
public void Bind_AttributeList_IconHasBackgroundSpriteWhenResolverProvided()
{
var list = new UiPanel();
var layout = Fake((CharacterStatController.ListBoxId, list));
// Non-null resolver — just returns a fake handle for any id.
CharacterStatController.Bind(layout, SampleData.SampleCharacter,
spriteResolve: id => (id, 16, 16)); // fake: handle == id
var rows = list.Children.OfType().ToList();
Assert.Equal(9, rows.Count);
// First icon = Strength row; expected DataID 0x060002C8.
var iconEl = rows[0].Children.OfType().First();
Assert.Equal(0x060002C8u, iconEl.BackgroundSprite);
// Vital (Health) = row 6; expected DataID 0x06004C3B.
var healthIcon = rows[6].Children.OfType().First();
Assert.Equal(0x06004C3Bu, healthIcon.BackgroundSprite);
}
[Fact]
public void Bind_AttributeList_IconBackgroundSprite_ZeroWhenNoResolver()
{
var list = new UiPanel();
var layout = Fake((CharacterStatController.ListBoxId, list));
CharacterStatController.Bind(layout, SampleData.SampleCharacter, spriteResolve: null);
var rows = list.Children.OfType().ToList();
Assert.Equal(9, rows.Count);
foreach (var row in rows)
{
var iconEl = row.Children.OfType().First();
Assert.Equal(0u, iconEl.BackgroundSprite);
}
}
// ── Footer State A ────────────────────────────────────────────────────────
[Fact]
public void Bind_FooterStateA_TitleEmpty()
{
var title = new UiText();
var layout = Fake((CharacterStatController.FooterTitleId, title));
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
Assert.Equal(string.Empty, title.LinesProvider()[0].Text);
}
[Fact]
public void Bind_FooterStateA_Line1LabelEmpty()
{
var lbl = new UiText();
var layout = Fake((CharacterStatController.FooterLine1Label, lbl));
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
Assert.Equal(string.Empty, lbl.LinesProvider()[0].Text);
}
[Fact]
public void Bind_FooterStateA_Line1ValueIsSelectPrompt()
{
var val = new UiText();
var layout = Fake((CharacterStatController.FooterLine1Value, val));
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
Assert.Equal("Select an Attribute to Improve", val.LinesProvider()[0].Text);
}
[Fact]
public void Bind_FooterStateA_Line2LabelEmpty()
{
var lbl = new UiText();
var layout = Fake((CharacterStatController.FooterLine2Label, lbl));
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
Assert.Equal(string.Empty, lbl.LinesProvider()[0].Text);
}
[Fact]
public void Bind_FooterStateA_Line2ValueIsSkillCredits()
{
var val = new UiText();
var layout = Fake((CharacterStatController.FooterLine2Value, val));
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
// SampleData.SampleCharacter().SkillCredits == 96
Assert.Equal("96", val.LinesProvider()[0].Text);
}
// ── SkillCredits propagation through SampleData ───────────────────────────
[Fact]
public void SampleCharacter_SkillCredits_Is96()
{
Assert.Equal(96, SampleData.SampleCharacter().SkillCredits);
}
// ── UiText.RightAligned — unit-level draw mode flag ──────────────────────
[Fact]
public void UiText_RightAligned_DefaultFalse()
{
var t = new UiText();
Assert.False(t.RightAligned);
}
[Fact]
public void UiText_RightAligned_CanBeSetTrue()
{
var t = new UiText { RightAligned = true };
Assert.True(t.RightAligned);
}
// ── Robustness: a partial layout (missing ids) must not throw ────────────
[Fact]
public void Bind_MissingElements_DoesNotThrow()
=> CharacterStatController.Bind(Fake(), SampleData.SampleCharacter);
// ── Helper ─────────────────────────────────────────────────────────────
private static ImportedLayout Fake(params (uint id, UiElement e)[] items)
{
var dict = new Dictionary();
var root = new UiPanel();
foreach (var (id, e) in items)
{
root.AddChild(e);
dict[id] = e;
}
return new ImportedLayout(root, dict);
}
}