using System;
using System.Collections.Generic;
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 StellaOpsTokenClientTests
{
    [Fact]
    public async Task RequestPasswordToken_ReturnsResultAndCaches()
    {
        var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-02-01T00:00:00Z"));
        var responses = new Queue<HttpResponseMessage>();
        responses.Enqueue(CreateJsonResponse("{\"token_endpoint\":\"https://authority.test/connect/token\",\"jwks_uri\":\"https://authority.test/jwks\"}"));
        responses.Enqueue(CreateJsonResponse("{\"access_token\":\"abc\",\"token_type\":\"Bearer\",\"expires_in\":120,\"scope\":\"feedser.jobs.trigger\"}"));
        responses.Enqueue(CreateJsonResponse("{\"keys\":[]}"));

        var handler = new StubHttpMessageHandler((request, cancellationToken) =>
        {
            Assert.True(responses.Count > 0, $"Unexpected request {request.Method} {request.RequestUri}");
            return Task.FromResult(responses.Dequeue());
        });

        var httpClient = new HttpClient(handler);

        var options = new StellaOpsAuthClientOptions
        {
            Authority = "https://authority.test",
            ClientId = "cli"
        };
        options.DefaultScopes.Add("feedser.jobs.trigger");
        options.Validate();

        var optionsMonitor = new TestOptionsMonitor<StellaOpsAuthClientOptions>(options);
        var cache = new InMemoryTokenCache(timeProvider, TimeSpan.FromSeconds(5));
        var discoveryCache = new StellaOpsDiscoveryCache(httpClient, optionsMonitor, timeProvider);
        var jwksCache = new StellaOpsJwksCache(httpClient, discoveryCache, optionsMonitor, timeProvider);
        var client = new StellaOpsTokenClient(httpClient, discoveryCache, jwksCache, optionsMonitor, cache, timeProvider, NullLogger<StellaOpsTokenClient>.Instance);

        var result = await client.RequestPasswordTokenAsync("user", "pass");

        Assert.Equal("abc", result.AccessToken);
        Assert.Contains("feedser.jobs.trigger", result.Scopes);

        await client.CacheTokenAsync("key", result.ToCacheEntry());
        var cached = await client.GetCachedTokenAsync("key");
        Assert.NotNull(cached);
        Assert.Equal("abc", cached!.AccessToken);

        var jwks = await client.GetJsonWebKeySetAsync();
        Assert.Empty(jwks.Keys);
    }

    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<TOptions> : IOptionsMonitor<TOptions>
        where TOptions : class
    {
        private readonly TOptions value;

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

        public TOptions CurrentValue => value;

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

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

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