package v6

import (
	"errors"
	"testing"
	"unicode"
	"unicode/utf8"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"github.com/google/uuid"
	"github.com/stretchr/testify/require"

	"github.com/anchore/grype/grype/distro"
	"github.com/anchore/grype/grype/pkg"
	"github.com/anchore/grype/grype/pkg/qualifier"
	"github.com/anchore/grype/grype/search"
	"github.com/anchore/grype/grype/version"
	"github.com/anchore/grype/grype/vulnerability"
	"github.com/anchore/syft/syft/cpe"
	syftPkg "github.com/anchore/syft/syft/pkg"
)

func Test_FindVulnerabilitiesByDistro(t *testing.T) {
	provider := testVulnerabilityProvider(t)

	d := distro.New(distro.Debian, "8", "")

	p := pkg.Package{
		ID:      pkg.ID(uuid.NewString()),
		Name:    "neutron",
		Version: "1.0.0",
		Type:    syftPkg.DebPkg,
	}

	actual, err := provider.FindVulnerabilities(search.ByDistro(*d), search.ByPackageName(p.Name), search.ByVersion(*version.New(p.Version, pkg.VersionFormat(p))))
	require.NoError(t, err)

	expected := []vulnerability.Vulnerability{
		{
			PackageName: "neutron",
			Constraint:  version.MustGetConstraint("< 2014.1.3-6", version.DebFormat),
			Reference: vulnerability.Reference{
				ID:        "CVE-2014-fake-1",
				Namespace: "debian:distro:debian:8",
			},
			PackageQualifiers: []qualifier.Qualifier{},
			CPEs:              nil,
			Advisories:        []vulnerability.Advisory{},
			Metadata: &vulnerability.Metadata{
				ID:          "CVE-2014-fake-1",
				DataSource:  "http://somewhere/CVE-2014-fake-1",
				Namespace:   "debian:distro:debian:8",
				Severity:    "High",
				URLs:        nil,
				Description: "CVE-2014-fake-1-description",
			},
			RelatedVulnerabilities: []vulnerability.Reference{{ID: "CVE-2014-fake-1", Namespace: "nvd:cpe"}},
		},
		{
			PackageName: "neutron",
			Constraint:  version.MustGetConstraint("< 2013.0.2-1", version.DebFormat),
			Reference: vulnerability.Reference{
				ID:        "CVE-2013-fake-2",
				Namespace: "debian:distro:debian:8",
			},
			PackageQualifiers: []qualifier.Qualifier{},
			CPEs:              nil,
			Advisories:        []vulnerability.Advisory{},
			Metadata: &vulnerability.Metadata{
				ID:          "CVE-2013-fake-2",
				DataSource:  "http://somewhere/CVE-2013-fake-2",
				Namespace:   "debian:distro:debian:8",
				Severity:    "High",
				URLs:        nil,
				Description: "CVE-2013-fake-2-description",
			},
			RelatedVulnerabilities: []vulnerability.Reference{{ID: "CVE-2013-fake-2", Namespace: "nvd:cpe"}},
		},
	}

	require.Len(t, actual, len(expected))

	for idx, vuln := range actual {
		if d := cmp.Diff(expected[idx], vuln, cmpOpts()...); d != "" {
			t.Errorf("diff: %+v", d)
		}
	}
}

func Test_FindVulnerabilitiesByEmptyDistro(t *testing.T) {
	provider := testVulnerabilityProvider(t)

	p := pkg.Package{
		ID:   pkg.ID(uuid.NewString()),
		Name: "neutron",
	}

	vulnerabilities, err := provider.FindVulnerabilities(search.ByDistro(distro.Distro{}), search.ByPackageName(p.Name))

	require.Empty(t, vulnerabilities)
	require.NoError(t, err)
}

