//go:build integration

package integration

import (
	"path/filepath"
	"testing"

	v1 "github.com/google/go-containerregistry/pkg/v1"
	"github.com/stretchr/testify/require"

	ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
	"github.com/aquasecurity/trivy/pkg/types"
)

// TestSBOM tests scanning SBOM files (CycloneDX, SPDX).
//
// NOTE: This test CAN update golden files with the -update flag because the golden files
// used here are not shared with other tests. These SBOM-specific golden files are unique
// to this test and should be updated here.
func TestSBOM(t *testing.T) {
	type args struct {
		input        string
		format       string
		artifactType string
		scanners     string
	}
	tests := []struct {
		name   string
		args   args
		golden string
	}{
		{
			name: "fluentd-multiple-lockfiles cyclonedx",
			args: args{
				input:        "testdata/fixtures/sbom/fluentd-multiple-lockfiles-cyclonedx.json",
				format:       "json",
				artifactType: "cyclonedx",
			},
			golden: goldenFluentdMultipleLockfiles,
		},
		{
			name: "scan SBOM into SBOM",
			args: args{
				input:        "testdata/fixtures/sbom/fluentd-multiple-lockfiles-cyclonedx.json",
				format:       "cyclonedx",
				artifactType: "cyclonedx",
			},
			golden: goldenFluentdMultipleLockfilesShortCDX,
		},
		{
			name: "minikube KBOM",
			args: args{
				input:        "testdata/fixtures/sbom/minikube-kbom.json",
				format:       "json",
				artifactType: "cyclonedx",
			},
			golden: goldenMinikubeKBOM,
		},
		{
			name: "license check cyclonedx json",
			args: args{
				input:        "testdata/fixtures/sbom/license-cyclonedx.json",
				format:       "json",
				artifactType: "cyclonedx",
				scanners:     "license",
			},
			golden: goldenLicenseCycloneDX,
		},
	}

	// Set up testing DB
	cacheDir := initDB(t)

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			scanners := "vuln"
			if tt.args.scanners != "" {
				scanners = tt.args.scanners
			}

			osArgs := []string{
				"sbom",
				"--cache-dir",
				cacheDir,
				"-q",
				"--skip-db-update",
				"--format",
				tt.args.format,
				"--scanners",
				scanners,
				"--list-all-pkgs=false",
				tt.args.input,
			}

			// Run "trivy sbom"
			runTest(t, osArgs, tt.golden, types.Format(tt.args.format), runOptions{
				fakeUUID: "3ff14136-e09f-4df9-80ea-%012d",
				override: nil, // Do not use overrides - golden files are generated from this test as the canonical source
			})
		})
	}
}

