package dynamodb_test

import (
	"context"
	"os"
	"testing"
	"time"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue"
	"github.com/aws/aws-sdk-go-v2/service/dynamodb"
	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
	guuid "github.com/gofrs/uuid"
	godigest "github.com/opencontainers/go-digest"
	ispec "github.com/opencontainers/image-spec/specs-go/v1"
	"github.com/rs/zerolog"
	. "github.com/smartystreets/goconvey/convey"

	"zotregistry.dev/zot/pkg/extensions/imagetrust"
	"zotregistry.dev/zot/pkg/log"
	mdynamodb "zotregistry.dev/zot/pkg/meta/dynamodb"
	mTypes "zotregistry.dev/zot/pkg/meta/types"
	reqCtx "zotregistry.dev/zot/pkg/requestcontext"
	. "zotregistry.dev/zot/pkg/test/image-utils"
	tskip "zotregistry.dev/zot/pkg/test/skip"
)

const badTablename = "bad tablename"

func TestIterator(t *testing.T) {
	tskip.SkipDynamo(t)

	const region = "us-east-2"

	endpoint := os.Getenv("DYNAMODBMOCK_ENDPOINT")

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

	repoMetaTablename := "RepoMetadataTable" + uuid.String()
	versionTablename := "Version" + uuid.String()
	imageMetaTablename := "ImageMeta" + uuid.String()
	repoBlobsTablename := "RepoBlobs" + uuid.String()
	userDataTablename := "UserDataTable" + uuid.String()
	apiKeyTablename := "ApiKeyTable" + uuid.String()

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

	Convey("TestIterator", t, func() {
		params := mdynamodb.DBDriverParameters{
			Endpoint:               endpoint,
			Region:                 region,
			RepoMetaTablename:      repoMetaTablename,
			ImageMetaTablename:     imageMetaTablename,
			RepoBlobsInfoTablename: repoBlobsTablename,
			VersionTablename:       versionTablename,
			APIKeyTablename:        apiKeyTablename,
			UserDataTablename:      userDataTablename,
		}
		client, err := mdynamodb.GetDynamoClient(params)
		So(err, ShouldBeNil)

		dynamoWrapper, err := mdynamodb.New(client, params, log)
		So(err, ShouldBeNil)

		So(dynamoWrapper.ResetTable(dynamoWrapper.ImageMetaTablename), ShouldBeNil)
		So(dynamoWrapper.ResetTable(dynamoWrapper.RepoMetaTablename), ShouldBeNil)

		err = dynamoWrapper.SetRepoReference(context.Background(), "repo1", "tag1", CreateRandomImage().AsImageMeta())
		So(err, ShouldBeNil)

		err = dynamoWrapper.SetRepoReference(context.Background(), "repo2", "tag2", CreateRandomImage().AsImageMeta())
		So(err, ShouldBeNil)

		err = dynamoWrapper.SetRepoReference(context.Background(), "repo3", "tag3", CreateRandomImage().AsImageMeta())
		So(err, ShouldBeNil)

		repoMetaAttributeIterator := mdynamodb.NewBaseDynamoAttributesIterator(
			dynamoWrapper.Client,
			dynamoWrapper.RepoMetaTablename,
			"RepoMeta",
			1,
			log,
		)

		attribute, err := repoMetaAttributeIterator.First(context.Background())
		So(err, ShouldBeNil)
		So(attribute, ShouldNotBeNil)

		attribute, err = repoMetaAttributeIterator.Next(context.Background())
		So(err, ShouldBeNil)
		So(attribute, ShouldNotBeNil)

		attribute, err = repoMetaAttributeIterator.Next(context.Background())
		So(err, ShouldBeNil)
		So(attribute, ShouldNotBeNil)

		attribute, err = repoMetaAttributeIterator.Next(context.Background())
		So(err, ShouldBeNil)
		So(attribute, ShouldBeNil)
	})
}

func TestIteratorErrors(t *testing.T) {
	Convey("errors", t, func() {
		customResolver := aws.EndpointResolverWithOptionsFunc( //nolint: staticcheck
			func(service, region string, options ...interface{}) (aws.Endpoint, error) {
				return aws.Endpoint{ //nolint: staticcheck
					PartitionID:   "aws",
					URL:           "endpoint",
					SigningRegion: region,
				}, nil
			})

		cfg, err := config.LoadDefaultConfig(context.Background(), config.WithRegion("region"),
			config.WithEndpointResolverWithOptions(customResolver)) //nolint: staticcheck
		So(err, ShouldBeNil)

		repoMetaAttributeIterator := mdynamodb.NewBaseDynamoAttributesIterator(
			dynamodb.NewFromConfig(cfg),
			"RepoMetadataTable",
			"RepoMeta",
			1,
			log.Logger{Logger: zerolog.New(os.Stdout)},
		)

		_, err = repoMetaAttributeIterator.First(context.Background())
		So(err, ShouldNotBeNil)
	})
}

