using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Tiku.Api.Contracts;
using Tiku.Application.PlatformBilling;
using Tiku.Application.Security;
using Tiku.Domain.Common;
using Tiku.Domain.Identity;
using Tiku.Domain.Operations;
using Tiku.Domain.Platform;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;

namespace Tiku.IntegrationTests.Api;

public sealed class SaasBillingLifecycleTests
{
    [Fact]
    public async Task Platform_can_publish_feature_limit_and_immutable_offering_version()
    {
        await using var factory = PlatformFactory();
        var platform = await SeedPlatformAdminAsync(factory);
        using var client = factory.CreateClient();
        client.UseAccessToken(await client.LoginAsPlatformAsync(platform.Email));

        var feature = await client.PutAsJsonAsync(
            "/api/platform-admin/saas/features",
            new UpsertSaasFeatureDto(
                null,
                SaasFeatureCatalog.PrivateQuestionBank,
                "私有题库",
                "content",
                "租户私有题库",
                20_000,
                "CNY",
                SaasFeatureStatus.Active,
                10));
        var limit = await client.PutAsJsonAsync(
            "/api/platform-admin/saas/feature-limits",
            new UpsertSaasFeatureLimitDto(
                null,
                SaasQuotaMetricCatalog.PrivateQuestionCount,
                SaasFeatureCatalog.PrivateQuestionBank,
                "私有题目数",
                "count",
                SaasFeatureLimitKind.Current,
                80,
                true));
        var offering = await client.PutAsJsonAsync(
            "/api/platform-admin/saas/offerings",
            new UpsertSaasOfferingDto(
                null,
                $"private-bank-{Guid.NewGuid():N}",
                "题库基础套餐",
                SaasOfferingType.BasePlan,
                SaasOfferingStatus.Draft,
                null,
                10));

        Assert.Equal(HttpStatusCode.OK, feature.StatusCode);
        Assert.Equal(HttpStatusCode.OK, limit.StatusCode);
        Assert.Equal(HttpStatusCode.OK, offering.StatusCode);
        var offeringId = await ReadGuidAsync(offering, "id");
        var versionRequest = new UpsertSaasOfferingVersionDto(
            null,
            offeringId,
            PlatformBillingCycle.Yearly,
            24_000,
            20_000,
            "CNY",
            null,
            [SaasFeatureCatalog.PrivateQuestionBank],
            new Dictionary<string, long>
            {
                [SaasQuotaMetricCatalog.PrivateQuestionCount] = 10_000
            },
            JsonDefaults.Object());
        var draft = await client.PutAsJsonAsync("/api/platform-admin/saas/offering-versions", versionRequest);

        Assert.Equal(HttpStatusCode.OK, draft.StatusCode);
        var versionId = await ReadGuidAsync(draft, "id");
        var publish = await client.PostAsync($"/api/platform-admin/saas/offering-versions/{versionId}/publish", null);
        var mutatePublished = await client.PutAsJsonAsync(
            "/api/platform-admin/saas/offering-versions",
            versionRequest with { Id = versionId, AmountCents = 19_000 });

        Assert.Equal(HttpStatusCode.OK, publish.StatusCode);
        Assert.Equal(HttpStatusCode.Conflict, mutatePublished.StatusCode);
        Assert.Equal("saas_offering_version_immutable", await ReadCodeAsync(mutatePublished));

        using var scope = factory.CreateSystemScope("Verify immutable SaaS catalog version");
        var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
        var stored = await dbContext.SaasOfferingVersions.AsNoTracking().SingleAsync(value => value.Id == versionId);
        Assert.Equal(SaasOfferingVersionStatus.Published, stored.Status);
        Assert.Equal(20_000, stored.AmountCents);
        Assert.True(await dbContext.SaasOfferingVersionLimits.AnyAsync(value =>
            value.OfferingVersionId == versionId &&
            value.MetricCode == SaasQuotaMetricCatalog.PrivateQuestionCount &&
            value.LimitValue == 10_000));
    }

