using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using MongoDB.Driver;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Storage.Mongo.Advisories;
using StellaOps.Feedser.Storage.Mongo.Aliases;

namespace StellaOps.Feedser.Storage.Mongo.Tests;

[Collection("mongo-fixture")]
public sealed class AdvisoryStoreTests : IClassFixture<MongoIntegrationFixture>
{
    private readonly MongoIntegrationFixture _fixture;

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

    [Fact]
    public async Task UpsertAndFetchAdvisory()
    {
        await DropCollectionAsync(MongoStorageDefaults.Collections.Advisory);
        await DropCollectionAsync(MongoStorageDefaults.Collections.Alias);

        var aliasStore = new AliasStore(_fixture.Database, NullLogger<AliasStore>.Instance);
        var store = new AdvisoryStore(
            _fixture.Database,
            aliasStore,
            NullLogger<AdvisoryStore>.Instance,
            Options.Create(new MongoStorageOptions()),
            TimeProvider.System);
        var advisory = new Advisory(
            advisoryKey: "ADV-1",
            title: "Sample Advisory",
            summary: "Demo",
            language: "en",
            published: DateTimeOffset.UtcNow,
            modified: DateTimeOffset.UtcNow,
            severity: "medium",
            exploitKnown: false,
            aliases: new[] { "ALIAS-1" },
            references: Array.Empty<AdvisoryReference>(),
            affectedPackages: Array.Empty<AffectedPackage>(),
            cvssMetrics: Array.Empty<CvssMetric>(),
            provenance: Array.Empty<AdvisoryProvenance>());

        await store.UpsertAsync(advisory, CancellationToken.None);

        var fetched = await store.FindAsync("ADV-1", CancellationToken.None);
        Assert.NotNull(fetched);
        Assert.Equal(advisory.AdvisoryKey, fetched!.AdvisoryKey);

        var recent = await store.GetRecentAsync(5, CancellationToken.None);
        Assert.NotEmpty(recent);

        var aliases = await aliasStore.GetByAdvisoryAsync("ADV-1", CancellationToken.None);
        Assert.Contains(aliases, record => record.Scheme == AliasStoreConstants.PrimaryScheme && record.Value == "ADV-1");
        Assert.Contains(aliases, record => record.Value == "ALIAS-1");
    }

