using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;
using StellaOps.Feedser.Storage.Mongo;
using StellaOps.Feedser.Storage.Mongo.Migrations;
using Xunit;

namespace StellaOps.Feedser.Storage.Mongo.Tests.Migrations;

[Collection("mongo-fixture")]
public sealed class MongoMigrationRunnerTests
{
    private readonly MongoIntegrationFixture _fixture;

    public MongoMigrationRunnerTests(MongoIntegrationFixture fixture)
    {
        _fixture = fixture;
    }

    [Fact]
    public async Task RunAsync_AppliesPendingMigrationsOnce()
    {
        var databaseName = $"feedser-migrations-{Guid.NewGuid():N}";
        var database = _fixture.Client.GetDatabase(databaseName);
        await database.CreateCollectionAsync(MongoStorageDefaults.Collections.Migrations);

        try
        {
            var migration = new TestMigration();
            var runner = new MongoMigrationRunner(
                database,
                new IMongoMigration[] { migration },
                NullLogger<MongoMigrationRunner>.Instance,
                TimeProvider.System);

            await runner.RunAsync(CancellationToken.None);
            await runner.RunAsync(CancellationToken.None);

            Assert.Equal(1, migration.ApplyCount);

            var count = await database
                .GetCollection<BsonDocument>(MongoStorageDefaults.Collections.Migrations)
                .CountDocumentsAsync(FilterDefinition<BsonDocument>.Empty);
            Assert.Equal(1, count);
        }
        finally
        {
            await _fixture.Client.DropDatabaseAsync(databaseName);
        }
    }

    [Fact]
    public async Task EnsureDocumentExpiryIndexesMigration_CreatesTtlIndexWhenRetentionEnabled()
    {
        var databaseName = $"feedser-doc-ttl-{Guid.NewGuid():N}";
        var database = _fixture.Client.GetDatabase(databaseName);
        await database.CreateCollectionAsync(MongoStorageDefaults.Collections.Document);
        await database.CreateCollectionAsync(MongoStorageDefaults.Collections.Migrations);

        try
        {
            var options = Options.Create(new MongoStorageOptions
            {
                RawDocumentRetention = TimeSpan.FromDays(45),
                RawDocumentRetentionTtlGrace = TimeSpan.FromHours(12),
            });

            var migration = new EnsureDocumentExpiryIndexesMigration(options);
            var runner = new MongoMigrationRunner(
                database,
                new IMongoMigration[] { migration },
                NullLogger<MongoMigrationRunner>.Instance,
                TimeProvider.System);

            await runner.RunAsync(CancellationToken.None);

            var indexes = await database
                .GetCollection<BsonDocument>(MongoStorageDefaults.Collections.Document)
                .Indexes.ListAsync();
            var indexList = await indexes.ToListAsync();

            var ttlIndex = indexList.Single(x => x["name"].AsString == "document_expiresAt_ttl");
            Assert.Equal(0, ttlIndex["expireAfterSeconds"].ToDouble());
            Assert.True(ttlIndex["partialFilterExpression"].AsBsonDocument["expiresAt"].AsBsonDocument["$exists"].ToBoolean());
        }
        finally
        {
            await _fixture.Client.DropDatabaseAsync(databaseName);
        }
    }

    [Fact]
    public async Task EnsureDocumentExpiryIndexesMigration_DropsTtlIndexWhenRetentionDisabled()
    {
        var databaseName = $"feedser-doc-notl-{Guid.NewGuid():N}";
        var database = _fixture.Client.GetDatabase(databaseName);
        await database.CreateCollectionAsync(MongoStorageDefaults.Collections.Document);
        await database.CreateCollectionAsync(MongoStorageDefaults.Collections.Migrations);

        try
        {
            var collection = database.GetCollection<BsonDocument>(MongoStorageDefaults.Collections.Document);
            var keys = Builders<BsonDocument>.IndexKeys.Ascending("expiresAt");
            var options = new CreateIndexOptions<BsonDocument>
            {
                Name = "document_expiresAt_ttl",
                ExpireAfter = TimeSpan.Zero,
                PartialFilterExpression = Builders<BsonDocument>.Filter.Exists("expiresAt", true),
            };

            await collection.Indexes.CreateOneAsync(new CreateIndexModel<BsonDocument>(keys, options));

            var migration = new EnsureDocumentExpiryIndexesMigration(Options.Create(new MongoStorageOptions
            {
                RawDocumentRetention = TimeSpan.Zero,
            }));

            var runner = new MongoMigrationRunner(
                database,
                new IMongoMigration[] { migration },
                NullLogger<MongoMigrationRunner>.Instance,
                TimeProvider.System);

            await runner.RunAsync(CancellationToken.None);

            var indexes = await collection.Indexes.ListAsync();
            var indexList = await indexes.ToListAsync();

            Assert.DoesNotContain(indexList, x => x["name"].AsString == "document_expiresAt_ttl");
            var nonTtl = indexList.Single(x => x["name"].AsString == "document_expiresAt");
            Assert.False(nonTtl.Contains("expireAfterSeconds"));
        }
        finally
        {
            await _fixture.Client.DropDatabaseAsync(databaseName);
        }
    }