    [Fact]
    public async Task Tenant_can_purchase_manual_plan_and_other_tenant_cannot_read_order()
    {
        await using var factory = PlatformFactory();
        var platform = await SeedPlatformAdminAsync(factory);
        var platformTenantId = Guid.NewGuid();
        var tenantA = await SeedTenantAdminWithoutSubscriptionAsync(factory, "billing-a");
        await factory.SeedAsync(new Tenant
        {
            Id = platformTenantId,
            Slug = $"platform-{platformTenantId:N}",
            Name = "Platform Owner",
            Mode = TenantMode.PlatformOwned,
            Status = TenantStatus.Active
        });
        var catalog = await SeedPublishedPlanAsync(factory, SaasFeatureCatalog.Exam, limit: null);

        using var tenantClient = factory.CreateClient();
        tenantClient.UseAccessToken(await tenantClient.LoginAsTenantAsync(tenantA.TenantId, tenantA.Phone));
        var catalogResponse = await tenantClient.GetAsync("/api/tenant-billing/catalog");
        var quoteIdempotencyKey = $"quote-{Guid.NewGuid():N}";
        var quoteRequest = new CreatePlatformBillingQuoteDto(
            catalog.VersionId,
            [],
            PlatformBillingOrderPurpose.NewSubscription,
            quoteIdempotencyKey);
        var quoteResponse = await tenantClient.PostAsJsonAsync(
            "/api/tenant-billing/quotes",
            quoteRequest);
        var repeatedQuoteResponse = await tenantClient.PostAsJsonAsync(
            "/api/tenant-billing/quotes",
            quoteRequest);
        Assert.Equal(HttpStatusCode.OK, catalogResponse.StatusCode);
        Assert.Equal(HttpStatusCode.OK, quoteResponse.StatusCode);
        Assert.Equal(HttpStatusCode.OK, repeatedQuoteResponse.StatusCode);
        var quoteId = await ReadGuidAsync(quoteResponse, "id");
        Assert.Equal(quoteId, await ReadGuidAsync(repeatedQuoteResponse, "id"));

        var idempotencyKey = $"order-{Guid.NewGuid():N}";
        var orderResponse = await tenantClient.PostAsJsonAsync(
            "/api/tenant-billing/orders",
            new CreatePlatformBillingOrderDto(quoteId, idempotencyKey));
        var repeatedOrder = await tenantClient.PostAsJsonAsync(
            "/api/tenant-billing/orders",
            new CreatePlatformBillingOrderDto(quoteId, idempotencyKey));
        Assert.Equal(HttpStatusCode.OK, orderResponse.StatusCode);
        Assert.Equal(HttpStatusCode.OK, repeatedOrder.StatusCode);
        var orderNo = await ReadStringAsync(orderResponse, "orderNo");
        Assert.Equal(orderNo, await ReadStringAsync(repeatedOrder, "orderNo"));

        var paymentResponse = await tenantClient.PostAsJsonAsync(
            $"/api/tenant-billing/orders/{orderNo}/payments",
            new CreatePlatformBillingPaymentDto(
                "manual",
                "bank_transfer",
                $"payment-{Guid.NewGuid():N}",
                null,
                null,
                null));
        Assert.Equal(HttpStatusCode.OK, paymentResponse.StatusCode);
        var paymentId = await ReadGuidAsync(paymentResponse, "id");

        using var platformClient = factory.CreateClient();
        platformClient.UseAccessToken(await platformClient.LoginAsPlatformAsync(platform.Email));
        var confirmation = new ConfirmManualPlatformPaymentDto(
            paymentId,
            $"manual-{Guid.NewGuid():N}",
            DateTimeOffset.UtcNow,
            "integration test receipt");
        var confirm = await platformClient.PostAsJsonAsync(
            "/api/platform-admin/saas/payments/manual/confirm",
            confirmation);
        var repeatedConfirm = await platformClient.PostAsJsonAsync(
            "/api/platform-admin/saas/payments/manual/confirm",
            confirmation);
        Assert.Equal(HttpStatusCode.OK, confirm.StatusCode);
        Assert.Equal(HttpStatusCode.OK, repeatedConfirm.StatusCode);

        var subscription = await tenantClient.GetAsync("/api/tenant-billing/subscription");
        Assert.Equal(HttpStatusCode.OK, subscription.StatusCode);
        Assert.Equal("Active", await ReadStringAsync(subscription, "status"));

        var tenantB = await SeedTenantAdminWithoutSubscriptionAsync(factory, "billing-b");
        using var tenantBClient = factory.CreateClient();
        tenantBClient.UseAccessToken(await tenantBClient.LoginAsTenantAsync(tenantB.TenantId, tenantB.Phone));
        var crossTenantOrder = await tenantBClient.GetAsync($"/api/tenant-billing/orders/{orderNo}");
        Assert.Equal(HttpStatusCode.NotFound, crossTenantOrder.StatusCode);

        using var scope = factory.CreateSystemScope("Verify SaaS billing settlement idempotency");
        var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
        Assert.Single(await dbContext.TenantSaasSubscriptions.AsNoTracking()
            .Where(value => value.TenantId == tenantA.TenantId)
            .ToArrayAsync());
        Assert.Single(await dbContext.PlatformBillingInvoices.AsNoTracking()
            .Where(value => value.TenantId == tenantA.TenantId)
            .ToArrayAsync());
        Assert.Single(await dbContext.PlatformBillingPaymentEvents.AsNoTracking()
            .Where(value => value.TenantId == tenantA.TenantId && value.PaymentId == paymentId)
            .ToArrayAsync());
    }

