using AspNetCoreRateLimit;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Infrastructure.EntityFramework.Repositories;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

namespace Bit.IntegrationTestCommon.Factories;

public static class FactoryConstants
{
    public const string DefaultDatabaseName = "test_database";
    public const string WhitelistedIp = "1.1.1.1";
}

public abstract class WebApplicationFactoryBase<T> : WebApplicationFactory<T>
    where T : class
{
    /// <summary>
    /// The database name to use for this instance of the factory. By default it will use a shared database name so all instances will connect to the same database during it's lifetime.
    /// </summary>
    /// <remarks>
    /// This will need to be set BEFORE using the <c>Server</c> property
    /// </remarks>
    public string DatabaseName { get; set; } = Guid.NewGuid().ToString();

    /// <summary>
    /// Configure the web host to use an EF in memory database
    /// </summary>
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureAppConfiguration(c =>
        {
            c.SetBasePath(AppContext.BaseDirectory)
                .AddJsonFile("appsettings.json")
                .AddJsonFile("appsettings.Development.json");

            c.AddUserSecrets(typeof(Identity.Startup).Assembly, optional: true);
            c.AddInMemoryCollection(new Dictionary<string, string>
            {
                // Manually insert a EF provider so that ConfigureServices will add EF repositories but we will override
                // DbContextOptions to use an in memory database
                { "globalSettings:databaseProvider", "postgres" },
                { "globalSettings:postgreSql:connectionString", "Host=localhost;Username=test;Password=test;Database=test" },

                // Clear the redis connection string for distributed caching, forcing an in-memory implementation
                { "globalSettings:redis:connectionString", ""}
            });
        });

        builder.ConfigureTestServices(services =>
        {
            var dbContextOptions = services.First(sd => sd.ServiceType == typeof(DbContextOptions<DatabaseContext>));
            services.Remove(dbContextOptions);
            services.AddScoped(services =>
            {
                return new DbContextOptionsBuilder<DatabaseContext>()
                    .UseInMemoryDatabase(DatabaseName)
                    .UseApplicationServiceProvider(services)
                    .Options;
            });

            // QUESTION: The normal licensing service should run fine on developer machines but not in CI
            // should we have a fork here to leave the normal service for developers?
            // TODO: Eventually add the license file to CI
            var licensingService = services.First(sd => sd.ServiceType == typeof(ILicensingService));
            services.Remove(licensingService);
            services.AddSingleton<ILicensingService, NoopLicensingService>();

            // FUTURE CONSIDERATION: Add way to run this self hosted/cloud, for now it is cloud only
            var pushRegistrationService = services.First(sd => sd.ServiceType == typeof(IPushRegistrationService));
            services.Remove(pushRegistrationService);
            services.AddSingleton<IPushRegistrationService, NoopPushRegistrationService>();

            // Even though we are cloud we currently set this up as cloud, we can use the EF/selfhosted service
            // instead of using Noop for this service
            // TODO: Install and use azurite in CI pipeline
            var eventWriteService = services.First(sd => sd.ServiceType == typeof(IEventWriteService));
            services.Remove(eventWriteService);
            services.AddSingleton<IEventWriteService, RepositoryEventWriteService>();

            var eventRepositoryService = services.First(sd => sd.ServiceType == typeof(IEventRepository));
            services.Remove(eventRepositoryService);
            services.AddSingleton<IEventRepository, EventRepository>();

            var mailDeliveryService = services.First(sd => sd.ServiceType == typeof(IMailDeliveryService));
            services.Remove(mailDeliveryService);
            services.AddSingleton<IMailDeliveryService, NoopMailDeliveryService>();

            var captchaValidationService = services.First(sd => sd.ServiceType == typeof(ICaptchaValidationService));
            services.Remove(captchaValidationService);
            services.AddSingleton<ICaptchaValidationService, NoopCaptchaValidationService>();

            // Our Rate limiter works so well that it begins to fail tests unless we carve out
            // one whitelisted ip. We should still test the rate limiter though and they should change the Ip
            // to something that is NOT whitelisted
            services.Configure<IpRateLimitOptions>(options =>
            {
                options.IpWhitelist = new List<string>
                {
                    FactoryConstants.WhitelistedIp,
                };
            });

            // Fix IP Rate Limiting
            services.AddSingleton<IStartupFilter, CustomStartupFilter>();

            // Disable logs
            services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
        });
    }

    public DatabaseContext GetDatabaseContext()
    {
        var scope = Services.CreateScope();
        return scope.ServiceProvider.GetRequiredService<DatabaseContext>();
    }

    public T GetService<T>()
    {
        var scope = Services.CreateScope();
        return scope.ServiceProvider.GetRequiredService<T>();
    }
}