using CashLineERP.Application.Finance;
using CashLineERP.Application.Integration;
using CashLineERP.Application.Notifications;
using CashLineERP.Application.Tax;
using CashLineERP.Contracts.Finance;
using CashLineERP.Contracts.Integration;
using CashLineERP.Contracts.Notifications;
using CashLineERP.Contracts.Tax;
using CashLineERP.Workers.Execution;
using CashLineERP.Workers.Processors;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;

namespace CashLineERP.Api.Tests;

public sealed class WorkersQaTests
{
    [Fact]
    public async Task WorkerExecutionCoordinator_runs_processors_in_order_and_records_failures()
    {
        var processors = new IWorkerBatchProcessor[]
        {
            new StubWorkerBatchProcessor("SECOND", 20, () => Task.FromResult(new WorkerBatchExecutionSummary("SECOND", "EXECUTED", 2, "Second ok"))),
            new StubWorkerBatchProcessor("FIRST", 10, () => Task.FromResult(new WorkerBatchExecutionSummary("FIRST", "EXECUTED", 1, "First ok"))),
            new StubWorkerBatchProcessor("BROKEN", 30, () => throw new InvalidOperationException("Broken processor"))
        };

        var coordinator = new WorkerExecutionCoordinator(
            processors,
            Options.Create(new WorkerExecutionOptions()),
            NullLogger<WorkerExecutionCoordinator>.Instance);

        var summaries = await coordinator.RunCycleAsync(new ServiceCollection().BuildServiceProvider(), CancellationToken.None);

        Assert.Collection(
            summaries,
            first =>
            {
                Assert.Equal("FIRST", first.ProcessorName);
                Assert.Equal("EXECUTED", first.StatusCode);
            },
            second =>
            {
                Assert.Equal("SECOND", second.ProcessorName);
                Assert.Equal("EXECUTED", second.StatusCode);
            },
            third =>
            {
                Assert.Equal("BROKEN", third.ProcessorName);
                Assert.Equal("FAILED", third.StatusCode);
                Assert.Contains("Broken processor", third.Details);
            });
    }

    [Fact]
    public async Task GatewayCallbackProcessor_only_processes_active_channels_with_inbound_endpoints()
    {
        var services = new ServiceCollection();
        services.AddSingleton<IPaymentChannelConfigurationService>(new FakePaymentChannelConfigurationService(
        [
            CreateChannel(10, usageCode: "CUSTOMER_REFUND", inboundEndpointId: 77, isActive: true),
            CreateChannel(11, usageCode: "CUSTOMER_REFUND", inboundEndpointId: null, isActive: true),
            CreateChannel(12, usageCode: "SUPPLIER_PAYOUT", inboundEndpointId: 88, isActive: true),
            CreateChannel(13, usageCode: "CUSTOMER_REFUND", inboundEndpointId: 99, isActive: false)
        ]));
        var gatewayCallbackService = new FakeGatewayCallbackService();
        services.AddSingleton<IGatewayCallbackService>(gatewayCallbackService);

        var processor = new GatewayCallbackProcessor();
        var summary = await processor.ExecuteAsync(
            services.BuildServiceProvider(),
            new WorkerExecutionOptions
            {
                CallbackUsageCode = "CUSTOMER_REFUND",
                MaxCallbackChannelsPerCycle = 5,
                GatewayCallbackBatchSize = 40,
                ProcessCodePrefix = "OPS"
            },
            CancellationToken.None);

        Assert.Equal("EXECUTED", summary.StatusCode);
        Assert.Equal(3, summary.ProcessedCount);
        Assert.Single(gatewayCallbackService.ProcessedChannelIds);
        Assert.Equal(10, gatewayCallbackService.ProcessedChannelIds[0]);
        Assert.Equal("OPS_CALLBACK", gatewayCallbackService.LastProcessorCode);
        Assert.Equal(40, gatewayCallbackService.LastBatchSize);
    }