// TestSBOMEquivalence tests that scanning an SBOM produces equivalent results to scanning the original artifact.
//
// This test verifies that scanning an image through SBOM:
//
//	trivy image centos:7 -f cyclonedx -o centos7.cdx.json && trivy sbom centos7.cdx.json
//
// produces the same vulnerability results as direct image scanning:
//
//	trivy image centos:7
//
// IMPORTANT: Golden files used in this test cannot be updated with the -update flag
// because the golden files are shared with TestTar.
// If golden files need to be updated, they should be generated from TestTar.
//
// All golden files used in TestSBOMEquivalence MUST also be used in TestTar
// to ensure they can be properly updated when needed.
func TestSBOMEquivalence(t *testing.T) {
	if *update {
		t.Skipf("Skipping TestSBOMEquivalence when -update flag is set. Golden files should be updated via TestTar.")
	}

	type args struct {
		input        string
		format       string
		artifactType string
	}
	tests := []struct {
		name     string
		args     args
		golden   string
		override OverrideFunc
	}{
		{
			name: "centos7 cyclonedx",
			args: args{
				input:        "testdata/fixtures/sbom/centos-7-cyclonedx.json",
				format:       "json",
				artifactType: "cyclonedx",
			},
			golden: goldenCentOS7,
			override: func(t *testing.T, want, got *types.Report) {
				want.ArtifactName = "testdata/fixtures/sbom/centos-7-cyclonedx.json"
				want.ArtifactType = ftypes.TypeCycloneDX

				require.Len(t, got.Results, 1)
				want.Results[0].Target = "testdata/fixtures/sbom/centos-7-cyclonedx.json (centos 7.6.1810)"

				require.Len(t, got.Results[0].Vulnerabilities, 3)
				want.Results[0].Vulnerabilities[0].PkgIdentifier.BOMRef = "pkg:rpm/centos/bash@4.2.46-31.el7?arch=x86_64&distro=centos-7.6.1810"
				want.Results[0].Vulnerabilities[1].PkgIdentifier.BOMRef = "pkg:rpm/centos/openssl-libs@1.0.2k-16.el7?arch=x86_64&epoch=1&distro=centos-7.6.1810"
				want.Results[0].Vulnerabilities[2].PkgIdentifier.BOMRef = "pkg:rpm/centos/openssl-libs@1.0.2k-16.el7?arch=x86_64&epoch=1&distro=centos-7.6.1810"

				// ReportID uses v7 UUID with independent counter from v4 UUIDs used for SBOM components
				want.ReportID = "017b7d41-e09f-7000-80ea-000000000001"
			},
		},
		{
			name: "centos7 spdx tag-value",
			args: args{
				input:        "testdata/fixtures/sbom/centos-7-spdx.txt",
				format:       "json",
				artifactType: "spdx",
			},
			golden: goldenCentOS7,
			override: func(t *testing.T, want, got *types.Report) {
				want.ArtifactName = "testdata/fixtures/sbom/centos-7-spdx.txt"
				want.ArtifactType = ftypes.TypeSPDX

				require.Len(t, got.Results, 1)
				want.Results[0].Target = "testdata/fixtures/sbom/centos-7-spdx.txt (centos 7.6.1810)"

				// ReportID uses v7 UUID with independent counter from v4 UUIDs used for SBOM components
				want.ReportID = "017b7d41-e09f-7000-80ea-000000000001"
			},
		},
		{
			name: "centos7 spdx json",
			args: args{
				input:        "testdata/fixtures/sbom/centos-7-spdx.json",
				format:       "json",
				artifactType: "spdx",
			},
			golden: goldenCentOS7,
			override: func(t *testing.T, want, got *types.Report) {
				want.ArtifactName = "testdata/fixtures/sbom/centos-7-spdx.json"
				want.ArtifactType = ftypes.TypeSPDX

				require.Len(t, got.Results, 1)
				want.Results[0].Target = "testdata/fixtures/sbom/centos-7-spdx.json (centos 7.6.1810)"

				// ReportID uses v7 UUID with independent counter from v4 UUIDs used for SBOM components
				want.ReportID = "017b7d41-e09f-7000-80ea-000000000001"
			},
		},
		{
			name: "centos7 in in-toto attestation",
			args: args{
				input:        "testdata/fixtures/sbom/centos-7-cyclonedx.intoto.jsonl",
				format:       "json",
				artifactType: "cyclonedx",
			},
			golden: goldenCentOS7,
			override: func(t *testing.T, want, got *types.Report) {
				want.ArtifactName = "testdata/fixtures/sbom/centos-7-cyclonedx.intoto.jsonl"
				want.ArtifactType = ftypes.TypeCycloneDX

				require.Len(t, got.Results, 1)
				want.Results[0].Target = "testdata/fixtures/sbom/centos-7-cyclonedx.intoto.jsonl (centos 7.6.1810)"

				require.Len(t, got.Results[0].Vulnerabilities, 3)
				want.Results[0].Vulnerabilities[0].PkgIdentifier.BOMRef = "pkg:rpm/centos/bash@4.2.46-31.el7?arch=x86_64&distro=centos-7.6.1810"
				want.Results[0].Vulnerabilities[1].PkgIdentifier.BOMRef = "pkg:rpm/centos/openssl-libs@1.0.2k-16.el7?arch=x86_64&epoch=1&distro=centos-7.6.1810"
				want.Results[0].Vulnerabilities[2].PkgIdentifier.BOMRef = "pkg:rpm/centos/openssl-libs@1.0.2k-16.el7?arch=x86_64&epoch=1&distro=centos-7.6.1810"

				// ReportID uses v7 UUID with independent counter from v4 UUIDs used for SBOM components
				want.ReportID = "017b7d41-e09f-7000-80ea-000000000001"
			},
		},
	}

	// Set up testing DB
	cacheDir := initDB(t)

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			osArgs := []string{
				"sbom",
				"--cache-dir",
				cacheDir,
				"-q",
				"--skip-db-update",
				"--format",
				tt.args.format,
				"--scanners",
				"vuln",
				"--list-all-pkgs=false",
				tt.args.input,
			}

			// Run "trivy sbom"
			runTest(t, osArgs, tt.golden, types.Format(tt.args.format), runOptions{
				override: overrideFuncs(overrideSBOMReport, overrideUID, overrideFingerprint, tt.override),
				fakeUUID: "3ff14136-e09f-4df9-80ea-%012d",
			})
		})
	}
}

func overrideSBOMReport(_ *testing.T, want, got *types.Report) {
	want.ArtifactID = ""
	want.Metadata.ImageConfig = v1.ConfigFile{}

	// SBOM file doesn't contain info about layers
	want.Metadata.Size = 0
	want.Metadata.Layers = nil

	// when running on Windows FS
	got.ArtifactName = filepath.ToSlash(filepath.Clean(got.ArtifactName))
	for i, result := range got.Results {
		got.Results[i].Target = filepath.ToSlash(filepath.Clean(result.Target))
	}
}
