package grype

import (
	"testing"

	"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/db"
	grypeDB "github.com/anchore/grype/grype/db/v5"
	"github.com/anchore/grype/grype/grypeerr"
	"github.com/anchore/grype/grype/match"
	"github.com/anchore/grype/grype/matcher"
	"github.com/anchore/grype/grype/matcher/ruby"
	"github.com/anchore/grype/grype/pkg"
	"github.com/anchore/grype/grype/pkg/qualifier"
	"github.com/anchore/grype/grype/search"
	"github.com/anchore/grype/grype/store"
	"github.com/anchore/grype/grype/version"
	"github.com/anchore/grype/grype/vulnerability"
	"github.com/anchore/syft/syft/linux"
	syftPkg "github.com/anchore/syft/syft/pkg"
)

type ack interface {
	grypeDB.VulnerabilityStoreReader
	grypeDB.VulnerabilityMetadataStoreReader
	grypeDB.VulnerabilityMatchExclusionStoreReader
}

var _ ack = (*mockStore)(nil)

type mockStore struct {
	vulnerabilities map[string]map[string][]grypeDB.Vulnerability
	metadata        map[string]map[string]*grypeDB.VulnerabilityMetadata
}

func (d *mockStore) GetVulnerabilityMatchExclusion(id string) ([]grypeDB.VulnerabilityMatchExclusion, error) {
	//panic("implement me")
	return nil, nil
}

func newMockStore() *mockStore {
	d := mockStore{
		vulnerabilities: make(map[string]map[string][]grypeDB.Vulnerability),
		metadata:        make(map[string]map[string]*grypeDB.VulnerabilityMetadata),
	}
	d.stub()
	return &d
}

func (d *mockStore) stub() {
	// METADATA /////////////////////////////////////////////////////////////////////////////////
	d.metadata["CVE-2014-fake-1"] = map[string]*grypeDB.VulnerabilityMetadata{
		"debian:distro:debian:8": {
			Severity: "medium",
		},
	}

	d.metadata["GHSA-2014-fake-3"] = map[string]*grypeDB.VulnerabilityMetadata{
		"github:language:ruby": {
			Severity: "medium",
		},
	}

	// VULNERABILITIES ///////////////////////////////////////////////////////////////////////////
	d.vulnerabilities["debian:distro:debian:8"] = map[string][]grypeDB.Vulnerability{
		"neutron": {
			{
				PackageName:       "neutron",
				Namespace:         "debian:distro:debian:8",
				VersionConstraint: "< 2014.1.3-6",
				ID:                "CVE-2014-fake-1",
				VersionFormat:     "deb",
			},
			{
				PackageName:       "neutron",
				Namespace:         "debian:distro:debian:8",
				VersionConstraint: "< 2013.0.2-1",
				ID:                "CVE-2013-fake-2",
				VersionFormat:     "deb",
			},
		},
	}
	d.vulnerabilities["github:language:ruby"] = map[string][]grypeDB.Vulnerability{
		"activerecord": {
			{
				PackageName:       "activerecord",
				Namespace:         "github:language:ruby",
				VersionConstraint: "< 3.7.6",
				ID:                "GHSA-2014-fake-3",
				VersionFormat:     "unknown",
				RelatedVulnerabilities: []grypeDB.VulnerabilityReference{
					{
						ID:        "CVE-2014-fake-3",
						Namespace: "nvd:cpe",
					},
				},
			},
		},
	}
	d.vulnerabilities["nvd:cpe"] = map[string][]grypeDB.Vulnerability{
		"activerecord": {
			{
				PackageName:       "activerecord",
				Namespace:         "nvd:cpe",
				VersionConstraint: "< 3.7.6",
				ID:                "CVE-2014-fake-3",
				VersionFormat:     "unknown",
				CPEs: []string{
					"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
				},
			},
			{
				PackageName:       "activerecord",
				Namespace:         "nvd:cpe",
				VersionConstraint: "< 3.7.4",
				ID:                "CVE-2014-fake-4",
				VersionFormat:     "unknown",
				CPEs: []string{
					"cpe:2.3:*:activerecord:activerecord:*:*:something:*:*:ruby:*:*",
				},
			},
			{
				PackageName:       "activerecord",
				Namespace:         "nvd:cpe",
				VersionConstraint: "= 4.0.1",
				ID:                "CVE-2014-fake-5",
				VersionFormat:     "unknown",
				CPEs: []string{
					"cpe:2.3:*:couldntgetthisrightcouldyou:activerecord:4.0.1:*:*:*:*:*:*:*",
				},
			},
			{
				PackageName:       "activerecord",
				Namespace:         "nvd:cpe",
				VersionConstraint: "< 98SP3",
				ID:                "CVE-2014-fake-6",
				VersionFormat:     "unknown",
				CPEs: []string{
					"cpe:2.3:*:awesome:awesome:*:*:*:*:*:*:*:*",
				},
			},
		},
	}
}

