using System.Net.Sockets;
using Mbproxy.Options;
using Mbproxy.Proxy.Supervision;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;

namespace Mbproxy.Tests.Proxy.Supervision;

/// <summary>
/// Unit tests for <see cref="PolicyFactory"/>. No network, no simulator.
/// </summary>
[Trait("Category", "Unit")]
public sealed class PolicyFactoryTests
{
    // ── 1. BuildBackendConnect: default 3-attempt pipeline ──────────────────────────────

    [Fact]
    public async Task BuildBackendConnect_ProducesPipeline_With3Attempts_Default()
    {
        var profile = new RetryProfile { MaxAttempts = 3, BackoffMs = [100, 500, 2000] };
        var pipeline = PolicyFactory.BuildBackendConnect(profile, NullLogger.Instance);

        // The pipeline should exist and be usable.
        int attempts = 0;

        await Assert.ThrowsAnyAsync<Exception>(async () =>
            await pipeline.ExecuteAsync(async _ =>
            {
                attempts++;
                await Task.Yield();
                throw new SocketException((int)SocketError.ConnectionRefused);
            }, CancellationToken.None));

        // 3 total attempts: 1 initial + 2 retries.
        Assert.Equal(3, attempts);
    }

    // ── 2. BuildBackendConnect: delay sequence matches BackoffMs ────────────────────────

    [Fact]
    public async Task BuildBackendConnect_Backoff_MatchesConfig()
    {
        // Use a short backoff so the test runs fast.
        var profile = new RetryProfile { MaxAttempts = 3, BackoffMs = [50, 100, 200] };
        var pipeline = PolicyFactory.BuildBackendConnect(profile, NullLogger.Instance);

        // Record the wall-clock timestamps of each attempt to infer delays.
        var timestamps = new List<DateTime>();

        await Assert.ThrowsAnyAsync<Exception>(async () =>
            await pipeline.ExecuteAsync(async _ =>
            {
                timestamps.Add(DateTime.UtcNow);
                await Task.Yield();
                throw new SocketException((int)SocketError.ConnectionRefused);
            }, CancellationToken.None));

        Assert.Equal(3, timestamps.Count);

        // Delay between attempt 0→1 should be ≥ 50 ms (allow generous tolerance for CI).
        double delay01 = (timestamps[1] - timestamps[0]).TotalMilliseconds;
        Assert.True(delay01 >= 40, $"Expected delay ≥ 40ms between attempt 0 and 1, got {delay01:F0}ms");

        // Delay between attempt 1→2 should be ≥ 100 ms.
        double delay12 = (timestamps[2] - timestamps[1]).TotalMilliseconds;
        Assert.True(delay12 >= 80, $"Expected delay ≥ 80ms between attempt 1 and 2, got {delay12:F0}ms");
    }

    // ── 3. BuildListenerRecovery: initial-backoff then steady-state ──────────────────────

    [Fact]
    public async Task BuildListenerRecovery_InitialBackoffFollowedBySteadyState()
    {
        // Use very short delays so the test runs fast.
        var profile = new RecoveryProfile
        {
            InitialBackoffMs = [10, 20, 30],  // 3-element initial array
            SteadyStateMs    = 50,
        };
        var pipeline = PolicyFactory.BuildListenerRecovery(profile, NullLogger.Instance);

        // Collect the delay values Polly would use for 7 retries (more than the initial array).
        var delays = new List<TimeSpan>();
        int maxRuns = 8; // 1 initial + 7 retries

        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        int runs = 0;

        await Assert.ThrowsAnyAsync<Exception>(async () =>
            await pipeline.ExecuteAsync(async token =>
            {
                runs++;
                await Task.Yield();
                if (runs < maxRuns)
                    throw new InvalidOperationException("simulate fault");
                // Last run: cancel the token to exit cleanly.
                throw new OperationCanceledException(token);
            }, cts.Token));

        // We can't easily intercept the per-delay values from inside the pipeline,
        // so we verify the timing instead. Just assert the run count was reached
        // and that the pipeline retried until the OperationCanceledException.
        // The key contract: MaxRetryAttempts = int.MaxValue (runs indefinitely).
        Assert.True(runs >= maxRuns - 1, $"Expected at least {maxRuns - 1} runs; got {runs}");
    }

    // ── 4. BuildBackendConnect: no retry on non-transient exceptions ─────────────────────

    [Fact]
    public async Task BuildBackendConnect_NoRetry_OnNonTransientException()
    {
        var profile  = new RetryProfile { MaxAttempts = 3, BackoffMs = [100, 500, 2000] };
        var pipeline = PolicyFactory.BuildBackendConnect(profile, NullLogger.Instance);

        int attempts = 0;

        // ArgumentException is not a transient socket error — pipeline should NOT retry it.
        await Assert.ThrowsAsync<ArgumentException>(async () =>
            await pipeline.ExecuteAsync(async _ =>
            {
                attempts++;
                await Task.Yield();
                throw new ArgumentException("bad argument");
            }, CancellationToken.None));

        // Only the first attempt should have run — no retries.
        Assert.Equal(1, attempts);
    }

    // ── 5. BuildBackendConnect: retries ConnectionRefused but not WSAEACCES ─────────────

    [Fact]
    public async Task BuildBackendConnect_Retries_ConnectionRefused_Not_SocketError_Access()
    {
        var profile  = new RetryProfile { MaxAttempts = 2, BackoffMs = [10] };
        var pipeline = PolicyFactory.BuildBackendConnect(profile, NullLogger.Instance);

        // SocketError.AccessDenied is NOT in the retryable set.
        int attempts = 0;

        await Assert.ThrowsAsync<SocketException>(async () =>
            await pipeline.ExecuteAsync(async _ =>
            {
                attempts++;
                await Task.Yield();
                throw new SocketException((int)SocketError.AccessDenied);
            }, CancellationToken.None));

        Assert.Equal(1, attempts); // Should not retry AccessDenied.

        // Now verify ConnectionRefused IS retried.
        int refusedAttempts = 0;
        await Assert.ThrowsAsync<SocketException>(async () =>
            await pipeline.ExecuteAsync(async _ =>
            {
                refusedAttempts++;
                await Task.Yield();
                throw new SocketException((int)SocketError.ConnectionRefused);
            }, CancellationToken.None));

        Assert.Equal(2, refusedAttempts); // 1 initial + 1 retry (MaxAttempts=2).
    }
}
