package storage_test

import (
	"bytes"
	"context"
	_ "crypto/sha256"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"path"
	"slices"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/alicebob/miniredis/v2"
	"github.com/distribution/distribution/v3/registry/storage/driver/factory"
	_ "github.com/distribution/distribution/v3/registry/storage/driver/s3-aws"
	guuid "github.com/gofrs/uuid"
	godigest "github.com/opencontainers/go-digest"
	ispec "github.com/opencontainers/image-spec/specs-go/v1"
	"github.com/redis/go-redis/v9"
	"github.com/rs/zerolog"
	. "github.com/smartystreets/goconvey/convey"
	"gopkg.in/resty.v1"

	zerr "zotregistry.dev/zot/errors"
	"zotregistry.dev/zot/pkg/api/config"
	rediscfg "zotregistry.dev/zot/pkg/api/config/redis"
	"zotregistry.dev/zot/pkg/extensions/monitoring"
	zlog "zotregistry.dev/zot/pkg/log"
	"zotregistry.dev/zot/pkg/storage"
	"zotregistry.dev/zot/pkg/storage/cache"
	storageCommon "zotregistry.dev/zot/pkg/storage/common"
	storageConstants "zotregistry.dev/zot/pkg/storage/constants"
	"zotregistry.dev/zot/pkg/storage/gc"
	"zotregistry.dev/zot/pkg/storage/imagestore"
	"zotregistry.dev/zot/pkg/storage/local"
	"zotregistry.dev/zot/pkg/storage/s3"
	storageTypes "zotregistry.dev/zot/pkg/storage/types"
	. "zotregistry.dev/zot/pkg/test/image-utils"
	"zotregistry.dev/zot/pkg/test/mocks"
	tskip "zotregistry.dev/zot/pkg/test/skip"
)

var trueVal bool = true //nolint: gochecknoglobals

var DeleteReferrers = config.ImageRetention{ //nolint: gochecknoglobals
	Delay: storageConstants.DefaultRetentionDelay,
	Policies: []config.RetentionPolicy{
		{
			Repositories:    []string{"**"},
			DeleteReferrers: true,
			DeleteUntagged:  &trueVal,
		},
	},
}

func cleanupStorage(store storageTypes.Driver, name string) {
	_ = store.Delete(name)
}

type createObjectStoreOpts struct {
	storageType   string
	rootDir       string
	cacheDir      string
	cacheType     string
	miniRedisAddr string
}

func createObjectsStore(options createObjectStoreOpts) (
	storageTypes.Driver, storageTypes.ImageStore, storageTypes.Cache, error,
) {
	var (
		cacheDriver storageTypes.Cache
		useRelPaths bool
	)

	log := zlog.Logger{Logger: zerolog.New(os.Stdout)}

	if options.storageType == storageConstants.S3StorageDriverName {
		useRelPaths = false
	} else {
		useRelPaths = true
	}

	if options.cacheType == storageConstants.RedisDriverName {
		client, _ := rediscfg.GetRedisClient(map[string]interface{}{"url": options.miniRedisAddr}, log)

		cacheDriver, _ = storage.Create("redis", cache.RedisDriverParameters{
			Client:      client,
			RootDir:     options.cacheDir,
			UseRelPaths: useRelPaths,
		}, log)
	} else {
		cacheDriver, _ = storage.Create("boltdb", cache.BoltDBDriverParameters{
			RootDir:     options.cacheDir,
			Name:        "cache",
			UseRelPaths: useRelPaths,
		}, log)
	}

	metrics := monitoring.NewMetricsServer(false, log)

	if options.storageType != storageConstants.S3StorageDriverName {
		storeDriver := local.New(true)

		imgStore := imagestore.NewImageStore(options.rootDir, options.cacheDir, true,
			true, log, metrics, nil, storeDriver, cacheDriver, nil, nil)

		return storeDriver, imgStore, cacheDriver, nil
	}

	bucket := "zot-storage-test"
	endpoint := os.Getenv("S3MOCK_ENDPOINT")
	storageDriverParams := map[string]interface{}{
		"rootDir":        options.rootDir,
		"name":           "s3",
		"region":         "us-east-2",
		"bucket":         bucket,
		"regionendpoint": endpoint,
		"accesskey":      "minioadmin",
		"secretkey":      "minioadmin",
		"secure":         false,
		"skipverify":     false,
		"forcepathstyle": true,
	}

	storeName := fmt.Sprintf("%v", storageDriverParams["name"])

	s3Driver, err := factory.Create(context.Background(), storeName, storageDriverParams)
	if err != nil {
		panic(err)
	}

	// create bucket if it doesn't exists
	_, err = resty.R().Put("http://" + endpoint + "/" + bucket)
	if err != nil {
		panic(err)
	}

	imgStore := s3.NewImageStore(options.rootDir, options.cacheDir, true, false, log,
		metrics, nil, s3Driver, cacheDriver, nil, nil)

	return s3.New(s3Driver), imgStore, cacheDriver, err
}

//nolint:gochecknoglobals
var testCases = []struct {
	testCaseName string
	storageType  string
	cacheType    string
}{
	{
		testCaseName: "S3APIs_BoltDB",
		storageType:  storageConstants.S3StorageDriverName,
		cacheType:    storageConstants.BoltdbName,
	},
	{
		testCaseName: "FileSystemAPIs_BoltDB",
		storageType:  storageConstants.LocalStorageDriverName,
		cacheType:    storageConstants.BoltdbName,
	},
	{
		testCaseName: "S3APIs_Redis",
		storageType:  storageConstants.S3StorageDriverName,
		cacheType:    storageConstants.RedisDriverName,
	},
	{
		testCaseName: "FileSystemAPIs_Redis",
		storageType:  storageConstants.LocalStorageDriverName,
		cacheType:    storageConstants.RedisDriverName,
	},
}

func TestStorageNew(t *testing.T) {
	Convey("New fail", t, func() {
		// store name is wrong
		conf := config.New()
		conf.Storage.RootDirectory = "dir"
		conf.Storage.StorageDriver = map[string]interface{}{}

		_, err := storage.New(conf, nil, nil, zlog.NewLogger("debug", ""), nil)
		So(err, ShouldNotBeNil)
	})
}

func TestGetAllDedupeReposCandidates(t *testing.T) {
	for _, testcase := range testCases {
		testcase := testcase
		t.Run(testcase.testCaseName, func(t *testing.T) {
			var imgStore storageTypes.ImageStore

			cacheDir := t.TempDir()

			opts := createObjectStoreOpts{
				rootDir:     cacheDir,
				cacheDir:    cacheDir,
				cacheType:   testcase.cacheType,
				storageType: testcase.storageType,
			}

			if testcase.cacheType == storageConstants.RedisDriverName {
				miniRedis := miniredis.RunT(t)
				opts.miniRedisAddr = "redis://" + miniRedis.Addr()
				defer DumpKeys(t, opts.miniRedisAddr)
			}

			if testcase.storageType == storageConstants.S3StorageDriverName {
				tskip.SkipS3(t)

				uuid, err := guuid.NewV4()
				if err != nil {
					panic(err)
				}

				testDir := path.Join("/oci-repo-test", uuid.String())
				opts.rootDir = testDir

				var store storageTypes.Driver
				store, imgStore, _, _ = createObjectsStore(opts)
				defer cleanupStorage(store, testDir)
			} else {
				_, imgStore, _, _ = createObjectsStore(opts)
			}

			Convey("Push repos with deduped blobs", t, func(c C) {
				repoNames := []string{
					"first",
					"second",
					"repo/a",
					"repo/a/b/c/d/e/f",
					"repo/repo-b/blobs",
					"foo/bar/baz",
					"blobs/foo/bar/blobs",
					"blobs",
					"blobs/foo",
				}

				storeController := storage.StoreController{DefaultStore: imgStore}

				image := CreateRandomImage()

				for _, repoName := range repoNames {
					err := WriteImageToFileSystem(image, repoName, tag, storeController)
					So(err, ShouldBeNil)
				}

				randomBlobDigest := image.Manifest.Layers[0].Digest

				repos, err := imgStore.GetAllDedupeReposCandidates(randomBlobDigest)
				So(err, ShouldBeNil)
				slices.Sort(repoNames)
				slices.Sort(repos)
				So(repoNames, ShouldResemble, repos)
			})
		})
	}
}

