package vulns_test

import (
	"testing"
	"time"

	"github.com/google/osv-scalibr/extractor"
	"github.com/google/osv-scalibr/extractor/filesystem/os/dpkg/metadata"
	"github.com/google/osv-scalibr/purl"
	"github.com/google/osv-scanner/v2/internal/imodels"
	"github.com/google/osv-scanner/v2/internal/imodels/ecosystem"
	"github.com/google/osv-scanner/v2/internal/utility/vulns"
	"github.com/ossf/osv-schema/bindings/go/osvschema"
)

func expectIsAffected(t *testing.T, vuln osvschema.Vulnerability, version string, expectAffected bool) {
	t.Helper()

	pkg := imodels.FromInventory(
		&extractor.Package{
			Name:     "my-package",
			Version:  version,
			PURLType: purl.TypeNPM,
		},
	)

	if vulns.IsAffected(vuln, pkg) != expectAffected {
		if expectAffected {
			t.Errorf("Expected OSV to affect package version %s but it did not", version)
		} else {
			t.Errorf("Expected OSV not to affect package version %s but it did", version)
		}
	}
}

func buildOSVWithAffected(affected ...osvschema.Affected) osvschema.Vulnerability {
	return osvschema.Vulnerability{
		ID:        "1",
		Published: time.Time{},
		Modified:  time.Time{},
		Details:   "This is an open source vulnerability!",
		Affected:  affected,
	}
}

func buildEcosystemAffectsRange(events ...osvschema.Event) osvschema.Range {
	return osvschema.Range{Type: osvschema.RangeEcosystem, Events: events}
}

func buildSemverAffectsRange(events ...osvschema.Event) osvschema.Range {
	return osvschema.Range{Type: osvschema.RangeSemVer, Events: events}
}

func TestOSV_AffectsEcosystem(t *testing.T) {
	t.Parallel()

	type AffectsTest struct {
		Affected  []osvschema.Affected
		Ecosystem string
		Expected  bool
	}

	tests := []AffectsTest{
		{Affected: nil, Ecosystem: "Go", Expected: false},
		{Affected: nil, Ecosystem: "npm", Expected: false},
		{Affected: nil, Ecosystem: "PyPI", Expected: false},
		{Affected: nil, Ecosystem: "", Expected: false},
		{
			Affected: []osvschema.Affected{
				{Package: osvschema.Package{Ecosystem: "crates.io"}},
				{Package: osvschema.Package{Ecosystem: "npm"}},
				{Package: osvschema.Package{Ecosystem: "PyPI"}},
			},
			Ecosystem: "Packagist",
			Expected:  false,
		},
		{
			Affected: []osvschema.Affected{
				{Package: osvschema.Package{Ecosystem: "NuGet"}},
			},
			Ecosystem: "NuGet",
			Expected:  true,
		},
		{
			Affected: []osvschema.Affected{
				{Package: osvschema.Package{Ecosystem: "npm"}},
				{Package: osvschema.Package{Ecosystem: "npm"}},
			},
			Ecosystem: "npm",
			Expected:  true,
		},
	}

	for i, tt := range tests {
		vuln := osvschema.Vulnerability{
			ID:        "1",
			Published: time.Time{},
			Modified:  time.Time{},
			Details:   "This is an open source vulnerability!",
			Affected:  tt.Affected,
		}

		if vulns.AffectsEcosystem(vuln, ecosystem.MustParse(tt.Ecosystem)) != tt.Expected {
			t.Errorf(
				"Test #%d: Expected OSV to return %t but it returned %t",
				i,
				tt.Expected,
				!tt.Expected,
			)
		}
	}

	// test when the OSV doesn't have an "Affected"
	vuln := osvschema.Vulnerability{
		ID:        "1",
		Published: time.Time{},
		Modified:  time.Time{},
		Details:   "This is an open source vulnerability!",
		Affected:  nil,
	}

	if vulns.AffectsEcosystem(vuln, ecosystem.MustParse("npm")) {
		t.Errorf(
			"Expected OSV to report 'false' when it doesn't have an Affected, but it reported true!",
		)
	}
}

