package grype

import (
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/anchore/grype/grype/matcher"
	"github.com/anchore/grype/grype/pkg"
	"github.com/anchore/grype/internal/dbtest"
	"github.com/anchore/syft/syft/artifact"
	syftPkg "github.com/anchore/syft/syft/pkg"
)

// Test_UnaffectedFiltering exercises the RPM unaffected + GitHub advisory ownership ignore mechanism
// using a real built grype database from the testdata/unaffected-db fixtures.
//
// Scenario: RHEL marks packages as "unaffected" (Version: 0) for certain CVEs. The same CVEs have
// GitHub advisories for language packages (npm, gem). When an RPM package owns the language package's
// files via OwnershipByFileOverlapRelationship, the RPM's "unaffected" status should suppress the
// GitHub advisory match on the language package.
func Test_UnaffectedFiltering(t *testing.T) {
	dbtest.DBs(t, "unaffected-db").Run(func(t *testing.T, db *dbtest.DB) {
		defaultMatchers := matcher.NewDefaultMatchers(matcher.Config{})

		tests := []struct {
			name        string
			packages    []pkg.Package
			wantVulnIDs []string
		}{
			{
				name: "RHEL10 unaffected nodejs-undici suppresses npm undici GHSAs (CVE-2024-24758 and related)",
				packages: func() []pkg.Package {
					rpmPkg := dbtest.NewPackage("nodejs-undici", "0:5.28.0-1.el10", syftPkg.RpmPkg).
						WithDistro(dbtest.RHEL10).
						Build()

					npmPkg := dbtest.NewPackage("undici", "5.28.0", syftPkg.NpmPkg).
						WithLanguage(syftPkg.JavaScript).
						WithLocation("/usr/lib/node_modules/undici/package.json").
						WithRelatedPackage(artifact.OwnershipByFileOverlapRelationship, &rpmPkg).
						Build()

					return []pkg.Package{rpmPkg, npmPkg}
				}(),
				wantVulnIDs: nil, // all suppressed
			},
			{
				name: "RHEL8 unaffected rubygem-rack-protection suppresses gem GHSA-688c-3x49-6rqj (CVE-2018-1000119)",
				packages: func() []pkg.Package {
					rpmPkg := dbtest.NewPackage("rubygem-rack-protection", "0:1.5.3-5.el8", syftPkg.RpmPkg).
						WithDistro(dbtest.RHEL8).
						Build()

					gemPkg := dbtest.NewPackage("rack-protection", "1.5.3", syftPkg.GemPkg).
						WithLanguage(syftPkg.Ruby).
						WithLocation("/usr/share/gems/gems/rack-protection-1.5.3/lib/rack/protection.rb").
						WithRelatedPackage(artifact.OwnershipByFileOverlapRelationship, &rpmPkg).
						Build()

					return []pkg.Package{rpmPkg, gemPkg}
				}(),
				wantVulnIDs: nil, // all suppressed
			},
			{
				name: "npm undici vulnerable without RPM ownership relationship",
				packages: []pkg.Package{
					dbtest.NewPackage("nodejs-undici", "0:5.28.0-1.el10", syftPkg.RpmPkg).
						WithDistro(dbtest.RHEL10).
						Build(),
					dbtest.NewPackage("undici", "5.28.0", syftPkg.NpmPkg).
						WithLanguage(syftPkg.JavaScript).
						WithLocation("/app/node_modules/undici/package.json").
						Build(), // no RelatedPackages — no RPM ownership
				},
				wantVulnIDs: []string{
					"GHSA-3787-6prv-h9w3", // CVE-2024-24758 — undici <= 5.28.2
					"GHSA-9qxr-qj54-h672", // CVE-2024-30261 — undici < 5.28.4
					"GHSA-m4v8-wqvr-p9f7", // CVE-2024-30260 — undici < 5.28.4
				},
			},
			{
				// The RHEL NAK for CVE-2024-29415 is on the *upstream* RPM "nodejs" (not the binary RPM name),
				// and includes modularity entries for nodejs:18 and nodejs:20. The npm "ip" package is bundled
				// inside the nodejs RPM, so the RPM owns the npm package's files via OwnershipByFileOverlap.
				// The upstream match path + modularity qualifier should produce an ownership ignore that
				// suppresses the GitHub advisory GHSA-2p57-rm9w-gvfp on the npm "ip" package.
				name: "RHEL9 unaffected upstream nodejs (nodejs:18 module) suppresses npm ip GHSA-2p57-rm9w-gvfp",
				packages: func() []pkg.Package {
					rpmPkg := dbtest.NewPackage("npm", "1:18.20.8-1.module+el9.5.0+22758+4ad2c198", syftPkg.RpmPkg).
						WithDistro(dbtest.RHEL9).
						WithMetadata(pkg.RpmMetadata{
							Epoch:           ptr(1),
							ModularityLabel: ptr("nodejs:18:9050020250203080038:8cf767d6"),
						}).
						WithUpstream("nodejs", "").
						Build()

					npmPkg := dbtest.NewPackage("ip", "2.0.1", syftPkg.NpmPkg).
						WithLanguage(syftPkg.JavaScript).
						WithLocation("/usr/lib/node_modules/npm/node_modules/ip/lib/ip.js").
						WithRelatedPackage(artifact.OwnershipByFileOverlapRelationship, &rpmPkg).
						Build()

					return []pkg.Package{rpmPkg, npmPkg}
				}(),
				wantVulnIDs: nil, // suppressed by upstream RPM NAK
			},
			{
				// Same as above but with nodejs:20 modularity — exercises the second module entry.
				name: "RHEL9 unaffected upstream nodejs (nodejs:20 module) suppresses npm ip GHSA-2p57-rm9w-gvfp",
				packages: func() []pkg.Package {
					rpmPkg := dbtest.NewPackage("npm", "1:20.18.3-1.module+el9.5.0+22758+abc12345", syftPkg.RpmPkg).
						WithDistro(dbtest.RHEL9).
						WithMetadata(pkg.RpmMetadata{
							Epoch:           ptr(1),
							ModularityLabel: ptr("nodejs:20:9050020250203080038:8cf767d6"),
						}).
						WithUpstream("nodejs", "").
						Build()

					npmPkg := dbtest.NewPackage("ip", "2.0.1", syftPkg.NpmPkg).
						WithLanguage(syftPkg.JavaScript).
						WithLocation("/usr/lib/node_modules/npm/node_modules/ip/lib/ip.js").
						WithRelatedPackage(artifact.OwnershipByFileOverlapRelationship, &rpmPkg).
						Build()

					return []pkg.Package{rpmPkg, npmPkg}
				}(),
				wantVulnIDs: nil, // suppressed by upstream RPM NAK
			},
			{
				// npm "ip" without any RPM ownership should still match the GitHub advisory.
				name: "npm ip vulnerable without RPM ownership relationship (CVE-2024-29415)",
				packages: []pkg.Package{
					dbtest.NewPackage("nodejs", "1:20.18.3-1.module+el9.5.0+22758+abc12345", syftPkg.RpmPkg).
						WithDistro(dbtest.RHEL9).
						WithMetadata(pkg.RpmMetadata{
							Epoch:           ptr(1),
							ModularityLabel: ptr("nodejs:20:9050020250203080038:8cf767d6"),
						}).
						WithUpstream("nodejs", "").
						Build(),
					dbtest.NewPackage("ip", "2.0.1", syftPkg.NpmPkg).
						WithLanguage(syftPkg.JavaScript).
						WithLocation("/app/node_modules/ip/lib/ip.js").
						Build(), // no RelatedPackages — no RPM ownership
				},
				wantVulnIDs: []string{
					"GHSA-2p57-rm9w-gvfp", // CVE-2024-29415 — ip <= 2.0.1
				},
			},
			{
				name: "gem rack-protection vulnerable without RPM ownership relationship",
				packages: []pkg.Package{
					dbtest.NewPackage("rubygem-rack-protection", "0:1.5.3-5.el8", syftPkg.RpmPkg).
						WithDistro(dbtest.RHEL8).
						Build(),
					dbtest.NewPackage("rack-protection", "1.5.3", syftPkg.GemPkg).
						WithLanguage(syftPkg.Ruby).
						WithLocation("/app/vendor/bundle/gems/rack-protection-1.5.3/lib/rack/protection.rb").
						Build(), // no RelatedPackages — no RPM ownership
				},
				wantVulnIDs: []string{
					"GHSA-688c-3x49-6rqj", // CVE-2018-1000119 — rack-protection < 1.5.5
				},
			},
		}

		for _, tt := range tests {
			t.Run(tt.name, func(t *testing.T) {
				m := &VulnerabilityMatcher{
					VulnerabilityProvider: db,
					Matchers:              defaultMatchers,
				}

				matches, _, err := m.FindMatches(tt.packages, pkg.Context{})
				require.NoError(t, err)

				var gotMatches []string
				for _, m := range matches.Sorted() {
					gotMatches = append(gotMatches, m.Vulnerability.ID)
				}
				require.ElementsMatch(t, tt.wantVulnIDs, gotMatches)
			})
		}
	})
}

func ptr[T any](t T) *T {
	return &t
}