func TestStorageAPIs(t *testing.T) {
	for _, testcase := range testCases {
		testcase := testcase
		t.Run(testcase.testCaseName, func(t *testing.T) {
			var imgStore storageTypes.ImageStore

			cacheDir := t.TempDir()

			opts := createObjectStoreOpts{
				rootDir:     cacheDir,
				cacheDir:    cacheDir,
				cacheType:   testcase.cacheType,
				storageType: testcase.storageType,
			}

			if testcase.cacheType == storageConstants.RedisDriverName {
				miniRedis := miniredis.RunT(t)
				opts.miniRedisAddr = "redis://" + miniRedis.Addr()
				defer DumpKeys(t, opts.miniRedisAddr)
			}

			if testcase.storageType == storageConstants.S3StorageDriverName {
				tskip.SkipS3(t)

				uuid, err := guuid.NewV4()
				if err != nil {
					panic(err)
				}

				testDir := path.Join("/oci-repo-test", uuid.String())
				opts.rootDir = testDir

				var store storageTypes.Driver
				store, imgStore, _, _ = createObjectsStore(opts)
				defer cleanupStorage(store, testDir)
			} else {
				_, imgStore, _, _ = createObjectsStore(opts)
			}

			Convey("Repo layout", t, func(c C) {
				repoName := "test"

				Convey("Get all blobs from repo without initialization", func() {
					allBlobs, err := imgStore.GetAllBlobs(repoName)
					So(err, ShouldBeNil)
					So(allBlobs, ShouldBeEmpty)

					ok := imgStore.DirExists(path.Join(imgStore.RootDir(), repoName))
					So(ok, ShouldBeFalse)
				})

				Convey("Validate repo without initialization", func() {
					v, err := imgStore.ValidateRepo(repoName)
					So(v, ShouldEqual, false)
					So(err, ShouldNotBeNil)

					ok := imgStore.DirExists(path.Join(imgStore.RootDir(), repoName))
					So(ok, ShouldBeFalse)
				})

				Convey("Initialize repo", func() {
					err := imgStore.InitRepo(repoName)
					So(err, ShouldBeNil)

					ok := imgStore.DirExists(path.Join(imgStore.RootDir(), repoName))
					So(ok, ShouldBeTrue)

					storeController := storage.StoreController{}
					storeController.DefaultStore = imgStore
					So(storeController.GetImageStore("test"), ShouldResemble, imgStore)
				})

				Convey("Validate repo", func() {
					repos, err := imgStore.ValidateRepo(repoName)
					So(err, ShouldBeNil)
					So(repos, ShouldEqual, true)
				})

				Convey("Get repos", func() {
					repos, err := imgStore.GetRepositories()
					So(err, ShouldBeNil)
					So(repos, ShouldNotBeEmpty)

					repos, more, err := imgStore.GetNextRepositories("", -1, func(repo string) (bool, error) {
						return true, nil
					})

					So(more, ShouldBeFalse)
					So(err, ShouldBeNil)
					So(repos, ShouldNotBeEmpty)
				})

				Convey("Get image tags", func() {
					v, err := imgStore.GetImageTags("test")
					So(err, ShouldBeNil)
					So(v, ShouldBeEmpty)
				})

				Convey("Full blob upload unavailable algorithm", func() {
					body := []byte("this blob will be hashed using an unavailable hashing algorithm")
					buf := bytes.NewBuffer(body)
					digest := godigest.Digest("md5:8114c3f59ef9dcf737410e0f4b00a154")
					upload, n, err := imgStore.FullBlobUpload("test", buf, digest)
					So(err, ShouldEqual, godigest.ErrDigestUnsupported)
					So(n, ShouldEqual, -1)
					So(upload, ShouldEqual, "")

					// Check no blobs are returned and there are no errors
					// if other paths for different algorithms are missing
					digests, err := imgStore.GetAllBlobs("test")
					So(err, ShouldBeNil)
					So(digests, ShouldBeEmpty)
				})

				Convey("Full blob upload", func() {
					body := []byte("this is a blob")
					buf := bytes.NewBuffer(body)
					digest := godigest.FromBytes(body)
					upload, n, err := imgStore.FullBlobUpload("test", buf, digest)
					So(err, ShouldBeNil)
					So(n, ShouldEqual, len(body))
					So(upload, ShouldNotBeEmpty)

					err = imgStore.VerifyBlobDigestValue("test", digest)
					So(err, ShouldBeNil)

					// Check the blob is returned and there are no errors
					// if other paths for different algorithms are missing
					digests, err := imgStore.GetAllBlobs("test")
					So(err, ShouldBeNil)
					So(digests, ShouldContain, digest)
					So(len(digests), ShouldEqual, 1)
				})

				Convey("Full blob upload sha512", func() {
					body := []byte("this blob will be hashed using sha512")
					buf := bytes.NewBuffer(body)
					digest := godigest.SHA512.FromBytes(body)
					upload, n, err := imgStore.FullBlobUpload("test", buf, digest)
					So(err, ShouldBeNil)
					So(n, ShouldEqual, len(body))
					So(upload, ShouldNotBeEmpty)

					// Check the blob is returned and there are no errors
					// if other paths for different algorithms are missing
					digests, err := imgStore.GetAllBlobs("test")
					So(err, ShouldBeNil)
					So(digests, ShouldContain, digest)
					// imgStore is reused so look for this digest and
					// the ones uploaded by previous tests
					So(len(digests), ShouldEqual, 2)
				})

				Convey("Full blob upload sha384", func() {
					body := []byte("this blob will be hashed using sha384")
					buf := bytes.NewBuffer(body)
					digest := godigest.SHA384.FromBytes(body)
					upload, n, err := imgStore.FullBlobUpload("test", buf, digest)
					So(err, ShouldBeNil)
					So(n, ShouldEqual, len(body))
					So(upload, ShouldNotBeEmpty)

					// Check the blob is returned and there are no errors
					// if other paths for different algorithms are missing
					digests, err := imgStore.GetAllBlobs("test")
					So(err, ShouldBeNil)
					So(digests, ShouldContain, digest)
					// imgStore is reused so look for this digest and
					// the ones uploaded by previous tests
					So(len(digests), ShouldEqual, 3)
				})

				Convey("New blob upload", func() {
					upload, err := imgStore.NewBlobUpload("test")
					So(err, ShouldBeNil)
					So(upload, ShouldNotBeEmpty)

					err = imgStore.DeleteBlobUpload("test", upload)
					So(err, ShouldBeNil)

					upload, err = imgStore.NewBlobUpload("test")
					So(err, ShouldBeNil)
					So(upload, ShouldNotBeEmpty)

					Convey("Get blob upload", func() {
						bupload, err := imgStore.GetBlobUpload("test", "invalid")
						So(err, ShouldNotBeNil)
						So(bupload, ShouldEqual, -1)

						bupload, err = imgStore.GetBlobUpload("hi", " \255")
						So(err, ShouldNotBeNil)
						So(bupload, ShouldEqual, -1)

						bupload, err = imgStore.GetBlobUpload("test", upload)
						So(err, ShouldBeNil)
						So(bupload, ShouldBeGreaterThanOrEqualTo, 0)

						bupload, err = imgStore.BlobUploadInfo("test", upload)
						So(err, ShouldBeNil)
						So(bupload, ShouldBeGreaterThanOrEqualTo, 0)

						content := []byte("test-data1")
						firstChunkContent := []byte("test")
						firstChunkBuf := bytes.NewBuffer(firstChunkContent)
						secondChunkContent := []byte("-data1")
						secondChunkBuf := bytes.NewBuffer(secondChunkContent)
						firstChunkLen := firstChunkBuf.Len()
						secondChunkLen := secondChunkBuf.Len()

						buf := bytes.NewBuffer(content)
						buflen := buf.Len()
						digest := godigest.FromBytes(content)
						blobDigest := digest

						// invalid chunk range
						_, err = imgStore.PutBlobChunk("test", upload, 10, int64(buflen), buf)
						So(err, ShouldNotBeNil)

						bupload, err = imgStore.PutBlobChunk("test", upload, 0, int64(firstChunkLen), firstChunkBuf)
						So(err, ShouldBeNil)
						So(bupload, ShouldEqual, firstChunkLen)

						bupload, err = imgStore.GetBlobUpload("test", upload)
						So(err, ShouldBeNil)
						So(bupload, ShouldEqual, int64(firstChunkLen))

						bupload, err = imgStore.BlobUploadInfo("test", upload)
						So(err, ShouldBeNil)
						So(bupload, ShouldEqual, int64(firstChunkLen))

						bupload, err = imgStore.PutBlobChunk("test", upload, int64(firstChunkLen), int64(buflen), secondChunkBuf)
						So(err, ShouldBeNil)
						So(bupload, ShouldEqual, int64(firstChunkLen+secondChunkLen))

						err = imgStore.FinishBlobUpload("test", upload, buf, digest)
						So(err, ShouldBeNil)

						_, _, err = imgStore.CheckBlob("test", digest)
						So(err, ShouldBeNil)

						ok, _, _, err := imgStore.StatBlob("test", digest)
						So(ok, ShouldBeTrue)
						So(err, ShouldBeNil)

						blob, _, err := imgStore.GetBlob("test", digest, "application/vnd.oci.image.layer.v1.tar+gzip")
						So(err, ShouldBeNil)

						blobBuf := new(strings.Builder)
						n, err := io.Copy(blobBuf, blob)
						// check errors
						So(n, ShouldEqual, buflen)
						So(err, ShouldBeNil)
						So(blobBuf.String(), ShouldEqual, buf.String())

						blobContent, err := imgStore.GetBlobContent("test", digest)
						So(err, ShouldBeNil)
						So(blobContent, ShouldResemble, content)

						err = blob.Close()
						So(err, ShouldBeNil)

						manifest := ispec.Manifest{}
						manifest.SchemaVersion = 2
						manifestBuf, err := json.Marshal(manifest)
						So(err, ShouldBeNil)

						Convey("Bad image manifest", func() {
							_, _, err = imgStore.PutImageManifest("test", digest.String(), "application/json",
								manifestBuf)
							So(err, ShouldNotBeNil)

							_, _, err = imgStore.PutImageManifest("test", digest.String(), ispec.MediaTypeImageManifest,
								[]byte{})
							So(err, ShouldNotBeNil)

							_, _, err = imgStore.PutImageManifest("test", digest.String(), ispec.MediaTypeImageManifest,
								[]byte(`{"test":true}`))
							So(err, ShouldNotBeNil)

							_, _, err = imgStore.PutImageManifest("test", digest.String(), ispec.MediaTypeImageManifest,
								manifestBuf)
							So(err, ShouldNotBeNil)

							_, _, _, err = imgStore.GetImageManifest("test", digest.String())
							So(err, ShouldNotBeNil)

							_, _, _, err = imgStore.GetImageManifest("inexistent", digest.String())
							So(err, ShouldNotBeNil)
						})

						Convey("Good image manifest", func() {
							cblob, cdigest := GetRandomImageConfig()
							_, clen, err := imgStore.FullBlobUpload("test", bytes.NewReader(cblob), cdigest)
							So(err, ShouldBeNil)
							So(clen, ShouldEqual, len(cblob))

							hasBlob, _, err := imgStore.CheckBlob("test", cdigest)
							So(err, ShouldBeNil)
							So(hasBlob, ShouldEqual, true)

							annotationsMap := make(map[string]string)
							annotationsMap[ispec.AnnotationRefName] = "1.0"
							manifest := ispec.Manifest{
								Config: ispec.Descriptor{
									MediaType: "application/vnd.oci.image.config.v1+json",
									Digest:    cdigest,
									Size:      int64(len(cblob)),
								},
								Layers: []ispec.Descriptor{
									{
										MediaType: "application/vnd.oci.image.layer.v1.tar",
										Digest:    digest,
										Size:      int64(buflen),
									},
								},
								Annotations: annotationsMap,
							}

							manifest.SchemaVersion = 2
							manifestBuf, err = json.Marshal(manifest)
							So(err, ShouldBeNil)

							digest := godigest.FromBytes(manifestBuf)

							// bad manifest
							manifest.Layers[0].Digest = godigest.FromBytes([]byte("inexistent"))
							badMb, err := json.Marshal(manifest)
							So(err, ShouldBeNil)

							_, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageManifest, badMb)
							So(err, ShouldNotBeNil)

							_, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageManifest, manifestBuf)
							So(err, ShouldBeNil)

							// same manifest for coverage
							_, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageManifest, manifestBuf)
							So(err, ShouldBeNil)

							_, _, err = imgStore.PutImageManifest("test", "2.0", ispec.MediaTypeImageManifest, manifestBuf)
							So(err, ShouldBeNil)

							_, _, err = imgStore.PutImageManifest("test", "3.0", ispec.MediaTypeImageManifest, manifestBuf)
							So(err, ShouldBeNil)

							_, err = imgStore.GetImageTags("inexistent")
							So(err, ShouldNotBeNil)

							// total tags should be 3 but they have same reference.
							tags, err := imgStore.GetImageTags("test")
							So(err, ShouldBeNil)
							So(len(tags), ShouldEqual, 3)

							_, _, _, err = imgStore.GetImageManifest("test", digest.String())
							So(err, ShouldBeNil)

							_, _, _, err = imgStore.GetImageManifest("test", "3.0")
							So(err, ShouldBeNil)

							err = imgStore.DeleteImageManifest("test", "1.0", false)
							So(err, ShouldBeNil)

							tags, err = imgStore.GetImageTags("test")
							So(err, ShouldBeNil)
							So(len(tags), ShouldEqual, 2)

							repos, err := imgStore.GetRepositories()
							So(err, ShouldBeNil)
							So(len(repos), ShouldEqual, 1)
							So(repos[0], ShouldEqual, "test")

							repos, more, err := imgStore.GetNextRepositories("", -1, func(repo string) (bool, error) {
								return true, nil
							})
							So(err, ShouldBeNil)
							So(more, ShouldBeFalse)
							So(len(repos), ShouldEqual, 1)
							So(repos[0], ShouldEqual, "test")

							repos, more, err = imgStore.GetNextRepositories("", -1, func(repo string) (bool, error) {
								return false, nil
							})
							So(err, ShouldBeNil)
							So(more, ShouldBeFalse)
							So(len(repos), ShouldEqual, 0)

							// We deleted only one tag, make sure blob should not be removed.
							hasBlob, _, err = imgStore.CheckBlob("test", digest)
							So(err, ShouldBeNil)
							So(hasBlob, ShouldEqual, true)

							// with detectManifestCollision should get error
							err = imgStore.DeleteImageManifest("test", digest.String(), true)
							So(err, ShouldNotBeNil)

							// If we pass reference all manifest with input reference should be deleted.
							err = imgStore.DeleteImageManifest("test", digest.String(), false)
							So(err, ShouldBeNil)

							tags, err = imgStore.GetImageTags("test")
							So(err, ShouldBeNil)
							So(len(tags), ShouldEqual, 0)

							// All tags/references are deleted, blob should not be present in disk.
							hasBlob, _, err = imgStore.CheckBlob("test", digest)
							So(err, ShouldNotBeNil)
							So(hasBlob, ShouldEqual, false)

							hasBlob, _, _, err = imgStore.StatBlob("test", digest)
							So(err, ShouldNotBeNil)
							So(hasBlob, ShouldEqual, false)

							err = imgStore.DeleteBlob("test", "inexistent")
							So(err, ShouldNotBeNil)

							err = imgStore.DeleteBlob("test", godigest.FromBytes([]byte("inexistent")))
							So(err, ShouldNotBeNil)

							err = imgStore.DeleteBlob("test", blobDigest)
							So(err, ShouldBeNil)

							_, _, _, err = imgStore.GetImageManifest("test", digest.String())
							So(err, ShouldNotBeNil)
						})
					})

					err = imgStore.DeleteBlobUpload("test", upload)
					So(err, ShouldNotBeNil)
				})

				Convey("New blob upload streamed", func() {
					bupload, err := imgStore.NewBlobUpload("test")
					So(err, ShouldBeNil)
					So(bupload, ShouldNotBeEmpty)

					Convey("Get blob upload", func() {
						upload, err := imgStore.GetBlobUpload("test", "invalid")
						So(err, ShouldNotBeNil)
						So(upload, ShouldEqual, -1)

						upload, err = imgStore.GetBlobUpload("test", bupload)
						So(err, ShouldBeNil)
						So(upload, ShouldBeGreaterThanOrEqualTo, 0)

						_, err = imgStore.BlobUploadInfo("test", "inexistent")
						So(err, ShouldNotBeNil)

						upload, err = imgStore.BlobUploadInfo("test", bupload)
						So(err, ShouldBeNil)
						So(upload, ShouldBeGreaterThanOrEqualTo, 0)

						content := []byte("test-data2")
						buf := bytes.NewBuffer(content)
						buflen := buf.Len()
						digest := godigest.FromBytes(content)
						upload, err = imgStore.PutBlobChunkStreamed("test", bupload, buf)
						So(err, ShouldBeNil)
						So(upload, ShouldEqual, buflen)

						_, err = imgStore.PutBlobChunkStreamed("test", "inexistent", buf)
						So(err, ShouldNotBeNil)

						err = imgStore.FinishBlobUpload("test", "inexistent", buf, digest)
						So(err, ShouldNotBeNil)

						// invalid digest
						err = imgStore.FinishBlobUpload("test", "inexistent", buf, "sha256:invalid")
						So(err, ShouldNotBeNil)

						err = imgStore.FinishBlobUpload("test", bupload, buf, digest)
						So(err, ShouldBeNil)

						ok, _, err := imgStore.CheckBlob("test", digest)
						So(ok, ShouldBeTrue)
						So(err, ShouldBeNil)

						ok, _, _, err = imgStore.StatBlob("test", digest)
						So(ok, ShouldBeTrue)
						So(err, ShouldBeNil)

						_, _, err = imgStore.GetBlob("test", "inexistent", "application/vnd.oci.image.layer.v1.tar+gzip")
						So(err, ShouldNotBeNil)

						blob, _, err := imgStore.GetBlob("test", digest, "application/vnd.oci.image.layer.v1.tar+gzip")
						So(err, ShouldBeNil)
						err = blob.Close()
						So(err, ShouldBeNil)

						blobContent, err := imgStore.GetBlobContent("test", digest)
						So(err, ShouldBeNil)
						So(content, ShouldResemble, blobContent)

						_, err = imgStore.GetBlobContent("inexistent", digest)
						So(err, ShouldNotBeNil)

						manifest := ispec.Manifest{}
						manifest.SchemaVersion = 2
						manifestBuf, err := json.Marshal(manifest)
						So(err, ShouldBeNil)

						Convey("Bad digests", func() {
							_, _, err := imgStore.FullBlobUpload("test", bytes.NewBuffer([]byte{}), "inexistent")
							So(err, ShouldNotBeNil)

							_, _, err = imgStore.CheckBlob("test", "inexistent")
							So(err, ShouldNotBeNil)

							_, _, _, err = imgStore.StatBlob("test", "inexistent")
							So(err, ShouldNotBeNil)
						})

						Convey("Bad image manifest", func() {
							_, _, err = imgStore.PutImageManifest("test", digest.String(),
								ispec.MediaTypeImageManifest, manifestBuf)
							So(err, ShouldNotBeNil)

							_, _, err = imgStore.PutImageManifest("test", digest.String(),
								ispec.MediaTypeImageManifest, []byte("bad json"))
							So(err, ShouldNotBeNil)

							_, _, _, err = imgStore.GetImageManifest("test", digest.String())
							So(err, ShouldNotBeNil)
						})

						Convey("Good image manifest", func() {
							cblob, cdigest := GetRandomImageConfig()
							_, clen, err := imgStore.FullBlobUpload("test", bytes.NewReader(cblob), cdigest)
							So(err, ShouldBeNil)
							So(clen, ShouldEqual, len(cblob))

							hasBlob, _, err := imgStore.CheckBlob("test", cdigest)
							So(err, ShouldBeNil)
							So(hasBlob, ShouldEqual, true)

							manifest := ispec.Manifest{
								Config: ispec.Descriptor{
									MediaType: "application/vnd.oci.image.config.v1+json",
									Digest:    cdigest,
									Size:      int64(len(cblob)),
								},
								Layers: []ispec.Descriptor{
									{
										MediaType: "application/vnd.oci.image.layer.v1.tar",
										Digest:    digest,
										Size:      int64(buflen),
									},
								},
							}
							manifest.SchemaVersion = 2
							manifestBuf, err = json.Marshal(manifest)
							So(err, ShouldBeNil)

							digest := godigest.FromBytes(manifestBuf)
							_, _, err = imgStore.PutImageManifest("test", digest.String(),
								ispec.MediaTypeImageManifest, manifestBuf)
							So(err, ShouldBeNil)

							// same manifest for coverage
							_, _, err = imgStore.PutImageManifest("test", digest.String(),
								ispec.MediaTypeImageManifest, manifestBuf)
							So(err, ShouldBeNil)

							_, _, _, err = imgStore.GetImageManifest("test", digest.String())
							So(err, ShouldBeNil)

							_, err = imgStore.GetIndexContent("inexistent")
							So(err, ShouldNotBeNil)

							indexContent, err := imgStore.GetIndexContent("test")
							So(err, ShouldBeNil)

							if testcase.storageType == storageConstants.LocalStorageDriverName {
								err = os.Chmod(path.Join(imgStore.RootDir(), "test", "index.json"), 0o000)
								So(err, ShouldBeNil)
								_, err = imgStore.GetIndexContent("test")
								So(err, ShouldNotBeNil)
								err = os.Chmod(path.Join(imgStore.RootDir(), "test", "index.json"), 0o644)
								So(err, ShouldBeNil)
							}

							var index ispec.Index

							err = json.Unmarshal(indexContent, &index)
							So(err, ShouldBeNil)

							So(len(index.Manifests), ShouldEqual, 1)

							err = imgStore.DeleteImageManifest("test", "1.0", false)
							So(err, ShouldNotBeNil)

							err = imgStore.DeleteImageManifest("inexistent", "1.0", false)
							So(err, ShouldNotBeNil)

							err = imgStore.DeleteImageManifest("test", digest.String(), false)
							So(err, ShouldBeNil)

							_, _, _, err = imgStore.GetImageManifest("test", digest.String())
							So(err, ShouldNotBeNil)
						})
					})

					err = imgStore.DeleteBlobUpload("test", bupload)
					So(err, ShouldNotBeNil)
				})

				Convey("Modify manifest in-place", func() {
					// original blob
					upload, err := imgStore.NewBlobUpload("replace")
					So(err, ShouldBeNil)
					So(upload, ShouldNotBeEmpty)

					content := []byte("test-data-replace-1")
					buf := bytes.NewBuffer(content)
					buflen := buf.Len()
					digest := godigest.FromBytes(content)
					blob, err := imgStore.PutBlobChunkStreamed("replace", upload, buf)
					So(err, ShouldBeNil)
					So(blob, ShouldEqual, buflen)

					blobDigest1 := strings.Split(digest.String(), ":")[1]
					So(blobDigest1, ShouldNotBeEmpty)

					err = imgStore.FinishBlobUpload("replace", upload, buf, digest)
					So(err, ShouldBeNil)
					So(blob, ShouldEqual, buflen)

					cblob, cdigest := GetRandomImageConfig()
					_, clen, err := imgStore.FullBlobUpload("replace", bytes.NewReader(cblob), cdigest)
					So(err, ShouldBeNil)
					So(clen, ShouldEqual, len(cblob))

					hasBlob, _, err := imgStore.CheckBlob("replace", cdigest)
					So(err, ShouldBeNil)
					So(hasBlob, ShouldEqual, true)

					manifest := ispec.Manifest{
						Config: ispec.Descriptor{
							MediaType: "application/vnd.oci.image.config.v1+json",
							Digest:    cdigest,
							Size:      int64(len(cblob)),
						},
						Layers: []ispec.Descriptor{
							{
								MediaType: "application/vnd.oci.image.layer.v1.tar",
								Digest:    digest,
								Size:      int64(buflen),
							},
						},
					}
					manifest.SchemaVersion = 2
					manifestBuf, err := json.Marshal(manifest)
					So(err, ShouldBeNil)

					digest = godigest.FromBytes(manifestBuf)
					_, _, err = imgStore.PutImageManifest("replace", "1.0", ispec.MediaTypeImageManifest, manifestBuf)
					So(err, ShouldBeNil)

					_, _, _, err = imgStore.GetImageManifest("replace", digest.String())
					So(err, ShouldBeNil)

					// new blob to replace
					upload, err = imgStore.NewBlobUpload("replace")
					So(err, ShouldBeNil)
					So(upload, ShouldNotBeEmpty)

					content = []byte("test-data-replace-2")
					buf = bytes.NewBuffer(content)
					buflen = buf.Len()
					digest = godigest.FromBytes(content)
					blob, err = imgStore.PutBlobChunkStreamed("replace", upload, buf)
					So(err, ShouldBeNil)
					So(blob, ShouldEqual, buflen)

					blobDigest2 := strings.Split(digest.String(), ":")[1]
					So(blobDigest2, ShouldNotBeEmpty)

					err = imgStore.FinishBlobUpload("replace", upload, buf, digest)
					So(err, ShouldBeNil)
					So(blob, ShouldEqual, buflen)

					cblob, cdigest = GetRandomImageConfig()
					_, clen, err = imgStore.FullBlobUpload("replace", bytes.NewReader(cblob), cdigest)
					So(err, ShouldBeNil)
					So(clen, ShouldEqual, len(cblob))

					hasBlob, _, err = imgStore.CheckBlob("replace", cdigest)
					So(err, ShouldBeNil)
					So(hasBlob, ShouldEqual, true)

					manifest = ispec.Manifest{
						Config: ispec.Descriptor{
							MediaType: "application/vnd.oci.image.config.v1+json",
							Digest:    cdigest,
							Size:      int64(len(cblob)),
						},
						Layers: []ispec.Descriptor{
							{
								MediaType: "application/vnd.oci.image.layer.v1.tar",
								Digest:    digest,
								Size:      int64(buflen),
							},
						},
					}
					manifest.SchemaVersion = 2
					manifestBuf, err = json.Marshal(manifest)
					So(err, ShouldBeNil)

					_ = godigest.FromBytes(manifestBuf)
					_, _, err = imgStore.PutImageManifest("replace", "1.0", ispec.MediaTypeImageManifest, manifestBuf)
					So(err, ShouldBeNil)
				})

				Convey("Locks", func() {
					// in parallel, a mix of read and write locks - mainly for coverage
					var wg sync.WaitGroup
					for i := 0; i < 1000; i++ {
						wg.Add(2)

						go func() {
							var lockLatency time.Time

							defer wg.Done()
							imgStore.Lock(&lockLatency)
							func() {}()
							imgStore.Unlock(&lockLatency)
						}()
						go func() {
							var lockLatency time.Time

							defer wg.Done()
							imgStore.RLock(&lockLatency)
							func() {}()
							imgStore.RUnlock(&lockLatency)
						}()
					}

					wg.Wait()
				})
			})
		})
	}
}

