using BotRescheduleHandler = GmRelay.Bot.Features.Sessions.RescheduleSession.HandleRescheduleTimeInputHandler;
using GmRelay.DiscordBot.Rendering;
using GmRelay.Shared.Domain;
using GmRelay.Shared.Features.Sessions.RescheduleSession;
using GmRelay.Shared.Rendering;
using NetCord.Rest;
using Telegram.Bot.Types.ReplyMarkups;

namespace GmRelay.Bot.Tests.Features.Landing;

public sealed class DiscordLandingPromisesSmokeTests
{
    private sealed record SmokeParseResult(
        string Title,
        string Link,
        int? MaxPlayers,
        IReadOnlyList<DateTimeOffset> ScheduledTimes);

    [Fact]
    public void Smoke_ShouldCoverDiscordLandingPromisesWithoutExternalDiscordApi()
    {
        var parseResult = BuildRecurringSessionParseResult();

        Assert.Equal(3, parseResult.ScheduledTimes.Count);
        Assert.Equal(2, parseResult.MaxPlayers);
        Assert.Equal(TimeSpan.FromDays(7), parseResult.ScheduledTimes[1] - parseResult.ScheduledTimes[0]);

        var scenario = DiscordLandingSmokeScenario.Publish(parseResult, SessionNotificationMode.GroupAndDirect);
        var publishedCallbacks = CallbackData(scenario.LastMessage.ActionRows);

        Assert.Contains(scenario.LastMessage.Embeds, e => e.Title!.Contains("Landing Promise Smoke"));
        Assert.Equal(6, publishedCallbacks.Count);
        Assert.All(scenario.Sessions, session =>
        {
            Assert.Contains($"join_session:{session.Id}", publishedCallbacks);
            Assert.Contains($"leave_session:{session.Id}", publishedCallbacks);
            Assert.Contains(scenario.LastMessage.Embeds, embed => embed.Title!.Contains(session.ScheduledAt.FormatMoscow()));
        });

        var firstSessionId = scenario.Sessions[0].Id;
        var alice = scenario.Join(firstSessionId, 1001UL, "Alice");
        var bob = scenario.Join(firstSessionId, 1002UL, "Bob");
        var carol = scenario.Join(firstSessionId, 1003UL, "Carol");

        Assert.Equal(ParticipantRegistrationStatus.Active, scenario.RegistrationStatus(firstSessionId, alice));
        Assert.Equal(ParticipantRegistrationStatus.Active, scenario.RegistrationStatus(firstSessionId, bob));
        Assert.Equal(ParticipantRegistrationStatus.Waitlisted, scenario.RegistrationStatus(firstSessionId, carol));

        var firstSessionEmbed = scenario.LastMessage.Embeds.First(e => e.Title!.Contains(scenario.Sessions[0].ScheduledAt.FormatMoscow()));
        Assert.Equal("2/2", firstSessionEmbed.Fields!.First().Value);
        Assert.Contains("Alice", firstSessionEmbed.Description);
        Assert.Contains("Bob", firstSessionEmbed.Description);
        Assert.Contains("Carol", firstSessionEmbed.Description);

        scenario.Leave(firstSessionId, alice);

        Assert.False(scenario.HasParticipant(firstSessionId, alice));
        Assert.Equal(ParticipantRegistrationStatus.Active, scenario.RegistrationStatus(firstSessionId, carol));
        firstSessionEmbed = scenario.LastMessage.Embeds.First(e => e.Title!.Contains(scenario.Sessions[0].ScheduledAt.FormatMoscow()));
        Assert.DoesNotContain("Alice", firstSessionEmbed.Description);
        Assert.Contains("Carol", firstSessionEmbed.Description);

        scenario.MarkRsvpConfirmed(firstSessionId, bob);
        scenario.MarkRsvpConfirmed(firstSessionId, carol);

        var option1Id = Guid.NewGuid();
        var option2Id = Guid.NewGuid();
        var options = new[]
        {
            new RescheduleOptionDto(option1Id, 1, new DateTimeOffset(2026, 5, 29, 16, 30, 0, TimeSpan.Zero)),
            new RescheduleOptionDto(option2Id, 2, new DateTimeOffset(2026, 5, 30, 15, 0, 0, TimeSpan.Zero))
        };
        var deadline = new DateTimeOffset(2026, 5, 20, 18, 0, 0, TimeSpan.Zero);
        var voteParticipants = scenario.ActiveVoteParticipants(firstSessionId);
        var voteMessage = BotRescheduleHandler.BuildVotingMessage(
            scenario.Title,
            scenario.Sessions[0].ScheduledAt,
            deadline,
            options,
            voteParticipants,
            []);
        var voteKeyboard = BotRescheduleHandler.BuildVotingKeyboard(options);

        Assert.Contains("Landing Promise Smoke", voteMessage);
        Assert.Contains("0/2", voteMessage);
        Assert.Contains($"reschedule_vote:{option1Id}", CallbackData(voteKeyboard));
        Assert.Contains($"reschedule_vote:{option2Id}", CallbackData(voteKeyboard));

        var votes = voteParticipants
            .Select(participant => new RescheduleOptionVoteDto(
                option2Id,
                participant.PlayerId,
                participant.DisplayName,
                participant.TelegramUsername))
            .ToList();
        var decision = RescheduleVoteRules.SelectWinner(
            options.Select(option => new RescheduleOptionVoteCount(
                option.OptionId,
                votes.Count(vote => vote.OptionId == option.OptionId))).ToList());

        Assert.Equal(RescheduleVoteOutcome.Approved, decision.Outcome);
        Assert.Equal(option2Id, decision.SelectedOptionId);

        scenario.ApplyReschedule(firstSessionId, options[1].ProposedAt);

        Assert.Equal(options[1].ProposedAt.UtcDateTime, scenario.Sessions[0].ScheduledAt);
        Assert.Equal(GmRelay.Shared.Domain.RsvpStatus.Pending, scenario.RsvpStatus(firstSessionId, bob));
        Assert.Equal(GmRelay.Shared.Domain.RsvpStatus.Pending, scenario.RsvpStatus(firstSessionId, carol));
        Assert.Contains(scenario.LastMessage.Embeds, e => e.Title!.Contains(options[1].ProposedAt.UtcDateTime.FormatMoscow()));

        Assert.Collection(
            scenario.Messenger.DirectMessages.Select(message => message.DiscordId).Order(),
            discordId => Assert.Equal(1002UL, discordId),
            discordId => Assert.Equal(1003UL, discordId));
        Assert.All(scenario.Messenger.DirectMessages, message =>
        {
            Assert.Contains("Landing Promise Smoke", message.Text);
            Assert.Contains(options[1].ProposedAt.UtcDateTime.FormatMoscow(), message.Text);
        });

        var editsBeforeDashboardUpdate = scenario.Messenger.Edits.Count;

        scenario.UpdateBatchFromDashboard("Landing Promise Smoke - Dashboard Sync");

        Assert.True(scenario.Messenger.Edits.Count > editsBeforeDashboardUpdate);
        Assert.Contains(scenario.LastMessage.Embeds, e => e.Title!.Contains("Landing Promise Smoke - Dashboard Sync"));
        firstSessionEmbed = scenario.LastMessage.Embeds.First(e => e.Title!.Contains(scenario.Sessions[0].ScheduledAt.FormatMoscow()));
        Assert.Contains("Bob", firstSessionEmbed.Description);
        Assert.Contains("Carol", firstSessionEmbed.Description);
    }