    [Fact]
    public async Task PaymentExecutionDispatchProcessor_dispatches_only_matching_active_channels()
    {
        var services = new ServiceCollection();
        services.AddSingleton<IPaymentChannelConfigurationService>(new FakePaymentChannelConfigurationService(
        [
            CreateChannel(21, usageCode: "CUSTOMER_REFUND", outboundEndpointId: 500, isActive: true),
            CreateChannel(22, usageCode: "CUSTOMER_REFUND", outboundEndpointId: 0, isActive: true),
            CreateChannel(23, usageCode: "SUPPLIER_PAYOUT", outboundEndpointId: 600, isActive: true),
            CreateChannel(24, usageCode: "CUSTOMER_REFUND", outboundEndpointId: 700, isActive: false)
        ]));
        var paymentExecutionService = new FakePaymentGatewayExecutionService();
        services.AddSingleton<IPaymentGatewayExecutionService>(paymentExecutionService);

        var processor = new PaymentExecutionDispatchProcessor();
        var summary = await processor.ExecuteAsync(
            services.BuildServiceProvider(),
            new WorkerExecutionOptions
            {
                PaymentExecutionUsageCode = "CUSTOMER_REFUND",
                MaxPaymentChannelsPerCycle = 5,
                PaymentExecutionBatchSize = 30,
                ProcessCodePrefix = "OPS"
            },
            CancellationToken.None);

        Assert.Equal("EXECUTED", summary.StatusCode);
        Assert.Equal(4, summary.ProcessedCount);
        Assert.Single(paymentExecutionService.DispatchedChannelIds);
        Assert.Equal(21, paymentExecutionService.DispatchedChannelIds[0]);
        Assert.Equal("OPS_PAYMENT_EXEC", paymentExecutionService.LastProcessorCode);
        Assert.Equal(30, paymentExecutionService.LastBatchSize);
    }

    [Fact]
    public async Task EtaNotificationProcessor_processes_eta_notifications_and_inbox_processor_skips_eta_provider()
    {
        var services = new ServiceCollection();
        var inboxService = new FakeInboxMessageService();
        var etaCallbackService = new FakeEtaAuthorityCallbackService();
        services.AddSingleton<IInboxMessageService>(inboxService);
        services.AddSingleton<IEtaAuthorityCallbackService>(etaCallbackService);

        var etaProcessor = new EtaNotificationProcessor();
        var etaSummary = await etaProcessor.ExecuteAsync(
            services.BuildServiceProvider(),
            new WorkerExecutionOptions
            {
                EtaNotificationBatchSize = 12,
                ProcessCodePrefix = "OPS"
            },
            CancellationToken.None);

        Assert.Equal("EXECUTED", etaSummary.StatusCode);
        Assert.Equal(2, etaSummary.ProcessedCount);
        Assert.Equal("OPS_ETA_NOTIFY", etaCallbackService.LastProcessorCode);
        Assert.Equal(12, etaCallbackService.LastBatchSize);

        var inboxProcessor = new InboxProcessor();
        var inboxSummary = await inboxProcessor.ExecuteAsync(
            services.BuildServiceProvider(),
            new WorkerExecutionOptions
            {
                InboxBatchSize = 18,
                ProcessCodePrefix = "OPS"
            },
            CancellationToken.None);

        Assert.Equal("EXECUTED", inboxSummary.StatusCode);
        Assert.Equal("OPS_INBOX", inboxService.LastProcessorCode);
        Assert.Equal(18, inboxService.LastBatchSize);
        Assert.NotNull(inboxService.LastExcludedProviders);
        Assert.Contains("ETA", inboxService.LastExcludedProviders);
    }

    private static PaymentChannelConfigurationResponse CreateChannel(
        long id,
        string usageCode,
        long outboundEndpointId = 100,
        long? inboundEndpointId = null,
        bool isActive = true) =>
        new(
            id,
            $"CH-{id:000}",
            $"Channel {id}",
            1,
            "PAYMENT_GATEWAY",
            "API",
            usageCode,
            "EGP",
            outboundEndpointId,
            inboundEndpointId,
            false,
            false,
            false,
            null,
            null,
            2,
            isActive ? "ACTIVE" : "DRAFT",
            isActive,
            null,
            null,
            null,
            null,
            null,
            null,
            null);

    private sealed class StubWorkerBatchProcessor : IWorkerBatchProcessor
    {
        private readonly Func<Task<WorkerBatchExecutionSummary>> _execute;

        public StubWorkerBatchProcessor(string processorName, int order, Func<Task<WorkerBatchExecutionSummary>> execute)
        {
            ProcessorName = processorName;
            Order = order;
            _execute = execute;
        }

        public string ProcessorName { get; }
        public int Order { get; }