    [Fact]
    public async Task Tenant_role_cannot_bind_permission_for_unpurchased_feature()
    {
        await using var factory = new ApiTestFactory();
        var tenant = await SeedTenantAdminWithoutSubscriptionAsync(factory, "role-feature");
        using var client = factory.CreateClient();
        client.UseAccessToken(await client.LoginAsTenantAsync(tenant.TenantId, tenant.Phone));
        var roleResponse = await client.PostAsJsonAsync(
            "/api/backoffice/tenant/roles",
            new UpsertBackofficeRoleDto
            {
                Code = $"content_editor_{Guid.NewGuid():N}",
                Name = "内容编辑",
                Status = BackendRoleStatus.Active,
                DataScope = JsonDefaults.Object()
            });
        Assert.Equal(HttpStatusCode.OK, roleResponse.StatusCode);
        var roleId = await ReadGuidAsync(roleResponse, "id");

        var bind = await client.PutAsJsonAsync(
            $"/api/backoffice/tenant/roles/{roleId}/bindings",
            new ReplaceRoleBindingsDto
            {
                PermissionCodes = [BackendPermissions.TenantContentManage],
                MenuCodes = []
            });

        Assert.Equal(HttpStatusCode.BadRequest, bind.StatusCode);
        Assert.Equal("feature_not_available", await ReadCodeAsync(bind));
        using var scope = factory.CreateSystemScope("Verify rejected tenant role binding");
        var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
        Assert.False(await dbContext.TenantBackendRolePermissions.AnyAsync(value =>
            value.TenantId == tenant.TenantId &&
            value.RoleId == roleId &&
            value.PermissionCode == BackendPermissions.TenantContentManage));
    }

    [Fact]
    public async Task Video_only_subscription_can_bind_video_permission_without_private_question_permission()
    {
        await using var factory = new ApiTestFactory();
        var tenant = await SeedTenantAdminWithoutSubscriptionAsync(factory, "video-role-feature");
        var graph = CreatePublishedPlan(SaasFeatureCatalog.Video);
        var now = DateTimeOffset.UtcNow;
        await factory.SeedAsync(
            graph.Offering,
            graph.Version,
            graph.Entitlement,
            new TenantSaasSubscription
            {
                Id = graph.SubscriptionId,
                TenantId = tenant.TenantId,
                BaseOfferingVersionId = graph.Version.Id,
                Status = TenantSaasSubscriptionStatus.Active,
                StartsAt = now.AddDays(-1),
                CurrentPeriodStart = now.AddDays(-1),
                CurrentPeriodEnd = now.AddMonths(1)
            },
            new TenantSaasSubscriptionItem
            {
                TenantId = tenant.TenantId,
                SubscriptionId = graph.SubscriptionId,
                OfferingVersionId = graph.Version.Id,
                ItemType = TenantSaasSubscriptionItemType.BasePlan,
                Status = TenantSaasSubscriptionItemStatus.Active,
                StartsAt = now.AddDays(-1),
                EndsAt = now.AddMonths(1)
            });
        await PublishFixtureVersionAsync(factory, graph.Version.Id);

        using var client = factory.CreateClient();
        client.UseAccessToken(await client.LoginAsTenantAsync(tenant.TenantId, tenant.Phone));
        var roleResponse = await client.PostAsJsonAsync(
            "/api/backoffice/tenant/roles",
            new UpsertBackofficeRoleDto
            {
                Code = $"video_editor_{Guid.NewGuid():N}",
                Name = "视频编辑",
                Status = BackendRoleStatus.Active,
                DataScope = JsonDefaults.Object()
            });
        Assert.Equal(HttpStatusCode.OK, roleResponse.StatusCode);
        var roleId = await ReadGuidAsync(roleResponse, "id");

        var videoBinding = await client.PutAsJsonAsync(
            $"/api/backoffice/tenant/roles/{roleId}/bindings",
            new ReplaceRoleBindingsDto
            {
                PermissionCodes = [BackendPermissions.TenantVideoManage],
                MenuCodes = []
            });
        var questionBinding = await client.PutAsJsonAsync(
            $"/api/backoffice/tenant/roles/{roleId}/bindings",
            new ReplaceRoleBindingsDto
            {
                PermissionCodes = [BackendPermissions.TenantContentManage],
                MenuCodes = []
            });

        Assert.Equal(HttpStatusCode.OK, videoBinding.StatusCode);
        Assert.Equal(HttpStatusCode.BadRequest, questionBinding.StatusCode);
        Assert.Equal("feature_not_available", await ReadCodeAsync(questionBinding));
    }

    [Fact]
    public async Task Quota_warns_at_eighty_percent_and_concurrent_consumers_cannot_exceed_limit()
    {
        await using var factory = new ApiTestFactory();
        var tenantId = Guid.NewGuid();
        await SeedQuotaSubscriptionAsync(factory, tenantId, limit: 10);

        using (var scope = factory.CreateSystemScope("Seed SaaS quota usage"))
        {
            var featureAccess = scope.ServiceProvider.GetRequiredService<IFeatureAccessService>();
            Assert.True(await featureAccess.TryConsumeQuotaAsync(
                tenantId,
                SaasQuotaMetricCatalog.ExportCount,
                8));
            var atWarning = Assert.Single(await featureAccess.GetQuotaSummaryAsync(tenantId));
            Assert.Equal(80, atWarning.UsedPercent);
            Assert.True(atWarning.Warning);
            Assert.False(atWarning.Exceeded);
        }

        async Task<bool> ConsumeFinalCapacityAsync()
        {
            using var scope = factory.CreateSystemScope("Race SaaS quota consumption");
            return await scope.ServiceProvider.GetRequiredService<IFeatureAccessService>()
                .TryConsumeQuotaAsync(tenantId, SaasQuotaMetricCatalog.ExportCount, 2);
        }

        var results = await Task.WhenAll(ConsumeFinalCapacityAsync(), ConsumeFinalCapacityAsync());
        Assert.Equal(1, results.Count(value => value));

        using var verificationScope = factory.CreateSystemScope("Verify SaaS quota atomicity");
        var finalUsage = Assert.Single(await verificationScope.ServiceProvider
            .GetRequiredService<IFeatureAccessService>()
            .GetQuotaSummaryAsync(tenantId));
        Assert.Equal(10, finalUsage.UsedValue);
        Assert.True(finalUsage.Warning);
        Assert.True(finalUsage.Exceeded);
    }

