using System.Security.Cryptography;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;
using StellaOps.Scanner.Storage;
using StellaOps.Scanner.Storage.Catalog;
using StellaOps.Scanner.Storage.Migrations;
using StellaOps.Scanner.Storage.Mongo;
using StellaOps.Scanner.Storage.ObjectStore;
using StellaOps.Scanner.Storage.Repositories;
using StellaOps.Scanner.Storage.Services;
using Xunit;

namespace StellaOps.Scanner.Storage.Tests;

[CollectionDefinition("scanner-mongo-fixture")]
public sealed class ScannerMongoCollection : ICollectionFixture<ScannerMongoFixture>
{
}

[Collection("scanner-mongo-fixture")]
public sealed class StorageDualWriteFixture
{
    private readonly ScannerMongoFixture _fixture;

    public StorageDualWriteFixture(ScannerMongoFixture fixture)
    {
        _fixture = fixture;
    }

    [Fact]
    public async Task StoreArtifactAsync_DualWrite_WritesToMirrorAndCatalog()
    {
        var options = BuildOptions(dualWrite: true, mirrorBucket: "mirror-bucket");
        var objectStore = new InMemoryArtifactObjectStore();

        await InitializeMongoAsync(options);
        var provider = new MongoCollectionProvider(_fixture.Database, Options.Create(options));
        var artifactRepository = new ArtifactRepository(provider);
        var lifecycleRepository = new LifecycleRuleRepository(provider);
        var service = new ArtifactStorageService(
            artifactRepository,
            lifecycleRepository,
            objectStore,
            Options.Create(options),
            NullLogger<ArtifactStorageService>.Instance);

        var bytes = System.Text.Encoding.UTF8.GetBytes("test artifact payload");
        using var stream = new MemoryStream(bytes);
        var expiresAt = DateTime.UtcNow.AddHours(6);

        var document = await service.StoreArtifactAsync(
            ArtifactDocumentType.LayerBom,
            ArtifactDocumentFormat.CycloneDxJson,
            mediaType: "application/vnd.cyclonedx+json",
            content: stream,
            immutable: true,
            ttlClass: "compliance",
            expiresAtUtc: expiresAt,
            cancellationToken: CancellationToken.None);

        var digest = Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant();
        var expectedKey = $"{options.ObjectStore.RootPrefix.TrimEnd('/')}/layers/{digest}/sbom.cdx.json";
        Assert.Contains(objectStore.Objects.Keys, key => key.Bucket == options.ObjectStore.BucketName && key.Key == expectedKey);
        Assert.Contains(objectStore.Objects.Keys, key => key.Bucket == options.DualWrite.MirrorBucket && key.Key == expectedKey);

        var artifact = await artifactRepository.GetAsync(document.Id, CancellationToken.None);
        Assert.NotNull(artifact);
        Assert.Equal($"sha256:{digest}", artifact!.BytesSha256);
        Assert.Equal(1, artifact.RefCount);
        Assert.Equal("compliance", artifact.TtlClass);
        Assert.True(artifact.Immutable);

        var lifecycleCollection = _fixture.Database.GetCollection<LifecycleRuleDocument>(ScannerStorageDefaults.Collections.LifecycleRules);
        var lifecycle = await lifecycleCollection.Find(x => x.ArtifactId == document.Id).FirstOrDefaultAsync();
        Assert.NotNull(lifecycle);
        Assert.Equal("compliance", lifecycle!.Class);
        Assert.True(lifecycle.ExpiresAtUtc.HasValue);
        Assert.True(lifecycle.ExpiresAtUtc.Value <= expiresAt.AddSeconds(5));
    }

    [Fact]
    public async Task Bootstrapper_CreatesLifecycleTtlIndex()
    {
        var options = BuildOptions(dualWrite: false, mirrorBucket: null);
        await InitializeMongoAsync(options);

        var collection = _fixture.Database.GetCollection<BsonDocument>(ScannerStorageDefaults.Collections.LifecycleRules);
        var cursor = await collection.Indexes.ListAsync();
        var indexes = await cursor.ToListAsync();
        var ttlIndex = indexes.SingleOrDefault(x => string.Equals(x["name"].AsString, "lifecycle_expiresAt", StringComparison.Ordinal));

        Assert.NotNull(ttlIndex);
        Assert.True(ttlIndex!.TryGetValue("expireAfterSeconds", out var expireValue));
        Assert.Equal(0, expireValue.ToInt64());

        var uniqueIndex = indexes.SingleOrDefault(x => string.Equals(x["name"].AsString, "lifecycle_artifact_class", StringComparison.Ordinal));
        Assert.NotNull(uniqueIndex);
        Assert.True(uniqueIndex!["unique"].AsBoolean);
    }

    private ScannerStorageOptions BuildOptions(bool dualWrite, string? mirrorBucket)
    {
        var options = new ScannerStorageOptions
        {
            Mongo =
            {
                ConnectionString = _fixture.Runner.ConnectionString,
                DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName,
            },
            ObjectStore =
            {
                BucketName = "primary-bucket",
                RootPrefix = "scanner",
                EnableObjectLock = true,
            },
        };

        options.DualWrite.Enabled = dualWrite;
        options.DualWrite.MirrorBucket = mirrorBucket;
        return options;
    }

    private async Task InitializeMongoAsync(ScannerStorageOptions options)
    {
        await _fixture.Client.DropDatabaseAsync(options.Mongo.DatabaseName);
        var migrations = new IMongoMigration[] { new EnsureLifecycleRuleTtlMigration() };
        var runner = new MongoMigrationRunner(
            _fixture.Database,
            migrations,
            NullLogger<MongoMigrationRunner>.Instance,
            TimeProvider.System);
        var bootstrapper = new MongoBootstrapper(
            _fixture.Database,
            Options.Create(options),
            NullLogger<MongoBootstrapper>.Instance,
            runner);

        await bootstrapper.InitializeAsync(CancellationToken.None);
    }
}
