using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Time.Testing;
using MongoDB.Bson;
using MongoDB.Driver;
using StellaOps.Feedser.Source.CertFr;
using StellaOps.Feedser.Source.CertFr.Configuration;
using StellaOps.Feedser.Source.Common;
using StellaOps.Feedser.Source.Common.Http;
using StellaOps.Feedser.Source.Common.Testing;
using StellaOps.Feedser.Storage.Mongo;
using StellaOps.Feedser.Storage.Mongo.Advisories;
using StellaOps.Feedser.Storage.Mongo.Documents;
using StellaOps.Feedser.Storage.Mongo.Dtos;
using StellaOps.Feedser.Testing;
using StellaOps.Feedser.Models;

namespace StellaOps.Feedser.Source.CertFr.Tests;

[Collection("mongo-fixture")]
public sealed class CertFrConnectorTests : IAsyncLifetime
{
    private static readonly Uri FeedUri = new("https://www.cert.ssi.gouv.fr/feed/alertes/");
    private static readonly Uri FirstDetailUri = new("https://www.cert.ssi.gouv.fr/alerte/AV-2024.001/");
    private static readonly Uri SecondDetailUri = new("https://www.cert.ssi.gouv.fr/alerte/AV-2024.002/");

    private readonly MongoIntegrationFixture _fixture;
    private readonly FakeTimeProvider _timeProvider;
    private readonly CannedHttpMessageHandler _handler;

    public CertFrConnectorTests(MongoIntegrationFixture fixture)
    {
        _fixture = fixture;
        _timeProvider = new FakeTimeProvider(new DateTimeOffset(2024, 10, 3, 0, 0, 0, TimeSpan.Zero));
        _handler = new CannedHttpMessageHandler();
    }

    [Fact]
    public async Task FetchParseMap_ProducesDeterministicSnapshot()
    {
        await using var provider = await BuildServiceProviderAsync();
        SeedFeed();
        SeedDetailResponses();

        var connector = provider.GetRequiredService<CertFrConnector>();
        await connector.FetchAsync(provider, CancellationToken.None);
        _timeProvider.Advance(TimeSpan.FromMinutes(1));
        await connector.ParseAsync(provider, CancellationToken.None);
        await connector.MapAsync(provider, CancellationToken.None);

        var advisoryStore = provider.GetRequiredService<IAdvisoryStore>();
        var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
        Assert.Equal(2, advisories.Count);

        var snapshot = SnapshotSerializer.ToSnapshot(advisories.OrderBy(static a => a.AdvisoryKey, StringComparer.Ordinal).ToArray());
        var expected = ReadFixture("certfr-advisories.snapshot.json");
        var normalizedSnapshot = Normalize(snapshot);
        var normalizedExpected = Normalize(expected);
        if (!string.Equals(normalizedExpected, normalizedSnapshot, StringComparison.Ordinal))
        {
            var actualPath = Path.Combine(AppContext.BaseDirectory, "Source", "CertFr", "Fixtures", "certfr-advisories.actual.json");
            Directory.CreateDirectory(Path.GetDirectoryName(actualPath)!);
            File.WriteAllText(actualPath, snapshot);
        }

        Assert.Equal(normalizedExpected, normalizedSnapshot);

        var documentStore = provider.GetRequiredService<IDocumentStore>();
        var firstDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, FirstDetailUri.ToString(), CancellationToken.None);
        Assert.NotNull(firstDocument);
        Assert.Equal(DocumentStatuses.Mapped, firstDocument!.Status);