func Test_FindVulnerabilitiesByCPE(t *testing.T) {

	tests := []struct {
		name     string
		cpe      cpe.CPE
		expected []vulnerability.Vulnerability
		err      bool
	}{
		{
			name: "match from name and target SW",
			cpe:  cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:ruby:*:*", ""),
			expected: []vulnerability.Vulnerability{
				{
					PackageName: "activerecord",
					Constraint:  version.MustGetConstraint("< 3.7.4", version.UnknownFormat),
					Reference: vulnerability.Reference{
						ID:        "CVE-2014-fake-4",
						Namespace: "nvd:cpe",
					},
					CPEs: []cpe.CPE{
						cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*", ""),
					},
					PackageQualifiers: []qualifier.Qualifier{},
					Advisories:        []vulnerability.Advisory{},
					Metadata: &vulnerability.Metadata{
						ID:          "CVE-2014-fake-4",
						DataSource:  "http://somewhere/CVE-2014-fake-4",
						Namespace:   "nvd:cpe",
						Severity:    "High",
						URLs:        nil,
						Description: "CVE-2014-fake-4-description",
					},
				},
			},
		},
		{
			name: "match with normalization",
			cpe:  cpe.Must("cpe:2.3:*:ActiVERecord:ACTiveRecord:*:*:*:*:*:ruby:*:*", ""),
			expected: []vulnerability.Vulnerability{
				{
					PackageName: "activerecord",
					Constraint:  version.MustGetConstraint("< 3.7.4", version.UnknownFormat),
					Reference: vulnerability.Reference{
						ID:        "CVE-2014-fake-4",
						Namespace: "nvd:cpe",
					},
					CPEs: []cpe.CPE{
						cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*", ""),
					},
					PackageQualifiers: []qualifier.Qualifier{},
					Advisories:        []vulnerability.Advisory{},
					Metadata: &vulnerability.Metadata{
						ID:          "CVE-2014-fake-4",
						DataSource:  "http://somewhere/CVE-2014-fake-4",
						Namespace:   "nvd:cpe",
						Severity:    "High",
						URLs:        nil,
						Description: "CVE-2014-fake-4-description",
					},
				},
			},
		},
		{
			name: "match from vendor & name",
			cpe:  cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:*:*:*", ""),
			expected: []vulnerability.Vulnerability{
				{
					PackageName: "activerecord",
					Constraint:  version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
					Reference: vulnerability.Reference{
						ID:        "CVE-2014-fake-3",
						Namespace: "nvd:cpe",
					},
					CPEs: []cpe.CPE{
						cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*", ""),
					},
					PackageQualifiers: []qualifier.Qualifier{},
					Advisories:        []vulnerability.Advisory{},
					Metadata: &vulnerability.Metadata{
						ID:          "CVE-2014-fake-3",
						DataSource:  "http://somewhere/CVE-2014-fake-3",
						Namespace:   "nvd:cpe",
						Severity:    "High",
						URLs:        nil,
						Description: "CVE-2014-fake-3-description",
					},
				},
				{
					PackageName: "activerecord",
					Constraint:  version.MustGetConstraint("< 3.7.4", version.UnknownFormat),
					Reference: vulnerability.Reference{
						ID:        "CVE-2014-fake-4",
						Namespace: "nvd:cpe",
					},
					CPEs: []cpe.CPE{
						cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*", ""),
					},
					PackageQualifiers: []qualifier.Qualifier{},
					Advisories:        []vulnerability.Advisory{},
					Metadata: &vulnerability.Metadata{
						ID:          "CVE-2014-fake-4",
						DataSource:  "http://somewhere/CVE-2014-fake-4",
						Namespace:   "nvd:cpe",
						Severity:    "High",
						URLs:        nil,
						Description: "CVE-2014-fake-4-description",
					},
				},
				{
					PackageName: "activerecord",
					Constraint:  version.MustGetConstraint("< 70.3.0-rc0", version.ApkFormat),
					Reference: vulnerability.Reference{
						ID:        "CVE-2014-fake-7",
						Namespace: "nvd:cpe",
					},
					CPEs: []cpe.CPE{
						cpe.Must("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*", ""),
					},
					PackageQualifiers: []qualifier.Qualifier{},
					Advisories:        []vulnerability.Advisory{},
					Metadata: &vulnerability.Metadata{
						ID:          "CVE-2014-fake-7",
						DataSource:  "http://somewhere/CVE-2014-fake-7",
						Namespace:   "nvd:cpe",
						Severity:    "High",
						URLs:        nil,
						Description: "CVE-2014-fake-7-description",
					},
				},
			},
		},
		{
			name: "allow query with only product",
			cpe:  cpe.Must("cpe:2.3:a:*:product:*:*:*:*:*:*:*:*", ""),
		},
		{
			name: "do not allow query without product",
			cpe: cpe.CPE{
				Attributes: cpe.Attributes{
					Part:   "a",
					Vendor: "v",
				},
			},
			err: true,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			provider := testVulnerabilityProvider(t)

			actual, err := provider.FindVulnerabilities(search.ByCPE(test.cpe))
			if err != nil && !test.err {
				t.Fatalf("expected no err, got: %+v", err)
			} else if err == nil && test.err {
				t.Fatalf("expected an err, gots" +
					" none")
			}

			require.Len(t, actual, len(test.expected))

			for idx, vuln := range actual {
				if d := cmp.Diff(test.expected[idx], vuln, cmpOpts()...); d != "" {
					t.Errorf("diff: %+v", d)
				}
			}
		})
	}

}

