using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.LocalDb.Internal;
using ZB.MOM.WW.LocalDb.Replication;
using ZB.MOM.WW.LocalDb.Replication.Internal;

namespace ZB.MOM.WW.LocalDb.Tests;

public sealed class MaintenanceTests : IDisposable
{
    private static readonly TimeSpan WaitTimeout = TimeSpan.FromSeconds(10);
    private static readonly TimeSpan Tick = TimeSpan.FromMilliseconds(50);

    private readonly List<string> _paths = [];
    private readonly List<IDisposable> _disposables = [];

    public void Dispose()
    {
        foreach (var d in _disposables)
            d.Dispose();
        Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
        foreach (var p in _paths)
        {
            if (File.Exists(p)) File.Delete(p);
            if (File.Exists(p + "-wal")) File.Delete(p + "-wal");
            if (File.Exists(p + "-shm")) File.Delete(p + "-shm");
        }
    }

    private async Task<(SqliteLocalDb Db, OplogStore Store)> NewDbAsync(ReplicationOptions options)
    {
        var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".db");
        _paths.Add(path);
        var db = new SqliteLocalDb(new LocalDbOptions { Path = path });
        _disposables.Add(db);
        await db.ExecuteAsync("CREATE TABLE orders (id INTEGER PRIMARY KEY, sku TEXT, qty INTEGER)");
        db.RegisterReplicated("orders");
        return (db, new OplogStore(db, options));
    }

    private static MaintenanceBackgroundService NewService(
        OplogStore store, ReplicationOptions options, ILoggerFactory? loggerFactory = null) =>
        new(store, Options.Create(options), loggerFactory ?? NullLoggerFactory.Instance);

    private static async Task WaitForAsync(Func<Task<bool>> predicate, TimeSpan timeout)
    {
        var deadline = DateTime.UtcNow + timeout;
        while (DateTime.UtcNow < deadline)
        {
            if (await predicate()) return;
            await Task.Delay(20);
        }
        throw new TimeoutException("Condition not reached within " + timeout);
    }

    [Fact]
    public async Task Maintenance_EnforcesCaps_SetsNeedsSnapshot()
    {
        var options = new ReplicationOptions { MaxOplogRows = 5, MaintenanceInterval = Tick };
        var (db, store) = await NewDbAsync(options);
        for (var i = 1; i <= 20; i++)
            await db.ExecuteAsync("INSERT INTO orders (id, sku, qty) VALUES (@id, 'R', @id)", new { id = i });

        var service = NewService(store, options);
        await service.StartAsync(CancellationToken.None);
        try
        {
            await WaitForAsync(async () =>
            {
                var snap = await db.QueryAsync("SELECT needs_snapshot FROM __localdb_peer_state WHERE id = 1", x => x.GetInt64(0));
                var depth = await db.QueryAsync("SELECT COUNT(*) FROM __localdb_oplog", x => x.GetInt64(0));
                return snap[0] == 1 && depth[0] <= 5;
            }, WaitTimeout);
        }
        finally
        {
            await service.StopAsync(CancellationToken.None);
        }
    }

    [Fact]
    public async Task Maintenance_PrunesExpiredTombstones()
    {
        var options = new ReplicationOptions { MaintenanceInterval = Tick };
        var (db, store) = await NewDbAsync(options);
        await db.ExecuteAsync("INSERT INTO orders (id, sku, qty) VALUES (1, 'DOOMED', 1)");
        await db.ExecuteAsync("DELETE FROM orders WHERE id = 1");
        // Backdate the delete-trigger's tombstone far past any retention window.
        var updated = await db.ExecuteAsync(
            "UPDATE __localdb_row_version SET tombstone_utc = '2000-01-01T00:00:00.000Z' WHERE is_tombstone = 1");
        Assert.Equal(1, updated);

        var service = NewService(store, options);
        await service.StartAsync(CancellationToken.None);
        try
        {
            await WaitForAsync(async () =>
            {
                var rows = await db.QueryAsync(
                    "SELECT COUNT(*) FROM __localdb_row_version WHERE is_tombstone = 1", x => x.GetInt64(0));
                return rows[0] == 0;
            }, WaitTimeout);
        }
        finally
        {
            await service.StopAsync(CancellationToken.None);
        }
    }

    [Fact]
    public async Task Maintenance_SurvivesTickFailure()
    {
        var options = new ReplicationOptions { MaintenanceInterval = Tick };
        var (db, store) = await NewDbAsync(options);
        // Deterministic per-tick failure: every EnforceCapsAsync now hits "no such table".
        await db.ExecuteAsync("DROP TABLE __localdb_oplog");

        var logger = new ListLoggerFactory();
        var service = NewService(store, options, logger);
        await service.StartAsync(CancellationToken.None);
        try
        {
            // At least ~5 failing ticks must have elapsed without killing the loop.
            await WaitForAsync(
                () => Task.FromResult(logger.Count(LogLevel.Warning) >= 5), WaitTimeout);
            Assert.NotNull(service.ExecuteTask);
            Assert.False(service.ExecuteTask!.IsCompleted, "maintenance loop died on a tick failure");
        }
        finally
        {
            await service.StopAsync(CancellationToken.None);
        }
        Assert.True(service.ExecuteTask!.IsCompletedSuccessfully);
    }

    [Fact]
    public void Validator_NonPositiveMaintenanceInterval_Fails()
    {
        var result = new ReplicationOptionsValidator().Validate(
            null, new ReplicationOptions { MaintenanceInterval = TimeSpan.Zero });

        Assert.True(result.Failed);
        Assert.Contains(result.Failures, f => f.Contains("MaintenanceInterval", StringComparison.Ordinal));
    }

    [Fact]
    public void AddZbLocalDbReplication_RegistersMaintenance_BothRoles()
    {
        foreach (var peerAddress in new string?[] { null, "http://localhost" })
        {
            var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".db");
            _paths.Add(path);
            var dict = new Dictionary<string, string?> { ["LocalDb:Path"] = path };
            if (peerAddress is not null) dict["LocalDb:Replication:PeerAddress"] = peerAddress;
            var config = new ConfigurationBuilder().AddInMemoryCollection(dict).Build();

            var services = new ServiceCollection();
            services.AddLogging();
            services.AddZbLocalDb(config, db =>
            {
                using var conn = db.CreateConnection();
                using var cmd = conn.CreateCommand();
                cmd.CommandText = "CREATE TABLE orders (id INTEGER PRIMARY KEY)";
                cmd.ExecuteNonQuery();
                db.RegisterReplicated("orders");
            });
            services.AddZbLocalDbReplication(config);
            var provider = services.BuildServiceProvider();
            _disposables.Add(provider);

            Assert.Contains(provider.GetServices<IHostedService>(), s => s is MaintenanceBackgroundService);
        }
    }

    private sealed class ListLoggerFactory : ILoggerFactory
    {
        private readonly List<LogLevel> _levels = [];

        public int Count(LogLevel level)
        {
            lock (_levels)
                return _levels.Count(l => l == level);
        }

        public ILogger CreateLogger(string categoryName) => new ListLogger(_levels);
        public void AddProvider(ILoggerProvider provider) { }
        public void Dispose() { }

        private sealed class ListLogger(List<LogLevel> levels) : ILogger
        {
            public IDisposable BeginScope<TState>(TState state) where TState : notnull => NullScope.Instance;
            public bool IsEnabled(LogLevel logLevel) => true;

            public void Log<TState>(
                LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
            {
                lock (levels)
                    levels.Add(logLevel);
            }

            private sealed class NullScope : IDisposable
            {
                public static readonly NullScope Instance = new();
                public void Dispose() { }
            }
        }
    }
}