func TestOSV_IsAffected_AffectsWithEcosystem_DifferentEcosystem(t *testing.T) {
	t.Parallel()

	vuln := buildOSVWithAffected(
		osvschema.Affected{
			Package: osvschema.Package{Ecosystem: string(osvschema.EcosystemPyPI), Name: "my-package"},
			Ranges: []osvschema.Range{
				buildEcosystemAffectsRange(osvschema.Event{Introduced: "0"}),
			},
		},
	)

	for _, v := range []string{"1.0.0", "1.1.1", "2.0.0"} {
		expectIsAffected(t, vuln, v, false)
	}
}

func TestOSV_IsAffected_AffectsWithEcosystem_SingleAffected(t *testing.T) {
	t.Parallel()

	var vuln osvschema.Vulnerability

	// "Introduced: 0" means everything is affected
	vuln = buildOSVWithAffected(
		osvschema.Affected{
			Package: osvschema.Package{Ecosystem: string(osvschema.EcosystemNPM), Name: "my-package"},
			Ranges: []osvschema.Range{
				buildEcosystemAffectsRange(osvschema.Event{Introduced: "0"}),
			},
		},
	)

	for _, v := range []string{"1.0.0", "1.1.1", "2.0.0"} {
		expectIsAffected(t, vuln, v, true)
	}

	// an empty version should always be treated as affected
	expectIsAffected(t, vuln, "", true)

	// "Fixed: 1" means all versions after this are not vulnerable
	vuln = buildOSVWithAffected(
		osvschema.Affected{
			Package: osvschema.Package{Ecosystem: string(osvschema.EcosystemNPM), Name: "my-package"},
			Ranges: []osvschema.Range{
				buildEcosystemAffectsRange(
					osvschema.Event{Introduced: "0"},
					osvschema.Event{Fixed: "1"},
				),
			},
		},
	)

	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
		expectIsAffected(t, vuln, v, true)
	}

	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0"} {
		expectIsAffected(t, vuln, v, false)
	}

	// an empty version should always be treated as affected
	expectIsAffected(t, vuln, "", true)

	// multiple fixes and introduced
	vuln = buildOSVWithAffected(
		osvschema.Affected{
			Package: osvschema.Package{Ecosystem: string(osvschema.EcosystemNPM), Name: "my-package"},
			Ranges: []osvschema.Range{
				buildEcosystemAffectsRange(
					osvschema.Event{Introduced: "0"},
					osvschema.Event{Fixed: "1"},
					osvschema.Event{Introduced: "2.1.0"},
					osvschema.Event{Fixed: "3.2.0"},
				),
			},
		},
	)

	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
		expectIsAffected(t, vuln, v, true)
	}

	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0rc2", "2.0.1"} {
		expectIsAffected(t, vuln, v, false)
	}

	for _, v := range []string{"2.1.1", "2.3.4", "3.0.0", "3.0.0-rc"} {
		expectIsAffected(t, vuln, v, true)
	}

	for _, v := range []string{"3.2.0", "3.2.1", "4.0.0"} {
		expectIsAffected(t, vuln, v, false)
	}

	// an empty version should always be treated as affected
	expectIsAffected(t, vuln, "", true)

	// "LastAffected: 1" means all versions after this are not vulnerable
	vuln = buildOSVWithAffected(
		osvschema.Affected{
			Package: osvschema.Package{Ecosystem: string(osvschema.EcosystemNPM), Name: "my-package"},
			Ranges: []osvschema.Range{
				buildEcosystemAffectsRange(
					osvschema.Event{Introduced: "0"},
					osvschema.Event{LastAffected: "1"},
				),
			},
		},
	)

	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc", "1.0.0"} {
		expectIsAffected(t, vuln, v, true)
	}

	for _, v := range []string{"1.0.1", "1.1.0", "2.0.0"} {
		expectIsAffected(t, vuln, v, false)
	}

	// an empty version should always be treated as affected
	expectIsAffected(t, vuln, "", true)

	// mix of fixes, last_known_affected, and introduced
	vuln = buildOSVWithAffected(
		osvschema.Affected{
			Package: osvschema.Package{Ecosystem: string(osvschema.EcosystemNPM), Name: "my-package"},
			Ranges: []osvschema.Range{
				buildEcosystemAffectsRange(
					osvschema.Event{Introduced: "0"},
					osvschema.Event{Fixed: "1"},
					osvschema.Event{Introduced: "2.1.0"},
					osvschema.Event{LastAffected: "3.1.9"},
				),
			},
		},
	)

	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
		expectIsAffected(t, vuln, v, true)
	}

	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0rc2", "2.0.1"} {
		expectIsAffected(t, vuln, v, false)
	}

	for _, v := range []string{"2.1.1", "2.3.4", "3.0.0", "3.0.0-rc", "3.1.9"} {
		expectIsAffected(t, vuln, v, true)
	}

	for _, v := range []string{"3.2.0", "3.2.1", "4.0.0"} {
		expectIsAffected(t, vuln, v, false)
	}

	// an empty version should always be treated as affected
	expectIsAffected(t, vuln, "", true)
}