func TestWrapperErrors(t *testing.T) {
	tskip.SkipDynamo(t)

	const region = "us-east-2"

	endpoint := os.Getenv("DYNAMODBMOCK_ENDPOINT")

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

	repoMetaTablename := "RepoMetadataTable" + uuid.String()
	versionTablename := "Version" + uuid.String()
	userDataTablename := "UserDataTable" + uuid.String()
	apiKeyTablename := "ApiKeyTable" + uuid.String()
	wrongTableName := "WRONG Tables"
	imageMetaTablename := "ImageMeta" + uuid.String()
	repoBlobsTablename := "RepoBlobs" + uuid.String()

	log := log.NewLogger("debug", "")
	testDigest := godigest.FromString("str")
	image := CreateDefaultImage()
	multi := CreateMultiarchWith().Images([]Image{image}).Build()
	imageMeta := image.AsImageMeta()
	multiarchImageMeta := multi.AsImageMeta()

	badProtoBlob := []byte("bad-repo-meta")
	// goodRepoMetaBlob, err := proto.Marshal(&proto_go.RepoMeta{Name: "repo"})
	// if err != nil {
	// 	t.FailNow()
	// }

	//nolint: contextcheck
	Convey("Errors", t, func() {
		params := mdynamodb.DBDriverParameters{ //nolint:contextcheck,staticcheck
			Endpoint:               endpoint,
			Region:                 region,
			RepoMetaTablename:      repoMetaTablename,
			ImageMetaTablename:     imageMetaTablename,
			RepoBlobsInfoTablename: repoBlobsTablename,
			UserDataTablename:      userDataTablename,
			APIKeyTablename:        apiKeyTablename,
			VersionTablename:       versionTablename,
		}
		client, err := mdynamodb.GetDynamoClient(params) //nolint:contextcheck
		So(err, ShouldBeNil)

		imgTrustStore, err := imagetrust.NewAWSImageTrustStore(params.Region, params.Endpoint)
		So(err, ShouldBeNil)

		dynamoWrapper, err := mdynamodb.New(client, params, log) //nolint:contextcheck
		So(err, ShouldBeNil)

		dynamoWrapper.SetImageTrustStore(imgTrustStore)

		So(dynamoWrapper.ResetTable(dynamoWrapper.RepoMetaTablename), ShouldBeNil)  //nolint:contextcheck
		So(dynamoWrapper.ResetTable(dynamoWrapper.RepoBlobsTablename), ShouldBeNil) //nolint:contextcheck
		So(dynamoWrapper.ResetTable(dynamoWrapper.ImageMetaTablename), ShouldBeNil) //nolint:contextcheck
		So(dynamoWrapper.ResetTable(dynamoWrapper.UserDataTablename), ShouldBeNil)  //nolint:contextcheck

		userAc := reqCtx.NewUserAccessControl()
		userAc.SetUsername("test")
		ctx := userAc.DeriveContext(context.Background())

		Convey("RemoveRepoReference", func() {
			Convey("getProtoRepoMeta errors", func() {
				err := setRepoMeta("repo", badProtoBlob, dynamoWrapper)
				So(err, ShouldBeNil)

				err = dynamoWrapper.RemoveRepoReference("repo", "ref", imageMeta.Digest)
				So(err, ShouldNotBeNil)
			})

			Convey("getProtoImageMeta errors", func() {
				err := dynamoWrapper.SetRepoMeta("repo", mTypes.RepoMeta{ //nolint: contextcheck
					Name: "repo",
					Tags: map[mTypes.Tag]mTypes.Descriptor{
						"tag": {
							MediaType: ispec.MediaTypeImageManifest,
							Digest:    imageMeta.Digest.String(),
						},
					},
				})
				So(err, ShouldBeNil)

				err = setImageMeta(imageMeta.Digest, badProtoBlob, dynamoWrapper)
				So(err, ShouldBeNil)

				err = dynamoWrapper.RemoveRepoReference("repo", "ref", imageMeta.Digest)
				So(err, ShouldNotBeNil)
			})

			Convey("unmarshalProtoRepoBlobs errors", func() {
				err := dynamoWrapper.SetRepoReference(ctx, "repo", "tag", imageMeta)
				So(err, ShouldBeNil)

				err = setRepoBlobInfo("repo", badProtoBlob, dynamoWrapper) //nolint: contextcheck
				So(err, ShouldBeNil)

				err = dynamoWrapper.RemoveRepoReference("repo", "ref", imageMeta.Digest) //nolint: contextcheck
				So(err, ShouldNotBeNil)
			})
		})
		Convey("FilterImageMeta", func() {
			Convey("FilterImageMeta with duplicate digests", func() {
				image := CreateRandomImage()

				err := dynamoWrapper.SetRepoReference(ctx, "repo", "tag", image.AsImageMeta())
				So(err, ShouldBeNil)

				_, err = dynamoWrapper.FilterImageMeta(ctx, []string{image.DigestStr(), image.DigestStr()})
				So(err, ShouldNotBeNil)
			})

			Convey("manifest meta unmarshal error", func() {
				err = setImageMeta(image.Digest(), badProtoBlob, dynamoWrapper) //nolint: contextcheck
				So(err, ShouldBeNil)

				_, err = dynamoWrapper.FilterImageMeta(ctx, []string{image.DigestStr()})
				So(err, ShouldNotBeNil)
			})

			Convey("MediaType ImageIndex, getProtoImageMeta fails", func() {
				err := dynamoWrapper.SetImageMeta(multiarchImageMeta.Digest, multiarchImageMeta) //nolint: contextcheck
				So(err, ShouldBeNil)

				err = setImageMeta(image.Digest(), badProtoBlob, dynamoWrapper) //nolint: contextcheck
				So(err, ShouldBeNil)

				// manifests are missing
				_, err = dynamoWrapper.FilterImageMeta(ctx, []string{multiarchImageMeta.Digest.String()})
				So(err, ShouldNotBeNil)
			})
		})
		Convey("UpdateSignaturesValidity", func() {
			digest := image.Digest()

			Convey("image meta blob not found", func() {
				err := dynamoWrapper.UpdateSignaturesValidity(ctx, "repo", digest)
				So(err, ShouldNotBeNil)
			})

			Convey("UpdateSignaturesValidity with context done", func() {
				ctx, cancel := context.WithCancel(context.Background())
				cancel()

				err := dynamoWrapper.UpdateSignaturesValidity(ctx, "repo", digest)
				So(err, ShouldNotBeNil)
			})

			Convey("image meta unmarshal fail", func() {
				err := setImageMeta(digest, badProtoBlob, dynamoWrapper)
				So(err, ShouldBeNil)

				err = dynamoWrapper.UpdateSignaturesValidity(ctx, "repo", digest)
				So(err, ShouldNotBeNil)
			})

			Convey("repo meta blob not found", func() {
				err := dynamoWrapper.SetImageMeta(digest, imageMeta)
				So(err, ShouldBeNil)

				err = dynamoWrapper.UpdateSignaturesValidity(ctx, "repo", digest)
				So(err, ShouldNotBeNil)
			})

			Convey("repo meta unmarshal fail", func() {
				err := dynamoWrapper.SetImageMeta(digest, imageMeta)
				So(err, ShouldBeNil)

				err = setRepoMeta("repo", badProtoBlob, dynamoWrapper)
				So(err, ShouldBeNil)

				err = dynamoWrapper.UpdateSignaturesValidity(ctx, "repo", digest)
				So(err, ShouldNotBeNil)
			})
		})

		Convey("UpdateStatsOnDownload", func() {
			Convey("unmarshalProtoRepoMeta error", func() {
				err := setRepoMeta("repo", badProtoBlob, dynamoWrapper)
				So(err, ShouldBeNil)

				err = dynamoWrapper.UpdateStatsOnDownload("repo", "ref")
				So(err, ShouldNotBeNil)
			})

			Convey("ref is tag and tag is not found", func() {
				err := dynamoWrapper.SetRepoReference(ctx, "repo", "tag", imageMeta)
				So(err, ShouldBeNil)

				err = dynamoWrapper.UpdateStatsOnDownload("repo", "not-found-tag") //nolint: contextcheck
				So(err, ShouldNotBeNil)
			})

			Convey("digest not found in statistics", func() {
				err := dynamoWrapper.SetRepoReference(ctx, "repo", "tag", imageMeta)
				So(err, ShouldBeNil)

				err = dynamoWrapper.UpdateStatsOnDownload("repo", godigest.FromString("not-found").String()) //nolint: contextcheck
				So(err, ShouldNotBeNil)
			})
		})
		Convey("GetReferrersInfo", func() {
			Convey("unmarshalProtoRepoMeta error", func() {
				err := setRepoMeta("repo", badProtoBlob, dynamoWrapper)
				So(err, ShouldBeNil)

				_, err = dynamoWrapper.GetReferrersInfo("repo", "refDig", []string{})
				So(err, ShouldNotBeNil)
			})
		})
		Convey("DecrementRepoStars", func() {
			Convey("unmarshalProtoRepoMeta error", func() {
				err := setRepoMeta("repo", badProtoBlob, dynamoWrapper)
				So(err, ShouldBeNil)

				err = dynamoWrapper.DecrementRepoStars("repo")
				So(err, ShouldNotBeNil)
			})
		})

		Convey("IncrementRepoStars", func() {
			Convey("unmarshalProtoRepoMeta error", func() {
				err := setRepoMeta("repo", badProtoBlob, dynamoWrapper)
				So(err, ShouldBeNil)

				err = dynamoWrapper.IncrementRepoStars("repo")
				So(err, ShouldNotBeNil)
			})
		})

		Convey("GetMultipleRepoMeta", func() {
			Convey("repoMetaAttributeIterator.First fails", func() {
				dynamoWrapper.RepoMetaTablename = badTablename
				_, err := dynamoWrapper.GetMultipleRepoMeta(ctx, func(repoMeta mTypes.RepoMeta) bool { return true })
				So(err, ShouldNotBeNil)
			})
			Convey("repo meta unmarshal fails", func() {
				err := setRepoMeta("repo", badProtoBlob, dynamoWrapper) //nolint: contextcheck
				So(err, ShouldBeNil)

				_, err = dynamoWrapper.GetMultipleRepoMeta(ctx, func(repoMeta mTypes.RepoMeta) bool { return true })
				So(err, ShouldNotBeNil)
			})
		})
		Convey("GetImageMeta", func() {
			Convey("get image meta fails", func() {
				_, err := dynamoWrapper.GetImageMeta(testDigest)
				So(err, ShouldNotBeNil)
			})
			Convey("image index, get manifest meta fails", func() {
				err := dynamoWrapper.SetRepoReference(ctx, "repo", "tag", multiarchImageMeta)
				So(err, ShouldBeNil)

				_, err = dynamoWrapper.GetImageMeta(multiarchImageMeta.Digest) //nolint: contextcheck
				So(err, ShouldNotBeNil)
			})
		})
		Convey("GetFullImageMeta", func() {
			Convey("repo meta not found", func() {
				_, err := dynamoWrapper.GetFullImageMeta(ctx, "repo", "tag")
				So(err, ShouldNotBeNil)
			})

			Convey("unmarshalProtoRepoMeta fails", func() {
				err := setRepoMeta("repo", badProtoBlob, dynamoWrapper) //nolint: contextcheck
				So(err, ShouldBeNil)

				_, err = dynamoWrapper.GetFullImageMeta(ctx, "repo", "tag")
				So(err, ShouldNotBeNil)
			})

			Convey("tag not found", func() {
				err := dynamoWrapper.SetRepoReference(ctx, "repo", "tag", imageMeta)
				So(err, ShouldBeNil)

				_, err = dynamoWrapper.GetFullImageMeta(ctx, "repo", "tag-not-found")
				So(err, ShouldNotBeNil)
			})

			Convey("getProtoImageMeta fails", func() {
				err := dynamoWrapper.SetRepoMeta("repo", mTypes.RepoMeta{ //nolint: contextcheck
					Name: "repo",
					Tags: map[mTypes.Tag]mTypes.Descriptor{
						"tag": {
							MediaType: ispec.MediaTypeImageManifest,
							Digest:    godigest.FromString("not-found").String(),
						},
					},
				})
				So(err, ShouldBeNil)

				_, err = dynamoWrapper.GetFullImageMeta(ctx, "repo", "tag")
				So(err, ShouldNotBeNil)
			})

			Convey("image is index, fail to get manifests", func() {
				err := dynamoWrapper.SetImageMeta(multiarchImageMeta.Digest, multiarchImageMeta) //nolint: contextcheck
				So(err, ShouldBeNil)

				err = dynamoWrapper.SetRepoMeta("repo", mTypes.RepoMeta{ //nolint: contextcheck
					Name: "repo",
					Tags: map[mTypes.Tag]mTypes.Descriptor{
						"tag": {
							MediaType: ispec.MediaTypeImageIndex,
							Digest:    multiarchImageMeta.Digest.String(),
						},
					},
				})
				So(err, ShouldBeNil)

				_, err = dynamoWrapper.GetFullImageMeta(ctx, "repo", "tag")
				So(err, ShouldNotBeNil)
			})
		})

		Convey("FilterTags", func() {
			Convey("repoMetaAttributeIterator.First fails", func() {
				dynamoWrapper.RepoMetaTablename = badTablename

				_, err = dynamoWrapper.FilterTags(ctx, mTypes.AcceptAllRepoTag, mTypes.AcceptAllImageMeta)
				So(err, ShouldNotBeNil)
			})
			Convey("repo meta unmarshal fails", func() {
				err := setRepoMeta("repo", badProtoBlob, dynamoWrapper) //nolint: contextcheck
				So(err, ShouldBeNil)

				_, err = dynamoWrapper.FilterTags(ctx, mTypes.AcceptAllRepoTag, mTypes.AcceptAllImageMeta)
				So(err, ShouldNotBeNil)
			})
			Convey("found repo meta", func() {
				Convey("bad image manifest", func() {
					badImageDigest := godigest.FromString("bad-image-manifest")
					err := dynamoWrapper.SetRepoMeta("repo", mTypes.RepoMeta{ //nolint: contextcheck
						Name: "repo",
						Tags: map[mTypes.Tag]mTypes.Descriptor{
							"bad-image-manifest": {
								MediaType: ispec.MediaTypeImageManifest,
								Digest:    badImageDigest.String(),
							},
						},
					})
					So(err, ShouldBeNil)

					err = setImageMeta(badImageDigest, badProtoBlob, dynamoWrapper) //nolint: contextcheck
					So(err, ShouldBeNil)

					_, err = dynamoWrapper.FilterTags(ctx, mTypes.AcceptAllRepoTag, mTypes.AcceptAllImageMeta)
					So(err, ShouldNotBeNil)
				})
				Convey("bad image index", func() {
					badIndexDigest := godigest.FromString("bad-image-manifest")
					err := dynamoWrapper.SetRepoMeta("repo", mTypes.RepoMeta{ //nolint: contextcheck
						Name: "repo",
						Tags: map[mTypes.Tag]mTypes.Descriptor{
							"bad-image-index": {
								MediaType: ispec.MediaTypeImageIndex,
								Digest:    badIndexDigest.String(),
							},
						},
					})
					So(err, ShouldBeNil)

					err = setImageMeta(badIndexDigest, badProtoBlob, dynamoWrapper) //nolint: contextcheck
					So(err, ShouldBeNil)

					_, err = dynamoWrapper.FilterTags(ctx, mTypes.AcceptAllRepoTag, mTypes.AcceptAllImageMeta)
					So(err, ShouldNotBeNil)
				})
				Convey("good image index, bad inside manifest", func() {
					goodIndexBadManifestDigest := godigest.FromString("good-index-bad-manifests")
					err := dynamoWrapper.SetRepoMeta("repo", mTypes.RepoMeta{ //nolint: contextcheck
						Name: "repo",
						Tags: map[mTypes.Tag]mTypes.Descriptor{
							"good-index-bad-manifests": {
								MediaType: ispec.MediaTypeImageIndex,
								Digest:    goodIndexBadManifestDigest.String(),
							},
						},
					})
					So(err, ShouldBeNil)

					err = dynamoWrapper.SetImageMeta(goodIndexBadManifestDigest, multiarchImageMeta) //nolint: contextcheck
					So(err, ShouldBeNil)

					err = setImageMeta(image.Digest(), badProtoBlob, dynamoWrapper) //nolint: contextcheck
					So(err, ShouldBeNil)

					_, err = dynamoWrapper.FilterTags(ctx, mTypes.AcceptAllRepoTag, mTypes.AcceptAllImageMeta)
					So(err, ShouldNotBeNil)
				})
			})
		})

		Convey("SearchTags", func() {
			Convey("getProtoRepoMeta errors", func() {
				dynamoWrapper.RepoMetaTablename = badTablename

				_, err := dynamoWrapper.SearchTags(ctx, "repo")
				So(err, ShouldNotBeNil)
			})
			Convey("found repo meta", func() {
				Convey("bad image manifest", func() {
					badImageDigest := godigest.FromString("bad-image-manifest")
					err := dynamoWrapper.SetRepoMeta("repo", mTypes.RepoMeta{ //nolint: contextcheck
						Name: "repo",
						Tags: map[mTypes.Tag]mTypes.Descriptor{
							"bad-image-manifest": {
								MediaType: ispec.MediaTypeImageManifest,
								Digest:    badImageDigest.String(),
							},
						},
					})
					So(err, ShouldBeNil)

					err = setImageMeta(badImageDigest, badProtoBlob, dynamoWrapper) //nolint: contextcheck
					So(err, ShouldBeNil)

					_, err = dynamoWrapper.SearchTags(ctx, "repo:")
					So(err, ShouldNotBeNil)
				})
				Convey("bad image index", func() {
					badIndexDigest := godigest.FromString("bad-image-manifest")
					err := dynamoWrapper.SetRepoMeta("repo", mTypes.RepoMeta{ //nolint: contextcheck
						Name: "repo",
						Tags: map[mTypes.Tag]mTypes.Descriptor{
							"bad-image-index": {
								MediaType: ispec.MediaTypeImageIndex,
								Digest:    badIndexDigest.String(),
							},
						},
					})
					So(err, ShouldBeNil)

					err = setImageMeta(badIndexDigest, badProtoBlob, dynamoWrapper) //nolint: contextcheck
					So(err, ShouldBeNil)

					_, err = dynamoWrapper.SearchTags(ctx, "repo:")
					So(err, ShouldNotBeNil)
				})
				Convey("good image index, bad inside manifest", func() {
					goodIndexBadManifestDigest := godigest.FromString("good-index-bad-manifests")
					err := dynamoWrapper.SetRepoMeta("repo", mTypes.RepoMeta{ //nolint: contextcheck
						Name: "repo",
						Tags: map[mTypes.Tag]mTypes.Descriptor{
							"good-index-bad-manifests": {
								MediaType: ispec.MediaTypeImageIndex,
								Digest:    goodIndexBadManifestDigest.String(),
							},
						},
					})
					So(err, ShouldBeNil)

					err = dynamoWrapper.SetImageMeta(goodIndexBadManifestDigest, multiarchImageMeta) //nolint: contextcheck
					So(err, ShouldBeNil)

					err = setImageMeta(image.Digest(), badProtoBlob, dynamoWrapper) //nolint: contextcheck
					So(err, ShouldBeNil)

					_, err = dynamoWrapper.SearchTags(ctx, "repo:")
					So(err, ShouldNotBeNil)
				})
				Convey("bad media type", func() {
					err := dynamoWrapper.SetRepoMeta("repo", mTypes.RepoMeta{ //nolint: contextcheck
						Name: "repo",
						Tags: map[mTypes.Tag]mTypes.Descriptor{
							"mad-media-type": {
								MediaType: "bad media type",
								Digest:    godigest.FromString("dig").String(),
							},
						},
					})
					So(err, ShouldBeNil)

					_, err = dynamoWrapper.SearchTags(ctx, "repo:")
					So(err, ShouldBeNil)
				})
			})
		})

		Convey("SearchRepos", func() {
			Convey("repoMetaAttributeIterator.First errors", func() {
				dynamoWrapper.RepoMetaTablename = badTablename

				_, err := dynamoWrapper.SearchRepos(ctx, "repo")
				So(err, ShouldNotBeNil)
			})

			Convey("repo meta unmarshal errors", func() {
				err := setRepoMeta("repo", badProtoBlob, dynamoWrapper) //nolint: contextcheck
				So(err, ShouldBeNil)

				_, err = dynamoWrapper.SearchRepos(ctx, "repo")
				So(err, ShouldNotBeNil)
			})
		})

		Convey("SetRepoReference", func() {
			Convey("SetProtoImageMeta fails", func() {
				dynamoWrapper.ImageMetaTablename = badTablename

				err := dynamoWrapper.SetRepoReference(ctx, "repo", "tag", image.AsImageMeta())
				So(err, ShouldNotBeNil)
			})
			Convey("getProtoRepoMeta fails", func() {
				dynamoWrapper.RepoMetaTablename = badTablename

				err := dynamoWrapper.SetRepoReference(ctx, "repo", "tag", image.AsImageMeta())
				So(err, ShouldNotBeNil)
			})
			Convey("getProtoRepoBlobs fails", func() {
				dynamoWrapper.RepoBlobsTablename = badTablename

				err := dynamoWrapper.SetRepoReference(ctx, "repo", "tag", image.AsImageMeta())
				So(err, ShouldNotBeNil)
			})
		})

		Convey("GetProtoImageMeta", func() {
			Convey("Get request fails", func() {
				dynamoWrapper.ImageMetaTablename = badTablename

				_, err := dynamoWrapper.GetProtoImageMeta(ctx, testDigest)
				So(err, ShouldNotBeNil)
			})
			Convey("unmarshal fails", func() {
				err := setRepoMeta("repo", badProtoBlob, dynamoWrapper) //nolint: contextcheck
				So(err, ShouldBeNil)

				_, err = dynamoWrapper.GetProtoImageMeta(ctx, testDigest)
				So(err, ShouldNotBeNil)
			})
		})

		Convey("SetUserData", func() {
			hashKey := "id"
			apiKeys := make(map[string]mTypes.APIKeyDetails)
			apiKeyDetails := mTypes.APIKeyDetails{
				Label:  "apiKey",
				Scopes: []string{"repo"},
				UUID:   hashKey,
			}

			apiKeys[hashKey] = apiKeyDetails

			userProfileSrc := mTypes.UserData{
				Groups:  []string{"group1", "group2"},
				APIKeys: apiKeys,
			}

			err := dynamoWrapper.SetUserData(ctx, userProfileSrc)
			So(err, ShouldBeNil)

			userAc := reqCtx.NewUserAccessControl()
			ctx := userAc.DeriveContext(context.Background())

			err = dynamoWrapper.SetUserData(ctx, mTypes.UserData{}) //nolint: contextcheck
			So(err, ShouldNotBeNil)
		})

		Convey("DeleteUserData", func() {
			err := dynamoWrapper.DeleteUserData(ctx)
			So(err, ShouldBeNil)

			userAc := reqCtx.NewUserAccessControl()
			ctx := userAc.DeriveContext(context.Background())

			err = dynamoWrapper.DeleteUserData(ctx) //nolint: contextcheck
			So(err, ShouldNotBeNil)
		})

		Convey("ToggleBookmarkRepo no access", func() {
			userAc := reqCtx.NewUserAccessControl()
			userAc.SetUsername("username")
			userAc.SetGlobPatterns("read", map[string]bool{
				"repo": false,
			})

			ctx := userAc.DeriveContext(context.Background())

			_, err := dynamoWrapper.ToggleBookmarkRepo(ctx, "unaccesible")
			So(err, ShouldNotBeNil)
		})

		Convey("ToggleBookmarkRepo GetUserMeta no user data", func() {
			userAc := reqCtx.NewUserAccessControl()
			userAc.SetUsername("username")
			userAc.SetGlobPatterns("read", map[string]bool{
				"repo": true,
			})

			ctx := userAc.DeriveContext(context.Background())

			status, err := dynamoWrapper.ToggleBookmarkRepo(ctx, "repo")
			So(err, ShouldBeNil)
			So(status, ShouldEqual, mTypes.Added)
		})

		Convey("ToggleBookmarkRepo GetUserMeta client error", func() {
			userAc := reqCtx.NewUserAccessControl()
			userAc.SetUsername("username")
			userAc.SetGlobPatterns("read", map[string]bool{
				"repo": false,
			})

			ctx := userAc.DeriveContext(context.Background())

			dynamoWrapper.UserDataTablename = badTablename

			status, err := dynamoWrapper.ToggleBookmarkRepo(ctx, "repo")
			So(err, ShouldNotBeNil)
			So(status, ShouldEqual, mTypes.NotChanged)
		})

		Convey("GetBookmarkedRepos", func() {
			userAc := reqCtx.NewUserAccessControl()
			userAc.SetUsername("username")
			userAc.SetGlobPatterns("read", map[string]bool{
				"repo": false,
			})

			ctx := userAc.DeriveContext(context.Background())

			repos, err := dynamoWrapper.GetBookmarkedRepos(ctx)
			So(err, ShouldBeNil)
			So(len(repos), ShouldEqual, 0)
		})

		Convey("ToggleStarRepo GetUserMeta bad context", func() {
			uacKey := reqCtx.GetContextKey()
			ctx := context.WithValue(context.Background(), uacKey, "bad context")

			_, err := dynamoWrapper.ToggleStarRepo(ctx, "repo")
			So(err, ShouldNotBeNil)
		})

		Convey("ToggleStarRepo GetUserMeta no access", func() {
			userAc := reqCtx.NewUserAccessControl()
			userAc.SetUsername("username")
			userAc.SetGlobPatterns("read", map[string]bool{
				"repo": false,
			})

			ctx := userAc.DeriveContext(context.Background())

			_, err := dynamoWrapper.ToggleStarRepo(ctx, "unaccesible")
			So(err, ShouldNotBeNil)
		})

		Convey("ToggleStarRepo GetUserMeta error", func() {
			userAc := reqCtx.NewUserAccessControl()
			userAc.SetUsername("username")
			userAc.SetGlobPatterns("read", map[string]bool{
				"repo": false,
			})

			ctx := userAc.DeriveContext(context.Background())

			dynamoWrapper.UserDataTablename = badTablename

			_, err := dynamoWrapper.ToggleStarRepo(ctx, "repo")
			So(err, ShouldNotBeNil)
		})

		Convey("ToggleStarRepo GetRepoMeta error", func() {
			userAc := reqCtx.NewUserAccessControl()
			userAc.SetUsername("username")
			userAc.SetGlobPatterns("read", map[string]bool{
				"repo": true,
			})

			ctx := userAc.DeriveContext(context.Background())

			dynamoWrapper.RepoMetaTablename = badTablename

			_, err := dynamoWrapper.ToggleStarRepo(ctx, "repo")
			So(err, ShouldNotBeNil)
		})

		Convey("GetUserData bad context", func() {
			uacKey := reqCtx.GetContextKey()
			ctx := context.WithValue(context.Background(), uacKey, "bad context")

			userData, err := dynamoWrapper.GetUserData(ctx)
			So(err, ShouldNotBeNil)
			So(userData.BookmarkedRepos, ShouldBeEmpty)
			So(userData.StarredRepos, ShouldBeEmpty)
		})

		Convey("GetUserData client error", func() {
			userAc := reqCtx.NewUserAccessControl()
			userAc.SetUsername("username")
			userAc.SetGlobPatterns("read", map[string]bool{
				"repo": true,
			})

			ctx := userAc.DeriveContext(context.Background())

			dynamoWrapper.UserDataTablename = badTablename

			_, err := dynamoWrapper.GetUserData(ctx)
			So(err, ShouldNotBeNil)
		})

		Convey("GetUserMeta unmarshal error, bad user data", func() {
			userAc := reqCtx.NewUserAccessControl()
			userAc.SetUsername("username")
			userAc.SetGlobPatterns("read", map[string]bool{
				"repo": true,
			})

			ctx := userAc.DeriveContext(context.Background())

			err := setBadUserData(dynamoWrapper.Client, userDataTablename, userAc.GetUsername())
			So(err, ShouldBeNil)

			_, err = dynamoWrapper.GetUserData(ctx)
			So(err, ShouldNotBeNil)
		})

		Convey("SetUserData bad context", func() {
			uacKey := reqCtx.GetContextKey()
			ctx := context.WithValue(context.Background(), uacKey, "bad context")

			err := dynamoWrapper.SetUserData(ctx, mTypes.UserData{})
			So(err, ShouldNotBeNil)
		})

		Convey("GetUserData bad context errors", func() {
			uacKey := reqCtx.GetContextKey()
			ctx := context.WithValue(context.Background(), uacKey, "bad context")

			_, err := dynamoWrapper.GetUserData(ctx)
			So(err, ShouldNotBeNil)
		})

		Convey("SetUserData bad context errors", func() {
			uacKey := reqCtx.GetContextKey()
			ctx := context.WithValue(context.Background(), uacKey, "bad context")

			err := dynamoWrapper.SetUserData(ctx, mTypes.UserData{})
			So(err, ShouldNotBeNil)
		})

		Convey("AddUserAPIKey bad context errors", func() {
			uacKey := reqCtx.GetContextKey()
			ctx := context.WithValue(context.Background(), uacKey, "bad context")

			err := dynamoWrapper.AddUserAPIKey(ctx, "", &mTypes.APIKeyDetails{})
			So(err, ShouldNotBeNil)
		})

		Convey("DeleteUserAPIKey bad context errors", func() {
			uacKey := reqCtx.GetContextKey()
			ctx := context.WithValue(context.Background(), uacKey, "bad context")

			err := dynamoWrapper.DeleteUserAPIKey(ctx, "")
			So(err, ShouldNotBeNil)
		})

		Convey("UpdateUserAPIKeyLastUsed bad context errors", func() {
			uacKey := reqCtx.GetContextKey()
			ctx := context.WithValue(context.Background(), uacKey, "bad context")

			err := dynamoWrapper.UpdateUserAPIKeyLastUsed(ctx, "")
			So(err, ShouldNotBeNil)
		})

		Convey("DeleteUserData bad context errors", func() {
			uacKey := reqCtx.GetContextKey()
			ctx := context.WithValue(context.Background(), uacKey, "bad context")

			err := dynamoWrapper.DeleteUserData(ctx)
			So(err, ShouldNotBeNil)
		})

		Convey("GetRepoLastUpdated", func() {
			Convey("bad table", func() {
				dynamoWrapper.RepoBlobsTablename = "bad-table"

				lastUpdated := dynamoWrapper.GetRepoLastUpdated("repo")
				So(lastUpdated, ShouldEqual, time.Time{})
			})

			Convey("unmarshal error", func() {
				err := setRepoLastUpdated("repo", []byte("bad-blob"), dynamoWrapper)
				So(err, ShouldBeNil)

				lastUpdated := dynamoWrapper.GetRepoLastUpdated("repo")
				So(lastUpdated, ShouldEqual, time.Time{})
			})
		})

		Convey("DeleteUserAPIKey returns nil", func() {
			userAc := reqCtx.NewUserAccessControl()
			userAc.SetUsername("email")
			ctx := userAc.DeriveContext(context.Background())

			apiKeyDetails := make(map[string]mTypes.APIKeyDetails)
			apiKeyDetails["id"] = mTypes.APIKeyDetails{
				UUID: "id",
			}
			err := dynamoWrapper.SetUserData(ctx, mTypes.UserData{
				APIKeys: apiKeyDetails,
			})
			So(err, ShouldBeNil)

			dynamoWrapper.APIKeyTablename = wrongTableName
			err = dynamoWrapper.DeleteUserAPIKey(ctx, "id")
			So(err, ShouldNotBeNil)
		})

		Convey("AddUserAPIKey", func() {
			Convey("no userid found", func() {
				userAc := reqCtx.NewUserAccessControl()
				ctx := userAc.DeriveContext(context.Background())

				err = dynamoWrapper.AddUserAPIKey(ctx, "key", &mTypes.APIKeyDetails{})
				So(err, ShouldNotBeNil)
			})

			userAc := reqCtx.NewUserAccessControl()
			userAc.SetUsername("email")
			ctx := userAc.DeriveContext(context.Background())

			err := dynamoWrapper.AddUserAPIKey(ctx, "key", &mTypes.APIKeyDetails{})
			So(err, ShouldBeNil)

			dynamoWrapper.APIKeyTablename = wrongTableName
			err = dynamoWrapper.AddUserAPIKey(ctx, "key", &mTypes.APIKeyDetails{})
			So(err, ShouldNotBeNil)
		})

		Convey("GetUserAPIKeyInfo", func() {
			dynamoWrapper.APIKeyTablename = wrongTableName
			_, err := dynamoWrapper.GetUserAPIKeyInfo("key")
			So(err, ShouldNotBeNil)
		})

		Convey("GetUserData", func() {
			userAc := reqCtx.NewUserAccessControl()
			ctx := userAc.DeriveContext(context.Background())

			_, err := dynamoWrapper.GetUserData(ctx)
			So(err, ShouldNotBeNil)

			userAc = reqCtx.NewUserAccessControl()
			userAc.SetUsername("email")
			ctx = userAc.DeriveContext(context.Background())

			dynamoWrapper.UserDataTablename = wrongTableName
			_, err = dynamoWrapper.GetUserData(ctx)
			So(err, ShouldNotBeNil)
		})

		Convey("PatchDB dwr.getDBVersion errors", func() {
			dynamoWrapper.VersionTablename = badTablename

			err := dynamoWrapper.PatchDB()
			So(err, ShouldNotBeNil)
		})

		Convey("PatchDB patchIndex < version.GetVersionIndex", func() {
			err := setVersion(dynamoWrapper.Client, versionTablename, "V2")
			So(err, ShouldBeNil)

			dynamoWrapper.Patches = []func(client *dynamodb.Client, tableNames map[string]string) error{
				func(client *dynamodb.Client, tableNames map[string]string) error { return nil },
				func(client *dynamodb.Client, tableNames map[string]string) error { return nil },
				func(client *dynamodb.Client, tableNames map[string]string) error { return nil },
			}

			err = dynamoWrapper.PatchDB()
			So(err, ShouldBeNil)
		})

		Convey("ResetRepoMetaTable client errors", func() {
			dynamoWrapper.RepoMetaTablename = badTablename

			err := dynamoWrapper.ResetTable(dynamoWrapper.RepoMetaTablename)
			So(err, ShouldNotBeNil)
		})

		Convey("getDBVersion client errors", func() {
			dynamoWrapper.VersionTablename = badTablename

			err := dynamoWrapper.PatchDB()
			So(err, ShouldNotBeNil)
		})
	})

	Convey("NewDynamoDBWrapper errors", t, func() {
		params := mdynamodb.DBDriverParameters{ //nolint:contextcheck
			Endpoint:               endpoint,
			Region:                 region,
			RepoMetaTablename:      "",
			ImageMetaTablename:     imageMetaTablename,
			RepoBlobsInfoTablename: repoBlobsTablename,
			UserDataTablename:      userDataTablename,
			APIKeyTablename:        apiKeyTablename,
			VersionTablename:       versionTablename,
		}
		client, err := mdynamodb.GetDynamoClient(params)
		So(err, ShouldBeNil)

		_, err = mdynamodb.New(client, params, log)
		So(err, ShouldNotBeNil)

		params = mdynamodb.DBDriverParameters{ //nolint:contextcheck
			Endpoint:               endpoint,
			Region:                 region,
			RepoMetaTablename:      repoMetaTablename,
			ImageMetaTablename:     "",
			RepoBlobsInfoTablename: repoBlobsTablename,
			UserDataTablename:      userDataTablename,
			APIKeyTablename:        apiKeyTablename,
			VersionTablename:       versionTablename,
		}
		client, err = mdynamodb.GetDynamoClient(params)
		So(err, ShouldBeNil)

		_, err = mdynamodb.New(client, params, log)
		So(err, ShouldNotBeNil)

		params = mdynamodb.DBDriverParameters{ //nolint:contextcheck
			Endpoint:               endpoint,
			Region:                 region,
			RepoMetaTablename:      repoMetaTablename,
			ImageMetaTablename:     imageMetaTablename,
			RepoBlobsInfoTablename: "",
			UserDataTablename:      userDataTablename,
			APIKeyTablename:        apiKeyTablename,
			VersionTablename:       versionTablename,
		}
		client, err = mdynamodb.GetDynamoClient(params)
		So(err, ShouldBeNil)

		_, err = mdynamodb.New(client, params, log)
		So(err, ShouldNotBeNil)

		params = mdynamodb.DBDriverParameters{ //nolint:contextcheck
			Endpoint:               endpoint,
			Region:                 region,
			RepoMetaTablename:      repoMetaTablename,
			ImageMetaTablename:     imageMetaTablename,
			RepoBlobsInfoTablename: repoBlobsTablename,
			UserDataTablename:      userDataTablename,
			APIKeyTablename:        apiKeyTablename,
			VersionTablename:       "",
		}
		client, err = mdynamodb.GetDynamoClient(params)
		So(err, ShouldBeNil)

		_, err = mdynamodb.New(client, params, log)
		So(err, ShouldNotBeNil)

		params = mdynamodb.DBDriverParameters{ //nolint:contextcheck
			Endpoint:               endpoint,
			Region:                 region,
			RepoMetaTablename:      repoMetaTablename,
			ImageMetaTablename:     imageMetaTablename,
			RepoBlobsInfoTablename: repoBlobsTablename,
			VersionTablename:       versionTablename,
			UserDataTablename:      "",
			APIKeyTablename:        apiKeyTablename,
		}
		client, err = mdynamodb.GetDynamoClient(params)
		So(err, ShouldBeNil)

		_, err = mdynamodb.New(client, params, log)
		So(err, ShouldNotBeNil)

		params = mdynamodb.DBDriverParameters{ //nolint:contextcheck
			Endpoint:               endpoint,
			Region:                 region,
			RepoMetaTablename:      repoMetaTablename,
			ImageMetaTablename:     imageMetaTablename,
			RepoBlobsInfoTablename: repoBlobsTablename,
			VersionTablename:       versionTablename,
			UserDataTablename:      userDataTablename,
			APIKeyTablename:        "",
		}
		client, err = mdynamodb.GetDynamoClient(params)
		So(err, ShouldBeNil)

		_, err = mdynamodb.New(client, params, log)
		So(err, ShouldNotBeNil)
	})
}

