//go:build needprivileges && linux

package gcs_test

import (
	"bytes"
	"context"
	"crypto/rand"
	"crypto/rsa"
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/json"
	"encoding/pem"
	"errors"
	"fmt"
	"io"
	"math/big"
	"net"
	"net/http"
	"os"
	"os/exec"
	"path"
	"regexp"
	"slices"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/distribution/distribution/v3/registry/storage/driver"
	"github.com/distribution/distribution/v3/registry/storage/driver/factory"
	guuid "github.com/gofrs/uuid"
	godigest "github.com/opencontainers/go-digest"
	ispec "github.com/opencontainers/image-spec/specs-go/v1"
	. "github.com/smartystreets/goconvey/convey"

	zerr "zotregistry.dev/zot/v2/errors"
	"zotregistry.dev/zot/v2/pkg/api/config"
	"zotregistry.dev/zot/v2/pkg/extensions/monitoring"
	"zotregistry.dev/zot/v2/pkg/log"
	"zotregistry.dev/zot/v2/pkg/storage"
	"zotregistry.dev/zot/v2/pkg/storage/cache"
	common "zotregistry.dev/zot/v2/pkg/storage/common"
	storageConstants "zotregistry.dev/zot/v2/pkg/storage/constants"
	"zotregistry.dev/zot/v2/pkg/storage/gc"
	"zotregistry.dev/zot/v2/pkg/storage/gcs"
	storageTypes "zotregistry.dev/zot/v2/pkg/storage/types"
	. "zotregistry.dev/zot/v2/pkg/test/image-utils"
	"zotregistry.dev/zot/v2/pkg/test/mocks"
	tskip "zotregistry.dev/zot/v2/pkg/test/skip"
)

//nolint:gochecknoglobals // test constants
const (
	repoName = "test"
	tag      = "0.0.1"
)

var (
	trueVal                  bool = true //nolint: gochecknoglobals
	errGCSMockEndpointNotSet      = errors.New("GCSMOCK_ENDPOINT must be set for GCS tests")
	errUnexpectedError            = errors.New("unexpected err")
	errBucketCreateFailed         = errors.New("failed to create bucket")
)

// httpsProxyServer manages an HTTPS proxy server on port 443.
type httpsProxyServer struct {
	server   *http.Server
	listener net.Listener
	wg       sync.WaitGroup
	target   string
	certFile string // Path to the certificate file for cleanup
}

// newHTTPSProxyServer creates a new HTTPS proxy server that forwards requests to the target.
func newHTTPSProxyServer(target string) (*httpsProxyServer, error) {
	// Generate self-signed certificate
	priv, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		return nil, fmt.Errorf("failed to generate private key: %w", err)
	}

	template := x509.Certificate{
		SerialNumber: big.NewInt(1),
		Subject: pkix.Name{
			CommonName: "oauth2.googleapis.com",
		},
		NotBefore:   time.Now(),
		NotAfter:    time.Now().Add(24 * time.Hour),
		KeyUsage:    x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
		ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
		DNSNames:    []string{"oauth2.googleapis.com", "www.googleapis.com", "storage.googleapis.com"},
		IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1)},
	}

	certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
	if err != nil {
		return nil, fmt.Errorf("failed to create certificate: %w", err)
	}

	certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
	keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})

	cert, err := tls.X509KeyPair(certPEM, keyPEM)
	if err != nil {
		return nil, fmt.Errorf("failed to create key pair: %w", err)
	}

	// Write certificate to a temporary file so we can add it to the trusted certificates
	// via SSL_CERT_FILE environment variable. This is the standard way to add custom
	// trusted certificates and works with Go's crypto/x509 package, including OAuth2 clients.
	certFile, err := os.CreateTemp("", "gcs-test-cert-*.pem")
	if err != nil {
		return nil, fmt.Errorf("failed to create temp cert file: %w", err)
	}
	if _, err := certFile.Write(certPEM); err != nil {
		certFile.Close()
		os.Remove(certFile.Name())

		return nil, fmt.Errorf("failed to write cert to file: %w", err)
	}

	if err := certFile.Close(); err != nil {
		os.Remove(certFile.Name())

		return nil, fmt.Errorf("failed to close cert file: %w", err)
	}

	// Create proxy handler
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Build target URL
		targetURL := target + r.URL.Path
		if r.URL.RawQuery != "" {
			targetURL += "?" + r.URL.RawQuery
		}

		// Create request to target.
		//nolint:gosec // proxy target is local test server
		req, err := http.NewRequestWithContext(
			r.Context(),
			r.Method,
			targetURL,
			r.Body,
		)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)

			return
		}

		// Copy headers
		for key, values := range r.Header {
			if key != "Host" && key != "Connection" {
				for _, value := range values {
					req.Header.Add(key, value)
				}
			}
		}

		// Make request
		client := &http.Client{Timeout: 30 * time.Second}
		resp, err := client.Do(req) //nolint:gosec // request is sent to local test server
		if err != nil {
			http.Error(w, err.Error(), http.StatusBadGateway)

			return
		}
		defer resp.Body.Close()

		// Copy response headers
		for key, values := range resp.Header {
			if key != "Connection" && key != "Transfer-Encoding" {
				for _, value := range values {
					w.Header().Add(key, value)
				}
			}
		}

		// Copy status and body
		w.WriteHeader(resp.StatusCode)
		_, _ = io.Copy(w, resp.Body)
	})

	// Create HTTP server with TLS config (test-only proxy).
	server := &http.Server{
		Handler:           handler,
		ReadHeaderTimeout: 10 * time.Second,
		TLSConfig: &tls.Config{
			Certificates: []tls.Certificate{cert},
			MinVersion:   tls.VersionTLS12,
		},
	}

	// Try to listen on port 443 (requires root or CAP_NET_BIND_SERVICE for tests).
	lc := net.ListenConfig{}
	listener, err := lc.Listen(context.Background(), "tcp", ":443") //nolint:gosec // G102: test proxy must listen on 443
	if err != nil {
		return nil, fmt.Errorf("failed to listen on port 443: %w (may require root or CAP_NET_BIND_SERVICE)", err)
	}

	tlsListener := tls.NewListener(listener, server.TLSConfig)

	return &httpsProxyServer{
		server:   server,
		listener: tlsListener,
		target:   target,
		certFile: certFile.Name(),
	}, nil
}

func (p *httpsProxyServer) Start() {
	p.wg.Go(func() {
		_ = p.server.Serve(p.listener)
	})
}

func (p *httpsProxyServer) Stop() {
	_ = p.listener.Close()
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	_ = p.server.Shutdown(ctx)
	p.wg.Wait()
}

var httpsProxy *httpsProxyServer //nolint:gochecknoglobals // Test fixture shared by TestMain.

// setupHostsEntries adds entries to /etc/hosts to redirect Google API domains to localhost.
func setupHostsEntries() error {
	entries := []string{
		"127.0.0.1 www.googleapis.com",
		"127.0.0.1 storage.googleapis.com",
		"127.0.0.1 oauth2.googleapis.com",
	}

	for _, entry := range entries {
		// Check if entry already exists.
		//nolint:gosec // G204: test-only, fixed entries
		cmd := exec.CommandContext(context.Background(), "grep", "-q", strings.Fields(entry)[1], "/etc/hosts")
		if cmd.Run() == nil {
			// Entry already exists, skip
			continue
		}

		// Add entry (requires privileges).
		//nolint:gosec // G204: test-only, controlled entry
		cmd = exec.CommandContext(context.Background(), "sh", "-c", fmt.Sprintf("echo '%s' >> /etc/hosts", entry))
		if err := cmd.Run(); err != nil {
			return fmt.Errorf("failed to add %s to /etc/hosts: %w", entry, err)
		}
	}

	return nil
}