func (d *mockStore) GetVulnerabilityMetadata(id, namespace string) (*grypeDB.VulnerabilityMetadata, error) {
	return d.metadata[id][namespace], nil
}

func (d *mockStore) GetAllVulnerabilityMetadata() (*[]grypeDB.VulnerabilityMetadata, error) {
	panic("implement me")
}

func (d *mockStore) GetVulnerability(namespace, id string) ([]grypeDB.Vulnerability, error) {
	var results []grypeDB.Vulnerability
	for _, vulns := range d.vulnerabilities[namespace] {
		for _, vuln := range vulns {
			if vuln.ID == id {
				results = append(results, vuln)
			}
		}
	}
	return results, nil
}

func (d *mockStore) SearchForVulnerabilities(namespace, name string) ([]grypeDB.Vulnerability, error) {
	return d.vulnerabilities[namespace][name], nil
}

func (d *mockStore) GetAllVulnerabilities() (*[]grypeDB.Vulnerability, error) {
	panic("implement me")
}

func (d *mockStore) GetVulnerabilityNamespaces() ([]string, error) {
	keys := make([]string, 0, len(d.vulnerabilities))
	for k := range d.vulnerabilities {
		keys = append(keys, k)
	}

	return keys, nil
}

func Test_HasSeverityAtOrAbove(t *testing.T) {
	thePkg := pkg.Package{
		ID:      pkg.ID(uuid.NewString()),
		Name:    "the-package",
		Version: "v0.1",
		Type:    syftPkg.RpmPkg,
	}

	matches := match.NewMatches()
	matches.Add(match.Match{
		Vulnerability: vulnerability.Vulnerability{
			ID:        "CVE-2014-fake-1",
			Namespace: "debian:distro:debian:8",
		},
		Package: thePkg,
		Details: match.Details{
			{
				Type: match.ExactDirectMatch,
			},
		},
	})

	tests := []struct {
		name           string
		failOnSeverity string
		matches        match.Matches
		expectedResult bool
	}{
		{
			name:           "no-severity-set",
			failOnSeverity: "",
			matches:        matches,
			expectedResult: false,
		},
		{
			name:           "below-threshold",
			failOnSeverity: "high",
			matches:        matches,
			expectedResult: false,
		},
		{
			name:           "at-threshold",
			failOnSeverity: "medium",
			matches:        matches,
			expectedResult: true,
		},
		{
			name:           "above-threshold",
			failOnSeverity: "low",
			matches:        matches,
			expectedResult: true,
		},
	}

	metadataProvider := db.NewVulnerabilityMetadataProvider(newMockStore())

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			var failOnSeverity vulnerability.Severity
			if test.failOnSeverity != "" {
				sev := vulnerability.ParseSeverity(test.failOnSeverity)
				if sev == vulnerability.UnknownSeverity {
					t.Fatalf("could not parse severity")
				}
				failOnSeverity = sev
			}

			actual := HasSeverityAtOrAbove(metadataProvider, failOnSeverity, test.matches)

			if test.expectedResult != actual {
				t.Errorf("expected: %v got : %v", test.expectedResult, actual)
			}
		})
	}
}

