//go:build search
// +build search

package cveinfo_test

import (
	"context"
	"errors"
	"io"
	"os"
	"testing"
	"time"

	regTypes "github.com/google/go-containerregistry/pkg/v1/types"
	godigest "github.com/opencontainers/go-digest"
	ispec "github.com/opencontainers/image-spec/specs-go/v1"
	. "github.com/smartystreets/goconvey/convey"

	zerr "zotregistry.dev/zot/errors"
	"zotregistry.dev/zot/pkg/api/config"
	zcommon "zotregistry.dev/zot/pkg/common"
	"zotregistry.dev/zot/pkg/extensions/monitoring"
	cveinfo "zotregistry.dev/zot/pkg/extensions/search/cve"
	cvecache "zotregistry.dev/zot/pkg/extensions/search/cve/cache"
	cvemodel "zotregistry.dev/zot/pkg/extensions/search/cve/model"
	"zotregistry.dev/zot/pkg/log"
	"zotregistry.dev/zot/pkg/meta"
	"zotregistry.dev/zot/pkg/meta/boltdb"
	mTypes "zotregistry.dev/zot/pkg/meta/types"
	"zotregistry.dev/zot/pkg/scheduler"
	"zotregistry.dev/zot/pkg/storage"
	"zotregistry.dev/zot/pkg/storage/local"
	test "zotregistry.dev/zot/pkg/test/common"
	. "zotregistry.dev/zot/pkg/test/image-utils"
	"zotregistry.dev/zot/pkg/test/mocks"
)

var (
	ErrBadTest    = errors.New("there is a bug in the test")
	ErrFailedScan = errors.New("scan has failed intentionally")
)