func TestOSV_IsAffected_AffectsWithEcosystem_MultipleAffected(t *testing.T) {
	t.Parallel()

	vuln := buildOSVWithAffected(
		osvschema.Affected{
			Package: osvschema.Package{Ecosystem: string(osvschema.EcosystemNPM), Name: "my-package"},
			Ranges: []osvschema.Range{
				buildEcosystemAffectsRange(
					osvschema.Event{Introduced: "0"},
					osvschema.Event{Fixed: "1"},
				),
			},
		},
		osvschema.Affected{
			Package: osvschema.Package{Ecosystem: string(osvschema.EcosystemNPM), Name: "my-package"},
			Ranges: []osvschema.Range{
				buildEcosystemAffectsRange(
					osvschema.Event{Introduced: "2.1.0"},
					osvschema.Event{Fixed: "3.2.0"},
				),
			},
		},
		osvschema.Affected{
			Package: osvschema.Package{Ecosystem: string(osvschema.EcosystemNPM), Name: "my-package"},
			Ranges: []osvschema.Range{
				buildEcosystemAffectsRange(
					osvschema.Event{Introduced: "3.3.0"},
					osvschema.Event{LastAffected: "3.5.0"},
				),
			},
		},
	)

	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
		expectIsAffected(t, vuln, v, true)
	}

	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0rc2", "2.0.1"} {
		expectIsAffected(t, vuln, v, false)
	}

	for _, v := range []string{"2.1.1", "2.3.4", "3.0.0", "3.0.0-rc"} {
		expectIsAffected(t, vuln, v, true)
	}

	for _, v := range []string{"3.2.0", "3.2.1", "4.0.0"} {
		expectIsAffected(t, vuln, v, false)
	}

	for _, v := range []string{"3.3.1", "3.4.5"} {
		expectIsAffected(t, vuln, v, true)
	}

	// an empty version should always be treated as affected
	expectIsAffected(t, vuln, "", true)
}

