using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Text;
using FluentAssertions;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Excititor.Connectors.Oracle.CSAF.Configuration;
using StellaOps.Excititor.Connectors.Oracle.CSAF.Metadata;
using System.IO.Abstractions.TestingHelpers;
using Xunit;
using System.Threading;

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

public sealed class OracleCatalogLoaderTests
{
    private const string SampleCatalog = """
        {
          "generated": "2025-09-30T18:00:00Z",
          "catalog": [
            {
              "id": "CPU2025Oct",
              "title": "Oracle Critical Patch Update Advisory - October 2025",
              "published": "2025-10-15T00:00:00Z",
              "revision": "2025-10-15T00:00:00Z",
              "document": {
                "url": "https://updates.oracle.com/cpu/2025-10/cpu2025oct.json",
                "sha256": "abc123",
                "size": 1024
              },
              "products": ["Oracle Database", "Java SE"]
            }
          ],
          "schedule": [
            { "window": "2025-Oct", "releaseDate": "2025-10-15T00:00:00Z" }
          ]
        }
        """;

    private const string SampleCalendar = """
        {
          "cpuWindows": [
            { "name": "2026-Jan", "releaseDate": "2026-01-21T00:00:00Z" }
          ]
        }
        """;

    [Fact]
    public async Task LoadAsync_FetchesAndCachesCatalog()
    {
        var handler = new TestHttpMessageHandler(new Dictionary<Uri, HttpResponseMessage>
        {
            [new Uri("https://www.oracle.com/security-alerts/cpu/csaf/catalog.json")] = CreateResponse(SampleCatalog),
            [new Uri("https://www.oracle.com/security-alerts/cpu/cal.json")] = CreateResponse(SampleCalendar),
        });
        var client = new HttpClient(handler);
        var factory = new SingleClientHttpClientFactory(client);
        var cache = new MemoryCache(new MemoryCacheOptions());
        var fileSystem = new MockFileSystem();
        var loader = new OracleCatalogLoader(factory, cache, fileSystem, NullLogger<OracleCatalogLoader>.Instance, new AdjustableTimeProvider());

        var options = new OracleConnectorOptions
        {
            CatalogUri = new Uri("https://www.oracle.com/security-alerts/cpu/csaf/catalog.json"),
            CpuCalendarUri = new Uri("https://www.oracle.com/security-alerts/cpu/cal.json"),
            OfflineSnapshotPath = "/snapshots/oracle-catalog.json",
        };

        var result = await loader.LoadAsync(options, CancellationToken.None);
        result.Metadata.Entries.Should().HaveCount(1);
        result.Metadata.CpuSchedule.Should().Contain(r => r.Window == "2025-Oct");
        result.Metadata.CpuSchedule.Should().Contain(r => r.Window == "2026-Jan");
        result.FromCache.Should().BeFalse();
        fileSystem.FileExists(options.OfflineSnapshotPath!).Should().BeTrue();

        handler.ResetInvocationCount();
        var cached = await loader.LoadAsync(options, CancellationToken.None);
        cached.FromCache.Should().BeTrue();
        handler.InvocationCount.Should().Be(0);
    }

    [Fact]
    public async Task LoadAsync_UsesOfflineSnapshotWhenNetworkFails()
    {
        var handler = new TestHttpMessageHandler(new Dictionary<Uri, HttpResponseMessage>());
        var client = new HttpClient(handler);
        var factory = new SingleClientHttpClientFactory(client);
        var cache = new MemoryCache(new MemoryCacheOptions());
        var fileSystem = new MockFileSystem();
        var offlineJson = """
            {
              "metadata": {
                "generatedAt": "2025-09-30T18:00:00Z",
                "entries": [
                  {
                    "id": "CPU2025Oct",
                    "title": "Oracle Critical Patch Update Advisory - October 2025",
                    "documentUri": "https://updates.oracle.com/cpu/2025-10/cpu2025oct.json",
                    "publishedAt": "2025-10-15T00:00:00Z",
                    "revision": "2025-10-15T00:00:00Z",
                    "sha256": "abc123",
                    "size": 1024,
                    "products": [ "Oracle Database" ]
                  }
                ],
                "cpuSchedule": [
                  { "window": "2025-Oct", "releaseDate": "2025-10-15T00:00:00Z" }
                ]
              },
              "fetchedAt": "2025-10-01T00:00:00Z"
            }
            """;
        fileSystem.AddFile("/snapshots/oracle-catalog.json", new MockFileData(offlineJson));
        var loader = new OracleCatalogLoader(factory, cache, fileSystem, NullLogger<OracleCatalogLoader>.Instance, new AdjustableTimeProvider());

        var options = new OracleConnectorOptions
        {
            CatalogUri = new Uri("https://www.oracle.com/security-alerts/cpu/csaf/catalog.json"),
            OfflineSnapshotPath = "/snapshots/oracle-catalog.json",
            PreferOfflineSnapshot = true,
        };

        var result = await loader.LoadAsync(options, CancellationToken.None);
        result.FromOfflineSnapshot.Should().BeTrue();
        result.Metadata.Entries.Should().NotBeEmpty();
    }

    [Fact]
    public async Task LoadAsync_ThrowsWhenOfflinePreferredButMissing()
    {
        var handler = new TestHttpMessageHandler(new Dictionary<Uri, HttpResponseMessage>());
        var client = new HttpClient(handler);
        var factory = new SingleClientHttpClientFactory(client);
        var cache = new MemoryCache(new MemoryCacheOptions());
        var fileSystem = new MockFileSystem();
        var loader = new OracleCatalogLoader(factory, cache, fileSystem, NullLogger<OracleCatalogLoader>.Instance);

        var options = new OracleConnectorOptions
        {
            CatalogUri = new Uri("https://www.oracle.com/security-alerts/cpu/csaf/catalog.json"),
            PreferOfflineSnapshot = true,
            OfflineSnapshotPath = "/missing/oracle-catalog.json",
        };

        await Assert.ThrowsAsync<InvalidOperationException>(() => loader.LoadAsync(options, CancellationToken.None));
    }

    private static HttpResponseMessage CreateResponse(string payload)
        => new(HttpStatusCode.OK)
        {
            Content = new StringContent(payload, Encoding.UTF8, "application/json"),
        };

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

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

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

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

        public override DateTimeOffset GetUtcNow() => _now;
    }

    private sealed class TestHttpMessageHandler : HttpMessageHandler
    {
        private readonly Dictionary<Uri, HttpResponseMessage> _responses;

        public TestHttpMessageHandler(Dictionary<Uri, HttpResponseMessage> responses)
        {
            _responses = responses;
        }

        public int InvocationCount { get; private set; }

        public void ResetInvocationCount() => InvocationCount = 0;

        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            InvocationCount++;
            if (request.RequestUri is not null && _responses.TryGetValue(request.RequestUri, out var response))
            {
                var payload = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
                return new HttpResponseMessage(response.StatusCode)
                {
                    Content = new StringContent(payload, Encoding.UTF8, "application/json"),
                };
            }

            return new HttpResponseMessage(HttpStatusCode.InternalServerError)
            {
                Content = new StringContent("unexpected request"),
            };
        }
    }
}
