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.Security;
using Tiku.Application.Storage;
using Tiku.Domain.Common;
using Tiku.Domain.Content;
using Tiku.Domain.Identity;
using Tiku.Domain.Platform;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Auth;
using Tiku.Infrastructure.Persistence;

namespace Tiku.IntegrationTests.Api;

public sealed class AssetManagementEndpointTests
{
    [Fact]
    public async Task Tenant_content_upload_sign_requires_authentication()
    {
        await using var factory = new ApiTestFactory(objectStorageService: new FakeObjectStorageService());
        using var client = factory.CreateClient();

        using var response = await client.PostAsJsonAsync(
            "/api/tenant-content/assets/uploads/sign",
            new AssetUploadSignDto
            {
                FileName = "lesson.pdf",
                MimeType = "application/pdf"
            });

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

    [Fact]
    public async Task Tenant_admin_can_sign_upload_and_create_pending_asset()
    {
        await using var factory = new ApiTestFactory(objectStorageService: new FakeObjectStorageService());
        var seed = await SeedAdminAsync(factory);
        using var client = factory.CreateClient();
        await LoginAsync(client, seed);

        using var response = await client.PostAsJsonAsync(
            "/api/tenant-content/assets/uploads/sign",
            new AssetUploadSignDto
            {
                FileName = "lesson.pdf",
                MimeType = "application/pdf",
                FileSizeBytes = 1024,
                Title = "课程讲义",
                AssetType = "pdf",
                Visibility = "members"
            });
        var body = await ReadJsonAsync(response);

        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        Assert.Equal("PUT", body.RootElement.GetProperty("upload").GetProperty("method").GetString());
        var item = body.RootElement.GetProperty("item");
        var assetId = item.GetProperty("id").GetGuid();
        Assert.Equal("Pending", item.GetProperty("uploadStatus").GetString());
        Assert.Equal("AliyunOss", item.GetProperty("storageProvider").GetString());
        Assert.StartsWith($"{seed.TenantId:N}/assets/", item.GetProperty("objectKey").GetString(), StringComparison.Ordinal);

        using var listResponse = await client.GetAsync("/api/tenant-content/assets?uploadStatus=pending");
        var list = await ReadJsonAsync(listResponse);
        var listItem = Assert.Single(list.RootElement.GetProperty("items").EnumerateArray());
        Assert.Equal(assetId, listItem.GetProperty("id").GetGuid());

        using var scope = factory.CreateSystemScope();
        var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
        var asset = dbContext.ContentAssets.Single(asset => asset.Id == assetId);
        Assert.Equal(seed.TenantId, asset.TenantId);
        Assert.Equal(seed.UserId, asset.CreatedBy);
        Assert.Equal(AssetUploadStatus.Pending, asset.UploadStatus);
        Assert.Equal(AssetSecurityScanStatus.Pending, asset.SecurityScanStatus);
    }

    [Fact]
    public async Task Tenant_admin_can_confirm_upload_and_mark_asset_verified()
    {
        var storage = new FakeObjectStorageService
        {
            MetadataSizeBytes = 2048,
            MetadataChecksumSha256 = new string('a', 64)
        };
        await using var factory = new ApiTestFactory(objectStorageService: storage);
        var seed = await SeedAdminAsync(factory);
        var assetId = Guid.NewGuid();
        await factory.SeedAsync(new ContentAsset
        {
            Id = assetId,
            TenantId = seed.TenantId,
            Title = "课程讲义",
            FileName = "lesson.pdf",
            StorageProvider = AssetStorageProvider.AliyunOss,
            Bucket = "tenant-assets",
            ObjectKey = $"{seed.TenantId:N}/assets/lesson.pdf",
            MimeType = "application/pdf",
            AssetType = ContentAssetType.Pdf,
            UploadStatus = AssetUploadStatus.Pending,
            SecurityScanStatus = AssetSecurityScanStatus.Pending
        });
        using var client = factory.CreateClient();
        await LoginAsync(client, seed);

        using var response = await client.PostAsJsonAsync(
            "/api/tenant-content/assets/uploads/confirm",
            new AssetUploadConfirmDto
            {
                AssetId = assetId,
                MimeType = "application/pdf",
                FileSizeBytes = 2048,
                ChecksumSha256 = new string('a', 64)
            });
        var body = await ReadJsonAsync(response);

        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        Assert.Equal("Verified", body.RootElement.GetProperty("item").GetProperty("uploadStatus").GetString());
        Assert.Equal(2048, body.RootElement.GetProperty("metadata").GetProperty("sizeBytes").GetInt64());

        using var scope = factory.CreateSystemScope();
        var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
        var asset = dbContext.ContentAssets.Single(asset => asset.Id == assetId);
        Assert.Equal(AssetUploadStatus.Verified, asset.UploadStatus);
        Assert.Equal(seed.UserId, asset.VerifiedBy);
        Assert.Equal(2048, asset.VerifiedSizeBytes);
        Assert.Equal(new string('a', 64), asset.VerifiedChecksumSha256);
        Assert.False(dbContext.TenantFeatureUsages.Any(value =>
            value.TenantId == seed.TenantId && value.MetricCode == SaasQuotaMetricCatalog.StorageBytes));
    }

    [Fact]
    public async Task Verified_upload_consumes_storage_quota_and_archive_releases_it()
    {
        var storage = new FakeObjectStorageService { MetadataSizeBytes = 6 };
        await using var factory = new ApiTestFactory(objectStorageService: storage);
        var seed = await SeedAdminWithStorageQuotaAsync(factory, 10);
        var firstAssetId = Guid.NewGuid();
        var secondAssetId = Guid.NewGuid();
        await factory.SeedAsync(
            PendingAsset(firstAssetId, seed.TenantId, "first.pdf"),
            PendingAsset(secondAssetId, seed.TenantId, "second.pdf"));
        using var client = factory.CreateClient();
        await LoginAsync(client, seed);

        using var firstConfirm = await client.PostAsJsonAsync(
            "/api/tenant-content/assets/uploads/confirm",
            new AssetUploadConfirmDto { AssetId = firstAssetId });
        Assert.Equal(HttpStatusCode.OK, firstConfirm.StatusCode);
        Assert.Equal(6, await StorageUsageAsync(factory, seed.TenantId));

        storage.MetadataSizeBytes = 5;
        using var exhausted = await client.PostAsJsonAsync(
            "/api/tenant-content/assets/uploads/confirm",
            new AssetUploadConfirmDto { AssetId = secondAssetId });
        Assert.Equal(HttpStatusCode.Conflict, exhausted.StatusCode);
        var exhaustedBody = await ReadJsonAsync(exhausted);
        Assert.Equal("feature_quota_exhausted", exhaustedBody.RootElement.GetProperty("code").GetString());
        Assert.Equal(6, await StorageUsageAsync(factory, seed.TenantId));

        using var archive = await client.DeleteAsync($"/api/tenant-content/assets/{firstAssetId}");
        Assert.Equal(HttpStatusCode.OK, archive.StatusCode);
        Assert.Equal(0, await StorageUsageAsync(factory, seed.TenantId));

        using var secondConfirm = await client.PostAsJsonAsync(
            "/api/tenant-content/assets/uploads/confirm",
            new AssetUploadConfirmDto { AssetId = secondAssetId });
        Assert.Equal(HttpStatusCode.OK, secondConfirm.StatusCode);
        Assert.Equal(5, await StorageUsageAsync(factory, seed.TenantId));

        using var repeatedConfirm = await client.PostAsJsonAsync(
            "/api/tenant-content/assets/uploads/confirm",
            new AssetUploadConfirmDto { AssetId = secondAssetId });
        Assert.Equal(HttpStatusCode.OK, repeatedConfirm.StatusCode);
        Assert.Equal(5, await StorageUsageAsync(factory, seed.TenantId));
    }

    [Fact]
    public async Task Import_jobs_are_scoped_to_current_tenant_and_include_detail()
    {
        await using var factory = new ApiTestFactory(objectStorageService: new FakeObjectStorageService());
        var seed = await SeedAdminAsync(factory);
        var otherTenantId = Guid.NewGuid();
        var jobId = Guid.NewGuid();
        var itemId = Guid.NewGuid();
        await factory.SeedAsync(
            new Tenant
            {
                Id = otherTenantId,
                Slug = otherTenantId.ToString("N"),
                Name = "Other Tenant",
                Status = TenantStatus.Active,
                Metadata = JsonDefaults.Object()
            },
            new ContentImportJob
            {
                Id = jobId,
                TenantId = seed.TenantId,
                SourceName = "questions.xlsx",
                ImportType = ContentImportType.Questions,
                SourceFormat = ImportSourceFormat.Excel,
                Status = ContentImportStatus.CompletedWithErrors,
                TotalCount = 1,
                ErrorCount = 1
            },
            new ContentImportItem
            {
                Id = itemId,
                TenantId = seed.TenantId,
                JobId = jobId,
                RowNo = 1,
                Status = ContentImportItemStatus.Invalid,
                IssuesCount = 1
            },
            new ContentImportIssue
            {
                Id = Guid.NewGuid(),
                TenantId = seed.TenantId,
                JobId = jobId,
                ItemId = itemId,
                RowNo = 1,
                Severity = ImportIssueSeverity.Error,
                Code = "missing_answer",
                Message = "答案不能为空"
            },
            new ContentImportJob
            {
                Id = Guid.NewGuid(),
                TenantId = otherTenantId,
                SourceName = "other.xlsx"
            });
        using var client = factory.CreateClient();
        await LoginAsync(client, seed);

        using var listResponse = await client.GetAsync("/api/tenant-content/import-jobs?status=completedWithErrors");
        using var detailResponse = await client.GetAsync($"/api/tenant-content/import-jobs/{jobId}");
        var list = await ReadJsonAsync(listResponse);
        var detail = await ReadJsonAsync(detailResponse);

        Assert.Equal(HttpStatusCode.OK, listResponse.StatusCode);
        var listItem = Assert.Single(list.RootElement.GetProperty("items").EnumerateArray());
        Assert.Equal(jobId, listItem.GetProperty("id").GetGuid());
        Assert.Equal(HttpStatusCode.OK, detailResponse.StatusCode);
        Assert.Equal(jobId, detail.RootElement.GetProperty("job").GetProperty("id").GetGuid());
        Assert.Single(detail.RootElement.GetProperty("items").EnumerateArray());
        Assert.Single(detail.RootElement.GetProperty("issues").EnumerateArray());
    }

    [Fact]
    public async Task Tenant_admin_can_upsert_asset_sign_access_and_query_asset_events()
    {
        await using var factory = new ApiTestFactory(objectStorageService: new FakeObjectStorageService());
        var seed = await SeedAdminAsync(factory);
        using var client = factory.CreateClient();
        await LoginAsync(client, seed);

        var upsertResponse = await client.PutAsJsonAsync(
            "/api/tenant-content/assets",
            new UpsertAssetDto
            {
                Title = "管理侧资料",
                FileName = "admin.pdf",
                AssetType = "pdf",
                Visibility = "members",
                Provider = "local_dev",
                Bucket = "tenant-assets",
                ObjectKey = $"{seed.TenantId:N}/admin.pdf",
                PreviewObjectKey = $"{seed.TenantId:N}/admin-preview.pdf",
                MimeType = "application/pdf",
                FileSizeBytes = 512
            });
        var upsert = await ReadJsonAsync(upsertResponse);
        var assetId = upsert.RootElement.GetProperty("item").GetProperty("id").GetGuid();
        await factory.SeedAsync(new ContentAssetSecurityScanEvent
        {
            TenantId = seed.TenantId,
            AssetId = assetId,
            Provider = "local",
            ScanStatus = AssetSecurityScanStatus.Passed,
            RiskLevel = AssetSecurityRiskLevel.None
        });

        var downloadResponse = await client.PostAsJsonAsync(
            "/api/tenant-content/assets/sign-download",
            new AssetAccessSignDto { AssetId = assetId, ExpiresInSeconds = 120 });
        var previewResponse = await client.PostAsJsonAsync(
            "/api/tenant-content/assets/sign-preview",
            new AssetAccessSignDto { AssetId = assetId, ExpiresInSeconds = 120 });
        var accessEventsResponse = await client.GetAsync($"/api/tenant-content/assets/access-events?assetId={assetId}");
        var scanEventsResponse = await client.GetAsync($"/api/tenant-content/assets/security-scan-events?assetId={assetId}");
        var accessEvents = await ReadJsonAsync(accessEventsResponse);
        var scanEvents = await ReadJsonAsync(scanEventsResponse);

        Assert.Equal(HttpStatusCode.OK, upsertResponse.StatusCode);
        Assert.Equal(HttpStatusCode.OK, downloadResponse.StatusCode);
        Assert.Equal(HttpStatusCode.OK, previewResponse.StatusCode);
        Assert.Equal(2, accessEvents.RootElement.GetProperty("items").GetArrayLength());
        Assert.Single(scanEvents.RootElement.GetProperty("items").EnumerateArray());
    }

    private static async Task<(Guid TenantId, Guid UserId, string Phone)> SeedAdminAsync(ApiTestFactory factory)
    {
        var tenantId = Guid.NewGuid();
        var userId = Guid.NewGuid();
        var phone = "13900000000";
        await factory.SeedAsync(
            new Tenant
            {
                Id = tenantId,
                Slug = tenantId.ToString("N"),
                Name = "Test Tenant",
                Status = TenantStatus.Active,
                Metadata = JsonDefaults.Object()
            },
            new User
            {
                Id = userId,
                Phone = phone,
                Name = "Tenant Admin"
            }.WithTestPassword(),
            new TenantMembership
            {
                TenantId = tenantId,
                UserId = userId,
                Role = TenantRole.TenantAdmin,
                Status = MembershipStatus.Active
            });

        return (tenantId, userId, phone);
    }

    private static async Task<(Guid TenantId, Guid UserId, string Phone)> SeedAdminWithStorageQuotaAsync(
        ApiTestFactory factory,
        long storageLimit)
    {
        var tenantId = Guid.NewGuid();
        var userId = Guid.NewGuid();
        var phone = "13900000001";
        var offeringId = Guid.NewGuid();
        var versionId = Guid.NewGuid();
        var subscriptionId = Guid.NewGuid();
        var now = DateTimeOffset.UtcNow;
        await factory.SeedAsync(
            new Tenant
            {
                Id = tenantId,
                Slug = tenantId.ToString("N"),
                Name = "Storage Quota Tenant",
                Status = TenantStatus.Active,
                BillingStatus = BillingStatus.Active
            },
            new User
            {
                Id = userId,
                Phone = phone,
                Name = "Storage Quota Admin"
            }.WithTestPassword(),
            new TenantMembership
            {
                TenantId = tenantId,
                UserId = userId,
                Role = TenantRole.TenantAdmin,
                Status = MembershipStatus.Active
            },
            new SaasFeature
            {
                Code = SaasFeatureCatalog.PrivateQuestionBank,
                Name = "Private question bank",
                Category = "content",
                Status = SaasFeatureStatus.Active
            },
            new SaasOffering
            {
                Id = offeringId,
                Code = $"storage-quota-{tenantId:N}",
                Name = "Storage Quota Plan",
                Type = SaasOfferingType.BasePlan,
                Status = SaasOfferingStatus.Active
            },
            new SaasOfferingVersion
            {
                Id = versionId,
                OfferingId = offeringId,
                Version = 1,
                Status = SaasOfferingVersionStatus.Draft,
                BillingCycle = PlatformBillingCycle.Monthly,
                OriginalAmountCents = 100,
                AmountCents = 100,
                EffectiveAt = now.AddDays(-1)
            },
            new SaasOfferingVersionFeature
            {
                OfferingVersionId = versionId,
                FeatureCode = SaasFeatureCatalog.PrivateQuestionBank
            },
            new SaasFeatureLimitDefinition
            {
                MetricCode = SaasQuotaMetricCatalog.StorageBytes,
                FeatureCode = SaasFeatureCatalog.PrivateQuestionBank,
                Name = "Storage bytes",
                Unit = "byte",
                Kind = SaasFeatureLimitKind.Current,
                WarningPercent = 80,
                IsHardLimit = true
            },
            new SaasOfferingVersionLimit
            {
                OfferingVersionId = versionId,
                MetricCode = SaasQuotaMetricCatalog.StorageBytes,
                LimitValue = storageLimit
            },
            new TenantSaasSubscription
            {
                Id = subscriptionId,
                TenantId = tenantId,
                BaseOfferingVersionId = versionId,
                Status = TenantSaasSubscriptionStatus.Active,
                StartsAt = now.AddDays(-1),
                CurrentPeriodStart = now.AddDays(-1),
                CurrentPeriodEnd = now.AddMonths(1)
            },
            new TenantSaasSubscriptionItem
            {
                TenantId = tenantId,
                SubscriptionId = subscriptionId,
                OfferingVersionId = versionId,
                ItemType = TenantSaasSubscriptionItemType.BasePlan,
                Status = TenantSaasSubscriptionItemStatus.Active,
                StartsAt = now.AddDays(-1),
                EndsAt = now.AddMonths(1)
            });

        using var scope = factory.CreateSystemScope("Publish storage quota fixture");
        var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
        var version = await dbContext.SaasOfferingVersions.SingleAsync(value => value.Id == versionId);
        version.Status = SaasOfferingVersionStatus.Published;
        version.PublishedAt = now;
        await dbContext.SaveChangesAsync();
        return (tenantId, userId, phone);
    }

    private static ContentAsset PendingAsset(Guid id, Guid tenantId, string fileName) => new()
    {
        Id = id,
        TenantId = tenantId,
        Title = fileName,
        FileName = fileName,
        StorageProvider = AssetStorageProvider.AliyunOss,
        Bucket = "tenant-assets",
        ObjectKey = $"{tenantId:N}/assets/{fileName}",
        MimeType = "application/pdf",
        AssetType = ContentAssetType.Pdf,
        UploadStatus = AssetUploadStatus.Pending,
        SecurityScanStatus = AssetSecurityScanStatus.Pending
    };

    private static async Task<long> StorageUsageAsync(ApiTestFactory factory, Guid tenantId)
    {
        using var scope = factory.CreateSystemScope("Read storage quota usage");
        return await scope.ServiceProvider.GetRequiredService<TikuDbContext>().TenantFeatureUsages
            .Where(value => value.TenantId == tenantId && value.MetricCode == SaasQuotaMetricCatalog.StorageBytes)
            .Select(value => value.UsedValue)
            .SingleOrDefaultAsync();
    }

    private static async Task LoginAsync(
        HttpClient client,
        (Guid TenantId, Guid UserId, string Phone) seed)
    {
        client.UseAccessToken(await client.LoginAsTenantAsync(seed.TenantId, seed.Phone));
    }

    private static async Task<JsonDocument> ReadJsonAsync(HttpResponseMessage response)
    {
        var stream = await response.Content.ReadAsStreamAsync();
        return await JsonDocument.ParseAsync(stream);
    }

    private sealed class FakeObjectStorageService : IObjectStorageService
    {
        public long? MetadataSizeBytes { get; set; }

        public string? MetadataChecksumSha256 { get; init; }

        public string ConfiguredDefaultProvider() => ObjectStorageProviders.AliyunOss;

        public string ConfiguredDefaultBucket() => "tenant-assets";

        public string NormalizeProvider(string? value, string? fallback = null)
        {
            return value ?? fallback ?? ObjectStorageProviders.AliyunOss;
        }

        public string ValidateObjectKey(Guid tenantId, string objectKey) => objectKey;

        public string ValidateMimeType(string mimeType) => mimeType;

        public long? ValidateFileSize(long? fileSizeBytes) => fileSizeBytes;

        public void AssertUploadProvider(string provider) { }

        public void AssertWritableLocation(StorageAssetLocation location) { }

        public Task<ObjectStorageSignedUrl> SignUploadAsync(
            ObjectStorageUploadSignRequest request,
            CancellationToken cancellationToken = default)
        {
            return Task.FromResult(new ObjectStorageSignedUrl(
                request.Provider,
                request.Bucket,
                request.ObjectKey,
                "PUT",
                new Uri($"https://storage.example.test/{request.ObjectKey}"),
                new Dictionary<string, string> { ["content-type"] = request.MimeType },
                DateTimeOffset.UtcNow.Add(request.ExpiresIn),
                request.ExpiresIn,
                "fake-signed-url"));
        }

        public Task<ObjectStorageSignedUrl> SignDownloadAsync(
            ObjectStorageDownloadSignRequest request,
            CancellationToken cancellationToken = default)
        {
            return Task.FromResult(new ObjectStorageSignedUrl(
                request.Provider,
                request.Bucket,
                request.ObjectKey,
                "GET",
                new Uri($"https://storage.example.test/{request.ObjectKey}"),
                new Dictionary<string, string>(),
                DateTimeOffset.UtcNow.Add(request.ExpiresIn),
                request.ExpiresIn,
                "fake-signed-url"));
        }

        public Task<ObjectStorageWriteResult> WriteObjectAsync(
            ObjectStorageWriteRequest request,
            CancellationToken cancellationToken = default)
        {
            return Task.FromResult(new ObjectStorageWriteResult(
                request.Provider,
                request.Bucket,
                request.ObjectKey,
                new Uri($"https://storage.example.test/{request.ObjectKey}"),
                request.FileSizeBytes,
                request.MimeType,
                request.ChecksumSha256,
                "fake-etag",
                new Dictionary<string, string>(),
                "fake-write"));
        }

        public Task<ObjectStorageMetadata> HeadObjectAsync(
            ObjectStorageHeadRequest request,
            CancellationToken cancellationToken = default)
        {
            return Task.FromResult(new ObjectStorageMetadata(
                request.Provider,
                request.Bucket,
                request.ObjectKey,
                Exists: true,
                MetadataSizeBytes ?? request.DeclaredFileSizeBytes,
                request.DeclaredMimeType,
                MetadataChecksumSha256 ?? request.DeclaredChecksumSha256,
                "etag",
                DateTimeOffset.UtcNow.ToString("O"),
                new Dictionary<string, string>(),
                "fake-head"));
        }
    }
}