// teardownHostsEntries removes entries from /etc/hosts that were added for the emulator.
func teardownHostsEntries() {
	domains := []string{
		"www.googleapis.com",
		"storage.googleapis.com",
		"oauth2.googleapis.com",
	}

	for _, domain := range domains {
		// Remove entry (requires privileges).
		//nolint:gosec // G204: test-only, fixed domains
		pattern := fmt.Sprintf("/%s/d", strings.ReplaceAll(domain, ".", "\\."))
		cmd := exec.CommandContext(context.Background(), "sed", "-i", pattern, "/etc/hosts")
		_ = cmd.Run() // Ignore errors - entry might not exist
	}
}

// TestMain sets up and tears down the HTTPS proxy and /etc/hosts entries for all tests in this package.
// TestMain runs once before all tests and once after all tests complete.
// It applies to all test files in the same package (gcs_test package).
func TestMain(m *testing.M) {
	// Setup /etc/hosts entries if GCSMOCK_ENDPOINT is set
	if os.Getenv("GCSMOCK_ENDPOINT") != "" {
		if err := setupHostsEntries(); err != nil {
			fmt.Printf("Warning: Could not modify /etc/hosts: %v\n", err)
			fmt.Printf("Tests may fail if /etc/hosts entries are not present\n")
		} else {
			fmt.Println("Added /etc/hosts entries for Google API domains")
		}
	}

	// Start HTTPS proxy before all tests if GCSMOCK_ENDPOINT is set
	if os.Getenv("GCSMOCK_ENDPOINT") != "" {
		endpoint := os.Getenv("GCSMOCK_ENDPOINT")
		endpoint = strings.TrimSuffix(endpoint, "/")
		target := endpoint

		var err error
		httpsProxy, err = newHTTPSProxyServer(target)
		if err != nil {
			// Fail fast: with /etc/hosts redirecting Google domains to 127.0.0.1,
			// OAuth/token calls will hit localhost:443 and fail with unclear errors
			// if the proxy is not listening. Require the proxy to start.
			fmt.Fprintf(os.Stderr, "Fatal: cannot start HTTPS proxy on port 443: %v\n", err)
			fmt.Fprintf(os.Stderr, "This may require root or CAP_NET_BIND_SERVICE. Exiting.\n")
			os.Exit(1)
		}
		httpsProxy.Start()
		// Set SSL_CERT_FILE to trust our self-signed certificate
		// This is respected by Go's crypto/x509 package when loading the system cert pool
		// and will affect all TLS connections, including those made by OAuth2 clients
		os.Setenv("SSL_CERT_FILE", httpsProxy.certFile)
		fmt.Printf("HTTPS proxy started on port 443, certificate: %s\n", httpsProxy.certFile)
	}

	// Run all tests
	code := m.Run()

	// Stop proxy after all tests finish
	if httpsProxy != nil {
		httpsProxy.Stop()
		fmt.Println("HTTPS proxy stopped")
		httpsProxy = nil
	}

	// Cleanup /etc/hosts entries
	if os.Getenv("GCSMOCK_ENDPOINT") != "" {
		teardownHostsEntries()
		fmt.Println("Removed /etc/hosts entries for Google API domains")
	}

	os.Exit(code)
}

func ensureDummyGCSCreds(t *testing.T) {
	t.Helper()

	if os.Getenv("GCSMOCK_ENDPOINT") != "" {
		credsFile := path.Join(t.TempDir(), "dummy_creds.json")

		priv, err := rsa.GenerateKey(rand.Reader, 2048)
		if err != nil {
			t.Fatal(err)
		}

		privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
		if err != nil {
			t.Fatal(err)
		}

		privPEM := pem.EncodeToMemory(&pem.Block{
			Type:  "PRIVATE KEY",
			Bytes: privBytes,
		})

		content := fmt.Sprintf(`{"type": "service_account", "project_id": "test-project", `+
			`"client_email": "test@test.com", "private_key": %q}`, string(privPEM))
		err = os.WriteFile(credsFile, []byte(content), 0o600)
		if err != nil {
			t.Fatal(err)
		}

		t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", credsFile)
	}
}

func cleanupStorage(store driver.StorageDriver, name string) {
	_ = store.Delete(context.Background(), name)
}

// createObjectsStore creates a GCS-backed store; dedupe is always true at call sites.
//
//nolint:unparam
func createObjectsStore(rootDir string, cacheDir string, dedupe bool) (
	driver.StorageDriver,
	storageTypes.ImageStore,
	error,
) {
	bucket := "zot-storage-test"

	endpoint := os.Getenv("GCSMOCK_ENDPOINT")
	if endpoint == "" {
		return nil, nil, errGCSMockEndpointNotSet
	}

	url := strings.TrimSuffix(endpoint, "/") + "/storage/v1/b?project=test-project"
	body := fmt.Sprintf(`{"name": "%s"}`, bucket)
	//nolint:gosec // URL points to gcsmock endpoint in tests
	req, err := http.NewRequestWithContext(
		context.Background(),
		http.MethodPost,
		url,
		strings.NewReader(body),
	)
	if err != nil {
		return nil, nil, err
	}
	req.Header.Set("Content-Type", "application/json")
	resp, err := http.DefaultClient.Do(req) //nolint:gosec // G107: Test mock
	if err != nil {
		return nil, nil, err
	}
	defer resp.Body.Close()

	// Check if bucket was created successfully or already exists.
	okStatus := resp.StatusCode == http.StatusOK ||
		resp.StatusCode == http.StatusCreated ||
		resp.StatusCode == http.StatusConflict
	if !okStatus {
		respBody, _ := io.ReadAll(resp.Body)

		return nil, nil, fmt.Errorf("%w %s: status %d body %s",
			errBucketCreateFailed, bucket, resp.StatusCode, string(respBody))
	}

	storageDriverParams := map[string]any{
		"rootDir": rootDir,
		"name":    "gcs",
		"bucket":  bucket,
	}

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

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

	log := log.NewTestLogger()
	metrics := monitoring.NewMetricsServer(false, log)
	defer metrics.Stop()

	var cacheDriver storageTypes.Cache

	// from pkg/cli/server/root.go/applyDefaultValues, s3 magic
	s3CacheDBPath := path.Join(cacheDir, storageConstants.BoltdbName+storageConstants.DBExtensionName)

	if _, err := os.Stat(s3CacheDBPath); dedupe || (!dedupe && err == nil) {
		cacheDriver, _ = storage.Create("boltdb", cache.BoltDBDriverParameters{
			RootDir:     cacheDir,
			Name:        "cache",
			UseRelPaths: false,
		}, log)
	}

	il := gcs.NewImageStore(rootDir, cacheDir, dedupe, false, log, metrics, nil, store, cacheDriver, nil, nil)

	return store, il, nil
}

func TestGCSDriver(t *testing.T) {
	tskip.SkipGCS(t)
	ensureDummyGCSCreds(t)

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

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

	Convey("GCS Driver E2E", t, func() {
		// Create a fresh temp dir for each run to avoid BoltDB lock issues
		tdir := t.TempDir()
		storeDriver, imgStore, err := createObjectsStore(testDir, tdir, true)
		So(err, ShouldBeNil)
		defer cleanupStorage(storeDriver, testDir)

		Convey("Init Repo", func() {
			repoName := "test-repo-init"
			err := imgStore.InitRepo(repoName)
			So(err, ShouldBeNil)

			isValid, err := imgStore.ValidateRepo(repoName)
			So(err, ShouldBeNil)
			So(isValid, ShouldBeTrue)
		})

		Convey("Push and Pull Image", func() {
			repoName := "test-repo-push"
			image := CreateDefaultImage()

			// Upload layers
			for _, content := range image.Layers {
				upload, err := imgStore.NewBlobUpload(repoName)
				So(err, ShouldBeNil)

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

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

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

			// Upload config
			cblob, err := json.Marshal(image.Config)
			So(err, ShouldBeNil)
			cdigest := godigest.FromBytes(cblob)
			_, _, err = imgStore.FullBlobUpload(repoName, bytes.NewBuffer(cblob), cdigest)
			So(err, ShouldBeNil)

			// Upload manifest
			mblob, err := json.Marshal(image.Manifest)
			So(err, ShouldBeNil)
			_, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageManifest, mblob, nil)
			So(err, ShouldBeNil)

			// Verify manifest
			_, _, _, err = imgStore.GetImageManifest(repoName, "1.0")
			So(err, ShouldBeNil)

			// Verify blob
			blobReadCloser, _, err := imgStore.GetBlob(repoName, cdigest, ispec.MediaTypeImageConfig)
			So(err, ShouldBeNil)
			defer blobReadCloser.Close()
			content, err := io.ReadAll(blobReadCloser)
			So(err, ShouldBeNil)
			So(content, ShouldResemble, cblob)
		})

		Convey("Delete Image", func() {
			repoName := "test-repo-delete"
			// Setup image
			image := CreateDefaultImage()

			// Upload layers first (required for manifest validation)
			for _, content := range image.Layers {
				upload, err := imgStore.NewBlobUpload(repoName)
				So(err, ShouldBeNil)

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

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

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

			// Upload config
			cblob, err := json.Marshal(image.Config)
			So(err, ShouldBeNil)
			cdigest := godigest.FromBytes(cblob)
			_, _, err = imgStore.FullBlobUpload(repoName, bytes.NewBuffer(cblob), cdigest)
			So(err, ShouldBeNil)

			// Upload manifest
			mblob, err := json.Marshal(image.Manifest)
			So(err, ShouldBeNil)
			_, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageManifest, mblob, nil)
			So(err, ShouldBeNil)

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

			_, _, _, err = imgStore.GetImageManifest(repoName, "1.0")
			So(err, ShouldNotBeNil)
			So(errors.Is(err, zerr.ErrManifestNotFound), ShouldBeTrue)
		})
	})
}

