package cache_test

import (
	"errors"
	"fmt"
	"path"
	"testing"
	"time"

	"github.com/alicebob/miniredis/v2"
	"github.com/go-redis/redismock/v9"
	"github.com/redis/go-redis/v9"
	. "github.com/smartystreets/goconvey/convey"

	zerr "zotregistry.dev/zot/errors"
	"zotregistry.dev/zot/pkg/log"
	"zotregistry.dev/zot/pkg/storage"
	"zotregistry.dev/zot/pkg/storage/cache"
	"zotregistry.dev/zot/pkg/storage/constants"
	test "zotregistry.dev/zot/pkg/test/common"
)

var ErrTestError = errors.New("TestError")

func TestRedisCache(t *testing.T) {
	miniRedis := miniredis.RunT(t)

	Convey("Make a new cache", t, func() {
		dir := t.TempDir()

		log := log.NewLogger("debug", "")
		So(log, ShouldNotBeNil)

		cacheDriver, err := storage.Create("redis", "failTypeAssertion", log)
		So(cacheDriver, ShouldBeNil)
		So(err, ShouldNotBeNil)

		connOpts, _ := redis.ParseURL("redis://" + miniRedis.Addr())
		client := redis.NewClient(connOpts)

		cacheDriver, err = storage.Create("redis",
			cache.RedisDriverParameters{client, dir, true, "zot"}, log)
		So(cacheDriver, ShouldNotBeNil)
		So(err, ShouldBeNil)

		name := cacheDriver.Name()
		So(name, ShouldEqual, "redis")

		val, err := cacheDriver.GetBlob("key")
		So(err, ShouldEqual, zerr.ErrCacheMiss)
		So(val, ShouldBeEmpty)

		exists := cacheDriver.HasBlob("key", path.Join(dir, "value"))
		So(exists, ShouldBeFalse)

		exists = cacheDriver.HasBlob("key", "value")
		So(exists, ShouldBeFalse)

		err = cacheDriver.PutBlob("key", path.Join(dir, "value"))
		So(err, ShouldBeNil)

		err = cacheDriver.PutBlob("key", "value")
		So(err, ShouldNotBeNil)

		exists = cacheDriver.HasBlob("key", path.Join(dir, "value"))
		So(exists, ShouldBeTrue)

		val, err = cacheDriver.GetBlob("key")
		So(err, ShouldBeNil)
		So(val, ShouldNotBeEmpty)

		err = cacheDriver.DeleteBlob("bogusKey", "bogusValue")
		So(err, ShouldEqual, zerr.ErrCacheMiss)

		err = cacheDriver.DeleteBlob("key", "bogusValue")
		So(err, ShouldBeNil)

		// try to insert empty path
		err = cacheDriver.PutBlob("key", "")
		So(err, ShouldNotBeNil)
		So(err, ShouldEqual, zerr.ErrEmptyValue)

		connOpts, _ = redis.ParseURL("redis://" + miniRedis.Addr() + "/5")
		client = redis.NewClient(connOpts)

		cacheDriver, err = storage.Create("redis",
			cache.RedisDriverParameters{client, t.TempDir(), false, "zot"}, log)
		So(cacheDriver, ShouldNotBeNil)
		So(err, ShouldBeNil)

		err = cacheDriver.PutBlob("key1", "originalBlobPath")
		So(err, ShouldBeNil)

		err = cacheDriver.PutBlob("key1", "duplicateBlobPath")
		So(err, ShouldBeNil)

		val, err = cacheDriver.GetBlob("key1")
		So(val, ShouldEqual, "originalBlobPath")
		So(err, ShouldBeNil)

		err = cacheDriver.DeleteBlob("key1", "duplicateBlobPath")
		So(err, ShouldBeNil)

		val, err = cacheDriver.GetBlob("key1")
		So(val, ShouldEqual, "originalBlobPath")
		So(err, ShouldBeNil)

		err = cacheDriver.PutBlob("key1", "duplicateBlobPath")
		So(err, ShouldBeNil)

		err = cacheDriver.DeleteBlob("key1", "originalBlobPath")
		So(err, ShouldBeNil)

		val, err = cacheDriver.GetBlob("key1")
		So(val, ShouldEqual, "duplicateBlobPath")
		So(err, ShouldBeNil)

		err = cacheDriver.DeleteBlob("key1", "duplicateBlobPath")
		So(err, ShouldBeNil)

		// should be empty
		val, err = cacheDriver.GetBlob("key1")
		So(err, ShouldNotBeNil)
		So(val, ShouldBeEmpty)

		// try to add three same values
		err = cacheDriver.PutBlob("key2", "duplicate")
		So(err, ShouldBeNil)

		err = cacheDriver.PutBlob("key2", "duplicate")
		So(err, ShouldBeNil)

		err = cacheDriver.PutBlob("key2", "duplicate")
		So(err, ShouldBeNil)

		val, err = cacheDriver.GetBlob("key2")
		So(val, ShouldEqual, "duplicate")
		So(err, ShouldBeNil)

		err = cacheDriver.DeleteBlob("key2", "duplicate")
		So(err, ShouldBeNil)

		// should be empty
		val, err = cacheDriver.GetBlob("key2")
		So(err, ShouldNotBeNil)
		So(val, ShouldBeEmpty)
	})

	Convey("Test cache.GetAllBlos()", t, func() {
		dir := t.TempDir()

		log := log.NewLogger("debug", "")
		So(log, ShouldNotBeNil)

		connOpts, _ := redis.ParseURL("redis://" + miniRedis.Addr())
		client := redis.NewClient(connOpts)

		cacheDriver, err := storage.Create("redis",
			cache.RedisDriverParameters{client, dir, true, "zot"}, log)
		So(cacheDriver, ShouldNotBeNil)
		So(err, ShouldBeNil)

		name := cacheDriver.Name()
		So(name, ShouldEqual, "redis")

		blobs, err := cacheDriver.GetAllBlobs("digest")
		So(err, ShouldEqual, zerr.ErrCacheMiss)
		So(blobs, ShouldBeNil)

		err = cacheDriver.PutBlob("digest", path.Join(dir, "first"))
		So(err, ShouldBeNil)

		err = cacheDriver.PutBlob("digest", path.Join(dir, "second"))
		So(err, ShouldBeNil)

		err = cacheDriver.PutBlob("digest", path.Join(dir, "third"))
		So(err, ShouldBeNil)

		blobs, err = cacheDriver.GetAllBlobs("digest")
		So(err, ShouldBeNil)

		So(blobs, ShouldResemble, []string{"first", "second", "third"})

		err = cacheDriver.DeleteBlob("digest", path.Join(dir, "first"))
		So(err, ShouldBeNil)

		blobs, err = cacheDriver.GetAllBlobs("digest")
		So(err, ShouldBeNil)
		So(len(blobs), ShouldEqual, 2)
		So(blobs, ShouldContain, "second")
		So(blobs, ShouldContain, "third")

		err = cacheDriver.DeleteBlob("digest", path.Join(dir, "third"))
		So(err, ShouldBeNil)

		blobs, err = cacheDriver.GetAllBlobs("digest")
		So(err, ShouldBeNil)

		So(blobs, ShouldResemble, []string{"second"})
	})
}