func TestOSV_IsAffected_AffectsWithEcosystem_Unsorted(t *testing.T) {
	t.Parallel()

	vuln := buildOSVWithAffected(
		osvschema.Affected{
			Package: osvschema.Package{Ecosystem: string(osvschema.EcosystemNPM), Name: "my-package"},
			Ranges: []osvschema.Range{
				buildEcosystemAffectsRange(
					osvschema.Event{Introduced: "0"},
					osvschema.Event{Introduced: "2.1.0"},
					osvschema.Event{Fixed: "1"},
					osvschema.Event{LastAffected: "3.1.9"},
				),
			},
		},
	)

	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
		expectIsAffected(t, vuln, v, true)
	}

	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0rc2", "2.0.1"} {
		expectIsAffected(t, vuln, v, false)
	}

	for _, v := range []string{"2.1.1", "2.3.4", "3.0.0", "3.0.0-rc", "3.1.9"} {
		expectIsAffected(t, vuln, v, true)
	}

	for _, v := range []string{"3.2.0", "3.2.1", "4.0.0"} {
		expectIsAffected(t, vuln, v, false)
	}

	// an empty version should always be treated as affected
	expectIsAffected(t, vuln, "", true)

	// zeros with build strings
	vuln = buildOSVWithAffected(
		osvschema.Affected{
			// golang.org/x/sys
			Package: osvschema.Package{Ecosystem: string(osvschema.EcosystemNPM), Name: "my-package"},
			Ranges: []osvschema.Range{
				buildEcosystemAffectsRange(
					osvschema.Event{Fixed: "0.0.0-20220412211240-33da011f77ad"},
					osvschema.Event{Introduced: "0"},
				),
			},
		},
		osvschema.Affected{
			// golang.org/x/net
			Package: osvschema.Package{Ecosystem: string(osvschema.EcosystemNPM), Name: "my-package"},
			Ranges: []osvschema.Range{
				buildEcosystemAffectsRange(
					osvschema.Event{Introduced: "0.0.0-20180925071336-cf3bd585ca2a"},
					osvschema.Event{Fixed: "0"},
				),
			},
		},
	)

	for _, v := range []string{"0.0.0", "0.14.0"} {
		expectIsAffected(t, vuln, v, false)
	}

	for _, v := range []string{"0.0.0-20180925071336-cf3bd585ca2a"} {
		expectIsAffected(t, vuln, v, true)
	}

	// an empty version should always be treated as affected
	expectIsAffected(t, vuln, "", true)
}

func TestOSV_IsAffected_AffectsWithSemver_DifferentEcosystem(t *testing.T) {
	t.Parallel()

	vuln := buildOSVWithAffected(
		osvschema.Affected{
			Package: osvschema.Package{Ecosystem: string(osvschema.EcosystemPyPI), Name: "my-package"},
			Ranges: []osvschema.Range{
				buildSemverAffectsRange(osvschema.Event{Introduced: "0"}),
			},
		},
	)

	for _, v := range []string{"1.0.0", "1.1.1", "2.0.0"} {
		expectIsAffected(t, vuln, v, false)
	}
}

func TestOSV_IsAffected_AffectsWithSemver_SingleAffected(t *testing.T) {
	t.Parallel()

	var vuln osvschema.Vulnerability

	// "Introduced: 0" means everything is affected
	vuln = buildOSVWithAffected(
		osvschema.Affected{
			Package: osvschema.Package{Ecosystem: string(osvschema.EcosystemNPM), Name: "my-package"},
			Ranges: []osvschema.Range{
				buildSemverAffectsRange(osvschema.Event{Introduced: "0"}),
			},
		},
	)

	for _, v := range []string{"v1.0.0", "v1.1.1", "v2.0.0"} {
		expectIsAffected(t, vuln, v, true)
	}

	// "Fixed: 1" means all versions after this are not vulnerable
	vuln = buildOSVWithAffected(
		osvschema.Affected{
			Package: osvschema.Package{Ecosystem: string(osvschema.EcosystemNPM), Name: "my-package"},
			Ranges: []osvschema.Range{
				buildSemverAffectsRange(
					osvschema.Event{Introduced: "0"},
					osvschema.Event{Fixed: "1.0.0"},
				),
			},
		},
	)

	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
		expectIsAffected(t, vuln, v, true)
	}

	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0"} {
		expectIsAffected(t, vuln, v, false)
	}

	// multiple fixes and introduced
	vuln = buildOSVWithAffected(
		osvschema.Affected{
			Package: osvschema.Package{Ecosystem: string(osvschema.EcosystemNPM), Name: "my-package"},
			Ranges: []osvschema.Range{
				buildSemverAffectsRange(
					osvschema.Event{Introduced: "0"},
					osvschema.Event{Fixed: "1"},
					osvschema.Event{Introduced: "2.1.0"},
					osvschema.Event{Fixed: "3.2.0"},
				),
			},
		},
	)

	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
		expectIsAffected(t, vuln, v, true)
	}

	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0rc2", "2.0.1"} {
		expectIsAffected(t, vuln, v, false)
	}

	for _, v := range []string{"2.1.1", "2.3.4", "3.0.0", "3.0.0-rc"} {
		expectIsAffected(t, vuln, v, true)
	}

	for _, v := range []string{"3.2.0", "3.2.1", "4.0.0"} {
		expectIsAffected(t, vuln, v, false)
	}

	// an empty version should always be treated as affected
	expectIsAffected(t, vuln, "", true)

	// "LastAffected: 1" means all versions after this are not vulnerable
	vuln = buildOSVWithAffected(
		osvschema.Affected{
			Package: osvschema.Package{Ecosystem: string(osvschema.EcosystemNPM), Name: "my-package"},
			Ranges: []osvschema.Range{
				buildSemverAffectsRange(
					osvschema.Event{Introduced: "0"},
					osvschema.Event{LastAffected: "1.0.0"},
				),
			},
		},
	)

	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc", "1.0.0"} {
		expectIsAffected(t, vuln, v, true)
	}

	for _, v := range []string{"1.0.1", "1.1.0", "2.0.0"} {
		expectIsAffected(t, vuln, v, false)
	}

	// mix of fixes, last_known_affected, and introduced
	vuln = buildOSVWithAffected(
		osvschema.Affected{
			Package: osvschema.Package{Ecosystem: string(osvschema.EcosystemNPM), Name: "my-package"},
			Ranges: []osvschema.Range{
				buildSemverAffectsRange(
					osvschema.Event{Introduced: "0"},
					osvschema.Event{Fixed: "1"},
					osvschema.Event{Introduced: "2.1.0"},
					osvschema.Event{LastAffected: "3.1.9"},
				),
			},
		},
	)

	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
		expectIsAffected(t, vuln, v, true)
	}

	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0rc2", "2.0.1"} {
		expectIsAffected(t, vuln, v, false)
	}

	for _, v := range []string{"2.1.1", "2.3.4", "3.0.0", "3.0.0-rc"} {
		expectIsAffected(t, vuln, v, true)
	}

	for _, v := range []string{"3.2.0", "3.2.1", "4.0.0"} {
		expectIsAffected(t, vuln, v, false)
	}

	// an empty version should always be treated as affected
	expectIsAffected(t, vuln, "", true)
}

