using System.Collections.Generic;
using AcDream.App.UI;
using AcDream.App.UI.Layout;
using AcDream.Core.Chat;
using AcDream.UI.Abstractions;
using AcDream.UI.Abstractions.Panels.Chat;
namespace AcDream.App.Tests.UI.Layout;
///
/// Smoke + filter-routing tests for
/// — no dats, no GL,
/// mirroring ChatWindowControllerTests 's synthetic-tree approach but
/// against the floating layout's element topology
/// (0x2100005B , research doc §2.2): the input row id
/// (0x10000509 ) differs from the main window's
/// (0x10000013 ), and there is no talk-focus menu / max-min button /
/// indicator row.
///
public class FloatingChatWindowControllerTests
{
private static (uint, int, int) NoTex(uint _) => (0u, 0, 0);
private sealed class CaptureBus : ICommandBus
{
public readonly List Published = new();
public void Publish(T cmd) where T : notnull => Published.Add(cmd!);
}
///
/// Synthetic tree mirroring the floating chat layout topology:
/// root (Type-3) [0x100004F7]
/// transcriptPanel (Type-3) [0x10000010]
/// transcript (Type-12, no media) [0x10000011]
/// track (Type-3) [0x10000012]
/// inputRow (Type-3) [0x10000509] ← the floaty-specific id
/// input (Type-12, Editable+Selectable) [0x10000016]
/// send (Type-3) [0x10000019]
/// titleBar (Type-3) [0x100004D9]
/// closeButton (Type-3) [0x1000052A]
///
private static (ElementInfo rootInfo, ImportedLayout layout, ChatVM vm) BuildTestTree(
ChatLog? log = null)
{
var transcriptNode = new ElementInfo
{
Id = 0x10000011u, Type = 12,
X = 5, Y = 20, Width = 224, Height = 60,
};
var trackNode = new ElementInfo
{
Id = 0x10000012u, Type = 3,
X = 229, Y = 20, Width = 16, Height = 60,
};
var transcriptPanel = new ElementInfo
{
Id = 0x10000010u, Type = 3, X = 0, Y = 20, Width = 250, Height = 60,
};
transcriptPanel.Children.Add(transcriptNode);
transcriptPanel.Children.Add(trackNode);
var inputNode = new ElementInfo
{
Id = 0x10000016u, Type = 12,
X = 0, Y = 80, Width = 202, Height = 18,
};
var inputState = new UiStateInfo { Id = UiStateInfo.DirectStateId };
inputState.Properties.Values[0x16u] = new UiPropertyValue { Kind = UiPropertyKind.Bool, BoolValue = true };
inputState.Properties.Values[0x20u] = new UiPropertyValue { Kind = UiPropertyKind.Bool, BoolValue = true };
inputState.Properties.Values[0x27u] = new UiPropertyValue { Kind = UiPropertyKind.Bool, BoolValue = true };
inputNode.States[UiStateInfo.DirectStateId] = inputState;
var sendNode = new ElementInfo
{
Id = 0x10000019u, Type = 3, X = 202, Y = 80, Width = 38, Height = 18,
};
var inputRow = new ElementInfo
{
Id = 0x10000509u, Type = 3, X = 0, Y = 80, Width = 250, Height = 18,
};
inputRow.Children.Add(inputNode);
inputRow.Children.Add(sendNode);
var titleBar = new ElementInfo { Id = 0x100004D9u, Type = 3, X = 0, Y = 0, Width = 240, Height = 16 };
var closeButton = new ElementInfo { Id = 0x1000052Au, Type = 3, X = 225, Y = 1, Width = 14, Height = 14 };
var root = new ElementInfo { Id = 0x100004F7u, Type = 3, Width = 250, Height = 108 };
root.Children.Add(transcriptPanel);
root.Children.Add(inputRow);
root.Children.Add(titleBar);
root.Children.Add(closeButton);
var layout = LayoutImporter.Build(root, NoTex, null);
var vm = new ChatVM(log ?? new ChatLog());
return (root, layout, vm);
}
// ── Bind smoke tests ──────────────────────────────────────────────────
[Fact]
public void Bind_Returns_NonNull_OnValidTree()
{
var (rootInfo, layout, vm) = BuildTestTree();
var bus = new CaptureBus();
var filters = new ChatWindowState();
var ctrl = FloatingChatWindowController.Bind(
1, rootInfo, layout, vm, () => bus, filters, null, null, NoTex);
Assert.NotNull(ctrl);
Assert.Equal(1, ctrl!.WindowId);
}
[Theory]
[InlineData(0)]
[InlineData(5)]
public void Bind_InvalidWindowId_Throws(int windowId)
{
var (rootInfo, layout, vm) = BuildTestTree();
var bus = new CaptureBus();
var filters = new ChatWindowState();
Assert.Throws(() =>
FloatingChatWindowController.Bind(
windowId, rootInfo, layout, vm, () => bus, filters, null, null, NoTex));
}
[Fact]
public void Bind_Returns_Null_WhenTranscriptPanelMissing()
{
var root = new ElementInfo { Id = 0x100004F7u, Type = 3, Width = 250, Height = 108 };
var layout = LayoutImporter.Build(root, NoTex, null);
var vm = new ChatVM(new ChatLog());
var bus = new CaptureBus();
var filters = new ChatWindowState();
var ctrl = FloatingChatWindowController.Bind(
1, root, layout, vm, () => bus, filters, null, null, NoTex);
Assert.Null(ctrl);
}
[Fact]
public void Bind_Transcript_IsChildOfTranscriptPanel()
{
var (rootInfo, layout, vm) = BuildTestTree();
var bus = new CaptureBus();
var filters = new ChatWindowState();
var ctrl = FloatingChatWindowController.Bind(
2, rootInfo, layout, vm, () => bus, filters, null, null, NoTex);
Assert.NotNull(ctrl);
var panel = layout.FindElement(0x10000010u);
Assert.NotNull(panel);
Assert.Contains(ctrl!.Transcript, panel!.Children);
}
[Fact]
public void Bind_Input_IsChildOfFloatyInputRow_NotMainWindowInputBar()
{
var (rootInfo, layout, vm) = BuildTestTree();
var bus = new CaptureBus();
var filters = new ChatWindowState();
var ctrl = FloatingChatWindowController.Bind(
3, rootInfo, layout, vm, () => bus, filters, null, null, NoTex);
Assert.NotNull(ctrl);
var row = layout.FindElement(0x10000509u);
Assert.NotNull(row);
Assert.Contains(ctrl!.Input, row!.Children);
}
// ── Chat entry always sends on Say (no talk-focus menu authored) ───────
[Fact]
public void Bind_InputSubmit_AlwaysSendsOnSayChannel()
{
var (rootInfo, layout, vm) = BuildTestTree();
var bus = new CaptureBus();
var filters = new ChatWindowState();
var ctrl = FloatingChatWindowController.Bind(
4, rootInfo, layout, vm, () => bus, filters, null, null, NoTex);
Assert.NotNull(ctrl);
ctrl!.Input.OnSubmit!.Invoke("hi everyone");
var cmd = Assert.IsType(Assert.Single(bus.Published));
Assert.Equal(ChatChannelKind.Say, cmd.Channel);
Assert.Equal("hi everyone", cmd.Text);
}
// ── Display rule matrix: this window's filter accepts/rejects a line ───
[Fact]
public void Transcript_ShowsOnlyLinesThisWindowsFilterAccepts()
{
var log = new ChatLog();
var (rootInfo, layout, vm) = BuildTestTree(log);
var bus = new CaptureBus();
var filters = new ChatWindowState(); // window 1 default: Speech/Tell/SpeechDirectSend/Emote
var ctrl = FloatingChatWindowController.Bind(
1, rootInfo, layout, vm, () => bus, filters, null, null, NoTex);
Assert.NotNull(ctrl);
log.OnSystemMessage("visible speech", chatType: 0x02u); // Speech — in window 1's filter
log.OnSystemMessage("hidden social", chatType: 0x0Au); // Social — NOT in window 1's filter
var lines = ctrl!.Transcript.LinesProvider();
Assert.Single(lines);
Assert.Equal("visible speech", lines[0].Text);
}
[Fact]
public void Transcript_DifferentWindow_AcceptsADifferentSubsetOfTypes()
{
var log = new ChatLog();
var (rootInfo, layout, vm) = BuildTestTree(log);
var bus = new CaptureBus();
var filters = new ChatWindowState(); // window 3 default: Fellowship only
var ctrl = FloatingChatWindowController.Bind(
3, rootInfo, layout, vm, () => bus, filters, null, null, NoTex);
Assert.NotNull(ctrl);
log.OnSystemMessage("speech line", chatType: 0x02u); // Speech — not window 3's filter
log.OnSystemMessage("fellowship line", chatType: 0x13u); // Fellowship — window 3's filter
var lines = ctrl!.Transcript.LinesProvider();
Assert.Single(lines);
Assert.Equal("fellowship line", lines[0].Text);
}
[Fact]
public void Transcript_FilterChange_IsReflectedOnNextRebuild()
{
var log = new ChatLog();
var (rootInfo, layout, vm) = BuildTestTree(log);
var bus = new CaptureBus();
var filters = new ChatWindowState();
var ctrl = FloatingChatWindowController.Bind(
2, rootInfo, layout, vm, () => bus, filters, null, null, NoTex);
Assert.NotNull(ctrl);
log.OnSystemMessage("speech line", chatType: 0x02u); // Speech — not window 2's default filter
Assert.Empty(ctrl!.Transcript.LinesProvider());
// Widen window 2's filter to include Speech (bit 0x02 = 0x4) without
// touching the chat log — the cached transcript must invalidate on
// the filter change alone.
filters.SetFilter(2, filters.GetFilter(2) | (1UL << 0x02));
var lines = ctrl.Transcript.LinesProvider();
Assert.Single(lines);
Assert.Equal("speech line", lines[0].Text);
}
[Fact]
public void Transcript_UnchangedFilterAndContent_ReusesCachedLayout()
{
var log = new ChatLog();
var (rootInfo, layout, vm) = BuildTestTree(log);
var bus = new CaptureBus();
var filters = new ChatWindowState();
var ctrl = FloatingChatWindowController.Bind(
1, rootInfo, layout, vm, () => bus, filters, null, null, NoTex);
Assert.NotNull(ctrl);
log.OnSystemMessage("speech line", chatType: 0x02u);
var first = ctrl!.Transcript.LinesProvider();
var unchanged = ctrl.Transcript.LinesProvider();
Assert.Same(first, unchanged);
Assert.Equal(1, ctrl.TranscriptLayoutBuildCount);
}
}