    private static SmokeParseResult BuildRecurringSessionParseResult() =>
        new(
            Title: "Landing Promise Smoke",
            Link: "https://example.test/table",
            MaxPlayers: 2,
            ScheduledTimes: new[]
            {
                new DateTimeOffset(2026, 5, 15, 16, 30, 0, TimeSpan.Zero),
                new DateTimeOffset(2026, 5, 22, 16, 30, 0, TimeSpan.Zero),
                new DateTimeOffset(2026, 5, 29, 16, 30, 0, TimeSpan.Zero),
            });

    private static IReadOnlyList<string> CallbackData(IReadOnlyList<ActionRowProperties> actionRows) =>
        actionRows
            .SelectMany(row => row)
            .OfType<ButtonProperties>()
            .Select(button => button.CustomId)
            .OfType<string>()
            .ToList();

    private static IReadOnlyList<string> CallbackData(InlineKeyboardMarkup markup) =>
        markup.InlineKeyboard
            .SelectMany(row => row)
            .Select(button => button.CallbackData)
            .OfType<string>()
            .ToList();

    private sealed class DiscordLandingSmokeScenario
    {
        private readonly List<SmokeSession> sessions;
        private readonly List<SmokeParticipant> participants = [];
        private readonly SessionNotificationMode notificationMode;

