package helpers

import (
	"fmt"
	"reflect"
	"testing"

	"github.com/CycloneDX/cyclonedx-go"
	"github.com/stretchr/testify/assert"

	"github.com/anchore/syft/syft/file"
	"github.com/anchore/syft/syft/pkg"
)

func Test_encodeComponentProperties(t *testing.T) {
	epoch := 2
	tests := []struct {
		name     string
		input    pkg.Package
		expected []cyclonedx.Property
	}{
		{
			name:     "no metadata",
			input:    pkg.Package{},
			expected: nil,
		},
		{
			name: "from apk",
			input: pkg.Package{
				FoundBy: "cataloger",
				Locations: file.NewLocationSet(
					file.NewLocationFromCoordinates(file.Coordinates{RealPath: "test"}),
				),
				Metadata: pkg.ApkDBEntry{
					Package:       "libc-utils",
					OriginPackage: "libc-dev",
					Maintainer:    "Natanael Copa <ncopa@alpinelinux.org>",
					Version:       "0.7.2-r0",
					Architecture:  "x86_64",
					URL:           "http://alpinelinux.org",
					Description:   "Meta package to pull in correct libc",
					Size:          0,
					InstalledSize: 4096,
					Dependencies:  []string{"musl-utils"},
					Provides:      []string{"so:libc.so.1"},
					Checksum:      "Q1p78yvTLG094tHE1+dToJGbmYzQE=",
					GitCommit:     "97b1c2842faa3bfa30f5811ffbf16d5ff9f1a479",
					Files:         []pkg.ApkFileRecord{},
				},
			},
			expected: []cyclonedx.Property{
				{Name: "syft:package:foundBy", Value: "cataloger"},
				{Name: "syft:package:metadataType", Value: "apk-db-entry"},
				{Name: "syft:location:0:path", Value: "test"},
				{Name: "syft:metadata:gitCommitOfApkPort", Value: "97b1c2842faa3bfa30f5811ffbf16d5ff9f1a479"},
				{Name: "syft:metadata:installedSize", Value: "4096"},
				{Name: "syft:metadata:originPackage", Value: "libc-dev"},
				{Name: "syft:metadata:provides:0", Value: "so:libc.so.1"},
				{Name: "syft:metadata:pullChecksum", Value: "Q1p78yvTLG094tHE1+dToJGbmYzQE="},
				{Name: "syft:metadata:pullDependencies:0", Value: "musl-utils"},
				{Name: "syft:metadata:size", Value: "0"},
			},
		},
		{
			name: "from dpkg",
			input: pkg.Package{
				Metadata: pkg.DpkgDBEntry{
					Package:       "tzdata",
					Version:       "2020a-0+deb10u1",
					Source:        "tzdata-dev",
					SourceVersion: "1.0",
					Architecture:  "all",
					InstalledSize: 3036,
					Maintainer:    "GNU Libc Maintainers <debian-glibc@lists.debian.org>",
					Files:         []pkg.DpkgFileRecord{},
				},
			},
			expected: []cyclonedx.Property{
				{Name: "syft:package:metadataType", Value: "dpkg-db-entry"},
				{Name: "syft:metadata:installedSize", Value: "3036"},
				{Name: "syft:metadata:source", Value: "tzdata-dev"},
				{Name: "syft:metadata:sourceVersion", Value: "1.0"},
			},
		},
		{
			name: "from go bin",
			input: pkg.Package{
				Name:     "golang.org/x/net",
				Version:  "v0.0.0-20211006190231-62292e806868",
				Language: pkg.Go,
				Type:     pkg.GoModulePkg,
				Metadata: pkg.GolangBinaryBuildinfoEntry{
					GoCompiledVersion: "1.17",
					Architecture:      "amd64",
					H1Digest:          "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k=",
				},
			},
			expected: []cyclonedx.Property{
				{Name: "syft:package:language", Value: pkg.Go.String()},
				{Name: "syft:package:metadataType", Value: "go-module-buildinfo-entry"},
				{Name: "syft:package:type", Value: "go-module"},
				{Name: "syft:metadata:architecture", Value: "amd64"},
				{Name: "syft:metadata:goCompiledVersion", Value: "1.17"},
				{Name: "syft:metadata:h1Digest", Value: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k="},
			},
		},
		{
			name: "from go mod",
			input: pkg.Package{
				Name:     "golang.org/x/net",
				Version:  "v0.0.0-20211006190231-62292e806868",
				Language: pkg.Go,
				Type:     pkg.GoModulePkg,
				Metadata: pkg.GolangModuleEntry{
					H1Digest: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k=",
				},
			},
			expected: []cyclonedx.Property{
				{Name: "syft:package:language", Value: pkg.Go.String()},
				{Name: "syft:package:metadataType", Value: "go-module-entry"},
				{Name: "syft:package:type", Value: "go-module"},
				{Name: "syft:metadata:h1Digest", Value: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k="},
			},
		},
		{
			name: "from rpm",
			input: pkg.Package{
				Name:    "dive",
				Version: "0.9.2-1",
				Type:    pkg.RpmPkg,
				Metadata: pkg.RpmDBEntry{
					Name:      "dive",
					Epoch:     &epoch,
					Arch:      "x86_64",
					Release:   "1",
					Version:   "0.9.2",
					SourceRpm: "dive-0.9.2-1.src.rpm",
					Size:      12406784,
					Vendor:    "",
					Files:     []pkg.RpmFileRecord{},
				},
			},
			expected: []cyclonedx.Property{
				{Name: "syft:package:metadataType", Value: "rpm-db-entry"},
				{Name: "syft:package:type", Value: "rpm"},
				{Name: "syft:metadata:epoch", Value: "2"},
				{Name: "syft:metadata:release", Value: "1"},
				{Name: "syft:metadata:size", Value: "12406784"},
				{Name: "syft:metadata:sourceRpm", Value: "dive-0.9.2-1.src.rpm"},
			},
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			sbomSupplier := ""
			c := EncodeComponent(test.input, sbomSupplier, file.LocationSorter(nil))
			if test.expected == nil {
				if c.Properties != nil {
					t.Fatalf("expected no properties, got: %+v", *c.Properties)
				}
				return
			}
			assert.ElementsMatch(t, test.expected, *c.Properties)
		})
	}
}

func Test_encodeCompomentType(t *testing.T) {
	tests := []struct {
		name string
		pkg  pkg.Package
		want cyclonedx.Component
	}{
		{
			name: "non-binary package",
			pkg: pkg.Package{
				Name:    "pkg1",
				Version: "1.9.2",
				Type:    pkg.GoModulePkg,
			},
			want: cyclonedx.Component{
				Name:    "pkg1",
				Version: "1.9.2",
				Type:    cyclonedx.ComponentTypeLibrary,
				Properties: &[]cyclonedx.Property{
					{
						Name:  "syft:package:type",
						Value: "go-module",
					},
				},
			},
		},
		{
			name: "non-binary package",
			pkg: pkg.Package{
				Name:    "pkg1",
				Version: "3.1.2",
				Type:    pkg.BinaryPkg,
			},
			want: cyclonedx.Component{
				Name:    "pkg1",
				Version: "3.1.2",
				Type:    cyclonedx.ComponentTypeApplication,
				Properties: &[]cyclonedx.Property{
					{
						Name:  "syft:package:type",
						Value: "binary",
					},
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tt.pkg.ID()
			sbomSupplier := ""
			p := EncodeComponent(tt.pkg, sbomSupplier, file.LocationSorter(nil))
			assert.Equal(t, tt.want, p)
		})
	}
}

func Test_deriveBomRef(t *testing.T) {
	pkgWithPurl := pkg.Package{
		Name:    "django",
		Version: "1.11.1",
		PURL:    "pkg:pypi/django@1.11.1",
	}
	pkgWithPurl.SetID()

	pkgWithOutPurl := pkg.Package{
		Name:    "django",
		Version: "1.11.1",
		PURL:    "",
	}
	pkgWithOutPurl.SetID()

	pkgWithBadPurl := pkg.Package{
		Name:    "django",
		Version: "1.11.1",
		PURL:    "pkg:pyjango@1.11.1",
	}
	pkgWithBadPurl.SetID()

	tests := []struct {
		name string
		pkg  pkg.Package
		want string
	}{
		{
			name: "use pURL-id hybrid",
			pkg:  pkgWithPurl,
			want: fmt.Sprintf("pkg:pypi/django@1.11.1?package-id=%s", pkgWithPurl.ID()),
		},
		{
			name: "fallback to ID when pURL is invalid",
			pkg:  pkgWithBadPurl,
			want: string(pkgWithBadPurl.ID()),
		},
		{
			name: "fallback to ID when pURL is missing",
			pkg:  pkgWithOutPurl,
			want: string(pkgWithOutPurl.ID()),
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tt.pkg.ID()
			assert.Equal(t, tt.want, DeriveBomRef(tt.pkg))
		})
	}
}

func Test_decodeComponent(t *testing.T) {
	tests := []struct {
		name         string
		component    cyclonedx.Component
		wantLanguage pkg.Language
		wantMetadata any
		wantPURL     string
	}{
		{
			name: "derive language from pURL if missing",
			component: cyclonedx.Component{
				Name:       "ch.qos.logback/logback-classic",
				Version:    "1.2.3",
				PackageURL: "pkg:maven/ch.qos.logback/logback-classic@1.2.3",
				Type:       "library",
				BOMRef:     "pkg:maven/ch.qos.logback/logback-classic@1.2.3",
			},
			wantLanguage: pkg.Java,
			wantPURL:     "pkg:maven/ch.qos.logback/logback-classic@1.2.3",
		},
		{
			name: "derive language from bomref if missing",
			component: cyclonedx.Component{
				Name:    "ch.qos.logback/logback-classic",
				Version: "1.2.3",
				Type:    "library",
				BOMRef:  "pkg:maven/ch.qos.logback/logback-classic@1.2.3",
			},
			wantLanguage: pkg.Java,
			wantPURL:     "pkg:maven/ch.qos.logback/logback-classic@1.2.3",
		},
		{
			name: "handle RpmdbMetadata type without properties",
			component: cyclonedx.Component{
				Name:       "acl",
				Version:    "2.2.53-1.el8",
				PackageURL: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8",
				Type:       "library",
				BOMRef:     "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8",
				Properties: &[]cyclonedx.Property{
					{
						Name:  "syft:package:metadataType",
						Value: "RpmdbMetadata",
					},
				},
			},
			wantMetadata: pkg.RpmDBEntry{Arch: "x86_64"},
			wantPURL:     "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8",
		},
		{
			name: "handle RpmdbMetadata type with properties",
			component: cyclonedx.Component{
				Name:       "acl",
				Version:    "2.2.53-1.el8",
				PackageURL: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8",
				Type:       "library",
				BOMRef:     "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8",
				Properties: &[]cyclonedx.Property{
					{
						Name:  "syft:package:metadataType",
						Value: "RpmDBMetadata",
					},
					{
						Name:  "syft:metadata:release",
						Value: "some-release",
					},
				},
			},
			wantMetadata: pkg.RpmDBEntry{
				Release: "some-release",
				Arch:    "x86_64",
			},
			wantPURL: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8",
		},
		{
			name: "generate a purl from package type",
			component: cyclonedx.Component{
				Name:    "log4j",
				Group:   "org.apache.logging.log4j",
				Version: "2.0.4",
				Type:    "library",
				BOMRef:  "log4j",
				Properties: &[]cyclonedx.Property{
					{
						Name:  "syft:package:type",
						Value: "java-archive",
					},
				},
			},
			wantPURL: "pkg:maven/org.apache.logging.log4j/log4j@2.0.4",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			p := decodeComponent(&tt.component)
			if tt.wantLanguage != "" {
				assert.Equal(t, tt.wantLanguage, p.Language)
			}
			if tt.wantMetadata != nil {
				assert.Truef(t, reflect.DeepEqual(tt.wantMetadata, p.Metadata), "metadata should match: %+v != %+v", tt.wantMetadata, p.Metadata)
			}

			if tt.wantPURL != "" {
				assert.Equal(t, tt.wantPURL, p.PURL, "purl should match")
			}

			if tt.wantMetadata == nil && tt.wantLanguage == "" && tt.wantPURL == "" {
				t.Fatal("this is a useless test, please remove it")
			}
		})
	}
}

