using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Excititor.Connectors.RedHat.CSAF.Configuration;
using StellaOps.Excititor.Connectors.RedHat.CSAF.Metadata;
using System.IO.Abstractions.TestingHelpers;

namespace StellaOps.Excititor.Connectors.RedHat.CSAF.Tests.Metadata;

public sealed class RedHatProviderMetadataLoaderTests
{
    private const string SampleJson = """
        {
          "metadata": {
            "provider": {
              "name": "Red Hat Product Security"
            }
          },
          "distributions": [
            { "directory": "https://access.redhat.com/security/data/csaf/v2/advisories/" }
          ],
          "rolie": {
            "feeds": [
              { "url": "https://access.redhat.com/security/data/csaf/v2/advisories/rolie/feed.atom" }
            ]
          }
        }
        """;

    [Fact]
    public async Task LoadAsync_FetchesMetadataAndCaches()
    {
        var handler = TestHttpMessageHandler.RespondWith(_ =>
        {
            var response = new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new StringContent(SampleJson, Encoding.UTF8, "application/json"),
            };
            response.Headers.ETag = new EntityTagHeaderValue("\"abc\"");
            return response;
        });

        var httpClient = new HttpClient(handler)
        {
            BaseAddress = new Uri("https://access.redhat.com/"),
        };

        var factory = new SingleClientHttpClientFactory(httpClient);
        var cache = new MemoryCache(new MemoryCacheOptions());
        var fileSystem = new MockFileSystem();
        var options = new RedHatConnectorOptions
        {
            MetadataUri = new Uri("https://access.redhat.com/.well-known/csaf/provider-metadata.json"),
            OfflineSnapshotPath = fileSystem.Path.Combine("/offline", "redhat-provider.json"),
            CosignIssuer = "https://sigstore.dev/redhat",
            CosignIdentityPattern = "^spiffe://redhat/.+$",
        };
        options.PgpFingerprints.Add("A1B2C3D4E5F6");
        options.Validate(fileSystem);

        var loader = new RedHatProviderMetadataLoader(factory, cache, Options.Create(options), NullLogger<RedHatProviderMetadataLoader>.Instance, fileSystem);

        var result = await loader.LoadAsync(CancellationToken.None);

        Assert.Equal("Red Hat Product Security", result.Provider.DisplayName);
        Assert.False(result.FromCache);
        Assert.False(result.FromOfflineSnapshot);
        Assert.Single(result.Provider.BaseUris);
        Assert.Equal("https://access.redhat.com/security/data/csaf/v2/advisories/", result.Provider.BaseUris[0].ToString());
        Assert.Equal("https://access.redhat.com/.well-known/csaf/provider-metadata.json", result.Provider.Discovery.WellKnownMetadata?.ToString());
        Assert.Equal("https://access.redhat.com/security/data/csaf/v2/advisories/rolie/feed.atom", result.Provider.Discovery.RolIeService?.ToString());
        Assert.Equal(1.0, result.Provider.Trust.Weight);
        Assert.NotNull(result.Provider.Trust.Cosign);
        Assert.Equal("https://sigstore.dev/redhat", result.Provider.Trust.Cosign!.Issuer);
        Assert.Equal("^spiffe://redhat/.+$", result.Provider.Trust.Cosign.IdentityPattern);
        Assert.Contains("A1B2C3D4E5F6", result.Provider.Trust.PgpFingerprints);
        Assert.True(fileSystem.FileExists(options.OfflineSnapshotPath));
        Assert.Equal(1, handler.CallCount);

