using System; using System.Linq; using GmRelay.Bot.Features.Sessions.CreateSession.Wizard; using GmRelay.Shared.Features.Sessions.CreateSession.Wizard; using Telegram.Bot.Types.ReplyMarkups; namespace GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard; /// /// Verifies the shape of each step's rendered keyboard: which buttons are /// present, where the Back/Cancel affordances sit, and that the title text /// is non-empty. Tests use substring matching so they survive label tweaks /// (e.g. emoji prefixes, suffix additions like "· 4 ч"). /// public sealed class WizardStepRenderTests { [Fact] public void TypeStep_HasBothChoicesAndCancel_ButNoBack() { var (text, kb) = Render(WizardStepNames.Type); Assert.False(string.IsNullOrWhiteSpace(text)); var labels = ButtonLabels(kb); Assert.Contains(labels, l => l.Contains("Одну игру", StringComparison.Ordinal)); Assert.Contains(labels, l => l.Contains("Пул игр", StringComparison.Ordinal)); Assert.Contains(labels, l => l.Contains("Отмена", StringComparison.Ordinal)); Assert.DoesNotContain(labels, l => l.Contains("Назад", StringComparison.Ordinal)); } [Fact] public void TitleStep_HasBackAndCancel_ButNoChoiceButtons() { var (text, kb) = Render(WizardStepNames.Title); Assert.False(string.IsNullOrWhiteSpace(text)); var labels = ButtonLabels(kb); Assert.Contains(labels, l => l.Contains("Назад", StringComparison.Ordinal)); Assert.Contains(labels, l => l.Contains("Отмена", StringComparison.Ordinal)); } [Fact] public void SystemStep_HasKnownSystemButtons() { var (text, kb) = Render(WizardStepNames.System); Assert.False(string.IsNullOrWhiteSpace(text)); var labels = ButtonLabels(kb); Assert.Contains(labels, l => l.Contains("D&D 5e", StringComparison.Ordinal)); Assert.Contains(labels, l => l.Contains("Pathfinder 2e", StringComparison.Ordinal)); Assert.Contains(labels, l => l.Contains("Call of Cthulhu", StringComparison.Ordinal)); Assert.Contains(labels, l => l.Contains("GURPS", StringComparison.Ordinal)); Assert.Contains(labels, l => l.Contains("Fate", StringComparison.Ordinal)); Assert.Contains(labels, l => l.Contains("Другое", StringComparison.Ordinal)); Assert.Contains(labels, l => l.Contains("Пропустить", StringComparison.Ordinal)); } [Fact] public void DurationStep_HasPresetButtons() { var (text, kb) = Render(WizardStepNames.Duration); Assert.False(string.IsNullOrWhiteSpace(text)); var labels = ButtonLabels(kb); Assert.Contains("3 часа", labels); Assert.Contains("4 часа", labels); Assert.Contains("5 часов", labels); Assert.Contains("6 часов", labels); } [Fact] public void CapacityStep_HasWaitlistButtons() { var (text, kb) = Render(WizardStepNames.Capacity); Assert.False(string.IsNullOrWhiteSpace(text)); var labels = ButtonLabels(kb); Assert.Contains(labels, l => l.Contains("Waitlist вкл", StringComparison.Ordinal)); Assert.Contains(labels, l => l.Contains("Без waitlist", StringComparison.Ordinal)); Assert.Contains(labels, l => l.Contains("Без лимита", StringComparison.Ordinal)); } [Fact] public void FormatStep_HasOnlineAndOfflineButtons() { var (text, kb) = Render(WizardStepNames.Format); Assert.False(string.IsNullOrWhiteSpace(text)); var labels = ButtonLabels(kb); Assert.Contains(labels, l => l.Contains("Online", StringComparison.Ordinal)); Assert.Contains(labels, l => l.Contains("Offline", StringComparison.Ordinal)); } [Fact] public void LocationStep_ForOnline_AsksForLink() { var (text, kb) = Render(WizardStepNames.Location, new WizardPayload { Format = WizardSessionFormat.Online }); Assert.Contains("ссыл", text, StringComparison.OrdinalIgnoreCase); var labels = ButtonLabels(kb); Assert.Contains(labels, l => l.Contains("Назад", StringComparison.Ordinal)); Assert.Contains(labels, l => l.Contains("Отмена", StringComparison.Ordinal)); } [Fact] public void LocationStep_ForOffline_AsksForAddress() { var (text, kb) = Render(WizardStepNames.Location, new WizardPayload { Format = WizardSessionFormat.Offline }); Assert.Contains("адрес", text, StringComparison.OrdinalIgnoreCase); var labels = ButtonLabels(kb); Assert.Contains(labels, l => l.Contains("Назад", StringComparison.Ordinal)); Assert.Contains(labels, l => l.Contains("Отмена", StringComparison.Ordinal)); } [Fact] public void VisibilityStep_HasAllFourVisibilityOptions() { var (text, kb) = Render(WizardStepNames.Visibility); Assert.False(string.IsNullOrWhiteSpace(text)); var labels = ButtonLabels(kb); Assert.Contains(labels, l => l.Contains("Публичная в общем showcase", StringComparison.Ordinal)); Assert.Contains(labels, l => l.Contains("Публичная в витрине клуба", StringComparison.Ordinal)); Assert.Contains(labels, l => l.Contains("Только для членов клуба", StringComparison.Ordinal)); Assert.Contains(labels, l => l.Contains("Выбрать клуб", StringComparison.Ordinal)); } [Fact] public void PickClub_WithoutClubs_RendersEmptyHint() { var (text, kb) = WizardStep.Render( NewDraft(WizardStepNames.PickClub), new WizardPayload(), clubs: null); Assert.False(string.IsNullOrWhiteSpace(text)); Assert.Contains("нет клубов", text, StringComparison.OrdinalIgnoreCase); } [Fact] public void PickClub_WithOneClub_RendersClubButton() { var clubId = Guid.NewGuid(); var (text, kb) = WizardStep.Render( NewDraft(WizardStepNames.PickClub), new WizardPayload(), new[] { new WizardClubOption(clubId, "Awesome Club") }); Assert.False(string.IsNullOrWhiteSpace(text)); Assert.Contains("Awesome Club", ButtonLabels(kb)); } [Fact] public void PublishStep_HasPublishAndChatOnlyButtons() { var (text, kb) = Render(WizardStepNames.Publish); Assert.False(string.IsNullOrWhiteSpace(text)); var labels = ButtonLabels(kb); Assert.Contains(labels, l => l.Contains("Опубликовать", StringComparison.Ordinal)); Assert.Contains(labels, l => l.Contains("Только в чате", StringComparison.Ordinal)); } [Fact] public void ConfirmStep_HasCreateAndCancel() { var (text, kb) = Render(WizardStepNames.Confirm, new WizardPayload { Type = WizardCreationType.Single, Title = "My Game", Format = WizardSessionFormat.Offline, LocationAddress = "Москва, ул. Кубиков, 12", }); Assert.False(string.IsNullOrWhiteSpace(text)); Assert.Contains("My Game", text); Assert.Contains("Offline", text); Assert.Contains("Москва, ул. Кубиков, 12", text); var labels = ButtonLabels(kb); Assert.Contains(labels, l => l.Contains("Создать", StringComparison.Ordinal)); Assert.Contains(labels, l => l.Contains("Отмена", StringComparison.Ordinal)); } [Fact] public void PoolSystemDuration_HasPresetsAndCustom() { var (text, kb) = Render(WizardStepNames.PoolSystemDuration); Assert.False(string.IsNullOrWhiteSpace(text)); var labels = ButtonLabels(kb); Assert.Contains(labels, l => l.Contains("D&D 5e", StringComparison.Ordinal)); Assert.Contains(labels, l => l.Contains("Pathfinder 2e", StringComparison.Ordinal)); Assert.Contains(labels, l => l.Contains("Call of Cthulhu", StringComparison.Ordinal)); Assert.Contains(labels, l => l.Contains("GURPS", StringComparison.Ordinal)); Assert.Contains(labels, l => l.Contains("Другое", StringComparison.Ordinal)); } [Fact] public void PoolAddSlots_HasAddAndDone_AndShowsCurrentCount() { var payload = new WizardPayload { Type = WizardCreationType.Pool, Title = "My Pool", Pool = new WizardPoolInput { Slots = { new WizardSlotInput { MaxPlayers = 4 }, new WizardSlotInput { MaxPlayers = 5 }, }, }, }; var (text, kb) = WizardStep.Render( NewDraft(WizardStepNames.PoolAddSlots), payload); Assert.Contains("My Pool", text); Assert.Contains("2", text); var labels = ButtonLabels(kb); Assert.Contains(labels, l => l.Contains("Добавить слот", StringComparison.Ordinal)); Assert.Contains(labels, l => l.Contains("Готово", StringComparison.Ordinal)); } [Fact] public void PoolSlotDateTime_HasBackAndCancel_ButNoChoiceButtons() { var (text, kb) = Render(WizardStepNames.PoolSlotDateTime); Assert.False(string.IsNullOrWhiteSpace(text)); var labels = ButtonLabels(kb); Assert.Contains(labels, l => l.Contains("Назад", StringComparison.Ordinal)); Assert.Contains(labels, l => l.Contains("Отмена", StringComparison.Ordinal)); } [Fact] public void PoolSlotCapacity_HasWaitlistButtons() { var (text, kb) = Render(WizardStepNames.PoolSlotCapacity); Assert.False(string.IsNullOrWhiteSpace(text)); var labels = ButtonLabels(kb); Assert.Contains(labels, l => l.Contains("Waitlist вкл", StringComparison.Ordinal)); Assert.Contains(labels, l => l.Contains("Без waitlist", StringComparison.Ordinal)); } [Fact] public void PoolConfirm_HasCreatePoolAndCancel() { var payload = new WizardPayload { Type = WizardCreationType.Pool, Title = "Pool", System = "Dnd5e", DurationMinutes = 240, Pool = new WizardPoolInput { Slots = { new WizardSlotInput { MaxPlayers = 4, Waitlist = true, ScheduledAt = DateTimeOffset.UtcNow.AddDays(7) }, new WizardSlotInput { MaxPlayers = 5, Waitlist = false, ScheduledAt = DateTimeOffset.UtcNow.AddDays(14) }, }, }, }; var (text, kb) = WizardStep.Render( NewDraft(WizardStepNames.PoolConfirm), payload); Assert.Contains("Pool", text); Assert.Contains("2", text); // slot count var labels = ButtonLabels(kb); Assert.Contains(labels, l => l.Contains("Создать пул", StringComparison.Ordinal)); Assert.Contains(labels, l => l.Contains("Отмена", StringComparison.Ordinal)); } [Fact] public void Render_UnknownStep_Throws() { var draft = new WizardDraft { Step = "Bogus" }; Assert.Throws(() => WizardStep.Render(draft, new WizardPayload())); } private static (string text, InlineKeyboardMarkup kb) Render(string step, WizardPayload? payload = null) => WizardStep.Render(NewDraft(step), payload ?? new WizardPayload()); private static WizardDraft NewDraft(string step) => new() { Id = Guid.NewGuid(), ChatId = "42", Step = step, PayloadJson = "{}", }; private static string[] ButtonLabels(InlineKeyboardMarkup kb) => kb.InlineKeyboard .SelectMany(row => row.Select(b => b.Text)) .ToArray(); }