func setRepoMeta(repo string, blob []byte, dynamoWrapper *mdynamodb.DynamoDB) error { //nolint: unparam
	userAttributeValue, err := attributevalue.Marshal(blob)
	if err != nil {
		return err
	}

	_, err = dynamoWrapper.Client.UpdateItem(context.Background(), &dynamodb.UpdateItemInput{
		ExpressionAttributeNames: map[string]string{
			"#RM": "RepoMeta",
		},
		ExpressionAttributeValues: map[string]types.AttributeValue{
			":RepoMeta": userAttributeValue,
		},
		Key: map[string]types.AttributeValue{
			"TableKey": &types.AttributeValueMemberS{
				Value: repo,
			},
		},
		TableName:        aws.String(dynamoWrapper.RepoMetaTablename),
		UpdateExpression: aws.String("SET #RM = :RepoMeta"),
	})

	return err
}

func setRepoLastUpdated(repo string, blob []byte, dynamoWrapper *mdynamodb.DynamoDB) error { //nolint: unparam
	lastUpdatedAttributeValue, err := attributevalue.Marshal(blob)
	if err != nil {
		return err
	}

	_, err = dynamoWrapper.Client.UpdateItem(context.Background(), &dynamodb.UpdateItemInput{
		ExpressionAttributeNames: map[string]string{
			"#RLU": "RepoLastUpdated",
		},
		ExpressionAttributeValues: map[string]types.AttributeValue{
			":RepoLastUpdated": lastUpdatedAttributeValue,
		},
		Key: map[string]types.AttributeValue{
			"TableKey": &types.AttributeValueMemberS{
				Value: repo,
			},
		},
		TableName:        aws.String(dynamoWrapper.RepoBlobsTablename),
		UpdateExpression: aws.String("SET #RLU = :RepoLastUpdated"),
	})

	return err
}