        public Task<WorkerBatchExecutionSummary> ExecuteAsync(IServiceProvider serviceProvider, WorkerExecutionOptions options, CancellationToken cancellationToken) =>
            _execute();
    }

    private sealed class FakePaymentChannelConfigurationService : IPaymentChannelConfigurationService
    {
        private readonly IReadOnlyCollection<PaymentChannelConfigurationResponse> _channels;

        public FakePaymentChannelConfigurationService(IReadOnlyCollection<PaymentChannelConfigurationResponse> channels)
        {
            _channels = channels;
        }

        public IReadOnlyCollection<PaymentChannelConfigurationResponse> GetPaymentChannels(bool activeOnly = false, string? integrationProviderCode = null, string? channelTypeCode = null, string? usageCode = null) =>
            _channels.Where(channel =>
                    (!activeOnly || channel.IsActive)
                    && (string.IsNullOrWhiteSpace(usageCode) || string.Equals(channel.UsageCode, usageCode, StringComparison.OrdinalIgnoreCase)))
                .ToArray();

        public PaymentChannelConfigurationResponse? GetPaymentChannel(long paymentChannelConfigurationId) =>
            _channels.SingleOrDefault(channel => channel.PaymentChannelConfigurationId == paymentChannelConfigurationId);

        public PaymentChannelConfigurationResponse CreatePaymentChannel(CreatePaymentChannelConfigurationRequest request) => throw new NotSupportedException();
        public PaymentChannelConfigurationResponse SubmitPaymentChannel(long paymentChannelConfigurationId, long actorUserId) => throw new NotSupportedException();
        public PaymentChannelConfigurationResponse ApprovePaymentChannel(long paymentChannelConfigurationId, long actorUserId) => throw new NotSupportedException();
        public PaymentChannelConfigurationResponse ActivatePaymentChannel(long paymentChannelConfigurationId, long actorUserId) => throw new NotSupportedException();
    }

    private sealed class FakeGatewayCallbackService : IGatewayCallbackService
    {
        public List<long> ProcessedChannelIds { get; } = [];
        public string? LastProcessorCode { get; private set; }
        public int LastBatchSize { get; private set; }

        public IReadOnlyCollection<GatewayCallbackReceiptResponse> GetCallbacks(long paymentChannelConfigurationId, string? statusCode = null) => [];
        public GatewayCallbackReceiptResponse? GetCallback(long paymentChannelConfigurationId, long inboxMessageId) => null;
        public GatewayCallbackReceiveResultResponse Receive(GatewayCallbackReceiveRequest request) => throw new NotSupportedException();
        public GatewayCallbackReceiptResponse ReplayCallback(long paymentChannelConfigurationId, long inboxMessageId, long actorUserId, ReplayDeadLetterQueueItemRequest request) => throw new NotSupportedException();

        public ProcessGatewayCallbackBatchResultResponse ProcessCallbacks(long paymentChannelConfigurationId, ProcessGatewayCallbackBatchRequest request)
        {
            ProcessedChannelIds.Add(paymentChannelConfigurationId);
            LastProcessorCode = request.ProcessorCode;
            LastBatchSize = request.BatchSize;
            return new ProcessGatewayCallbackBatchResultResponse(paymentChannelConfigurationId, request.ProcessorCode, request.BatchSize, 3, 0, 0, [], []);
        }
    }

    private sealed class FakePaymentGatewayExecutionService : IPaymentGatewayExecutionService
    {
        public List<long> DispatchedChannelIds { get; } = [];
        public string? LastProcessorCode { get; private set; }
        public int LastBatchSize { get; private set; }