        var second = await loader.LoadAsync(CancellationToken.None);
        Assert.True(second.FromCache);
        Assert.False(second.FromOfflineSnapshot);
        Assert.Equal(1, handler.CallCount);
    }

    [Fact]
    public async Task LoadAsync_UsesOfflineSnapshotWhenPreferred()
    {
        var handler = TestHttpMessageHandler.RespondWith(_ => throw new InvalidOperationException("HTTP should not be called"));

        var httpClient = new HttpClient(handler);
        var factory = new SingleClientHttpClientFactory(httpClient);
        var cache = new MemoryCache(new MemoryCacheOptions());

        var fileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
        {
            ["/snapshots/redhat.json"] = new MockFileData(SampleJson),
        });

        var options = new RedHatConnectorOptions
        {
            MetadataUri = new Uri("https://access.redhat.com/.well-known/csaf/provider-metadata.json"),
            OfflineSnapshotPath = "/snapshots/redhat.json",
            PreferOfflineSnapshot = true,
            PersistOfflineSnapshot = false,
        };
        options.Validate(fileSystem);

        var loader = new RedHatProviderMetadataLoader(factory, cache, Options.Create(options), NullLogger<RedHatProviderMetadataLoader>.Instance, fileSystem);

        var result = await loader.LoadAsync(CancellationToken.None);

        Assert.Equal("Red Hat Product Security", result.Provider.DisplayName);
        Assert.False(result.FromCache);
        Assert.True(result.FromOfflineSnapshot);
        Assert.Equal(0, handler.CallCount);

        var second = await loader.LoadAsync(CancellationToken.None);
        Assert.True(second.FromCache);
        Assert.True(second.FromOfflineSnapshot);
        Assert.Equal(0, handler.CallCount);
    }

    [Fact]
    public async Task LoadAsync_UsesETagForConditionalRequest()
    {
        var handler = TestHttpMessageHandler.Create(
            _ =>
            {
                var response = new HttpResponseMessage(HttpStatusCode.OK)
                {
                    Content = new StringContent(SampleJson, Encoding.UTF8, "application/json"),
                };
                response.Headers.ETag = new EntityTagHeaderValue("\"abc\"");
                return response;
            },
            request =>
            {
                Assert.Contains(request.Headers.IfNoneMatch, etag => etag.Tag == "\"abc\"");
                return new HttpResponseMessage(HttpStatusCode.NotModified);
            });

        var httpClient = new HttpClient(handler);
        var factory = new SingleClientHttpClientFactory(httpClient);
        var cache = new MemoryCache(new MemoryCacheOptions());
        var fileSystem = new MockFileSystem();
        var options = new RedHatConnectorOptions
        {
            MetadataUri = new Uri("https://access.redhat.com/.well-known/csaf/provider-metadata.json"),
            OfflineSnapshotPath = "/offline/redhat.json",
            MetadataCacheDuration = TimeSpan.FromMinutes(1),
        };
        options.Validate(fileSystem);

        var loader = new RedHatProviderMetadataLoader(factory, cache, Options.Create(options), NullLogger<RedHatProviderMetadataLoader>.Instance, fileSystem);

        var first = await loader.LoadAsync(CancellationToken.None);
        Assert.False(first.FromCache);
        Assert.False(first.FromOfflineSnapshot);

        Assert.True(cache.TryGetValue(RedHatProviderMetadataLoader.CacheKey, out var entryObj));
        Assert.NotNull(entryObj);

        var entryType = entryObj!.GetType();
        var provider = entryType.GetProperty("Provider")!.GetValue(entryObj);
        var fetchedAt = entryType.GetProperty("FetchedAt")!.GetValue(entryObj);
        var etag = entryType.GetProperty("ETag")!.GetValue(entryObj);
        var fromOffline = entryType.GetProperty("FromOffline")!.GetValue(entryObj);

        var expiredEntry = Activator.CreateInstance(entryType, provider, fetchedAt, DateTimeOffset.UtcNow - TimeSpan.FromSeconds(1), etag, fromOffline);
        cache.Set(RedHatProviderMetadataLoader.CacheKey, expiredEntry!, new MemoryCacheEntryOptions
        {
            AbsoluteExpiration = DateTimeOffset.UtcNow + TimeSpan.FromMinutes(1),
        });

        var second = await loader.LoadAsync(CancellationToken.None);

        var third = await loader.LoadAsync(CancellationToken.None);
        Assert.True(third.FromCache);

        Assert.Equal(2, handler.CallCount);
    }

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

        public SingleClientHttpClientFactory(HttpClient client)
        {
            _client = client ?? throw new ArgumentNullException(nameof(client));
        }

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

    private sealed class TestHttpMessageHandler : HttpMessageHandler
    {
        private readonly Queue<Func<HttpRequestMessage, HttpResponseMessage>> _responders;

        private TestHttpMessageHandler(IEnumerable<Func<HttpRequestMessage, HttpResponseMessage>> responders)
        {
            _responders = new Queue<Func<HttpRequestMessage, HttpResponseMessage>>(responders);
        }

        public int CallCount { get; private set; }

        public static TestHttpMessageHandler RespondWith(Func<HttpRequestMessage, HttpResponseMessage> responder)
            => new(new[] { responder });

        public static TestHttpMessageHandler Create(params Func<HttpRequestMessage, HttpResponseMessage>[] responders)
            => new(responders);

        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            CallCount++;
            if (_responders.Count == 0)
            {
                throw new InvalidOperationException("No responder configured for request.");
            }

            var responder = _responders.Count > 1
                ? _responders.Dequeue()
                : _responders.Peek();

            var response = responder(request);
            response.RequestMessage = request;
            return Task.FromResult(response);
        }
    }
}
