using Bit.Billing.Services;
using Bit.Billing.Services.Implementations;
using Bit.Billing.Test.Utilities;
using Bit.Core.Settings;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Stripe;
using Xunit;

namespace Bit.Billing.Test.Services;

public class StripeEventServiceTests
{
    private readonly IStripeFacade _stripeFacade;
    private readonly StripeEventService _stripeEventService;

    public StripeEventServiceTests()
    {
        var globalSettings = new GlobalSettings();
        var baseServiceUriSettings = new GlobalSettings.BaseServiceUriSettings(globalSettings) { CloudRegion = "US" };
        globalSettings.BaseServiceUri = baseServiceUriSettings;

        _stripeFacade = Substitute.For<IStripeFacade>();
        _stripeEventService = new StripeEventService(globalSettings, Substitute.For<ILogger<StripeEventService>>(), _stripeFacade);
    }

    #region GetCharge
    [Fact]
    public async Task GetCharge_EventNotChargeRelated_ThrowsException()
    {
        // Arrange
        var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated);

        // Act
        var function = async () => await _stripeEventService.GetCharge(stripeEvent);

        // Assert
        var exception = await Assert.ThrowsAsync<Exception>(function);
        Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Charge)}'", exception.Message);

        await _stripeFacade.DidNotReceiveWithAnyArgs().GetCharge(
            Arg.Any<string>(),
            Arg.Any<ChargeGetOptions>(),
            Arg.Any<RequestOptions>(),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task GetCharge_NotFresh_ReturnsEventCharge()
    {
        // Arrange
        var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.ChargeSucceeded);

        // Act
        var charge = await _stripeEventService.GetCharge(stripeEvent);

        // Assert
        Assert.Equivalent(stripeEvent.Data.Object as Charge, charge, true);

        await _stripeFacade.DidNotReceiveWithAnyArgs().GetCharge(
            Arg.Any<string>(),
            Arg.Any<ChargeGetOptions>(),
            Arg.Any<RequestOptions>(),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task GetCharge_Fresh_Expand_ReturnsAPICharge()
    {
        // Arrange
        var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.ChargeSucceeded);

        var eventCharge = stripeEvent.Data.Object as Charge;

        var apiCharge = Copy(eventCharge);

        var expand = new List<string> { "customer" };

        _stripeFacade.GetCharge(
                apiCharge.Id,
                Arg.Is<ChargeGetOptions>(options => options.Expand == expand))
            .Returns(apiCharge);

        // Act
        var charge = await _stripeEventService.GetCharge(stripeEvent, true, expand);

        // Assert
        Assert.Equal(apiCharge, charge);
        Assert.NotSame(eventCharge, charge);

        await _stripeFacade.Received().GetCharge(
            apiCharge.Id,
            Arg.Is<ChargeGetOptions>(options => options.Expand == expand),
            Arg.Any<RequestOptions>(),
            Arg.Any<CancellationToken>());
    }
    #endregion

    #region GetCustomer
    [Fact]
    public async Task GetCustomer_EventNotCustomerRelated_ThrowsException()
    {
        // Arrange
        var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated);

        // Act
        var function = async () => await _stripeEventService.GetCustomer(stripeEvent);

        // Assert
        var exception = await Assert.ThrowsAsync<Exception>(function);
        Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Customer)}'", exception.Message);

        await _stripeFacade.DidNotReceiveWithAnyArgs().GetCustomer(
            Arg.Any<string>(),
            Arg.Any<CustomerGetOptions>(),
            Arg.Any<RequestOptions>(),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task GetCustomer_NotFresh_ReturnsEventCustomer()
    {
        // Arrange
        var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated);

        // Act
        var customer = await _stripeEventService.GetCustomer(stripeEvent);

        // Assert
        Assert.Equivalent(stripeEvent.Data.Object as Customer, customer, true);

        await _stripeFacade.DidNotReceiveWithAnyArgs().GetCustomer(
            Arg.Any<string>(),
            Arg.Any<CustomerGetOptions>(),
            Arg.Any<RequestOptions>(),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task GetCustomer_Fresh_Expand_ReturnsAPICustomer()
    {
        // Arrange
        var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated);

        var eventCustomer = stripeEvent.Data.Object as Customer;

        var apiCustomer = Copy(eventCustomer);

        var expand = new List<string> { "subscriptions" };

        _stripeFacade.GetCustomer(
                apiCustomer.Id,
                Arg.Is<CustomerGetOptions>(options => options.Expand == expand))
            .Returns(apiCustomer);

        // Act
        var customer = await _stripeEventService.GetCustomer(stripeEvent, true, expand);

        // Assert
        Assert.Equal(apiCustomer, customer);
        Assert.NotSame(eventCustomer, customer);

        await _stripeFacade.Received().GetCustomer(
            apiCustomer.Id,
            Arg.Is<CustomerGetOptions>(options => options.Expand == expand),
            Arg.Any<RequestOptions>(),
            Arg.Any<CancellationToken>());
    }
    #endregion

    #region GetInvoice
    [Fact]
    public async Task GetInvoice_EventNotInvoiceRelated_ThrowsException()
    {
        // Arrange
        var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated);

        // Act
        var function = async () => await _stripeEventService.GetInvoice(stripeEvent);

        // Assert
        var exception = await Assert.ThrowsAsync<Exception>(function);
        Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Invoice)}'", exception.Message);

        await _stripeFacade.DidNotReceiveWithAnyArgs().GetInvoice(
            Arg.Any<string>(),
            Arg.Any<InvoiceGetOptions>(),
            Arg.Any<RequestOptions>(),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task GetInvoice_NotFresh_ReturnsEventInvoice()
    {
        // Arrange
        var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated);

        // Act
        var invoice = await _stripeEventService.GetInvoice(stripeEvent);

        // Assert
        Assert.Equivalent(stripeEvent.Data.Object as Invoice, invoice, true);

        await _stripeFacade.DidNotReceiveWithAnyArgs().GetInvoice(
            Arg.Any<string>(),
            Arg.Any<InvoiceGetOptions>(),
            Arg.Any<RequestOptions>(),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task GetInvoice_Fresh_Expand_ReturnsAPIInvoice()
    {
        // Arrange
        var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated);

        var eventInvoice = stripeEvent.Data.Object as Invoice;

        var apiInvoice = Copy(eventInvoice);

        var expand = new List<string> { "customer" };

        _stripeFacade.GetInvoice(
                apiInvoice.Id,
                Arg.Is<InvoiceGetOptions>(options => options.Expand == expand))
            .Returns(apiInvoice);

        // Act
        var invoice = await _stripeEventService.GetInvoice(stripeEvent, true, expand);

        // Assert
        Assert.Equal(apiInvoice, invoice);
        Assert.NotSame(eventInvoice, invoice);

        await _stripeFacade.Received().GetInvoice(
            apiInvoice.Id,
            Arg.Is<InvoiceGetOptions>(options => options.Expand == expand),
            Arg.Any<RequestOptions>(),
            Arg.Any<CancellationToken>());
    }
    #endregion

    #region GetPaymentMethod
    [Fact]
    public async Task GetPaymentMethod_EventNotPaymentMethodRelated_ThrowsException()
    {
        // Arrange
        var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated);

        // Act
        var function = async () => await _stripeEventService.GetPaymentMethod(stripeEvent);

        // Assert
        var exception = await Assert.ThrowsAsync<Exception>(function);
        Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(PaymentMethod)}'", exception.Message);

        await _stripeFacade.DidNotReceiveWithAnyArgs().GetPaymentMethod(
            Arg.Any<string>(),
            Arg.Any<PaymentMethodGetOptions>(),
            Arg.Any<RequestOptions>(),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task GetPaymentMethod_NotFresh_ReturnsEventPaymentMethod()
    {
        // Arrange
        var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.PaymentMethodAttached);

        // Act
        var paymentMethod = await _stripeEventService.GetPaymentMethod(stripeEvent);

        // Assert
        Assert.Equivalent(stripeEvent.Data.Object as PaymentMethod, paymentMethod, true);

        await _stripeFacade.DidNotReceiveWithAnyArgs().GetPaymentMethod(
            Arg.Any<string>(),
            Arg.Any<PaymentMethodGetOptions>(),
            Arg.Any<RequestOptions>(),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task GetPaymentMethod_Fresh_Expand_ReturnsAPIPaymentMethod()
    {
        // Arrange
        var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.PaymentMethodAttached);

        var eventPaymentMethod = stripeEvent.Data.Object as PaymentMethod;

        var apiPaymentMethod = Copy(eventPaymentMethod);

        var expand = new List<string> { "customer" };

        _stripeFacade.GetPaymentMethod(
                apiPaymentMethod.Id,
                Arg.Is<PaymentMethodGetOptions>(options => options.Expand == expand))
            .Returns(apiPaymentMethod);

        // Act
        var paymentMethod = await _stripeEventService.GetPaymentMethod(stripeEvent, true, expand);

        // Assert
        Assert.Equal(apiPaymentMethod, paymentMethod);
        Assert.NotSame(eventPaymentMethod, paymentMethod);

        await _stripeFacade.Received().GetPaymentMethod(
            apiPaymentMethod.Id,
            Arg.Is<PaymentMethodGetOptions>(options => options.Expand == expand),
            Arg.Any<RequestOptions>(),
            Arg.Any<CancellationToken>());
    }
    #endregion

    #region GetSubscription
    [Fact]
    public async Task GetSubscription_EventNotSubscriptionRelated_ThrowsException()
    {
        // Arrange
        var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated);

        // Act
        var function = async () => await _stripeEventService.GetSubscription(stripeEvent);

        // Assert
        var exception = await Assert.ThrowsAsync<Exception>(function);
        Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Subscription)}'", exception.Message);

        await _stripeFacade.DidNotReceiveWithAnyArgs().GetSubscription(
            Arg.Any<string>(),
            Arg.Any<SubscriptionGetOptions>(),
            Arg.Any<RequestOptions>(),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task GetSubscription_NotFresh_ReturnsEventSubscription()
    {
        // Arrange
        var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerSubscriptionUpdated);

        // Act
        var subscription = await _stripeEventService.GetSubscription(stripeEvent);

        // Assert
        Assert.Equivalent(stripeEvent.Data.Object as Subscription, subscription, true);

        await _stripeFacade.DidNotReceiveWithAnyArgs().GetSubscription(
            Arg.Any<string>(),
            Arg.Any<SubscriptionGetOptions>(),
            Arg.Any<RequestOptions>(),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task GetSubscription_Fresh_Expand_ReturnsAPISubscription()
    {
        // Arrange
        var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerSubscriptionUpdated);

        var eventSubscription = stripeEvent.Data.Object as Subscription;

        var apiSubscription = Copy(eventSubscription);

        var expand = new List<string> { "customer" };

        _stripeFacade.GetSubscription(
                apiSubscription.Id,
                Arg.Is<SubscriptionGetOptions>(options => options.Expand == expand))
            .Returns(apiSubscription);

        // Act
        var subscription = await _stripeEventService.GetSubscription(stripeEvent, true, expand);

        // Assert
        Assert.Equal(apiSubscription, subscription);
        Assert.NotSame(eventSubscription, subscription);

        await _stripeFacade.Received().GetSubscription(
            apiSubscription.Id,
            Arg.Is<SubscriptionGetOptions>(options => options.Expand == expand),
            Arg.Any<RequestOptions>(),
            Arg.Any<CancellationToken>());
    }
    #endregion

    #region ValidateCloudRegion
    [Fact]
    public async Task ValidateCloudRegion_SubscriptionUpdated_Success()
    {
        // Arrange
        var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerSubscriptionUpdated);

        var subscription = Copy(stripeEvent.Data.Object as Subscription);

        var customer = await GetCustomerAsync();

        subscription.Customer = customer;

        _stripeFacade.GetSubscription(
                subscription.Id,
                Arg.Any<SubscriptionGetOptions>())
            .Returns(subscription);

        // Act
        var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);

        // Assert
        Assert.True(cloudRegionValid);

        await _stripeFacade.Received(1).GetSubscription(
            subscription.Id,
            Arg.Any<SubscriptionGetOptions>(),
            Arg.Any<RequestOptions>(),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task ValidateCloudRegion_ChargeSucceeded_Success()
    {
        // Arrange
        var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.ChargeSucceeded);

        var charge = Copy(stripeEvent.Data.Object as Charge);

        var customer = await GetCustomerAsync();

        charge.Customer = customer;

        _stripeFacade.GetCharge(
                charge.Id,
                Arg.Any<ChargeGetOptions>())
            .Returns(charge);

        // Act
        var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);

        // Assert
        Assert.True(cloudRegionValid);

        await _stripeFacade.Received(1).GetCharge(
            charge.Id,
            Arg.Any<ChargeGetOptions>(),
            Arg.Any<RequestOptions>(),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task ValidateCloudRegion_UpcomingInvoice_Success()
    {
        // Arrange
        var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceUpcoming);

        var invoice = Copy(stripeEvent.Data.Object as Invoice);

        var customer = await GetCustomerAsync();

        _stripeFacade.GetCustomer(
                invoice.CustomerId,
                Arg.Any<CustomerGetOptions>())
            .Returns(customer);

        // Act
        var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);

        // Assert
        Assert.True(cloudRegionValid);

        await _stripeFacade.Received(1).GetCustomer(
            invoice.CustomerId,
            Arg.Any<CustomerGetOptions>(),
            Arg.Any<RequestOptions>(),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task ValidateCloudRegion_InvoiceCreated_Success()
    {
        // Arrange
        var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated);

        var invoice = Copy(stripeEvent.Data.Object as Invoice);

        var customer = await GetCustomerAsync();

        invoice.Customer = customer;

        _stripeFacade.GetInvoice(
                invoice.Id,
                Arg.Any<InvoiceGetOptions>())
            .Returns(invoice);

        // Act
        var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);

        // Assert
        Assert.True(cloudRegionValid);

        await _stripeFacade.Received(1).GetInvoice(
            invoice.Id,
            Arg.Any<InvoiceGetOptions>(),
            Arg.Any<RequestOptions>(),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task ValidateCloudRegion_PaymentMethodAttached_Success()
    {
        // Arrange
        var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.PaymentMethodAttached);

        var paymentMethod = Copy(stripeEvent.Data.Object as PaymentMethod);

        var customer = await GetCustomerAsync();

        paymentMethod.Customer = customer;

        _stripeFacade.GetPaymentMethod(
                paymentMethod.Id,
                Arg.Any<PaymentMethodGetOptions>())
            .Returns(paymentMethod);

        // Act
        var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);

        // Assert
        Assert.True(cloudRegionValid);

        await _stripeFacade.Received(1).GetPaymentMethod(
            paymentMethod.Id,
            Arg.Any<PaymentMethodGetOptions>(),
            Arg.Any<RequestOptions>(),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task ValidateCloudRegion_CustomerUpdated_Success()
    {
        // Arrange
        var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated);

        var customer = Copy(stripeEvent.Data.Object as Customer);

        _stripeFacade.GetCustomer(
                customer.Id,
                Arg.Any<CustomerGetOptions>())
            .Returns(customer);

        // Act
        var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);

        // Assert
        Assert.True(cloudRegionValid);

        await _stripeFacade.Received(1).GetCustomer(
            customer.Id,
            Arg.Any<CustomerGetOptions>(),
            Arg.Any<RequestOptions>(),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task ValidateCloudRegion_MetadataNull_ReturnsFalse()
    {
        // Arrange
        var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerSubscriptionUpdated);

        var subscription = Copy(stripeEvent.Data.Object as Subscription);

        var customer = await GetCustomerAsync();
        customer.Metadata = null;

        subscription.Customer = customer;

        _stripeFacade.GetSubscription(
                subscription.Id,
                Arg.Any<SubscriptionGetOptions>())
            .Returns(subscription);

        // Act
        var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);

        // Assert
        Assert.False(cloudRegionValid);

        await _stripeFacade.Received(1).GetSubscription(
            subscription.Id,
            Arg.Any<SubscriptionGetOptions>(),
            Arg.Any<RequestOptions>(),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task ValidateCloudRegion_MetadataNoRegion_DefaultUS_ReturnsTrue()
    {
        // Arrange
        var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerSubscriptionUpdated);

        var subscription = Copy(stripeEvent.Data.Object as Subscription);

        var customer = await GetCustomerAsync();
        customer.Metadata = new Dictionary<string, string>();

        subscription.Customer = customer;

        _stripeFacade.GetSubscription(
                subscription.Id,
                Arg.Any<SubscriptionGetOptions>())
            .Returns(subscription);

        // Act
        var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);

        // Assert
        Assert.True(cloudRegionValid);

        await _stripeFacade.Received(1).GetSubscription(
            subscription.Id,
            Arg.Any<SubscriptionGetOptions>(),
            Arg.Any<RequestOptions>(),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task ValidateCloudRegion_MetadataMiscasedRegion_ReturnsTrue()
    {
        // Arrange
        var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerSubscriptionUpdated);

        var subscription = Copy(stripeEvent.Data.Object as Subscription);

        var customer = await GetCustomerAsync();
        customer.Metadata = new Dictionary<string, string>
        {
            { "Region", "US" }
        };

        subscription.Customer = customer;

        _stripeFacade.GetSubscription(
                subscription.Id,
                Arg.Any<SubscriptionGetOptions>())
            .Returns(subscription);

        // Act
        var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);

        // Assert
        Assert.True(cloudRegionValid);

        await _stripeFacade.Received(1).GetSubscription(
            subscription.Id,
            Arg.Any<SubscriptionGetOptions>(),
            Arg.Any<RequestOptions>(),
            Arg.Any<CancellationToken>());
    }
    #endregion

    private static T Copy<T>(T input)
    {
        var copy = (T)Activator.CreateInstance(typeof(T));

        var properties = input.GetType().GetProperties();

        foreach (var property in properties)
        {
            var value = property.GetValue(input);
            copy!
                .GetType()
                .GetProperty(property.Name)!
                .SetValue(copy, value);
        }

        return copy;
    }

    private static async Task<Customer> GetCustomerAsync()
        => (await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated)).Data.Object as Customer;
}
