using System.Data;
using System.Data.Common;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using ScadaLink.Commons.Entities.ExternalSystems;
using ScadaLink.Commons.Interfaces.Repositories;

namespace ScadaLink.ExternalSystemGateway.Tests;

/// <summary>
/// WP-9: Tests for Database access — connection resolution, cached writes.
/// </summary>
public class DatabaseGatewayTests
{
    private readonly IExternalSystemRepository _repository = Substitute.For<IExternalSystemRepository>();

    /// <summary>
    /// Configures the repository substitute for the name-keyed connection-resolution
    /// path used by <c>DatabaseGateway</c> (ExternalSystemGateway-011). A <c>null</c>
    /// connection models a "not found" — the substitute returns <c>null</c> by default,
    /// so no stub is needed for the absent entity.
    /// </summary>
    private void StubConnection(DatabaseConnectionDefinition? connection)
    {
        if (connection != null)
        {
            _repository.GetDatabaseConnectionByNameAsync(connection.Name, Arg.Any<CancellationToken>())
                .Returns(connection);
        }
    }

    [Fact]
    public async Task GetConnection_NotFound_Throws()
    {
        StubConnection(connection: null);

        var gateway = new DatabaseGateway(
            _repository,
            NullLogger<DatabaseGateway>.Instance);

        await Assert.ThrowsAsync<InvalidOperationException>(
            () => gateway.GetConnectionAsync("nonexistent"));
    }

    [Fact]
    public async Task CachedWrite_NoStoreAndForward_Throws()
    {
        var conn = new DatabaseConnectionDefinition("testDb", "Server=localhost;Database=test") { Id = 1 };
        StubConnection(conn);

        var gateway = new DatabaseGateway(
            _repository,
            NullLogger<DatabaseGateway>.Instance,
            storeAndForward: null);

        await Assert.ThrowsAsync<InvalidOperationException>(
            () => gateway.CachedWriteAsync("testDb", "INSERT INTO t VALUES (1)"));
    }

    [Fact]
    public async Task CachedWrite_ConnectionNotFound_Throws()
    {
        StubConnection(connection: null);

        var gateway = new DatabaseGateway(
            _repository,
            NullLogger<DatabaseGateway>.Instance);

        await Assert.ThrowsAsync<InvalidOperationException>(
            () => gateway.CachedWriteAsync("nonexistent", "INSERT INTO t VALUES (1)"));
    }

    // ── ExternalSystemGateway-014: CachedWrite happy-path buffering ──

    [Fact]
    public async Task CachedWrite_BuffersTheWriteWithConnectionRetrySettings()
    {
        var conn = new DatabaseConnectionDefinition("testDb", "Server=localhost;Database=test")
        {
            Id = 1,
            MaxRetries = 5,
            RetryDelay = TimeSpan.FromSeconds(12),
        };
        StubConnection(conn);

        var dbName = $"EsgCachedWrite_{Guid.NewGuid():N}";
        var connStr = $"Data Source={dbName};Mode=Memory;Cache=Shared";
        using var keepAlive = new Microsoft.Data.Sqlite.SqliteConnection(connStr);
        keepAlive.Open();
        var storage = new ScadaLink.StoreAndForward.StoreAndForwardStorage(
            connStr, NullLogger<ScadaLink.StoreAndForward.StoreAndForwardStorage>.Instance);
        await storage.InitializeAsync();
        var sfOptions = new ScadaLink.StoreAndForward.StoreAndForwardOptions
        {
            DefaultMaxRetries = 99,
            DefaultRetryInterval = TimeSpan.FromMinutes(10),
            RetryTimerInterval = TimeSpan.FromMinutes(10),
        };
        var sf = new ScadaLink.StoreAndForward.StoreAndForwardService(
            storage, sfOptions, NullLogger<ScadaLink.StoreAndForward.StoreAndForwardService>.Instance);

        var gateway = new DatabaseGateway(_repository, NullLogger<DatabaseGateway>.Instance, storeAndForward: sf);

        await gateway.CachedWriteAsync("testDb", "INSERT INTO t VALUES (@v)",
            new Dictionary<string, object?> { ["v"] = 1 });

        var depth = await storage.GetBufferDepthByCategoryAsync();
        Assert.Equal(1, depth[ScadaLink.Commons.Types.Enums.StoreAndForwardCategory.CachedDbWrite]);

        var (maxRetries, retryIntervalMs) = ReadBufferedRetrySettings(connStr);
        Assert.Equal(5, maxRetries);
        Assert.Equal((long)TimeSpan.FromSeconds(12).TotalMilliseconds, retryIntervalMs);
    }

