using System.Threading;
using System.Threading.Tasks;
using GmRelay.Bot.Features.Sessions.CreateSession.Wizard;
using GmRelay.Bot.Infrastructure.Telegram;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using Telegram.Bot;
using Telegram.Bot.Types;
using static GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard.WizardTestFakes;
namespace GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard;
///
/// Verifies that the delegates to the wizard when
/// the GM has an active (non-expired) draft, and falls through to normal
/// handling when no draft is active. We instrument a real wizard via the
/// shared /
/// pair and verify side effects on the messenger (the wizard edits the
/// draft message) — that is the observable signal that
/// wizard.HandleUpdateAsync was called.
///
public sealed class UpdateRouterDelegationTests
{
[Fact]
public async Task ActiveDraft_Existing_RoutesToWizard()
{
var sut = BuildRouter(out var drafts, out var messenger);
var draft = NewDraft(WizardStepNames.Title);
drafts.Seed(draft);
var update = TextUpdate("Curse of Strahd", ownerId: draft.OwnerTelegramId);
await sut.RouteAsync(update, CancellationToken.None);
// Wizard edits the draft message when it processes a title.
Assert.NotEmpty(messenger.Edits);
}
[Fact]
public async Task ActiveDraft_Existing_OnCallback_AlsoRoutesToWizard()
{
var sut = BuildRouter(out var drafts, out _);
var draft = NewDraft(WizardStepNames.Title);
drafts.Seed(draft);
// "wizard:cancel" — wizard owns the cancel callback. The router
// delegates control-callbacks (resume/reset) but lets the wizard
// handle wizard:* callbacks.
var update = CallbackUpdate(WizardCallbackData.Cancel(), ownerId: draft.OwnerTelegramId);
await sut.RouteAsync(update, CancellationToken.None);
// Cancel deletes the draft via the wizard.
Assert.Contains(draft.Id, drafts.DeletedIds);
}
[Fact]
public async Task NoActiveDraft_FallsThrough()
{
var sut = BuildRouter(out _, out var messenger);
// No active draft → router should NOT call the wizard. It will
// attempt to run the /help command via the fallback command path.
// We send a /help message; the router has no draft to act on.
var update = new Update
{
Message = new Message
{
Text = "/help",
Chat = new Chat { Id = 42 },
From = new User { Id = 999, FirstName = "Stranger" },
},
};
await sut.RouteAsync(update, CancellationToken.None);
// The wizard should not have edited anything (no draft was active).
Assert.Empty(messenger.Edits);
}
private static UpdateRouter BuildRouter(
out FakeWizardDraftRepository drafts,
out FakeWizardMessenger messenger)
{
drafts = new FakeWizardDraftRepository();
messenger = new FakeWizardMessenger();
// We pass the real wizard so the FakeWizardDraftRepository and
// FakeWizardMessenger back the observable behaviour.
var wizard = new GameCreationWizard(drafts, messenger, NullLogger.Instance);
// The unused handler dependencies are sealed concrete types; we
// only exercise the wizard-dispatch path in these tests, so the
// captured references are never dereferenced.
var router = new UpdateRouter(
rsvpHandler: null!,
createSessionHandler: null!,
joinSessionHandler: null!,
leaveSessionHandler: null!,
promoteWaitlistedPlayerHandler: null!,
cancelSessionHandler: null!,
deleteSessionHandler: null!,
listSessionsHandler: null!,
exportCalendarHandler: null!,
initiateRescheduleHandler: null!,
rescheduleTimeInputHandler: null!,
rescheduleVoteHandler: null!,
wizard: wizard,
drafts: drafts,
bot: Substitute.For(),
configuration: Substitute.For(),
logger: NullLogger.Instance);
return router;
}
}