using System.Data.Common;

namespace ZB.MOM.WW.ScadaBridge.ExternalSystemGateway.Tests;

/// <summary>
/// M2.3 (#7): unit tests for the transient-vs-permanent SQL error-number
/// classifier that <c>DatabaseGateway</c> uses to decide whether a failed
/// cached write should be buffered (transient) or returned to the script
/// synchronously / parked (permanent).
/// </summary>
public class SqlErrorClassifierTests
{
    // The full transient set documented on SqlErrorClassifier — connection,
    // timeout, deadlock, and Azure throttle error numbers. A retry can plausibly
    // succeed for any of these, so they are buffered to store-and-forward.
    [Theory]
    [InlineData(-2)]      // timeout expired
    [InlineData(-1)]      // connection error
    [InlineData(2)]       // network / instance not found
    [InlineData(53)]      // network path not found
    [InlineData(64)]      // connection terminated mid-session
    [InlineData(233)]     // no process on the other end of the pipe
    [InlineData(1205)]    // deadlock victim
    [InlineData(10053)]   // transport-level abort
    [InlineData(10054)]   // connection reset by peer
    [InlineData(10060)]   // connection timed out
    [InlineData(40197)]   // Azure SQL service error, retry
    [InlineData(40501)]   // Azure SQL service busy
    [InlineData(40613)]   // Azure SQL database unavailable
    [InlineData(49918)]   // Azure SQL cannot process request (throttle)
    [InlineData(49919)]   // Azure SQL too many create/update operations
    [InlineData(49920)]   // Azure SQL too many operations (throttle)
    public void IsTransient_KnownTransientNumber_ReturnsTrue(int errorNumber)
    {
        Assert.True(SqlErrorClassifier.IsTransient(errorNumber));
    }

    // Constraint, syntax, and permission errors are permanent — retrying the
    // identical statement cannot succeed and may cause duplicate side effects.
    [Theory]
    [InlineData(547)]   // constraint violation (FK/CHECK)
    [InlineData(2627)]  // primary-key / unique constraint violation
    [InlineData(2601)]  // duplicate key in a unique index
    [InlineData(102)]   // incorrect syntax
    [InlineData(156)]   // incorrect syntax near a keyword
    [InlineData(207)]   // invalid column name
    [InlineData(208)]   // invalid object name
    [InlineData(229)]   // permission denied on object
    [InlineData(230)]   // permission denied on column
    [InlineData(262)]   // permission denied (CREATE etc.)
    public void IsTransient_KnownPermanentNumber_ReturnsFalse(int errorNumber)
    {
        Assert.False(SqlErrorClassifier.IsTransient(errorNumber));
    }

    [Theory]
    [InlineData(0)]       // no error number captured
    [InlineData(99999)]   // unknown / undocumented number
    [InlineData(12345)]
    [InlineData(int.MaxValue)]
    public void IsTransient_UnknownNumber_DefaultsToPermanent(int errorNumber)
    {
        // Fail-fast is the safer default: an unrecognised error number must NOT
        // be silently retried forever. Unknown => permanent => false.
        Assert.False(SqlErrorClassifier.IsTransient(errorNumber));
    }

    // ── M2.3 (#7) code-review fix: IsTransient(Exception) — a live DB outage does
    //    not always surface as a SqlException. Transport/connection/timeout/driver
    //    exception types are transient (buffer+retry), mirroring the HTTP path's
    //    ErrorClassifier.IsTransient(Exception). ──

    public static IEnumerable<object[]> TransientExceptionTypes()
    {
        yield return new object[] { new InvalidOperationException("connection not open") };
        yield return new object[] { new System.IO.IOException("transport reset") };
        yield return new object[] { new System.Net.Sockets.SocketException(10060) };
        yield return new object[] { new TimeoutException("timed out") };
        yield return new object[] { new TaskCanceledException("driver-level cancellation") };
        // Any DbException that is NOT a SqlException is a driver/transport error.
        yield return new object[] { new NonSqlDbException("provider transport error") };
    }

    [Theory]
    [MemberData(nameof(TransientExceptionTypes))]
    public void IsTransient_Exception_TrueForTransportTypes(Exception ex)
    {
        Assert.True(SqlErrorClassifier.IsTransient(ex));
    }

    [Fact]
    public void IsTransient_Exception_FalseForUnexpectedType()
    {
        // Authoring bugs are NOT a DB outage — they must propagate, exactly as the
        // HTTP path lets genuinely-unexpected exceptions escape its IsTransient filter.
        Assert.False(SqlErrorClassifier.IsTransient(new ArgumentException("authoring bug")));
        Assert.False(SqlErrorClassifier.IsTransient(new NullReferenceException()));
    }

    /// <summary>A concrete <see cref="DbException"/> that is not a SqlException, for the classifier unit test.</summary>
    private sealed class NonSqlDbException : DbException
    {
        public NonSqlDbException(string message) : base(message) { }
    }
}