func Test_setPackageName(t *testing.T) {
	tests := []struct {
		name     string
		pkg      pkg.Package
		comp     cyclonedx.Component
		wantName string
	}{
		{
			name:     "debian group excluded from name",
			pkg:      pkg.Package{Type: pkg.DebPkg},
			comp:     cyclonedx.Component{Name: "wget", Group: "debian"},
			wantName: "wget",
		},
		{
			name:     "rpm group excluded from name",
			pkg:      pkg.Package{Type: pkg.RpmPkg},
			comp:     cyclonedx.Component{Name: "acl", Group: "centos"},
			wantName: "acl",
		},
		{
			name:     "apk group excluded from name",
			pkg:      pkg.Package{Type: pkg.ApkPkg},
			comp:     cyclonedx.Component{Name: "musl", Group: "alpine"},
			wantName: "musl",
		},
		{
			name:     "npm group included in name",
			pkg:      pkg.Package{Type: pkg.NpmPkg},
			comp:     cyclonedx.Component{Name: "node", Group: "@types"},
			wantName: "@types/node",
		},
		{
			name:     "go module group included in name",
			pkg:      pkg.Package{Type: pkg.GoModulePkg},
			comp:     cyclonedx.Component{Name: "net", Group: "golang.org/x"},
			wantName: "golang.org/x/net",
		},
		{
			name:     "no group leaves name unchanged",
			pkg:      pkg.Package{Type: pkg.DebPkg},
			comp:     cyclonedx.Component{Name: "wget"},
			wantName: "wget",
		},
		{
			name:     "java group stored in metadata not name",
			pkg:      pkg.Package{Type: pkg.JavaPkg},
			comp:     cyclonedx.Component{Name: "log4j", Group: "org.apache.logging.log4j"},
			wantName: "log4j",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			p := tt.pkg
			setPackageName(&p, &tt.comp)
			assert.Equal(t, tt.wantName, p.Name)
		})
	}
}

