using System.Text;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Tiku.Application.Assets;
using Tiku.Application.Jobs;
using Tiku.Application.Storage;
using Tiku.Domain.Content;
using Tiku.Domain.Operations;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;

namespace Tiku.IntegrationTests.Api;

public sealed class P0AssetSecurityScanTests
{
    [Theory]
    [InlineData(AssetSecurityScanVerdict.Clean, AssetSecurityScanStatus.Passed, AssetSecurityRiskLevel.None)]
    [InlineData(AssetSecurityScanVerdict.Infected, AssetSecurityScanStatus.Failed, AssetSecurityRiskLevel.Critical)]
    public async Task Asset_scan_persists_terminal_verdict_and_audit_event(
        AssetSecurityScanVerdict verdict,
        AssetSecurityScanStatus expectedStatus,
        AssetSecurityRiskLevel expectedRisk)
    {
        var scanner = new FakeScanner(new AssetSecurityScanResult(
            verdict,
            "clamav",
            verdict == AssetSecurityScanVerdict.Infected ? "Eicar-Signature" : null,
            12,
            verdict == AssetSecurityScanVerdict.Infected ? "stream: Eicar-Signature FOUND" : "stream: OK"));
        await using var factory = new ApiTestFactory(
            objectStorageService: new ReadableStorage(),
            assetSecurityScanner: scanner);
        var (tenantId, assetId, jobId) = await SeedScanAsync(factory);

        using (var scope = factory.CreateSystemScope("Process asset security scan"))
        {
            Assert.Equal(1, await scope.ServiceProvider.GetRequiredService<IBackgroundJobService>()
                .ProcessPendingAsync("asset-scan-test", 10));
        }

        using var verifyScope = factory.CreateSystemScope("Verify asset security scan");
        var dbContext = verifyScope.ServiceProvider.GetRequiredService<TikuDbContext>();
        var asset = await dbContext.ContentAssets.AsNoTracking().SingleAsync(item => item.Id == assetId);
        var job = await dbContext.BackgroundJobs.AsNoTracking().SingleAsync(item => item.Id == jobId);
        var scanEvent = await dbContext.ContentAssetSecurityScanEvents.AsNoTracking()
            .SingleAsync(item => item.TenantId == tenantId && item.AssetId == assetId);

        Assert.Equal(expectedStatus, asset.SecurityScanStatus);
        Assert.Equal("clamav", asset.SecurityScanProvider);
        Assert.NotNull(asset.SecurityScannedAt);
        Assert.Equal(BackgroundJobStatus.Succeeded, job.Status);
        Assert.Equal(expectedStatus, scanEvent.ScanStatus);
        Assert.Equal(expectedRisk, scanEvent.RiskLevel);
        if (verdict == AssetSecurityScanVerdict.Infected)
            Assert.Contains("Eicar-Signature", scanEvent.IssueCodes);
        else
            Assert.Empty(scanEvent.IssueCodes);
    }

    [Fact]
    public async Task Unavailable_scanner_keeps_asset_pending_and_schedules_job_retry()
    {
        await using var factory = new ApiTestFactory(
            objectStorageService: new ReadableStorage(),
            assetSecurityScanner: new FakeScanner(new AssetSecurityScannerException(
                "clamav_unavailable",
                "ClamAV is unavailable.")));
        var (tenantId, assetId, jobId) = await SeedScanAsync(factory);

        using (var scope = factory.CreateSystemScope("Process unavailable asset security scan"))
        {
            Assert.Equal(1, await scope.ServiceProvider.GetRequiredService<IBackgroundJobService>()
                .ProcessPendingAsync("asset-scan-test", 10));
        }

        using var verifyScope = factory.CreateSystemScope("Verify asset security scan retry");
        var dbContext = verifyScope.ServiceProvider.GetRequiredService<TikuDbContext>();
        var asset = await dbContext.ContentAssets.AsNoTracking().SingleAsync(item => item.Id == assetId);
        var job = await dbContext.BackgroundJobs.AsNoTracking().SingleAsync(item => item.Id == jobId);
        var scanEvent = await dbContext.ContentAssetSecurityScanEvents.AsNoTracking()
            .SingleAsync(item => item.TenantId == tenantId && item.AssetId == assetId);

        Assert.Equal(AssetSecurityScanStatus.Pending, asset.SecurityScanStatus);
        Assert.Equal(BackgroundJobStatus.Pending, job.Status);
        Assert.Equal(1, job.RetryCount);
        Assert.NotNull(job.RunAfter);
        Assert.Contains("clamav_unavailable", job.LastError);
        Assert.Equal(AssetSecurityScanStatus.Pending, scanEvent.ScanStatus);
        Assert.Contains("clamav_unavailable", scanEvent.IssueCodes);
    }

