using Microsoft.Extensions.Logging.Abstractions;

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

/// <summary>
/// Host-010: startup preconditions (database migration) must tolerate a database
/// that is briefly unavailable at boot — common when an app container and its DB
/// container start together — via a bounded retry with backoff.
/// </summary>
public class StartupRetryTests
{
    [Fact]
    public async Task ExecuteWithRetry_SucceedsFirstTry_RunsOnce()
    {
        var attempts = 0;
        await StartupRetry.ExecuteWithRetryAsync(
            "test-op",
            () => { attempts++; return Task.CompletedTask; },
            maxAttempts: 5,
            initialDelay: TimeSpan.FromMilliseconds(1),
            NullLogger.Instance);

        Assert.Equal(1, attempts);
    }

    [Fact]
    public async Task ExecuteWithRetry_TransientFailures_RetriesUntilSuccess()
    {
        var attempts = 0;
        await StartupRetry.ExecuteWithRetryAsync(
            "test-op",
            () =>
            {
                attempts++;
                if (attempts < 3)
                    throw new InvalidOperationException("db not ready");
                return Task.CompletedTask;
            },
            maxAttempts: 5,
            initialDelay: TimeSpan.FromMilliseconds(1),
            NullLogger.Instance);

        Assert.Equal(3, attempts);
    }

    [Fact]
    public async Task ExecuteWithRetry_ExhaustsAttempts_RethrowsLastException()
    {
        var attempts = 0;
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
            StartupRetry.ExecuteWithRetryAsync(
                "test-op",
                () =>
                {
                    attempts++;
                    throw new InvalidOperationException($"failure {attempts}");
                },
                maxAttempts: 3,
                initialDelay: TimeSpan.FromMilliseconds(1),
                NullLogger.Instance,
                isTransient: _ => true));

        Assert.Equal(3, attempts);
        Assert.Equal("failure 3", ex.Message);
    }

    [Fact]
    public async Task ExecuteWithRetry_NonTransientFailure_RethrowsAfterSingleAttempt()
    {
        // Host-015: a permanent failure (e.g. a schema-version mismatch) must NOT be
        // retried — retrying it cannot succeed and only delays the fatal exit by
        // minutes. The isTransient predicate classifies it as non-retryable, so the
        // operation runs exactly once before the exception propagates.
        var attempts = 0;
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
            StartupRetry.ExecuteWithRetryAsync(
                "test-op",
                () =>
                {
                    attempts++;
                    throw new InvalidOperationException("permanent schema mismatch");
                },
                maxAttempts: 8,
                initialDelay: TimeSpan.FromMilliseconds(1),
                NullLogger.Instance,
                isTransient: _ => false));

        Assert.Equal(1, attempts);
        Assert.Equal("permanent schema mismatch", ex.Message);
    }

    [Fact]
    public async Task ExecuteWithRetry_TransientThenPermanent_StopsAtPermanent()
    {
        // A transient fault is retried; a subsequent permanent fault is not.
        var attempts = 0;
        await Assert.ThrowsAsync<InvalidOperationException>(() =>
            StartupRetry.ExecuteWithRetryAsync(
                "test-op",
                () =>
                {
                    attempts++;
                    if (attempts == 1)
                        throw new TimeoutException("transient");
                    throw new InvalidOperationException("permanent");
                },
                maxAttempts: 8,
                initialDelay: TimeSpan.FromMilliseconds(1),
                NullLogger.Instance,
                isTransient: e => e is TimeoutException));

        // 1 transient (retried) + 1 permanent (not retried) = 2.
        Assert.Equal(2, attempts);
    }
}