func TestScanGeneratorWithMockedData(t *testing.T) { //nolint: gocyclo
	Convey("Test CVE scanning task scheduler with diverse mocked data", t, func() {
		repo1 := "repo1"
		repoIndex := "repoIndex"

		logFile, err := os.CreateTemp(t.TempDir(), "zot-log*.txt")
		logPath := logFile.Name()

		So(err, ShouldBeNil)

		defer os.Remove(logFile.Name()) // clean up

		logger := log.NewLogger("debug", logPath)
		writers := io.MultiWriter(os.Stdout, logFile)
		logger.Logger = logger.Output(writers)

		cfg := config.New()
		cfg.Scheduler = &config.SchedulerConfig{NumWorkers: 3}
		metrics := monitoring.NewMetricsServer(true, logger)
		sch := scheduler.NewScheduler(cfg, metrics, logger)

		params := boltdb.DBParameters{
			RootDir: t.TempDir(),
		}
		boltDriver, err := boltdb.GetBoltDriver(params)
		So(err, ShouldBeNil)

		metaDB, err := boltdb.New(boltDriver, log.NewLogger("debug", ""))
		So(err, ShouldBeNil)

		// Refactor Idea: We can use InitializeTestMetaDB

		// Create metadb data for scannable image with vulnerabilities
		image11 := CreateImageWith().DefaultLayers().
			ImageConfig(ispec.Image{Created: DateRef(2008, 1, 1, 12, 0, 0, 0, time.UTC)}).Build()

		err = metaDB.SetRepoReference(context.Background(), "repo1", "0.1.0", image11.AsImageMeta())
		So(err, ShouldBeNil)

		image12 := CreateImageWith().DefaultLayers().
			ImageConfig(ispec.Image{Created: DateRef(2009, 1, 1, 12, 0, 0, 0, time.UTC)}).Build()

		err = metaDB.SetRepoReference(context.Background(), "repo1", "1.0.0", image12.AsImageMeta())
		So(err, ShouldBeNil)

		image13 := CreateImageWith().DefaultLayers().
			ImageConfig(ispec.Image{Created: DateRef(2010, 1, 1, 12, 0, 0, 0, time.UTC)}).Build()

		err = metaDB.SetRepoReference(context.Background(), "repo1", "1.1.0", image13.AsImageMeta())
		So(err, ShouldBeNil)

		image14 := CreateImageWith().DefaultLayers().
			ImageConfig(ispec.Image{Created: DateRef(2011, 1, 1, 12, 0, 0, 0, time.UTC)}).Build()

		err = metaDB.SetRepoReference(context.Background(), "repo1", "1.0.1", image14.AsImageMeta())
		So(err, ShouldBeNil)

		// Create metadb data for scannable image with no vulnerabilities
		image61 := CreateImageWith().DefaultLayers().
			ImageConfig(ispec.Image{Created: DateRef(2016, 1, 1, 12, 0, 0, 0, time.UTC)}).Build()

		err = metaDB.SetRepoReference(context.Background(), "repo6", "1.0.0", image61.AsImageMeta())
		So(err, ShouldBeNil)

		// Create metadb data for image not supporting scanning
		image21 := CreateImageWith().Layers([]Layer{{
			MediaType: ispec.MediaTypeImageLayerNonDistributableGzip, //nolint:staticcheck
			Blob:      []byte{10, 10, 10},
			Digest:    godigest.FromBytes([]byte{10, 10, 10}),
		}}).ImageConfig(ispec.Image{Created: DateRef(2009, 1, 1, 12, 0, 0, 0, time.UTC)}).Build()

		err = metaDB.SetRepoReference(context.Background(), "repo2", "1.0.0", image21.AsImageMeta())
		So(err, ShouldBeNil)

		// Create metadb data for invalid images/negative tests
		img := CreateRandomImage()
		digest31 := img.Digest()

		err = metaDB.SetRepoReference(context.Background(), "repo3", "invalid-manifest", img.AsImageMeta())
		So(err, ShouldBeNil)

		image41 := CreateImageWith().DefaultLayers().
			CustomConfigBlob([]byte("invalid config blob"), ispec.MediaTypeImageConfig).Build()

		err = metaDB.SetRepoReference(context.Background(), "repo4", "invalid-config", image41.AsImageMeta())
		So(err, ShouldBeNil)

		image15 := CreateRandomMultiarch()

		digest51 := image15.Digest()
		err = metaDB.SetRepoReference(context.Background(), "repo5", "nonexitent-manifests-for-multiarch",
			image15.AsImageMeta())
		So(err, ShouldBeNil)

		// Create metadb data for scannable image which errors during scan
		image71 := CreateImageWith().DefaultLayers().
			ImageConfig(ispec.Image{Created: DateRef(2000, 1, 1, 12, 0, 0, 0, time.UTC)}).Build()

		err = metaDB.SetRepoReference(context.Background(), "repo7", "1.0.0", image71.AsImageMeta())
		So(err, ShouldBeNil)

		// Create multiarch image with vulnerabilities
		multiarchImage := CreateRandomMultiarch()

		err = metaDB.SetRepoReference(context.Background(), repoIndex, multiarchImage.Images[0].DigestStr(),
			multiarchImage.Images[0].AsImageMeta())
		So(err, ShouldBeNil)
		err = metaDB.SetRepoReference(context.Background(), repoIndex, multiarchImage.Images[1].DigestStr(),
			multiarchImage.Images[1].AsImageMeta())
		So(err, ShouldBeNil)
		err = metaDB.SetRepoReference(context.Background(), repoIndex, multiarchImage.Images[2].DigestStr(),
			multiarchImage.Images[2].AsImageMeta())
		So(err, ShouldBeNil)

		err = metaDB.SetRepoReference(context.Background(), repoIndex, "tagIndex", multiarchImage.AsImageMeta())
		So(err, ShouldBeNil)

		err = metaDB.SetRepoMeta("repo-with-bad-tag-digest", mTypes.RepoMeta{
			Name: "repo-with-bad-tag-digest",
			Tags: map[mTypes.Tag]mTypes.Descriptor{
				"tag":            {MediaType: ispec.MediaTypeImageManifest, Digest: godigest.FromString("1").String()},
				"tag-multi-arch": {MediaType: ispec.MediaTypeImageIndex, Digest: godigest.FromString("2").String()},
			},
		})
		So(err, ShouldBeNil)

		// Keep a record of all the image references / digest pairings
		// This is normally done in MetaDB, but we want to verify
		// the whole flow, including MetaDB
		imageMap := map[string]string{}

		image11Digest := image11.ManifestDescriptor.Digest.String()
		image11Name := "repo1:0.1.0"
		imageMap[image11Name] = image11Digest
		image12Digest := image12.ManifestDescriptor.Digest.String()
		image12Name := "repo1:1.0.0"
		imageMap[image12Name] = image12Digest
		image13Digest := image13.ManifestDescriptor.Digest.String()
		image13Name := "repo1:1.1.0"
		imageMap[image13Name] = image13Digest
		image14Digest := image14.ManifestDescriptor.Digest.String()
		image14Name := "repo1:1.0.1"
		imageMap[image14Name] = image14Digest
		image21Digest := image21.ManifestDescriptor.Digest.String()
		image21Name := "repo2:1.0.0"
		imageMap[image21Name] = image21Digest
		image31Name := "repo3:invalid-manifest"
		imageMap[image31Name] = digest31.String()
		image41Digest := image41.ManifestDescriptor.Digest.String()
		image41Name := "repo4:invalid-config"
		imageMap[image41Name] = image41Digest
		image51Name := "repo5:nonexitent-manifest-for-multiarch"
		imageMap[image51Name] = digest51.String()
		image61Digest := image61.ManifestDescriptor.Digest.String()
		image61Name := "repo6:1.0.0"
		imageMap[image61Name] = image61Digest
		image71Digest := image71.ManifestDescriptor.Digest.String()
		image71Name := "repo7:1.0.0"
		imageMap[image71Name] = image71Digest
		indexDigest := multiarchImage.IndexDescriptor.Digest.String()
		indexName := "repoIndex:tagIndex"
		imageMap[indexName] = indexDigest
		indexM1Digest := multiarchImage.Images[0].ManifestDescriptor.Digest.String()
		indexM1Name := "repoIndex@" + indexM1Digest
		imageMap[indexM1Name] = indexM1Digest
		indexM2Digest := multiarchImage.Images[1].ManifestDescriptor.Digest.String()
		indexM2Name := "repoIndex@" + indexM2Digest
		imageMap[indexM2Name] = indexM2Digest
		indexM3Digest := multiarchImage.Images[2].ManifestDescriptor.Digest.String()
		indexM3Name := "repoIndex@" + indexM3Digest
		imageMap[indexM3Name] = indexM3Digest

		// Initialize a test CVE cache
		cache := cvecache.NewCveCache(20, logger)

		// MetaDB loaded with initial data, now mock the scanner
		// Setup test CVE data in mock scanner
		scanner := mocks.CveScannerMock{
			ScanImageFn: func(ctx context.Context, image string) (map[string]cvemodel.CVE, error) {
				result := cache.Get(image)
				// Will not match sending the repo:tag as a parameter, but we don't care
				if result != nil {
					return result, nil
				}

				repo, ref, isTag := zcommon.GetImageDirAndReference(image)
				if isTag {
					foundRef, ok := imageMap[image]
					if !ok {
						return nil, ErrBadTest
					}
					ref = foundRef
				}

				// Images in chronological order
				if repo == repo1 && ref == image11Digest {
					result := map[string]cvemodel.CVE{
						"CVE1": {
							ID:          "CVE1",
							Severity:    "MEDIUM",
							Title:       "Title CVE1",
							Description: "Description CVE1",
						},
					}

					cache.Add(ref, result)

					return result, nil
				}

				if repo == repo1 && zcommon.Contains([]string{image12Digest, image21Digest}, ref) {
					result := map[string]cvemodel.CVE{
						"CVE1": {
							ID:          "CVE1",
							Severity:    "MEDIUM",
							Title:       "Title CVE1",
							Description: "Description CVE1",
						},
						"CVE2": {
							ID:          "CVE2",
							Severity:    "HIGH",
							Title:       "Title CVE2",
							Description: "Description CVE2",
						},
						"CVE3": {
							ID:          "CVE3",
							Severity:    "LOW",
							Title:       "Title CVE3",
							Description: "Description CVE3",
						},
					}

					cache.Add(ref, result)

					return result, nil
				}

				if repo == repo1 && ref == image13Digest {
					result := map[string]cvemodel.CVE{
						"CVE3": {
							ID:          "CVE3",
							Severity:    "LOW",
							Title:       "Title CVE3",
							Description: "Description CVE3",
						},
					}

					cache.Add(ref, result)

					return result, nil
				}

				// As a minor release on 1.0.0 banch
				// does not include all fixes published in 1.1.0
				if repo == repo1 && ref == image14Digest {
					result := map[string]cvemodel.CVE{
						"CVE1": {
							ID:          "CVE1",
							Severity:    "MEDIUM",
							Title:       "Title CVE1",
							Description: "Description CVE1",
						},
						"CVE3": {
							ID:          "CVE3",
							Severity:    "LOW",
							Title:       "Title CVE3",
							Description: "Description CVE3",
						},
					}

					cache.Add(ref, result)

					return result, nil
				}

				// Unexpected error while scanning
				if repo == "repo7" {
					return map[string]cvemodel.CVE{}, ErrFailedScan
				}

				if (repo == repoIndex && ref == indexDigest) ||
					(repo == repoIndex && ref == indexM1Digest) {
					result := map[string]cvemodel.CVE{
						"CVE1": {
							ID:          "CVE1",
							Severity:    "MEDIUM",
							Title:       "Title CVE1",
							Description: "Description CVE1",
						},
					}

					// Simulate scanning an index results in scanning its manifests
					if ref == indexDigest {
						cache.Add(indexM1Digest, result)
						cache.Add(indexM2Digest, map[string]cvemodel.CVE{})
						cache.Add(indexM3Digest, map[string]cvemodel.CVE{})
					}

					cache.Add(ref, result)

					return result, nil
				}

				// By default the image has no vulnerabilities
				result = map[string]cvemodel.CVE{}
				cache.Add(ref, result)

				return result, nil
			},
			IsImageFormatScannableFn: func(repo string, reference string) (bool, error) {
				if repo == repoIndex {
					return true, nil
				}

				// Almost same logic compared to actual Trivy specific implementation
				imageDir, inputTag := repo, reference

				repoMeta, err := metaDB.GetRepoMeta(context.Background(), imageDir)
				if err != nil {
					return false, err
				}

				manifestDigestStr := reference

				if zcommon.IsTag(reference) {
					var ok bool

					descriptor, ok := repoMeta.Tags[inputTag]
					if !ok {
						return false, zerr.ErrTagMetaNotFound
					}

					manifestDigestStr = descriptor.Digest
				}

				manifestDigest, err := godigest.Parse(manifestDigestStr)
				if err != nil {
					return false, err
				}

				manifestData, err := metaDB.GetImageMeta(manifestDigest)
				if err != nil {
					return false, err
				}

				for _, imageLayer := range manifestData.Manifests[0].Manifest.Layers {
					switch imageLayer.MediaType {
					case ispec.MediaTypeImageLayerGzip, ispec.MediaTypeImageLayer, string(regTypes.DockerLayer):

						return true, nil
					default:

						return false, zerr.ErrScanNotSupported
					}
				}

				return false, nil
			},
			IsImageMediaScannableFn: func(repo, digest, mediaType string) (bool, error) {
				if repo == "repo2" {
					if digest == image21Digest {
						return false, nil
					}
				}

				return true, nil
			},
			IsResultCachedFn: func(digest string) bool {
				return cache.Contains(digest)
			},
			UpdateDBFn: func(ctx context.Context) error {
				cache.Purge()

				return nil
			},
		}

		// Purge scan, it should not be needed
		So(scanner.UpdateDB(context.Background()), ShouldBeNil)

		// Verify none of the entries are cached to begin with
		t.Log("verify cache is initially empty")

		for image, digestStr := range imageMap {
			t.Log("expecting " + image + " " + digestStr + " to be absent from cache")
			So(scanner.IsResultCached(digestStr), ShouldBeFalse)
		}

		// Start the generator
		generator := cveinfo.NewScanTaskGenerator(metaDB, scanner, logger)

		sch.SubmitGenerator(generator, 10*time.Second, scheduler.MediumPriority)

		sch.RunScheduler()

		defer sch.Shutdown()

		// Make sure the scanner generator has completed despite errors
		found, err := test.ReadLogFileAndSearchString(logPath,
			"finished scanning available images during scheduled cve scan", 40*time.Second)
		So(err, ShouldBeNil)
		So(found, ShouldBeTrue)

		t.Log("verify cache is up to date after scanner generator ran")

		// Verify all of the entries are cached
		for image, digestStr := range imageMap {
			repo, _, _ := zcommon.GetImageDirAndReference(image)

			ok, err := scanner.IsImageFormatScannable(repo, digestStr)
			if ok && err == nil && repo != "repo7" {
				t.Log("expecting " + image + " " + digestStr + " to be present in cache")
				So(scanner.IsResultCached(digestStr), ShouldBeTrue)
			} else {
				// We don't cache results for un-scannable manifests
				t.Log("expecting " + image + " " + digestStr + " to be absent from cache")
				So(scanner.IsResultCached(digestStr), ShouldBeFalse)
			}
		}

		found, err = test.ReadLogFileAndSearchString(logPath,
			"failed to obtain repo metadata during scheduled cve scan", 20*time.Second)
		So(err, ShouldBeNil)
		So(found, ShouldBeTrue)

		// Make sure the scanner generator is catching the scanning error for repo7
		found, err = test.ReadLogFileAndSearchString(logPath,
			"failed to perform scheduled cve scan for image", 20*time.Second)
		So(err, ShouldBeNil)
		So(found, ShouldBeTrue)

		// Make sure the scanner generator is triggered at least twice
		found, err = test.ReadLogFileAndCountStringOccurence(logPath,
			"finished scanning available images during scheduled cve scan", 30*time.Second, 2)
		So(err, ShouldBeNil)
		So(found, ShouldBeTrue)
	})
}

