using System.Net;
using System.Net.Http;
using System.Text;
using MongoDB.Bson;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Source.Common.Testing;
using StellaOps.Feedser.Source.Ghsa.Configuration;
using StellaOps.Feedser.Source.Ghsa.Internal;
using StellaOps.Feedser.Testing;
using StellaOps.Feedser.Storage.Mongo;
using StellaOps.Feedser.Storage.Mongo.Advisories;

namespace StellaOps.Feedser.Source.Ghsa.Tests;

[Collection("mongo-fixture")]
public sealed class GhsaConnectorTests : IAsyncLifetime
{
    private readonly MongoIntegrationFixture _fixture;
    private ConnectorTestHarness? _harness;

    public GhsaConnectorTests(MongoIntegrationFixture fixture)
    {
        _fixture = fixture;
    }

    [Fact]
    public async Task FetchParseMap_EmitsCanonicalAdvisory()
    {
        var initialTime = new DateTimeOffset(2024, 10, 2, 0, 0, 0, TimeSpan.Zero);
        await EnsureHarnessAsync(initialTime);
        var harness = _harness!;

        var since = initialTime - TimeSpan.FromDays(30);
        var listUri = new Uri($"https://ghsa.test/security/advisories?updated_since={Uri.EscapeDataString(since.ToString("O"))}&updated_until={Uri.EscapeDataString(initialTime.ToString("O"))}&page=1&per_page=5");
        harness.Handler.AddJsonResponse(listUri, ReadFixture("Fixtures/ghsa-list.json"));
        harness.Handler.SetFallback(request =>
        {
            if (request.RequestUri is null)
            {
                return new HttpResponseMessage(HttpStatusCode.NotFound);
            }

            if (request.RequestUri.AbsoluteUri.Equals("https://ghsa.test/security/advisories/GHSA-xxxx-yyyy-zzzz", StringComparison.OrdinalIgnoreCase))
            {
                return new HttpResponseMessage(HttpStatusCode.OK)
                {
                    Content = new StringContent(ReadFixture("Fixtures/ghsa-GHSA-xxxx-yyyy-zzzz.json"), Encoding.UTF8, "application/json")
                };
            }

            return new HttpResponseMessage(HttpStatusCode.NotFound);
        });

        var connector = new GhsaConnectorPlugin().Create(harness.ServiceProvider);

        await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None);
        await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None);
        await connector.MapAsync(harness.ServiceProvider, CancellationToken.None);

        var advisoryStore = harness.ServiceProvider.GetRequiredService<IAdvisoryStore>();
        var advisory = await advisoryStore.FindAsync("GHSA-xxxx-yyyy-zzzz", CancellationToken.None);
        Assert.NotNull(advisory);

        Assert.Collection(advisory!.Credits,
            credit =>
            {
                Assert.Equal("remediation_developer", credit.Role);
                Assert.Equal("maintainer-team", credit.DisplayName);
                Assert.Contains("https://github.com/maintainer-team", credit.Contacts);
            },
            credit =>
            {
                Assert.Equal("reporter", credit.Role);
                Assert.Equal("security-reporter", credit.DisplayName);
                Assert.Contains("https://github.com/security-reporter", credit.Contacts);
            });

        var weakness = Assert.Single(advisory.Cwes);
        Assert.Equal("CWE-79", weakness.Identifier);
        Assert.Equal("https://cwe.mitre.org/data/definitions/79.html", weakness.Uri);

        var metric = Assert.Single(advisory.CvssMetrics);
        Assert.Equal("3.1", metric.Version);
        Assert.Equal("CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", metric.Vector);
        Assert.Equal("critical", metric.BaseSeverity);
        Assert.Equal("3.1|CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", advisory.CanonicalMetricId);

        var snapshot = SnapshotSerializer.ToSnapshot(advisory).Replace("\r\n", "\n").TrimEnd();
        var expected = ReadFixture("Fixtures/expected-GHSA-xxxx-yyyy-zzzz.json").Replace("\r\n", "\n").TrimEnd();

        if (!string.Equals(expected, snapshot, StringComparison.Ordinal))
        {
            var actualPath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "expected-GHSA-xxxx-yyyy-zzzz.actual.json");
            Directory.CreateDirectory(Path.GetDirectoryName(actualPath)!);
            File.WriteAllText(actualPath, snapshot);
        }

        Assert.Equal(expected, snapshot);
        harness.Handler.AssertNoPendingResponses();
    }

    [Fact]
    public async Task FetchAsync_RateLimitDefersWindowAndRecordsSnapshot()
    {
        var initialTime = new DateTimeOffset(2024, 10, 5, 0, 0, 0, TimeSpan.Zero);
        await EnsureHarnessAsync(initialTime);
        var harness = _harness!;

        var since = initialTime - TimeSpan.FromDays(30);
        var until = initialTime;
        var listUri = new Uri($"https://ghsa.test/security/advisories?updated_since={Uri.EscapeDataString(since.ToString("O"))}&updated_until={Uri.EscapeDataString(until.ToString("O"))}&page=1&per_page=5");

        harness.Handler.AddResponse(HttpMethod.Get, listUri, _ =>
        {
            var response = new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new StringContent(ReadFixture("Fixtures/ghsa-list.json"), Encoding.UTF8, "application/json")
            };
            response.Headers.TryAddWithoutValidation("X-RateLimit-Resource", "core");
            response.Headers.TryAddWithoutValidation("X-RateLimit-Limit", "5000");
            response.Headers.TryAddWithoutValidation("X-RateLimit-Remaining", "0");
            return response;
        });

        harness.Handler.SetFallback(_ => new HttpResponseMessage(HttpStatusCode.NotFound));

        var connector = new GhsaConnectorPlugin().Create(harness.ServiceProvider);
        await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None);

        Assert.Single(harness.Handler.Requests);

        var diagnostics = harness.ServiceProvider.GetRequiredService<GhsaDiagnostics>();
        var snapshot = diagnostics.GetLastRateLimitSnapshot();
        Assert.True(snapshot.HasValue);
        Assert.Equal("list", snapshot!.Value.Phase);
        Assert.Equal("core", snapshot.Value.Resource);
        Assert.Equal(0, snapshot.Value.Remaining);

        var stateRepository = harness.ServiceProvider.GetRequiredService<ISourceStateRepository>();
        var state = await stateRepository.TryGetAsync(GhsaConnectorPlugin.SourceName, CancellationToken.None);
        Assert.NotNull(state);

        Assert.True(state!.Cursor.TryGetValue("currentWindowStart", out var startValue));
        Assert.True(state.Cursor.TryGetValue("currentWindowEnd", out var endValue));
        Assert.True(state.Cursor.TryGetValue("nextPage", out var nextPageValue));

        Assert.Equal(since.UtcDateTime, startValue.ToUniversalTime());
        Assert.Equal(until.UtcDateTime, endValue.ToUniversalTime());
        Assert.Equal(1, nextPageValue.AsInt32);

        Assert.True(state.Cursor.TryGetValue("pendingDocuments", out var pendingDocs));
        Assert.Empty(pendingDocs.AsBsonArray);
        Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMappings));
        Assert.Empty(pendingMappings.AsBsonArray);
    }

    private async Task EnsureHarnessAsync(DateTimeOffset initialTime)
    {
        if (_harness is not null)
        {
            return;
        }

        var harness = new ConnectorTestHarness(_fixture, initialTime, GhsaOptions.HttpClientName);
        await harness.EnsureServiceProviderAsync(services =>
        {
            services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
            services.AddGhsaConnector(options =>
            {
                options.BaseEndpoint = new Uri("https://ghsa.test/", UriKind.Absolute);
                options.ApiToken = "test-token";
                options.PageSize = 5;
                options.MaxPagesPerFetch = 2;
                options.RequestDelay = TimeSpan.Zero;
                options.InitialBackfill = TimeSpan.FromDays(30);
                options.SecondaryRateLimitBackoff = TimeSpan.FromMilliseconds(10);
            });
        });

        _harness = harness;
    }

    private static string ReadFixture(string relativePath)
    {
        var path = Path.Combine(AppContext.BaseDirectory, relativePath);
        return File.ReadAllText(path);
    }

    public async Task InitializeAsync()
    {
        await Task.CompletedTask;
    }

    public async Task DisposeAsync()
    {
        if (_harness is not null)
        {
            await _harness.DisposeAsync();
        }
    }
}