    [Fact]
    public async Task RangePrimitives_RoundTripThroughMongo()
    {
        await DropCollectionAsync(MongoStorageDefaults.Collections.Advisory);
        await DropCollectionAsync(MongoStorageDefaults.Collections.Alias);

        var aliasStore = new AliasStore(_fixture.Database, NullLogger<AliasStore>.Instance);
        var store = new AdvisoryStore(
            _fixture.Database,
            aliasStore,
            NullLogger<AdvisoryStore>.Instance,
            Options.Create(new MongoStorageOptions()),
            TimeProvider.System);

        var recordedAt = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
        var provenance = new AdvisoryProvenance("source-x", "mapper", "payload-123", recordedAt);
        var rangePrimitives = new RangePrimitives(
            new SemVerPrimitive(
                Introduced: "1.0.0",
                IntroducedInclusive: true,
                Fixed: "1.2.0",
                FixedInclusive: false,
                LastAffected: "1.1.5",
                LastAffectedInclusive: true,
                ConstraintExpression: ">=1.0.0 <1.2.0"),
            new NevraPrimitive(
                Introduced: new NevraComponent("pkg", 0, "1.0.0", "1", "x86_64"),
                Fixed: new NevraComponent("pkg", 1, "1.2.0", "2", "x86_64"),
                LastAffected: null),
            new EvrPrimitive(
                Introduced: new EvrComponent(1, "1.0.0", "1"),
                Fixed: null,
                LastAffected: new EvrComponent(1, "1.1.5", null)),
            new Dictionary<string, string>(StringComparer.Ordinal)
            {
                ["channel"] = "stable",
                ["notesHash"] = "abc123",
            });

        var versionRange = new AffectedVersionRange(
            rangeKind: "semver",
            introducedVersion: "1.0.0",
            fixedVersion: "1.2.0",
            lastAffectedVersion: "1.1.5",
            rangeExpression: ">=1.0.0 <1.2.0",
            provenance,
            rangePrimitives);

        var affectedPackage = new AffectedPackage(
            type: "semver",
            identifier: "pkg@1.x",
            platform: "linux",
            versionRanges: new[] { versionRange },
            statuses: Array.Empty<AffectedPackageStatus>(),
            provenance: new[] { provenance });

        var advisory = new Advisory(
            advisoryKey: "ADV-RANGE-1",
            title: "Sample Range Primitive",
            summary: "Testing range primitive persistence.",
            language: "en",
            published: recordedAt,
            modified: recordedAt,
            severity: "medium",
            exploitKnown: false,
            aliases: new[] { "CVE-2025-0001" },
            references: Array.Empty<AdvisoryReference>(),
            affectedPackages: new[] { affectedPackage },
            cvssMetrics: Array.Empty<CvssMetric>(),
            provenance: new[] { provenance });

        await store.UpsertAsync(advisory, CancellationToken.None);

        var fetched = await store.FindAsync("ADV-RANGE-1", CancellationToken.None);
        Assert.NotNull(fetched);
        var fetchedPackage = Assert.Single(fetched!.AffectedPackages);
        var fetchedRange = Assert.Single(fetchedPackage.VersionRanges);

        Assert.Equal(versionRange.RangeKind, fetchedRange.RangeKind);
        Assert.Equal(versionRange.IntroducedVersion, fetchedRange.IntroducedVersion);
        Assert.Equal(versionRange.FixedVersion, fetchedRange.FixedVersion);
        Assert.Equal(versionRange.LastAffectedVersion, fetchedRange.LastAffectedVersion);
        Assert.Equal(versionRange.RangeExpression, fetchedRange.RangeExpression);
        Assert.Equal(versionRange.Provenance.Source, fetchedRange.Provenance.Source);
        Assert.Equal(versionRange.Provenance.Kind, fetchedRange.Provenance.Kind);
        Assert.Equal(versionRange.Provenance.Value, fetchedRange.Provenance.Value);
        Assert.Equal(versionRange.Provenance.DecisionReason, fetchedRange.Provenance.DecisionReason);
        Assert.Equal(versionRange.Provenance.RecordedAt, fetchedRange.Provenance.RecordedAt);
        Assert.True(versionRange.Provenance.FieldMask.SequenceEqual(fetchedRange.Provenance.FieldMask));

        Assert.NotNull(fetchedRange.Primitives);
        Assert.Equal(rangePrimitives.SemVer, fetchedRange.Primitives!.SemVer);
        Assert.Equal(rangePrimitives.Nevra, fetchedRange.Primitives.Nevra);
        Assert.Equal(rangePrimitives.Evr, fetchedRange.Primitives.Evr);
        Assert.Equal(rangePrimitives.VendorExtensions, fetchedRange.Primitives.VendorExtensions);
    }

    [Fact]
    public async Task UpsertAsync_SkipsNormalizedVersionsWhenFeatureDisabled()
    {
        await DropCollectionAsync(MongoStorageDefaults.Collections.Advisory);
        await DropCollectionAsync(MongoStorageDefaults.Collections.Alias);

        var aliasStore = new AliasStore(_fixture.Database, NullLogger<AliasStore>.Instance);
        var store = new AdvisoryStore(
            _fixture.Database,
            aliasStore,
            NullLogger<AdvisoryStore>.Instance,
            Options.Create(new MongoStorageOptions { EnableSemVerStyle = false }),
            TimeProvider.System);

        var advisory = CreateNormalizedAdvisory("ADV-NORM-DISABLED");
        await store.UpsertAsync(advisory, CancellationToken.None);

        var document = await _fixture.Database
            .GetCollection<AdvisoryDocument>(MongoStorageDefaults.Collections.Advisory)
            .Find(x => x.AdvisoryKey == advisory.AdvisoryKey)
            .FirstOrDefaultAsync();

        Assert.NotNull(document);
        Assert.True(document!.NormalizedVersions is null || document.NormalizedVersions.Count == 0);
    }