func setRepoBlobInfo(repo string, blob []byte, dynamoWrapper *mdynamodb.DynamoDB) error {
	userAttributeValue, err := attributevalue.Marshal(blob)
	if err != nil {
		return err
	}

	_, err = dynamoWrapper.Client.UpdateItem(context.Background(), &dynamodb.UpdateItemInput{
		ExpressionAttributeNames: map[string]string{
			"#RB": "RepoBlobsInfo",
		},
		ExpressionAttributeValues: map[string]types.AttributeValue{
			":RepoBlobsInfo": userAttributeValue,
		},
		Key: map[string]types.AttributeValue{
			"TableKey": &types.AttributeValueMemberS{
				Value: repo,
			},
		},
		TableName:        aws.String(dynamoWrapper.RepoBlobsTablename),
		UpdateExpression: aws.String("SET #RB = :RepoBlobsInfo"),
	})

	return err
}

func setImageMeta(digest godigest.Digest, blob []byte, dynamoWrapper *mdynamodb.DynamoDB) error {
	userAttributeValue, err := attributevalue.Marshal(blob)
	if err != nil {
		return err
	}

	_, err = dynamoWrapper.Client.UpdateItem(context.Background(), &dynamodb.UpdateItemInput{
		ExpressionAttributeNames: map[string]string{
			"#IM": "ImageMeta",
		},
		ExpressionAttributeValues: map[string]types.AttributeValue{
			":ImageMeta": userAttributeValue,
		},
		Key: map[string]types.AttributeValue{
			"TableKey": &types.AttributeValueMemberS{
				Value: digest.String(),
			},
		},
		TableName:        aws.String(dynamoWrapper.ImageMetaTablename),
		UpdateExpression: aws.String("SET #IM = :ImageMeta"),
	})

	return err
}

