using System;
using System.Text.Json;
using GmRelay.Shared.Features.Sessions.CreateSession.Wizard;
using static GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard.WizardTestFakes;

namespace GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard;

/// <summary>
/// Verifies the wizard's state machine: clicking each Choice callback should
/// advance the draft to the expected next step and persist it.
/// </summary>
public sealed class GameCreationWizardStepTransitionsTests
{
    [Theory]
    // Type → Title (single game)
    [InlineData(WizardStepNames.Type, "single", WizardStepNames.Title)]
    // Type → Title (pool)
    [InlineData(WizardStepNames.Type, "pool", WizardStepNames.Title)]
    // System → Duration (a known system code)
    [InlineData(WizardStepNames.System, "Dnd5e", WizardStepNames.Duration)]
    // Duration → DateTime (single, no maxPlayers yet)
    [InlineData(WizardStepNames.Duration, "240", WizardStepNames.DateTime)]
    // Capacity → Visibility (only explicit no-limit can skip numeric capacity)
    [InlineData(WizardStepNames.Capacity, "no_limit", WizardStepNames.Visibility)]
    // Visibility → Publish (public, no club)
    [InlineData(WizardStepNames.Visibility, "public", WizardStepNames.Publish)]
    // Visibility → PickClub
    [InlineData(WizardStepNames.Visibility, "club", WizardStepNames.PickClub)]
    [InlineData(WizardStepNames.Visibility, "members", WizardStepNames.PickClub)]
    [InlineData(WizardStepNames.Visibility, "pickclub", WizardStepNames.PickClub)]
    // Publish → Confirm
    [InlineData(WizardStepNames.Publish, "yes", WizardStepNames.Confirm)]
    [InlineData(WizardStepNames.Publish, "no", WizardStepNames.Confirm)]
    public async Task ChoiceCallback_AdvancesToExpectedStep(
        string fromStep, string choice, string expectedStep)
    {
        var wizard = BuildWizard(out var drafts, out _);
        var draft = NewDraft(fromStep, PayloadForStep(fromStep));
        drafts.Seed(draft);

        var data = WizardCallbackData.Choice(fromStep, choice);
        await wizard.HandleInteractionAsync(CallbackInteraction(data, ownerId: draft.OwnerId), draft, CancellationToken.None);

        Assert.Equal(expectedStep, draft.Step);
        Assert.NotEmpty(drafts.Upserts); // was persisted
    }

    [Fact]
    public async Task PoolSystemDuration_PreselectedButton_AdvancesToVisibility()
    {
        var wizard = BuildWizard(out var drafts, out _);
        var payload = new WizardPayload
        {
            Type = WizardCreationType.Pool,
            Title = "Pool",
        };
        var draft = NewDraft(WizardStepNames.PoolSystemDuration, payload);
        drafts.Seed(draft);

        var data = WizardCallbackData.Choice(WizardStepNames.PoolSystemDuration, "Dnd5e:240");
        await wizard.HandleInteractionAsync(CallbackInteraction(data, ownerId: draft.OwnerId), draft, CancellationToken.None);

        Assert.Equal(WizardStepNames.Visibility, draft.Step);
        using var doc = JsonDocument.Parse(draft.PayloadJson);
        var root = doc.RootElement;
        Assert.True(root.TryGetProperty("system", out var sys));
        Assert.Equal("Dnd5e", sys.GetString());
        Assert.True(root.TryGetProperty("durationMinutes", out var dur));
        Assert.Equal(240, dur.GetInt32());
    }

    [Fact]
    public async Task NoLimitCapacityButton_AdvancesToVisibility_AndLeavesMaxPlayersNull()
    {
        var wizard = BuildWizard(out var drafts, out _);
        var draft = NewDraft(WizardStepNames.Capacity, PayloadForStep(WizardStepNames.Capacity));
        drafts.Seed(draft);

        var data = WizardCallbackData.Choice(WizardStepNames.Capacity, "no_limit");
        await wizard.HandleInteractionAsync(CallbackInteraction(data, ownerId: draft.OwnerId), draft, CancellationToken.None);

        Assert.Equal(WizardStepNames.Visibility, draft.Step);
        using var doc = JsonDocument.Parse(draft.PayloadJson);
        var root = doc.RootElement;
        Assert.True(root.TryGetProperty("single", out var single));
        // WizardPayloadJsonContext имеет DefaultIgnoreCondition=WhenWritingNull,
        // поэтому null-MaxPlayers просто не пишется. Оба варианта
        // (отсутствует / JsonValueKind.Null) десериализуются обратно в null
        // и уйдут в БД как NULL — то есть «без лимита».
        if (single.TryGetProperty("maxPlayers", out var maxPlayers))
        {
            Assert.True(
                maxPlayers.ValueKind == JsonValueKind.Null,
                $"expected maxPlayers to be null (no limit), got {maxPlayers.ValueKind}");
        }
    }