func TestGCSDedupe(t *testing.T) {
	tskip.SkipGCS(t)
	ensureDummyGCSCreds(t)

	Convey("Dedupe", t, func(c C) {
		uuid, err := guuid.NewV4()
		if err != nil {
			panic(err)
		}

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

		tdir := t.TempDir()

		storeDriver, imgStore, err := createObjectsStore(testDir, tdir, true)
		So(err, ShouldBeNil)
		defer cleanupStorage(storeDriver, testDir)

		// manifest1
		upload, err := imgStore.NewBlobUpload("dedupe1")
		So(err, ShouldBeNil)
		So(upload, ShouldNotBeEmpty)

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

		blobDigest1 := digest
		So(blobDigest1, ShouldNotBeEmpty)

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

		ok, checkBlobSize1, err := imgStore.CheckBlob("dedupe1", digest)
		So(ok, ShouldBeTrue)
		So(checkBlobSize1, ShouldBeGreaterThan, 0)
		So(err, ShouldBeNil)

		ok, checkBlobSize1, _, err = imgStore.StatBlob("dedupe1", digest)
		So(ok, ShouldBeTrue)
		So(checkBlobSize1, ShouldBeGreaterThan, 0)
		So(err, ShouldBeNil)

		blobReadCloser, getBlobSize1, err := imgStore.GetBlob("dedupe1", digest,
			"application/vnd.oci.image.layer.v1.tar+gzip")
		So(getBlobSize1, ShouldBeGreaterThan, 0)
		So(err, ShouldBeNil)
		err = blobReadCloser.Close()
		So(err, ShouldBeNil)

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

		hasBlob, _, err := imgStore.CheckBlob("dedupe1", 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)

		manifestDigest := godigest.FromBytes(manifestBuf)
		_, _, err = imgStore.PutImageManifest("dedupe1", manifestDigest.String(),
			ispec.MediaTypeImageManifest, manifestBuf, nil)
		So(err, ShouldBeNil)

		_, _, _, err = imgStore.GetImageManifest("dedupe1", manifestDigest.String())
		So(err, ShouldBeNil)

		// manifest2
		upload, err = imgStore.NewBlobUpload("dedupe2")
		So(err, ShouldBeNil)
		So(upload, ShouldNotBeEmpty)

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

		blob, err = imgStore.PutBlobChunkStreamed("dedupe2", upload, buf)
		So(err, ShouldBeNil)
		So(blob, ShouldEqual, buflen)

		blobDigest2 := digest
		So(blobDigest2, ShouldNotBeEmpty)

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

		ok, checkBlobSize2, err := imgStore.CheckBlob("dedupe2", digest)
		So(ok, ShouldBeTrue)
		So(checkBlobSize2, ShouldBeGreaterThan, 0)
		So(err, ShouldBeNil)

		ok, checkBlobSize2, _, err = imgStore.StatBlob("dedupe2", digest)
		So(ok, ShouldBeTrue)
		So(checkBlobSize2, ShouldBeGreaterThan, 0)
		So(err, ShouldBeNil)

		blobReadCloser, getBlobSize2, err := imgStore.GetBlob("dedupe2", digest,
			"application/vnd.oci.image.layer.v1.tar+gzip")
		So(getBlobSize2, ShouldBeGreaterThan, 0)
		So(err, ShouldBeNil)
		err = blobReadCloser.Close()
		So(err, ShouldBeNil)

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

		hasBlob, _, err = imgStore.CheckBlob("dedupe2", 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)

		manifestDigest = godigest.FromBytes(manifestBuf)
		_, _, err = imgStore.PutImageManifest("dedupe2", manifestDigest.String(),
			ispec.MediaTypeImageManifest, manifestBuf, nil)
		So(err, ShouldBeNil)

		_, _, _, err = imgStore.GetImageManifest("dedupe2", manifestDigest.String())
		So(err, ShouldBeNil)

		So(blobDigest1, ShouldEqual, blobDigest2)
		So(checkBlobSize1, ShouldEqual, checkBlobSize2)
		So(getBlobSize1, ShouldEqual, getBlobSize2)
	})
}

func TestGCSPullRange(t *testing.T) {
	tskip.SkipGCS(t)
	ensureDummyGCSCreds(t)

	Convey("Pull range", t, func(c C) {
		uuid, err := guuid.NewV4()
		if err != nil {
			panic(err)
		}

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

		tdir := t.TempDir()

		storeDriver, imgStore, err := createObjectsStore(testDir, tdir, true)
		So(err, ShouldBeNil)
		defer cleanupStorage(storeDriver, testDir)

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

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

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

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

		// get range
		blobReadCloser, _, _, err = imgStore.GetBlobPartial("test", digest,
			"application/vnd.oci.image.layer.v1.tar+gzip", 0, 4)
		So(err, ShouldBeNil)
		buf.Reset()
		_, err = buf.ReadFrom(blobReadCloser)
		So(err, ShouldBeNil)
		So(buf.String(), ShouldEqual, "test-")
		err = blobReadCloser.Close()
		So(err, ShouldBeNil)

		// get range - "data3" is bytes 5-9 (inclusive) of "test-data3"
		blobReadCloser, _, _, err = imgStore.GetBlobPartial("test", digest,
			"application/vnd.oci.image.layer.v1.tar+gzip", 5, 9)
		So(err, ShouldBeNil)
		buf.Reset()
		_, err = buf.ReadFrom(blobReadCloser)
		So(err, ShouldBeNil)
		So(buf.String(), ShouldEqual, "data3")
		err = blobReadCloser.Close()
		So(err, ShouldBeNil)

		// get range from negative offset
		blobReadCloser, _, _, err = imgStore.GetBlobPartial("test", digest,
			"application/vnd.oci.image.layer.v1.tar+gzip", -4, 4)
		So(err, ShouldNotBeNil)
		So(blobReadCloser, ShouldBeNil)
	})
}

func TestGCSGetAllDedupeReposCandidates(t *testing.T) {
	tskip.SkipGCS(t)
	ensureDummyGCSCreds(t)

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

	testDir := path.Join("/oci-repo-test", uuid.String())
	tdir := t.TempDir()

	storeDriver, imgStore, err := createObjectsStore(testDir, tdir, true)
	if err != nil {
		panic(err)
	}

	defer cleanupStorage(storeDriver, testDir)

	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 TestGCSDeleteBlobsInUse(t *testing.T) {
	tskip.SkipGCS(t)
	ensureDummyGCSCreds(t)

	Convey("Setup manifest", t, func() {
		uuid, err := guuid.NewV4()
		if err != nil {
			panic(err)
		}

		testDir := path.Join("/oci-repo-test", uuid.String())
		tdir := t.TempDir()

		storeDriver, imgStore, err := createObjectsStore(testDir, tdir, true)
		So(err, ShouldBeNil)
		defer cleanupStorage(storeDriver, testDir)
		// 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()

		var clen int64
		_, 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, nil)
		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)
		})
	})
}