    [Fact]
    public async Task Onboarding_is_ready_when_required_delivery_steps_are_complete()
    {
        await using var factory = new ApiTestFactory();
        var tenantId = Guid.NewGuid();
        var ownerId = Guid.NewGuid();
        var phone = $"139{Random.Shared.Next(10_000_000, 99_999_999)}";
        var plan = CreatePublishedPlan(SaasFeatureCatalog.Exam);
        var now = DateTimeOffset.UtcNow;
        await factory.SeedAsync(
            new Tenant
            {
                Id = tenantId,
                Slug = tenantId.ToString("N"),
                Name = "Onboarding Tenant",
                OwnerUserId = ownerId,
                Status = TenantStatus.Active,
                BillingStatus = BillingStatus.Active
            },
            new User
            {
                Id = ownerId,
                Phone = phone,
                Name = "Tenant Owner",
                ForcePasswordChange = false
            }.WithTestPassword(),
            new TenantMembership
            {
                TenantId = tenantId,
                UserId = ownerId,
                Role = TenantRole.TenantOwner,
                Status = MembershipStatus.Active
            },
            plan.Feature,
            plan.Offering,
            plan.Version,
            plan.Entitlement,
            new TenantSaasSubscription
            {
                Id = plan.SubscriptionId,
                TenantId = tenantId,
                BaseOfferingVersionId = plan.Version.Id,
                Status = TenantSaasSubscriptionStatus.Active,
                StartsAt = now.AddDays(-1),
                CurrentPeriodStart = now.AddDays(-1),
                CurrentPeriodEnd = now.AddMonths(1)
            },
            new TenantSaasSubscriptionItem
            {
                TenantId = tenantId,
                SubscriptionId = plan.SubscriptionId,
                OfferingVersionId = plan.Version.Id,
                ItemType = TenantSaasSubscriptionItemType.BasePlan,
                Status = TenantSaasSubscriptionItemStatus.Active,
                StartsAt = now.AddDays(-1),
                EndsAt = now.AddMonths(1)
            },
            new TenantDomain
            {
                TenantId = tenantId,
                Host = $"{tenantId:N}.example.test",
                Status = TenantDomainStatus.Active,
                IsPrimary = true,
                VerifiedAt = now,
                DnsVerifiedAt = now,
                TlsReadyAt = now
            },
            new TenantFrontendConfig
            {
                TenantId = tenantId,
                PublishedAt = now,
                ConfigVersion = 1
            },
            new TenantAuthPolicy
            {
                TenantId = tenantId,
                AllowedStudentLoginMethods = ["password"]
            });
        await PublishFixtureVersionAsync(factory, plan.Version.Id);

        using var client = factory.CreateClient();
        client.UseAccessToken(await client.LoginAsTenantAsync(tenantId, phone));
        var response = await client.GetAsync("/api/tenant-onboarding/status");

        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        using var payload = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
        Assert.True(payload.RootElement.GetProperty("readyForStudentTraffic").GetBoolean());
        Assert.Equal(
            payload.RootElement.GetProperty("requiredSteps").GetInt32(),
            payload.RootElement.GetProperty("completedRequiredSteps").GetInt32());
        var requiredIncomplete = payload.RootElement.GetProperty("steps").EnumerateArray()
            .Where(value => value.GetProperty("required").GetBoolean() && !value.GetProperty("completed").GetBoolean())
            .ToArray();
        Assert.Empty(requiredIncomplete);
    }

