using System.Collections.Generic;
using System.Linq;
using AcDream.App.UI;
using AcDream.App.UI.Layout;
using AcDream.Core.Items;
namespace AcDream.App.Tests.UI.Layout;
///
/// Slice 5.4 — mirrors 's
/// harness shape (hand-built over
/// ) for the behavioral suite, plus one
/// real-dat-fixture smoke test (mirroring
/// AppraisalUiControllerTests's use of )
/// that catches drift between the hardcoded ids in
/// and the actual LayoutDesc 0x21000012.
///
public sealed class VendorUiControllerTests
{
private const uint VendorGuid = 0x70000010u;
private const uint ArmorItemGuid = 0x60000101u;
private const uint FoodItemGuid = 0x60000102u;
private const uint StackedItemGuid = 0x60000103u;
private sealed class TestElement : UiElement { }
[Fact]
public void Bind_FromRealDatFixture_ResolvesAllRequiredControls()
{
ImportedLayout layout = FixtureLoader.LoadVendor();
var screen = new UiRoot { Width = 1280f, Height = 800f };
RetailWindowHandle window = RetailWindowFrame.Mount(
screen,
layout.Root,
static _ => (0u, 0, 0),
new RetailWindowFrame.Options
{
WindowName = "vendor-fixture-smoke",
Chrome = RetailWindowChrome.Imported,
Visible = false,
});
VendorUiController? controller = VendorUiController.Bind(
layout,
new VendorState(),
window,
static (_, _, _, _, _) => 0u);
Assert.NotNull(controller);
}
private sealed class Harness
{
public readonly VendorState State = new();
public readonly UiRoot Screen = new() { Width = 800f, Height = 600f };
public readonly UiItemList ItemList = new();
public readonly UiScrollbar ItemScrollbar = new();
public readonly UiMenu TypeMenu = new();
public readonly UiText ItemNameText = new();
public readonly UiText ItemCostText = new();
public readonly UiText ItemsTab = new();
public readonly UiText BuyingTab = new();
public readonly UiText SellingTab = new();
public readonly UiElement ItemsPage = new TestElement();
public readonly UiElement BuyingPage = new TestElement();
public readonly UiElement SellingPage = new TestElement();
public readonly UiButton CloseButton;
public readonly RetailWindowHandle Window;
public readonly VendorUiController Controller;
public Harness()
{
var root = new TestElement { Width = 800f, Height = 110f };
CloseButton = new UiButton(
new ElementInfo { Id = VendorUiController.CloseId, Type = 1 },
static _ => (0u, 0, 0));
root.AddChild(CloseButton);
root.AddChild(ItemsTab);
root.AddChild(BuyingTab);
root.AddChild(SellingTab);
root.AddChild(ItemsPage);
root.AddChild(BuyingPage);
root.AddChild(SellingPage);
ItemsPage.AddChild(ItemList);
ItemsPage.AddChild(ItemScrollbar);
ItemsPage.AddChild(TypeMenu);
ItemsPage.AddChild(ItemNameText);
ItemsPage.AddChild(ItemCostText);
var layout = new ImportedLayout(root, new Dictionary
{
[VendorUiController.CloseId] = CloseButton,
[VendorUiController.ItemsTabId] = ItemsTab,
[VendorUiController.BuyingTabId] = BuyingTab,
[VendorUiController.SellingTabId] = SellingTab,
[VendorUiController.ItemsPageId] = ItemsPage,
[VendorUiController.BuyingPageId] = BuyingPage,
[VendorUiController.SellingPageId] = SellingPage,
[VendorUiController.ItemListId] = ItemList,
[VendorUiController.ItemScrollbarId] = ItemScrollbar,
[VendorUiController.TypeFilterMenuId] = TypeMenu,
[VendorUiController.ItemNameTextId] = ItemNameText,
[VendorUiController.ItemCostTextId] = ItemCostText,
});
Window = RetailWindowFrame.Mount(
Screen,
root,
static _ => (0u, 0, 0),
new RetailWindowFrame.Options
{
WindowName = WindowNames.Vendor,
Chrome = RetailWindowChrome.Imported,
Visible = false,
Draggable = false,
Resizable = false,
});
Controller = VendorUiController.Bind(
layout,
State,
Window,
static (_, iconId, _, _, _) => iconId != 0u ? iconId + 1000u : 0u)!;
Screen.WindowManager.AttachController(WindowNames.Vendor, Controller);
}
}
private static VendorShopProfile Profile(
float sellRate = 1.5f, uint altCurrency = 0u, string altName = "") =>
new(0u, 0u, 0u, false, 1.0f, sellRate, altCurrency, 0u, altName);
private static string GetText(UiText text)
=> string.Concat(text.LinesProvider().Select(line => line.Text));
[Fact]
public void Bind_WiresTheScrollbarToTheItemListsScrollModel()
{
var h = new Harness();
Assert.True(h.ItemScrollbar.Horizontal);
Assert.Same(h.ItemList.Scroll, h.ItemScrollbar.Model);
}
[Fact]
public void Opened_ShowsWindowAndPopulatesFirstPresentCategoryInTableOrder()
{
var h = new Harness();
var items = new[]
{
// Food is table entry #5, Armor is #1 — retail's fixed table
// order, not insertion order, decides the default selection.
new VendorShopItem(FoodItemGuid, -1, 1u, "Bread", (uint)ItemType.Food, 100u, 5),
new VendorShopItem(ArmorItemGuid, -1, 2u, "Chainmail", (uint)ItemType.Armor, 200u, 500),
};
h.State.Apply(VendorGuid, Profile(), items);
Assert.True(h.Window.IsVisible);
Assert.True(h.ItemsPage.Visible);
Assert.False(h.BuyingPage.Visible);
Assert.False(h.SellingPage.Visible);
Assert.Equal(1, h.ItemList.GetNumUIItems());
Assert.Equal(ArmorItemGuid, h.ItemList.GetItem(0)!.ItemId);
Assert.Equal("Armor", h.TypeMenu.Items.Single(i => Equals(i.Payload, h.TypeMenu.Selected)).Label);
}
[Fact]
public void Closed_HidesWindowAndClearsListAndText()
{
var h = new Harness();
h.State.Apply(VendorGuid, Profile(), new[]
{
new VendorShopItem(ArmorItemGuid, -1, 2u, "Chainmail", (uint)ItemType.Armor, 200u, 500),
});
h.ItemList.GetItem(0)!.Clicked?.Invoke();
Assert.True(h.Window.IsVisible);
Assert.NotEqual(string.Empty, GetText(h.ItemNameText));
h.State.Close();
Assert.False(h.Window.IsVisible);
Assert.Equal(0, h.ItemList.GetNumUIItems());
Assert.Equal(string.Empty, GetText(h.ItemNameText));
Assert.Equal(string.Empty, GetText(h.ItemCostText));
Assert.Empty(h.TypeMenu.Items);
}
[Fact]
public void Reset_HidesWindowAndClearsContent()
{
var h = new Harness();
h.State.Apply(VendorGuid, Profile(), new[]
{
new VendorShopItem(ArmorItemGuid, -1, 2u, "Chainmail", (uint)ItemType.Armor, 200u, 500),
});
h.State.Reset();
Assert.False(h.Window.IsVisible);
Assert.Equal(0, h.ItemList.GetNumUIItems());
}
[Fact]
public void SelectingItem_ShowsRetailCorrectPerUnitPrice_ForStackedItem()
{
var h = new Harness();
h.State.Apply(VendorGuid, Profile(sellRate: 2.0f), new[]
{
// Value=1000 is the STACK-TOTAL wire value for a 100-unit stack;
// per-unit = 1000/100 = 10. SellPrice = ceil(2.0*10*1 - 0.1) = 20
// (VendorPricing.SellPrice, ShopSystem::SellPrice pc:702107).
new VendorShopItem(
StackedItemGuid, -1, 3u, "Arrows", (uint)ItemType.MissileWeapon, 300u, 1000,
DescStackSize: 100),
});
h.ItemList.GetItem(0)!.Clicked?.Invoke();
Assert.Equal("Arrows", GetText(h.ItemNameText));
Assert.Equal("20", GetText(h.ItemCostText));
}
[Fact]
public void SelectingItem_WithAlternateCurrency_AppendsCurrencyName()
{
var h = new Harness();
h.State.Apply(
VendorGuid,
Profile(sellRate: 1.0f, altCurrency: 0x12345678u, altName: "Trade Notes"),
new[]
{
new VendorShopItem(ArmorItemGuid, -1, 2u, "Chainmail", (uint)ItemType.Armor, 200u, 50),
});
h.ItemList.GetItem(0)!.Clicked?.Invoke();
Assert.Equal("50 Trade Notes", GetText(h.ItemCostText));
}
[Fact]
public void SelectingDifferentCategory_FiltersItemListToThatCategoryOnly()
{
var h = new Harness();
var armor = new VendorShopItem(ArmorItemGuid, -1, 2u, "Chainmail", (uint)ItemType.Armor, 200u, 500);
var food = new VendorShopItem(FoodItemGuid, -1, 1u, "Bread", (uint)ItemType.Food, 100u, 5);
h.State.Apply(VendorGuid, Profile(), new[] { armor, food });
Assert.Equal(ArmorItemGuid, h.ItemList.GetItem(0)!.ItemId); // default: Armor (table order)
object? foodPayload = h.TypeMenu.Items.First(i => i.Label == "Food").Payload;
h.TypeMenu.OnSelect!.Invoke(foodPayload);
Assert.Equal(1, h.ItemList.GetNumUIItems());
Assert.Equal(FoodItemGuid, h.ItemList.GetItem(0)!.ItemId);
}
[Fact]
public void CloseButton_RoutesThroughVendorStateClose_NotADirectFieldWrite()
{
var h = new Harness();
h.State.Apply(VendorGuid, Profile(), new[]
{
new VendorShopItem(ArmorItemGuid, -1, 2u, "Chainmail", (uint)ItemType.Armor, 200u, 500),
});
h.CloseButton.OnClick!.Invoke();
// The panel never mutates VendorState directly — the ONLY path from
// the close button to a cleared session is VendorState.Close()
// itself, so VendorId reads back 0 through the owner's own public
// surface, not a private field poke.
Assert.Equal(0u, h.State.VendorId);
Assert.False(h.Window.IsVisible);
}
[Fact]
public void SellingTab_SwitchesPageButPopulatesNoSellContent()
{
var h = new Harness();
h.State.Apply(VendorGuid, Profile(), new[]
{
new VendorShopItem(ArmorItemGuid, -1, 2u, "Chainmail", (uint)ItemType.Armor, 200u, 500),
});
h.SellingTab.OnClick!.Invoke();
Assert.True(h.SellingPage.Visible);
Assert.False(h.ItemsPage.Visible);
Assert.False(h.BuyingPage.Visible);
// Slice 6 fence: no sell-list/price/button wiring exists at all —
// the page is exactly the authored-empty container it started as.
Assert.Empty(h.SellingPage.Children);
}
[Fact]
public void BuyingTab_SwitchesPageButPopulatesNoBuyContent()
{
var h = new Harness();
h.State.Apply(VendorGuid, Profile(), new[]
{
new VendorShopItem(ArmorItemGuid, -1, 2u, "Chainmail", (uint)ItemType.Armor, 200u, 500),
});
h.BuyingTab.OnClick!.Invoke();
Assert.True(h.BuyingPage.Visible);
Assert.False(h.ItemsPage.Visible);
Assert.Empty(h.BuyingPage.Children);
}
[Fact]
public void ItemsTab_ReselectedAfterVisitingOtherTabs_ShowsBrowseListAgain()
{
var h = new Harness();
h.State.Apply(VendorGuid, Profile(), new[]
{
new VendorShopItem(ArmorItemGuid, -1, 2u, "Chainmail", (uint)ItemType.Armor, 200u, 500),
});
h.SellingTab.OnClick!.Invoke();
h.ItemsTab.OnClick!.Invoke();
Assert.True(h.ItemsPage.Visible);
Assert.False(h.SellingPage.Visible);
Assert.Equal(1, h.ItemList.GetNumUIItems());
}
}