using System.Numerics;
using AcDream.App.UI;
using AcDream.App.UI.Layout;
namespace AcDream.App.Tests.UI.Layout;
///
/// Campaign CC gate round 1 Batch C: unit tests for the shared
/// word-wrap + per-segment-color helper feeding GF-2/GF-3/GF-11a and the
/// Summary how-to text (Commit 3). Escape decoding moved to the string
/// source in the 2026-08-17 systemic round (DatStringResolver →
/// RetailStringEscapes) — segments reach Compose with real line breaks.
///
public class DatRichTextTests
{
private static readonly Vector4 White = Vector4.One;
private static readonly Vector4 Green = new(0f, 1f, 0f, 1f);
private static UiText MakeTarget(float width) =>
new() { Width = width, Height = 200f };
/// Segments arrive source-decoded (real '\n'); Compose keeps
/// the authored break as a line split. A literal backslash-n pair in a
/// segment must stay VERBATIM — re-decoding here is the double-decode
/// hazard the 2026-08-17 round retired.
[Fact]
public void Compose_SplitsOnRealNewlines_AndKeepsLiteralPairsVerbatim()
{
UiText target = MakeTarget(1000f); // wide enough that nothing wraps
var segments = new[]
{
new DatRichText.Segment("line one\nliteral \\n stays", White),
};
var lines = DatRichText.Compose(target, segments);
Assert.Equal(2, lines.Count);
Assert.Equal("line one", lines[0].Text);
Assert.Equal("literal \\n stays", lines[1].Text);
}
[Fact]
public void Compose_WordWrapsToTheTargetWidth()
{
// Bitmap-font-shaped measure: 8px/char, matching BuildText's own
// authored-string fallback measure.
UiText target = MakeTarget(80f); // 10 chars per line at 8px/char
var segments = new[]
{
new DatRichText.Segment("one two three four five six seven eight", White),
};
var lines = DatRichText.Compose(target, segments);
Assert.True(lines.Count > 1, "a long segment must wrap to more than one line");
foreach (UiText.Line line in lines)
Assert.True(line.Text.Length * 8f <= 80f, $"line '{line.Text}' overflowed the target width");
}
[Fact]
public void Compose_EachSegmentKeepsItsOwnColorAcrossItsWrappedLines()
{
UiText target = MakeTarget(1000f);
var segments = new[]
{
new DatRichText.Segment("Header:", Green),
new DatRichText.Segment("Body text.", White),
};
var lines = DatRichText.Compose(target, segments);
Assert.Equal(2, lines.Count);
Assert.Equal(Green, lines[0].Color);
Assert.Equal(White, lines[1].Color);
}
[Fact]
public void Compose_NullOrEmptySegmentText_IsSkipped()
{
UiText target = MakeTarget(1000f);
var segments = new[]
{
new DatRichText.Segment(null, White),
new DatRichText.Segment(string.Empty, White),
new DatRichText.Segment("real text", White),
};
var lines = DatRichText.Compose(target, segments);
Assert.Single(lines);
Assert.Equal("real text", lines[0].Text);
}
[Fact]
public void Compose_NoSeparatorInsertedBetweenSegments()
{
// Retail's own composition calls concatenate directly
// (AppendStringInfoWithFont / append_n_chars, no interposed
// literal) — this helper must not invent one either.
UiText target = MakeTarget(1000f);
var segments = new[]
{
new DatRichText.Segment("first", White),
new DatRichText.Segment("second", White),
};
var lines = DatRichText.Compose(target, segments);
// Each segment still wraps independently (so "first"/"second" stay
// on separate output lines, not glued into "firstsecond") — but no
// BLANK line is inserted between them unless the segment's own
// text carried one.
Assert.Equal(2, lines.Count);
Assert.Equal("first", lines[0].Text);
Assert.Equal("second", lines[1].Text);
}
[Fact]
public void Compose_WordWrapsToTheTargetWidth_MinusTheFourRetailMargins()
{
// R2-1 (Campaign CC gate round 1 Batch E): the wrap width must
// shrink by BOTH Padding and the four retail margins (properties
// 0x23-0x26 — MarginLeft's own doc comment on UiText), not just the
// element's raw Width. A 100px-wide box with margL=10/margR=10
// leaves only 80px of usable width — one 10-char/8px-per-char word
// ("aaaaaaaaaa", 80px) must fit on one line, but appending an 11th
// 'a' (88px) must force a wrap.
UiText fits = new() { Width = 100f, Height = 200f, MarginLeft = 10f, MarginRight = 10f };
var fitsLines = DatRichText.Compose(fits, [new DatRichText.Segment("aaaaaaaaaa", White)]);
Assert.Single(fitsLines);
UiText overflows = new() { Width = 100f, Height = 200f, MarginLeft = 10f, MarginRight = 10f };
var overflowLines = DatRichText.Compose(overflows, [new DatRichText.Segment("aaaaaaaaaaa", White)]);
Assert.True(overflowLines.Count > 1, "an 88px word in an 80px content width must wrap");
}
[Fact]
public void PaletteColor_ReturnsAuthoredPaletteEntry_WhenPresent()
{
UiText target = new()
{
FontColorPalette = [White, Green],
};
Assert.Equal(White, DatRichText.PaletteColor(target, 0, Green));
Assert.Equal(Green, DatRichText.PaletteColor(target, 1, White));
}
[Fact]
public void PaletteColor_FallsBack_WhenPaletteTooShortOrMissing()
{
UiText target = new(); // empty palette
Assert.Equal(Green, DatRichText.PaletteColor(target, 1, Green));
Assert.Equal(Green, DatRichText.PaletteColor(target, -1, Green));
}
}