    [Fact]
    public async Task Subscription_lifecycle_activates_scheduled_plan_once_at_period_boundary()
    {
        await using var factory = new ApiTestFactory();
        var tenantId = Guid.NewGuid();
        var subscriptionId = Guid.NewGuid();
        var periodEnd = DateTimeOffset.UtcNow.AddMinutes(-5);
        var oldFeature = new SaasFeature
        {
            Code = $"lifecycle.old.{Guid.NewGuid():N}",
            Name = "Old plan feature",
            Category = "integration",
            Status = SaasFeatureStatus.Active
        };
        var newFeature = new SaasFeature
        {
            Code = $"lifecycle.new.{Guid.NewGuid():N}",
            Name = "New plan feature",
            Category = "integration",
            Status = SaasFeatureStatus.Active
        };
        var oldOffering = new SaasOffering
        {
            Code = $"lifecycle-old-{Guid.NewGuid():N}",
            Name = "Old lifecycle plan",
            Type = SaasOfferingType.BasePlan,
            Status = SaasOfferingStatus.Active
        };
        var newOffering = new SaasOffering
        {
            Code = $"lifecycle-new-{Guid.NewGuid():N}",
            Name = "New lifecycle plan",
            Type = SaasOfferingType.BasePlan,
            Status = SaasOfferingStatus.Active
        };
        var oldVersion = CreatePublishedVersion(oldOffering.Id, 1);
        var newVersion = CreatePublishedVersion(newOffering.Id, 1);
        var nextPeriodEnd = periodEnd.AddMonths(1);
        await factory.SeedAsync(
            new Tenant
            {
                Id = tenantId,
                Slug = $"lifecycle-{tenantId:N}",
                Name = "Lifecycle Tenant",
                Status = TenantStatus.Active,
                BillingStatus = BillingStatus.Active
            },
            oldFeature,
            newFeature,
            oldOffering,
            newOffering,
            oldVersion,
            newVersion,
            new SaasOfferingVersionFeature
            {
                OfferingVersionId = oldVersion.Id,
                FeatureCode = oldFeature.Code
            },
            new SaasOfferingVersionFeature
            {
                OfferingVersionId = newVersion.Id,
                FeatureCode = newFeature.Code
            },
            new TenantSaasSubscription
            {
                Id = subscriptionId,
                TenantId = tenantId,
                BaseOfferingVersionId = oldVersion.Id,
                ScheduledBaseOfferingVersionId = newVersion.Id,
                Status = TenantSaasSubscriptionStatus.Active,
                StartsAt = periodEnd.AddMonths(-1),
                CurrentPeriodStart = periodEnd.AddMonths(-1),
                CurrentPeriodEnd = periodEnd
            },
            new TenantSaasSubscriptionItem
            {
                TenantId = tenantId,
                SubscriptionId = subscriptionId,
                OfferingVersionId = oldVersion.Id,
                ItemType = TenantSaasSubscriptionItemType.BasePlan,
                Status = TenantSaasSubscriptionItemStatus.Active,
                StartsAt = periodEnd.AddMonths(-1),
                EndsAt = periodEnd
            },
            new TenantSaasSubscriptionItem
            {
                TenantId = tenantId,
                SubscriptionId = subscriptionId,
                OfferingVersionId = newVersion.Id,
                ItemType = TenantSaasSubscriptionItemType.BasePlan,
                Status = TenantSaasSubscriptionItemStatus.Scheduled,
                StartsAt = periodEnd,
                EndsAt = nextPeriodEnd
            });

        async Task<int> RunLifecycleAsync(string reason)
        {
            using var lifecycleScope = factory.CreateSystemScope(reason);
            return await lifecycleScope.ServiceProvider
                .GetRequiredService<ISaasSubscriptionLifecycleService>()
                .ProcessDueAsync(periodEnd.AddMinutes(1));
        }

        var concurrentResults = await Task.WhenAll(
            RunLifecycleAsync("Run first concurrent SaaS subscription lifecycle worker"),
            RunLifecycleAsync("Run second concurrent SaaS subscription lifecycle worker"));
        Assert.Equal(1, concurrentResults.Count(value => value == 1));
        Assert.Equal(1, concurrentResults.Count(value => value == 0));
        Assert.Equal(0, await RunLifecycleAsync("Repeat completed SaaS subscription lifecycle worker"));

        using var verificationScope = factory.CreateSystemScope("Verify scheduled SaaS subscription activation");
        var dbContext = verificationScope.ServiceProvider.GetRequiredService<TikuDbContext>();
        var subscription = await dbContext.TenantSaasSubscriptions.AsNoTracking()
            .SingleAsync(value => value.Id == subscriptionId);
        var items = await dbContext.TenantSaasSubscriptionItems.AsNoTracking()
            .Where(value => value.SubscriptionId == subscriptionId)
            .OrderBy(value => value.StartsAt)
            .ToArrayAsync();
        Assert.Equal(newVersion.Id, subscription.BaseOfferingVersionId);
        Assert.Null(subscription.ScheduledBaseOfferingVersionId);
        Assert.Equal(TenantSaasSubscriptionStatus.Active, subscription.Status);
        Assert.Equal(periodEnd, subscription.CurrentPeriodStart);
        Assert.Equal(nextPeriodEnd, subscription.CurrentPeriodEnd);
        Assert.Equal(1, subscription.LifecycleVersion);
        Assert.Equal(TenantSaasSubscriptionItemStatus.Expired, items[0].Status);
        Assert.Equal(TenantSaasSubscriptionItemStatus.Active, items[1].Status);
        Assert.Single(await dbContext.AuditLogs.AsNoTracking().Where(value =>
            value.TenantId == tenantId &&
            value.Action == "platform_billing.subscription.scheduled_plan_activated").ToArrayAsync());
    }