func setBadUserData(client *dynamodb.Client, userDataTablename, userID string) error {
	userAttributeValue, err := attributevalue.Marshal("string")
	if err != nil {
		return err
	}

	_, err = client.UpdateItem(context.Background(), &dynamodb.UpdateItemInput{
		ExpressionAttributeNames: map[string]string{
			"#UM": "UserData",
		},
		ExpressionAttributeValues: map[string]types.AttributeValue{
			":UserData": userAttributeValue,
		},
		Key: map[string]types.AttributeValue{
			"TableKey": &types.AttributeValueMemberS{
				Value: userID,
			},
		},
		TableName:        aws.String(userDataTablename),
		UpdateExpression: aws.String("SET #UM = :UserData"),
	})

	return err
}

func setVersion(client *dynamodb.Client, versionTablename string, version string) error {
	mdAttributeValue, err := attributevalue.Marshal(version)
	if err != nil {
		return err
	}

	_, err = client.UpdateItem(context.TODO(), &dynamodb.UpdateItemInput{
		ExpressionAttributeNames: map[string]string{
			"#V": "Version",
		},
		ExpressionAttributeValues: map[string]types.AttributeValue{
			":Version": mdAttributeValue,
		},
		Key: map[string]types.AttributeValue{
			"TableKey": &types.AttributeValueMemberS{
				Value: "DBVersion",
			},
		},
		TableName:        aws.String(versionTablename),
		UpdateExpression: aws.String("SET #V = :Version"),
	})

	return err
}