func TestMandatoryAnnotations(t *testing.T) {
	for _, testcase := range testCases {
		testcase := testcase
		t.Run(testcase.testCaseName, func(t *testing.T) {
			var (
				imgStore storageTypes.ImageStore
				store    storageTypes.Driver
				testDir  string
			)

			log := zlog.Logger{Logger: zerolog.New(os.Stdout)}
			metrics := monitoring.NewMetricsServer(false, log)

			cacheDir := t.TempDir()

			opts := createObjectStoreOpts{
				rootDir:     cacheDir,
				cacheDir:    cacheDir,
				cacheType:   testcase.cacheType,
				storageType: testcase.storageType,
			}

			if testcase.cacheType == storageConstants.RedisDriverName {
				miniRedis := miniredis.RunT(t)
				opts.miniRedisAddr = "redis://" + miniRedis.Addr()
				defer DumpKeys(t, opts.miniRedisAddr)
			}

			if testcase.storageType == storageConstants.S3StorageDriverName {
				tskip.SkipS3(t)

				uuid, err := guuid.NewV4()
				if err != nil {
					panic(err)
				}

				testDir = path.Join("/oci-repo-test", uuid.String())
				opts.rootDir = testDir

				var cacheDriver storageTypes.Cache
				store, _, cacheDriver, _ = createObjectsStore(opts)

				imgStore = imagestore.NewImageStore(testDir, cacheDir, false, false, log, metrics,
					&mocks.MockedLint{
						LintFn: func(repo string, manifestDigest godigest.Digest, imageStore storageTypes.ImageStore) (bool, error) {
							return false, nil
						},
					}, store, cacheDriver, nil, nil)

				defer cleanupStorage(store, testDir)
			} else {
				var cacheDriver storageTypes.Cache
				store, _, cacheDriver, _ = createObjectsStore(opts)

				imgStore = imagestore.NewImageStore(cacheDir, cacheDir, true, true, log, metrics,
					&mocks.MockedLint{
						LintFn: func(repo string, manifestDigest godigest.Digest, imageStore storageTypes.ImageStore) (bool, error) {
							return false, nil
						},
					}, store, cacheDriver, nil, nil)
			}

			Convey("Setup manifest", t, func() {
				content := []byte("test-data1")
				buf := bytes.NewBuffer(content)
				buflen := buf.Len()
				digest := godigest.FromBytes(content)

				_, _, err := imgStore.FullBlobUpload("test", bytes.NewReader(buf.Bytes()), digest)
				So(err, ShouldBeNil)

				cblob, cdigest := GetRandomImageConfig()
				_, clen, err := imgStore.FullBlobUpload("test", bytes.NewReader(cblob), cdigest)
				So(err, ShouldBeNil)
				So(clen, ShouldEqual, len(cblob))

				annotationsMap := make(map[string]string)
				annotationsMap[ispec.AnnotationRefName] = "1.0"

				manifest := ispec.Manifest{
					Config: ispec.Descriptor{
						MediaType: "application/vnd.oci.image.config.v1+json",
						Digest:    cdigest,
						Size:      int64(len(cblob)),
					},
					Layers: []ispec.Descriptor{
						{
							MediaType: "application/vnd.oci.image.layer.v1.tar",
							Digest:    digest,
							Size:      int64(buflen),
						},
					},
					Annotations: annotationsMap,
				}

				manifest.SchemaVersion = 2
				manifestBuf, err := json.Marshal(manifest)
				So(err, ShouldBeNil)

				Convey("Missing mandatory annotations", func() {
					_, _, err = imgStore.PutImageManifest("test", "1.0.0", ispec.MediaTypeImageManifest, manifestBuf)
					So(err, ShouldNotBeNil)
				})

				Convey("Error on mandatory annotations", func() {
					if testcase.storageType == storageConstants.S3StorageDriverName {
						imgStore = imagestore.NewImageStore(testDir, cacheDir, false, false, log, metrics,
							&mocks.MockedLint{
								LintFn: func(repo string, manifestDigest godigest.Digest, imageStore storageTypes.ImageStore) (bool, error) {
									//nolint: err113
									return false, errors.New("linter error")
								},
							}, store, nil, nil, nil)
					} else {
						var cacheDriver storageTypes.Cache
						store, _, cacheDriver, _ = createObjectsStore(opts)

						imgStore = imagestore.NewImageStore(cacheDir, cacheDir, true, true, log, metrics,
							&mocks.MockedLint{
								LintFn: func(repo string, manifestDigest godigest.Digest, imageStore storageTypes.ImageStore) (bool, error) {
									//nolint: err113
									return false, errors.New("linter error")
								},
							}, store, cacheDriver, nil, nil)
					}

					_, _, err = imgStore.PutImageManifest("test", "1.0.0", ispec.MediaTypeImageManifest, manifestBuf)
					So(err, ShouldNotBeNil)
				})
			})
		})
	}
}

func TestStorageSubpaths(t *testing.T) {
	Convey("Reach for coverage", t, func() {
		tmpDirSubpath := t.TempDir()
		config := &config.Config{
			Storage: config.GlobalStorageConfig{
				StorageConfig: config.StorageConfig{RootDirectory: t.TempDir()},
				SubPaths: map[string]config.StorageConfig{
					"a/":          {RootDirectory: tmpDirSubpath},
					tmpDirSubpath: {RootDirectory: tmpDirSubpath},
				},
			},
		}

		_, err := storage.New(config, nil, nil, zlog.NewLogger("debug", ""), nil)
		So(err, ShouldBeNil)
	})

	Convey("Create unique subpath, error for cache driver", t, func() {
		tmpDirSubpath := t.TempDir()
		config := &config.Config{
			Storage: config.GlobalStorageConfig{
				StorageConfig: config.StorageConfig{RootDirectory: t.TempDir()},
				SubPaths: map[string]config.StorageConfig{
					"a/": {
						RootDirectory: tmpDirSubpath,
						RemoteCache:   true,
						StorageDriver: map[string]interface{}{},
						Dedupe:        true,
					},
				},
			},
		}

		// create boltdb file and make it un-openable
		dbPath := path.Join(tmpDirSubpath, storageConstants.BoltdbName+storageConstants.DBExtensionName)
		err := os.WriteFile(dbPath, []byte(""), 0o000)
		So(err, ShouldBeNil)

		_, err = storage.New(config, nil, nil, zlog.NewLogger("debug", ""), nil)
		So(err, ShouldNotBeNil)

		err = os.Chmod(dbPath, 0o600)
		So(err, ShouldBeNil)
	})

	Convey("storeName != constants.S3StorageDriverName", t, func() {
		tmpDirSubpath := t.TempDir()
		config := &config.Config{
			Storage: config.GlobalStorageConfig{
				StorageConfig: config.StorageConfig{RootDirectory: t.TempDir()},
				SubPaths: map[string]config.StorageConfig{
					"a/": {
						RootDirectory: tmpDirSubpath,
						RemoteCache:   true,
						StorageDriver: map[string]interface{}{
							"name": "bad-name",
						},
					},
				},
			},
		}

		_, err := storage.New(config, nil, nil, zlog.NewLogger("debug", ""), nil)
		So(err, ShouldNotBeNil)
	})
}

