using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Tiku.Api.Contracts;
using Tiku.Application.Security;
using Tiku.Domain.Common;
using Tiku.Domain.Growth;
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 PlatformAdminEndpointTests
{
    [Fact]
    public async Task Platform_staff_password_reset_revokes_sessions_and_requires_change()
    {
        await using var factory = new ApiTestFactory(configurationOverrides: new Dictionary<string, string?>
        {
            ["Tenancy:Resolution:PlatformHosts:0"] = "localhost"
        });
        var administrator = await SeedPlatformAdminAsync(factory);
        var target = await SeedAdditionalPlatformUserAsync(factory);
        using var targetClient = factory.CreateClient();
        var targetTokens = await targetClient.LoginAsPlatformAsync(target.Email);
        using var adminClient = factory.CreateClient();
        adminClient.UseAccessToken(await adminClient.LoginAsPlatformAsync(administrator.Email));

        var reset = await adminClient.PostAsJsonAsync(
            $"/api/platform/staff/{target.UserId}/password-reset",
            new AdministrativePasswordResetDto
            {
                TemporaryPassword = "TemporaryPassword2026",
                Reason = "Platform staff recovery verification"
            });

        Assert.Equal(HttpStatusCode.NoContent, reset.StatusCode);
        targetClient.UseAccessToken(targetTokens);
        Assert.Equal(HttpStatusCode.Unauthorized, (await targetClient.GetAsync("/api/tenant/me")).StatusCode);
        targetClient.DefaultRequestHeaders.Authorization = null;
        var login = await targetClient.PostAsJsonAsync(
            "/api/tenant/auth/login/password",
            new PasswordLoginDto
            {
                Realm = AuthRealm.Platform,
                Identifier = target.Email,
                Password = "TemporaryPassword2026"
            });
        Assert.Equal(HttpStatusCode.OK, login.StatusCode);
        Assert.Contains("password_change_required", await login.Content.ReadAsStringAsync(), StringComparison.Ordinal);

        using var scope = factory.CreateSystemScope("Verify platform administrative password reset");
        var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
        Assert.True(await dbContext.Users.Where(item => item.Id == target.UserId)
            .Select(item => item.ForcePasswordChange).SingleAsync());
        Assert.True(await dbContext.AuditLogs.AnyAsync(item =>
            item.TenantId == null &&
            item.ActorUserId == administrator.UserId &&
            item.Action == "auth.password.reset_by_administrator" &&
            item.TargetId == target.UserId.ToString()));
    }

    [Fact]
    public async Task Platform_super_admin_can_load_every_platform_console_bootstrap_endpoint()
    {
        await using var factory = new ApiTestFactory(configurationOverrides: new Dictionary<string, string?>
        {
            ["Tenancy:Resolution:PlatformHosts:0"] = "localhost"
        });
        var platform = await SeedPlatformAdminAsync(factory);
        await factory.SeedAsync(new Tenant
        {
            Id = Guid.NewGuid(),
            Slug = "platform-console-content",
            Name = "Platform Console Content",
            Mode = TenantMode.PlatformOwned,
            Status = TenantStatus.Active,
            BillingStatus = BillingStatus.Active
        });

        using var client = factory.CreateClient();
        client.UseAccessToken(await client.LoginAsPlatformAsync(platform.Email));
        string[] endpoints =
        [
            "/api/platform/access/bootstrap",
            "/api/platform/overview",
            "/api/platform/domains?limit=200",
            "/api/platform/saas/catalog",
            "/api/platform/tenants?limit=200",
            "/api/platform/saas/subscriptions?limit=200",
            "/api/platform/saas/orders?limit=200",
            "/api/platform/saas/refunds?limit=200",
            "/api/platform/saas/invoices?limit=200",
            "/api/platform/saas/payments?limit=200",
            "/api/platform/saas/usage?limit=200",
            "/api/platform/saas/invoices/reminders?limit=100",
            "/api/platform/question-banks?status=all",
            "/api/platform/staff?limit=200",
            "/api/platform/saas/dunning/channels?limit=100",
            "/api/platform/saas/dunning/events?limit=100",
            "/api/platform/audit-logs?limit=200",
            "/api/platform/audit-alerts?limit=100",
            "/api/platform/tenant-capabilities/crm/configs?limit=200",
            "/api/platform/tenant-capabilities/crm/leads?limit=200",
            "/api/platform/tenant-capabilities/crm/logs?limit=200",
            "/api/platform/tenant-capabilities/sms/channels?limit=200",
            "/api/platform/tenant-capabilities/sms/templates?limit=200",
            "/api/platform/tenant-capabilities/sms/logs?limit=200",
            "/api/platform/payment-settings/apps?limit=200",
            "/api/platform/payment-settings/channels?limit=200",
            "/api/platform/payment-settings/rebates/summary",
            "/api/platform/tenant-capabilities/payments/apps?limit=200",
            "/api/platform/payment-settings/events?limit=100",
            "/api/platform/tenant-capabilities/payments/events?limit=100"
        ];

        var responses = await Task.WhenAll(endpoints.Select(async endpoint =>
        {
            using var response = await client.GetAsync(endpoint);
            return new
            {
                Endpoint = endpoint,
                response.StatusCode,
                Body = await response.Content.ReadAsStringAsync()
            };
        }));
        var failures = responses.Where(response => response.StatusCode != HttpStatusCode.OK).ToArray();

        Assert.True(
            failures.Length == 0,
            string.Join(Environment.NewLine, failures.Select(failure =>
                $"{failure.Endpoint}: {(int)failure.StatusCode} {failure.StatusCode} {failure.Body}")));
    }

    [Fact]
    public async Task Platform_admin_can_publish_immutable_saas_offering_and_manage_tenant_operations()
    {
        await using var factory = new ApiTestFactory(configurationOverrides: new Dictionary<string, string?>
        {
            ["Tenancy:Resolution:PlatformHosts:0"] = "localhost"
        });
        var platform = await SeedPlatformAdminAsync(factory);
        var tenantId = Guid.NewGuid();
        var domainId = Guid.NewGuid();
        await factory.SeedAsync(
            new Tenant
            {
                Id = tenantId,
                Slug = "tenant-six-a",
                Name = "Tenant Six A",
                Status = TenantStatus.Active,
                BillingStatus = BillingStatus.Trial
            },
            new TenantDomain
            {
                Id = domainId,
                TenantId = tenantId,
                Host = "six-a.example.test",
                Status = TenantDomainStatus.Active,
                IsPrimary = true,
                VerificationToken = "verify-six-a",
                VerifiedAt = DateTimeOffset.UtcNow,
                DnsVerifiedAt = DateTimeOffset.UtcNow,
                TlsReadyAt = DateTimeOffset.UtcNow
            });

        using var client = factory.CreateClient();
        client.UseAccessToken(await client.LoginAsPlatformAsync(platform.Email));

        var overview = await client.GetAsync("/api/platform/overview");
        var tenants = await client.GetAsync("/api/platform/tenants?search=six-a");
        var feature = await client.PutAsJsonAsync(
            "/api/platform/saas/features",
            new UpsertSaasFeatureDto(
                null,
                SaasFeatureCatalog.Exam,
                "考试",
                "learning",
                "租户考试模块",
                60_000,
                "CNY",
                SaasFeatureStatus.Active,
                30));
        var offering = await client.PutAsJsonAsync(
            "/api/platform/saas/offerings",
            new UpsertSaasOfferingDto(
                null,
                $"standard-{Guid.NewGuid():N}",
                "标准套餐",
                SaasOfferingType.BasePlan,
                SaasOfferingStatus.Draft,
                "集成测试套餐",
                10));
        Assert.Equal(HttpStatusCode.OK, feature.StatusCode);
        Assert.Equal(HttpStatusCode.OK, offering.StatusCode);
        var offeringJson = await JsonDocument.ParseAsync(await offering.Content.ReadAsStreamAsync());
        var offeringId = offeringJson.RootElement.GetProperty("id").GetGuid();
        var versionRequest = new UpsertSaasOfferingVersionDto(
            null,
            offeringId,
            PlatformBillingCycle.Yearly,
            72_000,
            60_000,
            "CNY",
            null,
            [SaasFeatureCatalog.Exam],
            new Dictionary<string, long>(),
            JsonDefaults.Object());
        var version = await client.PutAsJsonAsync("/api/platform/saas/offering-versions", versionRequest);
        Assert.Equal(HttpStatusCode.OK, version.StatusCode);
        var versionJson = await JsonDocument.ParseAsync(await version.Content.ReadAsStreamAsync());
        var versionId = versionJson.RootElement.GetProperty("id").GetGuid();
        var published = await client.PostAsync($"/api/platform/saas/offering-versions/{versionId}/publish", null);
        var immutableUpdate = await client.PutAsJsonAsync(
            "/api/platform/saas/offering-versions",
            versionRequest with { Id = versionId, AmountCents = 50_000 });
        var catalog = await client.GetAsync("/api/platform/saas/catalog");
        var recheck = await client.PostAsync($"/api/platform/domains/{domainId}/recheck", null);
        using var suspendRequest = new HttpRequestMessage(HttpMethod.Patch, "/api/platform/tenants/status")
        {
            Content = JsonContent.Create(new UpdatePlatformTenantStatusDto
            {
                TenantId = tenantId,
                Status = TenantStatus.Suspended,
                Reason = "integration test suspension"
            })
        };
        suspendRequest.Headers.Add("Idempotency-Key", $"suspend-{tenantId:N}");
        var suspended = await client.SendAsync(suspendRequest);

        using var runtimeRequest = new HttpRequestMessage(HttpMethod.Get, "/api/public/runtime/bootstrap");
        runtimeRequest.Headers.Host = "six-a.example.test";
        var runtimeAfterSuspend = await client.SendAsync(runtimeRequest);

        Assert.Equal(HttpStatusCode.OK, overview.StatusCode);
        Assert.Equal(HttpStatusCode.OK, tenants.StatusCode);
        Assert.Equal(HttpStatusCode.OK, published.StatusCode);
        Assert.Equal(HttpStatusCode.Conflict, immutableUpdate.StatusCode);
        Assert.Equal(HttpStatusCode.OK, catalog.StatusCode);
        Assert.Equal(HttpStatusCode.OK, recheck.StatusCode);
        Assert.Equal(HttpStatusCode.OK, suspended.StatusCode);
        Assert.Equal(HttpStatusCode.NotFound, runtimeAfterSuspend.StatusCode);

        using var scope = factory.CreateSystemScope("Verify platform admin side effects");
        var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
        Assert.True(await dbContext.BackgroundJobs.AnyAsync(job =>
            job.TenantId == tenantId &&
            job.JobType == "tenant_domain_recheck"));
        Assert.True(await dbContext.AuditLogs.AnyAsync(log =>
            log.ActorUserId == platform.UserId &&
            log.Action == "platform.tenant.status_changed"));
        Assert.True(await dbContext.AuditLogs.AnyAsync(log =>
            log.ActorUserId == platform.UserId &&
            log.Action == "platform.saas.feature.upserted"));
        Assert.True(await dbContext.AuditLogs.AnyAsync(log =>
            log.ActorUserId == platform.UserId &&
            log.Action == "platform.saas.offering_version.published"));
        Assert.Equal(
            60_000,
            await dbContext.SaasOfferingVersions
                .Where(item => item.Id == versionId)
                .Select(item => item.AmountCents)
                .SingleAsync());
    }

    [Fact]
    public async Task Tenant_token_cannot_access_platform_admin_endpoints()
    {
        await using var factory = new ApiTestFactory();
        var tenantId = Guid.NewGuid();
        var userId = Guid.NewGuid();
        var phone = "13866660000";
        await factory.SeedAsync(
            new Tenant
            {
                Id = tenantId,
                Slug = tenantId.ToString("N"),
                Name = "Tenant Realm"
            },
            new User
            {
                Id = userId,
                Phone = phone,
                Name = "Tenant Admin"
            }.WithTestPassword(),
            new TenantMembership
            {
                TenantId = tenantId,
                UserId = userId,
                Role = TenantRole.TenantAdmin,
                Status = MembershipStatus.Active
            });
        using var client = factory.CreateClient();
        client.UseAccessToken(await client.LoginAsTenantAsync(tenantId, phone));

        var response = await client.GetAsync("/api/platform/saas/catalog");

        Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
    }

    [Fact]
    public async Task Platform_admin_can_manage_crm_sms_and_payment_settings()
    {
        await using var factory = new ApiTestFactory(configurationOverrides: new Dictionary<string, string?>
        {
            ["Tenancy:Resolution:PlatformHosts:0"] = "localhost"
        });
        var platform = await SeedPlatformAdminAsync(factory);
        var tenantA = Guid.NewGuid();
        var tenantB = Guid.NewGuid();
        var failedQueueId = Guid.NewGuid();
        var otherQueueId = Guid.NewGuid();
        await factory.SeedAsync(
            new Tenant
            {
                Id = tenantA, Slug = "capability-a", Name = "Capability A", Status = TenantStatus.Active,
                BillingStatus = BillingStatus.Active
            },
            new Tenant
            {
                Id = tenantB, Slug = "capability-b", Name = "Capability B", Status = TenantStatus.Active,
                BillingStatus = BillingStatus.Active
            },
            new CrmWebhookQueueItem
            {
                Id = failedQueueId, TenantId = tenantA, RecordId = "lead-a", Source = "tenant.student.crm_push",
                Status = CrmWebhookQueueStatus.Failed, Attempts = 2, IdempotencyKey = "platform-capability-lead-a",
                LastError = "timeout"
            },
            new CrmWebhookQueueItem
            {
                Id = otherQueueId, TenantId = tenantB, RecordId = "lead-b", Source = "tenant.student.crm_push",
                Status = CrmWebhookQueueStatus.Failed, Attempts = 1, IdempotencyKey = "platform-capability-lead-b",
                LastError = "still failed"
            });
        using var client = factory.CreateClient();
        client.UseAccessToken(await client.LoginAsPlatformAsync(platform.Email));

        var crm = await client.PutAsJsonAsync("/api/platform/tenant-capabilities/crm/configs",
            new UpsertPlatformCrmConfigDto
            {
                TenantId = tenantA,
                Enabled = true,
                Url = "https://crm.example.test/webhook",
                SecretRef = "tenant_secret:crm:redacted",
                AssignmentMode = "round_robin",
                AssignmentPool = JsonSerializer.SerializeToElement(new[] { "sales-a" }),
                AssignmentConfig = JsonSerializer.SerializeToElement(new { retry = 3 })
            });
        var retry = await client.PostAsJsonAsync("/api/platform/tenant-capabilities/crm/leads/retry",
            new RetryPlatformCrmLeadDto { QueueId = failedQueueId, Note = "retry" });
        var smsChannel = await client.PutAsJsonAsync("/api/platform/tenant-capabilities/sms/channels",
            new UpsertPlatformSmsChannelDto
            {
                TenantId = tenantA,
                Provider = "aliyun",
                Name = "阿里云短信",
                Signature = "题库测试",
                Scene = "login",
                Status = TenantExternalProviderStatus.Active,
                SecretRef = "tenant_secret:sms:aliyun:redacted",
                MonthlyQuota = 1000
            });
        var smsJson = JsonDocument.Parse(await smsChannel.Content.ReadAsStringAsync());
        var channelId = smsJson.RootElement.GetProperty("id").GetGuid();
        var template = await client.PutAsJsonAsync("/api/platform/tenant-capabilities/sms/templates",
            new UpsertPlatformSmsTemplateDto
            {
                TenantId = tenantA,
                ChannelId = channelId,
                Code = "login_code",
                Name = "登录验证码",
                Type = SmsTemplateType.VerificationCode,
                AuditStatus = SmsTemplateAuditStatus.Draft,
                Status = SmsTemplateStatus.Active,
                Content = "验证码 ${code}"
            });
        var paymentApp = await client.PutAsJsonAsync("/api/platform/payment-settings/apps",
            new UpsertPlatformPaymentAppDto
            {
                AppCode = "platform_collect_test",
                AppName = "平台收款测试",
                Status = PlatformPaymentAppStatus.Active,
                SettlementMode = "PlatformCollect"
            });
        var tenantPayment = await client.PutAsJsonAsync("/api/platform/tenant-capabilities/payments/apps",
            new UpsertPlatformTenantPaymentAppDto
            {
                TenantId = tenantA,
                Provider = "manual",
                Status = TenantExternalProviderStatus.Active,
                DisplayName = "线下收款",
                SecretRef = "tenant_payment:manual:redacted"
            });
        var reads = await Task.WhenAll(
            client.GetAsync("/api/platform/tenant-capabilities/crm/configs"),
            client.GetAsync("/api/platform/tenant-capabilities/crm/leads"),
            client.GetAsync("/api/platform/tenant-capabilities/sms/channels"),
            client.GetAsync("/api/platform/tenant-capabilities/sms/templates"),
            client.GetAsync("/api/platform/payment-settings/apps"),
            client.GetAsync("/api/platform/tenant-capabilities/payments/apps"));

        Assert.Equal(HttpStatusCode.OK, crm.StatusCode);
        Assert.Equal(HttpStatusCode.OK, retry.StatusCode);
        Assert.Equal(HttpStatusCode.OK, smsChannel.StatusCode);
        Assert.Equal(HttpStatusCode.OK, template.StatusCode);
        Assert.Equal(HttpStatusCode.OK, paymentApp.StatusCode);
        Assert.Equal(HttpStatusCode.OK, tenantPayment.StatusCode);
        Assert.All(reads, response => Assert.Equal(HttpStatusCode.OK, response.StatusCode));

        using var scope = factory.CreateSystemScope("Verify platform capability endpoints");
        var db = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
        Assert.Equal(CrmWebhookQueueStatus.Retrying,
            await db.CrmWebhookQueue.Where(item => item.Id == failedQueueId).Select(item => item.Status).SingleAsync());
        Assert.Equal(CrmWebhookQueueStatus.Failed,
            await db.CrmWebhookQueue.Where(item => item.Id == otherQueueId).Select(item => item.Status).SingleAsync());
        Assert.True(await db.AuditLogs.AnyAsync(item =>
            item.ActorUserId == platform.UserId && item.Action == "platform.crm.lead.retry"));
        Assert.True(await db.AuditLogs.AnyAsync(item =>
            item.ActorUserId == platform.UserId && item.Action == "platform.sms.channel.upserted"));
        Assert.True(await db.AuditLogs.AnyAsync(item =>
            item.ActorUserId == platform.UserId && item.Action == "platform.payment.app.upserted"));
    }

    [Fact]
    public async Task Platform_capability_read_permissions_cannot_write()
    {
        await using var factory = new ApiTestFactory(configurationOverrides: new Dictionary<string, string?>
        {
            ["Tenancy:Resolution:PlatformHosts:0"] = "localhost"
        });
        var platform = await SeedPlatformUserAsync(factory,
        [
            BackendPermissions.PlatformCrmRead,
            BackendPermissions.PlatformSmsRead,
            BackendPermissions.PlatformPaymentRead
        ]);
        var tenantId = Guid.NewGuid();
        await factory.SeedAsync(
            new Tenant { Id = tenantId, Slug = "readonly-capability", Name = "Readonly Capability" });
        using var client = factory.CreateClient();
        client.UseAccessToken(await client.LoginAsPlatformAsync(platform.Email));

        var read = await client.GetAsync("/api/platform/tenant-capabilities/sms/channels");
        var write = await client.PutAsJsonAsync("/api/platform/tenant-capabilities/sms/channels",
            new UpsertPlatformSmsChannelDto
            {
                TenantId = tenantId,
                Provider = "aliyun",
                Name = "只读不应写入",
                Signature = "题库",
                Scene = "login",
                Status = TenantExternalProviderStatus.Active
            });

        Assert.Equal(HttpStatusCode.OK, read.StatusCode);
        Assert.Equal(HttpStatusCode.Forbidden, write.StatusCode);
    }

    [Fact]
    public async Task Platform_admin_can_manage_dunning_channels_and_retry_events()
    {
        await using var factory = new ApiTestFactory(configurationOverrides: new Dictionary<string, string?>
        {
            ["Tenancy:Resolution:PlatformHosts:0"] = "localhost"
        });
        var platform = await SeedPlatformAdminAsync(factory);
        var tenantId = Guid.NewGuid();
        var invoiceId = Guid.NewGuid();
        var reminderId = Guid.NewGuid();
        var ignoredReminderId = Guid.NewGuid();
        var eventId = Guid.NewGuid();
        var ignoredEventId = Guid.NewGuid();
        await factory.SeedAsync(
            new Tenant
            {
                Id = tenantId,
                Slug = "tenant-dunning-a",
                Name = "Tenant Dunning A",
                Status = TenantStatus.Active,
                BillingStatus = BillingStatus.PastDue
            },
            new PlatformBillingInvoice
            {
                Id = invoiceId,
                TenantId = tenantId,
                InvoiceNo = "INV-DUNNING-1",
                Status = PlatformBillingInvoiceStatus.Overdue,
                TotalAmountCents = 10_000
            },
            new PlatformBillingInvoiceReminder
            {
                Id = reminderId,
                TenantId = tenantId,
                InvoiceId = invoiceId,
                ReminderType = PlatformBillingInvoiceReminderType.Overdue,
                Channel = PlatformBillingInvoiceReminderChannel.Wechat,
                Status = PlatformBillingInvoiceReminderStatus.Failed,
                ReminderDate = DateOnly.FromDateTime(DateTime.UtcNow.Date),
                BalanceCentsSnapshot = 10_000
            },
            new PlatformBillingInvoiceReminder
            {
                Id = ignoredReminderId,
                TenantId = tenantId,
                InvoiceId = invoiceId,
                ReminderType = PlatformBillingInvoiceReminderType.FinalNotice,
                Channel = PlatformBillingInvoiceReminderChannel.Wechat,
                Status = PlatformBillingInvoiceReminderStatus.Failed,
                ReminderDate = DateOnly.FromDateTime(DateTime.UtcNow.Date),
                BalanceCentsSnapshot = 10_000
            });
        using var client = factory.CreateClient();
        client.UseAccessToken(await client.LoginAsPlatformAsync(platform.Email));

        var upsertResponse = await client.PutAsJsonAsync(
            "/api/platform/saas/dunning/channels",
            new UpsertPlatformBillingDunningChannelDto
            {
                ChannelCode = "wecom-overdue",
                Name = "企业微信逾期提醒",
                Provider = PlatformBillingDunningProvider.Wecom,
                WebhookUrl = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=secret-key",
                SecretRef = "platform_secrets:dunning:wecom:default",
                ReminderTypes = ["overdue"],
                ReminderChannels = ["wechat"],
                MinReminderLevel = 2,
                TenantIds = [tenantId]
            });
        var upsertBody = await upsertResponse.Content.ReadAsStringAsync();
        var channelJson = JsonDocument.Parse(upsertBody);
        var channelId = channelJson.RootElement.GetProperty("id").GetGuid();
        await factory.SeedAsync(
            new PlatformBillingDunningNotificationEvent
            {
                Id = eventId,
                TenantId = tenantId,
                ChannelId = channelId,
                ReminderId = ignoredReminderId,
                InvoiceId = invoiceId,
                Provider = PlatformBillingDunningProvider.Wecom,
                Status = PlatformBillingDunningNotificationStatus.Failed,
                Attempts = 2,
                LastError = "timeout",
                LastHttpCode = 500,
                LastResponseSummary = "server error",
                RequestPayload = JsonSerializer.SerializeToElement(new { phone = "13800001111", amount = 10000 })
            },
            new PlatformBillingDunningNotificationEvent
            {
                Id = ignoredEventId,
                TenantId = tenantId,
                ChannelId = channelId,
                ReminderId = reminderId,
                InvoiceId = invoiceId,
                Provider = PlatformBillingDunningProvider.Wecom,
                Status = PlatformBillingDunningNotificationStatus.Failed,
                Attempts = 5,
                LastError = "permanent failure",
                RequestPayload = JsonSerializer.SerializeToElement(new { phone = "13800001111", amount = 10000 })
            });

        var channelsResponse = await client.GetAsync("/api/platform/saas/dunning/channels?search=wecom");
        var eventsResponse = await client.GetAsync("/api/platform/saas/dunning/events?status=failed");
        var detailResponse = await client.GetAsync($"/api/platform/saas/dunning/events/detail?eventId={eventId}");
        var retryResponse = await client.PostAsJsonAsync(
            "/api/platform/saas/dunning/events/retry",
            new RetryPlatformBillingDunningEventDto
            {
                EventId = eventId,
                Reason = "manual retry"
            });
        var acknowledgeResponse = await client.PostAsJsonAsync(
            "/api/platform/saas/dunning/events/acknowledge",
            new ResolvePlatformBillingDunningEventDto { EventId = eventId, Reason = "delivery confirmed manually" });
        var ignoreResponse = await client.PostAsJsonAsync(
            "/api/platform/saas/dunning/events/ignore",
            new ResolvePlatformBillingDunningEventDto
                { EventId = ignoredEventId, Reason = "tenant requested no further delivery" });
        var disableResponse = await client.PostAsJsonAsync(
            "/api/platform/saas/dunning/channels/disable",
            new DisablePlatformBillingDunningChannelDto
            {
                ChannelId = channelId,
                Reason = "disable test"
            });

        Assert.Equal(HttpStatusCode.OK, upsertResponse.StatusCode);
        Assert.DoesNotContain("secret-key", upsertBody, StringComparison.OrdinalIgnoreCase);
        Assert.Contains("https://qyapi.weixin.qq.com/****", upsertBody, StringComparison.OrdinalIgnoreCase);
        Assert.Equal(HttpStatusCode.OK, channelsResponse.StatusCode);
        Assert.Equal(HttpStatusCode.OK, eventsResponse.StatusCode);
        Assert.Equal(HttpStatusCode.OK, detailResponse.StatusCode);
        Assert.Equal(HttpStatusCode.OK, retryResponse.StatusCode);
        Assert.Equal(HttpStatusCode.OK, acknowledgeResponse.StatusCode);
        Assert.Equal(HttpStatusCode.OK, ignoreResponse.StatusCode);
        Assert.Equal(HttpStatusCode.OK, disableResponse.StatusCode);

        using var scope = factory.CreateSystemScope("Verify platform dunning side effects");
        var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
        var storedEvent = await dbContext.PlatformBillingDunningNotificationEvents.AsNoTracking()
            .SingleAsync(item => item.Id == eventId);
        var storedChannel = await dbContext.PlatformBillingDunningNotificationChannels.AsNoTracking()
            .SingleAsync(item => item.Id == channelId);
        Assert.Equal(PlatformBillingDunningNotificationStatus.Acknowledged, storedEvent.Status);
        Assert.Null(storedEvent.LastError);
        Assert.Equal(PlatformBillingDunningNotificationStatus.Ignored, await dbContext
            .PlatformBillingDunningNotificationEvents.AsNoTracking()
            .Where(item => item.Id == ignoredEventId)
            .Select(item => item.Status)
            .SingleAsync());
        Assert.False(storedChannel.Enabled);
        Assert.True(await dbContext.AuditLogs.AnyAsync(log =>
            log.ActorUserId == platform.UserId &&
            log.Action == "platform.billing_dunning_event.retry_requested"));
        Assert.True(await dbContext.AuditLogs.AnyAsync(log =>
            log.ActorUserId == platform.UserId &&
            log.Action == "platform.billing_dunning_channel.disabled"));
        Assert.True(await dbContext.AuditLogs.AnyAsync(log =>
            log.ActorUserId == platform.UserId &&
            log.Action == "platform.billing_dunning_event.acknowledged"));
        Assert.True(await dbContext.AuditLogs.AnyAsync(log =>
            log.ActorUserId == platform.UserId &&
            log.Action == "platform.billing_dunning_event.ignored"));
    }

    [Fact]
    public async Task Tenant_provisioning_and_domain_bound_owner_activation_are_transactional_and_single_use()
    {
        await using var factory = new ApiTestFactory(configurationOverrides: new Dictionary<string, string?>
        {
            ["Tenancy:Resolution:PlatformHosts:0"] = "localhost",
            ["TenantProvisioning:DefaultBaseOfferingCode"] = "starter",
            ["TenantProvisioning:DefaultTrialDays"] = "17"
        });
        var platform = await SeedPlatformAdminAsync(factory);
        await factory.SeedBuiltinBackofficeCatalogAsync();
        var offering = new SaasOffering
        {
            Code = "starter",
            Name = "Starter",
            Type = SaasOfferingType.BasePlan,
            Status = SaasOfferingStatus.Active
        };
        var offeringVersion = new SaasOfferingVersion
        {
            OfferingId = offering.Id,
            Version = 1,
            Status = SaasOfferingVersionStatus.Published,
            OriginalAmountCents = 100,
            AmountCents = 100,
            EffectiveAt = DateTimeOffset.UtcNow.AddDays(-1),
            PublishedAt = DateTimeOffset.UtcNow.AddDays(-1)
        };
        await factory.SeedAsync(
            offering,
            offeringVersion,
            new SaasOfferingVersionFeature
            {
                OfferingVersionId = offeringVersion.Id,
                FeatureCode = SaasFeatureCatalog.CoreBackoffice
            });
        using var client = factory.CreateClient();
        client.UseAccessToken(await client.LoginAsPlatformAsync(platform.Email));
        var key = $"tenant-create-{Guid.NewGuid():N}";
        client.DefaultRequestHeaders.Add("Idempotency-Key", key);
        var slug = $"activation-{Guid.NewGuid():N}";
        var primaryHost = $"{slug}.example.test";
        var phone = $"137{Random.Shared.Next(10_000_000, 99_999_999)}";
        var request = new CreatePlatformTenantDto
        {
            Slug = slug,
            Name = "Activation Tenant",
            PrimaryDomainHost = primaryHost,
            OwnerPhone = phone,
            OwnerName = "Activation Owner",
            DefaultPaymentProvider = "manual",
            CollectionMode = TenantBillingCollectionMode.Manual,
            RenewalLeadDays = 21
        };

        var responses = await Task.WhenAll(
            client.PostAsJsonAsync("/api/platform/tenants", request),
            client.PostAsJsonAsync("/api/platform/tenants", request));
        var failedProvisioning = await Task.WhenAll(responses
            .Where(response => response.StatusCode != HttpStatusCode.OK)
            .Select(async response => $"{(int)response.StatusCode}: {await response.Content.ReadAsStringAsync()}"));
        Assert.True(failedProvisioning.Length == 0, string.Join(Environment.NewLine, failedProvisioning));
        var payloads = await Task.WhenAll(responses.Select(async response =>
            await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync())));
        var tenantIds = payloads
            .Select(payload => payload.RootElement.GetProperty("tenant").GetProperty("id").GetGuid()).Distinct()
            .ToArray();
        Assert.Single(tenantIds);
        Assert.Single(payloads, payload => payload.RootElement.GetProperty("isReplay").GetBoolean());
        Assert.All(payloads, payload =>
        {
            Assert.Equal(primaryHost, payload.RootElement.GetProperty("primaryDomain").GetProperty("host").GetString());
            Assert.Equal("Pending", payload.RootElement.GetProperty("primaryDomain").GetProperty("status").GetString());
            Assert.Equal("domain_pending",
                payload.RootElement.GetProperty("ownerActivation").GetProperty("status").GetString());
            Assert.False(payload.RootElement.TryGetProperty("activationToken", out _));
        });

        var conflictingRequest = new CreatePlatformTenantDto
        {
            Slug = slug,
            Name = "Different Tenant Name",
            PrimaryDomainHost = primaryHost,
            OwnerPhone = phone,
            OwnerName = request.OwnerName,
            DefaultPaymentProvider = request.DefaultPaymentProvider,
            CollectionMode = request.CollectionMode,
            RenewalLeadDays = request.RenewalLeadDays
        };
        var conflict = await client.PostAsJsonAsync("/api/platform/tenants", conflictingRequest);
        Assert.Equal(HttpStatusCode.Conflict, conflict.StatusCode);
        Assert.Equal("idempotency_conflict", await ReadProblemCodeAsync(conflict));

        var tenantId = tenantIds[0];
        using (var pendingIssueRequest = new HttpRequestMessage(
                   HttpMethod.Post, $"/api/platform/tenants/{tenantId}/owner-activation-links")
               {
                   Content = JsonContent.Create(new IssuePlatformOwnerActivationLinkDto
                   {
                       Reason = "Domain is not ready yet"
                   })
               })
        {
            pendingIssueRequest.Headers.Add("Idempotency-Key", $"pending-{Guid.NewGuid():N}");
            var pendingIssue = await client.SendAsync(pendingIssueRequest);
            Assert.Equal(HttpStatusCode.BadRequest, pendingIssue.StatusCode);
            Assert.Equal("primary_domain_not_active", await ReadProblemCodeAsync(pendingIssue));
        }

        Guid domainId;
        using (var activationScope = factory.CreateSystemScope("Activate provisioned primary domain"))
        {
            var activationDb = activationScope.ServiceProvider.GetRequiredService<TikuDbContext>();
            var domain =
                await activationDb.TenantDomains.SingleAsync(value => value.TenantId == tenantId && value.IsPrimary);
            domainId = domain.Id;
            domain.Status = TenantDomainStatus.Active;
            domain.DnsVerifiedAt = DateTimeOffset.UtcNow;
            domain.TlsReadyAt = DateTimeOffset.UtcNow;
            domain.VerifiedAt = DateTimeOffset.UtcNow;
            await activationDb.SaveChangesAsync();
        }

        using var browserClient =
            factory.CreateClient(new WebApplicationFactoryClientOptions { HandleCookies = false });
        using (var setupRuntimeRequest = new HttpRequestMessage(
                   HttpMethod.Get, $"https://{primaryHost}/api/public/runtime/bootstrap"))
        {
            var setupRuntime = await browserClient.SendAsync(setupRuntimeRequest);
            using var setupPayload = await JsonDocument.ParseAsync(await setupRuntime.Content.ReadAsStreamAsync());
            Assert.Equal(HttpStatusCode.OK, setupRuntime.StatusCode);
            Assert.Equal("setup_required", setupPayload.RootElement.GetProperty("siteState").GetString());
        }

        var issueKeys = new[] { $"issue-{Guid.NewGuid():N}", $"issue-{Guid.NewGuid():N}" };
        var issueRequests = issueKeys.Select(issueKey =>
        {
            var issueRequest = new HttpRequestMessage(
                HttpMethod.Post, $"/api/platform/tenants/{tenantId}/owner-activation-links")
            {
                Content = JsonContent.Create(new IssuePlatformOwnerActivationLinkDto
                {
                    Reason = "Secure tenant handoff"
                })
            };
            issueRequest.Headers.Add("Idempotency-Key", issueKey);
            return issueRequest;
        }).ToArray();
        var issueResponses = await Task.WhenAll(issueRequests.Select(client.SendAsync));
        var issuedIndex = Array.FindIndex(issueResponses, response => response.StatusCode == HttpStatusCode.OK);
        Assert.True(issuedIndex >= 0);
        var issued = issueResponses[issuedIndex];
        var concurrentConflict = issueResponses.Single(response => response.StatusCode != HttpStatusCode.OK);
        Assert.Equal(HttpStatusCode.Conflict, concurrentConflict.StatusCode);
        Assert.Equal("owner_activation_already_issued", await ReadProblemCodeAsync(concurrentConflict));
        Assert.Equal(HttpStatusCode.OK, issued.StatusCode);
        using var issuedPayload = await JsonDocument.ParseAsync(await issued.Content.ReadAsStreamAsync());
        var firstActivationId = issuedPayload.RootElement.GetProperty("activationId").GetGuid();
        var activationUrl = issuedPayload.RootElement.GetProperty("activationUrl").GetString()!;
        Assert.StartsWith($"https://{primaryHost}/activate/{firstActivationId}#token=", activationUrl,
            StringComparison.Ordinal);
        var firstActivationToken = new Uri(activationUrl).Fragment["#token=".Length..];

        using var replayIssueRequest = new HttpRequestMessage(
            HttpMethod.Post, $"/api/platform/tenants/{tenantId}/owner-activation-links")
        {
            Content = JsonContent.Create(new IssuePlatformOwnerActivationLinkDto { Reason = "Secure tenant handoff" })
        };
        replayIssueRequest.Headers.Add("Idempotency-Key", issueKeys[issuedIndex]);
        var issueReplay = await client.SendAsync(replayIssueRequest);
        using var replayPayload = await JsonDocument.ParseAsync(await issueReplay.Content.ReadAsStreamAsync());
        Assert.Equal(HttpStatusCode.OK, issueReplay.StatusCode);
        Assert.True(replayPayload.RootElement.GetProperty("isReplay").GetBoolean());
        Assert.Equal(JsonValueKind.Null, replayPayload.RootElement.GetProperty("activationUrl").ValueKind);

        using var replaceIssueRequest = new HttpRequestMessage(
            HttpMethod.Post, $"/api/platform/tenants/{tenantId}/owner-activation-links")
        {
            Content = JsonContent.Create(new IssuePlatformOwnerActivationLinkDto
            {
                Reason = "Owner requested a replacement link",
                ReplaceExisting = true
            })
        };
        replaceIssueRequest.Headers.Add("Idempotency-Key", $"replace-{Guid.NewGuid():N}");
        var replacement = await client.SendAsync(replaceIssueRequest);
        using var replacementPayload = await JsonDocument.ParseAsync(await replacement.Content.ReadAsStreamAsync());
        Assert.Equal(HttpStatusCode.OK, replacement.StatusCode);
        var activationId = replacementPayload.RootElement.GetProperty("activationId").GetGuid();
        var replacementUrl = replacementPayload.RootElement.GetProperty("activationUrl").GetString()!;
        var activationToken = new Uri(replacementUrl).Fragment["#token=".Length..];

        using (var revokedRequest = new HttpRequestMessage(
                   HttpMethod.Post, $"https://{primaryHost}/api/tenant/auth/browser/activation/complete")
               {
                   Content = JsonContent.Create(new CompleteOwnerActivationDto
                   {
                       ActivationId = firstActivationId,
                       Token = firstActivationToken,
                       NewPassword = "ActivatedOwner2026"
                   })
               })
        {
            revokedRequest.Headers.Add("Origin", $"https://{primaryHost}");
            var revoked = await browserClient.SendAsync(revokedRequest);
            Assert.Equal(HttpStatusCode.BadRequest, revoked.StatusCode);
            Assert.Equal("owner_activation_invalid", await ReadProblemCodeAsync(revoked));
        }

        using var invalidPasswordRequest = new HttpRequestMessage(
            HttpMethod.Post, $"https://{primaryHost}/api/tenant/auth/browser/activation/complete")
        {
            Content = JsonContent.Create(new CompleteOwnerActivationDto
            {
                ActivationId = activationId,
                Token = activationToken,
                NewPassword = "weak"
            })
        };
        invalidPasswordRequest.Headers.Add("Origin", $"https://{primaryHost}");
        var invalidPassword = await browserClient.SendAsync(invalidPasswordRequest);
        Assert.Equal(HttpStatusCode.BadRequest, invalidPassword.StatusCode);

        using var activationRequest = new HttpRequestMessage(
            HttpMethod.Post, $"https://{primaryHost}/api/tenant/auth/browser/activation/complete")
        {
            Content = JsonContent.Create(new CompleteOwnerActivationDto
            {
                ActivationId = activationId,
                Token = activationToken,
                NewPassword = "ActivatedOwner2026"
            })
        };
        activationRequest.Headers.Add("Origin", $"https://{primaryHost}");
        var activated = await browserClient.SendAsync(activationRequest);
        Assert.Equal(HttpStatusCode.OK, activated.StatusCode);
        var cookies = activated.Headers.GetValues("Set-Cookie").ToArray();
        var accessCookie = cookies.Single(value => value.StartsWith("__Host-tiku-at=", StringComparison.Ordinal));
        accessCookie = accessCookie[..accessCookie.IndexOf(';')];

        using (var readyRuntimeRequest = new HttpRequestMessage(
                   HttpMethod.Get, $"https://{primaryHost}/api/public/runtime/bootstrap"))
        {
            var readyRuntime = await browserClient.SendAsync(readyRuntimeRequest);
            using var readyPayload = await JsonDocument.ParseAsync(await readyRuntime.Content.ReadAsStreamAsync());
            Assert.Equal(HttpStatusCode.OK, readyRuntime.StatusCode);
            Assert.Equal("ready_to_launch", readyPayload.RootElement.GetProperty("siteState").GetString());
        }

        using var bootstrapRequest = new HttpRequestMessage(
            HttpMethod.Get, $"https://{primaryHost}/api/tenant/access/ui-bootstrap");
        bootstrapRequest.Headers.Add("Cookie", accessCookie);
        bootstrapRequest.Headers.Add("Origin", $"https://{primaryHost}");
        var bootstrap = await browserClient.SendAsync(bootstrapRequest);
        Assert.True(
            bootstrap.StatusCode == HttpStatusCode.OK,
            $"Expected UI bootstrap success, got {(int)bootstrap.StatusCode}: {await bootstrap.Content.ReadAsStringAsync()}");

        using var consumedRequest = new HttpRequestMessage(
            HttpMethod.Post, $"https://{primaryHost}/api/tenant/auth/browser/activation/complete")
        {
            Content = JsonContent.Create(new CompleteOwnerActivationDto
            {
                ActivationId = activationId,
                Token = activationToken,
                NewPassword = "ActivatedOwner2026"
            })
        };
        consumedRequest.Headers.Add("Origin", $"https://{primaryHost}");
        var consumed = await browserClient.SendAsync(consumedRequest);
        Assert.Equal(HttpStatusCode.Conflict, consumed.StatusCode);
        Assert.Equal("owner_activation_consumed", await ReadProblemCodeAsync(consumed));

        var policy = await client.GetAsync($"/api/platform/tenants/{tenantIds[0]}/billing-policy");
        Assert.Equal(HttpStatusCode.OK, policy.StatusCode);
        using var policyPayload = await JsonDocument.ParseAsync(await policy.Content.ReadAsStreamAsync());
        Assert.Equal(21, policyPayload.RootElement.GetProperty("renewalLeadDays").GetInt32());

        using var scope = factory.CreateSystemScope("Verify tenant provisioning and owner activation");
        var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
        var grant = await dbContext.TenantOwnerActivationGrants.AsNoTracking()
            .SingleAsync(value => value.Id == activationId);
        var revokedGrant = await dbContext.TenantOwnerActivationGrants.AsNoTracking()
            .SingleAsync(value => value.Id == firstActivationId);
        var subscription = await dbContext.TenantSaasSubscriptions.AsNoTracking()
            .SingleAsync(value => value.TenantId == tenantId);
        Assert.NotNull(grant.ConsumedAt);
        Assert.NotNull(revokedGrant.RevokedAt);
        Assert.Equal(platform.UserId, revokedGrant.RevokedBy);
        Assert.Equal(domainId, grant.DomainId);
        Assert.DoesNotContain(activationToken, grant.TokenHash, StringComparison.Ordinal);
        Assert.InRange(subscription.CurrentPeriodEnd - subscription.StartsAt, TimeSpan.FromDays(16.9),
            TimeSpan.FromDays(17.1));
        Assert.True(await dbContext.TenantFrontendConfigs.AnyAsync(value => value.TenantId == tenantId));
        Assert.True(await dbContext.AuditLogs.AnyAsync(value =>
            value.TenantId == tenantIds[0] && value.Action == "tenant.owner.activated"));
        foreach (var payload in payloads) payload.Dispose();
    }

    private static async Task<string?> ReadProblemCodeAsync(HttpResponseMessage response)
    {
        using var payload = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
        return payload.RootElement.TryGetProperty("code", out var code) ? code.GetString() : null;
    }

    private static async Task<(Guid UserId, string Email)> SeedPlatformAdminAsync(ApiTestFactory factory)
    {
        return await SeedPlatformUserAsync(factory, BackendPermissions.Platform);
    }

    private static async Task<(Guid UserId, string Email)> SeedAdditionalPlatformUserAsync(ApiTestFactory factory)
    {
        var userId = Guid.NewGuid();
        var roleId = Guid.NewGuid();
        var email = $"platform-target-{Guid.NewGuid():N}@example.test";
        await factory.SeedAsync(
            new User
            {
                Id = userId,
                Email = email,
                NormalizedEmail = email.ToUpperInvariant(),
                UserName = email,
                NormalizedUserName = email.ToUpperInvariant(),
                Name = "Platform Reset Target",
                PrimaryRole = "platform_staff",
                RawProfile = JsonDefaults.Object()
            }.WithTestPassword(),
            new PlatformBackendRole
            {
                Id = roleId,
                Code = $"platform_reset_target_{roleId:N}",
                Name = "Platform Reset Target",
                Status = BackendRoleStatus.Active
            },
            new PlatformBackendRolePermission
            {
                RoleId = roleId,
                PermissionCode = BackendPermissions.PlatformDashboardView
            },
            new PlatformBackendUserRole
            {
                UserId = userId,
                RoleId = roleId
            });
        return (userId, email);
    }

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