using GmRelay.Bot.Features.Sessions.CreateSession;
using GmRelay.Bot.Features.Sessions.RescheduleSession;
using GmRelay.Shared.Domain;
using GmRelay.Shared.Rendering;
using Telegram.Bot.Types.ReplyMarkups;
using GmRelay.Bot.Infrastructure.Telegram;

namespace GmRelay.Bot.Tests.Features.Landing;

public sealed class TelegramLandingPromisesSmokeTests
{
    [Fact]
    public void Smoke_ShouldCoverTelegramLandingPromisesWithoutExternalTelegramApi()
    {
        var nowUtc = new DateTimeOffset(2026, 5, 1, 12, 0, 0, TimeSpan.Zero);
        var parseResult = NewSessionCommandParser.Parse(BuildRecurringSessionCommand(), nowUtc);

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

        var scenario = TelegramLandingSmokeScenario.Publish(parseResult, SessionNotificationMode.GroupAndDirect);
        var publishedCallbacks = CallbackData(scenario.LastMessage.Markup);

        Assert.Contains("Landing Promise Smoke", scenario.LastMessage.Text);
        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(session.ScheduledAt.FormatMoscow(), scenario.LastMessage.Text);
        });

        var firstSessionId = scenario.Sessions[0].Id;
        var alice = scenario.Join(firstSessionId, 1001, "Alice", "alice");
        var bob = scenario.Join(firstSessionId, 1002, "Bob", "bob");
        var carol = scenario.Join(firstSessionId, 1003, "Carol", "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));
        Assert.Contains("2/2", scenario.LastMessage.Text);
        Assert.Contains("@alice", scenario.LastMessage.Text);
        Assert.Contains("@bob", scenario.LastMessage.Text);
        Assert.Contains("@carol", scenario.LastMessage.Text);

        scenario.Leave(firstSessionId, alice);

        Assert.False(scenario.HasParticipant(firstSessionId, alice));
        Assert.Equal(ParticipantRegistrationStatus.Active, scenario.RegistrationStatus(firstSessionId, carol));
        Assert.DoesNotContain("@alice", scenario.LastMessage.Text);
        Assert.Contains("@carol", scenario.LastMessage.Text);

        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 = HandleRescheduleTimeInputHandler.BuildVotingMessage(
            scenario.Title,
            scenario.Sessions[0].ScheduledAt,
            deadline,
            options,
            voteParticipants,
            []);
        var voteKeyboard = HandleRescheduleTimeInputHandler.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(RsvpStatus.Pending, scenario.RsvpStatus(firstSessionId, bob));
        Assert.Equal(RsvpStatus.Pending, scenario.RsvpStatus(firstSessionId, carol));
        Assert.Contains(options[1].ProposedAt.UtcDateTime.FormatMoscow(), scenario.LastMessage.Text);
        Assert.Collection(
            scenario.Messenger.DirectMessages.Select(message => message.TelegramId).Order(),
            telegramId => Assert.Equal(1002, telegramId),
            telegramId => Assert.Equal(1003, telegramId));
        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("Landing Promise Smoke - Dashboard Sync", scenario.LastMessage.Text);
        Assert.Contains("@bob", scenario.LastMessage.Text);
        Assert.Contains("@carol", scenario.LastMessage.Text);
    }

    private static string BuildRecurringSessionCommand() =>
        string.Join(
            '\n',
            "/newsession",
            "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435: Landing Promise Smoke",
            "\u0412\u0440\u0435\u043c\u044f: 15.05.2026 19:30",
            "\u0418\u0433\u0440: 3",
            "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b: 7",
            "\u041c\u0435\u0441\u0442: 2",
            "\u0421\u0441\u044b\u043b\u043a\u0430: https://example.test/table");

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

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

        private TelegramLandingSmokeScenario(
            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 FakeTelegramMessenger Messenger { get; } = new();
        public IReadOnlyList<SmokeSession> Sessions => sessions;
        public FakeTelegramMessage LastMessage => Messenger.LastMessage;

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

            scenario.RenderBatch();
            return scenario;
        }

        public Guid Join(Guid sessionId, long telegramId, string displayName, string? telegramUsername)
        {
            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,
                telegramId,
                displayName,
                telegramUsername,
                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,
                    value.TelegramUsername,
                    value.TelegramId))
                .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.TelegramId, 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,
                        participant.TelegramUsername,
                        participant.RegistrationStatus))
                    .ToList());
            var renderResult = TelegramSessionBatchRenderer.Render(view);

            if (Messenger.HasPublishedMessage)
            {
                Messenger.EditBatchMessage(renderResult.Text, renderResult.Markup);
            }
            else
            {
                Messenger.PublishBatchMessage(renderResult.Text, renderResult.Markup);
            }
        }
    }

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

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

        public void PublishBatchMessage(string text, InlineKeyboardMarkup markup) =>
            Sends.Add(new FakeTelegramMessage(BatchMessageId, text, markup));

        public void EditBatchMessage(string text, InlineKeyboardMarkup markup) =>
            Edits.Add(new FakeTelegramMessage(BatchMessageId, text, markup));

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

    private sealed record FakeTelegramMessage(
        int MessageId,
        string Text,
        InlineKeyboardMarkup Markup);

    private sealed record FakeDirectMessage(long TelegramId, 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,
        long TelegramId,
        string DisplayName,
        string? TelegramUsername,
        string RegistrationStatus,
        string RsvpStatus,
        int JoinOrder)
    {
        public string RegistrationStatus { get; set; } = RegistrationStatus;
        public string RsvpStatus { get; set; } = RsvpStatus;
    }
}