    [Fact]
    public async Task CachedWrite_ZeroMaxRetriesIsTreatedAsUnsetNotRetryForever()
    {
        // ExternalSystemGateway-015: a stored MaxRetries of 0 is interpreted by the
        // Store-and-Forward retry sweep as "no limit" (retry forever). The entity's
        // non-nullable int default is also 0, so the gateway must treat the
        // connection's MaxRetries == 0 as "unset" and pass null — the bounded S&F
        // default must apply, never 0.
        var conn = new DatabaseConnectionDefinition("testDb", "Server=localhost;Database=test")
        {
            Id = 1,
            MaxRetries = 0,
            RetryDelay = TimeSpan.FromSeconds(3),
        };
        StubConnection(conn);

        var dbName = $"EsgCachedWriteZero_{Guid.NewGuid():N}";
        var connStr = $"Data Source={dbName};Mode=Memory;Cache=Shared";
        using var keepAlive = new Microsoft.Data.Sqlite.SqliteConnection(connStr);
        keepAlive.Open();
        var storage = new ScadaLink.StoreAndForward.StoreAndForwardStorage(
            connStr, NullLogger<ScadaLink.StoreAndForward.StoreAndForwardStorage>.Instance);
        await storage.InitializeAsync();
        var sfOptions = new ScadaLink.StoreAndForward.StoreAndForwardOptions
        {
            DefaultMaxRetries = 99,
            DefaultRetryInterval = TimeSpan.FromMinutes(10),
            RetryTimerInterval = TimeSpan.FromMinutes(10),
        };
        var sf = new ScadaLink.StoreAndForward.StoreAndForwardService(
            storage, sfOptions, NullLogger<ScadaLink.StoreAndForward.StoreAndForwardService>.Instance);

        var gateway = new DatabaseGateway(_repository, NullLogger<DatabaseGateway>.Instance, storeAndForward: sf);

        await gateway.CachedWriteAsync("testDb", "INSERT INTO t VALUES (1)");

        var (maxRetries, _) = ReadBufferedRetrySettings(connStr);
        // Must be the bounded S&F default, never 0 — a stored 0 would mean retry-forever.
        Assert.Equal(99, maxRetries);
        Assert.NotEqual(0, maxRetries);
    }

    private static (int MaxRetries, long RetryIntervalMs) ReadBufferedRetrySettings(string connStr)
    {
        using var conn = new Microsoft.Data.Sqlite.SqliteConnection(connStr);
        conn.Open();
        using var cmd = conn.CreateCommand();
        cmd.CommandText = "SELECT max_retries, retry_interval_ms FROM sf_messages";
        using var reader = cmd.ExecuteReader();
        Assert.True(reader.Read(), "expected exactly one buffered message");
        var result = (reader.GetInt32(0), reader.GetInt64(1));
        Assert.False(reader.Read(), "expected exactly one buffered message");
        return result;
    }

    // ── ExternalSystemGateway-001: buffered CachedDbWrite delivery handler ──

    [Fact]
    public async Task DeliverBuffered_ConnectionNoLongerExists_ReturnsFalseSoMessageParks()
    {
        StubConnection(connection: null);
        var gateway = new DatabaseGateway(_repository, NullLogger<DatabaseGateway>.Instance);

        var message = new ScadaLink.StoreAndForward.StoreAndForwardMessage
        {
            Id = Guid.NewGuid().ToString("N"),
            Category = ScadaLink.Commons.Types.Enums.StoreAndForwardCategory.CachedDbWrite,
            Target = "gone-db",
            PayloadJson =
                """{"ConnectionName":"gone-db","Sql":"INSERT INTO t VALUES (1)","Parameters":null}""",
        };

        var delivered = await gateway.DeliverBufferedAsync(message);

        Assert.False(delivered); // permanent — the S&F engine parks the message
    }

    // ── ExternalSystemGateway-010: SqlConnection must not leak when OpenAsync fails ──

    [Fact]
    public async Task GetConnection_OpenFails_DisposesConnectionBeforeRethrowing()
    {
        var conn = new DatabaseConnectionDefinition("testDb", "Server=localhost;Database=test") { Id = 1 };
        StubConnection(conn);

        var fake = new ThrowingDbConnection();
        var gateway = new ConnectionFactoryStubGateway(_repository, fake);

        await Assert.ThrowsAsync<InvalidOperationException>(
            () => gateway.GetConnectionAsync("testDb"));

        Assert.True(fake.WasDisposed, "The SqlConnection was leaked — it must be disposed when OpenAsync fails");
    }

    /// <summary>Test gateway that substitutes the connection factory with a stub.</summary>
    private sealed class ConnectionFactoryStubGateway : DatabaseGateway
    {
        private readonly DbConnection _connection;

        public ConnectionFactoryStubGateway(IExternalSystemRepository repository, DbConnection connection)
            : base(repository, NullLogger<DatabaseGateway>.Instance) => _connection = connection;

        internal override DbConnection CreateConnection(string connectionString) => _connection;
    }

    /// <summary>A DbConnection whose OpenAsync always fails, tracking whether it was disposed.</summary>
    private sealed class ThrowingDbConnection : DbConnection
    {
        public bool WasDisposed { get; private set; }

        public override Task OpenAsync(CancellationToken cancellationToken) =>
            throw new InvalidOperationException("simulated open failure");
        public override void Open() => throw new InvalidOperationException("simulated open failure");

        protected override void Dispose(bool disposing)
        {
            if (disposing) WasDisposed = true;
            base.Dispose(disposing);
        }

        public override ValueTask DisposeAsync()
        {
            WasDisposed = true;
            return base.DisposeAsync();
        }

        // Unused abstract members.
        [System.Diagnostics.CodeAnalysis.AllowNull]
        public override string ConnectionString { get; set; } = string.Empty;
        public override string Database => string.Empty;
        public override string DataSource => string.Empty;
        public override string ServerVersion => string.Empty;
        public override ConnectionState State => ConnectionState.Closed;
        public override void ChangeDatabase(string databaseName) => throw new NotSupportedException();
        public override void Close() { }
        protected override DbTransaction BeginDbTransaction(IsolationLevel il) => throw new NotSupportedException();
        protected override DbCommand CreateDbCommand() => throw new NotSupportedException();
    }
}
