package model

import (
	"encoding/json"
	"reflect"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

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

func Test_UnmarshalJSON(t *testing.T) {
	tests := []struct {
		name        string
		packageData []byte
		assert      func(*Package)
	}{
		{
			name: "unmarshal package metadata",
			packageData: []byte(`{
				"id": "8b594519bc23da50",
				"name": "gopkg.in/square/go-jose.v2",
				"version": "v2.6.0",
				"type": "go-module",
				"foundBy": "go-module-binary-cataloger",
				"locations": [
				  {
				    "path": "/Users/hal/go/bin/syft"
				  }
				],
				"licenses": [
				  {
				   "value": "MIT",
				   "spdxExpression": "MIT",
				   "type": "declared",
				   "url": []
				  }
				],
				"language": "go",
				"cpes": [],
				"purl": "pkg:golang/gopkg.in/square/go-jose.v2@v2.6.0",
				"metadataType": "GolangBinMetadata",
				"metadata": {
				  "goCompiledVersion": "go1.18",
				  "architecture": "amd64",
				  "h1Digest": "h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI="
				}
			}`),
			assert: func(p *Package) {
				require.NotNil(t, p.Metadata)
				golangMetadata := p.Metadata.(pkg.GolangBinaryBuildinfoEntry)
				require.NotEmpty(t, golangMetadata)
				assert.Equal(t, "go1.18", golangMetadata.GoCompiledVersion)
			},
		},
		{
			name: "can handle package without metadata",
			packageData: []byte(`{
				"id": "8b594519bc23da50",
				"name": "gopkg.in/square/go-jose.v2",
				"version": "v2.6.0",
				"type": "go-module",
				"foundBy": "go-mod-cataloger",
				"locations": [
				  {
				    "path": "/Users/hal/go/bin/syft"
				  }
				],
				"licenses": [
				  {
				    "value": "MIT",
					"spdxExpression": "MIT",
					"type": "declared",
					"url": ["https://www.github.com"]
				  },
				  {
				    "value": "MIT",
					"spdxExpression": "MIT",
					"type": "declared",
					"locations": [{"path": "/Users/hal/go/bin/syft"}]
				  }
				],
				"language": "go",
				"cpes": [],
				"purl": "pkg:golang/gopkg.in/square/go-jose.v2@v2.6.0"
			}`),
			assert: func(p *Package) {
				assert.Empty(t, p.MetadataType)
				assert.Empty(t, p.Metadata)
			},
		},
		{
			name: "can handle package with []string licenses",
			packageData: []byte(`{
				"id": "8b594519bc23da50",
				"name": "gopkg.in/square/go-jose.v2",
				"version": "v2.6.0",
				"type": "go-module",
				"foundBy": "go-mod-cataloger",
				"locations": [
				  {
				    "path": "/Users/hal/go/bin/syft"
				  }
				],
				"licenses": ["MIT", "Apache-2.0"],
				"language": "go",
				"cpes": [],
				"purl": "pkg:golang/gopkg.in/square/go-jose.v2@v2.6.0"
			}`),
			assert: func(p *Package) {
				assert.Equal(t, licenses{
					{
						Value:          "MIT",
						SPDXExpression: "MIT",
						Type:           license.Declared,
					},
					{
						Value:          "Apache-2.0",
						SPDXExpression: "Apache-2.0",
						Type:           license.Declared,
					},
				}, p.Licenses)
			},
		},
		{
			name: "can handle package with []pkg.License licenses",
			packageData: []byte(`{
				"id": "8b594519bc23da50",
				"name": "gopkg.in/square/go-jose.v2",
				"version": "v2.6.0",
				"type": "go-module",
				"foundBy": "go-mod-cataloger",
				"locations": [
				  {
				    "path": "/Users/hal/go/bin/syft"
				  }
				],
				"licenses": [	
					{
						"value": "MIT",
						"spdxExpression": "MIT",
						"type": "declared"
					},
					{
						"value": "Apache-2.0",
						"spdxExpression": "Apache-2.0",
						"type": "declared"
					}
				],
				"language": "go",
				"cpes": [],
				"purl": "pkg:golang/gopkg.in/square/go-jose.v2@v2.6.0"
			}`),
			assert: func(p *Package) {
				assert.Equal(t, licenses{
					{
						Value:          "MIT",
						SPDXExpression: "MIT",
						Type:           license.Declared,
					},
					{
						Value:          "Apache-2.0",
						SPDXExpression: "Apache-2.0",
						Type:           license.Declared,
					},
				}, p.Licenses)
			},
		},
		{
			name: "breaking v11-v12 schema change: rpm db vs archive (select db)",
			packageData: []byte(`{
  "id": "739158935bfffc4d",
  "name": "dbus",
  "version": "1:1.12.8-12.el8",
  "type": "rpm",
  "foundBy": "rpm-db-cataloger",
  "locations": [
    {
      "path": "/var/lib/rpm/Packages",
      "layerID": "sha256:d871dadfb37b53ef1ca45be04fc527562b91989991a8f545345ae3be0b93f92a",
      "annotations": {
        "evidence": "primary"
      }
    }
  ],
  "licenses": [],
  "language": "",
  "cpes": [],
  "purl": "pkg:rpm/centos/dbus@1.12.8-12.el8?arch=aarch64&epoch=1&upstream=dbus-1.12.8-12.el8.src.rpm&distro=centos-8",
  "metadataType": "RpmMetadata",
  "metadata": {
    "name": "dbus",
    "version": "1.12.8",
    "epoch": 1,
    "architecture": "aarch64",
    "release": "12.el8",
    "sourceRpm": "dbus-1.12.8-12.el8.src.rpm",
    "size": 0,
    "vendor": "CentOS",
    "modularityLabel": "",
    "files": []
  }
}
`),
			assert: func(p *Package) {
				assert.Equal(t, pkg.RpmPkg, p.Type)
				assert.Equal(t, reflect.TypeFor[pkg.RpmDBEntry]().Name(), reflect.TypeOf(p.Metadata).Name())
			},
		},
		{
			name: "breaking v11-v12 schema change: rpm db vs archive (select archive)",
			packageData: []byte(`{
  "id": "739158935bfffc4d",
  "name": "dbus",
  "version": "1:1.12.8-12.el8",
  "type": "rpm",
  "foundBy": "rpm-db-cataloger",
  "locations": [
    {
      "path": "/var/cache/dbus-1.12.8-12.el8.rpm",
      "layerID": "sha256:d871dadfb37b53ef1ca45be04fc527562b91989991a8f545345ae3be0b93f92a",
      "annotations": {
        "evidence": "primary"
      }
    }
  ],
  "licenses": [],
  "language": "",
  "cpes": [],
  "purl": "pkg:rpm/centos/dbus@1.12.8-12.el8?arch=aarch64&epoch=1&upstream=dbus-1.12.8-12.el8.src.rpm&distro=centos-8",
  "metadataType": "RpmMetadata",
  "metadata": {
    "name": "dbus",
    "version": "1.12.8",
    "epoch": 1,
    "architecture": "aarch64",
    "release": "12.el8",
    "sourceRpm": "dbus-1.12.8-12.el8.src.rpm",
    "size": 0,
    "vendor": "CentOS",
    "modularityLabel": "",
    "files": []
  }
}
`),
			assert: func(p *Package) {
				assert.Equal(t, pkg.RpmPkg, p.Type)
				assert.Equal(t, reflect.TypeFor[pkg.RpmArchive]().Name(), reflect.TypeOf(p.Metadata).Name())
			},
		},
		{
			name: "breaking v11-v12 schema change: stack.yaml vs stack.yaml.lock (select stack.yaml)",
			packageData: []byte(`{
  "id": "46ff1a71f7715f38",
  "name": "hspec-discover",
  "version": "2.9.4",
  "type": "hackage",
  "foundBy": "haskell-cataloger",
  "locations": [
    {
      "path": "/stack.yaml",
      "annotations": {
        "evidence": "primary"
      }
    }
  ],
  "licenses": [],
  "language": "haskell",
  "cpes": [
    "cpe:2.3:a:hspec-discover:hspec-discover:2.9.4:*:*:*:*:*:*:*",
    "cpe:2.3:a:hspec-discover:hspec_discover:2.9.4:*:*:*:*:*:*:*",
    "cpe:2.3:a:hspec_discover:hspec-discover:2.9.4:*:*:*:*:*:*:*",
    "cpe:2.3:a:hspec_discover:hspec_discover:2.9.4:*:*:*:*:*:*:*",
    "cpe:2.3:a:hspec:hspec-discover:2.9.4:*:*:*:*:*:*:*",
    "cpe:2.3:a:hspec:hspec_discover:2.9.4:*:*:*:*:*:*:*"
  ],
  "purl": "pkg:hackage/hspec-discover@2.9.4",
  "metadataType": "HackageMetadataType",
  "metadata": {
    "name": "",
    "version": "",
    "pkgHash": "fbcf49ecfc3d4da53e797fd0275264cba776ffa324ee223e2a3f4ec2d2c9c4a6"
  }
}`),
			assert: func(p *Package) {
				assert.Equal(t, pkg.HackagePkg, p.Type)
				assert.Equal(t, reflect.TypeFor[pkg.HackageStackYamlEntry]().Name(), reflect.TypeOf(p.Metadata).Name())
			},
		},
		{
			name: "breaking v11-v12 schema change: stack.yaml vs stack.yaml.lock (select stack.yaml.lock)",
			packageData: []byte(`{
  "id": "87939e95124ceb92",
  "name": "optparse-applicative",
  "version": "0.16.1.0",
  "type": "hackage",
  "foundBy": "haskell-cataloger",
  "locations": [
    {
      "path": "/stack.yaml.lock",
      "annotations": {
        "evidence": "primary"
      }
    }
  ],
  "licenses": [],
  "language": "haskell",
  "cpes": [
    "cpe:2.3:a:optparse-applicative:optparse-applicative:0.16.1.0:*:*:*:*:*:*:*",
    "cpe:2.3:a:optparse-applicative:optparse_applicative:0.16.1.0:*:*:*:*:*:*:*",
    "cpe:2.3:a:optparse_applicative:optparse-applicative:0.16.1.0:*:*:*:*:*:*:*",
    "cpe:2.3:a:optparse_applicative:optparse_applicative:0.16.1.0:*:*:*:*:*:*:*",
    "cpe:2.3:a:optparse:optparse-applicative:0.16.1.0:*:*:*:*:*:*:*",
    "cpe:2.3:a:optparse:optparse_applicative:0.16.1.0:*:*:*:*:*:*:*"
  ],
  "purl": "pkg:hackage/optparse-applicative@0.16.1.0",
  "metadataType": "HackageMetadataType",
  "metadata": {
    "name": "",
    "version": "",
    "pkgHash": "418c22ed6a19124d457d96bc66bd22c93ac22fad0c7100fe4972bbb4ac989731",
    "snapshotURL": "https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/19/14.yaml"
  }
}`),
			assert: func(p *Package) {
				assert.Equal(t, pkg.HackagePkg, p.Type)
				assert.Equal(t, reflect.TypeFor[pkg.HackageStackYamlLockEntry]().Name(), reflect.TypeOf(p.Metadata).Name())
			},
		},
		{
			name: "breaking v11-v12 schema change: rust cargo.lock vs audit (select cargo.lock)",
			packageData: []byte(`{
  "id": "95124ceb9287939e",
  "name": "optpkg",
  "version": "1.16.1",
  "type": "hackage",
  "foundBy": "rust-cataloger",
  "locations": [
    {
      "path": "/cargo.lock",
      "annotations": {
        "evidence": "primary"
      }
    }
  ],
  "licenses": [],
  "language": "rust",
  "cpes": [],
  "purl": "pkg:cargo/optpkg@1.16.1",
  "metadataType": "RustCargoPackageMetadata",
  "metadata": {}
}`),
			assert: func(p *Package) {
				assert.Equal(t, pkg.HackagePkg, p.Type)
				assert.Equal(t, reflect.TypeFor[pkg.RustCargoLockEntry]().Name(), reflect.TypeOf(p.Metadata).Name())
			},
		},
		{
			name: "breaking v11-v12 schema change: rust cargo.lock vs audit (select audit binary)",
			packageData: []byte(`{
  "id": "95124ceb9287939e",
  "name": "optpkg",
  "version": "1.16.1",
  "type": "hackage",
  "foundBy": "rust-cataloger",
  "locations": [
    {
      "path": "/my-binary",
      "annotations": {
        "evidence": "primary"
      }
    }
  ],
  "licenses": [],
  "language": "rust",
  "cpes": [],
  "purl": "pkg:cargo/optpkg@1.16.1",
  "metadataType": "RustCargoPackageMetadata",
  "metadata": {}
}`),
			assert: func(p *Package) {
				assert.Equal(t, pkg.HackagePkg, p.Type)
				assert.Equal(t, reflect.TypeFor[pkg.RustBinaryAuditEntry]().Name(), reflect.TypeOf(p.Metadata).Name())
			},
		},
		{
			name: "map-based java metadata",
			packageData: []byte(`{
  "id": "e6f845bdaa69ddb2",
  "name": "SparseBitSet",
  "version": "1.2",
  "type": "java-archive",
  "foundBy": "java-archive-cataloger",
  "locations": [],
  "licenses": [],
  "language": "java",
  "cpes": [],
  "purl": "pkg:maven/com.zaxxer/SparseBitSet@1.2",
  "metadataType": "java-archive",
  "metadata": {
    "virtualPath": "/opt/solr-9.4.1/modules/extraction/lib/SparseBitSet-1.2.jar",
    "manifest": {
      "main": {
        "Archiver-Version": "Plexus Archiver",
        "Build-Jdk": "1.8.0_73",
        "Built-By": "lbayer",
        "Created-By": "Apache Maven 3.5.0",
        "Manifest-Version": "1.0"
      },
      "namedSections": {
        "META-INF/mailcap": {
          "SHA-256-Digest": "kXN4VupOQOJhduMGwxumj4ijmD/YAlz97a9Mp7CVXtk="
        },
        "META-INF/versions/9/module-info.class": {
          "SHA-256-Digest": "cMeIRa5l8DWPgrVWavr/6TKVBUGixVKGcu6yOTZMlKk="
        }
      }
    }
  }
}`),
			assert: func(p *Package) {
				meta := p.Metadata.(pkg.JavaArchive)
				manifest := meta.Manifest
				assert.Equal(t, "1.8.0_73", manifest.Main.MustGet("Build-Jdk"))
				require.Equal(t, 2, len(manifest.Sections))
				assert.Equal(t, "META-INF/mailcap", manifest.Sections[0].MustGet("Name"))
				assert.Equal(t, "kXN4VupOQOJhduMGwxumj4ijmD/YAlz97a9Mp7CVXtk=", manifest.Sections[0].MustGet("SHA-256-Digest"))
				assert.Equal(t, "META-INF/versions/9/module-info.class", manifest.Sections[1].MustGet("Name"))
				assert.Equal(t, "cMeIRa5l8DWPgrVWavr/6TKVBUGixVKGcu6yOTZMlKk=", manifest.Sections[1].MustGet("SHA-256-Digest"))
			},
		},
		{
			name: "pre key-value golang metadata",
			packageData: []byte(`{
  "id": "e348ed25484a94c9",
  "name": "github.com/anchore/syft",
  "version": "v0.101.1-SNAPSHOT-4c777834",
  "type": "go-module",
  "foundBy": "go-module-binary-cataloger",
  "locations": [
    {
      "path": "/syft",
      "layerID": "sha256:2274947a5f3527e48d8725a96646aefdcce3d99340c1eefb1e7c894043863c92",
      "accessPath": "/syft",
      "annotations": {
        "evidence": "primary"
      }
    }
  ],
  "licenses": [],
  "language": "go",
  "cpes": [
    "cpe:2.3:a:anchore:syft:v0.101.1-SNAPSHOT-4c777834:*:*:*:*:*:*:*"
  ],
  "purl": "pkg:golang/github.com/anchore/syft@v0.101.1-SNAPSHOT-4c777834",
  "metadataType": "go-module-buildinfo-entry",
  "metadata": {
    "goBuildSettings": {
      "-buildmode": "exe",
      "-compiler": "gc",
      "-ldflags": "-w -s -extldflags '-static' -X main.version=0.101.1-SNAPSHOT-4c777834 -X main.gitCommit=4c777834618b2ad8ad94cd200a45d6670bc1c013 -X main.buildDate=2024-01-22T16:43:49Z -X main.gitDescription=v0.101.1-4-g4c777834 ",
      "CGO_ENABLED": "0",
      "GOAMD64": "v1",
      "GOARCH": "amd64",
      "GOOS": "linux",
      "vcs": "git",
      "vcs.modified": "false",
      "vcs.revision": "4c777834618b2ad8ad94cd200a45d6670bc1c013",
      "vcs.time": "2024-01-22T16:31:41Z"
    },
    "goCompiledVersion": "go1.21.2",
    "architecture": "amd64",
    "mainModule": "github.com/anchore/syft"
  }
}
`),
			assert: func(p *Package) {
				buildInfo := p.Metadata.(pkg.GolangBinaryBuildinfoEntry)
				assert.Equal(t, "exe", buildInfo.BuildSettings.MustGet("-buildmode"))
			},
		},
		{
			name: "conan lock with legacy options",
			packageData: []byte(`{
  "id": "75eb35307226c921",
  "name": "boost",
  "version": "1.75.0",
  "type": "conan",
  "foundBy": "conan-cataloger",
  "locations": [
    {
      "path": "/conan.lock",
      "accessPath": "/conan.lock",
      "annotations": {
        "evidence": "primary"
      }
    }
  ],
  "licenses": [],
  "language": "c++",
  "cpes": [
    "cpe:2.3:a:boost:boost:1.75.0:*:*:*:*:*:*:*"
  ],
  "purl": "pkg:conan/boost@1.75.0",
  "metadataType": "c-conan-lock-entry",
  "metadata": {
    "ref": "boost/1.75.0#a9c318f067216f900900e044e7af4ab1",
    "package_id": "dc8aedd23a0f0a773a5fcdcfe1ae3e89c4205978",
    "prev": "b9d7912e6131dfa453c725593b36c808",
    "options": {
      "addr2line_location": "/usr/bin/addr2line",
      "asio_no_deprecated": "False",
      "zstd": "False"
    },
    "context": "host"
  }
}`),
			assert: func(p *Package) {
				metadata := p.Metadata.(pkg.ConanV1LockEntry)
				assert.Equal(t, "False", metadata.Options.MustGet("asio_no_deprecated"))
			},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			p := &Package{}
			err := p.UnmarshalJSON(test.packageData)
			require.NoError(t, err)
			test.assert(p)
		})
	}
}