    [Fact]
    public async Task UpsertAsync_PopulatesNormalizedVersionsWhenFeatureEnabled()
    {
        await DropCollectionAsync(MongoStorageDefaults.Collections.Advisory);
        await DropCollectionAsync(MongoStorageDefaults.Collections.Alias);

        var aliasStore = new AliasStore(_fixture.Database, NullLogger<AliasStore>.Instance);
        var store = new AdvisoryStore(
            _fixture.Database,
            aliasStore,
            NullLogger<AdvisoryStore>.Instance,
            Options.Create(new MongoStorageOptions { EnableSemVerStyle = true }),
            TimeProvider.System);

        var advisory = CreateNormalizedAdvisory("ADV-NORM-ENABLED");
        await store.UpsertAsync(advisory, CancellationToken.None);

        var document = await _fixture.Database
            .GetCollection<AdvisoryDocument>(MongoStorageDefaults.Collections.Advisory)
            .Find(x => x.AdvisoryKey == advisory.AdvisoryKey)
            .FirstOrDefaultAsync();

        Assert.NotNull(document);
        var normalizedCollection = document!.NormalizedVersions;
        Assert.NotNull(normalizedCollection);
        var normalized = Assert.Single(normalizedCollection!);
        Assert.Equal("pkg:npm/example", normalized.PackageId);
        Assert.Equal(AffectedPackageTypes.SemVer, normalized.PackageType);
        Assert.Equal(NormalizedVersionSchemes.SemVer, normalized.Scheme);
        Assert.Equal(NormalizedVersionRuleTypes.Range, normalized.Type);
        Assert.Equal("range", normalized.Style);
        Assert.Equal("1.0.0", normalized.Min);
        Assert.True(normalized.MinInclusive);
        Assert.Equal("2.0.0", normalized.Max);
        Assert.False(normalized.MaxInclusive);
        Assert.Null(normalized.Value);
        Assert.Equal("ghsa:pkg:npm/example", normalized.Notes);
        Assert.Equal("range-decision", normalized.DecisionReason);
        Assert.Equal(">= 1.0.0 < 2.0.0", normalized.Constraint);
        Assert.Equal("ghsa", normalized.Source);
        Assert.Equal(new DateTime(2025, 10, 9, 0, 0, 0, DateTimeKind.Utc), normalized.RecordedAtUtc);
    }

    private static Advisory CreateNormalizedAdvisory(string advisoryKey)
    {
        var recordedAt = new DateTimeOffset(2025, 10, 9, 0, 0, 0, TimeSpan.Zero);
        var rangeProvenance = new AdvisoryProvenance(
            source: "ghsa",
            kind: "affected-range",
            value: "pkg:npm/example",
            recordedAt: recordedAt,
            fieldMask: new[] { "affectedpackages[].versionranges[]" },
            decisionReason: "range-decision");

        var semverPrimitive = new SemVerPrimitive(
            Introduced: "1.0.0",
            IntroducedInclusive: true,
            Fixed: "2.0.0",
            FixedInclusive: false,
            LastAffected: null,
            LastAffectedInclusive: false,
            ConstraintExpression: ">= 1.0.0 < 2.0.0");

        var normalizedRule = semverPrimitive.ToNormalizedVersionRule("ghsa:pkg:npm/example")!;
        var versionRange = new AffectedVersionRange(
            rangeKind: "semver",
            introducedVersion: "1.0.0",
            fixedVersion: "2.0.0",
            lastAffectedVersion: null,
            rangeExpression: ">= 1.0.0 < 2.0.0",
            provenance: rangeProvenance,
            primitives: new RangePrimitives(semverPrimitive, null, null, null));

        var package = new AffectedPackage(
            type: AffectedPackageTypes.SemVer,
            identifier: "pkg:npm/example",
            platform: "npm",
            versionRanges: new[] { versionRange },
            statuses: Array.Empty<AffectedPackageStatus>(),
            provenance: new[] { rangeProvenance },
            normalizedVersions: new[] { normalizedRule });

        var advisoryProvenance = new AdvisoryProvenance(
            source: "ghsa",
            kind: "document",
            value: advisoryKey,
            recordedAt: recordedAt,
            fieldMask: new[] { "advisory" },
            decisionReason: "document-decision");

        return new Advisory(
            advisoryKey: advisoryKey,
            title: "Normalized advisory",
            summary: "Contains normalized versions for storage testing.",
            language: "en",
            published: recordedAt,
            modified: recordedAt,
            severity: "medium",
            exploitKnown: false,
            aliases: new[] { $"{advisoryKey}-ALIAS" },
            references: Array.Empty<AdvisoryReference>(),
            affectedPackages: new[] { package },
            cvssMetrics: Array.Empty<CvssMetric>(),
            provenance: new[] { advisoryProvenance });
    }

    private async Task DropCollectionAsync(string collectionName)
    {
        try
        {
            await _fixture.Database.DropCollectionAsync(collectionName);
        }
        catch (MongoDB.Driver.MongoCommandException ex) when (ex.CodeName == "NamespaceNotFound" || ex.Message.Contains("ns not found", StringComparison.OrdinalIgnoreCase))
        {
            // ignore missing collection
        }
    }
}