func TestGCSStorageAPIs(t *testing.T) {
	tskip.SkipGCS(t)
	ensureDummyGCSCreds(t)

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

	testDir := path.Join("/oci-repo-test", uuid.String())
	tdir := t.TempDir()

	storeDriver, imgStore, err := createObjectsStore(testDir, tdir, true)
	if err != nil {
		panic(err)
	}

	defer cleanupStorage(storeDriver, testDir)

	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, nil)
					So(err, ShouldNotBeNil)

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

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

					_, _, err = imgStore.PutImageManifest("test", digest.String(), ispec.MediaTypeImageManifest,
						manifestBuf, nil)
					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, nil)
					So(err, ShouldNotBeNil)

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

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

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

					_, _, err = imgStore.PutImageManifest("test", "3.0", ispec.MediaTypeImageManifest, manifestBuf, nil)
					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, nil)
					So(err, ShouldNotBeNil)

					_, _, err = imgStore.PutImageManifest("test", digest.String(),
						ispec.MediaTypeImageManifest, []byte("bad json"), nil)
					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, nil)
					So(err, ShouldBeNil)

					// same manifest for coverage
					_, _, err = imgStore.PutImageManifest("test", digest.String(),
						ispec.MediaTypeImageManifest, manifestBuf, nil)
					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)

					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, nil)
			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, nil)
			So(err, ShouldBeNil)
		})

		Convey("Locks", func() {
			// in parallel, a mix of read and write locks - mainly for coverage
			var wg sync.WaitGroup
			for range 1000 {
				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 TestGCSReuploadCorruptedBlob(t *testing.T) {
	tskip.SkipGCS(t)
	ensureDummyGCSCreds(t)

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

	testDir := path.Join("/oci-repo-test", uuid.String())
	tdir := t.TempDir()

	rawDriver, imgStore, err := createObjectsStore(testDir, tdir, true)
	if err != nil {
		panic(err)
	}

	defer cleanupStorage(rawDriver, testDir)

	// Wrap driver for WriteFile access
	gcsDriver := gcs.New(rawDriver)

	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 = gcsDriver.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 = gcsDriver.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 TestGCSStorageHandler(t *testing.T) {
	tskip.SkipGCS(t)
	ensureDummyGCSCreds(t)

	Convey("Test storage handler", t, func() {
		firstRootDir := "/util_test1"
		firstCacheDir := t.TempDir()

		firstStorageDriver, firstStore, err := createObjectsStore(firstRootDir, firstCacheDir, true)
		So(err, ShouldBeNil)
		defer cleanupStorage(firstStorageDriver, firstRootDir)

		secondRootDir := "/util_test2"
		secondCacheDir := t.TempDir()

		secondStorageDriver, secondStore, err := createObjectsStore(secondRootDir, secondCacheDir, true)
		So(err, ShouldBeNil)
		defer cleanupStorage(secondStorageDriver, secondRootDir)

		thirdRootDir := "/util_test3"
		thirdCacheDir := t.TempDir()

		thirdStorageDriver, thirdStore, err := createObjectsStore(thirdRootDir, thirdCacheDir, true)
		So(err, ShouldBeNil)
		defer cleanupStorage(thirdStorageDriver, thirdRootDir)
		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 TestGCSMandatoryAnnotations(t *testing.T) {
	tskip.SkipGCS(t)
	ensureDummyGCSCreds(t)

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

	testDir := path.Join("/oci-repo-test", uuid.String())
	tdir := t.TempDir()

	testLog := log.NewTestLogger()
	metrics := monitoring.NewMetricsServer(false, testLog)
	defer metrics.Stop()

	storeDriver, imgStore, err := createObjectsStore(testDir, tdir, true)
	if err != nil {
		panic(err)
	}

	defer cleanupStorage(storeDriver, testDir)

	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()

		var clen int64
		_, 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() {
			// Create imgStore with linter that returns false (missing annotations)
			cacheDriver, _ := storage.Create("boltdb", cache.BoltDBDriverParameters{
				RootDir:     tdir,
				Name:        "cache",
				UseRelPaths: false,
			}, testLog)

			imgStoreWithLinter := gcs.NewImageStore(testDir, tdir, false, false, testLog, metrics,
				&mocks.MockedLint{
					LintFn: func(repo string, manifestDigest godigest.Digest, imageStore storageTypes.ImageStore) (bool, error) {
						return false, nil
					},
				}, storeDriver, cacheDriver, nil, nil)

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

		Convey("Error on mandatory annotations", func() {
			// Create imgStore with linter that returns error
			_, _ = storage.Create("boltdb", cache.BoltDBDriverParameters{
				RootDir:     tdir,
				Name:        "cache",
				UseRelPaths: false,
			}, testLog)

			imgStoreWithLinter := gcs.NewImageStore(testDir, tdir, false, false, testLog, metrics,
				&mocks.MockedLint{
					LintFn: func(repo string, manifestDigest godigest.Digest, imageStore storageTypes.ImageStore) (bool, error) {
						//nolint: err113
						return false, errors.New("linter error")
					},
				}, storeDriver, nil, nil, nil)

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

// pushRandomImageIndexGCS is a helper for GC tests.
func pushRandomImageIndexGCS(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 range 4 {
		// 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, nil)
		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, nil)
	So(err, ShouldBeNil)

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

func TestGCSGarbageCollectImageManifest(t *testing.T) {
	tskip.SkipGCS(t)
	ensureDummyGCSCreds(t)

	testLog := log.NewTestLogger()
	audit := log.NewAuditLogger("debug", "")
	metrics := monitoring.NewMetricsServer(false, testLog)
	defer metrics.Stop()

	ctx := context.Background()

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

	testDir := path.Join("/oci-repo-test", uuid.String())
	tdir := t.TempDir()

	storeDriver, imgStore, err := createObjectsStore(testDir, tdir, true)
	if err != nil {
		panic(err)
	}

	defer cleanupStorage(storeDriver, testDir)

	Convey("Garbage collect with short delay", t, func(c C) {
		gcDelay := 1 * time.Second

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

		// 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, nil)
		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, nil)
		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, nil)
		So(err, ShouldBeNil)

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

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

		hasBlob, _, err = imgStore.CheckBlob(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 = garbageCollect.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, orphanArtifactManifestDigest.String())
			So(err, ShouldNotBeNil)

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

func TestGCSGarbageCollectImageIndex(t *testing.T) {
	tskip.SkipGCS(t)
	ensureDummyGCSCreds(t)

	testLog := log.NewTestLogger()
	audit := log.NewAuditLogger("debug", "")
	metrics := monitoring.NewMetricsServer(false, testLog)
	defer metrics.Stop()

	ctx := context.Background()

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

	testDir := path.Join("/oci-repo-test", uuid.String())
	tdir := t.TempDir()

	storeDriver, imgStore, err := createObjectsStore(testDir, tdir, true)
	if err != nil {
		panic(err)
	}

	defer cleanupStorage(storeDriver, testDir)

	Convey("Garbage collect with short delay", t, func(c C) {
		gcDelay := 2 * time.Second
		imageRetentionDelay := 2 * time.Second

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

		// 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 := pushRandomImageIndexGCS(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, nil)
		So(err, ShouldBeNil)

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

		time.Sleep(2 * time.Second)

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

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

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

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

			// orphan blob
			hasBlob, _, err = imgStore.CheckBlob(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)

			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 TestGCSGarbageCollectChainedImageIndexes(t *testing.T) {
	tskip.SkipGCS(t)
	ensureDummyGCSCreds(t)

	testLog := log.NewTestLogger()
	audit := log.NewAuditLogger("debug", "")
	metrics := monitoring.NewMetricsServer(false, testLog)
	defer metrics.Stop()

	ctx := context.Background()

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

	testDir := path.Join("/oci-repo-test", uuid.String())
	tdir := t.TempDir()

	storeDriver, imgStore, err := createObjectsStore(testDir, tdir, true)
	if err != nil {
		panic(err)
	}

	defer cleanupStorage(storeDriver, testDir)

	Convey("Garbage collect with short delay", t, func() {
		gcDelay := 5 * time.Second
		imageRetentionDelay := 5 * time.Second

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

		// 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 range 4 {
			// 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, nil)
			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, nil)
			So(err, ShouldBeNil)
		}

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

		for range 3 {
			// 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, nil)
			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, nil)
		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, nil)
		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, nil)
		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, nil)
		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, nil)
		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, nil)
		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, nil)
		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 = garbageCollect.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 = garbageCollect.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 = garbageCollect.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 = garbageCollect.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 TestGCSCheckAllBlobsIntegrity(t *testing.T) {
	tskip.SkipGCS(t)
	ensureDummyGCSCreds(t)

	Convey("test with GCS storage", t, func() {
		uuid, err := guuid.NewV4()
		So(err, ShouldBeNil)

		testDir := path.Join("/oci-repo-test", uuid.String())
		tdir := t.TempDir()

		storeDriver, imgStore, err := createObjectsStore(testDir, tdir, true)
		So(err, ShouldBeNil)

		defer cleanupStorage(storeDriver, testDir)

		testLog := log.NewTestLogger()

		RunGCSCheckAllBlobsIntegrityTests(t, imgStore, gcs.New(storeDriver), testLog)
	})
}

func RunGCSCheckAllBlobsIntegrityTests( //nolint: thelper
	t *testing.T, imgStore storageTypes.ImageStore, driver storageTypes.Driver, testLog log.Logger,
) {
	Convey("Scrub only one repo", func() {
		// initialize repo
		err := imgStore.InitRepo(repoName)
		So(err, ShouldBeNil)

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

		storeCtlr := storage.StoreController{}
		storeCtlr.DefaultStore = imgStore
		So(storeCtlr.GetImageStore(repoName), ShouldResemble, imgStore)

		image := CreateRandomImage()

		err = WriteImageToFileSystem(image, repoName, "1.0", storeCtlr)
		So(err, ShouldBeNil)

		Convey("Blobs integrity not affected", func() {
			buff := bytes.NewBufferString("")

			res, err := storeCtlr.CheckAllBlobsIntegrity(context.Background())
			res.PrintScrubResults(buff)
			So(err, ShouldBeNil)

			space := regexp.MustCompile(`\s+`)
			str := space.ReplaceAllString(buff.String(), " ")
			actual := strings.TrimSpace(str)
			So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
			So(actual, ShouldContainSubstring, "test 1.0 ok")

			err = WriteMultiArchImageToFileSystem(CreateMultiarchWith().RandomImages(0).Build(), repoName, "2.0", storeCtlr)
			So(err, ShouldBeNil)

			buff = bytes.NewBufferString("")

			res, err = storeCtlr.CheckAllBlobsIntegrity(context.Background())
			res.PrintScrubResults(buff)
			So(err, ShouldBeNil)

			str = space.ReplaceAllString(buff.String(), " ")
			actual = strings.TrimSpace(str)
			So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
			So(actual, ShouldContainSubstring, "test 1.0 ok")
			So(actual, ShouldContainSubstring, "test 2.0 ok")
		})

		Convey("Blobs integrity with context done", func() {
			buff := bytes.NewBufferString("")
			ctx, cancel := context.WithCancel(context.Background())
			cancel()

			res, err := storeCtlr.CheckAllBlobsIntegrity(ctx)
			res.PrintScrubResults(buff)
			So(err, ShouldNotBeNil)

			space := regexp.MustCompile(`\s+`)
			str := space.ReplaceAllString(buff.String(), " ")
			actual := strings.TrimSpace(str)
			So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
			So(actual, ShouldNotContainSubstring, "test 1.0 ok")
		})

		Convey("Manifest integrity affected", func() {
			// get content of manifest file
			content, _, _, err := imgStore.GetImageManifest(repoName, image.ManifestDescriptor.Digest.String())
			So(err, ShouldBeNil)

			// delete content of manifest file
			manifestDig := image.ManifestDescriptor.Digest.Encoded()
			manifestFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", manifestDig)
			err = driver.Delete(manifestFile)
			So(err, ShouldBeNil)

			defer func() {
				// put manifest content back to file
				_, err = driver.WriteFile(manifestFile, content)
				So(err, ShouldBeNil)
			}()

			buff := bytes.NewBufferString("")

			res, err := storeCtlr.CheckAllBlobsIntegrity(context.Background())
			res.PrintScrubResults(buff)
			So(err, ShouldBeNil)

			space := regexp.MustCompile(`\s+`)
			str := space.ReplaceAllString(buff.String(), " ")
			actual := strings.TrimSpace(str)
			So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
			So(actual, ShouldNotContainSubstring, "affected")

			index, err := common.GetIndex(imgStore, repoName, testLog)
			So(err, ShouldBeNil)

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

			_, err = driver.WriteFile(manifestFile, []byte("invalid content"))
			So(err, ShouldBeNil)

			buff = bytes.NewBufferString("")

			res, err = storeCtlr.CheckAllBlobsIntegrity(context.Background())
			res.PrintScrubResults(buff)
			So(err, ShouldBeNil)

			str = space.ReplaceAllString(buff.String(), " ")
			actual = strings.TrimSpace(str)
			So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
			// verify error message
			So(actual, ShouldContainSubstring, fmt.Sprintf("test 1.0 affected %s invalid manifest content", manifestDig))

			index, err = common.GetIndex(imgStore, repoName, testLog)
			So(err, ShouldBeNil)

			So(len(index.Manifests), ShouldEqual, 1)
			manifestDescriptor := index.Manifests[0]

			_, _, err = storage.CheckManifestAndConfig(repoName, manifestDescriptor, []byte("invalid content"), imgStore)
			So(err, ShouldNotBeNil)
		})

		Convey("Config integrity affected", func() {
			// get content of config file
			content, err := imgStore.GetBlobContent(repoName, image.ConfigDescriptor.Digest)
			So(err, ShouldBeNil)

			// delete content of config file
			configDig := image.ConfigDescriptor.Digest.Encoded()
			configFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", configDig)
			err = driver.Delete(configFile)
			So(err, ShouldBeNil)

			defer func() {
				// put config content back to file
				_, err = driver.WriteFile(configFile, content)
				So(err, ShouldBeNil)
			}()

			buff := bytes.NewBufferString("")

			res, err := storeCtlr.CheckAllBlobsIntegrity(context.Background())
			res.PrintScrubResults(buff)
			So(err, ShouldBeNil)

			space := regexp.MustCompile(`\s+`)
			str := space.ReplaceAllString(buff.String(), " ")
			actual := strings.TrimSpace(str)
			So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
			So(actual, ShouldContainSubstring, fmt.Sprintf("test 1.0 affected %s blob not found", configDig))

			_, err = driver.WriteFile(configFile, []byte("invalid content"))
			So(err, ShouldBeNil)

			buff = bytes.NewBufferString("")

			res, err = storeCtlr.CheckAllBlobsIntegrity(context.Background())
			res.PrintScrubResults(buff)
			So(err, ShouldBeNil)

			str = space.ReplaceAllString(buff.String(), " ")
			actual = strings.TrimSpace(str)
			So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
			So(actual, ShouldContainSubstring, fmt.Sprintf("test 1.0 affected %s invalid server config", configDig))
		})

		Convey("Layers integrity affected", func() {
			// get content of layer
			content, err := imgStore.GetBlobContent(repoName, image.Manifest.Layers[0].Digest)
			So(err, ShouldBeNil)

			// delete content of layer file
			layerDig := image.Manifest.Layers[0].Digest.Encoded()
			layerFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", layerDig)
			_, err = driver.WriteFile(layerFile, []byte(" "))
			So(err, ShouldBeNil)

			defer func() {
				// put layer content back to file
				_, err = driver.WriteFile(layerFile, content)
				So(err, ShouldBeNil)
			}()

			buff := bytes.NewBufferString("")

			res, err := storeCtlr.CheckAllBlobsIntegrity(context.Background())
			res.PrintScrubResults(buff)
			So(err, ShouldBeNil)

			space := regexp.MustCompile(`\s+`)
			str := space.ReplaceAllString(buff.String(), " ")
			actual := strings.TrimSpace(str)
			So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
			So(actual, ShouldContainSubstring, fmt.Sprintf("test 1.0 affected %s bad blob digest", layerDig))
		})

		Convey("Layer not found", func() {
			// get content of layer
			digest := image.Manifest.Layers[0].Digest
			content, err := imgStore.GetBlobContent(repoName, digest)
			So(err, ShouldBeNil)

			// change layer file permissions
			layerDig := image.Manifest.Layers[0].Digest.Encoded()
			repoDir := path.Join(imgStore.RootDir(), repoName)
			layerFile := path.Join(repoDir, "/blobs/sha256", layerDig)
			err = driver.Delete(layerFile)
			So(err, ShouldBeNil)

			defer func() {
				_, err := driver.WriteFile(layerFile, content)
				So(err, ShouldBeNil)
			}()

			index, err := common.GetIndex(imgStore, repoName, testLog)
			So(err, ShouldBeNil)

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

			// get content of layer
			imageRes := storage.CheckLayers(repoName, "1.0", []ispec.Descriptor{{Digest: digest}}, imgStore)
			So(imageRes.Status, ShouldEqual, "affected")
			So(imageRes.Error, ShouldEqual, "blob not found")

			buff := bytes.NewBufferString("")

			res, err := storeCtlr.CheckAllBlobsIntegrity(context.Background())
			res.PrintScrubResults(buff)
			So(err, ShouldBeNil)

			space := regexp.MustCompile(`\s+`)
			str := space.ReplaceAllString(buff.String(), " ")
			actual := strings.TrimSpace(str)
			So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
			So(actual, ShouldContainSubstring, fmt.Sprintf("test 1.0 affected %s blob not found", layerDig))
		})

		Convey("Scrub index with missing manifest blob - graceful handling", func() {
			// Create a multiarch image with multiple manifests
			multiarchImage := CreateMultiarchWith().RandomImages(2).Build()
			err = WriteMultiArchImageToFileSystem(multiarchImage, repoName, "2.0", storeCtlr)
			So(err, ShouldBeNil)

			// Get the index to find the index manifest digest
			idx, err := common.GetIndex(imgStore, repoName, testLog)
			So(err, ShouldBeNil)

			// Find the index manifest
			var indexManifestDesc ispec.Descriptor

			for _, desc := range idx.Manifests {
				if desc.MediaType == ispec.MediaTypeImageIndex {
					indexManifestDesc = desc

					break
				}
			}

			// Get the index content to find the manifest digests within it
			indexBlob, err := imgStore.GetBlobContent(repoName, indexManifestDesc.Digest)
			So(err, ShouldBeNil)

			var indexContent ispec.Index
			err = json.Unmarshal(indexBlob, &indexContent)
			So(err, ShouldBeNil)

			// Delete one of the manifest blobs within the index (but not all)
			missingManifestDig := indexContent.Manifests[0].Digest.Encoded()
			missingManifestFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", missingManifestDig)
			err = driver.Delete(missingManifestFile)
			So(err, ShouldBeNil)

			buff := bytes.NewBufferString("")

			res, err := storeCtlr.CheckAllBlobsIntegrity(context.Background())
			res.PrintScrubResults(buff)
			So(err, ShouldBeNil)

			space := regexp.MustCompile(`\s+`)
			str := space.ReplaceAllString(buff.String(), " ")
			actual := strings.TrimSpace(str)

			// Should mark the index as affected due to missing manifest
			So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
			So(actual, ShouldContainSubstring, "test 2.0 affected")
			// Should continue processing and report the missing manifest
			So(actual, ShouldContainSubstring, missingManifestDig)
		})

		Convey("Scrub index with non-missing error on manifest blob via file permissions", func() {
			// Skip for non-local storage
			if driver.Name() != storageConstants.LocalStorageDriverName {
				return
			}

			// Create a multiarch image with multiple manifests
			multiarchImage := CreateMultiarchWith().RandomImages(2).Build()
			err = WriteMultiArchImageToFileSystem(multiarchImage, repoName, "2.1", storeCtlr)
			So(err, ShouldBeNil)

			// Get the index to find the index manifest digest
			idx, err := common.GetIndex(imgStore, repoName, testLog)
			So(err, ShouldBeNil)

			// Find the index manifest
			var indexManifestDesc ispec.Descriptor

			for _, desc := range idx.Manifests {
				if desc.MediaType == ispec.MediaTypeImageIndex {
					indexManifestDesc = desc

					break
				}
			}

			// Get the index content to find the manifest digests within it
			indexBlob, err := imgStore.GetBlobContent(repoName, indexManifestDesc.Digest)
			So(err, ShouldBeNil)

			var indexContent ispec.Index
			err = json.Unmarshal(indexBlob, &indexContent)
			So(err, ShouldBeNil)

			// Remove read permissions on one of the manifest blobs to cause a permission denied error (non-missing error)
			manifestDig := indexContent.Manifests[0].Digest.Encoded()
			manifestFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", manifestDig)
			err = os.Chmod(manifestFile, 0o000)
			So(err, ShouldBeNil)

			// Restore permissions after test
			defer func() {
				_ = os.Chmod(manifestFile, 0o644)
			}()

			buff := bytes.NewBufferString("")

			res, err := storeCtlr.CheckAllBlobsIntegrity(context.Background())
			res.PrintScrubResults(buff)
			So(err, ShouldBeNil)

			space := regexp.MustCompile(`\s+`)
			str := space.ReplaceAllString(buff.String(), " ")
			actual := strings.TrimSpace(str)

			// Should mark the index as affected due to non-missing error on manifest
			So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
			So(actual, ShouldContainSubstring, "test 2.1 affected")
			// Should report the manifest digest as affected blob
			So(actual, ShouldContainSubstring, manifestDig)
			// Should have "bad blob digest" error
			So(actual, ShouldContainSubstring, "bad blob digest")
		})

		Convey("Scrub index", func() {
			newImage := CreateRandomImage()
			newManifestDigest := newImage.ManifestDescriptor.Digest

			err = WriteImageToFileSystem(newImage, repoName, "2.0", storeCtlr)
			So(err, ShouldBeNil)

			idx, err := common.GetIndex(imgStore, repoName, testLog)
			So(err, ShouldBeNil)

			manifestDescriptor, ok := common.GetManifestDescByReference(idx, image.ManifestDescriptor.Digest.String())
			So(ok, ShouldBeTrue)

			var index ispec.Index
			index.SchemaVersion = 2
			index.Subject = &manifestDescriptor
			index.Manifests = []ispec.Descriptor{
				{
					MediaType: ispec.MediaTypeImageManifest,
					Digest:    newManifestDigest,
					Size:      newImage.ManifestDescriptor.Size,
				},
			}

			indexBlob, err := json.Marshal(index)
			So(err, ShouldBeNil)
			indexDigest, _, err := imgStore.PutImageManifest(repoName, "", ispec.MediaTypeImageIndex, indexBlob, nil)
			So(err, ShouldBeNil)

			buff := bytes.NewBufferString("")

			res, err := storeCtlr.CheckAllBlobsIntegrity(context.Background())
			res.PrintScrubResults(buff)
			So(err, ShouldBeNil)

			space := regexp.MustCompile(`\s+`)
			str := space.ReplaceAllString(buff.String(), " ")
			actual := strings.TrimSpace(str)
			So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
			So(actual, ShouldContainSubstring, "test 1.0 ok")
			So(actual, ShouldContainSubstring, "test ok")

			// test scrub context done
			buff = bytes.NewBufferString("")

			ctx, cancel := context.WithCancel(context.Background())
			cancel()

			res, err = storeCtlr.CheckAllBlobsIntegrity(ctx)
			res.PrintScrubResults(buff)
			So(err, ShouldNotBeNil)

			str = space.ReplaceAllString(buff.String(), " ")
			actual = strings.TrimSpace(str)
			So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
			So(actual, ShouldNotContainSubstring, "test 1.0 ok")
			So(actual, ShouldNotContainSubstring, "test ok")

			// test scrub index - errors
			manifestFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", newManifestDigest.Encoded())
			_, err = driver.WriteFile(manifestFile, []byte("invalid content"))
			So(err, ShouldBeNil)

			buff = bytes.NewBufferString("")

			res, err = storeCtlr.CheckAllBlobsIntegrity(context.Background())
			res.PrintScrubResults(buff)
			So(err, ShouldBeNil)

			str = space.ReplaceAllString(buff.String(), " ")
			actual = strings.TrimSpace(str)
			So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
			So(actual, ShouldContainSubstring, "test affected")

			// delete content of manifest file
			err = driver.Delete(manifestFile)
			So(err, ShouldBeNil)

			buff = bytes.NewBufferString("")

			res, err = storeCtlr.CheckAllBlobsIntegrity(context.Background())
			res.PrintScrubResults(buff)
			So(err, ShouldBeNil)

			str = space.ReplaceAllString(buff.String(), " ")
			actual = strings.TrimSpace(str)
			So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
			So(actual, ShouldContainSubstring, "test affected")

			indexFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", indexDigest.Encoded())
			err = driver.Delete(indexFile)
			So(err, ShouldBeNil)

			buff = bytes.NewBufferString("")

			res, err = storeCtlr.CheckAllBlobsIntegrity(context.Background())
			res.PrintScrubResults(buff)
			So(err, ShouldBeNil)

			str = space.ReplaceAllString(buff.String(), " ")
			actual = strings.TrimSpace(str)
			So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
			So(actual, ShouldContainSubstring, "test 1.0 ok")
			So(actual, ShouldNotContainSubstring, "test affected")

			index.Manifests[0].MediaType = "invalid"
			indexBlob, err = json.Marshal(index)
			So(err, ShouldBeNil)

			_, err = driver.WriteFile(indexFile, indexBlob)
			So(err, ShouldBeNil)

			buff = bytes.NewBufferString("")

			res, err = storeCtlr.CheckAllBlobsIntegrity(context.Background())
			res.PrintScrubResults(buff)
			So(err, ShouldBeNil)

			_, _, err = storage.CheckManifestAndConfig(repoName, index.Manifests[0], []byte{}, imgStore)
			So(err, ShouldNotBeNil)
			So(err, ShouldEqual, zerr.ErrBadManifest)

			str = space.ReplaceAllString(buff.String(), " ")
			actual = strings.TrimSpace(str)
			So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
			So(actual, ShouldContainSubstring, "test affected")

			_, err = driver.WriteFile(indexFile, []byte("invalid cotent"))
			So(err, ShouldBeNil)

			defer func() {
				err := driver.Delete(indexFile)
				So(err, ShouldBeNil)
			}()

			buff = bytes.NewBufferString("")

			res, err = storeCtlr.CheckAllBlobsIntegrity(context.Background())
			res.PrintScrubResults(buff)
			So(err, ShouldBeNil)

			str = space.ReplaceAllString(buff.String(), " ")
			actual = strings.TrimSpace(str)
			So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
			So(actual, ShouldContainSubstring, "test affected")
		})

		Convey("Manifest not found", func() {
			// delete manifest file
			manifestDig := image.ManifestDescriptor.Digest.Encoded()
			manifestFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", manifestDig)
			err = driver.Delete(manifestFile)
			So(err, ShouldBeNil)

			buff := bytes.NewBufferString("")

			res, err := storeCtlr.CheckAllBlobsIntegrity(context.Background())
			res.PrintScrubResults(buff)
			So(err, ShouldBeNil)

			space := regexp.MustCompile(`\s+`)
			str := space.ReplaceAllString(buff.String(), " ")
			actual := strings.TrimSpace(str)
			So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
			So(actual, ShouldNotContainSubstring, fmt.Sprintf("test 1.0 affected %s blob not found", manifestDig))

			index, err := common.GetIndex(imgStore, repoName, testLog)
			So(err, ShouldBeNil)

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

		Convey("use the result of an already scrubed manifest which is the subject of the current manifest", func() {
			index, err := common.GetIndex(imgStore, repoName, testLog)
			So(err, ShouldBeNil)

			manifestDescriptor, ok := common.GetManifestDescByReference(index, image.ManifestDescriptor.Digest.String())
			So(ok, ShouldBeTrue)

			err = WriteImageToFileSystem(CreateDefaultImageWith().Subject(&manifestDescriptor).Build(),
				repoName, "0.0.1", storeCtlr)
			So(err, ShouldBeNil)

			buff := bytes.NewBufferString("")

			res, err := storeCtlr.CheckAllBlobsIntegrity(context.Background())
			res.PrintScrubResults(buff)
			So(err, ShouldBeNil)

			space := regexp.MustCompile(`\s+`)
			str := space.ReplaceAllString(buff.String(), " ")
			actual := strings.TrimSpace(str)
			So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
			So(actual, ShouldContainSubstring, "test 1.0 ok")
			So(actual, ShouldContainSubstring, "test 0.0.1 ok")
		})

		Convey("preserve affected status when CheckLayers would overwrite it", func() {
			// Create an image with a subject
			index, err := common.GetIndex(imgStore, repoName, testLog)
			So(err, ShouldBeNil)

			manifestDescriptor, ok := common.GetManifestDescByReference(index, image.ManifestDescriptor.Digest.String())
			So(ok, ShouldBeTrue)

			subjectImage := CreateDefaultImageWith().Subject(&manifestDescriptor).Build()
			err = WriteImageToFileSystem(subjectImage, repoName, "0.0.3", storeCtlr)
			So(err, ShouldBeNil)

			// Delete the subject manifest to mark it as affected
			subjectManifestDig := manifestDescriptor.Digest.Encoded()
			subjectManifestFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", subjectManifestDig)
			err = driver.Delete(subjectManifestFile)
			So(err, ShouldBeNil)

			buff := bytes.NewBufferString("")

			res, err := storeCtlr.CheckAllBlobsIntegrity(context.Background())
			res.PrintScrubResults(buff)
			So(err, ShouldBeNil)

			space := regexp.MustCompile(`\s+`)
			str := space.ReplaceAllString(buff.String(), " ")
			actual := strings.TrimSpace(str)

			// The manifest with the missing subject should be marked as affected
			So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
			So(actual, ShouldContainSubstring, "test 0.0.3 affected")
			// Even if CheckLayers would pass, the affected status from the missing subject should be preserved
			So(actual, ShouldContainSubstring, subjectManifestDig)
		})

		Convey("the subject of the current manifest doesn't exist", func() {
			index, err := common.GetIndex(imgStore, repoName, testLog)
			So(err, ShouldBeNil)

			manifestDescriptor, ok := common.GetManifestDescByReference(index, image.ManifestDescriptor.Digest.String())
			So(ok, ShouldBeTrue)

			err = WriteImageToFileSystem(CreateDefaultImageWith().Subject(&manifestDescriptor).Build(),
				repoName, "0.0.2", storeCtlr)
			So(err, ShouldBeNil)

			// get content of manifest file
			content, _, _, err := imgStore.GetImageManifest(repoName, manifestDescriptor.Digest.String())
			So(err, ShouldBeNil)

			// delete content of manifest file
			manifestDig := image.ManifestDescriptor.Digest.Encoded()
			manifestFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", manifestDig)
			err = driver.Delete(manifestFile)
			So(err, ShouldBeNil)

			defer func() {
				// put manifest content back to file
				_, err = driver.WriteFile(manifestFile, content)
				So(err, ShouldBeNil)
			}()

			buff := bytes.NewBufferString("")

			res, err := storeCtlr.CheckAllBlobsIntegrity(context.Background())
			res.PrintScrubResults(buff)
			So(err, ShouldBeNil)

			space := regexp.MustCompile(`\s+`)
			str := space.ReplaceAllString(buff.String(), " ")
			actual := strings.TrimSpace(str)
			So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
			So(actual, ShouldContainSubstring, "test 0.0.2 affected")
		})

		Convey("the subject of the current index doesn't exist", func() {
			index, err := common.GetIndex(imgStore, repoName, testLog)
			So(err, ShouldBeNil)

			manifestDescriptor, ok := common.GetManifestDescByReference(index, image.ManifestDescriptor.Digest.String())
			So(ok, ShouldBeTrue)

			err = WriteMultiArchImageToFileSystem(CreateMultiarchWith().RandomImages(1).Subject(&manifestDescriptor).Build(),
				repoName, "0.0.2", storeCtlr)
			So(err, ShouldBeNil)

			// get content of manifest file
			content, _, _, err := imgStore.GetImageManifest(repoName, manifestDescriptor.Digest.String())
			So(err, ShouldBeNil)

			// delete content of manifest file
			manifestDig := image.ManifestDescriptor.Digest.Encoded()
			manifestFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", manifestDig)
			err = driver.Delete(manifestFile)
			So(err, ShouldBeNil)

			defer func() {
				// put manifest content back to file
				_, err = driver.WriteFile(manifestFile, content)
				So(err, ShouldBeNil)
			}()

			buff := bytes.NewBufferString("")

			res, err := storeCtlr.CheckAllBlobsIntegrity(context.Background())
			res.PrintScrubResults(buff)
			So(err, ShouldBeNil)

			space := regexp.MustCompile(`\s+`)
			str := space.ReplaceAllString(buff.String(), " ")
			actual := strings.TrimSpace(str)
			So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
			So(actual, ShouldContainSubstring, "test 0.0.2 affected")
		})

		Convey("test errors", func() {
			mockedImgStore := mocks.MockedImageStore{
				GetRepositoriesFn: func() ([]string, error) {
					return []string{repoName}, nil
				},
				ValidateRepoFn: func(name string) (bool, error) {
					return false, nil
				},
			}

			storeController := storage.StoreController{}
			storeController.DefaultStore = mockedImgStore

			_, err := storeController.CheckAllBlobsIntegrity(context.Background())
			So(err, ShouldNotBeNil)
			So(err, ShouldEqual, zerr.ErrRepoBadLayout)

			mockedImgStore = mocks.MockedImageStore{
				GetRepositoriesFn: func() ([]string, error) {
					return []string{repoName}, nil
				},
				GetIndexContentFn: func(repo string) ([]byte, error) {
					return []byte{}, errUnexpectedError
				},
			}

			storeController.DefaultStore = mockedImgStore

			_, err = storeController.CheckAllBlobsIntegrity(context.Background())
			So(err, ShouldNotBeNil)
			So(err, ShouldEqual, errUnexpectedError)

			manifestDigest := godigest.FromString("abcd")

			mockedImgStore = mocks.MockedImageStore{
				GetRepositoriesFn: func() ([]string, error) {
					return []string{repoName}, nil
				},
				GetIndexContentFn: func(repo string) ([]byte, error) {
					var index ispec.Index
					index.SchemaVersion = 2
					index.Manifests = []ispec.Descriptor{
						{
							MediaType:   "InvalidMediaType",
							Digest:      manifestDigest,
							Size:        int64(100),
							Annotations: map[string]string{ispec.AnnotationRefName: "1.0"},
						},
					}

					return json.Marshal(index)
				},
			}

			storeController.DefaultStore = mockedImgStore

			res, err := storeController.CheckAllBlobsIntegrity(context.Background())
			So(err, ShouldBeNil)

			buff := bytes.NewBufferString("")
			res.PrintScrubResults(buff)

			space := regexp.MustCompile(`\s+`)
			str := space.ReplaceAllString(buff.String(), " ")
			actual := strings.TrimSpace(str)
			So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
			So(actual, ShouldContainSubstring, fmt.Sprintf("%s 1.0 affected %s invalid manifest content",
				repoName, manifestDigest.Encoded()))
		})

		Convey("scrub with non-missing error on manifest subject blob via file permissions", func() {
			// Skip for non-local storage
			if driver.Name() != storageConstants.LocalStorageDriverName {
				return
			}

			index, err := common.GetIndex(imgStore, repoName, testLog)
			So(err, ShouldBeNil)

			manifestDescriptor, ok := common.GetManifestDescByReference(index, image.ManifestDescriptor.Digest.String())
			So(ok, ShouldBeTrue)

			// Create an image with a subject
			subjectImage := CreateDefaultImageWith().Subject(&manifestDescriptor).Build()
			err = WriteImageToFileSystem(subjectImage, repoName, "0.0.6", storeCtlr)
			So(err, ShouldBeNil)

			// Get the subject manifest digest
			subjectManifestDig := manifestDescriptor.Digest.Encoded()
			subjectManifestFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", subjectManifestDig)

			// Remove read permissions to cause a permission denied error (non-missing error)
			err = os.Chmod(subjectManifestFile, 0o000)
			So(err, ShouldBeNil)

			// Restore permissions after test
			defer func() {
				_ = os.Chmod(subjectManifestFile, 0o644)
			}()

			buff := bytes.NewBufferString("")

			res, err := storeCtlr.CheckAllBlobsIntegrity(context.Background())
			res.PrintScrubResults(buff)
			So(err, ShouldBeNil)

			space := regexp.MustCompile(`\s+`)
			str := space.ReplaceAllString(buff.String(), " ")
			actual := strings.TrimSpace(str)

			// Should mark the manifest as affected due to non-missing error on subject
			So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
			So(actual, ShouldContainSubstring, "test 0.0.6 affected")
			// Should report the subject digest as affected blob
			So(actual, ShouldContainSubstring, subjectManifestDig)
			// Should have "bad blob digest" error
			So(actual, ShouldContainSubstring, "bad blob digest")
		})

		Convey("scrub with non-missing error on index subject blob via file permissions", func() {
			// Skip for non-local storage
			if driver.Name() != storageConstants.LocalStorageDriverName {
				return
			}

			index, err := common.GetIndex(imgStore, repoName, testLog)
			So(err, ShouldBeNil)

			manifestDescriptor, ok := common.GetManifestDescByReference(index, image.ManifestDescriptor.Digest.String())
			So(ok, ShouldBeTrue)

			// Create a multiarch image with a subject
			err = WriteMultiArchImageToFileSystem(CreateMultiarchWith().RandomImages(1).Subject(&manifestDescriptor).Build(),
				repoName, "0.0.7", storeCtlr)
			So(err, ShouldBeNil)

			// Get the subject manifest digest
			subjectManifestDig := manifestDescriptor.Digest.Encoded()
			subjectManifestFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", subjectManifestDig)

			// Remove read permissions to cause a permission denied error (non-missing error)
			err = os.Chmod(subjectManifestFile, 0o000)
			So(err, ShouldBeNil)

			// Restore permissions after test
			defer func() {
				_ = os.Chmod(subjectManifestFile, 0o644)
			}()

			buff := bytes.NewBufferString("")

			res, err := storeCtlr.CheckAllBlobsIntegrity(context.Background())
			res.PrintScrubResults(buff)
			So(err, ShouldBeNil)

			space := regexp.MustCompile(`\s+`)
			str := space.ReplaceAllString(buff.String(), " ")
			actual := strings.TrimSpace(str)

			// Should mark the index as affected due to non-missing error on subject
			So(actual, ShouldContainSubstring, "REPOSITORY TAG STATUS AFFECTED BLOB ERROR")
			So(actual, ShouldContainSubstring, "test 0.0.7 affected")
			// Should report the subject digest as affected blob
			So(actual, ShouldContainSubstring, subjectManifestDig)
			// Should have "bad blob digest" error
			So(actual, ShouldContainSubstring, "bad blob digest")
		})
	})
}
