using System.Net;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Tiku.Application.Storage;
using Tiku.Domain.Commerce;
using Tiku.Domain.Common;
using Tiku.Domain.Content;
using Tiku.Domain.Identity;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;

namespace Tiku.IntegrationTests.Api;

public sealed class AssetAccessEndpointTests
{
    [Fact]
    public async Task Public_asset_can_be_downloaded_anonymously_and_is_audited()
    {
        var tenantId = Guid.NewGuid();
        var assetId = Guid.NewGuid();
        await using var factory = new ApiTestFactory(objectStorageService: new FakeObjectStorageService());
        await factory.SeedAsync(
            Tenant(tenantId, "master"),
            PublicAsset(tenantId, assetId));
        using var client = factory.CreateClient();

        using var response = await client.GetAsync($"/api/student/assets/{assetId}/download?tenantCode=master");
        var body = await ReadJsonAsync(response);

        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        Assert.Equal("https://storage.example.test/download.pdf",
            body.RootElement.GetProperty("url").GetProperty("url").GetString());

        using var scope = factory.CreateSystemScope();
        var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
        Assert.Equal(1, dbContext.ContentAssets.Single(asset => asset.Id == assetId).DownloadCount);
        Assert.Contains(dbContext.ContentAssetAccessEvents, item =>
            item.AssetId == assetId &&
            item.Result == AssetAccessResult.Granted &&
            item.AccessType == AssetAccessType.Download);
    }

    [Fact]
    public async Task Members_asset_requires_authentication()
    {
        var tenantId = Guid.NewGuid();
        var assetId = Guid.NewGuid();
        await using var factory = new ApiTestFactory(objectStorageService: new FakeObjectStorageService());
        await factory.SeedAsync(
            Tenant(tenantId, "master"),
            new ContentAsset
            {
                Id = assetId,
                TenantId = tenantId,
                Title = "会员资料",
                Visibility = ContentVisibility.Members,
                StorageProvider = AssetStorageProvider.LocalDev,
                Bucket = "tenant-assets",
                ObjectKey = $"{tenantId:N}/members.pdf",
                MimeType = "application/pdf",
                AssetType = ContentAssetType.Pdf,
                UploadStatus = AssetUploadStatus.Verified,
                SecurityScanStatus = AssetSecurityScanStatus.Passed
            });
        using var client = factory.CreateClient();

        using var response = await client.GetAsync($"/api/student/assets/{assetId}/download?tenantCode=master");
        var body = await ReadJsonAsync(response);

        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
        Assert.Equal("auth_required", body.RootElement.GetProperty("code").GetString());
    }

    [Fact]
    public async Task Members_asset_can_be_downloaded_by_active_member()
    {
        var tenantId = Guid.NewGuid();
        var assetId = Guid.NewGuid();
        await using var factory = new ApiTestFactory(objectStorageService: new FakeObjectStorageService());
        var seed = await SeedLoginUserAsync(factory, tenantId);
        await factory.SeedAsync(
            new ContentAsset
            {
                Id = assetId,
                TenantId = tenantId,
                Title = "会员资料",
                Visibility = ContentVisibility.Members,
                StorageProvider = AssetStorageProvider.LocalDev,
                Bucket = "tenant-assets",
                ObjectKey = $"{tenantId:N}/members.pdf",
                MimeType = "application/pdf",
                AssetType = ContentAssetType.Pdf,
                UploadStatus = AssetUploadStatus.Verified,
                SecurityScanStatus = AssetSecurityScanStatus.Passed
            });
        using var client = factory.CreateClient();
        await LoginAsync(client, seed);

        using var response = await client.GetAsync($"/api/student/assets/{assetId}/download");
        var body = await ReadJsonAsync(response);

        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        Assert.Equal(seed.UserId, body.RootElement.GetProperty("access").GetProperty("userId").GetGuid());
    }

    [Fact]
    public async Task Svip_asset_requires_active_entitlement()
    {
        var tenantId = Guid.NewGuid();
        var assetId = Guid.NewGuid();
        await using var factory = new ApiTestFactory(objectStorageService: new FakeObjectStorageService());
        var seed = await SeedLoginUserAsync(factory, tenantId);
        await factory.SeedAsync(
            new ContentAsset
            {
                Id = assetId,
                TenantId = tenantId,
                Title = "SVIP 资料",
                Visibility = ContentVisibility.Svip,
                StorageProvider = AssetStorageProvider.LocalDev,
                Bucket = "tenant-assets",
                ObjectKey = $"{tenantId:N}/svip.pdf",
                MimeType = "application/pdf",
                AssetType = ContentAssetType.Pdf,
                UploadStatus = AssetUploadStatus.Verified,
                SecurityScanStatus = AssetSecurityScanStatus.Passed
            });
        using var client = factory.CreateClient();
        await LoginAsync(client, seed);

        using var deniedResponse = await client.GetAsync($"/api/student/assets/{assetId}/download");
        await factory.SeedAsync(new Entitlement
        {
            TenantId = tenantId,
            UserId = seed.UserId,
            EntitlementType = "svip",
            ScopeType = EntitlementScopeType.Tenant,
            Status = EntitlementStatus.Active,
            StartsAt = DateTimeOffset.UtcNow.AddMinutes(-1)
        });
        using var grantedResponse = await client.GetAsync($"/api/student/assets/{assetId}/download");

        Assert.Equal(HttpStatusCode.Forbidden, deniedResponse.StatusCode);
        Assert.Equal(HttpStatusCode.OK, grantedResponse.StatusCode);
    }