func Test_FindVulnerabilitiesByID(t *testing.T) {
	provider := testVulnerabilityProvider(t)

	d := distro.New(distro.Debian, "8", "")

	// with distro
	actual, err := provider.FindVulnerabilities(search.ByDistro(*d), search.ByID("CVE-2014-fake-1"))
	require.NoError(t, err)

	expected := []vulnerability.Vulnerability{
		{
			Reference: vulnerability.Reference{
				ID:        "CVE-2014-fake-1",
				Namespace: "debian:distro:debian:8",
			},
			PackageName:       "neutron",
			Constraint:        version.MustGetConstraint("< 2014.1.3-6", version.DebFormat),
			PackageQualifiers: []qualifier.Qualifier{},
			CPEs:              nil,
			Advisories:        []vulnerability.Advisory{},
			Metadata: &vulnerability.Metadata{
				ID:          "CVE-2014-fake-1",
				DataSource:  "http://somewhere/CVE-2014-fake-1",
				Namespace:   "debian:distro:debian:8",
				Severity:    "High",
				URLs:        nil,
				Description: "CVE-2014-fake-1-description",
			},
			RelatedVulnerabilities: []vulnerability.Reference{{ID: "CVE-2014-fake-1", Namespace: "nvd:cpe"}},
		},
	}

	require.Len(t, actual, len(expected))

	for idx, vuln := range actual {
		if d := cmp.Diff(expected[idx], vuln, cmpOpts()...); d != "" {
			t.Errorf("diff: %+v", d)
		}
	}

	// without distro
	actual, err = provider.FindVulnerabilities(search.ByID("CVE-2014-fake-1"))
	require.NoError(t, err)

	for idx, vuln := range actual {
		if d := cmp.Diff(expected[idx], vuln, cmpOpts()...); d != "" {
			t.Errorf("diff: %+v", d)
		}
	}
}

func Test_FindVulnerabilitiesByEcosystem_UnknownPackageType(t *testing.T) {
	tests := []struct {
		name        string
		packageName string
		packageType syftPkg.Type
		language    syftPkg.Language
		expectedIDs []string
	}{
		{
			name:        "known package type",
			packageName: "Newtonsoft.Json",
			packageType: syftPkg.DotnetPkg,
			language:    syftPkg.Java, // deliberately wrong to prove we're using package type
			expectedIDs: []string{"GHSA-5crp-9r3c-p9vr"},
		},
		{
			name:        "unknown package type, known language",
			packageName: "Newtonsoft.Json",
			packageType: syftPkg.UnknownPkg,
			language:    syftPkg.Dotnet,
			expectedIDs: []string{"GHSA-5crp-9r3c-p9vr"},
		},
		{
			name:        "unknown package type, unknown language",
			packageName: "Newtonsoft.Json",
			packageType: syftPkg.UnknownPkg,
			language:    syftPkg.UnknownLanguage,
			// The vuln GHSA-5crp-9r3c-p9vr is specifically associated
			// with the dotnet ecosystem, so it should not be returned here.
			// In a real search for UnknownPkg + UnknownLanguage, there should
			// be a separate search.ByCPE run that _does_ return it.
			expectedIDs: []string{},
		},
	}
	provider := testVulnerabilityProvider(t)
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			actual, err := provider.FindVulnerabilities(
				search.ByEcosystem(test.language, test.packageType),
				search.ByPackageName(test.packageName),
			)
			require.NoError(t, err)
			actualIDs := make([]string, len(actual))
			for idx, vuln := range actual {
				actualIDs[idx] = vuln.ID
			}
			if d := cmp.Diff(test.expectedIDs, actualIDs); d != "" {
				t.Errorf("diff: %+v", d)
			}
		})
	}
}

