using System.Collections.Generic;
using System.Collections.Immutable;
using System.Net;
using System.Net.Http;
using System.Text;
using FluentAssertions;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Excititor.Connectors.Abstractions;
using StellaOps.Excititor.Connectors.RedHat.CSAF.Configuration;
using StellaOps.Excititor.Connectors.RedHat.CSAF.Metadata;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Storage.Mongo;

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

public sealed class RedHatCsafConnectorTests
{
    private static readonly VexConnectorDescriptor Descriptor = new("excititor:redhat", VexProviderKind.Distro, "Red Hat CSAF");

    [Fact]
    public async Task FetchAsync_EmitsDocumentsAfterSince()
    {
        var metadata = """
            {
              "metadata": {
                "provider": { "name": "Red Hat Product Security" }
              },
              "distributions": [
                { "directory": "https://example.com/security/data/csaf/v2/advisories/" }
              ],
              "rolie": {
                "feeds": [
                  { "url": "https://example.com/security/data/csaf/v2/advisories/rolie/feed.atom" }
                ]
              }
            }
            """;

        var feed = """
            <feed xmlns="http://www.w3.org/2005/Atom">
              <entry>
                <id>urn:redhat:1</id>
                <updated>2025-10-16T10:00:00Z</updated>
                <link href="https://example.com/doc1.json" rel="enclosure" />
            </entry>
            <entry>
                <id>urn:redhat:2</id>
                <updated>2025-10-17T10:00:00Z</updated>
                <link href="https://example.com/doc2.json" rel="enclosure" />
              </entry>
            </feed>
            """;

        var handler = TestHttpMessageHandler.Create(
            request => Response(HttpStatusCode.OK, metadata, "application/json"),
            request => Response(HttpStatusCode.OK, feed, "application/atom+xml"),
            request => Response(HttpStatusCode.OK, "{ \"csaf\": 1 }", "application/json"));

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

        var factory = new SingleClientHttpClientFactory(httpClient);
        var cache = new MemoryCache(new MemoryCacheOptions());
        var options = Options.Create(new RedHatConnectorOptions());
        var metadataLoader = new RedHatProviderMetadataLoader(factory, cache, options, NullLogger<RedHatProviderMetadataLoader>.Instance);
        var stateRepository = new InMemoryConnectorStateRepository();
        var connector = new RedHatCsafConnector(Descriptor, metadataLoader, factory, stateRepository, NullLogger<RedHatCsafConnector>.Instance, TimeProvider.System);

        var rawSink = new CapturingRawSink();
        var context = new VexConnectorContext(
            new DateTimeOffset(2025, 10, 16, 12, 0, 0, TimeSpan.Zero),
            VexConnectorSettings.Empty,
            rawSink,
            new NoopSignatureVerifier(),
            new NoopNormalizerRouter(),
            new ServiceCollection().BuildServiceProvider());

        var results = new List<VexRawDocument>();
        await foreach (var document in connector.FetchAsync(context, CancellationToken.None))
        {
            results.Add(document);
        }

        Assert.Single(results);
        Assert.Single(rawSink.Documents);
        Assert.Equal("https://example.com/doc2.json", results[0].SourceUri.ToString());
        Assert.Equal("https://example.com/doc2.json", rawSink.Documents[0].SourceUri.ToString());
        Assert.Equal(3, handler.CallCount);
        stateRepository.State.Should().NotBeNull();
        stateRepository.State!.LastUpdated.Should().Be(new DateTimeOffset(2025, 10, 17, 10, 0, 0, TimeSpan.Zero));
        stateRepository.State.DocumentDigests.Should().HaveCount(1);
    }