func TestDeleteBlobsInUse(t *testing.T) {
	for _, testcase := range testCases {
		testcase := testcase
		t.Run(testcase.testCaseName, func(t *testing.T) {
			var imgStore storageTypes.ImageStore

			log := zlog.Logger{Logger: zerolog.New(os.Stdout)}

			cacheDir := t.TempDir()

			opts := createObjectStoreOpts{
				rootDir:     cacheDir,
				cacheDir:    cacheDir,
				cacheType:   testcase.cacheType,
				storageType: testcase.storageType,
			}

			if testcase.cacheType == storageConstants.RedisDriverName {
				miniRedis := miniredis.RunT(t)
				opts.miniRedisAddr = "redis://" + miniRedis.Addr()
				defer DumpKeys(t, opts.miniRedisAddr)
			}

			if testcase.storageType == storageConstants.S3StorageDriverName {
				tskip.SkipS3(t)

				uuid, err := guuid.NewV4()
				if err != nil {
					panic(err)
				}

				testDir := path.Join("/oci-repo-test", uuid.String())
				opts.rootDir = testDir

				var store storageTypes.Driver
				store, imgStore, _, _ = createObjectsStore(opts)

				defer cleanupStorage(store, testDir)
			} else {
				_, imgStore, _, _ = createObjectsStore(opts)
			}

			Convey("Setup manifest", t, func() {
				// put an unused blob
				content := []byte("unused blob")
				buf := bytes.NewBuffer(content)
				unusedDigest := godigest.FromBytes(content)

				_, _, err := imgStore.FullBlobUpload("repo", bytes.NewReader(buf.Bytes()), unusedDigest)
				So(err, ShouldBeNil)

				content = []byte("test-data1")
				buf = bytes.NewBuffer(content)
				buflen := buf.Len()
				digest := godigest.FromBytes(content)

				_, _, err = imgStore.FullBlobUpload("repo", bytes.NewReader(buf.Bytes()), digest)
				So(err, ShouldBeNil)

				cblob, cdigest := GetRandomImageConfig()
				_, clen, err := imgStore.FullBlobUpload("repo", bytes.NewReader(cblob), cdigest)
				So(err, ShouldBeNil)
				So(clen, ShouldEqual, len(cblob))

				annotationsMap := make(map[string]string)
				annotationsMap[ispec.AnnotationRefName] = tag

				manifest := ispec.Manifest{
					Config: ispec.Descriptor{
						MediaType: "application/vnd.oci.image.config.v1+json",
						Digest:    cdigest,
						Size:      int64(len(cblob)),
					},
					Layers: []ispec.Descriptor{
						{
							MediaType: "application/vnd.oci.image.layer.v1.tar",
							Digest:    digest,
							Size:      int64(buflen),
						},
					},
					Annotations: annotationsMap,
				}

				manifest.SchemaVersion = 2
				manifestBuf, err := json.Marshal(manifest)
				So(err, ShouldBeNil)

				manifestDigest, _, err := imgStore.PutImageManifest("repo", tag, ispec.MediaTypeImageManifest, manifestBuf)
				So(err, ShouldBeNil)

				Convey("Try to delete blob currently in use", func() {
					// layer blob
					err := imgStore.DeleteBlob("repo", digest)
					So(err, ShouldEqual, zerr.ErrBlobReferenced)

					// manifest
					err = imgStore.DeleteBlob("repo", manifestDigest)
					So(err, ShouldEqual, zerr.ErrBlobReferenced)

					// config
					err = imgStore.DeleteBlob("repo", cdigest)
					So(err, ShouldEqual, zerr.ErrBlobReferenced)
				})

				Convey("Delete unused blob", func() {
					err := imgStore.DeleteBlob("repo", unusedDigest)
					So(err, ShouldBeNil)
				})

				Convey("Delete manifest first, then blob", func() {
					err := imgStore.DeleteImageManifest("repo", manifestDigest.String(), false)
					So(err, ShouldBeNil)

					err = imgStore.DeleteBlob("repo", digest)
					So(err, ShouldBeNil)

					// config
					err = imgStore.DeleteBlob("repo", cdigest)
					So(err, ShouldBeNil)
				})

				if testcase.storageType != storageConstants.S3StorageDriverName {
					Convey("get image manifest error", func() {
						err := os.Chmod(path.Join(imgStore.RootDir(), "repo", "blobs", "sha256", manifestDigest.Encoded()), 0o000)
						So(err, ShouldBeNil)

						ok, _ := storageCommon.IsBlobReferenced(imgStore, "repo", unusedDigest, log)
						So(ok, ShouldBeFalse)

						err = os.Chmod(path.Join(imgStore.RootDir(), "repo", "blobs", "sha256", manifestDigest.Encoded()), 0o755)
						So(err, ShouldBeNil)
					})
				}
			})

			Convey("Setup multiarch manifest", t, func() {
				// put an unused blob
				content := []byte("unused blob")
				buf := bytes.NewBuffer(content)
				unusedDigest := godigest.FromBytes(content)

				_, _, err := imgStore.FullBlobUpload(repoName, bytes.NewReader(buf.Bytes()), unusedDigest)
				So(err, ShouldBeNil)

				// create a blob/layer
				upload, err := imgStore.NewBlobUpload(repoName)
				So(err, ShouldBeNil)
				So(upload, ShouldNotBeEmpty)

				content = []byte("this is a blob1")
				buf = bytes.NewBuffer(content)
				buflen := buf.Len()
				digest := godigest.FromBytes(content)
				So(digest, ShouldNotBeNil)

				blob, err := imgStore.PutBlobChunkStreamed(repoName, upload, buf)
				So(err, ShouldBeNil)
				So(blob, ShouldEqual, buflen)

				bdgst1 := digest
				bsize1 := len(content)

				err = imgStore.FinishBlobUpload(repoName, upload, buf, digest)
				So(err, ShouldBeNil)
				So(blob, ShouldEqual, buflen)

				var index ispec.Index
				index.SchemaVersion = 2
				index.MediaType = ispec.MediaTypeImageIndex

				var cdigest godigest.Digest

				var cblob []byte

				//nolint: dupl
				for i := 0; i < 4; i++ {
					// upload image config blob
					upload, err = imgStore.NewBlobUpload(repoName)
					So(err, ShouldBeNil)
					So(upload, ShouldNotBeEmpty)

					cblob, cdigest = GetRandomImageConfig()
					buf = bytes.NewBuffer(cblob)
					buflen = buf.Len()
					blob, err = imgStore.PutBlobChunkStreamed(repoName, upload, buf)
					So(err, ShouldBeNil)
					So(blob, ShouldEqual, buflen)

					err = imgStore.FinishBlobUpload(repoName, upload, buf, cdigest)
					So(err, ShouldBeNil)
					So(blob, ShouldEqual, buflen)

					// create a manifest
					manifest := ispec.Manifest{
						Config: ispec.Descriptor{
							MediaType: ispec.MediaTypeImageConfig,
							Digest:    cdigest,
							Size:      int64(len(cblob)),
						},
						Layers: []ispec.Descriptor{
							{
								MediaType: ispec.MediaTypeImageLayer,
								Digest:    bdgst1,
								Size:      int64(bsize1),
							},
						},
					}
					manifest.SchemaVersion = 2
					content, err = json.Marshal(manifest)
					So(err, ShouldBeNil)

					digest = godigest.FromBytes(content)
					So(digest, ShouldNotBeNil)
					_, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content)
					So(err, ShouldBeNil)

					index.Manifests = append(index.Manifests, ispec.Descriptor{
						Digest:    digest,
						MediaType: ispec.MediaTypeImageManifest,
						Size:      int64(len(content)),
					})
				}

				// upload index image
				indexContent, err := json.Marshal(index)
				So(err, ShouldBeNil)

				indexDigest := godigest.FromBytes(indexContent)
				So(indexDigest, ShouldNotBeNil)

				indexManifestDigest, _, err := imgStore.PutImageManifest(repoName, "index", ispec.MediaTypeImageIndex, indexContent)
				So(err, ShouldBeNil)

				Convey("Try to delete manifest being referenced by image index", func() {
					// modifying multi arch images should not be allowed
					err := imgStore.DeleteImageManifest(repoName, digest.String(), false)
					So(err, ShouldEqual, zerr.ErrManifestReferenced)
				})

				Convey("Try to delete blob currently in use", func() {
					// layer blob
					err := imgStore.DeleteBlob("test", bdgst1)
					So(err, ShouldEqual, zerr.ErrBlobReferenced)

					// manifest
					err = imgStore.DeleteBlob("test", digest)
					So(err, ShouldEqual, zerr.ErrBlobReferenced)

					// config
					err = imgStore.DeleteBlob("test", cdigest)
					So(err, ShouldEqual, zerr.ErrBlobReferenced)
				})

				Convey("Delete unused blob", func() {
					err := imgStore.DeleteBlob(repoName, unusedDigest)
					So(err, ShouldBeNil)
				})

				Convey("Delete manifests first, then blob", func() {
					err := imgStore.DeleteImageManifest(repoName, indexManifestDigest.String(), false)
					So(err, ShouldBeNil)

					for _, manifestDesc := range index.Manifests {
						err := imgStore.DeleteImageManifest(repoName, manifestDesc.Digest.String(), false)
						So(err, ShouldBeNil)
					}

					err = imgStore.DeleteBlob(repoName, bdgst1)
					So(err, ShouldBeNil)

					// config
					err = imgStore.DeleteBlob("test", cdigest)
					So(err, ShouldBeNil)
				})

				if testcase.storageType != storageConstants.S3StorageDriverName {
					Convey("repo not found", func() {
						// delete repo
						err := os.RemoveAll(path.Join(imgStore.RootDir(), repoName))
						So(err, ShouldBeNil)

						ok, err := storageCommon.IsBlobReferenced(imgStore, repoName, bdgst1, log)
						So(err, ShouldNotBeNil)
						So(ok, ShouldBeFalse)
					})

					Convey("index.json not found", func() {
						err := os.Remove(path.Join(imgStore.RootDir(), repoName, "index.json"))
						So(err, ShouldBeNil)

						ok, err := storageCommon.IsBlobReferenced(imgStore, repoName, bdgst1, log)
						So(err, ShouldNotBeNil)
						So(ok, ShouldBeFalse)
					})

					Convey("multiarch image not found", func() {
						err := os.Remove(path.Join(imgStore.RootDir(), repoName, "blobs", "sha256", indexManifestDigest.Encoded()))
						So(err, ShouldBeNil)

						ok, err := storageCommon.IsBlobReferenced(imgStore, repoName, unusedDigest, log)
						So(err, ShouldNotBeNil)
						So(ok, ShouldBeFalse)
					})
				}
			})
		})
	}
}

func TestReuploadCorruptedBlob(t *testing.T) {
	for _, testcase := range testCases {
		testcase := testcase
		t.Run(testcase.testCaseName, func(t *testing.T) {
			var (
				imgStore storageTypes.ImageStore
				driver   storageTypes.Driver
			)

			cacheDir := t.TempDir()

			opts := createObjectStoreOpts{
				rootDir:     cacheDir,
				cacheDir:    cacheDir,
				cacheType:   testcase.cacheType,
				storageType: testcase.storageType,
			}

			if testcase.cacheType == storageConstants.RedisDriverName {
				miniRedis := miniredis.RunT(t)
				opts.miniRedisAddr = "redis://" + miniRedis.Addr()
				defer DumpKeys(t, opts.miniRedisAddr)
			}

			if testcase.storageType == storageConstants.S3StorageDriverName {
				tskip.SkipS3(t)

				uuid, err := guuid.NewV4()
				if err != nil {
					panic(err)
				}

				testDir := path.Join("/oci-repo-test", uuid.String())
				opts.rootDir = testDir

				driver, imgStore, _, _ = createObjectsStore(opts)
				defer cleanupStorage(driver, testDir)
			} else {
				driver, imgStore, _, _ = createObjectsStore(opts)
			}

			Convey("Test errors paths", t, func() {
				storeController := storage.StoreController{DefaultStore: imgStore}

				image := CreateRandomImage()

				err := WriteImageToFileSystem(image, repoName, tag, storeController)
				So(err, ShouldBeNil)
			})

			Convey("Test reupload repair corrupted image", t, func() {
				storeController := storage.StoreController{DefaultStore: imgStore}

				image := CreateRandomImage()

				err := WriteImageToFileSystem(image, repoName, tag, storeController)
				So(err, ShouldBeNil)

				blob := image.Layers[0]
				blobDigest := godigest.FromBytes(blob)
				blobSize := len(blob)
				blobPath := imgStore.BlobPath(repoName, blobDigest)

				ok, size, err := imgStore.CheckBlob(repoName, blobDigest)
				So(ok, ShouldBeTrue)
				So(size, ShouldEqual, blobSize)
				So(err, ShouldBeNil)

				_, err = driver.WriteFile(blobPath, []byte("corrupted"))
				So(err, ShouldBeNil)

				ok, size, err = imgStore.CheckBlob(repoName, blobDigest)
				So(ok, ShouldBeFalse)
				So(size, ShouldNotEqual, blobSize)
				So(err, ShouldEqual, zerr.ErrBlobNotFound)

				err = WriteImageToFileSystem(image, repoName, tag, storeController)
				So(err, ShouldBeNil)

				ok, size, _, err = imgStore.StatBlob(repoName, blobDigest)
				So(ok, ShouldBeTrue)
				So(blobSize, ShouldEqual, size)
				So(err, ShouldBeNil)

				ok, size, err = imgStore.CheckBlob(repoName, blobDigest)
				So(ok, ShouldBeTrue)
				So(size, ShouldEqual, blobSize)
				So(err, ShouldBeNil)
			})

			Convey("Test reupload repair corrupted image index", t, func() {
				storeController := storage.StoreController{DefaultStore: imgStore}

				image := CreateRandomMultiarch()

				tag := "index"

				err := WriteMultiArchImageToFileSystem(image, repoName, tag, storeController)
				So(err, ShouldBeNil)

				blob := image.Images[0].Layers[0]
				blobDigest := godigest.FromBytes(blob)
				blobSize := len(blob)
				blobPath := imgStore.BlobPath(repoName, blobDigest)

				ok, size, err := imgStore.CheckBlob(repoName, blobDigest)
				So(ok, ShouldBeTrue)
				So(size, ShouldEqual, blobSize)
				So(err, ShouldBeNil)

				_, err = driver.WriteFile(blobPath, []byte("corrupted"))
				So(err, ShouldBeNil)

				ok, size, err = imgStore.CheckBlob(repoName, blobDigest)
				So(ok, ShouldBeFalse)
				So(size, ShouldNotEqual, blobSize)
				So(err, ShouldEqual, zerr.ErrBlobNotFound)

				err = WriteMultiArchImageToFileSystem(image, repoName, tag, storeController)
				So(err, ShouldBeNil)

				ok, size, _, err = imgStore.StatBlob(repoName, blobDigest)
				So(ok, ShouldBeTrue)
				So(blobSize, ShouldEqual, size)
				So(err, ShouldBeNil)

				ok, size, err = imgStore.CheckBlob(repoName, blobDigest)
				So(ok, ShouldBeTrue)
				So(size, ShouldEqual, blobSize)
				So(err, ShouldBeNil)
			})
		})
	}
}

func TestStorageHandler(t *testing.T) {
	for _, testcase := range testCases {
		testcase := testcase
		t.Run(testcase.testCaseName, func(t *testing.T) {
			var (
				firstStore    storageTypes.ImageStore
				secondStore   storageTypes.ImageStore
				thirdStore    storageTypes.ImageStore
				firstRootDir  string
				secondRootDir string
				thirdRootDir  string
			)

			opts := createObjectStoreOpts{
				cacheType:   testcase.cacheType,
				storageType: testcase.storageType,
			}

			if testcase.cacheType == storageConstants.RedisDriverName {
				miniRedis := miniredis.RunT(t)
				opts.miniRedisAddr = "redis://" + miniRedis.Addr()
				defer DumpKeys(t, opts.miniRedisAddr)
			}

			if testcase.storageType == storageConstants.S3StorageDriverName {
				tskip.SkipS3(t)

				var (
					firstStorageDriver  storageTypes.Driver
					secondStorageDriver storageTypes.Driver
					thirdStorageDriver  storageTypes.Driver
				)

				firstRootDir = "/util_test1"
				opts.rootDir = firstRootDir
				opts.cacheDir = t.TempDir()

				firstStorageDriver, firstStore, _, _ = createObjectsStore(opts)
				defer cleanupStorage(firstStorageDriver, firstRootDir)

				secondRootDir = "/util_test2"
				opts.rootDir = secondRootDir
				opts.cacheDir = t.TempDir()

				secondStorageDriver, secondStore, _, _ = createObjectsStore(opts)
				defer cleanupStorage(secondStorageDriver, secondRootDir)

				thirdRootDir = "/util_test3"
				opts.rootDir = thirdRootDir
				opts.cacheDir = t.TempDir()

				thirdStorageDriver, thirdStore, _, _ = createObjectsStore(opts)
				defer cleanupStorage(thirdStorageDriver, thirdRootDir)
			} else {
				firstRootDir = t.TempDir()
				opts.rootDir = firstRootDir
				opts.cacheDir = firstRootDir

				_, firstStore, _, _ = createObjectsStore(opts)

				secondRootDir = t.TempDir()
				opts.rootDir = secondRootDir
				opts.cacheDir = secondRootDir

				_, secondStore, _, _ = createObjectsStore(opts)

				thirdRootDir = t.TempDir()
				opts.rootDir = thirdRootDir
				opts.cacheDir = thirdRootDir

				_, thirdStore, _, _ = createObjectsStore(opts)
			}

			Convey("Test storage handler", t, func() {
				storeController := storage.StoreController{}

				storeController.DefaultStore = firstStore

				subStore := make(map[string]storageTypes.ImageStore)

				subStore["/a"] = secondStore
				subStore["/b"] = thirdStore

				storeController.SubStore = subStore

				imgStore := storeController.GetImageStore("zot-x-test")
				So(imgStore.RootDir(), ShouldEqual, firstRootDir)

				imgStore = storeController.GetImageStore("a/zot-a-test")
				So(imgStore.RootDir(), ShouldEqual, secondRootDir)

				imgStore = storeController.GetImageStore("b/zot-b-test")
				So(imgStore.RootDir(), ShouldEqual, thirdRootDir)

				imgStore = storeController.GetImageStore("c/zot-c-test")
				So(imgStore.RootDir(), ShouldEqual, firstRootDir)
			})
		})
	}
}

func TestRoutePrefix(t *testing.T) {
	Convey("Test route prefix", t, func() {
		routePrefix := storage.GetRoutePrefix("test:latest")
		So(routePrefix, ShouldEqual, "/")

		routePrefix = storage.GetRoutePrefix("a/test:latest")
		So(routePrefix, ShouldEqual, "/a")

		routePrefix = storage.GetRoutePrefix("a/b/test:latest")
		So(routePrefix, ShouldEqual, "/a")
	})
}

