using System.Numerics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Tests.Rendering.Gpu;
using AcDream.App.UI;
using AcDream.App.UI.Layout;
namespace AcDream.App.Tests.UI.Layout;
public class UiDatElementTests
{
private sealed class NullGpuFrameSource : ICurrentGpuFrameSource
{
public IGpuFrame? CurrentFrame => null;
}
private static (TextRenderer renderer, UiRenderContext ctx) BuildRenderContext()
{
var device = new RecordingGpuDevice();
var renderer = new TextRenderer(device, new NullGpuFrameSource(), "unused");
renderer.Begin(new Vector2(1920f, 1080f));
var ctx = new UiRenderContext(renderer, new Vector2(1920f, 1080f));
return (renderer, ctx);
}
///
/// Campaign LA gate round 2: the char-select root's background (native 800x600,
/// resolved from a JPEG surface) was drawn with the ordinary UiDatElement TILE
/// UV formula (u1 = Width/tw) after CharacterManagementUiController resized the
/// root to the live viewport — at 1920x1080 that produces u1 = 2.4, v1 = 1.8,
/// which GL_REPEAT wraps into a visibly tiled background instead of one stretched
/// image. See 's doc comment
/// for the retail mechanism this substitutes.
///
[Fact]
public void StretchOwnBackgroundToFill_False_TilesUvPastOne_WhenRectExceedsNativeSize()
{
var info = new ElementInfo { Width = 1920, Height = 1080 };
info.StateMedia[""] = (0x06007576u, 1);
var e = new UiDatElement(info, _ => (7u, 800, 600))
{
Left = 0,
Top = 0,
Width = 1920,
Height = 1080,
};
(TextRenderer renderer, UiRenderContext ctx) = BuildRenderContext();
e.DrawSelfAndChildren(ctx);
var (texture, verts) = Assert.Single(renderer.DebugSpriteSegmentVerts);
Assert.Equal(7u, texture);
// Vertex layout: x,y,u,v,r,g,b,a, 6 verts/quad. TextRenderer.AppendQuad emits
// vertex index 1 as (x+w, y+h, u1, v1) — the (u1,v1) far corner.
float uMax = verts[1 * 8 + 2];
float vMax = verts[1 * 8 + 3];
Assert.Equal(1920f / 800f, uMax, 3);
Assert.Equal(1080f / 600f, vMax, 3);
}
///
/// Campaign LA gate round 2 fix: with the flag set, the SAME oversized rect draws
/// as one quad spanning UV 0..1 — a single stretched image, matching retail's
/// observed (never-tiled) char-select background.
///
[Fact]
public void StretchOwnBackgroundToFill_True_ClampsUvToOne_WhenRectExceedsNativeSize()
{
var info = new ElementInfo { Width = 1920, Height = 1080 };
info.StateMedia[""] = (0x06007576u, 1);
var e = new UiDatElement(info, _ => (7u, 800, 600))
{
Left = 0,
Top = 0,
Width = 1920,
Height = 1080,
StretchOwnBackgroundToFill = true,
};
(TextRenderer renderer, UiRenderContext ctx) = BuildRenderContext();
e.DrawSelfAndChildren(ctx);
var (texture, verts) = Assert.Single(renderer.DebugSpriteSegmentVerts);
Assert.Equal(7u, texture);
float uMax = verts[1 * 8 + 2];
float vMax = verts[1 * 8 + 3];
Assert.Equal(1f, uMax, 3);
Assert.Equal(1f, vMax, 3);
}
///
/// The flag must not change anything for an element whose rect already matches
/// its native texture size (every ordinary panel/window root today) — stretch
/// (UV 0..1) and tile (UV Width/tw) are numerically identical at that size, so
/// this only changes behavior for elements deliberately grown past their art.
///
[Fact]
public void StretchOwnBackgroundToFill_True_MatchesTile_WhenRectEqualsNativeSize()
{
var info = new ElementInfo { Width = 800, Height = 600 };
info.StateMedia[""] = (0x06007576u, 1);
var e = new UiDatElement(info, _ => (7u, 800, 600))
{
Left = 0,
Top = 0,
Width = 800,
Height = 600,
StretchOwnBackgroundToFill = true,
};
(TextRenderer renderer, UiRenderContext ctx) = BuildRenderContext();
e.DrawSelfAndChildren(ctx);
var (_, verts) = Assert.Single(renderer.DebugSpriteSegmentVerts);
Assert.Equal(1f, verts[1 * 8 + 2], 3);
Assert.Equal(1f, verts[1 * 8 + 3], 3);
}
[Fact]
public void ActiveMedia_PrefersNamedStateOverDirect()
{
var info = new ElementInfo();
info.StateMedia[""] = (0x06000001, 1); // DirectState (DrawMode Normal=1)
info.StateMedia["ShowDetail"] = (0x06000002, 3); // named (Alphablend=3)
var e = new UiDatElement(info, _ => (0, 0, 0)) { ActiveState = "ShowDetail" };
Assert.Equal(0x06000002u, e.ActiveMedia().File);
Assert.Equal(3, e.ActiveMedia().DrawMode);
e.ActiveState = "";
Assert.Equal(0x06000001u, e.ActiveMedia().File);
Assert.Equal(1, e.ActiveMedia().DrawMode);
}
[Fact]
public void ActiveMedia_NoMedia_ReturnsZero()
{
var e = new UiDatElement(new ElementInfo(), _ => (0, 0, 0));
Assert.Equal(0u, e.ActiveMedia().File);
Assert.Equal(0, e.ActiveMedia().DrawMode);
}
[Fact]
public void ActiveMedia_MissingNamedState_FallsBackToDirect()
{
var info = new ElementInfo();
info.StateMedia[""] = (0x06000005, 1);
var e = new UiDatElement(info, _ => (0, 0, 0)) { ActiveState = "NoSuchState" };
Assert.Equal(0x06000005u, e.ActiveMedia().File);
}
// ── G1 tests: DefaultStateName + "Normal" implicit default ───────────────
///
/// Task G1 change 5: when an element has no DefaultStateName but does have a "Normal"
/// state sprite, the ctor should default ActiveState to "Normal" so the element
/// renders its normal-state sprite without requiring explicit state assignment.
///
[Fact]
public void UiDatElement_DefaultsActiveStateToNormal_WhenNormalPresent()
{
var info = new ElementInfo();
info.StateMedia["Normal"] = (0x0000AAAAu, 1);
info.StateMedia["Hover"] = (0x0000BBBBu, 1);
var e = new UiDatElement(info, _ => (0, 0, 0));
// Should have defaulted to "Normal" state.
Assert.Equal(0x0000AAAAu, e.ActiveMedia().File);
}
///
/// Task G1 change 5: when DefaultStateName is set (e.g. "Minimized"),
/// it takes priority over the "Normal" implicit default.
///
[Fact]
public void UiDatElement_DefaultsActiveStateToDefaultStateName_WhenSet()
{
var info = new ElementInfo { DefaultStateName = "Minimized" };
info.StateMedia["Minimized"] = (0x0000BBBBu, 1);
info.StateMedia["Maximized"] = (0x0000CCCCu, 1);
info.StateMedia["Normal"] = (0x0000DDDDu, 1);
var e = new UiDatElement(info, _ => (0, 0, 0));
// DefaultStateName "Minimized" wins over "Normal" implicit default.
Assert.Equal(0x0000BBBBu, e.ActiveMedia().File);
}
///
/// Task G1 change 5: elements with only a DirectState sprite and no "Normal" state
/// should still default to "" (DirectState) — no regression for chrome/grip elements.
///
[Fact]
public void UiDatElement_NoDefaultStateName_NoNormal_DefaultsToDirectState()
{
var info = new ElementInfo();
info.StateMedia[""] = (0x06007777u, 1); // DirectState only (e.g. vitals chrome corner)
var e = new UiDatElement(info, _ => (0, 0, 0));
// No DefaultStateName, no "Normal" state → ActiveState stays "" (DirectState).
Assert.Equal(0x06007777u, e.ActiveMedia().File);
}
[Fact]
public void NumericStateBridge_SetsNamedStateWithoutChangingGeometry()
{
var info = new ElementInfo { X = 4, Y = 5, Width = 30, Height = 40 };
info.States[RetailUiStateIds.ShowDetail] = new UiStateInfo
{
Id = RetailUiStateIds.ShowDetail,
Name = "ShowDetail",
Image = new UiImageMedia(0x06000009u, 3),
};
info.StateMedia["ShowDetail"] = (0x06000009u, 3);
var element = new UiDatElement(info, _ => (0u, 0, 0))
{
Left = info.X,
Top = info.Y,
Width = info.Width,
Height = info.Height,
};
Assert.True(element.TrySetRetailState(RetailUiStateIds.ShowDetail));
Assert.Equal(RetailUiStateIds.ShowDetail, element.ActiveRetailStateId);
Assert.Equal((0x06000009u, 3), element.ActiveMedia());
Assert.Equal((4f, 5f, 30f, 40f),
(element.Left, element.Top, element.Width, element.Height));
}
[Fact]
public void NumericStateBridge_MissingStatePreservesCurrentState()
{
var info = new ElementInfo { DefaultStateName = "Normal" };
info.StateMedia["Normal"] = (0x06000001u, 1);
var element = new UiDatElement(info, _ => (0u, 0, 0));
Assert.False(element.TrySetRetailState(RetailUiStateIds.ShowDetail));
Assert.Equal("Normal", element.ActiveState);
}
}