func Test_DataSource(t *testing.T) {
	tests := []struct {
		name     string
		vuln     VulnerabilityHandle
		expected vulnerability.Metadata
	}{
		{
			name: "no reference urls",
			vuln: VulnerabilityHandle{
				BlobValue: &VulnerabilityBlob{
					References: nil,
				},
			},
			expected: vulnerability.Metadata{
				DataSource: "",
				URLs:       nil,
			},
		},
		{
			name: "one reference url",
			vuln: VulnerabilityHandle{
				BlobValue: &VulnerabilityBlob{
					References: []Reference{
						{
							URL: "url1",
						},
					},
				},
			},
			expected: vulnerability.Metadata{
				DataSource: "url1",
				URLs:       nil,
			},
		},
		{
			name: "two reference urls",
			vuln: VulnerabilityHandle{
				BlobValue: &VulnerabilityBlob{
					References: []Reference{
						{
							URL: "url1",
						},
						{
							URL: "url2",
						},
					},
				},
			},
			expected: vulnerability.Metadata{
				DataSource: "url1",
				URLs:       []string{"url2"},
			},
		},
		{
			name: "many reference urls",
			vuln: VulnerabilityHandle{
				BlobValue: &VulnerabilityBlob{
					References: []Reference{
						{
							URL: "url4",
						},
						{
							URL: "url3",
						},
						{
							URL: "url2",
						},
						{
							URL: "url1",
						},
					},
				},
			},
			expected: vulnerability.Metadata{
				DataSource: "url4",
				URLs:       []string{"url3", "url2", "url1"},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := newVulnerabilityMetadata(&tt.vuln, "", nil, nil, nil)
			got.Severity = ""
			require.NoError(t, err)
			if diff := cmp.Diff(&tt.expected, got, cmpOpts()...); diff != "" {
				t.Fatal(diff)
			}
		})
	}
}

func Test_filterAffectedPackageRanges(t *testing.T) {
	tests := []struct {
		name                     string
		ranges                   []Range
		matchesConstraint        func(constraint version.Constraint) (bool, error)
		expectedAllRangesRemoved bool
		expectedUnmatchedStrings []string
	}{
		{
			name:                     "no ranges",
			ranges:                   nil,
			expectedAllRangesRemoved: false, // important! we assume that a vulnerability with no ranges is always vulnerable
			expectedUnmatchedStrings: nil,
		},
		{
			name: "has ranges within constraint",
			ranges: []Range{
				{
					Version: Version{
						Type:       "rpm",
						Constraint: "< 1.0.0",
					},
				},
				{
					Version: Version{
						Type:       "rpm",
						Constraint: "< 2.0.0",
					},
				},
			},
			matchesConstraint: func(constraint version.Constraint) (bool, error) {
				return true, nil
			},
			expectedAllRangesRemoved: false,
			expectedUnmatchedStrings: nil,
		},
		{
			name: "has ranges outside constraint",
			ranges: []Range{
				{
					Version: Version{
						Type:       "rpm",
						Constraint: "< 1.0.0",
					},
				},
				{
					Version: Version{
						Type:       "rpm",
						Constraint: "< 2.0.0",
					},
				},
			},
			matchesConstraint: func(constraint version.Constraint) (bool, error) {
				return false, nil
			},
			expectedAllRangesRemoved: true,
			expectedUnmatchedStrings: []string{"< 1.0.0", "< 2.0.0"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockMatcher := &mockVersionConstraintMatcher{
				matchesConstraintFunc: tt.matchesConstraint,
			}

			blob := &PackageBlob{
				Ranges: tt.ranges,
			}

			allRangesRemoved, unmatchedConstraints := filterAffectedPackageRanges(mockMatcher, blob)

			require.Equal(t, tt.expectedAllRangesRemoved, allRangesRemoved)
			require.Equal(t, tt.expectedUnmatchedStrings, unmatchedConstraints)
		})
	}
}