func TestGarbageCollectImageManifest(t *testing.T) {
	for _, testcase := range testCases {
		testcase := testcase
		t.Run(testcase.testCaseName, func(t *testing.T) {
			log := zlog.NewLogger("debug", "")
			audit := zlog.NewAuditLogger("debug", "")

			ctx := context.Background()

			//nolint: contextcheck
			Convey("Repo layout", t, func(c C) {
				cacheDir := t.TempDir()

				opts := createObjectStoreOpts{
					rootDir:     cacheDir,
					cacheDir:    cacheDir,
					cacheType:   testcase.cacheType,
					storageType: testcase.storageType,
				}

				if testcase.cacheType == storageConstants.RedisDriverName {
					miniRedis := miniredis.RunT(t)
					opts.miniRedisAddr = "redis://" + miniRedis.Addr()
					defer DumpKeys(t, opts.miniRedisAddr)
				}

				Convey("Garbage collect with default/long delay", func() {
					var imgStore storageTypes.ImageStore

					if testcase.storageType == storageConstants.S3StorageDriverName {
						tskip.SkipS3(t)

						uuid, err := guuid.NewV4()
						if err != nil {
							panic(err)
						}

						testDir := path.Join("/oci-repo-test", uuid.String())
						opts.rootDir = testDir

						var store storageTypes.Driver
						store, imgStore, _, _ = createObjectsStore(opts)
						defer cleanupStorage(store, testDir)
					} else {
						_, imgStore, _, _ = createObjectsStore(opts)
					}

					gc := gc.NewGarbageCollect(imgStore, mocks.MetaDBMock{}, gc.Options{
						Delay: storageConstants.DefaultGCDelay,
						ImageRetention: config.ImageRetention{
							Delay: storageConstants.DefaultRetentionDelay,
							Policies: []config.RetentionPolicy{
								{
									Repositories:    []string{"**"},
									DeleteReferrers: true,
								},
							},
						},
					}, audit, log)

					repoName := "gc-long"

					upload, err := imgStore.NewBlobUpload(repoName)
					So(err, ShouldBeNil)
					So(upload, ShouldNotBeEmpty)

					content := []byte("test-data1")
					buf := bytes.NewBuffer(content)
					buflen := buf.Len()
					bdigest := godigest.FromBytes(content)

					blob, err := imgStore.PutBlobChunk(repoName, upload, 0, int64(buflen), buf)
					So(err, ShouldBeNil)
					So(blob, ShouldEqual, buflen)

					err = imgStore.FinishBlobUpload(repoName, upload, buf, bdigest)
					So(err, ShouldBeNil)

					annotationsMap := make(map[string]string)
					annotationsMap[ispec.AnnotationRefName] = tag

					cblob, cdigest := GetRandomImageConfig()
					_, clen, err := imgStore.FullBlobUpload(repoName, bytes.NewReader(cblob), cdigest)
					So(err, ShouldBeNil)
					So(clen, ShouldEqual, len(cblob))

					hasBlob, _, err := imgStore.CheckBlob(repoName, cdigest)
					So(err, ShouldBeNil)
					So(hasBlob, ShouldEqual, true)

					manifest := ispec.Manifest{
						Config: ispec.Descriptor{
							MediaType: "application/vnd.oci.image.config.v1+json",
							Digest:    cdigest,
							Size:      int64(len(cblob)),
						},
						Layers: []ispec.Descriptor{
							{
								MediaType: "application/vnd.oci.image.layer.v1.tar",
								Digest:    bdigest,
								Size:      int64(buflen),
							},
						},
						Annotations: annotationsMap,
					}

					manifest.SchemaVersion = 2
					manifestBuf, err := json.Marshal(manifest)
					So(err, ShouldBeNil)

					digest := godigest.FromBytes(manifestBuf)

					_, _, err = imgStore.PutImageManifest(repoName, tag, ispec.MediaTypeImageManifest, manifestBuf)
					So(err, ShouldBeNil)

					err = gc.CleanRepo(ctx, repoName)
					So(err, ShouldBeNil)

					// put artifact referencing above image
					artifactBlob := []byte("artifact")
					artifactBlobDigest := godigest.FromBytes(artifactBlob)

					// push layer
					_, _, err = imgStore.FullBlobUpload(repoName, bytes.NewReader(artifactBlob), artifactBlobDigest)
					So(err, ShouldBeNil)

					// push config
					_, _, err = imgStore.FullBlobUpload(repoName, bytes.NewReader(ispec.DescriptorEmptyJSON.Data),
						ispec.DescriptorEmptyJSON.Digest)
					So(err, ShouldBeNil)

					artifactManifest := ispec.Manifest{
						MediaType: ispec.MediaTypeImageManifest,
						Layers: []ispec.Descriptor{
							{
								MediaType: "application/octet-stream",
								Digest:    artifactBlobDigest,
								Size:      int64(len(artifactBlob)),
							},
						},
						Config: ispec.DescriptorEmptyJSON,
						Subject: &ispec.Descriptor{
							MediaType: "application/vnd.oci.image.manifest.v1+json",
							Digest:    digest,
							Size:      int64(len(manifestBuf)),
						},
					}
					artifactManifest.SchemaVersion = 2

					artifactManifestBuf, err := json.Marshal(artifactManifest)
					So(err, ShouldBeNil)

					artifactDigest := godigest.FromBytes(artifactManifestBuf)

					// push artifact manifest
					_, _, err = imgStore.PutImageManifest(repoName, artifactDigest.String(),
						ispec.MediaTypeImageManifest, artifactManifestBuf)
					So(err, ShouldBeNil)

					err = gc.CleanRepo(ctx, repoName)
					So(err, ShouldBeNil)

					hasBlob, _, err = imgStore.CheckBlob(repoName, bdigest)
					So(err, ShouldBeNil)
					So(hasBlob, ShouldEqual, true)

					hasBlob, _, err = imgStore.CheckBlob(repoName, artifactBlobDigest)
					So(err, ShouldBeNil)
					So(hasBlob, ShouldEqual, true)

					err = imgStore.DeleteImageManifest(repoName, digest.String(), false)
					So(err, ShouldBeNil)

					err = gc.CleanRepo(ctx, repoName)
					So(err, ShouldBeNil)

					hasBlob, _, err = imgStore.CheckBlob(repoName, bdigest)
					So(err, ShouldBeNil)
					So(hasBlob, ShouldEqual, true)

					hasBlob, _, err = imgStore.CheckBlob(repoName, artifactBlobDigest)
					So(err, ShouldBeNil)
					So(hasBlob, ShouldEqual, true)
				})

				Convey("Garbage collect with short delay", func() {
					var imgStore storageTypes.ImageStore

					gcDelay := 1 * time.Second

					if testcase.storageType == storageConstants.S3StorageDriverName {
						tskip.SkipS3(t)

						uuid, err := guuid.NewV4()
						if err != nil {
							panic(err)
						}

						testDir := path.Join("/oci-repo-test", uuid.String())
						opts.rootDir = testDir

						var store storageTypes.Driver
						store, imgStore, _, _ = createObjectsStore(opts)
						defer cleanupStorage(store, testDir)
					} else {
						_, imgStore, _, _ = createObjectsStore(opts)
					}

					gc := gc.NewGarbageCollect(imgStore, mocks.MetaDBMock{}, gc.Options{
						Delay: gcDelay,
						ImageRetention: config.ImageRetention{ //nolint: gochecknoglobals
							Delay: gcDelay,
							Policies: []config.RetentionPolicy{
								{
									Repositories:    []string{"**"},
									DeleteReferrers: true,
									DeleteUntagged:  &trueVal,
								},
							},
						},
					}, audit, log)

					// upload orphan blob
					upload, err := imgStore.NewBlobUpload(repoName)
					So(err, ShouldBeNil)
					So(upload, ShouldNotBeEmpty)

					content := []byte("test-data1")
					buf := bytes.NewBuffer(content)
					buflen := buf.Len()
					odigest := godigest.FromBytes(content)

					blob, err := imgStore.PutBlobChunk(repoName, upload, 0, int64(buflen), buf)
					So(err, ShouldBeNil)
					So(blob, ShouldEqual, buflen)

					err = imgStore.FinishBlobUpload(repoName, upload, buf, odigest)
					So(err, ShouldBeNil)

					// sleep so orphan blob can be GC'ed
					time.Sleep(1 * time.Second)

					// upload blob
					upload, err = imgStore.NewBlobUpload(repoName)
					So(err, ShouldBeNil)
					So(upload, ShouldNotBeEmpty)

					content = []byte("test-data2")
					buf = bytes.NewBuffer(content)
					buflen = buf.Len()
					bdigest := godigest.FromBytes(content)

					blob, err = imgStore.PutBlobChunk(repoName, upload, 0, int64(buflen), buf)
					So(err, ShouldBeNil)
					So(blob, ShouldEqual, buflen)

					err = imgStore.FinishBlobUpload(repoName, upload, buf, bdigest)
					So(err, ShouldBeNil)

					annotationsMap := make(map[string]string)
					annotationsMap[ispec.AnnotationRefName] = tag

					cblob, cdigest := GetRandomImageConfig()
					_, clen, err := imgStore.FullBlobUpload(repoName, bytes.NewReader(cblob), cdigest)
					So(err, ShouldBeNil)
					So(clen, ShouldEqual, len(cblob))

					hasBlob, _, err := imgStore.CheckBlob(repoName, cdigest)
					So(err, ShouldBeNil)
					So(hasBlob, ShouldEqual, true)

					manifest := ispec.Manifest{
						Config: ispec.Descriptor{
							MediaType: "application/vnd.oci.image.config.v1+json",
							Digest:    cdigest,
							Size:      int64(len(cblob)),
						},
						Layers: []ispec.Descriptor{
							{
								MediaType: "application/vnd.oci.image.layer.v1.tar",
								Digest:    bdigest,
								Size:      int64(buflen),
							},
						},
						Annotations: annotationsMap,
					}

					manifest.SchemaVersion = 2
					manifestBuf, err := json.Marshal(manifest)
					So(err, ShouldBeNil)

					digest := godigest.FromBytes(manifestBuf)

					_, _, err = imgStore.PutImageManifest(repoName, tag, ispec.MediaTypeImageManifest, manifestBuf)
					So(err, ShouldBeNil)

					// put artifact referencing above image
					artifactBlob := []byte("artifact")
					artifactBlobDigest := godigest.FromBytes(artifactBlob)

					// push layer
					_, _, err = imgStore.FullBlobUpload(repoName, bytes.NewReader(artifactBlob), artifactBlobDigest)
					So(err, ShouldBeNil)

					// push config
					_, _, err = imgStore.FullBlobUpload(repoName, bytes.NewReader(ispec.DescriptorEmptyJSON.Data),
						ispec.DescriptorEmptyJSON.Digest)
					So(err, ShouldBeNil)

					artifactManifest := ispec.Manifest{
						MediaType: ispec.MediaTypeImageManifest,
						Layers: []ispec.Descriptor{
							{
								MediaType: "application/octet-stream",
								Digest:    artifactBlobDigest,
								Size:      int64(len(artifactBlob)),
							},
						},
						Config: ispec.DescriptorEmptyJSON,
						Subject: &ispec.Descriptor{
							MediaType: ispec.MediaTypeImageManifest,
							Digest:    digest,
							Size:      int64(len(manifestBuf)),
						},
					}
					artifactManifest.SchemaVersion = 2

					artifactManifestBuf, err := json.Marshal(artifactManifest)
					So(err, ShouldBeNil)

					artifactDigest := godigest.FromBytes(artifactManifestBuf)

					// push artifact manifest
					_, _, err = imgStore.PutImageManifest(repoName, artifactDigest.String(),
						ispec.MediaTypeImageManifest, artifactManifestBuf)
					So(err, ShouldBeNil)

					// push artifact manifest pointing to artifact above
					artifactManifest.Subject = &ispec.Descriptor{
						MediaType: ispec.MediaTypeImageManifest,
						Digest:    artifactDigest,
						Size:      int64(len(artifactManifestBuf)),
					}
					artifactManifest.ArtifactType = "application/forArtifact" //nolint: goconst

					artifactManifestBuf, err = json.Marshal(artifactManifest)
					So(err, ShouldBeNil)

					artifactOfArtifactManifestDigest := godigest.FromBytes(artifactManifestBuf)
					_, _, err = imgStore.PutImageManifest(repoName, artifactOfArtifactManifestDigest.String(),
						ispec.MediaTypeImageManifest, artifactManifestBuf)
					So(err, ShouldBeNil)

					// push orphan artifact (missing subject)
					artifactManifest.Subject = &ispec.Descriptor{
						MediaType: ispec.MediaTypeImageManifest,
						Digest:    godigest.FromBytes([]byte("miss")),
						Size:      int64(30),
					}
					artifactManifest.ArtifactType = "application/orphan" //nolint: goconst

					artifactManifestBuf, err = json.Marshal(artifactManifest)
					So(err, ShouldBeNil)

					orphanArtifactManifestDigest := godigest.FromBytes(artifactManifestBuf)

					// push orphan artifact manifest
					_, _, err = imgStore.PutImageManifest(repoName, orphanArtifactManifestDigest.String(),
						ispec.MediaTypeImageManifest, artifactManifestBuf)
					So(err, ShouldBeNil)

					err = gc.CleanRepo(ctx, repoName)
					So(err, ShouldBeNil)

					hasBlob, _, err = imgStore.CheckBlob(repoName, odigest)
					So(err, ShouldNotBeNil)
					So(hasBlob, ShouldEqual, false)

					hasBlob, _, _, err = imgStore.StatBlob(repoName, odigest)
					So(err, ShouldNotBeNil)
					So(hasBlob, ShouldEqual, false)

					hasBlob, _, err = imgStore.CheckBlob(repoName, bdigest)
					So(err, ShouldBeNil)
					So(hasBlob, ShouldEqual, true)

					hasBlob, _, _, err = imgStore.StatBlob(repoName, bdigest)
					So(err, ShouldBeNil)
					So(hasBlob, ShouldEqual, true)

					// sleep so orphan blob can be GC'ed
					time.Sleep(1 * time.Second)

					Convey("Garbage collect blobs after manifest is removed", func() {
						err = imgStore.DeleteImageManifest(repoName, digest.String(), false)
						So(err, ShouldBeNil)

						err = gc.CleanRepo(ctx, repoName)
						So(err, ShouldBeNil)

						hasBlob, _, err = imgStore.CheckBlob(repoName, bdigest)
						So(err, ShouldNotBeNil)
						So(hasBlob, ShouldEqual, false)

						hasBlob, _, err = imgStore.CheckBlob(repoName, artifactBlobDigest)
						So(err, ShouldNotBeNil)
						So(hasBlob, ShouldEqual, false)

						// check artifacts are gc'ed
						_, _, _, err := imgStore.GetImageManifest(repoName, artifactDigest.String())
						So(err, ShouldNotBeNil)

						_, _, _, err = imgStore.GetImageManifest(repoName, artifactOfArtifactManifestDigest.String())
						So(err, ShouldNotBeNil)

						_, _, _, err = imgStore.GetImageManifest(repoName, orphanArtifactManifestDigest.String())
						So(err, ShouldNotBeNil)

						// check it gc'ed repo
						exists := imgStore.DirExists(path.Join(imgStore.RootDir(), repoName))
						So(exists, ShouldBeFalse)
					})

					Convey("Garbage collect - don't gc manifests/blobs which are referenced by another image", func() {
						// upload same image with another tag
						_, _, err = imgStore.PutImageManifest(repoName, "2.0", ispec.MediaTypeImageManifest, manifestBuf)
						So(err, ShouldBeNil)

						err = imgStore.DeleteImageManifest(repoName, tag, false)
						So(err, ShouldBeNil)

						err = gc.CleanRepo(ctx, repoName)
						So(err, ShouldBeNil)

						hasBlob, _, err = imgStore.CheckBlob(repoName, bdigest)
						So(err, ShouldBeNil)
						So(hasBlob, ShouldEqual, true)

						hasBlob, _, err = imgStore.CheckBlob(repoName, digest)
						So(err, ShouldBeNil)
						So(hasBlob, ShouldEqual, true)

						hasBlob, _, err = imgStore.CheckBlob(repoName, artifactBlobDigest)
						So(err, ShouldBeNil)
						So(hasBlob, ShouldEqual, true)

						// orphan artifact should be deleted
						_, _, _, err = imgStore.GetImageManifest(repoName, orphanArtifactManifestDigest.String())
						So(err, ShouldNotBeNil)

						// check artifacts manifests
						_, _, _, err := imgStore.GetImageManifest(repoName, artifactDigest.String())
						So(err, ShouldBeNil)

						_, _, _, err = imgStore.GetImageManifest(repoName, artifactOfArtifactManifestDigest.String())
						So(err, ShouldBeNil)
					})
				})

				Convey("Garbage collect with dedupe", func() {
					// garbage-collect is repo-local and dedupe is global and they can interact in strange ways
					var imgStore storageTypes.ImageStore

					gcDelay := 3 * time.Second

					if testcase.storageType == storageConstants.S3StorageDriverName {
						tskip.SkipS3(t)

						uuid, err := guuid.NewV4()
						if err != nil {
							panic(err)
						}

						testDir := path.Join("/oci-repo-test", uuid.String())
						opts.rootDir = testDir

						var store storageTypes.Driver
						store, imgStore, _, _ = createObjectsStore(opts)
						defer cleanupStorage(store, testDir)
					} else {
						_, imgStore, _, _ = createObjectsStore(opts)
					}

					gc := gc.NewGarbageCollect(imgStore, mocks.MetaDBMock{}, gc.Options{
						Delay:          gcDelay,
						ImageRetention: DeleteReferrers,
					}, audit, log)

					// first upload an image to the first repo and wait for GC timeout

					repo1Name := "gc1"

					// upload blob
					upload, err := imgStore.NewBlobUpload(repo1Name)
					So(err, ShouldBeNil)
					So(upload, ShouldNotBeEmpty)

					content := []byte("test-data")
					buf := bytes.NewBuffer(content)
					buflen := buf.Len()
					bdigest := godigest.FromBytes(content)
					tdigest := bdigest

					blob, err := imgStore.PutBlobChunk(repo1Name, upload, 0, int64(buflen), buf)
					So(err, ShouldBeNil)
					So(blob, ShouldEqual, buflen)

					err = imgStore.FinishBlobUpload(repo1Name, upload, buf, bdigest)
					So(err, ShouldBeNil)

					annotationsMap := make(map[string]string)
					annotationsMap[ispec.AnnotationRefName] = tag

					cblob, cdigest := GetRandomImageConfig()
					_, clen, err := imgStore.FullBlobUpload(repo1Name, bytes.NewReader(cblob), cdigest)
					So(err, ShouldBeNil)
					So(clen, ShouldEqual, len(cblob))

					hasBlob, _, err := imgStore.CheckBlob(repo1Name, cdigest)
					So(err, ShouldBeNil)
					So(hasBlob, ShouldEqual, true)

					manifest := ispec.Manifest{
						Config: ispec.Descriptor{
							MediaType: "application/vnd.oci.image.config.v1+json",
							Digest:    cdigest,
							Size:      int64(len(cblob)),
						},
						Layers: []ispec.Descriptor{
							{
								MediaType: "application/vnd.oci.image.layer.v1.tar",
								Digest:    bdigest,
								Size:      int64(buflen),
							},
						},
						Annotations: annotationsMap,
					}

					manifest.SchemaVersion = 2
					manifestBuf, err := json.Marshal(manifest)
					So(err, ShouldBeNil)

					_, _, err = imgStore.PutImageManifest(repo1Name, tag, ispec.MediaTypeImageManifest, manifestBuf)
					So(err, ShouldBeNil)

					// sleep so past GC timeout
					time.Sleep(3 * time.Second)

					hasBlob, _, err = imgStore.CheckBlob(repo1Name, tdigest)
					So(err, ShouldBeNil)
					So(hasBlob, ShouldEqual, true)

					hasBlob, _, err = imgStore.CheckBlob(repo1Name, tdigest)
					So(err, ShouldBeNil)
					So(hasBlob, ShouldEqual, true)

					// upload another image into a second repo with the same blob contents so dedupe is triggered

					repo2Name := "gc2"

					upload, err = imgStore.NewBlobUpload(repo2Name)
					So(err, ShouldBeNil)
					So(upload, ShouldNotBeEmpty)

					buf = bytes.NewBuffer(content)
					buflen = buf.Len()

					blob, err = imgStore.PutBlobChunk(repo2Name, upload, 0, int64(buflen), buf)
					So(err, ShouldBeNil)
					So(blob, ShouldEqual, buflen)

					err = imgStore.FinishBlobUpload(repo2Name, upload, buf, bdigest)
					So(err, ShouldBeNil)

					annotationsMap = make(map[string]string)
					annotationsMap[ispec.AnnotationRefName] = tag

					cblob, cdigest = GetRandomImageConfig()
					_, clen, err = imgStore.FullBlobUpload(repo2Name, bytes.NewReader(cblob), cdigest)
					So(err, ShouldBeNil)
					So(clen, ShouldEqual, len(cblob))

					hasBlob, _, err = imgStore.CheckBlob(repo2Name, cdigest)
					So(err, ShouldBeNil)
					So(hasBlob, ShouldEqual, true)

					manifest = ispec.Manifest{
						Config: ispec.Descriptor{
							MediaType: "application/vnd.oci.image.config.v1+json",
							Digest:    cdigest,
							Size:      int64(len(cblob)),
						},
						Layers: []ispec.Descriptor{
							{
								MediaType: "application/vnd.oci.image.layer.v1.tar",
								Digest:    bdigest,
								Size:      int64(buflen),
							},
						},
						Annotations: annotationsMap,
					}

					manifest.SchemaVersion = 2
					manifestBuf, err = json.Marshal(manifest)
					So(err, ShouldBeNil)

					_, _, err = imgStore.PutImageManifest(repo2Name, tag, ispec.MediaTypeImageManifest, manifestBuf)
					So(err, ShouldBeNil)

					hasBlob, _, err = imgStore.CheckBlob(repo2Name, bdigest)
					So(err, ShouldBeNil)
					So(hasBlob, ShouldEqual, true)

					// immediately upload any other image to second repo which should invoke GC inline, but expect layers to persist

					upload, err = imgStore.NewBlobUpload(repo2Name)
					So(err, ShouldBeNil)
					So(upload, ShouldNotBeEmpty)

					content = []byte("test-data-more")
					buf = bytes.NewBuffer(content)
					buflen = buf.Len()
					bdigest = godigest.FromBytes(content)

					blob, err = imgStore.PutBlobChunk(repo2Name, upload, 0, int64(buflen), buf)
					So(err, ShouldBeNil)
					So(blob, ShouldEqual, buflen)

					err = imgStore.FinishBlobUpload(repo2Name, upload, buf, bdigest)
					So(err, ShouldBeNil)

					annotationsMap = make(map[string]string)
					annotationsMap[ispec.AnnotationRefName] = tag

					cblob, cdigest = GetRandomImageConfig()
					_, clen, err = imgStore.FullBlobUpload(repo2Name, bytes.NewReader(cblob), cdigest)
					So(err, ShouldBeNil)
					So(clen, ShouldEqual, len(cblob))

					hasBlob, _, err = imgStore.CheckBlob(repo2Name, cdigest)
					So(err, ShouldBeNil)
					So(hasBlob, ShouldEqual, true)

					manifest = ispec.Manifest{
						Config: ispec.Descriptor{
							MediaType: "application/vnd.oci.image.config.v1+json",
							Digest:    cdigest,
							Size:      int64(len(cblob)),
						},
						Layers: []ispec.Descriptor{
							{
								MediaType: "application/vnd.oci.image.layer.v1.tar",
								Digest:    bdigest,
								Size:      int64(buflen),
							},
						},
						Annotations: annotationsMap,
					}

					manifest.SchemaVersion = 2
					manifestBuf, err = json.Marshal(manifest)
					So(err, ShouldBeNil)

					digest := godigest.FromBytes(manifestBuf)

					_, _, err = imgStore.PutImageManifest(repo2Name, tag, ispec.MediaTypeImageManifest, manifestBuf)
					So(err, ShouldBeNil)

					err = gc.CleanRepo(ctx, repo2Name)
					So(err, ShouldBeNil)

					// original blob should exist
					hasBlob, _, err = imgStore.CheckBlob(repo2Name, tdigest)
					So(err, ShouldBeNil)
					So(hasBlob, ShouldEqual, true)

					_, _, _, err = imgStore.GetImageManifest(repo2Name, digest.String())
					So(err, ShouldBeNil)
				})
			})
		})
	}
}