func TestRedisCacheError(t *testing.T) {
	Convey("Make a new cache", t, func() {
		dir := t.TempDir()
		redisURL := "redis://127.0.0.1:" + test.GetFreePort()
		connOpts, _ := redis.ParseURL(redisURL)
		brokenClient := redis.NewClient(connOpts)

		log := log.NewLogger("debug", "")
		So(log, ShouldNotBeNil)

		// redis server is not running
		cacheDriver, err := storage.Create("redis",
			cache.RedisDriverParameters{brokenClient, dir, true, "zot"}, log)
		So(err, ShouldNotBeNil)
		So(cacheDriver, ShouldBeNil)
	})

	Convey("Redis unreachable", t, func() {
		miniRedis := miniredis.RunT(t)
		dir := t.TempDir()

		log := log.NewLogger("debug", "")
		So(log, ShouldNotBeNil)

		connOpts, _ := redis.ParseURL("redis://" + miniRedis.Addr())
		workingClient := redis.NewClient(connOpts)

		redisURL := "redis://127.0.0.1:" + test.GetFreePort() // must not match miniRedis.Addr()
		connOpts, _ = redis.ParseURL(redisURL)
		brokenClient := redis.NewClient(connOpts)

		cacheDriver, err := cache.NewRedisCache(
			cache.RedisDriverParameters{workingClient, dir, false, "zot"}, log)
		So(cacheDriver, ShouldNotBeNil)
		So(err, ShouldBeNil)

		// replace the working driver with the broken one
		cacheDriver.SetClient(brokenClient)

		err = cacheDriver.PutBlob("key", "val")
		So(err, ShouldNotBeNil)

		found := cacheDriver.HasBlob("key", "val")
		So(found, ShouldEqual, false)

		_, err = cacheDriver.GetBlob("key")
		So(err, ShouldNotBeNil)

		_, err = cacheDriver.GetAllBlobs("key")
		So(err, ShouldNotBeNil)

		err = cacheDriver.DeleteBlob("key", "val")
		So(err, ShouldNotBeNil)
	})
}