    [Fact]
    public async Task Preview_rejects_non_previewable_assets()
    {
        var tenantId = Guid.NewGuid();
        var assetId = Guid.NewGuid();
        await using var factory = new ApiTestFactory(objectStorageService: new FakeObjectStorageService());
        await factory.SeedAsync(
            Tenant(tenantId, "master"),
            new ContentAsset
            {
                Id = assetId,
                TenantId = tenantId,
                Title = "压缩包",
                Visibility = ContentVisibility.Public,
                StorageProvider = AssetStorageProvider.LocalDev,
                Bucket = "tenant-assets",
                ObjectKey = $"{tenantId:N}/archive.zip",
                MimeType = "application/zip",
                AssetType = ContentAssetType.Package,
                UploadStatus = AssetUploadStatus.Verified,
                SecurityScanStatus = AssetSecurityScanStatus.Passed
            });
        using var client = factory.CreateClient();

        using var response = await client.GetAsync($"/api/student/assets/{assetId}/preview?tenantCode=master");
        var body = await ReadJsonAsync(response);

        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
        Assert.Equal("asset_preview_not_supported", body.RootElement.GetProperty("code").GetString());
    }

    [Theory]
    [InlineData(AssetSecurityScanStatus.Pending)]
    [InlineData(AssetSecurityScanStatus.Scanning)]
    [InlineData(AssetSecurityScanStatus.Failed)]
    [InlineData(AssetSecurityScanStatus.Skipped)]
    public async Task Asset_access_fails_closed_until_security_scan_is_trusted(
        AssetSecurityScanStatus scanStatus)
    {
        var tenantId = Guid.NewGuid();
        var assetId = Guid.NewGuid();
        await using var factory = new ApiTestFactory(objectStorageService: new FakeObjectStorageService());
        var asset = PublicAsset(tenantId, assetId);
        asset.SecurityScanStatus = scanStatus;
        await factory.SeedAsync(Tenant(tenantId, "scan-gate"), asset);
        using var client = factory.CreateClient();

        using var response = await client.GetAsync($"/api/student/assets/{assetId}/download?tenantCode=scan-gate");
        var body = await ReadJsonAsync(response);

        Assert.Equal(HttpStatusCode.Conflict, response.StatusCode);
        Assert.Equal("asset_security_scan_not_passed", body.RootElement.GetProperty("code").GetString());
    }

    private static ContentAsset PublicAsset(Guid tenantId, Guid assetId)
    {
        return new ContentAsset
        {
            Id = assetId,
            TenantId = tenantId,
            Title = "公开资料",
            FileName = "download.pdf",
            Visibility = ContentVisibility.Public,
            StorageProvider = AssetStorageProvider.LocalDev,
            Bucket = "tenant-assets",
            ObjectKey = $"{tenantId:N}/download.pdf",
            MimeType = "application/pdf",
            AssetType = ContentAssetType.Pdf,
            UploadStatus = AssetUploadStatus.Verified,
            SecurityScanStatus = AssetSecurityScanStatus.Passed
        };
    }

    private static Tenant Tenant(Guid id, string slug)
    {
        return new Tenant
        {
            Id = id,
            Slug = slug,
            Name = slug,
            Status = TenantStatus.Active,
            Metadata = JsonDefaults.Object()
        };
    }

    private static async Task<(Guid TenantId, Guid UserId, string Phone)> SeedLoginUserAsync(
        ApiTestFactory factory,
        Guid tenantId)
    {
        var userId = Guid.NewGuid();
        var phone = "13800000000";
        await factory.SeedAsync(
            Tenant(tenantId, tenantId.ToString("N")),
            new User
            {
                Id = userId,
                Phone = phone,
                Name = "Test User"
            }.WithTestPassword(),
            new TenantMembership
            {
                TenantId = tenantId,
                UserId = userId,
                Role = TenantRole.Student,
                Status = MembershipStatus.Active
            });

        return (tenantId, userId, phone);
    }

    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 string ConfiguredDefaultProvider()
        {
            return ObjectStorageProviders.LocalDev;
        }

        public string ConfiguredDefaultBucket()
        {
            return "tenant-assets";
        }

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

        public string ValidateObjectKey(Guid tenantId, string objectKey)
        {
            return objectKey;
        }

        public string ValidateMimeType(string mimeType)
        {
            return mimeType;
        }

        public long? ValidateFileSize(long? fileSizeBytes)
        {
            return fileSizeBytes;
        }

        public void AssertUploadProvider(string provider)
        {
        }

        public void AssertWritableLocation(StorageAssetLocation location)
        {
        }

        public Task<ObjectStorageSignedUrl> SignUploadAsync(
            ObjectStorageUploadSignRequest request,
            CancellationToken cancellationToken = default)
        {
            return Task.FromResult(Signed("PUT", request.Bucket, request.ObjectKey, request.ExpiresIn));
        }

        public Task<ObjectStorageSignedUrl> SignDownloadAsync(
            ObjectStorageDownloadSignRequest request,
            CancellationToken cancellationToken = default)
        {
            return Task.FromResult(Signed("GET", request.Bucket, request.ObjectKey, request.ExpiresIn));
        }

        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,
                true,
                request.DeclaredFileSizeBytes,
                request.DeclaredMimeType,
                request.DeclaredChecksumSha256,
                null,
                null,
                new Dictionary<string, string>(),
                "fake"));
        }

        private static ObjectStorageSignedUrl Signed(
            string method,
            string? bucket,
            string? objectKey,
            TimeSpan expiresIn)
        {
            return new ObjectStorageSignedUrl(
                ObjectStorageProviders.LocalDev,
                bucket,
                objectKey,
                method,
                new Uri("https://storage.example.test/download.pdf"),
                new Dictionary<string, string>(),
                DateTimeOffset.UtcNow.Add(expiresIn),
                expiresIn,
                "fake-signed-url");
        }
    }
}