    private static async Task<(Guid TenantId, Guid AssetId, Guid JobId)> SeedScanAsync(ApiTestFactory factory)
    {
        var tenantId = Guid.NewGuid();
        var assetId = Guid.NewGuid();
        await factory.SeedAsync(
            new Tenant
            {
                Id = tenantId,
                Slug = tenantId.ToString("N"),
                Name = "Asset scan tenant",
                Status = TenantStatus.Active
            },
            new ContentAsset
            {
                Id = assetId,
                TenantId = tenantId,
                Title = "Scannable asset",
                FileName = "scan.txt",
                StorageProvider = AssetStorageProvider.LocalDev,
                Bucket = "tenant-assets",
                ObjectKey = $"{tenantId:N}/assets/scan.txt",
                MimeType = "text/plain",
                FileSizeBytes = 12,
                VerifiedSizeBytes = 12,
                UploadStatus = AssetUploadStatus.Verified,
                SecurityScanStatus = AssetSecurityScanStatus.Pending
            });

        using var scope = factory.CreateSystemScope("Queue asset security scan");
        var job = await scope.ServiceProvider.GetRequiredService<IBackgroundJobService>().EnqueueAsync(
            new CreateBackgroundJobCommand(
                tenantId,
                "asset_security_scan",
                JsonSerializer.SerializeToElement(new { assetId }),
                MaxRetries: 5,
                IdempotencyKey: $"asset:{assetId:N}:integration-test",
                IsSystemJob: true));
        return (tenantId, assetId, job.Id);
    }

    private sealed class FakeScanner(object outcome) : IAssetSecurityScanner
    {
        public Task<AssetSecurityScanResult> ScanAsync(
            Stream content,
            long? declaredLength,
            CancellationToken cancellationToken = default)
        {
            return outcome switch
            {
                AssetSecurityScanResult result => Task.FromResult(result),
                Exception exception => Task.FromException<AssetSecurityScanResult>(exception),
                _ => throw new InvalidOperationException("Unsupported scanner outcome.")
            };
        }

        public Task<bool> CheckHealthAsync(CancellationToken cancellationToken = default)
        {
            return Task.FromResult(outcome is AssetSecurityScanResult);
        }
    }

    private sealed class ReadableStorage : 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)
        {
            throw new NotSupportedException();
        }

        public Task<ObjectStorageSignedUrl> SignDownloadAsync(ObjectStorageDownloadSignRequest request,
            CancellationToken cancellationToken = default)
        {
            throw new NotSupportedException();
        }

        public Task<ObjectStorageWriteResult> WriteObjectAsync(ObjectStorageWriteRequest request,
            CancellationToken cancellationToken = default)
        {
            throw new NotSupportedException();
        }

        public Task<ObjectStorageMetadata> HeadObjectAsync(ObjectStorageHeadRequest request,
            CancellationToken cancellationToken = default)
        {
            throw new NotSupportedException();
        }

        public Task<Stream> OpenReadAsync(ObjectStorageReadRequest request,
            CancellationToken cancellationToken = default)
        {
            return Task.FromResult<Stream>(new MemoryStream(Encoding.UTF8.GetBytes("hello world!")));
        }
    }
}