    [Fact]
    public async Task FetchAsync_UsesStateToSkipDuplicateDocuments()
    {
        var metadata = """
            {
              "metadata": {
                "provider": { "name": "Red Hat Product Security" }
              },
              "distributions": [
                { "directory": "https://example.com/security/data/csaf/v2/advisories/" }
              ],
              "rolie": {
                "feeds": [
                  { "url": "https://example.com/security/data/csaf/v2/advisories/rolie/feed.atom" }
                ]
              }
            }
            """;

        var feed = """
            <feed xmlns="http://www.w3.org/2005/Atom">
              <entry>
                <id>urn:redhat:1</id>
                <updated>2025-10-17T10:00:00Z</updated>
                <link href="https://example.com/doc1.json" rel="enclosure" />
              </entry>
            </feed>
            """;

        var handler1 = TestHttpMessageHandler.Create(
            _ => Response(HttpStatusCode.OK, metadata, "application/json"),
            _ => Response(HttpStatusCode.OK, feed, "application/atom+xml"),
            _ => Response(HttpStatusCode.OK, "{ \"csaf\": 1 }", "application/json"));

        var stateRepository = new InMemoryConnectorStateRepository();
        await ExecuteFetchAsync(handler1, stateRepository);

        stateRepository.State.Should().NotBeNull();
        var previousState = stateRepository.State!;

        var handler2 = TestHttpMessageHandler.Create(
            _ => Response(HttpStatusCode.OK, metadata, "application/json"),
            _ => Response(HttpStatusCode.OK, feed, "application/atom+xml"),
            _ => Response(HttpStatusCode.OK, "{ \"csaf\": 1 }", "application/json"));

        var (results, rawSink) = await ExecuteFetchAsync(handler2, stateRepository);

        results.Should().BeEmpty();
        rawSink.Documents.Should().BeEmpty();
        stateRepository.State!.DocumentDigests.Should().Equal(previousState.DocumentDigests);
    }

    private static HttpResponseMessage Response(HttpStatusCode statusCode, string content, string contentType)
        => new(statusCode)
        {
            Content = new StringContent(content, Encoding.UTF8, contentType),
        };

    private sealed class CapturingRawSink : IVexRawDocumentSink
    {
        public List<VexRawDocument> Documents { get; } = new();

        public ValueTask StoreAsync(VexRawDocument document, CancellationToken cancellationToken)
        {
            Documents.Add(document);
            return ValueTask.CompletedTask;
        }
    }

    private sealed class NoopSignatureVerifier : IVexSignatureVerifier
    {
        public ValueTask<VexSignatureMetadata?> VerifyAsync(VexRawDocument document, CancellationToken cancellationToken)
            => ValueTask.FromResult<VexSignatureMetadata?>(null);
    }

    private sealed class NoopNormalizerRouter : IVexNormalizerRouter
    {
        public ValueTask<VexClaimBatch> NormalizeAsync(VexRawDocument document, CancellationToken cancellationToken)
            => ValueTask.FromResult(new VexClaimBatch(document, ImmutableArray<VexClaim>.Empty, ImmutableDictionary<string, string>.Empty));
    }

    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<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 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);
        }
    }

    private static async Task<(List<VexRawDocument> Documents, CapturingRawSink Sink)> ExecuteFetchAsync(
        TestHttpMessageHandler handler,
        InMemoryConnectorStateRepository stateRepository)
    {
        var httpClient = new HttpClient(handler)
        {
            BaseAddress = new Uri("https://example.com/"),
        };

        var factory = new SingleClientHttpClientFactory(httpClient);
        var cache = new MemoryCache(new MemoryCacheOptions());
        var options = Options.Create(new RedHatConnectorOptions());
        var metadataLoader = new RedHatProviderMetadataLoader(factory, cache, options, NullLogger<RedHatProviderMetadataLoader>.Instance);
        var connector = new RedHatCsafConnector(Descriptor, metadataLoader, factory, stateRepository, NullLogger<RedHatCsafConnector>.Instance, TimeProvider.System);

        var rawSink = new CapturingRawSink();
        var context = new VexConnectorContext(
            null,
            VexConnectorSettings.Empty,
            rawSink,
            new NoopSignatureVerifier(),
            new NoopNormalizerRouter(),
            new ServiceCollection().BuildServiceProvider());

        var documents = new List<VexRawDocument>();
        await foreach (var document in connector.FetchAsync(context, CancellationToken.None))
        {
            documents.Add(document);
        }

        return (documents, rawSink);
    }

    private sealed class InMemoryConnectorStateRepository : IVexConnectorStateRepository
    {
        public VexConnectorState? State { get; private set; }

        public ValueTask<VexConnectorState?> GetAsync(string connectorId, CancellationToken cancellationToken)
        {
            if (State is not null && string.Equals(State.ConnectorId, connectorId, StringComparison.OrdinalIgnoreCase))
            {
                return ValueTask.FromResult<VexConnectorState?>(State);
            }

            return ValueTask.FromResult<VexConnectorState?>(null);
        }

        public ValueTask SaveAsync(VexConnectorState state, CancellationToken cancellationToken)
        {
            State = state;
            return ValueTask.CompletedTask;
        }
    }
}
