using GmRelay.DiscordBot.Rendering; using GmRelay.Shared.Domain; using GmRelay.Shared.Rendering; using NetCord; using NetCord.Rest; namespace GmRelay.Bot.Tests.Rendering; public sealed class DiscordSessionBatchRendererTests { [Fact] public void Render_ShouldProduceEmbedsAndButtonsForMultipleSessions() { var firstSessionId = Guid.NewGuid(); var secondSessionId = Guid.NewGuid(); var cancelledSessionId = Guid.NewGuid(); var sessions = new[] { new SessionBatchDto(secondSessionId, new DateTime(2026, 4, 27, 18, 0, 0, DateTimeKind.Utc), SessionStatus.Planned, 4, "https://example.com/game2"), new SessionBatchDto(cancelledSessionId, new DateTime(2026, 4, 28, 18, 0, 0, DateTimeKind.Utc), SessionStatus.Cancelled, null, ""), new SessionBatchDto(firstSessionId, new DateTime(2026, 4, 26, 18, 0, 0, DateTimeKind.Utc), SessionStatus.Planned, 2, "https://example.com/game1") }; var participants = new[] { new ParticipantBatchDto(secondSessionId, "Alice", "alice", ParticipantRegistrationStatus.Active), new ParticipantBatchDto(secondSessionId, "Charlie", null, ParticipantRegistrationStatus.Waitlisted), new ParticipantBatchDto(cancelledSessionId, "Bob", null, ParticipantRegistrationStatus.Active) }; var view = SessionBatchViewBuilder.Build("Campaign", sessions, participants); var (embeds, actionRows) = DiscordSessionBatchRenderer.Render(view); Assert.Equal(3, embeds.Count); Assert.Equal(2, actionRows.Count); // cancelled skipped // Embed titles contain game title and Moscow date Assert.Contains(embeds, e => e.Title!.Contains("Campaign") && e.Title.Contains("26")); Assert.Contains(embeds, e => e.Title!.Contains("Campaign") && e.Title.Contains("27")); Assert.Contains(embeds, e => e.Title!.Contains("Campaign") && e.Title.Contains("28")); // Cancelled session embed description indicates cancellation var cancelledEmbed = embeds.First(e => e.Description!.Contains("отменена") || e.Description.Contains("Отменена")); Assert.NotNull(cancelledEmbed); // Active session embeds contain player names Assert.Contains(embeds, e => e.Description!.Contains("Alice")); Assert.Contains(embeds, e => e.Description!.Contains("Charlie")); // Buttons for active sessions var allButtons = actionRows.SelectMany(r => r).OfType().ToList(); Assert.Contains(allButtons, b => b.CustomId == $"join_session:{firstSessionId}"); Assert.Contains(allButtons, b => b.CustomId == $"leave_session:{firstSessionId}"); Assert.Contains(allButtons, b => b.CustomId == $"join_session:{secondSessionId}"); Assert.Contains(allButtons, b => b.CustomId == $"leave_session:{secondSessionId}"); } [Fact] public void Render_ShouldSkipActionRowsForCancelledSessions() { var cancelledSessionId = Guid.NewGuid(); var sessions = new[] { new SessionBatchDto(cancelledSessionId, DateTime.UtcNow, SessionStatus.Cancelled, null, "") }; var participants = Array.Empty(); var view = SessionBatchViewBuilder.Build("Test", sessions, participants); var (embeds, actionRows) = DiscordSessionBatchRenderer.Render(view); Assert.Single(embeds); Assert.Empty(actionRows); } [Fact] public void Render_ShouldShowWaitlistButtonWhenFull() { var sessionId = Guid.NewGuid(); var sessions = new[] { new SessionBatchDto(sessionId, DateTime.UtcNow, SessionStatus.Planned, 1, "https://example.com/game") }; var participants = new[] { new ParticipantBatchDto(sessionId, "Alice", "alice", ParticipantRegistrationStatus.Active) }; var view = SessionBatchViewBuilder.Build("Test", sessions, participants); var (_, actionRows) = DiscordSessionBatchRenderer.Render(view); var buttons = actionRows.SelectMany(r => r).OfType().ToList(); var joinButton = buttons.First(b => b.CustomId == $"join_session:{sessionId}"); Assert.Contains("ожидания", joinButton.Label); } [Fact] public void Render_ShouldUseRedColorForCancelledSessions() { var sessionId = Guid.NewGuid(); var sessions = new[] { new SessionBatchDto(sessionId, DateTime.UtcNow, SessionStatus.Cancelled, null, "") }; var participants = Array.Empty(); var view = SessionBatchViewBuilder.Build("Test", sessions, participants); var (embeds, _) = DiscordSessionBatchRenderer.Render(view); Assert.Equal(0xED4245, embeds[0].Color.RawValue); } [Fact] public void Render_ShouldUseGreenColorForOpenSessions() { var sessionId = Guid.NewGuid(); var sessions = new[] { new SessionBatchDto(sessionId, DateTime.UtcNow, SessionStatus.Planned, 4, "https://example.com/game") }; var participants = Array.Empty(); var view = SessionBatchViewBuilder.Build("Test", sessions, participants); var (embeds, _) = DiscordSessionBatchRenderer.Render(view); Assert.Equal(0x57F287, embeds[0].Color.RawValue); } [Fact] public void Render_ShouldUseYellowColorForFullSessions() { var sessionId = Guid.NewGuid(); var sessions = new[] { new SessionBatchDto(sessionId, DateTime.UtcNow, SessionStatus.Planned, 1, "https://example.com/game") }; var participants = new[] { new ParticipantBatchDto(sessionId, "Alice", "alice", ParticipantRegistrationStatus.Active) }; var view = SessionBatchViewBuilder.Build("Test", sessions, participants); var (embeds, _) = DiscordSessionBatchRenderer.Render(view); Assert.Equal(0xFEE75C, embeds[0].Color.RawValue); } [Fact] public void Render_ShouldHandleRescheduleStatus() { var sessionId = Guid.NewGuid(); var sessions = new[] { new SessionBatchDto(sessionId, DateTime.UtcNow, "Rescheduled", 4, "") }; var participants = Array.Empty(); var view = SessionBatchViewBuilder.Build("Test", sessions, participants); var (embeds, actionRows) = DiscordSessionBatchRenderer.Render(view); Assert.Single(embeds); Assert.Single(actionRows); // not cancelled → actions present } [Fact] public void Render_ShouldUseBlueColorForConfirmedSessions() { var sessionId = Guid.NewGuid(); var sessions = new[] { new SessionBatchDto(sessionId, DateTime.UtcNow, SessionStatus.Confirmed, 4, "https://example.com/game") }; var participants = Array.Empty(); var view = SessionBatchViewBuilder.Build("Test", sessions, participants); var (embeds, _) = DiscordSessionBatchRenderer.Render(view); Assert.Equal(0x5865F2, embeds[0].Color.RawValue); } [Fact] public void Render_ShouldShowEmptyPlayerDescription() { var sessionId = Guid.NewGuid(); var sessions = new[] { new SessionBatchDto(sessionId, DateTime.UtcNow, SessionStatus.Planned, 4, "https://example.com/game") }; var participants = Array.Empty(); var view = SessionBatchViewBuilder.Build("Test", sessions, participants); var (embeds, _) = DiscordSessionBatchRenderer.Render(view); Assert.Contains("Пока никто не записался", embeds[0].Description); } [Fact] public void Render_ShouldSetEmbedUrlWhenJoinLinkPresent() { var sessionId = Guid.NewGuid(); var sessions = new[] { new SessionBatchDto(sessionId, DateTime.UtcNow, SessionStatus.Planned, 4, "https://example.com/game") }; var participants = Array.Empty(); var view = SessionBatchViewBuilder.Build("Test", sessions, participants); var (embeds, _) = DiscordSessionBatchRenderer.Render(view); Assert.Equal("https://example.com/game", embeds[0].Url); } [Fact] public void Render_ShouldNormalizeBareDomainJoinLinkForEmbedUrl() { var sessionId = Guid.NewGuid(); var sessions = new[] { new SessionBatchDto(sessionId, DateTime.UtcNow, SessionStatus.Planned, 4, "mobaxterm.mobatek.net/game") }; var participants = Array.Empty(); var view = SessionBatchViewBuilder.Build("Test", sessions, participants); var (embeds, _) = DiscordSessionBatchRenderer.Render(view); Assert.Equal("https://mobaxterm.mobatek.net/game", embeds[0].Url); } [Fact] public void Render_ShouldNotSetEmbedUrlWhenJoinLinkIsNotHttpUrl() { var sessionId = Guid.NewGuid(); var sessions = new[] { new SessionBatchDto(sessionId, DateTime.UtcNow, SessionStatus.Planned, 4, "test") }; var participants = Array.Empty(); var view = SessionBatchViewBuilder.Build("Test", sessions, participants); var (embeds, _) = DiscordSessionBatchRenderer.Render(view); Assert.Null(embeds[0].Url); } [Fact] public void Render_ShouldEmbedCorrectFieldValues() { var sessionId = Guid.NewGuid(); var sessions = new[] { new SessionBatchDto(sessionId, DateTime.UtcNow, SessionStatus.Planned, 4, "https://example.com/game") }; var participants = new[] { new ParticipantBatchDto(sessionId, "Alice", "alice", ParticipantRegistrationStatus.Active), new ParticipantBatchDto(sessionId, "Bob", null, ParticipantRegistrationStatus.Waitlisted) }; var view = SessionBatchViewBuilder.Build("Test", sessions, participants); var (embeds, _) = DiscordSessionBatchRenderer.Render(view); var embed = embeds[0]; var fields = embed.Fields!.ToList(); Assert.Equal(3, fields.Count); Assert.Equal("👥 Заполненность", fields[0].Name); Assert.Equal("1/4", fields[0].Value); Assert.Equal("⏳ Лист ожидания", fields[1].Name); Assert.Equal("1", fields[1].Value); Assert.Equal("📊 Статус", fields[2].Name); Assert.Equal("Запланирована", fields[2].Value); } }