using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Source.Common;
using StellaOps.Feedser.Source.Common.Http;
using StellaOps.Feedser.Source.Common.Fetch;
using StellaOps.Feedser.Source.Common.Testing;
using StellaOps.Feedser.Source.Common.Packages;
using StellaOps.Feedser.Source.Vndr.Apple;
using StellaOps.Feedser.Storage.Mongo;
using StellaOps.Feedser.Storage.Mongo.Advisories;
using StellaOps.Feedser.Storage.Mongo.PsirtFlags;
using StellaOps.Feedser.Testing;
using Xunit;

namespace StellaOps.Feedser.Source.Vndr.Apple.Tests;

[Collection("mongo-fixture")]
public sealed class AppleConnectorTests : IAsyncLifetime
{
    private static readonly Uri IndexUri = new("https://support.example.com/index.json");
    private static readonly Uri DetailBaseUri = new("https://support.example.com/en-us/");

    private readonly MongoIntegrationFixture _fixture;
    private readonly FakeTimeProvider _timeProvider;

    public AppleConnectorTests(MongoIntegrationFixture fixture)
    {
        _fixture = fixture;
        _timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 10, 0, 0, 0, TimeSpan.Zero));
    }

    [Fact]
    public async Task FetchParseMap_EndToEnd_ProducesCanonicalAdvisories()
    {
        var handler = new CannedHttpMessageHandler();
        SeedIndex(handler);
        SeedDetail(handler);

        await using var provider = await BuildServiceProviderAsync(handler);
        var connector = provider.GetRequiredService<AppleConnector>();

        await connector.FetchAsync(provider, CancellationToken.None);
        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(5, advisories.Count);

        var advisoriesByKey = advisories.ToDictionary(static advisory => advisory.AdvisoryKey, StringComparer.Ordinal);

        var iosBaseline = advisoriesByKey["125326"];
        Assert.Contains("CVE-2025-43400", iosBaseline.Aliases, StringComparer.OrdinalIgnoreCase);
        Assert.Equal(3, iosBaseline.AffectedPackages.Count());
        AssertPackageDetails(iosBaseline, "iPhone 16 Pro", "26.0.1", "24A341", "iOS");
        AssertPackageDetails(iosBaseline, "iPhone 16 Pro", "26.0.1 (a)", "24A341a", "iOS");
        AssertPackageDetails(iosBaseline, "iPad Pro (M4)", "26", "24B120", "iPadOS");

        var macBaseline = advisoriesByKey["125328"];
        Assert.Contains("CVE-2025-43400", macBaseline.Aliases, StringComparer.OrdinalIgnoreCase);
        Assert.Equal(2, macBaseline.AffectedPackages.Count());
        AssertPackageDetails(macBaseline, "MacBook Pro (M4)", "26.0.1", "26A123", "macOS");
        AssertPackageDetails(macBaseline, "Mac Studio", "26", "26A120b", "macOS");

        var venturaRsr = advisoriesByKey["106355"];
        Assert.Contains("CVE-2023-37450", venturaRsr.Aliases, StringComparer.OrdinalIgnoreCase);
        Assert.Equal(2, venturaRsr.AffectedPackages.Count());
        AssertPackageDetails(venturaRsr, "macOS Ventura", string.Empty, "22F400", "macOS Ventura");
        AssertPackageDetails(venturaRsr, "macOS Ventura (Intel)", string.Empty, "22F400a", "macOS Ventura");

        var visionOs = advisoriesByKey["HT214108"];
        Assert.Contains("CVE-2024-27800", visionOs.Aliases, StringComparer.OrdinalIgnoreCase);
        Assert.False(visionOs.AffectedPackages.Any());

        var rsrAdvisory = advisoriesByKey["HT215500"];
        Assert.Contains("CVE-2025-2468", rsrAdvisory.Aliases, StringComparer.OrdinalIgnoreCase);
        var rsrPackage = AssertPackageDetails(rsrAdvisory, "iPhone 15 Pro", "18.0.1 (c)", "22A123c", "iOS");
        Assert.True(rsrPackage.NormalizedVersions.IsDefaultOrEmpty || rsrPackage.NormalizedVersions.Length == 0);

        var flagStore = provider.GetRequiredService<IPsirtFlagStore>();
        var rsrFlag = await flagStore.FindAsync("HT215500", CancellationToken.None);
        Assert.NotNull(rsrFlag);
    }



    private static AffectedPackage AssertPackageDetails(
        Advisory advisory,
        string identifier,
        string expectedRangeExpression,
        string? expectedBuild,
        string expectedPlatform)
    {
        var candidates = advisory.AffectedPackages
            .Where(package => string.Equals(package.Identifier, identifier, StringComparison.Ordinal))
            .ToList();
        Assert.NotEmpty(candidates);

        var package = Assert.Single(candidates, candidate =>
        {
            var rangeCandidate = candidate.VersionRanges.SingleOrDefault();
            return rangeCandidate is not null
                && string.Equals(rangeCandidate.RangeExpression ?? string.Empty, expectedRangeExpression, StringComparison.Ordinal);
        });

        Assert.Equal(expectedPlatform, package.Platform);

        var range = Assert.Single(package.VersionRanges);
        Assert.Equal(expectedRangeExpression, range.RangeExpression ?? string.Empty);
        Assert.Equal("vendor", range.RangeKind);

        Assert.NotNull(range.Primitives);
        Assert.NotNull(range.Primitives!.VendorExtensions);
        var vendorExtensions = range.Primitives.VendorExtensions;

        if (!string.IsNullOrWhiteSpace(expectedRangeExpression))
        {
            if (!vendorExtensions.TryGetValue("apple.version.raw", out var rawVersion))
            {
                throw new Xunit.Sdk.XunitException($"Missing apple.version.raw for {identifier}; available keys: {string.Join(", ", vendorExtensions.Keys)}");
            }

            Assert.Equal(expectedRangeExpression, rawVersion);
        }
        else
        {
            Assert.False(vendorExtensions.ContainsKey("apple.version.raw"));
        }

        if (!string.IsNullOrWhiteSpace(expectedPlatform))
        {
            if (vendorExtensions.TryGetValue("apple.platform", out var platformExtension))
            {
                Assert.Equal(expectedPlatform, platformExtension);
            }
            else
            {
                throw new Xunit.Sdk.XunitException($"Missing apple.platform extension for {identifier}; available keys: {string.Join(", ", vendorExtensions.Keys)}");
            }
        }

        if (!string.IsNullOrWhiteSpace(expectedBuild))
        {
            Assert.True(vendorExtensions.TryGetValue("apple.build", out var buildExtension));
            Assert.Equal(expectedBuild, buildExtension);
        }
        else
        {
            Assert.False(vendorExtensions.ContainsKey("apple.build"));
        }

        if (PackageCoordinateHelper.TryParseSemVer(expectedRangeExpression, out _, out var normalized))
        {
            var normalizedRule = Assert.Single(package.NormalizedVersions);
            Assert.Equal(NormalizedVersionSchemes.SemVer, normalizedRule.Scheme);
            Assert.Equal(NormalizedVersionRuleTypes.LessThanOrEqual, normalizedRule.Type);
            Assert.Equal(normalized, normalizedRule.Max);
            Assert.Equal($"apple:{expectedPlatform}:{identifier}", normalizedRule.Notes);
        }
        else
        {
            Assert.True(package.NormalizedVersions.IsDefaultOrEmpty || package.NormalizedVersions.Length == 0);
        }

        return package;
    }

    public Task InitializeAsync() => Task.CompletedTask;

    public Task DisposeAsync() => Task.CompletedTask;

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

        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.AddAppleConnector(opts =>
        {
            opts.SoftwareLookupUri = IndexUri;
            opts.AdvisoryBaseUri = DetailBaseUri;
            opts.LocaleSegment = "en-us";
            opts.InitialBackfill = TimeSpan.FromDays(120);
            opts.ModifiedTolerance = TimeSpan.FromHours(2);
            opts.MaxAdvisoriesPerFetch = 10;
        });

        services.Configure<HttpClientFactoryOptions>(AppleOptions.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 static void SeedIndex(CannedHttpMessageHandler handler)
    {
        handler.AddJsonResponse(IndexUri, ReadFixture("index.json"));
    }

    private static void SeedDetail(CannedHttpMessageHandler handler)
    {
        AddHtmlResponse(handler, new Uri(DetailBaseUri, "125326"), "125326.html");
        AddHtmlResponse(handler, new Uri(DetailBaseUri, "125328"), "125328.html");
        AddHtmlResponse(handler, new Uri(DetailBaseUri, "106355"), "106355.html");
        AddHtmlResponse(handler, new Uri(DetailBaseUri, "HT214108"), "ht214108.html");
        AddHtmlResponse(handler, new Uri(DetailBaseUri, "HT215500"), "ht215500.html");
    }

    private static void AddHtmlResponse(CannedHttpMessageHandler handler, Uri uri, string fixture)
    {
        handler.AddResponse(uri, () =>
        {
            var content = ReadFixture(fixture);
            return new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new StringContent(content, Encoding.UTF8, "text/html"),
            };
        });
    }

    private static string ReadFixture(string name)
    {
        var path = Path.Combine(
            AppContext.BaseDirectory,
            "Source",
            "Vndr",
            "Apple",
            "Fixtures",
            name);
        return File.ReadAllText(path);
    }
}