func TestRedisMocked(t *testing.T) {
	Convey("Redis tests using mocks", t, func() {
		dir := t.TempDir()

		log := log.NewLogger("debug", "")
		So(log, ShouldNotBeNil)

		tests := []cache.RedisDriverParameters{
			{
				RootDir:     dir,
				UseRelPaths: true,
			}, {
				RootDir:     dir,
				UseRelPaths: false,
			}, {
				RootDir:     dir,
				UseRelPaths: true,
				KeyPrefix:   "someprefix",
			}, {
				RootDir:     dir,
				UseRelPaths: true,
				KeyPrefix:   "zot",
			},
		}

		for i, redisDriverParams := range tests {
			testID := fmt.Sprintf(" %d", i)

			keyPrefix := redisDriverParams.KeyPrefix
			if len(keyPrefix) == 0 {
				// check default
				keyPrefix = "zot"
			}
			keyPrefix += ":"

			// depending on UseRelPaths value we check the relative or absolute value
			// in results using path.Join(pathPrefix, path) in both cases
			pathPrefix := ""
			if !redisDriverParams.UseRelPaths {
				pathPrefix = redisDriverParams.RootDir
			}

			Convey("PutBlob HExists error"+testID, func() {
				// initialize mock client
				cacheDB, mock := redismock.NewClientMock()
				redisDriverParams.Client = cacheDB

				mock.ExpectPing().SetVal("OK")
				cacheDriver, err := cache.NewRedisCache(redisDriverParams, log)
				So(cacheDriver, ShouldNotBeNil)
				So(err, ShouldBeNil)

				mock.Regexp().ExpectSetNX(keyPrefix+"locks:key", `.*`, 8*time.Second).SetVal(true)
				mock.ExpectHExists(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
					SetErr(ErrTestError)

				err = cacheDriver.PutBlob("key", path.Join(dir, "val"))
				So(err, ShouldEqual, ErrTestError)

				err = mock.ExpectationsWereMet()
				So(err, ShouldBeNil)
			})

			Convey("PutBlob HSet error"+testID, func() {
				// initialize mock client
				cacheDB, mock := redismock.NewClientMock()
				redisDriverParams.Client = cacheDB

				mock.ExpectPing().SetVal("OK")
				cacheDriver, err := cache.NewRedisCache(redisDriverParams, log)
				So(cacheDriver, ShouldNotBeNil)
				So(err, ShouldBeNil)

				mock.Regexp().ExpectSetNX(keyPrefix+"locks:key", `.*`, 8*time.Second).SetVal(true)
				mock.ExpectHExists(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
					SetVal(false)
				mock.ExpectTxPipeline()
				mock.ExpectHSet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key",
					path.Join(pathPrefix, "val")).SetErr(ErrTestError)

				err = cacheDriver.PutBlob("key", path.Join(dir, "val"))
				So(err, ShouldEqual, ErrTestError)

				err = mock.ExpectationsWereMet()
				So(err, ShouldBeNil)
			})

			Convey("PutBlob SAdd error"+testID, func() {
				// initialize mock client
				cacheDB, mock := redismock.NewClientMock()
				redisDriverParams.Client = cacheDB

				mock.ExpectPing().SetVal("OK")
				cacheDriver, err := cache.NewRedisCache(redisDriverParams, log)
				So(cacheDriver, ShouldNotBeNil)
				So(err, ShouldBeNil)

				mock.Regexp().ExpectSetNX(keyPrefix+"locks:key", `.*`, 8*time.Second).SetVal(true)
				mock.ExpectHExists(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
					SetVal(false)
				mock.ExpectTxPipeline()
				mock.ExpectHSet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key",
					path.Join(pathPrefix, "val")).SetVal(1)
				mock.ExpectSAdd(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
					path.Join(pathPrefix, "val")).SetErr(ErrTestError)

				err = cacheDriver.PutBlob("key", path.Join(dir, "val"))
				So(err, ShouldEqual, ErrTestError)

				err = mock.ExpectationsWereMet()
				So(err, ShouldBeNil)
			})

			Convey("PutBlob succeeds original bucket is created"+testID, func() {
				// initialize mock client
				cacheDB, mock := redismock.NewClientMock()
				redisDriverParams.Client = cacheDB

				mock.ExpectPing().SetVal("OK")
				cacheDriver, err := cache.NewRedisCache(redisDriverParams, log)
				So(cacheDriver, ShouldNotBeNil)
				So(err, ShouldBeNil)

				mock.Regexp().ExpectSetNX(keyPrefix+"locks:key", `.*`, 8*time.Second).SetVal(true)
				mock.ExpectHExists(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
					SetVal(false)
				mock.ExpectTxPipeline()
				mock.ExpectHSet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key",
					path.Join(pathPrefix, "val")).SetVal(1)
				mock.ExpectSAdd(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
					path.Join(pathPrefix, "val")).SetVal(1)
				mock.ExpectTxPipelineExec()

				err = cacheDriver.PutBlob("key", path.Join(dir, "val"))
				So(err, ShouldBeNil)

				err = mock.ExpectationsWereMet()
				So(err, ShouldBeNil)
			})

			Convey("PutBlob succeeds original bucket is reused"+testID, func() {
				// initialize mock client
				cacheDB, mock := redismock.NewClientMock()
				redisDriverParams.Client = cacheDB

				mock.ExpectPing().SetVal("OK")
				cacheDriver, err := cache.NewRedisCache(redisDriverParams, log)
				So(cacheDriver, ShouldNotBeNil)
				So(err, ShouldBeNil)

				mock.Regexp().ExpectSetNX(keyPrefix+"locks:key", `.*`, 8*time.Second).SetVal(true)
				mock.ExpectHExists(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
					SetVal(true)
				mock.ExpectTxPipeline()
				mock.ExpectSAdd(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
					path.Join(pathPrefix, "val")).SetVal(1)
				mock.ExpectTxPipelineExec()

				err = cacheDriver.PutBlob("key", path.Join(dir, "val"))
				So(err, ShouldBeNil)

				err = mock.ExpectationsWereMet()
				So(err, ShouldBeNil)
			})

			Convey("SMembers error in GetAllBlobs"+testID, func() {
				// initialize mock client
				cacheDB, mock := redismock.NewClientMock()
				redisDriverParams.Client = cacheDB

				mock.ExpectPing().SetVal("OK")
				cacheDriver, err := cache.NewRedisCache(redisDriverParams, log)
				So(cacheDriver, ShouldNotBeNil)
				So(err, ShouldBeNil)

				mock.ExpectHGet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
					SetVal(path.Join(pathPrefix, "val"))
				mock.ExpectSMembers(keyPrefix + constants.BlobsCache + ":" + constants.DuplicatesBucket + ":key").
					SetErr(ErrTestError)

				_, err = cacheDriver.GetAllBlobs("key")
				So(err, ShouldEqual, ErrTestError)

				err = mock.ExpectationsWereMet()
				So(err, ShouldBeNil)
			})

			Convey("GetAllBlobs succeeds"+testID, func() {
				// initialize mock client
				cacheDB, mock := redismock.NewClientMock()
				redisDriverParams.Client = cacheDB

				mock.ExpectPing().SetVal("OK")
				cacheDriver, err := cache.NewRedisCache(redisDriverParams, log)
				So(cacheDriver, ShouldNotBeNil)
				So(err, ShouldBeNil)

				mock.Regexp().ExpectSetNX(keyPrefix+"locks:key", `.*`, 8*time.Second).SetVal(true)
				mock.ExpectHExists(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
					SetVal(false)
				mock.ExpectTxPipeline()
				mock.ExpectHSet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key",
					path.Join(pathPrefix, "val1")).SetVal(1)
				mock.ExpectSAdd(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
					path.Join(pathPrefix, "val1")).SetVal(1)
				mock.ExpectTxPipelineExec()

				err = cacheDriver.PutBlob("key", path.Join(dir, "val1"))
				So(err, ShouldBeNil)

				mock.Regexp().ExpectSetNX(keyPrefix+"locks:key", `.*`, 8*time.Second).SetVal(true)
				mock.ExpectHExists(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
					SetVal(false)
				mock.ExpectTxPipeline()
				mock.ExpectHSet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key",
					path.Join(pathPrefix, "val2")).SetVal(1)
				mock.ExpectSAdd(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
					path.Join(pathPrefix, "val2")).SetVal(1)
				mock.ExpectTxPipelineExec()

				err = cacheDriver.PutBlob("key", path.Join(dir, "val2"))
				So(err, ShouldBeNil)

				mock.ExpectHGet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
					SetVal(path.Join(pathPrefix, "val1"))
				mock.ExpectSMembers(keyPrefix + constants.BlobsCache + ":" + constants.DuplicatesBucket + ":key").
					SetVal([]string{path.Join(pathPrefix, "val1"), path.Join(pathPrefix, "val2")})

				allBlobs, err := cacheDriver.GetAllBlobs("key")
				So(err, ShouldBeNil)
				So(allBlobs, ShouldResemble, []string{path.Join(pathPrefix, "val1"), path.Join(pathPrefix, "val2")})

				err = mock.ExpectationsWereMet()
				So(err, ShouldBeNil)
			})

			Convey("HasBlob HExists returns error"+testID, func() {
				// initialize mock client
				cacheDB, mock := redismock.NewClientMock()
				redisDriverParams.Client = cacheDB

				mock.ExpectPing().SetVal("OK")
				cacheDriver, err := cache.NewRedisCache(redisDriverParams, log)
				So(cacheDriver, ShouldNotBeNil)
				So(err, ShouldBeNil)

				mock.ExpectSIsMember(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
					path.Join(pathPrefix, "val")).SetVal(true)
				mock.ExpectHExists(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
					SetErr(ErrTestError)

				ok := cacheDriver.HasBlob("key", path.Join(dir, "val"))
				So(ok, ShouldBeFalse)

				err = mock.ExpectationsWereMet()
				So(err, ShouldBeNil)
			})

			Convey("HasBlob SIsMember returns error"+testID, func() {
				// initialize mock client
				cacheDB, mock := redismock.NewClientMock()
				redisDriverParams.Client = cacheDB

				mock.ExpectPing().SetVal("OK")
				cacheDriver, err := cache.NewRedisCache(redisDriverParams, log)
				So(cacheDriver, ShouldNotBeNil)
				So(err, ShouldBeNil)

				mock.ExpectSIsMember(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
					path.Join(pathPrefix, "val")).SetErr(ErrTestError)

				ok := cacheDriver.HasBlob("key", path.Join(dir, "val"))
				So(ok, ShouldBeFalse)

				err = mock.ExpectationsWereMet()
				So(err, ShouldBeNil)
			})

			Convey("HasBlob HExists returns false"+testID, func() {
				// initialize mock client
				cacheDB, mock := redismock.NewClientMock()
				redisDriverParams.Client = cacheDB

				mock.ExpectPing().SetVal("OK")
				cacheDriver, err := cache.NewRedisCache(redisDriverParams, log)
				So(cacheDriver, ShouldNotBeNil)
				So(err, ShouldBeNil)

				mock.ExpectSIsMember(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
					path.Join(pathPrefix, "val")).SetVal(true)
				mock.ExpectHExists(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
					SetVal(false)

				ok := cacheDriver.HasBlob("key", path.Join(dir, "val"))
				So(ok, ShouldBeFalse)

				err = mock.ExpectationsWereMet()
				So(err, ShouldBeNil)
			})

			Convey("DeleteBlob tests"+testID, func() {
				// initialize mock client
				cacheDB, mock := redismock.NewClientMock()
				redisDriverParams.Client = cacheDB

				mock.ExpectPing().SetVal("OK")
				cacheDriver, err := cache.NewRedisCache(redisDriverParams, log)
				So(cacheDriver, ShouldNotBeNil)
				So(err, ShouldBeNil)

				// Create entry for 1st path
				mock.Regexp().ExpectSetNX(keyPrefix+"locks:key", `.*`, 8*time.Second).SetVal(true)
				mock.ExpectHExists(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
					SetVal(false)
				mock.ExpectTxPipeline()
				mock.ExpectHSet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key",
					path.Join(pathPrefix, "val1")).SetVal(1)
				mock.ExpectSAdd(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
					path.Join(pathPrefix, "val1")).SetVal(1)
				mock.ExpectTxPipelineExec()

				err = cacheDriver.PutBlob("key", path.Join(dir, "val1"))
				So(err, ShouldBeNil)

				Convey("DeleteBlob error in HDel"+testID, func() {
					// If the 2nd path does not exist, HDel is callled
					// Error switching to new path
					mock.Regexp().ExpectSetNX(keyPrefix+"locks:key", `.*`, 8*time.Second).SetVal(true)
					mock.ExpectSRem(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
						path.Join(pathPrefix, "val1")).SetVal(1)
					mock.ExpectHGet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
						SetVal(path.Join(pathPrefix, "val1"))
					// failed to get new path
					mock.ExpectSRandMember(keyPrefix + constants.BlobsCache + ":" + constants.DuplicatesBucket + ":key").
						RedisNil()
					mock.ExpectHDel(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
						SetErr(ErrTestError)

					err = cacheDriver.DeleteBlob("key", path.Join(dir, "val1"))
					So(err, ShouldEqual, ErrTestError)
				})

				Convey("DeleteBlob succeeds in deleting all data for original blob"+testID, func() {
					// If the 2nd path does not exist, HDel is callled
					// Error switching to new path
					mock.Regexp().ExpectSetNX(keyPrefix+"locks:key", `.*`, 8*time.Second).SetVal(true)
					mock.ExpectSRem(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
						path.Join(pathPrefix, "val1")).SetVal(1)
					mock.ExpectHGet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
						SetVal(path.Join(pathPrefix, "val1"))
					// failed to get new path
					mock.ExpectSRandMember(keyPrefix + constants.BlobsCache + ":" + constants.DuplicatesBucket + ":key").
						RedisNil()
					mock.ExpectHDel(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
						SetVal(1)

					err = cacheDriver.DeleteBlob("key", path.Join(dir, "val1"))
					So(err, ShouldBeNil)
				})

				Convey("DeleteBlob error in SRandMember"+testID, func() {
					// Create entry for 2nd path
					mock.Regexp().ExpectSetNX(keyPrefix+"locks:key", `.*`, 8*time.Second).SetVal(true)
					mock.ExpectHExists(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
						SetVal(false)
					mock.ExpectTxPipeline()
					mock.ExpectHSet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key",
						path.Join(pathPrefix, "val2")).SetVal(1)
					mock.ExpectSAdd(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
						path.Join(pathPrefix, "val2")).SetVal(1)
					mock.ExpectTxPipelineExec()

					err = cacheDriver.PutBlob("key", path.Join(dir, "val2"))
					So(err, ShouldBeNil)

					// Error switching to new path
					mock.Regexp().ExpectSetNX(keyPrefix+"locks:key", `.*`, 8*time.Second).SetVal(true)
					mock.ExpectSRem(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
						path.Join(pathPrefix, "val1")).SetVal(1)
					mock.ExpectHGet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
						SetVal(path.Join(pathPrefix, "val1"))
					// failed to get new path
					mock.ExpectSRandMember(keyPrefix + constants.BlobsCache + ":" + constants.DuplicatesBucket + ":key").
						SetErr(ErrTestError)

					err = cacheDriver.DeleteBlob("key", path.Join(dir, "val1"))
					So(err, ShouldEqual, ErrTestError)
				})

				Convey("DeleteBlob error in HSet"+testID, func() {
					// Create entry for 2nd path
					mock.Regexp().ExpectSetNX(keyPrefix+"locks:key", `.*`, 8*time.Second).SetVal(true)
					mock.ExpectHExists(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
						SetVal(false)
					mock.ExpectTxPipeline()
					mock.ExpectHSet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key",
						path.Join(pathPrefix, "val2")).SetVal(1)
					mock.ExpectSAdd(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
						path.Join(pathPrefix, "val2")).SetVal(1)
					mock.ExpectTxPipelineExec()

					err = cacheDriver.PutBlob("key", path.Join(dir, "val2"))
					So(err, ShouldBeNil)

					// Error switching to new path
					mock.Regexp().ExpectSetNX(keyPrefix+"locks:key", `.*`, 8*time.Second).SetVal(true)
					mock.ExpectSRem(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
						path.Join(pathPrefix, "val1")).SetVal(1)
					mock.ExpectHGet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
						SetVal(path.Join(pathPrefix, "val1"))
					mock.ExpectSRandMember(keyPrefix + constants.BlobsCache + ":" + constants.DuplicatesBucket + ":key").
						SetVal(path.Join(pathPrefix, "val2"))
					mock.ExpectHSet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key",
						path.Join(pathPrefix, "val2")).SetErr(ErrTestError)

					err = cacheDriver.DeleteBlob("key", path.Join(dir, "val1"))
					So(err, ShouldEqual, ErrTestError)
				})

				Convey("DeleteBlob succeeds in switching original blob path"+testID, func() {
					// Create entry for 2nd path
					mock.Regexp().ExpectSetNX(keyPrefix+"locks:key", `.*`, 8*time.Second).SetVal(true)
					mock.ExpectHExists(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
						SetVal(false)
					mock.ExpectTxPipeline()
					mock.ExpectHSet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key",
						path.Join(pathPrefix, "val2")).SetVal(1)
					mock.ExpectSAdd(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
						path.Join(pathPrefix, "val2")).SetVal(1)
					mock.ExpectTxPipelineExec()

					err = cacheDriver.PutBlob("key", path.Join(dir, "val2"))
					So(err, ShouldBeNil)

					mock.Regexp().ExpectSetNX(keyPrefix+"locks:key", `.*`, 8*time.Second).SetVal(true)
					mock.ExpectSRem(keyPrefix+constants.BlobsCache+":"+constants.DuplicatesBucket+":key",
						path.Join(pathPrefix, "val1")).SetVal(1)
					mock.ExpectHGet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key").
						SetVal(path.Join(pathPrefix, "val1"))
					mock.ExpectSRandMember(keyPrefix + constants.BlobsCache + ":" + constants.DuplicatesBucket + ":key").
						SetVal(path.Join(pathPrefix, "val2"))
					mock.ExpectHSet(keyPrefix+constants.BlobsCache+":"+constants.OriginalBucket, "key",
						path.Join(pathPrefix, "val2")).SetVal(1)

					err = cacheDriver.DeleteBlob("key", path.Join(dir, "val1"))
					So(err, ShouldBeNil)
				})

				err = mock.ExpectationsWereMet()
				So(err, ShouldBeNil)
			})
		}
	})
}
