using AcDream.UI.Abstractions.Panels.Chat; namespace AcDream.UI.Abstractions.Tests.Panels.Chat; /// /// TDD coverage for . Pure function: /// no fakes, no setup. Each prefix in the holtburger /// chat.rs alias table gets at least one round-trip test plus /// every documented edge case. /// public sealed class ChatInputParserTests { // -- Default channel (no prefix / explicit /say) -------------------- [Fact] public void NoPrefix_UsesDefaultChannelLiteral() { var parsed = ChatInputParser.Parse("hello world", ChatChannelKind.Say, lastTellSender: null); Assert.NotNull(parsed); Assert.Equal(ChatChannelKind.Say, parsed!.Value.Channel); Assert.Null(parsed.Value.TargetName); Assert.Equal("hello world", parsed.Value.Text); } [Fact] public void SayPrefix_StripsTheSlashSay() { var parsed = ChatInputParser.Parse("/say hi there", ChatChannelKind.Say, lastTellSender: null); Assert.NotNull(parsed); Assert.Equal(ChatChannelKind.Say, parsed!.Value.Channel); Assert.Null(parsed.Value.TargetName); Assert.Equal("hi there", parsed.Value.Text); } // -- Tell aliases ---------------------------------------------------- [Fact] public void TellPrefix_FullForm_ParsesTargetAndMessage() { var parsed = ChatInputParser.Parse("/tell Bestie hi there", ChatChannelKind.Say, lastTellSender: null); Assert.NotNull(parsed); Assert.Equal(ChatChannelKind.Tell, parsed!.Value.Channel); Assert.Equal("Bestie", parsed.Value.TargetName); Assert.Equal("hi there", parsed.Value.Text); } [Fact] public void TellPrefix_ShortForm_ParsesTargetAndMessage() { var parsed = ChatInputParser.Parse("/t Bestie hi", ChatChannelKind.Say, lastTellSender: null); Assert.NotNull(parsed); Assert.Equal(ChatChannelKind.Tell, parsed!.Value.Channel); Assert.Equal("Bestie", parsed.Value.TargetName); Assert.Equal("hi", parsed.Value.Text); } [Fact] public void TellPrefix_MultiWordTarget_ChompsFirstTokenAsTarget() { // Holtburger split_once on whitespace: first token is target. // "/t Sir Lancelot hello" -> target="Sir", text="Lancelot hello". var parsed = ChatInputParser.Parse("/t Sir Lancelot hello", ChatChannelKind.Say, lastTellSender: null); Assert.NotNull(parsed); Assert.Equal(ChatChannelKind.Tell, parsed!.Value.Channel); Assert.Equal("Sir", parsed.Value.TargetName); Assert.Equal("Lancelot hello", parsed.Value.Text); } [Theory] [InlineData("/t Caith, hi", "Caith")] [InlineData("/t Caith: hi", "Caith")] [InlineData("/t Caith; hi", "Caith")] [InlineData("/t Caith. hi", "Caith")] [InlineData("/t Caith! hi", "Caith")] [InlineData("/t Caith? hi", "Caith")] [InlineData("/tell Caith, hi", "Caith")] public void TellPrefix_StripsTrailingPunctuationFromTarget(string raw, string expectedTarget) { // Retail muscle memory: "/t Name, message" — comma is the // separator. Filed after a 2026-04-25 live-launch session // where typing "/t je, hello" produced target="je," and the // server responded with WeenieError 0x052B (CharacterNotAvailable) // because no character "je," exists. Strip a trailing // ,;:.!? from the target so both forms work. var parsed = ChatInputParser.Parse(raw, ChatChannelKind.Say, lastTellSender: null); Assert.NotNull(parsed); Assert.Equal(ChatChannelKind.Tell, parsed!.Value.Channel); Assert.Equal(expectedTarget, parsed.Value.TargetName); Assert.Equal("hi", parsed.Value.Text); } // -- Reply aliases --------------------------------------------------- [Fact] public void ReplyPrefix_UsesLastIncomingTellSenderAsTarget() { var parsed = ChatInputParser.Parse("/r back at you", ChatChannelKind.Say, lastTellSender: "Bestie"); Assert.NotNull(parsed); Assert.Equal(ChatChannelKind.Tell, parsed!.Value.Channel); Assert.Equal("Bestie", parsed.Value.TargetName); Assert.Equal("back at you", parsed.Value.Text); } [Fact] public void ReplyPrefix_NoLastSender_ReturnsNull() { var parsed = ChatInputParser.Parse("/r hi", ChatChannelKind.Say, lastTellSender: null); Assert.Null(parsed); } // -- Channel aliases (single-message) ------------------------------- [Theory] [InlineData("/g raid time", ChatChannelKind.General, "raid time")] [InlineData("/f buff up", ChatChannelKind.Fellowship, "buff up")] [InlineData("/a swearing in", ChatChannelKind.Allegiance, "swearing in")] [InlineData("/m monarch broadcast", ChatChannelKind.Monarch, "monarch broadcast")] [InlineData("/p patron only", ChatChannelKind.Patron, "patron only")] [InlineData("/v vassals only", ChatChannelKind.Vassals, "vassals only")] [InlineData("/cv covassals only", ChatChannelKind.CoVassals, "covassals only")] [InlineData("/lfg need 3 more", ChatChannelKind.Lfg, "need 3 more")] [InlineData("/trade wts gem", ChatChannelKind.Trade, "wts gem")] [InlineData("/role *waves*", ChatChannelKind.Roleplay, "*waves*")] [InlineData("/society olthoi raid", ChatChannelKind.Society, "olthoi raid")] [InlineData("/olthoi for the queen", ChatChannelKind.Olthoi, "for the queen")] public void ChannelPrefixes_RouteToTheirChannel(string raw, ChatChannelKind expectedChannel, string expectedText) { var parsed = ChatInputParser.Parse(raw, ChatChannelKind.Say, lastTellSender: null); Assert.NotNull(parsed); Assert.Equal(expectedChannel, parsed!.Value.Channel); Assert.Null(parsed.Value.TargetName); Assert.Equal(expectedText, parsed.Value.Text); } // -- Edge cases: empty / whitespace / no-message -------------------- [Fact] public void Empty_ReturnsNull() { Assert.Null(ChatInputParser.Parse("", ChatChannelKind.Say, lastTellSender: null)); } [Fact] public void WhitespaceOnly_ReturnsNull() { Assert.Null(ChatInputParser.Parse(" \t ", ChatChannelKind.Say, lastTellSender: null)); } [Fact] public void SayWithNoMessage_ReturnsNull() { Assert.Null(ChatInputParser.Parse("/say", ChatChannelKind.Say, lastTellSender: null)); Assert.Null(ChatInputParser.Parse("/say ", ChatChannelKind.Say, lastTellSender: null)); } [Fact] public void TellWithNoTargetNoMessage_ReturnsNull() { Assert.Null(ChatInputParser.Parse("/t", ChatChannelKind.Say, lastTellSender: null)); Assert.Null(ChatInputParser.Parse("/tell", ChatChannelKind.Say, lastTellSender: null)); } [Fact] public void TellWithTargetButNoMessage_ReturnsNull() { Assert.Null(ChatInputParser.Parse("/t Bestie", ChatChannelKind.Say, lastTellSender: null)); Assert.Null(ChatInputParser.Parse("/tell Bestie ", ChatChannelKind.Say, lastTellSender: null)); } [Fact] public void ReplyWithNoMessage_ReturnsNull_EvenWithLastSender() { Assert.Null(ChatInputParser.Parse("/r", ChatChannelKind.Say, lastTellSender: "Bestie")); Assert.Null(ChatInputParser.Parse("/r ", ChatChannelKind.Say, lastTellSender: "Bestie")); } [Fact] public void ChannelPrefixWithNoMessage_ReturnsNull() { Assert.Null(ChatInputParser.Parse("/g", ChatChannelKind.Say, lastTellSender: null)); Assert.Null(ChatInputParser.Parse("/f ", ChatChannelKind.Say, lastTellSender: null)); Assert.Null(ChatInputParser.Parse("/a", ChatChannelKind.Say, lastTellSender: null)); } // -- Unknown slash command: holtburger fall-through to Talk(literal) // ("/wave hello" → Talk("/wave hello") + treat-as-Say). See // chat.rs::handle_slash_command default arm at line ~744. [Fact] public void UnknownSlashCommand_FallsBackToDefaultChannelWithLiteralText() { var parsed = ChatInputParser.Parse("/xyz hello", ChatChannelKind.Say, lastTellSender: null); Assert.NotNull(parsed); Assert.Equal(ChatChannelKind.Say, parsed!.Value.Channel); Assert.Null(parsed.Value.TargetName); Assert.Equal("/xyz hello", parsed.Value.Text); } // -- Default-channel parameter is honoured when no prefix is given -- [Fact] public void NoPrefix_HonoursAlternateDefaultChannel() { // If the panel ever defaults to e.g. Fellowship channel input, // the parser must use that for unprefixed text. var parsed = ChatInputParser.Parse("hi gang", ChatChannelKind.Fellowship, lastTellSender: null); Assert.NotNull(parsed); Assert.Equal(ChatChannelKind.Fellowship, parsed!.Value.Channel); Assert.Null(parsed.Value.TargetName); Assert.Equal("hi gang", parsed.Value.Text); } // -- The verb itself must be exact: prefix-of-other-word doesn't count -- [Fact] public void PrefixSubstring_IsNotAVerbMatch() { // Phase J added long-form aliases (/general, /allegiance, // /patron, etc.). The exact-token rule still applies — a // verb prefix that ISN'T one of the listed aliases falls // through. The Parse-level behaviour for unknown /-verbs is // still "Say with the literal text" (matches holtburger); // the ChatPanel layer is what catches unknowns and shows the // local "Unknown command" line. ChatPanelInputTests cover // that end-to-end behaviour. var parsed = ChatInputParser.Parse("/genio public", ChatChannelKind.Say, lastTellSender: null); Assert.NotNull(parsed); Assert.Equal(ChatChannelKind.Say, parsed!.Value.Channel); Assert.Equal("/genio public", parsed.Value.Text); } [Theory] [InlineData("/g", true)] [InlineData("/say", true)] [InlineData("/tell", true)] [InlineData("/retell", true)] [InlineData("/allegiance", true)] [InlineData("/lookingforgroup", true)] [InlineData("/genio", false)] [InlineData("/ls", false)] [InlineData("/foo", false)] [InlineData("/", false)] public void IsKnownVerb_ChecksAgainstAliasTables(string verb, bool expected) { Assert.Equal(expected, ChatInputParser.IsKnownVerb(verb)); } [Theory] [InlineData("/g hello", "/g")] [InlineData("/tell Bob hi", "/tell")] [InlineData("/foo", "/foo")] [InlineData("/", "/")] public void GetVerbToken_PullsFirstWhitespaceToken(string command, string expected) { Assert.Equal(expected, ChatInputParser.GetVerbToken(command)); } [Theory] [InlineData("/general what's the deal", ChatChannelKind.General, "what's the deal")] [InlineData("/allegiance recall", ChatChannelKind.Allegiance, "recall")] [InlineData("/patron need help", ChatChannelKind.Patron, "need help")] [InlineData("/vassals listen up", ChatChannelKind.Vassals, "listen up")] [InlineData("/monarch heads up", ChatChannelKind.Monarch, "heads up")] [InlineData("/covassals tax season", ChatChannelKind.CoVassals, "tax season")] [InlineData("/fellowship buff time", ChatChannelKind.Fellowship, "buff time")] [InlineData("/fellow buff time", ChatChannelKind.Fellowship, "buff time")] [InlineData("/lookingforgroup hunt invite", ChatChannelKind.Lfg, "hunt invite")] [InlineData("/roleplay walk-up", ChatChannelKind.Roleplay, "walk-up")] [InlineData("/rp walk-up", ChatChannelKind.Roleplay, "walk-up")] public void LongFormAliases_RouteToTheirChannel(string raw, ChatChannelKind expected, string text) { // Phase J: retail muscle memory uses long forms ("/patron" // not just "/p"). Filed after a 2026-04-25 live test where // "/patron hello" fell through as /say with the literal // slash-prefixed text. var parsed = ChatInputParser.Parse(raw, ChatChannelKind.Say, lastTellSender: null); Assert.NotNull(parsed); Assert.Equal(expected, parsed!.Value.Channel); Assert.Null(parsed.Value.TargetName); Assert.Equal(text, parsed.Value.Text); } }