type mockVersionConstraintMatcher struct {
	matchesConstraintFunc func(constraint version.Constraint) (bool, error)
}

func (m *mockVersionConstraintMatcher) MatchesConstraint(constraint version.Constraint) (bool, error) {
	if m.matchesConstraintFunc != nil {
		return m.matchesConstraintFunc(constraint)
	}
	return false, nil
}

func Test_FindVulnerabilitiesByUnaffectedCriteria(t *testing.T) {
	provider := testVulnerabilityProvider(t)

	tests := []struct {
		name        string
		criteria    []vulnerability.Criteria
		expected    []vulnerability.Vulnerability
		description string
	}{
		{
			name: "search for unaffected packages",
			criteria: []vulnerability.Criteria{
				search.ForUnaffected(),
				search.ByPackageName("test-unaffected-package"),
			},
			expected: []vulnerability.Vulnerability{
				{
					PackageName: "test-unaffected-package",
					Constraint:  version.MustGetConstraint("< 1.0.0", version.DebFormat),
					Reference: vulnerability.Reference{
						ID:        "CVE-2024-unaffected-test",
						Namespace: "nvd:language:deb",
					},
					PackageQualifiers:      []qualifier.Qualifier{},
					CPEs:                   nil,
					Advisories:             []vulnerability.Advisory{},
					RelatedVulnerabilities: nil,
					Metadata: &vulnerability.Metadata{
						ID:          "CVE-2024-unaffected-test",
						DataSource:  "http://somewhere/CVE-2024-unaffected-test",
						Namespace:   "nvd:language:deb",
						Severity:    "Medium",
						URLs:        nil,
						Description: "CVE-2024-unaffected-test-description",
					},
					Unaffected: true,
				},
			},
			description: "should use unaffected package store",
		},
		{
			name: "search for unaffected CPEs",
			criteria: []vulnerability.Criteria{
				search.ForUnaffected(),
				search.ByCPE(cpe.Must("cpe:2.3:a:test:unaffected:*:*:*:*:*:*:*:*", "")),
			},
			expected: []vulnerability.Vulnerability{
				{
					PackageName: "unaffected",
					Constraint:  version.MustGetConstraint("< 1.0.0", version.UnknownFormat),
					Reference: vulnerability.Reference{
						ID:        "CVE-2024-unaffected-test",
						Namespace: "nvd:cpe",
					},
					PackageQualifiers:      []qualifier.Qualifier{},
					CPEs:                   []cpe.CPE{cpe.Must("cpe:2.3:a:test:unaffected:*:*:*:*:*:*:*:*", "")},
					Advisories:             []vulnerability.Advisory{},
					RelatedVulnerabilities: nil,
					Metadata: &vulnerability.Metadata{
						ID:          "CVE-2024-unaffected-test",
						DataSource:  "http://somewhere/CVE-2024-unaffected-test",
						Namespace:   "nvd:cpe",
						Severity:    "Medium",
						URLs:        nil,
						Description: "CVE-2024-unaffected-test-description",
					},
					Unaffected: true,
				},
			},
			description: "should use unaffected CPE store",
		},
		{
			name: "search with unaffected criteria and distro",
			criteria: []vulnerability.Criteria{
				search.ForUnaffected(),
				search.ByDistro(*distro.New(distro.Debian, "8", "")),
				search.ByPackageName("neutron"),
			},
			expected: []vulnerability.Vulnerability{
				{
					PackageName: "neutron",
					Constraint:  version.MustGetConstraint(">= 2015.0.0", version.DebFormat),
					Reference: vulnerability.Reference{
						ID:        "CVE-2024-unaffected-test",
						Namespace: "nvd:distro:debian:8",
					},
					PackageQualifiers:      []qualifier.Qualifier{},
					CPEs:                   nil,
					Advisories:             []vulnerability.Advisory{},
					RelatedVulnerabilities: nil,
					Metadata: &vulnerability.Metadata{
						ID:          "CVE-2024-unaffected-test",
						DataSource:  "http://somewhere/CVE-2024-unaffected-test",
						Namespace:   "nvd:distro:debian:8",
						Severity:    "Medium",
						URLs:        nil,
						Description: "CVE-2024-unaffected-test-description",
					},
					Unaffected: true,
				},
			},
			description: "should combine unaffected and distro criteria",
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			actual, err := provider.FindVulnerabilities(test.criteria...)
			require.NoError(t, err, test.description)
			require.Len(t, actual, len(test.expected), test.description)

			for idx, vuln := range actual {
				if d := cmp.Diff(test.expected[idx], vuln, cmpOpts()...); d != "" {
					t.Errorf("diff: %+v", d)
				}
			}
		})
	}
}