        private DiscordLandingSmokeScenario(
            string title,
            IReadOnlyList<DateTimeOffset> scheduledTimes,
            int? maxPlayers,
            string joinLink,
            SessionNotificationMode notificationMode)
        {
            Title = title;
            this.notificationMode = notificationMode;
            sessions = scheduledTimes
                .Select(scheduledAt => new SmokeSession(
                    Guid.NewGuid(),
                    scheduledAt.UtcDateTime,
                    SessionStatus.Planned,
                    maxPlayers,
                    joinLink))
                .ToList();
        }

        public string Title { get; private set; }
        public FakeDiscordMessenger Messenger { get; } = new();
        public IReadOnlyList<SmokeSession> Sessions => sessions;
        public FakeDiscordMessage LastMessage => Messenger.LastMessage;

        public static DiscordLandingSmokeScenario Publish(
            SmokeParseResult parseResult,
            SessionNotificationMode notificationMode)
        {
            var scenario = new DiscordLandingSmokeScenario(
                parseResult.Title,
                parseResult.ScheduledTimes,
                parseResult.MaxPlayers,
                parseResult.Link,
                notificationMode);

            scenario.RenderBatch();
            return scenario;
        }

        public Guid Join(Guid sessionId, ulong discordId, string displayName)
        {
            var session = sessions.Single(value => value.Id == sessionId);
            var activeParticipants = participants.Count(participant =>
                participant.SessionId == sessionId &&
                participant.RegistrationStatus == ParticipantRegistrationStatus.Active);
            var registrationStatus = SessionCapacityRules.DecideJoinStatus(
                session.MaxPlayers,
                activeParticipants);
            var playerId = Guid.NewGuid();

            participants.Add(new SmokeParticipant(
                sessionId,
                playerId,
                discordId,
                displayName,
                registrationStatus,
                GmRelay.Shared.Domain.RsvpStatus.Pending,
                participants.Count));

            RenderBatch();
            return playerId;
        }

        public void Leave(Guid sessionId, Guid playerId)
        {
            var participant = participants.Single(value =>
                value.SessionId == sessionId &&
                value.PlayerId == playerId);
            participants.Remove(participant);

            var session = sessions.Single(value => value.Id == sessionId);
            var activeParticipantsAfterLeave = participants.Count(value =>
                value.SessionId == sessionId &&
                value.RegistrationStatus == ParticipantRegistrationStatus.Active);
            var waitlistedParticipants = participants.Count(value =>
                value.SessionId == sessionId &&
                value.RegistrationStatus == ParticipantRegistrationStatus.Waitlisted);

            if (SessionCapacityRules.ShouldPromoteAfterParticipantLeaves(
                    participant.RegistrationStatus,
                    session.MaxPlayers,
                    activeParticipantsAfterLeave,
                    waitlistedParticipants))
            {
                var promoted = participants
                    .Where(value =>
                        value.SessionId == sessionId &&
                        value.RegistrationStatus == ParticipantRegistrationStatus.Waitlisted)
                    .OrderBy(value => value.JoinOrder)
                    .First();

                promoted.RegistrationStatus = ParticipantRegistrationStatus.Active;
                promoted.RsvpStatus = GmRelay.Shared.Domain.RsvpStatus.Pending;
            }

            RenderBatch();
        }

        public bool HasParticipant(Guid sessionId, Guid playerId) =>
            participants.Any(value => value.SessionId == sessionId && value.PlayerId == playerId);

        public string RegistrationStatus(Guid sessionId, Guid playerId) =>
            participants.Single(value =>
                value.SessionId == sessionId &&
                value.PlayerId == playerId).RegistrationStatus;

        public string RsvpStatus(Guid sessionId, Guid playerId) =>
            participants.Single(value =>
                value.SessionId == sessionId &&
                value.PlayerId == playerId).RsvpStatus;

        public void MarkRsvpConfirmed(Guid sessionId, Guid playerId)
        {
            participants.Single(value =>
                value.SessionId == sessionId &&
                value.PlayerId == playerId).RsvpStatus = GmRelay.Shared.Domain.RsvpStatus.Confirmed;
        }

