using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Auth.Client;
using Xunit;

namespace StellaOps.Auth.Client.Tests;

public class StellaOpsDiscoveryCacheTests
{
    [Fact]
    public async Task GetAsync_UsesOfflineFallbackWithinTolerance()
    {
        var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-01-01T00:00:00Z"));
        var callCount = 0;
        var handler = new StubHttpMessageHandler((request, _) =>
        {
            callCount++;

            if (callCount == 1)
            {
                return Task.FromResult(CreateJsonResponse("{\"token_endpoint\":\"https://authority.test/connect/token\",\"jwks_uri\":\"https://authority.test/jwks\"}"));
            }

            throw new HttpRequestException("offline");
        });

        var httpClient = new HttpClient(handler);

        var options = new StellaOpsAuthClientOptions
        {
            Authority = "https://authority.test",
            DiscoveryCacheLifetime = TimeSpan.FromMinutes(1),
            OfflineCacheTolerance = TimeSpan.FromMinutes(5),
            AllowOfflineCacheFallback = true
        };
        options.Validate();

        var monitor = new TestOptionsMonitor<StellaOpsAuthClientOptions>(options);
        var cache = new StellaOpsDiscoveryCache(httpClient, monitor, timeProvider, NullLogger<StellaOpsDiscoveryCache>.Instance);

        var configuration = await cache.GetAsync(CancellationToken.None);
        Assert.Equal(new Uri("https://authority.test/connect/token"), configuration.TokenEndpoint);

        timeProvider.Advance(TimeSpan.FromMinutes(1) + TimeSpan.FromSeconds(5));

        configuration = await cache.GetAsync(CancellationToken.None);
        Assert.Equal(new Uri("https://authority.test/connect/token"), configuration.TokenEndpoint);
        Assert.Equal(2, callCount);

        var offlineExpiry = GetOfflineExpiry(cache);
        Assert.True(offlineExpiry > timeProvider.GetUtcNow());

        timeProvider.Advance(options.OfflineCacheTolerance + TimeSpan.FromSeconds(1));

        Assert.True(offlineExpiry < timeProvider.GetUtcNow());

        HttpRequestException? exception = null;
        try
        {
            await cache.GetAsync(CancellationToken.None);
        }
        catch (HttpRequestException ex)
        {
            exception = ex;
        }

        Assert.NotNull(exception);
        Assert.Equal(3, callCount);
    }

    private static HttpResponseMessage CreateJsonResponse(string json)
    {
        return new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent(json)
            {
                Headers = { ContentType = new MediaTypeHeaderValue("application/json") }
            }
        };
    }

    private sealed class StubHttpMessageHandler : HttpMessageHandler
    {
        private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> responder;

        public StubHttpMessageHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> responder)
        {
            this.responder = responder;
        }

        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
            => responder(request, cancellationToken);
    }

    private sealed class TestOptionsMonitor<T> : IOptionsMonitor<T>
        where T : class
    {
        private readonly T value;

        public TestOptionsMonitor(T value)
        {
            this.value = value;
        }

        public T CurrentValue => value;

        public T Get(string? name) => value;

        public IDisposable OnChange(Action<T, string> listener) => NullDisposable.Instance;

        private sealed class NullDisposable : IDisposable
        {
            public static NullDisposable Instance { get; } = new();

            public void Dispose()
            {
            }
        }
    }

    private static DateTimeOffset GetOfflineExpiry(StellaOpsDiscoveryCache cache)
    {
        var field = typeof(StellaOpsDiscoveryCache).GetField("offlineExpiresAt", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
        Assert.NotNull(field);
        return (DateTimeOffset)field!.GetValue(cache)!;
    }
}
