using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using GmRelay.Bot.Features.Sessions.CreateSession.Wizard; using GmRelay.Shared.Features.Sessions.CreateSession.Wizard; using Microsoft.Extensions.Logging.Abstractions; using Telegram.Bot.Types; using Telegram.Bot.Types.ReplyMarkups; using WizardBot = GmRelay.Bot.Features.Sessions.CreateSession.Wizard.GameCreationWizard; using WizardMessenger = GmRelay.Bot.Features.Sessions.CreateSession.Wizard.ITelegramWizardMessenger; namespace GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard; /// /// Hand-rolled test doubles and helpers for wizard unit tests. The project /// convention is to use fakes (not a mocking framework) so the suite stays /// AOT-friendly and the production code doesn't grow virtual members just /// for tests. /// internal static class WizardTestFakes { public static WizardBot BuildWizard(out FakeWizardDraftRepository drafts, out FakeWizardMessenger messenger) { drafts = new FakeWizardDraftRepository(); messenger = new FakeWizardMessenger(); return new WizardBot(drafts, messenger, NullLogger.Instance); } public static WizardDraft NewDraft(string step, WizardPayload? payload = null, long ownerId = 100) => new() { Id = Guid.NewGuid(), ChatId = 42, MessageThreadId = null, OwnerTelegramId = ownerId, Step = step, DraftMessageId = 7, PayloadJson = System.Text.Json.JsonSerializer.Serialize( payload ?? new WizardPayload(), WizardPayloadJsonContext.Default.WizardPayload), CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow, ExpiresAt = DateTimeOffset.UtcNow.AddHours(24), }; public static Update CallbackUpdate(string data, long ownerId = 100) => new() { CallbackQuery = new CallbackQuery { Id = "cb-1", Data = data, From = new User { Id = ownerId, FirstName = "GM" }, Message = new Message { Chat = new Chat { Id = 42 }, }, }, }; public static Update TextUpdate(string text, long ownerId = 100) => new() { Message = new Message { Text = text, Chat = new Chat { Id = 42 }, From = new User { Id = ownerId, FirstName = "GM" }, }, }; } /// /// Records every call the wizard makes against the draft repository. Backed by /// an in-memory dictionary so tests can pre-seed an "active" draft for the /// wizard to mutate. /// internal sealed class FakeWizardDraftRepository : IWizardDraftRepository { private readonly Dictionary store = new(); public List DeletedIds { get; } = new(); public List Upserts { get; } = new(); public int ExpiredDeleted { get; set; } public void Seed(WizardDraft draft) => store[draft.Id] = draft; public Task GetActiveAsync(long chatId, int? messageThreadId, long ownerTelegramId, CancellationToken ct) { foreach (var d in store.Values) { if (d.ChatId == chatId && d.MessageThreadId == messageThreadId && d.OwnerTelegramId == ownerTelegramId && d.ExpiresAt > DateTimeOffset.UtcNow) { return Task.FromResult(d); } } return Task.FromResult(null); } public Task UpsertAsync(WizardDraft draft, CancellationToken ct) { // Clone so tests can compare state without aliasing. Upserts.Add(new WizardDraft { Id = draft.Id, ChatId = draft.ChatId, MessageThreadId = draft.MessageThreadId, OwnerTelegramId = draft.OwnerTelegramId, Step = draft.Step, PayloadJson = draft.PayloadJson, DraftMessageId = draft.DraftMessageId, CreatedAt = draft.CreatedAt, UpdatedAt = draft.UpdatedAt, ExpiresAt = draft.ExpiresAt, }); store[draft.Id] = draft; return Task.CompletedTask; } public Task DeleteAsync(Guid id, CancellationToken ct) { DeletedIds.Add(id); store.Remove(id); return Task.CompletedTask; } public Task DeleteExpiredAsync(CancellationToken ct) { var count = ExpiredDeleted; ExpiredDeleted = 0; return Task.FromResult(count); } } /// /// Records every call the wizard makes against the messenger. Default return /// values (empty clubs, message-id 1) match what the wizard expects to see /// in steady state. /// internal sealed class FakeWizardMessenger : ITelegramWizardMessenger { public List<(long ChatId, int? ThreadId, long MsgId, string Text)> Edits { get; } = new(); public List AnsweredCallbacks { get; } = new(); public List<(long ChatId, int? ThreadId, string Text)> Sends { get; } = new(); public IReadOnlyList Clubs { get; set; } = Array.Empty(); public Task EditMessageTextAsync( long chatId, int? messageThreadId, long messageId, string text, InlineKeyboardMarkup keyboard, CancellationToken ct) { Edits.Add((chatId, messageThreadId, messageId, text)); return Task.FromResult(messageId); } public Task SendGroupMessageAsync( long chatId, int? messageThreadId, string text, InlineKeyboardMarkup keyboard, CancellationToken ct) { Sends.Add((chatId, messageThreadId, text)); return Task.FromResult(99L); } public Task AnswerCallbackAsync(string callbackId, string? text, CancellationToken ct) { AnsweredCallbacks.Add(callbackId); return Task.CompletedTask; } public Task> GetGmClubsAsync(long ownerTelegramId, CancellationToken ct) => Task.FromResult(Clubs); }