func Test_FindVulnerabilitiesErrorHandling(t *testing.T) {
	provider := testVulnerabilityProvider(t)

	tests := []struct {
		name        string
		criteria    []vulnerability.Criteria
		expectError bool
		description string
	}{
		{
			name: "CPE without product should error",
			criteria: []vulnerability.Criteria{
				search.ByCPE(cpe.CPE{
					Attributes: cpe.Attributes{
						Part:   "a",
						Vendor: "vendor",
						// Product is missing - should cause error
					},
				}),
			},
			expectError: true,
			description: "CPE searches require a product",
		},
		{
			name:        "empty criteria should work",
			criteria:    []vulnerability.Criteria{},
			expectError: false,
			description: "empty criteria should be handled gracefully",
		},
		{
			name: "unaffected with CPE without product should error",
			criteria: []vulnerability.Criteria{
				search.ForUnaffected(),
				search.ByCPE(cpe.CPE{
					Attributes: cpe.Attributes{
						Part:   "a",
						Vendor: "vendor",
					},
				}),
			},
			expectError: true,
			description: "unaffected CPE searches also require a product",
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			_, err := provider.FindVulnerabilities(test.criteria...)
			if test.expectError {
				require.Error(t, err, test.description)
			} else {
				require.NoError(t, err, test.description)
			}
		})
	}
}

func Test_FindVulnerabilitiesMixedCriteria(t *testing.T) {
	provider := testVulnerabilityProvider(t)

	// test complex criteria combinations
	d := distro.New(distro.Debian, "8", "")

	tests := []struct {
		name        string
		criteria    []vulnerability.Criteria
		expectedLen int
		description string
	}{
		{
			name: "package name with version constraint",
			criteria: []vulnerability.Criteria{
				search.ByDistro(*d),
				search.ByPackageName("neutron"),
				search.ByVersion(*version.New("1.0.0", version.DebFormat)),
			},
			expectedLen: 2, // based on existing test data
			description: "should find vulnerabilities matching version constraints",
		},
		{
			name: "ID search with distro",
			criteria: []vulnerability.Criteria{
				search.ByDistro(*d),
				search.ByID("CVE-2014-fake-1"),
			},
			expectedLen: 1,
			description: "should find specific vulnerability by ID and distro",
		},
		{
			name: "ecosystem search with package name",
			criteria: []vulnerability.Criteria{
				search.ByEcosystem(syftPkg.Dotnet, syftPkg.DotnetPkg),
				search.ByPackageName("Newtonsoft.Json"),
			},
			expectedLen: 1,
			description: "should find vulnerabilities by ecosystem",
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			actual, err := provider.FindVulnerabilities(test.criteria...)
			require.NoError(t, err, test.description)
			require.Len(t, actual, test.expectedLen, test.description)
		})
	}
}

func Test_FindVulnerabilitiesNormalization(t *testing.T) {
	provider := testVulnerabilityProvider(t)

	tests := []struct {
		name            string
		packageName     string
		packageType     syftPkg.Type
		expectedResults int
		description     string
	}{
		{
			name:            "package name normalization with known type",
			packageName:     "Newtonsoft.Json", // mixed case
			packageType:     syftPkg.DotnetPkg,
			expectedResults: 1,
			description:     "package names should be normalized based on ecosystem",
		},
		{
			name:            "package name with unknown type",
			packageName:     "SomePackage",
			packageType:     syftPkg.UnknownPkg,
			expectedResults: 0, // likely no matches
			description:     "unknown package types should still be handled",
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			actual, err := provider.FindVulnerabilities(
				search.ByEcosystem(syftPkg.UnknownLanguage, test.packageType),
				search.ByPackageName(test.packageName),
			)
			require.NoError(t, err, test.description)
			require.Len(t, actual, test.expectedResults, test.description)
		})
	}
}