func TestGarbageCollectImageIndex(t *testing.T) {
	for _, testcase := range testCases {
		testcase := testcase
		t.Run(testcase.testCaseName, func(t *testing.T) {
			log := zlog.NewLogger("debug", "")
			audit := zlog.NewAuditLogger("debug", "")

			ctx := context.Background()

			//nolint: contextcheck
			Convey("Repo layout", t, func(c C) {
				cacheDir := t.TempDir()

				opts := createObjectStoreOpts{
					rootDir:     cacheDir,
					cacheDir:    cacheDir,
					cacheType:   testcase.cacheType,
					storageType: testcase.storageType,
				}

				if testcase.cacheType == storageConstants.RedisDriverName {
					miniRedis := miniredis.RunT(t)
					opts.miniRedisAddr = "redis://" + miniRedis.Addr()
					defer DumpKeys(t, opts.miniRedisAddr)
				}

				Convey("Garbage collect with default/long delay", func() {
					var imgStore storageTypes.ImageStore

					if testcase.storageType == storageConstants.S3StorageDriverName {
						tskip.SkipS3(t)

						uuid, err := guuid.NewV4()
						if err != nil {
							panic(err)
						}

						testDir := path.Join("/oci-repo-test", uuid.String())
						opts.rootDir = testDir

						var store storageTypes.Driver
						store, imgStore, _, _ = createObjectsStore(opts)
						defer cleanupStorage(store, testDir)
					} else {
						_, imgStore, _, _ = createObjectsStore(opts)
					}

					gc := gc.NewGarbageCollect(imgStore, mocks.MetaDBMock{}, gc.Options{
						Delay:          storageConstants.DefaultGCDelay,
						ImageRetention: DeleteReferrers,
					}, audit, log)

					repoName := "gc-long"

					bdgst, digest, indexDigest, indexSize := pushRandomImageIndex(imgStore, repoName)

					// put artifact referencing above image
					artifactBlob := []byte("artifact")
					artifactBlobDigest := godigest.FromBytes(artifactBlob)

					// push layer
					_, _, err := imgStore.FullBlobUpload(repoName, bytes.NewReader(artifactBlob), artifactBlobDigest)
					So(err, ShouldBeNil)

					// push config
					_, _, err = imgStore.FullBlobUpload(repoName, bytes.NewReader(ispec.DescriptorEmptyJSON.Data),
						ispec.DescriptorEmptyJSON.Digest)
					So(err, ShouldBeNil)

					artifactManifest := ispec.Manifest{
						MediaType: ispec.MediaTypeImageManifest,
						Layers: []ispec.Descriptor{
							{
								MediaType: "application/octet-stream",
								Digest:    artifactBlobDigest,
								Size:      int64(len(artifactBlob)),
							},
						},
						Config: ispec.DescriptorEmptyJSON,
						Subject: &ispec.Descriptor{
							MediaType: ispec.MediaTypeImageIndex,
							Digest:    indexDigest,
							Size:      indexSize,
						},
					}
					artifactManifest.SchemaVersion = 2

					artifactManifestBuf, err := json.Marshal(artifactManifest)
					So(err, ShouldBeNil)

					artifactDigest := godigest.FromBytes(artifactManifestBuf)

					// push artifact manifest referencing index image
					_, _, err = imgStore.PutImageManifest(repoName, artifactDigest.String(),
						ispec.MediaTypeImageManifest, artifactManifestBuf)
					So(err, ShouldBeNil)

					artifactManifest.Subject = &ispec.Descriptor{
						MediaType: ispec.MediaTypeImageManifest,
						Digest:    digest,
						Size:      indexSize,
					}

					artifactManifestBuf, err = json.Marshal(artifactManifest)
					So(err, ShouldBeNil)

					artifactManifestDigest := godigest.FromBytes(artifactManifestBuf)

					// push artifact manifest referencing a manifest from index image
					_, _, err = imgStore.PutImageManifest(repoName, artifactManifestDigest.String(),
						ispec.MediaTypeImageManifest, artifactManifestBuf)
					So(err, ShouldBeNil)

					err = gc.CleanRepo(ctx, repoName)
					So(err, ShouldBeNil)

					hasBlob, _, err := imgStore.CheckBlob(repoName, bdgst)
					So(err, ShouldBeNil)
					So(hasBlob, ShouldEqual, true)

					Convey("delete index manifest, layers should be persisted", func() {
						err = imgStore.DeleteImageManifest(repoName, indexDigest.String(), false)
						So(err, ShouldBeNil)

						err = gc.CleanRepo(ctx, repoName)
						So(err, ShouldBeNil)

						hasBlob, _, err = imgStore.CheckBlob(repoName, bdgst)
						So(err, ShouldBeNil)
						So(hasBlob, ShouldEqual, true)

						// check last manifest from index image
						hasBlob, _, err = imgStore.CheckBlob(repoName, digest)
						So(err, ShouldBeNil)
						So(hasBlob, ShouldEqual, true)

						hasBlob, _, err = imgStore.CheckBlob(repoName, artifactBlobDigest)
						So(err, ShouldBeNil)
						So(hasBlob, ShouldEqual, true)
					})
				})

				Convey("Garbage collect with short delay", func() {
					var imgStore storageTypes.ImageStore

					gcDelay := 2 * time.Second
					imageRetentionDelay := 2 * time.Second

					if testcase.storageType == storageConstants.S3StorageDriverName {
						tskip.SkipS3(t)

						uuid, err := guuid.NewV4()
						if err != nil {
							panic(err)
						}

						testDir := path.Join("/oci-repo-test", uuid.String())
						opts.rootDir = testDir

						var store storageTypes.Driver
						store, imgStore, _, _ = createObjectsStore(opts)
						defer cleanupStorage(store, testDir)
					} else {
						_, imgStore, _, _ = createObjectsStore(opts)
					}

					gc := gc.NewGarbageCollect(imgStore, mocks.MetaDBMock{}, gc.Options{
						Delay: gcDelay,
						ImageRetention: config.ImageRetention{ //nolint: gochecknoglobals
							Delay: imageRetentionDelay,
							Policies: []config.RetentionPolicy{
								{
									Repositories:    []string{"**"},
									DeleteReferrers: true,
									DeleteUntagged:  &trueVal,
								},
							},
						},
					}, audit, log)

					// upload orphan blob
					upload, err := imgStore.NewBlobUpload(repoName)
					So(err, ShouldBeNil)
					So(upload, ShouldNotBeEmpty)

					content := []byte("test-data1")
					buf := bytes.NewBuffer(content)
					buflen := buf.Len()
					odigest := godigest.FromBytes(content)

					blob, err := imgStore.PutBlobChunk(repoName, upload, 0, int64(buflen), buf)
					So(err, ShouldBeNil)
					So(blob, ShouldEqual, buflen)

					err = imgStore.FinishBlobUpload(repoName, upload, buf, odigest)
					So(err, ShouldBeNil)

					bdgst, digest, indexDigest, indexSize := pushRandomImageIndex(imgStore, repoName)

					// put artifact referencing above image
					artifactBlob := []byte("artifact")
					artifactBlobDigest := godigest.FromBytes(artifactBlob)

					// push layer
					_, _, err = imgStore.FullBlobUpload(repoName, bytes.NewReader(artifactBlob), artifactBlobDigest)
					So(err, ShouldBeNil)

					// push config
					_, _, err = imgStore.FullBlobUpload(repoName, bytes.NewReader(ispec.DescriptorEmptyJSON.Data),
						ispec.DescriptorEmptyJSON.Digest)
					So(err, ShouldBeNil)

					// push artifact manifest pointing to index
					artifactManifest := ispec.Manifest{
						MediaType: ispec.MediaTypeImageManifest,
						Layers: []ispec.Descriptor{
							{
								MediaType: "application/octet-stream",
								Digest:    artifactBlobDigest,
								Size:      int64(len(artifactBlob)),
							},
						},
						Config: ispec.DescriptorEmptyJSON,
						Subject: &ispec.Descriptor{
							MediaType: ispec.MediaTypeImageIndex,
							Digest:    indexDigest,
							Size:      indexSize,
						},
						ArtifactType: "application/forIndex",
					}
					artifactManifest.SchemaVersion = 2

					artifactManifestBuf, err := json.Marshal(artifactManifest)
					So(err, ShouldBeNil)

					artifactDigest := godigest.FromBytes(artifactManifestBuf)

					// push artifact manifest
					_, _, err = imgStore.PutImageManifest(repoName, artifactDigest.String(),
						ispec.MediaTypeImageManifest, artifactManifestBuf)
					So(err, ShouldBeNil)

					artifactManifest.Subject = &ispec.Descriptor{
						MediaType: ispec.MediaTypeImageManifest,
						Digest:    digest,
						Size:      int64(len(content)),
					}
					artifactManifest.ArtifactType = "application/forManifestInIndex"

					artifactManifestIndexBuf, err := json.Marshal(artifactManifest)
					So(err, ShouldBeNil)

					artifactManifestIndexDigest := godigest.FromBytes(artifactManifestIndexBuf)

					// push artifact manifest referencing a manifest from index image
					_, _, err = imgStore.PutImageManifest(repoName, artifactManifestIndexDigest.String(),
						ispec.MediaTypeImageManifest, artifactManifestIndexBuf)
					So(err, ShouldBeNil)

					// push artifact manifest pointing to artifact above
					artifactManifest.Subject = &ispec.Descriptor{
						MediaType: ispec.MediaTypeImageManifest,
						Digest:    artifactDigest,
						Size:      int64(len(artifactManifestBuf)),
					}
					artifactManifest.ArtifactType = "application/forArtifact"

					artifactManifestBuf, err = json.Marshal(artifactManifest)
					So(err, ShouldBeNil)

					artifactOfArtifactManifestDigest := godigest.FromBytes(artifactManifestBuf)
					_, _, err = imgStore.PutImageManifest(repoName, artifactOfArtifactManifestDigest.String(),
						ispec.MediaTypeImageManifest, artifactManifestBuf)
					So(err, ShouldBeNil)

					// push orphan artifact (missing subject)
					artifactManifest.Subject = &ispec.Descriptor{
						MediaType: ispec.MediaTypeImageManifest,
						Digest:    godigest.FromBytes([]byte("miss")),
						Size:      int64(30),
					}
					artifactManifest.ArtifactType = "application/orphan"

					artifactManifestBuf, err = json.Marshal(artifactManifest)
					So(err, ShouldBeNil)

					orphanArtifactManifestDigest := godigest.FromBytes(artifactManifestBuf)

					// push orphan artifact manifest
					_, _, err = imgStore.PutImageManifest(repoName, orphanArtifactManifestDigest.String(),
						ispec.MediaTypeImageManifest, artifactManifestBuf)
					So(err, ShouldBeNil)

					hasBlob, _, err := imgStore.CheckBlob(repoName, bdgst)
					So(err, ShouldBeNil)
					So(hasBlob, ShouldEqual, true)

					hasBlob, _, _, err = imgStore.StatBlob(repoName, bdgst)
					So(err, ShouldBeNil)
					So(hasBlob, ShouldEqual, true)

					hasBlob, _, err = imgStore.CheckBlob(repoName, artifactBlobDigest)
					So(err, ShouldBeNil)
					So(hasBlob, ShouldEqual, true)

					time.Sleep(2 * time.Second)

					Convey("delete inner referenced manifest", func() {
						err = gc.CleanRepo(ctx, repoName)
						So(err, ShouldBeNil)

						// check orphan artifact is gc'ed
						_, _, _, err = imgStore.GetImageManifest(repoName, orphanArtifactManifestDigest.String())
						So(err, ShouldNotBeNil)

						_, _, _, err = imgStore.GetImageManifest(repoName, artifactOfArtifactManifestDigest.String())
						So(err, ShouldBeNil)

						_, _, _, err = imgStore.GetImageManifest(repoName, artifactManifestIndexDigest.String())
						So(err, ShouldBeNil)

						_, _, _, err = imgStore.GetImageManifest(repoName, artifactDigest.String())
						So(err, ShouldBeNil)

						err = imgStore.DeleteImageManifest(repoName, artifactDigest.String(), false)
						So(err, ShouldBeNil)

						err = gc.CleanRepo(ctx, repoName)
						So(err, ShouldBeNil)

						_, _, _, err = imgStore.GetImageManifest(repoName, artifactOfArtifactManifestDigest.String())
						So(err, ShouldNotBeNil)

						_, _, _, err = imgStore.GetImageManifest(repoName, artifactManifestIndexDigest.String())
						So(err, ShouldBeNil)
					})

					Convey("delete index manifest, references should not be persisted", func() {
						err = gc.CleanRepo(ctx, repoName)
						So(err, ShouldBeNil)

						// check orphan artifact is gc'ed
						_, _, _, err = imgStore.GetImageManifest(repoName, orphanArtifactManifestDigest.String())
						So(err, ShouldNotBeNil)

						_, _, _, err = imgStore.GetImageManifest(repoName, artifactOfArtifactManifestDigest.String())
						So(err, ShouldBeNil)

						_, _, _, err = imgStore.GetImageManifest(repoName, artifactManifestIndexDigest.String())
						So(err, ShouldBeNil)

						_, _, _, err = imgStore.GetImageManifest(repoName, artifactDigest.String())
						So(err, ShouldBeNil)

						err = imgStore.DeleteImageManifest(repoName, indexDigest.String(), false)
						So(err, ShouldBeNil)

						err = gc.CleanRepo(ctx, repoName)
						So(err, ShouldBeNil)

						_, _, _, err = imgStore.GetImageManifest(repoName, artifactDigest.String())
						So(err, ShouldNotBeNil)

						_, _, _, err = imgStore.GetImageManifest(repoName, artifactOfArtifactManifestDigest.String())
						So(err, ShouldNotBeNil)

						_, _, _, err = imgStore.GetImageManifest(repoName, artifactManifestIndexDigest.String())
						So(err, ShouldNotBeNil)

						// orphan blob
						hasBlob, _, err = imgStore.CheckBlob(repoName, odigest)
						So(err, ShouldNotBeNil)
						So(hasBlob, ShouldEqual, false)

						hasBlob, _, _, err = imgStore.StatBlob(repoName, odigest)
						So(err, ShouldNotBeNil)
						So(hasBlob, ShouldEqual, false)

						hasBlob, _, err = imgStore.CheckBlob(repoName, bdgst)
						So(err, ShouldNotBeNil)
						So(hasBlob, ShouldEqual, false)

						// check last manifest from index image
						hasBlob, _, err = imgStore.CheckBlob(repoName, digest)
						So(err, ShouldNotBeNil)
						So(hasBlob, ShouldEqual, false)

						// check referrer is gc'ed
						_, _, _, err := imgStore.GetImageManifest(repoName, artifactDigest.String())
						So(err, ShouldNotBeNil)

						_, _, _, err = imgStore.GetImageManifest(repoName, artifactManifestIndexDigest.String())
						So(err, ShouldNotBeNil)

						hasBlob, _, err = imgStore.CheckBlob(repoName, artifactBlobDigest)
						So(err, ShouldNotBeNil)
						So(hasBlob, ShouldEqual, false)

						// check it gc'ed repo
						exists := imgStore.DirExists(path.Join(imgStore.RootDir(), repoName))
						So(exists, ShouldBeFalse)
					})
				})
			})
		})
	}
}

