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>();

    [Fact]
    public async Task GetConnection_NotFound_Throws()
    {
        _repository.GetAllDatabaseConnectionsAsync().Returns(new List<DatabaseConnectionDefinition>());

        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 };
        _repository.GetAllDatabaseConnectionsAsync()
            .Returns(new List<DatabaseConnectionDefinition> { 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()
    {
        _repository.GetAllDatabaseConnectionsAsync().Returns(new List<DatabaseConnectionDefinition>());

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

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

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

    [Fact]
    public async Task DeliverBuffered_ConnectionNoLongerExists_ReturnsFalseSoMessageParks()
    {
        _repository.GetAllDatabaseConnectionsAsync().Returns(new List<DatabaseConnectionDefinition>());
        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 };
        _repository.GetAllDatabaseConnectionsAsync().Returns(new List<DatabaseConnectionDefinition> { 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();
    }
}