        var secondDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, SecondDetailUri.ToString(), CancellationToken.None);
        Assert.NotNull(secondDocument);
        Assert.Equal(DocumentStatuses.Mapped, secondDocument!.Status);

        var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
        var state = await stateRepository.TryGetAsync(CertFrConnectorPlugin.SourceName, CancellationToken.None);
        Assert.NotNull(state);
        Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs) && pendingDocs.AsBsonArray.Count == 0);
        Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMaps) && pendingMaps.AsBsonArray.Count == 0);
    }

    [Fact]
    public async Task FetchFailure_RecordsBackoffAndReason()
    {
        await using var provider = await BuildServiceProviderAsync();
        _handler.AddResponse(FeedUri, () => new HttpResponseMessage(HttpStatusCode.InternalServerError)
        {
            Content = new StringContent("feed error", Encoding.UTF8, "text/plain"),
        });

        var connector = provider.GetRequiredService<CertFrConnector>();
        await Assert.ThrowsAsync<HttpRequestException>(() => connector.FetchAsync(provider, CancellationToken.None));

        var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
        var state = await stateRepository.TryGetAsync(CertFrConnectorPlugin.SourceName, CancellationToken.None);
        Assert.NotNull(state);
        Assert.Equal(1, state!.FailCount);
        Assert.NotNull(state.LastFailureReason);
        Assert.Contains("500", state.LastFailureReason, StringComparison.Ordinal);
        Assert.NotNull(state.BackoffUntil);
        Assert.True(state.BackoffUntil > _timeProvider.GetUtcNow());
    }

    [Fact]
    public async Task Fetch_NotModifiedResponsesMaintainDocumentState()
    {
        await using var provider = await BuildServiceProviderAsync();
        SeedFeed();
        SeedDetailResponses();

        var connector = provider.GetRequiredService<CertFrConnector>();
        await connector.FetchAsync(provider, CancellationToken.None);
        await connector.ParseAsync(provider, CancellationToken.None);
        await connector.MapAsync(provider, CancellationToken.None);

        var documentStore = provider.GetRequiredService<IDocumentStore>();
        var firstDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, FirstDetailUri.ToString(), CancellationToken.None);
        Assert.NotNull(firstDocument);
        Assert.Equal(DocumentStatuses.Mapped, firstDocument!.Status);

        var secondDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, SecondDetailUri.ToString(), CancellationToken.None);
        Assert.NotNull(secondDocument);
        Assert.Equal(DocumentStatuses.Mapped, secondDocument!.Status);

        SeedFeed();
        SeedNotModifiedDetailResponses();

        await connector.FetchAsync(provider, CancellationToken.None);

        firstDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, FirstDetailUri.ToString(), CancellationToken.None);
        Assert.NotNull(firstDocument);
        Assert.Equal(DocumentStatuses.Mapped, firstDocument!.Status);

        secondDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, SecondDetailUri.ToString(), CancellationToken.None);
        Assert.NotNull(secondDocument);
        Assert.Equal(DocumentStatuses.Mapped, secondDocument!.Status);

        var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
        var state = await stateRepository.TryGetAsync(CertFrConnectorPlugin.SourceName, CancellationToken.None);
        Assert.NotNull(state);
        Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs) && pendingDocs.AsBsonArray.Count == 0);
        Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMaps) && pendingMaps.AsBsonArray.Count == 0);
    }

    [Fact]
    public async Task Fetch_DuplicateContentSkipsRequeue()
    {
        await using var provider = await BuildServiceProviderAsync();
        SeedFeed();
        SeedDetailResponses();

        var connector = provider.GetRequiredService<CertFrConnector>();
        await connector.FetchAsync(provider, CancellationToken.None);
        await connector.ParseAsync(provider, CancellationToken.None);
        await connector.MapAsync(provider, CancellationToken.None);

        var documentStore = provider.GetRequiredService<IDocumentStore>();
        var firstDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, FirstDetailUri.ToString(), CancellationToken.None);
        Assert.NotNull(firstDocument);
        Assert.Equal(DocumentStatuses.Mapped, firstDocument!.Status);

        var secondDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, SecondDetailUri.ToString(), CancellationToken.None);
        Assert.NotNull(secondDocument);
        Assert.Equal(DocumentStatuses.Mapped, secondDocument!.Status);

        SeedFeed();
        SeedDetailResponses();

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

        firstDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, FirstDetailUri.ToString(), CancellationToken.None);
        Assert.NotNull(firstDocument);
        Assert.Equal(DocumentStatuses.Mapped, firstDocument!.Status);

        secondDocument = await documentStore.FindBySourceAndUriAsync(CertFrConnectorPlugin.SourceName, SecondDetailUri.ToString(), CancellationToken.None);
        Assert.NotNull(secondDocument);
        Assert.Equal(DocumentStatuses.Mapped, secondDocument!.Status);

        var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
        var state = await stateRepository.TryGetAsync(CertFrConnectorPlugin.SourceName, CancellationToken.None);
        Assert.NotNull(state);
        Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs) && pendingDocs.AsBsonArray.Count == 0);
        Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMaps) && pendingMaps.AsBsonArray.Count == 0);
    }

    private async Task<ServiceProvider> BuildServiceProviderAsync()
    {
        await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
        _handler.Clear();

        var services = new ServiceCollection();
        services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
        services.AddSingleton<TimeProvider>(_timeProvider);
        services.AddSingleton(_handler);

        services.AddMongoStorage(options =>
        {
            options.ConnectionString = _fixture.Runner.ConnectionString;
            options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
            options.CommandTimeout = TimeSpan.FromSeconds(5);
        });

        services.AddSourceCommon();
        services.AddCertFrConnector(opts =>
        {
            opts.FeedUri = FeedUri;
            opts.InitialBackfill = TimeSpan.FromDays(30);
            opts.WindowOverlap = TimeSpan.FromDays(2);
            opts.MaxItemsPerFetch = 50;
        });

        services.Configure<HttpClientFactoryOptions>(CertFrOptions.HttpClientName, builderOptions =>
        {
            builderOptions.HttpMessageHandlerBuilderActions.Add(builder =>
            {
                builder.PrimaryHandler = _handler;
            });
        });

        var provider = services.BuildServiceProvider();
        var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
        await bootstrapper.InitializeAsync(CancellationToken.None);
        return provider;
    }

    private void SeedFeed()
    {
        _handler.AddTextResponse(FeedUri, ReadFixture("certfr-feed.xml"), "application/atom+xml");
    }

    private void SeedDetailResponses()
    {
        AddDetailResponse(FirstDetailUri, "certfr-detail-AV-2024-001.html", "\"certfr-001\"");
        AddDetailResponse(SecondDetailUri, "certfr-detail-AV-2024-002.html", "\"certfr-002\"");
    }

    private void SeedNotModifiedDetailResponses()
    {
        AddNotModifiedResponse(FirstDetailUri, "\"certfr-001\"");
        AddNotModifiedResponse(SecondDetailUri, "\"certfr-002\"");
    }

    private void AddDetailResponse(Uri uri, string fixture, string? etag)
    {
        _handler.AddResponse(uri, () =>
        {
            var response = new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new StringContent(ReadFixture(fixture), Encoding.UTF8, "text/html"),
            };

            if (!string.IsNullOrEmpty(etag))
            {
                response.Headers.ETag = new EntityTagHeaderValue(etag);
            }

            return response;
        });
    }

    private void AddNotModifiedResponse(Uri uri, string? etag)
    {
        _handler.AddResponse(uri, () =>
        {
            var response = new HttpResponseMessage(HttpStatusCode.NotModified);
            if (!string.IsNullOrEmpty(etag))
            {
                response.Headers.ETag = new EntityTagHeaderValue(etag);
            }

            return response;
        });
    }

    private static string ReadFixture(string filename)
    {
        var baseDirectory = AppContext.BaseDirectory;
        var primary = Path.Combine(baseDirectory, "Source", "CertFr", "Fixtures", filename);
        if (File.Exists(primary))
        {
            return File.ReadAllText(primary);
        }

        var fallback = Path.Combine(baseDirectory, "CertFr", "Fixtures", filename);
        return File.ReadAllText(fallback);
    }

    private static string Normalize(string value)
        => value.Replace("\r\n", "\n", StringComparison.Ordinal);

    public Task InitializeAsync() => Task.CompletedTask;

    public async Task DisposeAsync()
    {
        await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
    }
}