    [Fact]
    public async Task Subscription_lifecycle_applies_cancel_trial_past_due_and_expired_transitions_idempotently()
    {
        await using var factory = new ApiTestFactory();
        var asOf = DateTimeOffset.UtcNow;
        var feature = new SaasFeature
        {
            Code = $"lifecycle.status.{Guid.NewGuid():N}",
            Name = "Lifecycle status feature",
            Category = "integration",
            Status = SaasFeatureStatus.Active
        };
        var offering = new SaasOffering
        {
            Code = $"lifecycle-status-{Guid.NewGuid():N}",
            Name = "Lifecycle status plan",
            Type = SaasOfferingType.BasePlan,
            Status = SaasOfferingStatus.Active
        };
        var version = CreatePublishedVersion(offering.Id, 1);
        var fixtures = new[]
        {
            CreateLifecycleFixture(version.Id, TenantSaasSubscriptionStatus.Active, asOf.AddDays(-1), cancelAtPeriodEnd: true),
            CreateLifecycleFixture(version.Id, TenantSaasSubscriptionStatus.Trial, asOf.AddDays(-1)),
            CreateLifecycleFixture(version.Id, TenantSaasSubscriptionStatus.Active, asOf.AddDays(-8)),
            CreateLifecycleFixture(version.Id, TenantSaasSubscriptionStatus.PastDue, asOf.AddDays(-8))
        };
        await factory.SeedAsync(
        [
            feature,
            offering,
            version,
            new SaasOfferingVersionFeature
            {
                OfferingVersionId = version.Id,
                FeatureCode = feature.Code
            },
            ..fixtures.SelectMany(value => new object[] { value.Tenant, value.Subscription, value.Item })
        ]);

        using var lifecycleScope = factory.CreateSystemScope("Run SaaS subscription terminal lifecycle transitions");
        var lifecycle = lifecycleScope.ServiceProvider.GetRequiredService<ISaasSubscriptionLifecycleService>();
        Assert.Equal(4, await lifecycle.ProcessDueAsync(asOf));
        Assert.Equal(1, await lifecycle.ProcessDueAsync(asOf));
        Assert.Equal(0, await lifecycle.ProcessDueAsync(asOf));

        using var verificationScope = factory.CreateSystemScope("Verify SaaS subscription terminal lifecycle transitions");
        var dbContext = verificationScope.ServiceProvider.GetRequiredService<TikuDbContext>();
        var subscriptions = await dbContext.TenantSaasSubscriptions.AsNoTracking()
            .Where(value => fixtures.Select(fixture => fixture.Subscription.Id).Contains(value.Id))
            .ToDictionaryAsync(value => value.Id);
        Assert.Equal(TenantSaasSubscriptionStatus.Cancelled, subscriptions[fixtures[0].Subscription.Id].Status);
        Assert.Equal(TenantSaasSubscriptionStatus.Expired, subscriptions[fixtures[1].Subscription.Id].Status);
        Assert.Equal(TenantSaasSubscriptionStatus.Expired, subscriptions[fixtures[2].Subscription.Id].Status);
        Assert.Equal(TenantSaasSubscriptionStatus.Expired, subscriptions[fixtures[3].Subscription.Id].Status);
        Assert.Equal(1, subscriptions[fixtures[0].Subscription.Id].LifecycleVersion);
        Assert.Equal(1, subscriptions[fixtures[1].Subscription.Id].LifecycleVersion);
        Assert.Equal(2, subscriptions[fixtures[2].Subscription.Id].LifecycleVersion);
        Assert.Equal(1, subscriptions[fixtures[3].Subscription.Id].LifecycleVersion);
    }

    private static ApiTestFactory PlatformFactory() => new(configurationOverrides: new Dictionary<string, string?>
    {
        ["Tenancy:Resolution:PlatformHosts:0"] = "localhost"
    });

    private static async Task<(Guid UserId, string Email)> SeedPlatformAdminAsync(ApiTestFactory factory)
    {
        var userId = Guid.NewGuid();
        var roleId = Guid.NewGuid();
        var email = $"saas-platform-{Guid.NewGuid():N}@example.test";
        var modules = BackendPermissions.Platform
            .Select(PermissionModuleCatalog.ResolvePermissionModuleCode)
            .Distinct(StringComparer.Ordinal)
            .Select(code => new PermissionModule
            {
                Code = code,
                Name = code,
                Area = BackendPermissionArea.Platform,
                RequiredFeatureCode = PermissionModuleCatalog.RequiredFeatures[code]
            })
            .Cast<object>();
        var permissions = BackendPermissions.Platform.Select(code => new BackendPermission
        {
            Code = code,
            Name = code,
            Area = BackendPermissionArea.Platform,
            PermissionModuleCode = PermissionModuleCatalog.ResolvePermissionModuleCode(code),
            IsSystem = true
        }).Cast<object>();
        await factory.SeedAsync(
        [
            ..modules,
            ..permissions,
            new User
            {
                Id = userId,
                Email = email,
                NormalizedEmail = email.ToUpperInvariant(),
                UserName = email,
                NormalizedUserName = email.ToUpperInvariant(),
                Name = "SaaS Platform Admin",
                PrimaryRole = "platform_admin",
                RawProfile = JsonDefaults.Object()
            }.WithTestPassword(),
            new PlatformBackendRole
            {
                Id = roleId,
                Code = $"saas_platform_admin_{Guid.NewGuid():N}",
                Name = "SaaS Platform Admin",
                Status = BackendRoleStatus.Active,
                IsSystem = true
            },
            ..BackendPermissions.Platform.Select(code => new PlatformBackendRolePermission
            {
                RoleId = roleId,
                PermissionCode = code
            }),
            new PlatformBackendUserRole
            {
                UserId = userId,
                RoleId = roleId
            }
        ]);
        return (userId, email);
    }