    [Fact]
    public async Task StaleCapacityWaitlistCallback_WithoutCapacity_StaysOnCurrentStep()
    {
        // A stale waitlist button from Capacity must not move a draft forward
        // unless MaxPlayers is already set. Otherwise users can reach Confirm
        // with a missing capacity and get "Не заполнены поля: лимит мест".
        var wizard = BuildWizard(out var drafts, out _);
        var draft = NewDraft(WizardStepNames.System);
        drafts.Seed(draft);

        var data = WizardCallbackData.Choice(WizardStepNames.Capacity, "waitlist:on");
        await wizard.HandleInteractionAsync(CallbackInteraction(data, ownerId: draft.OwnerId), draft, CancellationToken.None);

        Assert.Equal(WizardStepNames.System, draft.Step);
    }

    [Fact]
    public async Task PickClub_ValidGuid_AdvancesToPublishOnFirstClick()
    {
        var wizard = BuildWizard(out var drafts, out _);
        var clubId = Guid.NewGuid();
        var payload = new WizardPayload
        {
            Type = WizardCreationType.Single,
            Title = "T",
            System = "Dnd5e",
            DurationMinutes = 240,
            Visibility = WizardVisibility.Club,
        };
        var draft = NewDraft(WizardStepNames.PickClub, payload);
        drafts.Seed(draft);

        var data = WizardCallbackData.Choice(WizardStepNames.PickClub, clubId.ToString());
        await wizard.HandleInteractionAsync(CallbackInteraction(data, ownerId: draft.OwnerId), draft, CancellationToken.None);

        Assert.Equal(WizardStepNames.Publish, draft.Step);
        using var doc = JsonDocument.Parse(draft.PayloadJson);
        var root = doc.RootElement;
        Assert.True(root.TryGetProperty("clubId", out var clubIdJson));
        Assert.Equal(clubId, clubIdJson.GetGuid());
    }

    [Fact]
    public async Task PickClub_InvalidGuid_StaysOnPickClub()
    {
        var wizard = BuildWizard(out var drafts, out _);
        var payload = new WizardPayload
        {
            Type = WizardCreationType.Single,
            Title = "T",
            System = "Dnd5e",
            DurationMinutes = 240,
            Visibility = WizardVisibility.Club,
        };
        var draft = NewDraft(WizardStepNames.PickClub, payload);
        drafts.Seed(draft);

        var data = WizardCallbackData.Choice(WizardStepNames.PickClub, "not-a-guid");
        await wizard.HandleInteractionAsync(CallbackInteraction(data, ownerId: draft.OwnerId), draft, CancellationToken.None);

        Assert.Equal(WizardStepNames.PickClub, draft.Step);
    }

    /// <summary>
    /// Builds a payload that already contains the values the wizard expects to
    /// be set when the user is sitting on a given step. Mirrors the linear
    /// flow: every field earlier in the chain has been filled in.
    /// </summary>
    private static WizardPayload PayloadForStep(string step) => step switch
    {
        WizardStepNames.Type or WizardStepNames.Title => new WizardPayload(),
        WizardStepNames.System => new WizardPayload { Type = WizardCreationType.Single, Title = "T" },
        WizardStepNames.Duration => new WizardPayload { Type = WizardCreationType.Single, Title = "T", System = "Dnd5e" },
        WizardStepNames.Capacity => new WizardPayload
        {
            Type = WizardCreationType.Single,
            Title = "T",
            System = "Dnd5e",
            DurationMinutes = 240,
            Single = new WizardSingleInput { ScheduledAt = DateTimeOffset.UtcNow.AddDays(1) },
        },
        WizardStepNames.Visibility => new WizardPayload
        {
            Type = WizardCreationType.Single,
            Title = "T",
            System = "Dnd5e",
            DurationMinutes = 240,
        },
        WizardStepNames.PickClub => new WizardPayload
        {
            Type = WizardCreationType.Single,
            Title = "T",
            System = "Dnd5e",
            DurationMinutes = 240,
            Visibility = WizardVisibility.Club,
        },
        WizardStepNames.Publish => new WizardPayload
        {
            Type = WizardCreationType.Single,
            Title = "T",
            System = "Dnd5e",
            DurationMinutes = 240,
            Visibility = WizardVisibility.Public,
        },
        WizardStepNames.Confirm => new WizardPayload
        {
            Type = WizardCreationType.Single,
            Title = "T",
            System = "Dnd5e",
            DurationMinutes = 240,
            Visibility = WizardVisibility.Public,
        },
        _ => new WizardPayload(),
    };
}