func Test_unpackMetadata(t *testing.T) {
	tests := []struct {
		name         string
		packageData  []byte
		wantMetadata any
		wantErr      require.ErrorAssertionFunc
	}{
		{
			name:         "unmarshal package metadata",
			wantMetadata: pkg.GolangBinaryBuildinfoEntry{},
			packageData: []byte(`{
				"id": "8b594519bc23da50",
				"name": "gopkg.in/square/go-jose.v2",
				"version": "v2.6.0",
				"type": "go-module",
				"foundBy": "go-module-binary-cataloger",
				"locations": [
				  {
				    "path": "/Users/hal/go/bin/syft"
				  }
				],
				"licenses": [],
				"language": "go",
				"cpes": [],
				"purl": "pkg:golang/gopkg.in/square/go-jose.v2@v2.6.0",
				"metadataType": "GolangBinMetadata",
				"metadata": {
				  "goCompiledVersion": "go1.18",
				  "architecture": "amd64",
				  "h1Digest": "h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI="
				}
			}`),
		},
		{
			name:         "can handle package without metadata",
			wantMetadata: nil,
			packageData: []byte(`{
				"id": "8b594519bc23da50",
				"name": "gopkg.in/square/go-jose.v2",
				"version": "v2.6.0",
				"type": "go-module",
				"foundBy": "go-mod-cataloger",
				"locations": [
				  {
				    "path": "/Users/hal/go/bin/syft"
				  }
				],
				"licenses": [],
				"language": "go",
				"cpes": [],
				"purl": "pkg:golang/gopkg.in/square/go-jose.v2@v2.6.0"
			}`),
		},
		{
			name:         "can handle RpmdbMetadata",
			wantMetadata: pkg.RpmDBEntry{},
			packageData: []byte(`{
				"id": "4ac699c3b8fe1835",
				"name": "acl",
				"version": "2.2.53-1.el8",
				"type": "rpm",
				"foundBy": "rpm-db-cataloger",
				"locations": [
					{
					 "path": "/var/lib/rpm/Packages",
					 "layerID": "sha256:74ddd0ec08fa43d09f32636ba91a0a3053b02cb4627c35051aff89f853606b59"
					}
				],
				"language": "",
				"cpes": [
					"cpe:2.3:a:centos:acl:2.2.53-1.el8:*:*:*:*:*:*:*",
					"cpe:2.3:a:acl:acl:2.2.53-1.el8:*:*:*:*:*:*:*"
				],
				"purl": "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8",
				"metadataType": "RpmdbMetadata",
				"metadata": {
					"name": "acl",
					"version": "2.2.53",
					"epoch": null,
					"architecture": "x86_64",
					"release": "1.el8",
					"sourceRpm": "acl-2.2.53-1.el8.src.rpm",
					"size": 205740,
					"license": "GPLv2+",
					"vendor": "CentOS",
					"modularityLabel": ""
				}
			}`),
		},
		{
			name:    "bad metadata type is an error",
			wantErr: require.Error,
			packageData: []byte(`{
				"id": "8b594519bc23da50",
				"name": "gopkg.in/square/go-jose.v2",
				"version": "v2.6.0",
				"type": "go-module",
				"foundBy": "go-mod-cataloger",
				"locations": [
				  {
				    "path": "/Users/hal/go/bin/syft"
				  }
				],
				"licenses": [],
				"language": "go",
				"cpes": [],
				"purl": "pkg:golang/gopkg.in/square/go-jose.v2@v2.6.0",
				"metadataType": "BOGOSITY"
			}`),
		},
		{
			name: "unknown metadata type",
			packageData: []byte(`{
				"metadataType": "NewMetadataType",
				"metadata": {
					"thing": "thing-1"
				}
			}`),
			wantErr: require.Error,
			wantMetadata: map[string]any{
				"thing": "thing-1",
			},
		},
		{
			name: "can handle package with metadata type but missing metadata",
			packageData: []byte(`{
				"metadataType": "GolangBinMetadata"
			}`),
			wantMetadata: pkg.GolangBinaryBuildinfoEntry{},
		},
		{
			name: "can handle package with golang bin metadata type",
			packageData: []byte(`{
				"metadataType": "GolangBinMetadata"
			}`),
			wantMetadata: pkg.GolangBinaryBuildinfoEntry{},
		},
		{
			name: "can handle package with unknown metadata type and missing metadata",
			packageData: []byte(`{
				"metadataType": "BadMetadata"
			}`),
			wantErr: require.Error,
		},
		{
			name: "can handle package with unknown metadata type and metadata",
			packageData: []byte(`{
				"metadataType": "BadMetadata",
				"metadata": {
					"random": "thing"
				}
			}`),
			wantErr: require.Error,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			if test.wantErr == nil {
				test.wantErr = require.NoError
			}
			p := &Package{}

			var unpacker packageMetadataUnpacker
			require.NoError(t, json.Unmarshal(test.packageData, &unpacker))

			err := unpackPkgMetadata(p, unpacker)
			test.wantErr(t, err)

			if test.wantMetadata != nil {
				if p.Metadata == nil {
					t.Fatalf("expected metadata to be populated")
					return
				}
				assert.Equal(t, reflect.TypeOf(test.wantMetadata).Name(), reflect.TypeOf(p.Metadata).Name())
			}
		})
	}
}
