using System.Text.Json;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Npgsql;
using Tiku.Api;
using Tiku.Application.Assets;
using Tiku.Application.Auth;
using Tiku.Application.Commerce;
using Tiku.Application.Growth;
using Tiku.Application.PlatformBilling;
using Tiku.Application.Security;
using Tiku.Application.Storage;
using Tiku.Application.Tenancy;
using Tiku.Domain.Content;
using Tiku.Domain.Identity;
using Tiku.Domain.Operations;
using Tiku.Domain.Platform;
using Tiku.Domain.QuestionBanks;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Bootstrap;
using Tiku.Infrastructure.Persistence;
using Tiku.IntegrationTests.Infrastructure;

namespace Tiku.IntegrationTests.Api;

public sealed class ApiTestFactory(
    IWechatOAuthClient? wechatOAuthClient = null,
    IObjectStorageService? objectStorageService = null,
    IReferralQrcodeGenerator? referralQrcodeGenerator = null,
    IPaymentProviderGateway? paymentProviderGateway = null,
    IPlatformBillingPaymentGateway? platformBillingPaymentGateway = null,
    IDomainOwnershipVerifier? domainOwnershipVerifier = null,
    IDomainGatewayProvisioner? domainGatewayProvisioner = null,
    ISmsProvider? smsProvider = null,
    IAssetSecurityScanner? assetSecurityScanner = null,
    IReadOnlyDictionary<string, string?>? configurationOverrides = null,
    DbCommandInterceptor? dbCommandInterceptor = null) : WebApplicationFactory<ApiProgramMarker>
{
    private readonly PostgresTestDatabase database = PostgresTestDatabase.Create();

    public string DatabaseConnectionString => database.ConnectionString;

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        if (configurationOverrides is not null)
            foreach (var pair in configurationOverrides.Where(pair => pair.Value is not null))
                builder.UseSetting(pair.Key, pair.Value);

        builder.ConfigureAppConfiguration((_, configuration) =>
        {
            var values = new Dictionary<string, string?>
            {
                ["Security:Jwt:KeyId"] = TestJwtKeys.KeyId,
                ["Security:Jwt:PrivateKeyPem"] = TestJwtKeys.PrivateKeyPem,
                ["Tenancy:Resolution:TenantCodePathPrefixes:0"] = "/api"
            };
            if (configurationOverrides is not null)
                foreach (var pair in configurationOverrides)
                    values[pair.Key] = pair.Value;

            configuration.AddInMemoryCollection(values);
        });

        builder.ConfigureServices(services =>
        {
            foreach (var descriptor in services
                         .Where(descriptor =>
                             descriptor.ServiceType == typeof(NpgsqlDataSource) ||
                             descriptor.ServiceType == typeof(TenantIsolationSaveChangesInterceptor) ||
                             descriptor.ServiceType == typeof(DbContextOptions<TikuDbContext>) ||
                             descriptor.ServiceType == typeof(TikuDbContext))
                         .ToArray())
                services.Remove(descriptor);

            services.AddSingleton(_ => NpgsqlDataSource.Create(database.ConnectionString));
            services.AddScoped<TenantIsolationSaveChangesInterceptor>();
            services.AddDbContext<TikuDbContext>((serviceProvider, options) =>
            {
                var dataSource = serviceProvider.GetRequiredService<NpgsqlDataSource>();
                options.UseNpgsql(dataSource, npgsql =>
                    npgsql.MigrationsAssembly(typeof(TikuDbContext).Assembly.FullName));
                options.AddInterceptors(serviceProvider.GetRequiredService<TenantIsolationSaveChangesInterceptor>());
                if (dbCommandInterceptor is not null) options.AddInterceptors(dbCommandInterceptor);
            });
            services.RemoveAll<IJwtKeyRing>();
            services.AddSingleton<IJwtKeyRing, TestJwtKeyRing>();

            if (wechatOAuthClient is not null) services.AddSingleton(wechatOAuthClient);

            if (objectStorageService is not null) services.AddSingleton(objectStorageService);

            if (referralQrcodeGenerator is not null) services.AddSingleton(referralQrcodeGenerator);

            if (paymentProviderGateway is not null) services.AddSingleton(paymentProviderGateway);

            if (platformBillingPaymentGateway is not null)
            {
                services.RemoveAll<IPlatformBillingPaymentGateway>();
                services.AddSingleton(platformBillingPaymentGateway);
            }

            if (domainOwnershipVerifier is not null)
            {
                services.RemoveAll<IDomainOwnershipVerifier>();
                services.AddSingleton(domainOwnershipVerifier);
            }

            if (domainGatewayProvisioner is not null)
            {
                services.RemoveAll<IDomainGatewayProvisioner>();
                services.AddSingleton(domainGatewayProvisioner);
            }

            if (smsProvider is not null)
            {
                services.RemoveAll<ISmsProvider>();
                services.AddSingleton(smsProvider);
            }

            if (assetSecurityScanner is not null)
            {
                services.RemoveAll<IAssetSecurityScanner>();
                services.AddSingleton(assetSecurityScanner);
            }
        });
    }

    public async Task SeedAsync(params object[] entities)
    {
        using var scope = Services.CreateScope();
        scope.ServiceProvider.GetRequiredService<ITenantContextInitializer>()
            .InitializeSystem(null, "Integration test fixture seeding");
        var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
        var tenants = entities.OfType<Tenant>().Where(tenant => tenant.Mode == TenantMode.Saas).ToArray();
        var hasExplicitCapabilitySetup = entities.Any(entity =>
            entity is SaasOffering or SaasOfferingVersion or TenantSaasSubscription);
        if (tenants.Length > 0 && !hasExplicitCapabilitySetup)
        {
            const string integrationOfferingCode = "integration-full-access";
            var existingFeatures = await dbContext.SaasFeatures.Select(feature => feature.Code).ToArrayAsync();
            dbContext.SaasFeatures.AddRange(SaasFeatureCatalog.All
                .Except(existingFeatures, StringComparer.Ordinal)
                .Select((code, index) => new SaasFeature
                {
                    Code = code,
                    Name = code,
                    Category = code.Split('.')[0],
                    IsCore = code == SaasFeatureCatalog.CoreBackoffice,
                    Status = SaasFeatureStatus.Active,
                    SortOrder = index * 10
                }));
            var offering =
                await dbContext.SaasOfferings.SingleOrDefaultAsync(value => value.Code == integrationOfferingCode);
            if (offering is null)
            {
                offering = new SaasOffering
                {
                    Code = integrationOfferingCode,
                    Name = "Integration Full Access",
                    Type = SaasOfferingType.BasePlan,
                    Status = SaasOfferingStatus.Active
                };
                dbContext.SaasOfferings.Add(offering);
            }

            var version = await dbContext.SaasOfferingVersions.SingleOrDefaultAsync(value =>
                value.OfferingId == offering.Id && value.Version == 1);
            if (version is null)
            {
                version = new SaasOfferingVersion
                {
                    OfferingId = offering.Id,
                    Version = 1,
                    Status = SaasOfferingVersionStatus.Draft,
                    OriginalAmountCents = 100,
                    AmountCents = 100,
                    EffectiveAt = DateTimeOffset.UtcNow.AddDays(-1)
                };
                dbContext.SaasOfferingVersions.Add(version);
            }

            await dbContext.SaveChangesAsync();
            var entitledFeatures = await dbContext.SaasOfferingVersionFeatures
                .Where(entitlement => entitlement.OfferingVersionId == version.Id)
                .Select(entitlement => entitlement.FeatureCode)
                .ToArrayAsync();
            dbContext.SaasOfferingVersionFeatures.AddRange(SaasFeatureCatalog.All
                .Where(code => code != SaasFeatureCatalog.CoreBackoffice)
                .Except(entitledFeatures, StringComparer.Ordinal)
                .Select(code => new SaasOfferingVersionFeature
                {
                    OfferingVersionId = version.Id,
                    FeatureCode = code
                }));
            await dbContext.SaveChangesAsync();
            if (version.Status == SaasOfferingVersionStatus.Draft)
            {
                version.Status = SaasOfferingVersionStatus.Published;
                version.PublishedAt = DateTimeOffset.UtcNow.AddDays(-1);
                await dbContext.SaveChangesAsync();
            }

            var now = DateTimeOffset.UtcNow;
            var subscriptionGraphs = tenants.SelectMany(tenant =>
            {
                var subscription = new TenantSaasSubscription
                {
                    TenantId = tenant.Id,
                    BaseOfferingVersionId = version.Id,
                    Status = TenantSaasSubscriptionStatus.Active,
                    StartsAt = now.AddDays(-1),
                    CurrentPeriodStart = now.AddDays(-1),
                    CurrentPeriodEnd = now.AddYears(1)
                };
                return new object[]
                {
                    subscription,
                    new TenantSaasSubscriptionItem
                    {
                        TenantId = tenant.Id,
                        SubscriptionId = subscription.Id,
                        OfferingVersionId = version.Id,
                        ItemType = TenantSaasSubscriptionItemType.BasePlan,
                        Status = TenantSaasSubscriptionItemStatus.Active,
                        StartsAt = subscription.CurrentPeriodStart,
                        EndsAt = subscription.CurrentPeriodEnd
                    }
                };
            });
            entities = entities.Concat(subscriptionGraphs).ToArray();
        }

        var explicitPermissions = entities.OfType<BackendPermission>().ToArray();
        if (explicitPermissions.Length > 0)
        {
            var moduleCodes = explicitPermissions.Select(value => value.PermissionModuleCode)
                .Distinct(StringComparer.Ordinal)
                .ToArray();
            var existingModuleCodes = await dbContext.PermissionModules
                .Where(value => moduleCodes.Contains(value.Code))
                .Select(value => value.Code)
                .ToArrayAsync();
            var suppliedModuleCodes = entities.OfType<PermissionModule>().Select(value => value.Code).ToArray();
            var modules = moduleCodes
                .Except(existingModuleCodes, StringComparer.Ordinal)
                .Except(suppliedModuleCodes, StringComparer.Ordinal)
                .Select(code => new PermissionModule
                {
                    Code = code,
                    Name = code,
                    Area = explicitPermissions.First(value => value.PermissionModuleCode == code).Area,
                    RequiredFeatureCode = PermissionModuleCatalog.RequiredFeatures.GetValueOrDefault(code)
                })
                .ToArray();
            var requiredFeatureCodes = modules.Select(value => value.RequiredFeatureCode)
                .Where(value => value is not null)
                .Cast<string>()
                .Distinct(StringComparer.Ordinal)
                .ToArray();
            var existingRequiredFeatures = await dbContext.SaasFeatures
                .Where(value => requiredFeatureCodes.Contains(value.Code))
                .Select(value => value.Code)
                .ToArrayAsync();
            var suppliedFeatureCodes = entities.OfType<SaasFeature>().Select(value => value.Code).ToArray();
            dbContext.SaasFeatures.AddRange(requiredFeatureCodes
                .Except(existingRequiredFeatures, StringComparer.Ordinal)
                .Except(suppliedFeatureCodes, StringComparer.Ordinal)
                .Select(code => new SaasFeature
                {
                    Code = code,
                    Name = code,
                    Category = code.Split('.')[0],
                    Status = SaasFeatureStatus.Active
                }));
            dbContext.PermissionModules.AddRange(modules);
            await dbContext.SaveChangesAsync();
        }

        var offeringVersionsToFinalize = entities.OfType<SaasOfferingVersion>()
            .Where(value => value.Status != SaasOfferingVersionStatus.Draft)
            .Select(value => new { Version = value, TargetStatus = value.Status })
            .ToArray();
        foreach (var item in offeringVersionsToFinalize) item.Version.Status = SaasOfferingVersionStatus.Draft;

        dbContext.AddRange(entities);
        await dbContext.SaveChangesAsync();
        foreach (var item in offeringVersionsToFinalize) item.Version.Status = item.TargetStatus;
        await dbContext.SaveChangesAsync();

        var backendMembers = entities
            .OfType<TenantMembership>()
            .Where(membership =>
                membership.Status == MembershipStatus.Active &&
                membership.Role is TenantRole.TenantOwner or TenantRole.TenantAdmin)
            .Select(membership => (membership.TenantId, membership.UserId))
            .Distinct()
            .ToArray();
        if (backendMembers.Length > 0) await EnsureTenantBackendAccessAsync(dbContext, backendMembers);
    }

    public async Task SeedBuiltinBackofficeCatalogAsync()
    {
        using var scope = Services.CreateScope();
        scope.ServiceProvider.GetRequiredService<ITenantContextInitializer>()
            .InitializeSystem(null, "Integration test built-in backoffice catalog seeding");
        var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
        await new BuiltinBackofficeCatalogSeeder(dbContext).SeedAsync();
    }

    private static async Task EnsureTenantBackendAccessAsync(
        TikuDbContext dbContext,
        IEnumerable<(Guid TenantId, Guid UserId)> members)
    {
        var permissionCodes = BackendPermissions.Tenant.Order(StringComparer.Ordinal).ToArray();
        var featureCodes = PermissionModuleCatalog.RequiredFeatures.Values
            .Where(value => value is not null)
            .Cast<string>()
            .Append(SaasFeatureCatalog.CoreBackoffice)
            .Distinct(StringComparer.Ordinal)
            .ToArray();
        var existingFeatureCodes = await dbContext.SaasFeatures
            .Where(value => featureCodes.Contains(value.Code))
            .Select(value => value.Code)
            .ToArrayAsync();
        dbContext.SaasFeatures.AddRange(featureCodes.Except(existingFeatureCodes, StringComparer.Ordinal).Select(code =>
            new SaasFeature
            {
                Code = code,
                Name = code,
                Category = code.Split('.')[0],
                IsCore = code == SaasFeatureCatalog.CoreBackoffice,
                Status = SaasFeatureStatus.Active
            }));
        var moduleCodes = permissionCodes.Select(PermissionModuleCatalog.ResolvePermissionModuleCode)
            .Distinct(StringComparer.Ordinal).ToArray();
        var existingModuleCodes = await dbContext.PermissionModules.Where(value => moduleCodes.Contains(value.Code))
            .Select(value => value.Code).ToArrayAsync();
        dbContext.PermissionModules.AddRange(moduleCodes.Except(existingModuleCodes, StringComparer.Ordinal)
            .Select(code => new PermissionModule
            {
                Code = code,
                Name = code,
                Area = BackendPermissionArea.Tenant,
                RequiredFeatureCode = PermissionModuleCatalog.RequiredFeatures[code]
            }));
        await dbContext.SaveChangesAsync();
        var existingPermissionCodes = await dbContext.BackendPermissions
            .Where(permission => permissionCodes.Contains(permission.Code))
            .Select(permission => permission.Code)
            .ToListAsync();
        foreach (var permissionCode in permissionCodes.Except(existingPermissionCodes, StringComparer.Ordinal))
            dbContext.BackendPermissions.Add(new BackendPermission
            {
                Code = permissionCode,
                Name = permissionCode,
                Area = BackendPermissionArea.Tenant,
                PermissionModuleCode = PermissionModuleCatalog.ResolvePermissionModuleCode(permissionCode),
                IsSystem = true
            });

        foreach (var tenantGroup in members.GroupBy(member => member.TenantId))
        {
            var tenantId = tenantGroup.Key;
            var role = await dbContext.TenantBackendRoles.SingleOrDefaultAsync(entity =>
                entity.TenantId == tenantId && entity.Code == "integration_test_admin");
            if (role is null)
            {
                role = new TenantBackendRole
                {
                    TenantId = tenantId,
                    Code = "integration_test_admin",
                    Name = "Integration Test Administrator",
                    Status = BackendRoleStatus.Active,
                    IsSystem = true,
                    DataScope = JsonSerializer.SerializeToElement(new { mode = "all" })
                };
                dbContext.TenantBackendRoles.Add(role);
            }

            var assignedPermissionCodes = await dbContext.TenantBackendRolePermissions
                .Where(entity => entity.TenantId == tenantId && entity.RoleId == role.Id)
                .Select(entity => entity.PermissionCode)
                .ToListAsync();
            foreach (var permissionCode in permissionCodes.Except(assignedPermissionCodes, StringComparer.Ordinal))
                dbContext.TenantBackendRolePermissions.Add(new TenantBackendRolePermission
                {
                    TenantId = tenantId,
                    RoleId = role.Id,
                    PermissionCode = permissionCode
                });

            var assignedUserIds = await dbContext.TenantBackendUserRoles
                .Where(entity => entity.TenantId == tenantId && entity.RoleId == role.Id)
                .Select(entity => entity.UserId)
                .ToListAsync();
            foreach (var member in tenantGroup.Where(member => !assignedUserIds.Contains(member.UserId)))
                dbContext.TenantBackendUserRoles.Add(new TenantBackendUserRole
                {
                    TenantId = tenantId,
                    UserId = member.UserId,
                    RoleId = role.Id
                });
        }

        await dbContext.SaveChangesAsync();
    }

    public IServiceScope CreateSystemScope(string reason = "Integration test verification")
    {
        var scope = Services.CreateScope();
        scope.ServiceProvider.GetRequiredService<ITenantContextInitializer>()
            .InitializeSystem(null, reason);
        return scope;
    }

    public IServiceScope CreateTenantScope(Guid tenantId, string? tenantCode = null)
    {
        var scope = Services.CreateScope();
        scope.ServiceProvider.GetRequiredService<ITenantContextInitializer>()
            .Initialize(tenantId, tenantCode, TenantResolutionSource.TenantCode);
        return scope;
    }

    public async Task SeedQuestionWithVersionAsync(Question question, QuestionVersion version)
    {
        question.CurrentVersionId = null;
        await SeedAsync(question);
        await SeedAsync(version);

        using var scope = Services.CreateScope();
        scope.ServiceProvider.GetRequiredService<ITenantContextInitializer>()
            .InitializeSystem(question.TenantId, "Integration test question version linking");
        var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
        var persistedQuestion = await dbContext.Questions.SingleAsync(item =>
            item.TenantId == question.TenantId && item.Id == question.Id);
        persistedQuestion.CurrentVersionId = version.Id;
        await dbContext.SaveChangesAsync();
    }

    public async Task<TenantQuestionReference> SeedQuestionReferenceAsync(
        Guid tenantId,
        Guid questionOwnerTenantId,
        Guid questionId,
        QuestionSource source = QuestionSource.Tenant)
    {
        var reference = new TenantQuestionReference
        {
            Id = Guid.NewGuid(),
            TenantId = tenantId,
            QuestionOwnerTenantId = questionOwnerTenantId,
            QuestionId = questionId,
            Source = source
        };
        await SeedAsync(reference);
        return reference;
    }

    public async Task<Guid> SeedActiveSessionAsync(
        Guid userId,
        Guid? tenantId = null,
        string tokenHash = "integration-test-token-hash",
        bool includeMembership = false)
    {
        var resolvedTenantId = tenantId ?? Guid.NewGuid();
        var user = new User
        {
            Id = userId,
            Phone = "13800000000"
        };
        var session = new AuthSession
        {
            Id = Guid.NewGuid(),
            Realm = AuthRealm.Tenant,
            TenantId = resolvedTenantId,
            UserId = userId,
            TokenFamilyId = Guid.NewGuid(),
            TokenHash = tokenHash,
            SecurityStamp = user.SecurityStamp ?? string.Empty,
            Provider = "test",
            ExpiresAt = DateTimeOffset.UtcNow.AddHours(1)
        };
        var entities = new List<object>
        {
            new Tenant
            {
                Id = resolvedTenantId,
                Slug = resolvedTenantId.ToString("N"),
                Name = "Test Tenant"
            },
            user,
            session
        };
        if (includeMembership)
            entities.Add(new TenantMembership
            {
                TenantId = resolvedTenantId,
                UserId = userId,
                Role = TenantRole.Student,
                Status = MembershipStatus.Active
            });

        await SeedAsync([.. entities]);
        return session.Id;
    }

    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);
        if (disposing) database.Dispose();
    }
}