using System.Net;
using System.Net.Http;
using System.Text;
using FluentAssertions;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using NSubstitute;
using StellaOps.Excititor.Connectors.MSRC.CSAF.Authentication;
using StellaOps.Excititor.Connectors.MSRC.CSAF.Configuration;
using System.IO.Abstractions.TestingHelpers;
using Xunit;

namespace StellaOps.Excititor.Connectors.MSRC.CSAF.Tests.Authentication;

public sealed class MsrcTokenProviderTests
{
    [Fact]
    public async Task GetAccessTokenAsync_CachesUntilExpiry()
    {
        var handler = new TestHttpMessageHandler(new[]
        {
            CreateTokenResponse("token-1"),
            CreateTokenResponse("token-2"),
        });
        var client = new HttpClient(handler) { BaseAddress = new Uri("https://login.microsoftonline.com/") };
        var factory = new SingleClientHttpClientFactory(client);
        var cache = new MemoryCache(new MemoryCacheOptions());
        var fileSystem = new MockFileSystem();
        var options = Options.Create(new MsrcConnectorOptions
        {
            TenantId = "contoso.onmicrosoft.com",
            ClientId = "client-id",
            ClientSecret = "secret",
        });

        var timeProvider = new AdjustableTimeProvider();
        var provider = new MsrcTokenProvider(factory, cache, fileSystem, options, NullLogger<MsrcTokenProvider>.Instance, timeProvider);

        var first = await provider.GetAccessTokenAsync(CancellationToken.None);
        first.Value.Should().Be("token-1");
        handler.InvocationCount.Should().Be(1);

        var second = await provider.GetAccessTokenAsync(CancellationToken.None);
        second.Value.Should().Be("token-1");
        handler.InvocationCount.Should().Be(1);
    }

    [Fact]
    public async Task GetAccessTokenAsync_RefreshesWhenExpired()
    {
        var handler = new TestHttpMessageHandler(new[]
        {
            CreateTokenResponse("token-1", expiresIn: 120),
            CreateTokenResponse("token-2", expiresIn: 3600),
        });
        var client = new HttpClient(handler) { BaseAddress = new Uri("https://login.microsoftonline.com/") };
        var factory = new SingleClientHttpClientFactory(client);
        var cache = new MemoryCache(new MemoryCacheOptions());
        var fileSystem = new MockFileSystem();
        var options = Options.Create(new MsrcConnectorOptions
        {
            TenantId = "contoso.onmicrosoft.com",
            ClientId = "client-id",
            ClientSecret = "secret",
            ExpiryLeewaySeconds = 60,
        });

        var timeProvider = new AdjustableTimeProvider();
        var provider = new MsrcTokenProvider(factory, cache, fileSystem, options, NullLogger<MsrcTokenProvider>.Instance, timeProvider);

        var first = await provider.GetAccessTokenAsync(CancellationToken.None);
        first.Value.Should().Be("token-1");
        handler.InvocationCount.Should().Be(1);

        timeProvider.Advance(TimeSpan.FromMinutes(2));
        var second = await provider.GetAccessTokenAsync(CancellationToken.None);
        second.Value.Should().Be("token-2");
        handler.InvocationCount.Should().Be(2);
    }

    [Fact]
    public async Task GetAccessTokenAsync_OfflineStaticToken()
    {
        var factory = Substitute.For<IHttpClientFactory>();
        var cache = new MemoryCache(new MemoryCacheOptions());
        var fileSystem = new MockFileSystem();
        var options = Options.Create(new MsrcConnectorOptions
        {
            PreferOfflineToken = true,
            StaticAccessToken = "offline-token",
        });

        var provider = new MsrcTokenProvider(factory, cache, fileSystem, options, NullLogger<MsrcTokenProvider>.Instance);
        var token = await provider.GetAccessTokenAsync(CancellationToken.None);
        token.Value.Should().Be("offline-token");
        token.ExpiresAt.Should().Be(DateTimeOffset.MaxValue);
    }

    [Fact]
    public async Task GetAccessTokenAsync_OfflineFileToken()
    {
        var factory = Substitute.For<IHttpClientFactory>();
        var cache = new MemoryCache(new MemoryCacheOptions());
        var fileSystem = new MockFileSystem();
        var offlinePath = fileSystem.Path.Combine("/tokens", "msrc.txt");
        fileSystem.AddFile(offlinePath, new MockFileData("file-token"));

        var options = Options.Create(new MsrcConnectorOptions
        {
            PreferOfflineToken = true,
            OfflineTokenPath = offlinePath,
        });

        var provider = new MsrcTokenProvider(factory, cache, fileSystem, options, NullLogger<MsrcTokenProvider>.Instance);
        var token = await provider.GetAccessTokenAsync(CancellationToken.None);
        token.Value.Should().Be("file-token");
        token.ExpiresAt.Should().Be(DateTimeOffset.MaxValue);
    }

    private static HttpResponseMessage CreateTokenResponse(string token, int expiresIn = 3600)
    {
        var json = $"{{\"access_token\":\"{token}\",\"token_type\":\"Bearer\",\"expires_in\":{expiresIn}}}";
        return new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent(json, Encoding.UTF8, "application/json"),
        };
    }

    private sealed class AdjustableTimeProvider : TimeProvider
    {
        private DateTimeOffset _now = DateTimeOffset.UtcNow;

        public override DateTimeOffset GetUtcNow() => _now;

        public void Advance(TimeSpan span) => _now = _now.Add(span);
    }

    private sealed class SingleClientHttpClientFactory : IHttpClientFactory
    {
        private readonly HttpClient _client;

        public SingleClientHttpClientFactory(HttpClient client)
        {
            _client = client;
        }

        public HttpClient CreateClient(string name) => _client;
    }

    private sealed class TestHttpMessageHandler : HttpMessageHandler
    {
        private readonly Queue<HttpResponseMessage> _responses;

        public TestHttpMessageHandler(IEnumerable<HttpResponseMessage> responses)
        {
            _responses = new Queue<HttpResponseMessage>(responses);
        }

        public int InvocationCount { get; private set; }

        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            InvocationCount++;
            if (_responses.Count == 0)
            {
                return Task.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError)
                {
                    Content = new StringContent("no responses remaining"),
                });
            }

            return Task.FromResult(_responses.Dequeue());
        }
    }
}