        public IReadOnlyCollection<PaymentGatewayExecutionResponse> GetExecutions(long? paymentChannelConfigurationId = null, string? statusCode = null) => [];
        public PaymentGatewayExecutionResponse? GetExecution(long paymentGatewayExecutionId) => null;
        public IReadOnlyCollection<PaymentGatewayExecutionResponse> GetCustomerCollectionExecutions(long customerPaymentRequestId) => [];
        public PaymentGatewayExecutionResponse QueueCustomerCollection(long customerPaymentRequestId, long actorUserId) => throw new NotSupportedException();
        public IReadOnlyCollection<PaymentGatewayExecutionResponse> GetSupplierPayoutExecutions(long accountsPayablePaymentId) => [];
        public PaymentGatewayExecutionResponse QueueSupplierPayout(long accountsPayablePaymentId, string integrationProviderCode, long actorUserId) => throw new NotSupportedException();
        public IReadOnlyCollection<PaymentGatewayExecutionResponse> GetRefundExecutions(long customerRefundId) => [];
        public PaymentGatewayExecutionResponse QueueCustomerRefund(long customerRefundId, long actorUserId) => throw new NotSupportedException();
        public PaymentGatewayProviderContractResponse GetCustomerCollectionProviderContract(long customerPaymentRequestId) => throw new NotSupportedException();
        public PaymentGatewayProviderContractResponse GetSupplierPayoutProviderContract(long accountsPayablePaymentId, string? integrationProviderCode = null) => throw new NotSupportedException();
        public PaymentGatewayProviderContractResponse GetCustomerRefundProviderContract(long customerRefundId) => throw new NotSupportedException();
        public PaymentGatewayProviderContractResponse GetExecutionProviderStatusContract(long paymentGatewayExecutionId) => throw new NotSupportedException();
        public PaymentGatewayProviderContractResponse GetExecutionProviderCancelContract(long paymentGatewayExecutionId) => throw new NotSupportedException();

        public DispatchPaymentGatewayExecutionBatchResultResponse DispatchPendingExecutions(DispatchPaymentGatewayExecutionBatchRequest request)
        {
            if (request.PaymentChannelConfigurationId.HasValue)
            {
                DispatchedChannelIds.Add(request.PaymentChannelConfigurationId.Value);
            }

            LastProcessorCode = request.ProcessorCode;
            LastBatchSize = request.BatchSize;
            return new DispatchPaymentGatewayExecutionBatchResultResponse(request.ProcessorCode, request.BatchSize, 4, []);
        }
    }

    private sealed class FakeInboxMessageService : IInboxMessageService
    {
        public string? LastProcessorCode { get; private set; }
        public int LastBatchSize { get; private set; }
        public IReadOnlyCollection<string>? LastExcludedProviders { get; private set; }

        public IReadOnlyCollection<InboxMessageResponse> GetInboxMessages(string? statusCode = null, string? integrationProviderCode = null) => [];
        public InboxMessageResponse? GetInboxMessage(long inboxMessageId) => null;
        public InboxMessageResponse? FindExistingMessage(long integrationEndpointId, string? externalMessageId, string? correlationKey) => null;
        public InboxMessageResponse ReceiveInboxMessage(long actorUserId, ReceiveInboxMessageRequest request) => throw new NotSupportedException();
        public InboxMessageResponse MoveInboxMessageToDeadLetter(long inboxMessageId, long actorUserId, DeadLetterQueueItemRequest request) => throw new NotSupportedException();
        public InboxMessageResponse ReplayInboxMessage(long inboxMessageId, long actorUserId, ReplayDeadLetterQueueItemRequest request) => throw new NotSupportedException();

        public ProcessInboxBatchResultResponse ProcessPendingMessages(ProcessInboxBatchRequest request)
        {
            LastProcessorCode = request.ProcessorCode;
            LastBatchSize = request.BatchSize;
            LastExcludedProviders = request.ExcludedIntegrationProviderCodes;
            return new ProcessInboxBatchResultResponse(request.ProcessorCode, request.BatchSize, 5, []);
        }
    }

    private sealed class FakeEtaAuthorityCallbackService : IEtaAuthorityCallbackService
    {
        public string? LastProcessorCode { get; private set; }
        public int LastBatchSize { get; private set; }

        public EtaDocumentNotificationReceiptResponse ReceiveDocumentNotification(string? authorizationHeader, string payloadText) => throw new NotSupportedException();

        public ProcessEtaDocumentNotificationBatchResultResponse ProcessPendingNotifications(ProcessEtaDocumentNotificationBatchRequest request)
        {
            LastProcessorCode = request.ProcessorCode;
            LastBatchSize = request.BatchSize;
            return new ProcessEtaDocumentNotificationBatchResultResponse(
                request.ProcessorCode,
                request.BatchSize,
                2,
                1,
                1,
                0,
                [101L, 102L],
                [7001L]);
        }

        public EtaNotificationProcessingReceiptResponse ProcessNotification(long inboxMessageId, string processorCode, int retryDelaySeconds = 300) =>
            new(
                inboxMessageId,
                $"INBOX-{inboxMessageId:D6}",
                "PROCESSED",
                true,
                7001L,
                "ACCEPTED",
                null,
                null);
    }
}