func TestGarbageCollectChainedImageIndexes(t *testing.T) {
	for _, testcase := range testCases {
		testcase := testcase
		t.Run(testcase.testCaseName, func(t *testing.T) {
			log := zlog.NewLogger("debug", "")
			audit := zlog.NewAuditLogger("debug", "")

			ctx := context.Background()

			//nolint: contextcheck
			Convey("Garbage collect with short delay", t, func() {
				var imgStore storageTypes.ImageStore

				gcDelay := 5 * time.Second
				imageRetentionDelay := 5 * time.Second

				cacheDir := t.TempDir()

				opts := createObjectStoreOpts{
					rootDir:     cacheDir,
					cacheDir:    cacheDir,
					cacheType:   testcase.cacheType,
					storageType: testcase.storageType,
				}

				if testcase.cacheType == storageConstants.RedisDriverName {
					miniRedis := miniredis.RunT(t)
					opts.miniRedisAddr = "redis://" + miniRedis.Addr()
					defer DumpKeys(t, opts.miniRedisAddr)
				}

				if testcase.storageType == storageConstants.S3StorageDriverName {
					tskip.SkipS3(t)

					uuid, err := guuid.NewV4()
					if err != nil {
						panic(err)
					}

					testDir := path.Join("/oci-repo-test", uuid.String())
					opts.rootDir = testDir

					var store storageTypes.Driver
					store, imgStore, _, _ = createObjectsStore(opts)
					defer cleanupStorage(store, testDir)
				} else {
					_, imgStore, _, _ = createObjectsStore(opts)
				}

				gc := gc.NewGarbageCollect(imgStore, mocks.MetaDBMock{}, gc.Options{
					Delay: gcDelay,
					ImageRetention: config.ImageRetention{ //nolint: gochecknoglobals
						Delay: imageRetentionDelay,
						Policies: []config.RetentionPolicy{
							{
								Repositories:    []string{"**"},
								DeleteReferrers: true,
								DeleteUntagged:  &trueVal,
							},
						},
					},
				}, audit, log)

				// upload orphan blob
				upload, err := imgStore.NewBlobUpload(repoName)
				So(err, ShouldBeNil)
				So(upload, ShouldNotBeEmpty)

				content := []byte("test-data1")
				buf := bytes.NewBuffer(content)
				buflen := buf.Len()
				odigest := godigest.FromBytes(content)

				blob, err := imgStore.PutBlobChunk(repoName, upload, 0, int64(buflen), buf)
				So(err, ShouldBeNil)
				So(blob, ShouldEqual, buflen)

				err = imgStore.FinishBlobUpload(repoName, upload, buf, odigest)
				So(err, ShouldBeNil)

				content = []byte("this is a blob")
				bdgst := godigest.FromBytes(content)
				So(bdgst, ShouldNotBeNil)

				_, bsize, err := imgStore.FullBlobUpload(repoName, bytes.NewReader(content), bdgst)
				So(err, ShouldBeNil)
				So(bsize, ShouldEqual, len(content))

				artifactBlob := []byte("artifact")
				artifactBlobDigest := godigest.FromBytes(artifactBlob)

				// push layer
				_, _, err = imgStore.FullBlobUpload(repoName, bytes.NewReader(artifactBlob), artifactBlobDigest)
				So(err, ShouldBeNil)

				// push config
				_, _, err = imgStore.FullBlobUpload(repoName, bytes.NewReader(ispec.DescriptorEmptyJSON.Data),
					ispec.DescriptorEmptyJSON.Digest)
				So(err, ShouldBeNil)

				var index ispec.Index
				index.SchemaVersion = 2
				index.MediaType = ispec.MediaTypeImageIndex

				var digest godigest.Digest

				for i := 0; i < 4; i++ { //nolint: dupl
					// upload image config blob
					upload, err := imgStore.NewBlobUpload(repoName)
					So(err, ShouldBeNil)
					So(upload, ShouldNotBeEmpty)

					cblob, cdigest := GetRandomImageConfig()
					buf := bytes.NewBuffer(cblob)
					buflen := buf.Len()
					blob, err := imgStore.PutBlobChunkStreamed(repoName, upload, buf)
					So(err, ShouldBeNil)
					So(blob, ShouldEqual, buflen)

					err = imgStore.FinishBlobUpload(repoName, upload, buf, cdigest)
					So(err, ShouldBeNil)
					So(blob, ShouldEqual, buflen)

					// create a manifest
					manifest := ispec.Manifest{
						Config: ispec.Descriptor{
							MediaType: ispec.MediaTypeImageConfig,
							Digest:    cdigest,
							Size:      int64(len(cblob)),
						},
						Layers: []ispec.Descriptor{
							{
								MediaType: ispec.MediaTypeImageLayer,
								Digest:    bdgst,
								Size:      bsize,
							},
						},
					}
					manifest.SchemaVersion = 2
					content, err = json.Marshal(manifest)
					So(err, ShouldBeNil)

					digest = godigest.FromBytes(content)
					So(digest, ShouldNotBeNil)
					_, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content)
					So(err, ShouldBeNil)

					index.Manifests = append(index.Manifests, ispec.Descriptor{
						Digest:    digest,
						MediaType: ispec.MediaTypeImageManifest,
						Size:      int64(len(content)),
					})

					// for each manifest inside index, push an artifact
					artifactManifest := ispec.Manifest{
						MediaType: ispec.MediaTypeImageManifest,
						Layers: []ispec.Descriptor{
							{
								MediaType: "application/octet-stream",
								Digest:    artifactBlobDigest,
								Size:      int64(len(artifactBlob)),
							},
						},
						Config: ispec.DescriptorEmptyJSON,
						Subject: &ispec.Descriptor{
							MediaType: ispec.MediaTypeImageManifest,
							Digest:    digest,
							Size:      int64(len(content)),
						},
						ArtifactType: "application/forManifestInInnerIndex",
					}
					artifactManifest.SchemaVersion = 2

					artifactManifestBuf, err := json.Marshal(artifactManifest)
					So(err, ShouldBeNil)

					artifactDigest := godigest.FromBytes(artifactManifestBuf)

					// push artifact manifest
					_, _, err = imgStore.PutImageManifest(repoName, artifactDigest.String(),
						ispec.MediaTypeImageManifest, artifactManifestBuf)
					So(err, ShouldBeNil)
				}

				// also add a new image index inside this one
				var innerIndex ispec.Index
				innerIndex.SchemaVersion = 2
				innerIndex.MediaType = ispec.MediaTypeImageIndex

				for i := 0; i < 3; i++ { //nolint: dupl
					// upload image config blob
					upload, err := imgStore.NewBlobUpload(repoName)
					So(err, ShouldBeNil)
					So(upload, ShouldNotBeEmpty)

					cblob, cdigest := GetRandomImageConfig()
					buf := bytes.NewBuffer(cblob)
					buflen := buf.Len()
					blob, err := imgStore.PutBlobChunkStreamed(repoName, upload, buf)
					So(err, ShouldBeNil)
					So(blob, ShouldEqual, buflen)

					err = imgStore.FinishBlobUpload(repoName, upload, buf, cdigest)
					So(err, ShouldBeNil)
					So(blob, ShouldEqual, buflen)

					// create a manifest
					manifest := ispec.Manifest{
						Config: ispec.Descriptor{
							MediaType: ispec.MediaTypeImageConfig,
							Digest:    cdigest,
							Size:      int64(len(cblob)),
						},
						Layers: []ispec.Descriptor{
							{
								MediaType: ispec.MediaTypeImageLayer,
								Digest:    bdgst,
								Size:      bsize,
							},
						},
					}
					manifest.SchemaVersion = 2
					content, err = json.Marshal(manifest)
					So(err, ShouldBeNil)

					digest := godigest.FromBytes(content)
					So(digest, ShouldNotBeNil)
					_, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content)
					So(err, ShouldBeNil)

					innerIndex.Manifests = append(innerIndex.Manifests, ispec.Descriptor{
						Digest:    digest,
						MediaType: ispec.MediaTypeImageManifest,
						Size:      int64(len(content)),
					})
				}

				// upload inner index image
				innerIndexContent, err := json.Marshal(index)
				So(err, ShouldBeNil)

				innerIndexDigest := godigest.FromBytes(innerIndexContent)
				So(innerIndexDigest, ShouldNotBeNil)

				_, _, err = imgStore.PutImageManifest(repoName, innerIndexDigest.String(),
					ispec.MediaTypeImageIndex, innerIndexContent)
				So(err, ShouldBeNil)

				// add inner index into  root index
				index.Manifests = append(index.Manifests, ispec.Descriptor{
					Digest:    innerIndexDigest,
					MediaType: ispec.MediaTypeImageIndex,
					Size:      int64(len(innerIndexContent)),
				})

				// push root index
				// upload index image
				indexContent, err := json.Marshal(index)
				So(err, ShouldBeNil)

				indexDigest := godigest.FromBytes(indexContent)
				So(indexDigest, ShouldNotBeNil)

				_, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageIndex, indexContent)
				So(err, ShouldBeNil)

				artifactManifest := ispec.Manifest{
					MediaType: ispec.MediaTypeImageManifest,
					Layers: []ispec.Descriptor{
						{
							MediaType: "application/octet-stream",
							Digest:    artifactBlobDigest,
							Size:      int64(len(artifactBlob)),
						},
					},
					Config: ispec.DescriptorEmptyJSON,
					Subject: &ispec.Descriptor{
						MediaType: ispec.MediaTypeImageIndex,
						Digest:    indexDigest,
						Size:      int64(len(indexContent)),
					},
					ArtifactType: "application/forIndex",
				}
				artifactManifest.SchemaVersion = 2

				artifactManifestBuf, err := json.Marshal(artifactManifest)
				So(err, ShouldBeNil)

				artifactDigest := godigest.FromBytes(artifactManifestBuf)

				// push artifact manifest
				_, _, err = imgStore.PutImageManifest(repoName, artifactDigest.String(),
					ispec.MediaTypeImageManifest, artifactManifestBuf)
				So(err, ShouldBeNil)

				artifactManifest.Subject = &ispec.Descriptor{
					MediaType: ispec.MediaTypeImageManifest,
					Digest:    digest,
					Size:      int64(len(content)),
				}
				artifactManifest.ArtifactType = "application/forManifestInIndex"

				artifactManifestIndexBuf, err := json.Marshal(artifactManifest)
				So(err, ShouldBeNil)

				artifactManifestIndexDigest := godigest.FromBytes(artifactManifestIndexBuf)

				// push artifact manifest referencing a manifest from index image
				_, _, err = imgStore.PutImageManifest(repoName, artifactManifestIndexDigest.String(),
					ispec.MediaTypeImageManifest, artifactManifestIndexBuf)
				So(err, ShouldBeNil)

				artifactManifest.Subject = &ispec.Descriptor{
					MediaType: ispec.MediaTypeImageIndex,
					Digest:    innerIndexDigest,
					Size:      int64(len(innerIndexContent)),
				}
				artifactManifest.ArtifactType = "application/forInnerIndex"

				artifactManifestInnerIndexBuf, err := json.Marshal(artifactManifest)
				So(err, ShouldBeNil)

				artifactManifestInnerIndexDigest := godigest.FromBytes(artifactManifestInnerIndexBuf)

				// push artifact manifest referencing a manifest from index image
				_, _, err = imgStore.PutImageManifest(repoName, artifactManifestInnerIndexDigest.String(),
					ispec.MediaTypeImageManifest, artifactManifestInnerIndexBuf)
				So(err, ShouldBeNil)

				// push artifact manifest pointing to artifact above

				artifactManifest.Subject = &ispec.Descriptor{
					MediaType: ispec.MediaTypeImageManifest,
					Digest:    artifactDigest,
					Size:      int64(len(artifactManifestBuf)),
				}
				artifactManifest.ArtifactType = "application/forArtifact"

				artifactManifestBuf, err = json.Marshal(artifactManifest)
				So(err, ShouldBeNil)

				artifactOfArtifactManifestDigest := godigest.FromBytes(artifactManifestBuf)
				_, _, err = imgStore.PutImageManifest(repoName, artifactOfArtifactManifestDigest.String(),
					ispec.MediaTypeImageManifest, artifactManifestBuf)
				So(err, ShouldBeNil)

				// push orphan artifact (missing subject)
				artifactManifest.Subject = &ispec.Descriptor{
					MediaType: ispec.MediaTypeImageManifest,
					Digest:    godigest.FromBytes([]byte("miss")),
					Size:      int64(30),
				}
				artifactManifest.ArtifactType = "application/orphan"

				artifactManifestBuf, err = json.Marshal(artifactManifest)
				So(err, ShouldBeNil)

				orphanArtifactManifestDigest := godigest.FromBytes(artifactManifestBuf)

				// push orphan artifact manifest
				_, _, err = imgStore.PutImageManifest(repoName, orphanArtifactManifestDigest.String(),
					ispec.MediaTypeImageManifest, artifactManifestBuf)
				So(err, ShouldBeNil)

				hasBlob, _, err := imgStore.CheckBlob(repoName, bdgst)
				So(err, ShouldBeNil)
				So(hasBlob, ShouldEqual, true)

				hasBlob, _, _, err = imgStore.StatBlob(repoName, bdgst)
				So(err, ShouldBeNil)
				So(hasBlob, ShouldEqual, true)

				hasBlob, _, err = imgStore.CheckBlob(repoName, artifactBlobDigest)
				So(err, ShouldBeNil)
				So(hasBlob, ShouldEqual, true)

				time.Sleep(5 * time.Second)

				Convey("delete inner referenced manifest", func() {
					err = gc.CleanRepo(ctx, repoName)
					So(err, ShouldBeNil)

					// check orphan artifact is gc'ed
					_, _, _, err = imgStore.GetImageManifest(repoName, orphanArtifactManifestDigest.String())
					So(err, ShouldNotBeNil)

					_, _, _, err = imgStore.GetImageManifest(repoName, artifactOfArtifactManifestDigest.String())
					So(err, ShouldBeNil)

					_, _, _, err = imgStore.GetImageManifest(repoName, artifactManifestIndexDigest.String())
					So(err, ShouldBeNil)

					_, _, _, err = imgStore.GetImageManifest(repoName, artifactDigest.String())
					So(err, ShouldBeNil)

					err = imgStore.DeleteImageManifest(repoName, artifactDigest.String(), false)
					So(err, ShouldBeNil)

					err = gc.CleanRepo(ctx, repoName)
					So(err, ShouldBeNil)

					_, _, _, err = imgStore.GetImageManifest(repoName, artifactOfArtifactManifestDigest.String())
					So(err, ShouldNotBeNil)

					_, _, _, err = imgStore.GetImageManifest(repoName, artifactManifestIndexDigest.String())
					So(err, ShouldBeNil)
				})

				Convey("delete index manifest, references should not be persisted", func() {
					err = gc.CleanRepo(ctx, repoName)
					So(err, ShouldBeNil)

					// check orphan artifact is gc'ed
					_, _, _, err = imgStore.GetImageManifest(repoName, orphanArtifactManifestDigest.String())
					So(err, ShouldNotBeNil)

					_, _, _, err = imgStore.GetImageManifest(repoName, artifactOfArtifactManifestDigest.String())
					So(err, ShouldBeNil)

					_, _, _, err = imgStore.GetImageManifest(repoName, artifactManifestIndexDigest.String())
					So(err, ShouldBeNil)

					_, _, _, err = imgStore.GetImageManifest(repoName, artifactDigest.String())
					So(err, ShouldBeNil)

					err = imgStore.DeleteImageManifest(repoName, indexDigest.String(), false)
					So(err, ShouldBeNil)

					err = gc.CleanRepo(ctx, repoName)
					So(err, ShouldBeNil)

					_, _, _, err = imgStore.GetImageManifest(repoName, artifactDigest.String())
					So(err, ShouldNotBeNil)

					_, _, _, err = imgStore.GetImageManifest(repoName, artifactOfArtifactManifestDigest.String())
					So(err, ShouldNotBeNil)

					// orphan blob
					hasBlob, _, err = imgStore.CheckBlob(repoName, odigest)
					So(err, ShouldNotBeNil)
					So(hasBlob, ShouldEqual, false)

					hasBlob, _, _, err = imgStore.StatBlob(repoName, odigest)
					So(err, ShouldNotBeNil)
					So(hasBlob, ShouldEqual, false)

					// check artifact is gc'ed
					_, _, _, err := imgStore.GetImageManifest(repoName, artifactDigest.String())
					So(err, ShouldNotBeNil)

					// check inner index artifact is gc'ed
					_, _, _, err = imgStore.GetImageManifest(repoName, artifactManifestInnerIndexDigest.String())
					So(err, ShouldNotBeNil)

					// check last manifest from index image
					hasBlob, _, err = imgStore.CheckBlob(repoName, digest)
					So(err, ShouldNotBeNil)
					So(hasBlob, ShouldEqual, false)

					_, _, _, err = imgStore.GetImageManifest(repoName, artifactManifestIndexDigest.String())
					So(err, ShouldNotBeNil)

					hasBlob, _, err = imgStore.CheckBlob(repoName, artifactBlobDigest)
					So(err, ShouldNotBeNil)
					So(hasBlob, ShouldEqual, false)

					hasBlob, _, err = imgStore.CheckBlob(repoName, bdgst)
					So(err, ShouldNotBeNil)
					So(hasBlob, ShouldEqual, false)

					// check it gc'ed repo
					exists := imgStore.DirExists(path.Join(imgStore.RootDir(), repoName))
					So(exists, ShouldBeFalse)
				})
			})
		})
	}
}

