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;

/// <summary>
/// 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.
/// </summary>
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<WizardBot>.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 = DateTime.UtcNow,
        UpdatedAt = DateTime.UtcNow,
        ExpiresAt = DateTime.UtcNow.AddHours(24),
    };

    /// <summary>
    /// Build the platform-neutral <see cref="WizardInteraction"/> the
    /// wizard now consumes. Pre-V112 callers passed
    /// <c>Telegram.Bot.Types.Update</c> directly; tests now build the
    /// neutral interaction via the same mapper the production code uses.
    /// </summary>
    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);
    }

    /// <summary>
    /// Build a text-style <see cref="WizardInteraction"/> mirroring what
    /// <c>WizardInteractionMapper</c> would produce for a Telegram text
    /// message.
    /// </summary>
    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}");
    }

    /// <summary>
    /// Build a photo-style <see cref="WizardInteraction"/> mirroring
    /// what <c>WizardInteractionMapper</c> would produce for a Telegram
    /// photo message.
    /// </summary>
    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}");
    }

    /// <summary>
    /// Build a Telegram <see cref="Update"/> carrying a callback query.
    /// Used by router-level tests that exercise
    /// <c>UpdateRouter.RouteAsync</c> end-to-end.
    /// </summary>
    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 },
            },
        },
    };

    /// <summary>
    /// Build a Telegram <see cref="Update"/> carrying a text message.
    /// Used by router-level tests that exercise
    /// <c>UpdateRouter.RouteAsync</c> end-to-end.
    /// </summary>
    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" },
        },
    };
}

/// <summary>
/// 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.
/// </summary>
internal sealed class FakeWizardDraftRepository : IWizardDraftRepository
{
    private readonly Dictionary<Guid, WizardDraft> store = new();

    public List<Guid> DeletedIds { get; } = new();

    public List<WizardDraft> Upserts { get; } = new();

    public int ExpiredDeleted { get; set; }

    public void Seed(WizardDraft draft) => store[draft.Id] = draft;

    public Task<WizardDraft?> 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<WizardDraft?>(d);
            }
        }
        return Task.FromResult<WizardDraft?>(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<int> DeleteExpiredAsync(CancellationToken ct)
    {
        var count = ExpiredDeleted;
        ExpiredDeleted = 0;
        return Task.FromResult(count);
    }
}

/// <summary>
/// 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 <c>ITelegramWizardMessenger</c> recorders so existing test
/// assertions (<c>edit.ChatId</c>, <c>edit.Text</c>, …) keep working
/// after the refactor.
/// </summary>
internal sealed class FakeWizardMessenger : IWizardMessenger
{
    public List<(long ChatId, int? ThreadId, long MsgId, string Text)> Edits { get; } = new();

    public List<string> AnsweredCallbacks { get; } = new();

    public List<(long ChatId, int? ThreadId, string Text)> Sends { get; } = new();

    public List<(string OwnerId, IReadOnlyList<WizardAction> Actions)> EditActions { get; } = new();

    public IReadOnlyList<WizardClubOption> Clubs { get; set; } = Array.Empty<WizardClubOption>();

    public Task<string> EditDraftMessageAsync(
        WizardDraft draft,
        string text,
        IReadOnlyList<WizardAction> 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<string> SendDraftMessageAsync(
        WizardDraft draft,
        string text,
        IReadOnlyList<WizardAction> 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<IReadOnlyList<WizardClubOption>> GetOwnerClubsAsync(string ownerId, CancellationToken ct)
        => Task.FromResult(Clubs);
}