    private static async Task<(Guid TenantId, Guid UserId, string Phone)> SeedTenantAdminWithoutSubscriptionAsync(
        ApiTestFactory factory,
        string slugPrefix)
    {
        var tenantId = Guid.NewGuid();
        var userId = Guid.NewGuid();
        var phone = $"138{Random.Shared.Next(10_000_000, 99_999_999)}";
        await factory.SeedAsync(
            new Tenant
            {
                Id = tenantId,
                Slug = tenantId.ToString("N"),
                Name = $"{slugPrefix} tenant",
                Status = TenantStatus.Active,
                BillingStatus = BillingStatus.Trial
            },
            new User
            {
                Id = userId,
                Phone = phone,
                Name = $"{slugPrefix} admin"
            }.WithTestPassword(),
            new TenantMembership
            {
                TenantId = tenantId,
                UserId = userId,
                Role = TenantRole.TenantAdmin,
                Status = MembershipStatus.Active
            },
            new SaasOffering
            {
                Code = $"fixture-suppress-default-{Guid.NewGuid():N}",
                Name = "Fixture only",
                Type = SaasOfferingType.AddOn,
                Status = SaasOfferingStatus.Draft
            });
        return (tenantId, userId, phone);
    }

    private static async Task<(Guid OfferingId, Guid VersionId)> SeedPublishedPlanAsync(
        ApiTestFactory factory,
        string featureCode,
        (string MetricCode, long Value)? limit)
    {
        var graph = CreatePublishedPlan(featureCode);
        var entities = new List<object> { graph.Feature, graph.Offering, graph.Version, graph.Entitlement };
        if (limit is { } quota)
        {
            entities.Add(new SaasFeatureLimitDefinition
            {
                MetricCode = quota.MetricCode,
                FeatureCode = featureCode,
                Name = quota.MetricCode,
                Unit = "count",
                Kind = SaasFeatureLimitKind.Period,
                WarningPercent = 80,
                IsHardLimit = true
            });
            entities.Add(new SaasOfferingVersionLimit
            {
                OfferingVersionId = graph.Version.Id,
                MetricCode = quota.MetricCode,
                LimitValue = quota.Value
            });
        }
        await factory.SeedAsync([.. entities]);
        await PublishFixtureVersionAsync(factory, graph.Version.Id);
        return (graph.Offering.Id, graph.Version.Id);
    }

    private static async Task SeedQuotaSubscriptionAsync(ApiTestFactory factory, Guid tenantId, long limit)
    {
        var graph = CreatePublishedPlan(SaasFeatureCatalog.PrivateQuestionBank);
        var now = DateTimeOffset.UtcNow;
        await factory.SeedAsync(
            new Tenant
            {
                Id = tenantId,
                Slug = $"quota-{tenantId:N}",
                Name = "Quota Tenant",
                Status = TenantStatus.Active,
                BillingStatus = BillingStatus.Active
            },
            graph.Feature,
            graph.Offering,
            graph.Version,
            graph.Entitlement,
            new SaasFeatureLimitDefinition
            {
                MetricCode = SaasQuotaMetricCatalog.ExportCount,
                FeatureCode = SaasFeatureCatalog.PrivateQuestionBank,
                Name = "Export count",
                Unit = "count",
                Kind = SaasFeatureLimitKind.Period,
                WarningPercent = 80,
                IsHardLimit = true
            },
            new SaasOfferingVersionLimit
            {
                OfferingVersionId = graph.Version.Id,
                MetricCode = SaasQuotaMetricCatalog.ExportCount,
                LimitValue = limit
            },
            new TenantSaasSubscription
            {
                Id = graph.SubscriptionId,
                TenantId = tenantId,
                BaseOfferingVersionId = graph.Version.Id,
                Status = TenantSaasSubscriptionStatus.Active,
                StartsAt = now.AddDays(-1),
                CurrentPeriodStart = now.AddDays(-1),
                CurrentPeriodEnd = now.AddMonths(1)
            },
            new TenantSaasSubscriptionItem
            {
                TenantId = tenantId,
                SubscriptionId = graph.SubscriptionId,
                OfferingVersionId = graph.Version.Id,
                ItemType = TenantSaasSubscriptionItemType.BasePlan,
                Status = TenantSaasSubscriptionItemStatus.Active,
                StartsAt = now.AddDays(-1),
                EndsAt = now.AddMonths(1)
            });
        await PublishFixtureVersionAsync(factory, graph.Version.Id);
    }

