using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging.Abstractions;
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, 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, 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, fetchedRange.Provenance);

        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);
    }

    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
        }
    }
}