func TestOSV_IsAffected_AffectsWithSemver_MultipleAffected(t *testing.T) {
	t.Parallel()

	vuln := buildOSVWithAffected(
		osvschema.Affected{
			Package: osvschema.Package{Ecosystem: string(osvschema.EcosystemNPM), Name: "my-package"},
			Ranges: []osvschema.Range{
				buildSemverAffectsRange(
					osvschema.Event{Introduced: "0"},
					osvschema.Event{Fixed: "1"},
				),
			},
		},
		osvschema.Affected{
			Package: osvschema.Package{Ecosystem: string(osvschema.EcosystemNPM), Name: "my-package"},
			Ranges: []osvschema.Range{
				buildSemverAffectsRange(
					osvschema.Event{Introduced: "2.1.0"},
					osvschema.Event{Fixed: "3.2.0"},
				),
			},
		},
		osvschema.Affected{
			Package: osvschema.Package{Ecosystem: string(osvschema.EcosystemNPM), Name: "my-package"},
			Ranges: []osvschema.Range{
				buildSemverAffectsRange(
					osvschema.Event{Introduced: "3.3.0"},
					osvschema.Event{LastAffected: "3.5.0"},
				),
			},
		},
	)

	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
		expectIsAffected(t, vuln, v, true)
	}

	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0rc2", "2.0.1"} {
		expectIsAffected(t, vuln, v, false)
	}

	for _, v := range []string{"2.1.1", "2.3.4", "3.0.0", "3.0.0-rc"} {
		expectIsAffected(t, vuln, v, true)
	}

	for _, v := range []string{"3.2.0", "3.2.1", "4.0.0"} {
		expectIsAffected(t, vuln, v, false)
	}

	for _, v := range []string{"3.3.1", "3.4.5", "3.5.0"} {
		expectIsAffected(t, vuln, v, true)
	}

	// an empty version should always be treated as affected
	expectIsAffected(t, vuln, "", true)
}