func pushRandomImageIndex(imgStore storageTypes.ImageStore, repoName string,
) (godigest.Digest, godigest.Digest, godigest.Digest, int64) {
	content := []byte("this is a blob")
	bdgst := godigest.FromBytes(content)
	So(bdgst, ShouldNotBeNil)

	_, bsize, err := imgStore.FullBlobUpload(repoName, bytes.NewReader(content), bdgst)
	So(err, ShouldBeNil)
	So(bsize, ShouldEqual, len(content))

	var index ispec.Index
	index.SchemaVersion = 2
	index.MediaType = ispec.MediaTypeImageIndex

	var digest godigest.Digest

	for i := 0; i < 4; i++ { //nolint: dupl
		// upload image config blob
		upload, err := imgStore.NewBlobUpload(repoName)
		So(err, ShouldBeNil)
		So(upload, ShouldNotBeEmpty)

		cblob, cdigest := GetRandomImageConfig()
		buf := bytes.NewBuffer(cblob)
		buflen := buf.Len()
		blob, err := imgStore.PutBlobChunkStreamed(repoName, upload, buf)
		So(err, ShouldBeNil)
		So(blob, ShouldEqual, buflen)

		err = imgStore.FinishBlobUpload(repoName, upload, buf, cdigest)
		So(err, ShouldBeNil)
		So(blob, ShouldEqual, buflen)

		// create a manifest
		manifest := ispec.Manifest{
			Config: ispec.Descriptor{
				MediaType: ispec.MediaTypeImageConfig,
				Digest:    cdigest,
				Size:      int64(len(cblob)),
			},
			Layers: []ispec.Descriptor{
				{
					MediaType: ispec.MediaTypeImageLayer,
					Digest:    bdgst,
					Size:      bsize,
				},
			},
		}
		manifest.SchemaVersion = 2
		content, err = json.Marshal(manifest)
		So(err, ShouldBeNil)

		digest = godigest.FromBytes(content)
		So(digest, ShouldNotBeNil)
		_, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content)
		So(err, ShouldBeNil)

		index.Manifests = append(index.Manifests, ispec.Descriptor{
			Digest:    digest,
			MediaType: ispec.MediaTypeImageManifest,
			Size:      int64(len(content)),
		})
	}

	// upload index image
	indexContent, err := json.Marshal(index)
	So(err, ShouldBeNil)

	indexDigest := godigest.FromBytes(indexContent)
	So(indexDigest, ShouldNotBeNil)

	_, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageIndex, indexContent)
	So(err, ShouldBeNil)

	return bdgst, digest, indexDigest, int64(len(indexContent))
}

func DumpKeys(t *testing.T, redisURL string) {
	t.Helper()

	// Initialize redis client
	connOpts, err := redis.ParseURL(redisURL)
	if err != nil {
		return
	}

	client := redis.NewClient(connOpts)

	// Retrieve all keys
	keys, err := client.Keys(context.Background(), "*").Result()
	if err != nil {
		t.Log("Error retrieving keys:", err)

		return
	}

	// Print the keys
	t.Log("Keys in Redis:")

	for _, key := range keys {
		keyType, err := client.Type(context.Background(), key).Result()
		if err != nil {
			t.Logf("Error retrieving type for key %s: %v\n", key, err)

			continue
		}

		var value string

		switch keyType {
		case "string":
			value, err = client.Get(context.Background(), key).Result()
		case "list":
			values, err := client.LRange(context.Background(), key, 0, -1).Result()
			if err == nil {
				value = fmt.Sprintf("%v", values)
			}
		case "hash":
			values, err := client.HGetAll(context.Background(), key).Result()
			if err == nil {
				value = fmt.Sprintf("%v", values)
			}
		case "set":
			values, err := client.SMembers(context.Background(), key).Result()
			if err == nil {
				value = fmt.Sprintf("%v", values)
			}
		case "zset":
			values, err := client.ZRange(context.Background(), key, 0, -1).Result()
			if err == nil {
				value = fmt.Sprintf("%v", values)
			}
		default:
			value = "Unsupported type"
		}

		if err != nil {
			t.Logf("Error retrieving value for key %s: %v\n", key, err)
		} else {
			t.Logf("Key: %s, Type: %s, Value: %s\n", key, keyType, value)
		}
	}
}