    [Fact]
    public async Task EnsureGridFsExpiryIndexesMigration_CreatesTtlIndexWhenRetentionEnabled()
    {
        var databaseName = $"feedser-gridfs-ttl-{Guid.NewGuid():N}";
        var database = _fixture.Client.GetDatabase(databaseName);
        await database.CreateCollectionAsync("documents.files");
        await database.CreateCollectionAsync(MongoStorageDefaults.Collections.Migrations);

        try
        {
            var migration = new EnsureGridFsExpiryIndexesMigration(Options.Create(new MongoStorageOptions
            {
                RawDocumentRetention = TimeSpan.FromDays(30),
            }));

            var runner = new MongoMigrationRunner(
                database,
                new IMongoMigration[] { migration },
                NullLogger<MongoMigrationRunner>.Instance,
                TimeProvider.System);

            await runner.RunAsync(CancellationToken.None);

            var indexes = await database.GetCollection<BsonDocument>("documents.files").Indexes.ListAsync();
            var indexList = await indexes.ToListAsync();

            var ttlIndex = indexList.Single(x => x["name"].AsString == "gridfs_files_expiresAt_ttl");
            Assert.Equal(0, ttlIndex["expireAfterSeconds"].ToDouble());
        }
        finally
        {
            await _fixture.Client.DropDatabaseAsync(databaseName);
        }
    }

    [Fact]
    public async Task EnsureGridFsExpiryIndexesMigration_DropsTtlIndexWhenRetentionDisabled()
    {
        var databaseName = $"feedser-gridfs-notl-{Guid.NewGuid():N}";
        var database = _fixture.Client.GetDatabase(databaseName);
        await database.CreateCollectionAsync("documents.files");
        await database.CreateCollectionAsync(MongoStorageDefaults.Collections.Migrations);

        try
        {
            var collection = database.GetCollection<BsonDocument>("documents.files");
            var keys = Builders<BsonDocument>.IndexKeys.Ascending("metadata.expiresAt");
            var options = new CreateIndexOptions<BsonDocument>
            {
                Name = "gridfs_files_expiresAt_ttl",
                ExpireAfter = TimeSpan.Zero,
                PartialFilterExpression = Builders<BsonDocument>.Filter.Exists("metadata.expiresAt", true),
            };

            await collection.Indexes.CreateOneAsync(new CreateIndexModel<BsonDocument>(keys, options));

            var migration = new EnsureGridFsExpiryIndexesMigration(Options.Create(new MongoStorageOptions
            {
                RawDocumentRetention = TimeSpan.Zero,
            }));

            var runner = new MongoMigrationRunner(
                database,
                new IMongoMigration[] { migration },
                NullLogger<MongoMigrationRunner>.Instance,
                TimeProvider.System);

            await runner.RunAsync(CancellationToken.None);

            var indexes = await collection.Indexes.ListAsync();
            var indexList = await indexes.ToListAsync();

            Assert.DoesNotContain(indexList, x => x["name"].AsString == "gridfs_files_expiresAt_ttl");
        }
        finally
        {
            await _fixture.Client.DropDatabaseAsync(databaseName);
        }
    }

    private sealed class TestMigration : IMongoMigration
    {
        public int ApplyCount { get; private set; }

        public string Id => "999_test";

        public string Description => "test migration";

        public Task ApplyAsync(IMongoDatabase database, CancellationToken cancellationToken)
        {
            ApplyCount++;
            return Task.CompletedTask;
        }
    }
}