func Test_HandleOSError(t *testing.T) {
	provider := testVulnerabilityProvider(t).(*vulnerabilityProvider)

	osSpecs := OSSpecifiers{&OSSpecifier{Name: "debian", MajorVersion: "8"}}

	tests := []struct {
		name        string
		inputError  error
		expectError bool
		description string
	}{
		{
			name:        "nil error returns nil",
			inputError:  nil,
			expectError: false,
			description: "nil errors should pass through as nil",
		},
		{
			name:        "ErrOSNotPresent returns nil",
			inputError:  ErrOSNotPresent,
			expectError: false,
			description: "ErrOSNotPresent should be handled and return nil",
		},
		{
			name:        "other errors return unchanged",
			inputError:  errors.New("some other error"),
			expectError: true,
			description: "other errors should be returned unchanged",
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			err := provider.handleOSError(test.inputError, osSpecs)
			if test.expectError {
				require.Error(t, err, test.description)
			} else {
				require.NoError(t, err, test.description)
			}
		})
	}
}

func Test_FetchAndProcessPackagesEdgeCases(t *testing.T) {
	provider := testVulnerabilityProvider(t).(*vulnerabilityProvider)

	tests := []struct {
		name        string
		ctx         *searchQuery
		description string
	}{
		{
			name: "context with no specs should return empty results",
			ctx: &searchQuery{
				// no pkgSpec, no vulnSpecs
				osSpecs:        OSSpecifiers{NoOSSpecified},
				unaffectedOnly: false,
			},
			description: "should handle context with no package or vulnerability specs",
		},
		{
			name: "unaffected context with no specs should return empty results",
			ctx: &searchQuery{
				// no pkgSpec, no vulnSpecs
				osSpecs:        OSSpecifiers{NoOSSpecified},
				unaffectedOnly: true,
			},
			description: "should handle unaffected context with no package or vulnerability specs",
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			vulns, err := provider.fetchAndProcessPackages(test.ctx)
			require.NoError(t, err, test.description)
			require.Empty(t, vulns, "should return empty vulnerabilities")
		})
	}
}

func Test_FetchAndProcessCPEsEdgeCases(t *testing.T) {
	provider := testVulnerabilityProvider(t).(*vulnerabilityProvider)

	tests := []struct {
		name        string
		ctx         *searchQuery
		description string
	}{
		{
			name: "context with no CPE spec should return empty results",
			ctx: &searchQuery{
				// no cpeSpec
				osSpecs:        OSSpecifiers{NoOSSpecified},
				unaffectedOnly: false,
			},
			description: "should handle context with no CPE spec",
		},
		{
			name: "unaffected context with no CPE spec should return empty results",
			ctx: &searchQuery{
				// no cpeSpec
				osSpecs:        OSSpecifiers{NoOSSpecified},
				unaffectedOnly: true,
			},
			description: "should handle unaffected context with no CPE spec",
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			vulns, err := provider.fetchAndProcessCPEs(test.ctx)
			require.NoError(t, err, test.description)
			require.Empty(t, vulns, "should return empty vulnerabilities")
		})
	}
}

func cmpOpts() []cmp.Option {
	return []cmp.Option{
		// globally ignore unexported -- these are unexported structs we cannot reference here to use cmpopts.IgnoreUnexported
		cmp.FilterPath(func(p cmp.Path) bool {
			sf, ok := p.Index(-1).(cmp.StructField)
			if !ok {
				return false
			}
			r, _ := utf8.DecodeRuneInString(sf.Name())
			return !unicode.IsUpper(r)
		}, cmp.Ignore()),
		cmpopts.EquateEmpty(),
		cmpopts.IgnoreFields(vulnerability.Reference{}, "Internal"),
	}
}