    private static PublishedPlanGraph CreatePublishedPlan(string featureCode)
    {
        var offering = new SaasOffering
        {
            Id = Guid.NewGuid(),
            Code = $"plan-{Guid.NewGuid():N}",
            Name = "Integration SaaS Plan",
            Type = SaasOfferingType.BasePlan,
            Status = SaasOfferingStatus.Active
        };
        var version = new SaasOfferingVersion
        {
            Id = Guid.NewGuid(),
            OfferingId = offering.Id,
            Version = 1,
            Status = SaasOfferingVersionStatus.Draft,
            BillingCycle = PlatformBillingCycle.Monthly,
            OriginalAmountCents = 1_000,
            AmountCents = 1_000,
            Currency = "CNY",
            EffectiveAt = DateTimeOffset.UtcNow.AddDays(-1),
            PublishedAt = null
        };
        return new PublishedPlanGraph(
            new SaasFeature
            {
                Code = featureCode,
                Name = featureCode,
                Category = "integration",
                Status = SaasFeatureStatus.Active
            },
            offering,
            version,
            new SaasOfferingVersionFeature
            {
                OfferingVersionId = version.Id,
                FeatureCode = featureCode
            },
            Guid.NewGuid());
    }

    private static SaasOfferingVersion CreatePublishedVersion(Guid offeringId, int version) => new()
    {
        OfferingId = offeringId,
        Version = version,
        Status = SaasOfferingVersionStatus.Published,
        BillingCycle = PlatformBillingCycle.Monthly,
        OriginalAmountCents = 1_000,
        AmountCents = 1_000,
        Currency = "CNY",
        EffectiveAt = DateTimeOffset.UtcNow.AddDays(-1),
        PublishedAt = DateTimeOffset.UtcNow.AddDays(-1)
    };

    private static LifecycleFixture CreateLifecycleFixture(
        Guid offeringVersionId,
        TenantSaasSubscriptionStatus status,
        DateTimeOffset periodEnd,
        bool cancelAtPeriodEnd = false)
    {
        var tenantId = Guid.NewGuid();
        var subscriptionId = Guid.NewGuid();
        var periodStart = periodEnd.AddMonths(-1);
        return new(
            new Tenant
            {
                Id = tenantId,
                Slug = $"lifecycle-status-{tenantId:N}",
                Name = "Lifecycle Status Tenant",
                Status = TenantStatus.Active,
                BillingStatus = status == TenantSaasSubscriptionStatus.PastDue
                    ? BillingStatus.PastDue
                    : BillingStatus.Active
            },
            new TenantSaasSubscription
            {
                Id = subscriptionId,
                TenantId = tenantId,
                BaseOfferingVersionId = offeringVersionId,
                Status = status,
                StartsAt = periodStart,
                CurrentPeriodStart = periodStart,
                CurrentPeriodEnd = periodEnd,
                CancelAtPeriodEnd = cancelAtPeriodEnd
            },
            new TenantSaasSubscriptionItem
            {
                TenantId = tenantId,
                SubscriptionId = subscriptionId,
                OfferingVersionId = offeringVersionId,
                ItemType = TenantSaasSubscriptionItemType.BasePlan,
                Status = TenantSaasSubscriptionItemStatus.Active,
                StartsAt = periodStart,
                EndsAt = periodEnd
            });
    }

    private static async Task PublishFixtureVersionAsync(ApiTestFactory factory, Guid versionId)
    {
        using var scope = factory.CreateSystemScope("Publish integration SaaS plan fixture");
        var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
        var version = await dbContext.SaasOfferingVersions.SingleAsync(value => value.Id == versionId);
        version.Status = SaasOfferingVersionStatus.Published;
        version.PublishedAt = DateTimeOffset.UtcNow;
        version.EffectiveAt ??= version.PublishedAt;
        await dbContext.SaveChangesAsync();
    }

    private static async Task<Guid> ReadGuidAsync(HttpResponseMessage response, string propertyName)
    {
        using var payload = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
        return payload.RootElement.GetProperty(propertyName).GetGuid();
    }

    private static async Task<string> ReadStringAsync(HttpResponseMessage response, string propertyName)
    {
        using var payload = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
        return payload.RootElement.GetProperty(propertyName).GetString()
            ?? throw new InvalidOperationException($"Response property '{propertyName}' was null.");
    }

    private static Task<string> ReadCodeAsync(HttpResponseMessage response) => ReadStringAsync(response, "code");

    private sealed record PublishedPlanGraph(
        SaasFeature Feature,
        SaasOffering Offering,
        SaasOfferingVersion Version,
        SaasOfferingVersionFeature Entitlement,
        Guid SubscriptionId);

    private sealed record LifecycleFixture(
        Tenant Tenant,
        TenantSaasSubscription Subscription,
        TenantSaasSubscriptionItem Item);
}