func TestGetPURL(t *testing.T) {
	tests := []struct {
		name      string
		component *cyclonedx.Component
		pkgType   pkg.Type
		expected  string
	}{
		{
			name: "component with PackageURL",
			component: &cyclonedx.Component{
				PackageURL: "pkg:npm/lodash@4.17.21",
				Name:       "lodash",
				Version:    "4.17.20", // different version to verify PackageURL is used
				Group:      "npm",
			},
			pkgType:  pkg.NpmPkg,
			expected: "pkg:npm/lodash@4.17.21",
		},
		{
			name: "component with BOMRef as valid PURL",
			component: &cyclonedx.Component{
				BOMRef:  "pkg:maven/org.apache.commons/commons-lang3@3.12.0",
				Name:    "commons-lang3",
				Version: "3.11.0", // different version to verify BOMRef is used
				Group:   "org.apache.commons",
			},
			pkgType:  pkg.JavaPkg,
			expected: "pkg:maven/org.apache.commons/commons-lang3@3.12.0",
		},
		{
			name: "component with BOMRef not a valid PURL",
			component: &cyclonedx.Component{
				BOMRef:  "not-a-purl-ref",
				Name:    "commons-lang3",
				Version: "3.12.0",
				Group:   "org.apache.commons",
			},
			pkgType:  pkg.JavaPkg,
			expected: "pkg:maven/org.apache.commons/commons-lang3@3.12.0",
		},
		{
			name: "component with unknown package type",
			component: &cyclonedx.Component{
				Name:    "some-component",
				Version: "1.0.0",
				Group:   "org.example",
			},
			pkgType:  pkg.UnknownPkg,
			expected: "",
		},
		{
			name: "component with empty package type",
			component: &cyclonedx.Component{
				Name:    "some-component",
				Version: "1.0.0",
				Group:   "org.example",
			},
			pkgType:  "",
			expected: "",
		},
		{
			name: "component with generic package type",
			component: &cyclonedx.Component{
				Name:    "some-component",
				Version: "1.0.0",
				Group:   "org.example",
			},
			pkgType:  pkg.LinuxKernelModulePkg,
			expected: "",
		},
		{
			name: "component with valid package type",
			component: &cyclonedx.Component{
				Name:    "react",
				Version: "18.2.0",
				Group:   "facebook",
			},
			pkgType:  pkg.NpmPkg,
			expected: "pkg:npm/facebook/react@18.2.0",
		},
		{
			name: "component with no group",
			component: &cyclonedx.Component{
				Name:    "express",
				Version: "4.18.2",
			},
			pkgType:  pkg.NpmPkg,
			expected: "pkg:npm/express@4.18.2",
		},
		{
			name: "component with no version",
			component: &cyclonedx.Component{
				Name:  "express",
				Group: "npm",
			},
			pkgType:  pkg.NpmPkg,
			expected: "pkg:npm/npm/express",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := getPURL(tt.component, tt.pkgType)
			assert.Equal(t, tt.expected, result)
		})
	}
}