        public IReadOnlyList<VoteParticipantDto> ActiveVoteParticipants(Guid sessionId) =>
            participants
                .Where(value =>
                    value.SessionId == sessionId &&
                    value.RegistrationStatus == ParticipantRegistrationStatus.Active)
                .OrderBy(value => value.DisplayName)
                .Select(value => new VoteParticipantDto(
                    value.PlayerId,
                    value.DisplayName,
                    null,
                    0))
                .ToList();

        public void ApplyReschedule(Guid sessionId, DateTimeOffset newScheduledAt)
        {
            sessions.Single(value => value.Id == sessionId).ScheduledAt = newScheduledAt.UtcDateTime;

            foreach (var participant in participants.Where(value =>
                         value.SessionId == sessionId &&
                         value.RegistrationStatus == ParticipantRegistrationStatus.Active))
            {
                participant.RsvpStatus = GmRelay.Shared.Domain.RsvpStatus.Pending;
            }

            RenderBatch();

            if (!notificationMode.ShouldSendDirectMessages())
            {
                return;
            }

            var notification = $"""
                Reschedule approved
                {Title}
                {newScheduledAt.UtcDateTime.FormatMoscow()}
                """;
            foreach (var participant in participants.Where(value =>
                         value.SessionId == sessionId &&
                         value.RegistrationStatus == ParticipantRegistrationStatus.Active))
            {
                Messenger.SendDirectMessage(participant.DiscordId, notification);
            }
        }

        public void UpdateBatchFromDashboard(string title)
        {
            Title = title;
            RenderBatch();
        }

        private void RenderBatch()
        {
            var view = SessionBatchViewBuilder.Build(
                Title,
                sessions
                    .Select(session => new SessionBatchDto(
                        session.Id,
                        session.ScheduledAt,
                        session.Status,
                        session.MaxPlayers,
                        session.JoinLink))
                    .ToList(),
                participants
                    .Select(participant => new ParticipantBatchDto(
                        participant.SessionId,
                        participant.DisplayName,
                        null,
                        participant.RegistrationStatus))
                    .ToList());
            var renderResult = DiscordSessionBatchRenderer.Render(view);

            if (Messenger.HasPublishedMessage)
            {
                Messenger.EditBatchMessage(renderResult.Embeds, renderResult.ActionRows);
            }
            else
            {
                Messenger.PublishBatchMessage(renderResult.Embeds, renderResult.ActionRows);
            }
        }
    }

    private sealed class FakeDiscordMessenger
    {
        private const int BatchMessageId = 7001;

        public List<FakeDiscordMessage> Sends { get; } = [];
        public List<FakeDiscordMessage> Edits { get; } = [];
        public List<FakeDirectMessage> DirectMessages { get; } = [];
        public bool HasPublishedMessage => Sends.Count > 0;
        public FakeDiscordMessage LastMessage => Edits.Count > 0 ? Edits[^1] : Sends[^1];

        public void PublishBatchMessage(IReadOnlyList<EmbedProperties> embeds, IReadOnlyList<ActionRowProperties> actionRows) =>
            Sends.Add(new FakeDiscordMessage(BatchMessageId, embeds, actionRows));

        public void EditBatchMessage(IReadOnlyList<EmbedProperties> embeds, IReadOnlyList<ActionRowProperties> actionRows) =>
            Edits.Add(new FakeDiscordMessage(BatchMessageId, embeds, actionRows));

        public void SendDirectMessage(ulong discordId, string text) =>
            DirectMessages.Add(new FakeDirectMessage(discordId, text));
    }

    private sealed record FakeDiscordMessage(
        int MessageId,
        IReadOnlyList<EmbedProperties> Embeds,
        IReadOnlyList<ActionRowProperties> ActionRows);

    private sealed record FakeDirectMessage(ulong DiscordId, string Text);

    private sealed record SmokeSession(
        Guid Id,
        DateTime ScheduledAt,
        string Status,
        int? MaxPlayers,
        string JoinLink)
    {
        public DateTime ScheduledAt { get; set; } = ScheduledAt;
    }

    private sealed record SmokeParticipant(
        Guid SessionId,
        Guid PlayerId,
        ulong DiscordId,
        string DisplayName,
        string RegistrationStatus,
        string RsvpStatus,
        int JoinOrder)
    {
        public string RegistrationStatus { get; set; } = RegistrationStatus;
        public string RsvpStatus { get; set; } = RsvpStatus;
    }
}
