using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using GmRelay.Shared.Features.Sessions.CreateSession.Wizard;
using Microsoft.Extensions.Logging.Abstractions;
using Telegram.Bot.Types;
using Telegram.Bot.Types.ReplyMarkups;
using WizardBot = GmRelay.Shared.Features.Sessions.CreateSession.Wizard.GameCreationWizard;
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 const string PlatformName = "Telegram";
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,
OwnerId = ownerId.ToString(CultureInfo.InvariantCulture),
Platform = PlatformName,
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),
};
///
/// Build the platform-neutral the
/// wizard now consumes. Pre-V112 callers passed
/// Telegram.Bot.Types.Update directly; tests now build the
/// neutral interaction via the same mapper the production code uses.
///
public static WizardInteraction CallbackInteraction(
string data, string ownerId = "100", string callbackId = "cb-1")
{
return new WizardInteraction(
OwnerId: ownerId,
Text: null,
CallbackPayload: data,
PhotoFileId: null,
PhotoUrl: null,
InteractionId: callbackId);
}
///
/// Build a text-style mirroring what
/// WizardInteractionMapper would produce for a Telegram text
/// message.
///
public static WizardInteraction TextInteraction(
string text, string ownerId = "100", int messageId = 1)
{
return new WizardInteraction(
OwnerId: ownerId,
Text: text,
CallbackPayload: null,
PhotoFileId: null,
PhotoUrl: null,
InteractionId: $"msg-{messageId}");
}
///
/// Build a photo-style mirroring
/// what WizardInteractionMapper would produce for a Telegram
/// photo message.
///
public static WizardInteraction PhotoInteraction(
string fileId, string ownerId = "100", int messageId = 1)
{
return new WizardInteraction(
OwnerId: ownerId,
Text: null,
CallbackPayload: null,
PhotoFileId: fileId,
PhotoUrl: null,
InteractionId: $"msg-{messageId}");
}
///
/// Build a Telegram carrying a callback query.
/// Used by router-level tests that exercise
/// UpdateRouter.RouteAsync end-to-end.
///
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 },
},
},
};
///
/// Build a Telegram carrying a text message.
/// Used by router-level tests that exercise
/// UpdateRouter.RouteAsync end-to-end.
///
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(string platform, string ownerId, CancellationToken ct)
{
foreach (var d in store.Values)
{
if (d.Platform == platform &&
d.OwnerId == ownerId &&
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,
OwnerId = draft.OwnerId,
Platform = draft.Platform,
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 99) match what the wizard
/// expects to see in steady state. The recorded tuple shapes match
/// the old ITelegramWizardMessenger recorders so existing test
/// assertions (edit.ChatId, edit.Text, …) keep working
/// after the refactor.
///
internal sealed class FakeWizardMessenger : IWizardMessenger
{
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 List<(string OwnerId, IReadOnlyList Actions)> EditActions { get; } = new();
public IReadOnlyList Clubs { get; set; } = Array.Empty();
public Task EditDraftMessageAsync(
WizardDraft draft,
string text,
IReadOnlyList keyboard,
CancellationToken ct)
{
Edits.Add((
long.TryParse(draft.ChatId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var chatId) ? chatId : 0,
int.TryParse(draft.MessageThreadId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var threadId) ? threadId : (int?)null,
long.TryParse(draft.DraftMessageId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var msgId) ? msgId : 0,
text));
EditActions.Add((draft.OwnerId, keyboard));
return Task.FromResult(draft.DraftMessageId ?? "0");
}
public Task SendDraftMessageAsync(
WizardDraft draft,
string text,
IReadOnlyList keyboard,
CancellationToken ct)
{
Sends.Add((
long.TryParse(draft.ChatId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var chatId) ? chatId : 0,
int.TryParse(draft.MessageThreadId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var threadId) ? threadId : (int?)null,
text));
return Task.FromResult("99");
}
public Task AnswerInteractionAsync(string interactionId, string? text, CancellationToken ct)
{
AnsweredCallbacks.Add(interactionId);
return Task.CompletedTask;
}
public Task> GetOwnerClubsAsync(string ownerId, CancellationToken ct)
=> Task.FromResult(Clubs);
}