func TestScanGeneratorWithRealData(t *testing.T) {
	Convey("Test CVE scanning task scheduler real data", t, func() {
		rootDir := t.TempDir()

		logFile, err := os.CreateTemp(t.TempDir(), "zot-log*.txt")
		logPath := logFile.Name()

		So(err, ShouldBeNil)

		defer os.Remove(logFile.Name()) // clean up

		logger := log.NewLogger("debug", logPath)
		writers := io.MultiWriter(os.Stdout, logFile)
		logger.Logger = logger.Output(writers)

		cfg := config.New()
		cfg.Scheduler = &config.SchedulerConfig{NumWorkers: 3}

		boltDriver, err := boltdb.GetBoltDriver(boltdb.DBParameters{RootDir: rootDir})
		So(err, ShouldBeNil)

		metaDB, err := boltdb.New(boltDriver, logger)
		So(err, ShouldBeNil)

		metrics := monitoring.NewMetricsServer(true, logger)
		imageStore := local.NewImageStore(rootDir, false, false,
			logger, metrics, nil, nil, nil, nil)
		storeController := storage.StoreController{DefaultStore: imageStore}

		image := CreateRandomVulnerableImage()

		err = WriteImageToFileSystem(image, "zot-test", "0.0.1", storeController)
		So(err, ShouldBeNil)

		err = meta.ParseStorage(metaDB, storeController, logger)
		So(err, ShouldBeNil)

		scanner := cveinfo.NewScanner(storeController, metaDB, "ghcr.io/project-zot/trivy-db", "", logger)
		err = scanner.UpdateDB(context.Background())
		So(err, ShouldBeNil)

		So(scanner.IsResultCached(image.DigestStr()), ShouldBeFalse)

		sch := scheduler.NewScheduler(cfg, metrics, logger)

		generator := cveinfo.NewScanTaskGenerator(metaDB, scanner, logger)

		// Start the generator
		sch.SubmitGenerator(generator, 120*time.Second, scheduler.MediumPriority)

		sch.RunScheduler()

		defer sch.Shutdown()

		// Make sure the scanner generator has completed
		found, err := test.ReadLogFileAndSearchString(logPath,
			"finished scanning available images during scheduled cve scan", 120*time.Second)
		So(err, ShouldBeNil)
		So(found, ShouldBeTrue)

		found, err = test.ReadLogFileAndSearchString(logPath,
			image.ManifestDescriptor.Digest.String(), 120*time.Second)
		So(err, ShouldBeNil)
		So(found, ShouldBeTrue)

		found, err = test.ReadLogFileAndSearchString(logPath,
			"scheduled cve scan completed successfully for image", 120*time.Second)
		So(err, ShouldBeNil)
		So(found, ShouldBeTrue)

		So(scanner.IsResultCached(image.DigestStr()), ShouldBeTrue)

		cveMap, err := scanner.ScanImage(context.Background(), "zot-test:0.0.1")
		So(err, ShouldBeNil)
		t.Logf("cveMap: %v", cveMap)
		// As of September 22 2023 there are 5 CVEs:
		// CVE-2023-1255, CVE-2023-2650, CVE-2023-2975, CVE-2023-3817, CVE-2023-3446
		// There may be more discovered in the future
		So(len(cveMap), ShouldBeGreaterThanOrEqualTo, 5)
		So(cveMap, ShouldContainKey, "CVE-2023-1255")
		So(cveMap, ShouldContainKey, "CVE-2023-2650")
		So(cveMap, ShouldContainKey, "CVE-2023-2975")
		So(cveMap, ShouldContainKey, "CVE-2023-3817")
		So(cveMap, ShouldContainKey, "CVE-2023-3446")

		cveInfo := cveinfo.NewCVEInfo(scanner, metaDB, logger)

		// Based on cache population only, no extra scanning
		cveSummary, err := cveInfo.GetCVESummaryForImageMedia(context.Background(), "zot-test", image.DigestStr(),
			image.ManifestDescriptor.MediaType)
		So(err, ShouldBeNil)
		So(cveSummary.Count, ShouldBeGreaterThanOrEqualTo, 5)
		// As of September 22 the max severity is MEDIUM, but new CVEs could appear in the future
		So([]string{"MEDIUM", "HIGH", "CRITICAL", "UNKNOWN"}, ShouldContain, cveSummary.MaxSeverity)
	})
}