func TestOSV_IsAffected_AffectsWithSemver_Unsorted(t *testing.T) {
	t.Parallel()

	// mix of fixes, last_known_affected, and introduced
	vuln := buildOSVWithAffected(
		osvschema.Affected{
			Package: osvschema.Package{Ecosystem: string(osvschema.EcosystemNPM), Name: "my-package"},
			Ranges: []osvschema.Range{
				buildSemverAffectsRange(
					osvschema.Event{Introduced: "0"},
					osvschema.Event{Introduced: "2.1.0"},
					osvschema.Event{Fixed: "1"},
					osvschema.Event{LastAffected: "3.1.9"},
				),
			},
		},
	)

	for _, v := range []string{"0.0.0", "0.1.0", "0.0.0.1", "1.0.0-rc"} {
		expectIsAffected(t, vuln, v, true)
	}

	for _, v := range []string{"1.0.0", "1.1.0", "2.0.0rc2", "2.0.1"} {
		expectIsAffected(t, vuln, v, false)
	}

	for _, v := range []string{"2.1.1", "2.3.4", "3.0.0", "3.0.0-rc"} {
		expectIsAffected(t, vuln, v, true)
	}

	for _, v := range []string{"3.2.0", "3.2.1", "4.0.0"} {
		expectIsAffected(t, vuln, v, false)
	}

	// an empty version should always be treated as affected
	expectIsAffected(t, vuln, "", true)

	// an empty version should always be treated as affected
	expectIsAffected(t, vuln, "", true)

	// zeros with build strings
	vuln = buildOSVWithAffected(
		osvschema.Affected{
			// golang.org/x/sys
			Package: osvschema.Package{Ecosystem: string(osvschema.EcosystemNPM), Name: "my-package"},
			Ranges: []osvschema.Range{
				buildSemverAffectsRange(
					osvschema.Event{Fixed: "0.0.0-20220412211240-33da011f77ad"},
					osvschema.Event{Introduced: "0"},
				),
			},
		},
		osvschema.Affected{
			// golang.org/x/net
			Package: osvschema.Package{Ecosystem: string(osvschema.EcosystemNPM), Name: "my-package"},
			Ranges: []osvschema.Range{
				buildSemverAffectsRange(
					osvschema.Event{Introduced: "0.0.0-20180925071336-cf3bd585ca2a"},
					osvschema.Event{Fixed: "0"},
				),
			},
		},
	)

	for _, v := range []string{"0.0.0", "0.14.0"} {
		expectIsAffected(t, vuln, v, false)
	}

	for _, v := range []string{"0.0.0-20180925071336-cf3bd585ca2a"} {
		expectIsAffected(t, vuln, v, true)
	}

	// an empty version should always be treated as affected
	expectIsAffected(t, vuln, "", true)
}

func TestOSV_IsAffected_OnlyVersions(t *testing.T) {
	t.Parallel()

	vuln := buildOSVWithAffected(
		osvschema.Affected{
			Package:  osvschema.Package{Ecosystem: string(osvschema.EcosystemNPM), Name: "my-package"},
			Versions: []string{"1.0.0"},
		},
	)

	expectIsAffected(t, vuln, "0.0.0", false)
	expectIsAffected(t, vuln, "1.0.0", true)
	expectIsAffected(t, vuln, "1.0.0-beta1", false)
	expectIsAffected(t, vuln, "1.1.0", false)

	// an empty version should always be treated as affected
	expectIsAffected(t, vuln, "", true)
}

func TestOSV_EcosystemsWithSuffix(t *testing.T) {
	t.Parallel()

	vuln := buildOSVWithAffected(
		osvschema.Affected{
			Package: osvschema.Package{Ecosystem: "Debian:12", Name: "my-package"},
			Ranges: []osvschema.Range{
				buildSemverAffectsRange(
					osvschema.Event{Introduced: "0"},
				),
			},
		},
	)

	pkg := imodels.FromInventory(
		&extractor.Package{
			Name:     "my-package",
			Version:  "0.0.0",
			PURLType: purl.TypeDebian,
			Metadata: &metadata.Metadata{
				OSID:        "Debian",
				OSVersionID: "12",
			},
		},
	)

	if !vulns.IsAffected(vuln, pkg) {
		t.Errorf("Expected OSV to affect package version %s but it did not", "0.0.0")
	}
}