func TestVulnerabilityMatcher_FindMatches(t *testing.T) {
	mkStr := newMockStore()
	vp, err := db.NewVulnerabilityProvider(mkStr)
	require.NoError(t, err)
	str := store.Store{
		Provider:          vp,
		MetadataProvider:  db.NewVulnerabilityMetadataProvider(mkStr),
		ExclusionProvider: db.NewMatchExclusionProvider(mkStr),
	}

	neutron2013Pkg := pkg.Package{
		ID:      pkg.ID(uuid.NewString()),
		Name:    "neutron",
		Version: "2013.1.1-1",
		Type:    syftPkg.DebPkg,
	}

	mustCPE := func(c string) syftPkg.CPE {
		cp, err := syftPkg.NewCPE(c)
		if err != nil {
			t.Fatal(err)
		}
		return cp
	}

	activerecordPkg := pkg.Package{
		ID:      pkg.ID(uuid.NewString()),
		Name:    "activerecord",
		Version: "3.7.5",
		CPEs: []syftPkg.CPE{
			mustCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"),
		},
		Type:     syftPkg.GemPkg,
		Language: syftPkg.Ruby,
	}

	type fields struct {
		Store          store.Store
		Matchers       []matcher.Matcher
		IgnoreRules    []match.IgnoreRule
		FailSeverity   *vulnerability.Severity
		NormalizeByCVE bool
	}
	type args struct {
		pkgs    []pkg.Package
		context pkg.Context
	}

	tests := []struct {
		name               string
		fields             fields
		args               args
		wantMatches        match.Matches
		wantIgnoredMatches []match.IgnoredMatch
		wantErr            error
	}{
		{
			name: "no matches",
			fields: fields{
				Store:    str,
				Matchers: matcher.NewDefaultMatchers(matcher.Config{}),
			},
			args: args{
				pkgs: []pkg.Package{
					{
						ID:      pkg.ID(uuid.NewString()),
						Name:    "neutrino",
						Version: "2099.1.1-1",
						Type:    syftPkg.DebPkg,
					},
				},
				context: pkg.Context{
					Distro: &linux.Release{
						ID:        "debian",
						VersionID: "8",
					},
				},
			},
		},
		{
			name: "matches by exact-direct match (OS)",
			fields: fields{
				Store:    str,
				Matchers: matcher.NewDefaultMatchers(matcher.Config{}),
			},
			args: args{
				pkgs: []pkg.Package{
					neutron2013Pkg,
				},
				context: pkg.Context{
					Distro: &linux.Release{
						ID:        "debian",
						VersionID: "8",
					},
				},
			},
			wantMatches: match.NewMatches(
				match.Match{
					Vulnerability: vulnerability.Vulnerability{
						Constraint:        version.MustGetConstraint("< 2014.1.3-6", version.DebFormat),
						ID:                "CVE-2014-fake-1",
						Namespace:         "debian:distro:debian:8",
						PackageQualifiers: []qualifier.Qualifier{},
						CPEs:              []syftPkg.CPE{},
						Advisories:        []vulnerability.Advisory{},
					},
					Package: neutron2013Pkg,
					Details: match.Details{
						{
							Type: match.ExactDirectMatch,
							SearchedBy: map[string]any{
								"distro":    map[string]string{"type": "debian", "version": "8"},
								"namespace": "debian:distro:debian:8",
								"package":   map[string]string{"name": "neutron", "version": "2013.1.1-1"},
							},
							Found: map[string]any{
								"versionConstraint": "< 2014.1.3-6 (deb)",
								"vulnerabilityID":   "CVE-2014-fake-1",
							},
							Matcher:    "dpkg-matcher",
							Confidence: 1,
						},
					},
				},
			),
			wantIgnoredMatches: nil,
			wantErr:            nil,
		},
		{
			name: "fail on severity threshold",
			fields: fields{
				Store:    str,
				Matchers: matcher.NewDefaultMatchers(matcher.Config{}),
				FailSeverity: func() *vulnerability.Severity {
					x := vulnerability.LowSeverity
					return &x
				}(),
			},
			args: args{
				pkgs: []pkg.Package{
					neutron2013Pkg,
				},
				context: pkg.Context{
					Distro: &linux.Release{
						ID:        "debian",
						VersionID: "8",
					},
				},
			},
			wantMatches: match.NewMatches(
				match.Match{
					Vulnerability: vulnerability.Vulnerability{
						Constraint:        version.MustGetConstraint("< 2014.1.3-6", version.DebFormat),
						ID:                "CVE-2014-fake-1",
						Namespace:         "debian:distro:debian:8",
						PackageQualifiers: []qualifier.Qualifier{},
						CPEs:              []syftPkg.CPE{},
						Advisories:        []vulnerability.Advisory{},
					},
					Package: neutron2013Pkg,
					Details: match.Details{
						{
							Type: match.ExactDirectMatch,
							SearchedBy: map[string]any{
								"distro":    map[string]string{"type": "debian", "version": "8"},
								"namespace": "debian:distro:debian:8",
								"package":   map[string]string{"name": "neutron", "version": "2013.1.1-1"},
							},
							Found: map[string]any{
								"versionConstraint": "< 2014.1.3-6 (deb)",
								"vulnerabilityID":   "CVE-2014-fake-1",
							},
							Matcher:    "dpkg-matcher",
							Confidence: 1,
						},
					},
				},
			),
			wantIgnoredMatches: nil,
			wantErr:            grypeerr.ErrAboveSeverityThreshold,
		},
		{
			name: "matches by exact-direct match (language)",
			fields: fields{
				Store: str,
				Matchers: matcher.NewDefaultMatchers(matcher.Config{
					Ruby: ruby.MatcherConfig{
						UseCPEs: true,
					},
				}),
			},
			args: args{
				pkgs: []pkg.Package{
					activerecordPkg,
				},
				context: pkg.Context{},
			},
			wantMatches: match.NewMatches(
				match.Match{
					Vulnerability: vulnerability.Vulnerability{
						Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
						ID:         "CVE-2014-fake-3",
						Namespace:  "nvd:cpe",
						CPEs: []syftPkg.CPE{
							mustCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"),
						},
						PackageQualifiers: []qualifier.Qualifier{},
						Advisories:        []vulnerability.Advisory{},
					},
					Package: activerecordPkg,
					Details: match.Details{
						{
							Type: match.CPEMatch,
							SearchedBy: search.CPEParameters{
								Namespace: "nvd:cpe",
								CPEs: []string{
									"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
								},
							},
							Found: search.CPEResult{
								VulnerabilityID:   "CVE-2014-fake-3",
								VersionConstraint: "< 3.7.6 (unknown)",
								CPEs: []string{
									"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
								},
							},
							Matcher:    "ruby-gem-matcher",
							Confidence: 0.9,
						},
					},
				},
				match.Match{
					Vulnerability: vulnerability.Vulnerability{
						Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
						ID:         "GHSA-2014-fake-3",
						Namespace:  "github:language:ruby",
						RelatedVulnerabilities: []vulnerability.Reference{
							{
								ID:        "CVE-2014-fake-3",
								Namespace: "nvd:cpe",
							},
						},
						PackageQualifiers: []qualifier.Qualifier{},
						Advisories:        []vulnerability.Advisory{},
						CPEs:              []syftPkg.CPE{},
					},
					Package: activerecordPkg,
					Details: match.Details{
						{
							Type: match.ExactDirectMatch,
							SearchedBy: map[string]any{
								"language":  "ruby",
								"namespace": "github:language:ruby",
							},
							Found: map[string]any{
								"versionConstraint": "< 3.7.6 (unknown)",
								"vulnerabilityID":   "GHSA-2014-fake-3",
							},
							Matcher:    "ruby-gem-matcher",
							Confidence: 1,
						},
					},
				},
			),
			wantIgnoredMatches: nil,
			wantErr:            nil,
		},
		{
			name: "normalize by cve",
			fields: fields{
				Store: str,
				Matchers: matcher.NewDefaultMatchers(
					matcher.Config{
						Ruby: ruby.MatcherConfig{
							UseCPEs: true,
						},
					},
				),
				NormalizeByCVE: true, // IMPORTANT!
			},
			args: args{
				pkgs: []pkg.Package{
					activerecordPkg,
				},
				context: pkg.Context{},
			},
			wantMatches: match.NewMatches(
				match.Match{
					Vulnerability: vulnerability.Vulnerability{
						Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
						ID:         "CVE-2014-fake-3",
						Namespace:  "nvd:cpe",
						CPEs: []syftPkg.CPE{
							mustCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"),
						},
						PackageQualifiers: []qualifier.Qualifier{},
						Advisories:        []vulnerability.Advisory{},
					},
					Package: activerecordPkg,
					Details: match.Details{
						{
							Type: match.CPEMatch,
							SearchedBy: search.CPEParameters{
								Namespace: "nvd:cpe",
								CPEs: []string{
									"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
								},
							},
							Found: search.CPEResult{
								VulnerabilityID:   "CVE-2014-fake-3",
								VersionConstraint: "< 3.7.6 (unknown)",
								CPEs: []string{
									"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
								},
							},
							Matcher:    "ruby-gem-matcher",
							Confidence: 0.9,
						},
						{
							Type: match.ExactDirectMatch,
							SearchedBy: map[string]any{
								"language":  "ruby",
								"namespace": "github:language:ruby",
							},
							Found: map[string]any{
								"versionConstraint": "< 3.7.6 (unknown)",
								"vulnerabilityID":   "GHSA-2014-fake-3",
							},
							Matcher:    "ruby-gem-matcher",
							Confidence: 1,
						},
					},
				},
			),
			wantIgnoredMatches: nil,
			wantErr:            nil,
		},
		{
			name: "normalize by cve -- ignore GHSA",
			fields: fields{
				Store: str,
				Matchers: matcher.NewDefaultMatchers(
					matcher.Config{
						Ruby: ruby.MatcherConfig{
							UseCPEs: true,
						},
					},
				),
				IgnoreRules: []match.IgnoreRule{
					{
						Vulnerability: "GHSA-2014-fake-3",
					},
				},
				NormalizeByCVE: true, // IMPORTANT!
			},
			args: args{
				pkgs: []pkg.Package{
					activerecordPkg,
				},
				context: pkg.Context{},
			},
			wantMatches: match.NewMatches(
				match.Match{
					Vulnerability: vulnerability.Vulnerability{
						Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
						ID:         "CVE-2014-fake-3",
						Namespace:  "nvd:cpe",
						CPEs: []syftPkg.CPE{
							mustCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"),
						},
						PackageQualifiers: []qualifier.Qualifier{},
						Advisories:        []vulnerability.Advisory{},
					},
					Package: activerecordPkg,
					Details: match.Details{
						{
							Type: match.CPEMatch,
							SearchedBy: search.CPEParameters{
								Namespace: "nvd:cpe",
								CPEs: []string{
									"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
								},
							},
							Found: search.CPEResult{
								VulnerabilityID:   "CVE-2014-fake-3",
								VersionConstraint: "< 3.7.6 (unknown)",
								CPEs: []string{
									"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
								},
							},
							Matcher:    "ruby-gem-matcher",
							Confidence: 0.9,
						},
					},
				},
			),
			wantErr: nil,
		},
		{
			name: "normalize by cve -- ignore CVE",
			fields: fields{
				Store: str,
				Matchers: matcher.NewDefaultMatchers(
					matcher.Config{
						Ruby: ruby.MatcherConfig{
							UseCPEs: true,
						},
					},
				),
				IgnoreRules: []match.IgnoreRule{
					{
						Vulnerability: "CVE-2014-fake-3",
					},
				},
				NormalizeByCVE: true, // IMPORTANT!
			},
			args: args{
				pkgs: []pkg.Package{
					activerecordPkg,
				},
				context: pkg.Context{},
			},
			wantMatches: match.NewMatches(),
			wantIgnoredMatches: []match.IgnoredMatch{
				{
					AppliedIgnoreRules: []match.IgnoreRule{
						{
							Vulnerability: "CVE-2014-fake-3",
						},
					},
					Match: match.Match{
						Vulnerability: vulnerability.Vulnerability{
							Constraint:        version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
							ID:                "CVE-2014-fake-3",
							Namespace:         "nvd:cpe",
							CPEs:              []syftPkg.CPE{},
							PackageQualifiers: []qualifier.Qualifier{},
							Advisories:        []vulnerability.Advisory{},
							RelatedVulnerabilities: []vulnerability.Reference{
								{
									ID:        "GHSA-2014-fake-3",
									Namespace: "github:language:ruby",
								},
							},
						},
						Package: activerecordPkg,
						Details: match.Details{
							{
								Type: match.ExactDirectMatch,
								SearchedBy: map[string]any{
									"language":  "ruby",
									"namespace": "github:language:ruby",
								},
								Found: map[string]any{
									"versionConstraint": "< 3.7.6 (unknown)",
									"vulnerabilityID":   "GHSA-2014-fake-3",
								},
								Matcher:    "ruby-gem-matcher",
								Confidence: 1,
							},
						},
					},
				},
			},
			wantErr: nil,
		},
		{
			name: "ignore CVE (not normalized by CVE)",
			fields: fields{
				Store: str,
				Matchers: matcher.NewDefaultMatchers(matcher.Config{
					Ruby: ruby.MatcherConfig{
						UseCPEs: true,
					},
				}),
				IgnoreRules: []match.IgnoreRule{
					{
						Vulnerability: "CVE-2014-fake-3",
					},
				},
			},
			args: args{
				pkgs: []pkg.Package{
					activerecordPkg,
				},
			},
			wantMatches: match.NewMatches(
				match.Match{
					Vulnerability: vulnerability.Vulnerability{
						Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
						ID:         "GHSA-2014-fake-3",
						Namespace:  "github:language:ruby",
						RelatedVulnerabilities: []vulnerability.Reference{
							{
								ID:        "CVE-2014-fake-3",
								Namespace: "nvd:cpe",
							},
						},
						PackageQualifiers: []qualifier.Qualifier{},
						Advisories:        []vulnerability.Advisory{},
						CPEs:              []syftPkg.CPE{},
					},
					Package: activerecordPkg,
					Details: match.Details{
						{
							Type: match.ExactDirectMatch,
							SearchedBy: map[string]any{
								"language":  "ruby",
								"namespace": "github:language:ruby",
							},
							Found: map[string]any{
								"versionConstraint": "< 3.7.6 (unknown)",
								"vulnerabilityID":   "GHSA-2014-fake-3",
							},
							Matcher:    "ruby-gem-matcher",
							Confidence: 1,
						},
					},
				},
			),
			wantIgnoredMatches: []match.IgnoredMatch{
				{
					AppliedIgnoreRules: []match.IgnoreRule{
						{
							Vulnerability: "CVE-2014-fake-3",
						},
					},
					Match: match.Match{
						Vulnerability: vulnerability.Vulnerability{
							Constraint: version.MustGetConstraint("< 3.7.6", version.UnknownFormat),
							ID:         "CVE-2014-fake-3",
							Namespace:  "nvd:cpe",
							CPEs: []syftPkg.CPE{
								mustCPE("cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*"),
							},
							PackageQualifiers: []qualifier.Qualifier{},
							Advisories:        []vulnerability.Advisory{},
						},
						Package: activerecordPkg,
						Details: match.Details{
							{
								Type: match.CPEMatch,
								SearchedBy: search.CPEParameters{
									Namespace: "nvd:cpe",
									CPEs: []string{
										"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
									},
								},
								Found: search.CPEResult{
									VulnerabilityID:   "CVE-2014-fake-3",
									VersionConstraint: "< 3.7.6 (unknown)",
									CPEs: []string{
										"cpe:2.3:*:activerecord:activerecord:*:*:*:*:*:rails:*:*",
									},
								},
								Matcher:    "ruby-gem-matcher",
								Confidence: 0.9,
							},
						},
					},
				},
			},
			wantErr: nil,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			m := &VulnerabilityMatcher{
				Store:          tt.fields.Store,
				Matchers:       tt.fields.Matchers,
				IgnoreRules:    tt.fields.IgnoreRules,
				FailSeverity:   tt.fields.FailSeverity,
				NormalizeByCVE: tt.fields.NormalizeByCVE,
			}
			actualMatches, actualIgnoreMatches, err := m.FindMatches(tt.args.pkgs, tt.args.context)
			if tt.wantErr != nil {
				require.ErrorIs(t, err, tt.wantErr)
				return
			} else if err != nil {
				t.Errorf("FindMatches() error = %v, wantErr %v", err, tt.wantErr)
				return
			}

			var opts = []cmp.Option{
				cmpopts.IgnoreUnexported(match.Match{}),
				cmpopts.IgnoreFields(vulnerability.Vulnerability{}, "Constraint"),
				cmpopts.IgnoreFields(pkg.Package{}, "Locations"),
				cmpopts.IgnoreUnexported(match.IgnoredMatch{}),
			}

			if d := cmp.Diff(tt.wantMatches.Sorted(), actualMatches.Sorted(), opts...); d != "" {
				t.Errorf("FindMatches() matches mismatch [ha!] (-want +got):\n%s", d)
			}

			if d := cmp.Diff(tt.wantIgnoredMatches, actualIgnoreMatches, opts...); d != "" {
				t.Errorf("FindMatches() ignored matches mismatch [ha!] (-want +got):\n%s", d)
			}
		})
	}
}
