package s3

import (
	"bytes"
	"context"
	"crypto/rand"
	"errors"
	"fmt"
	"net/http"
	"os"
	"path"
	"reflect"
	"slices"
	"sort"
	"strconv"
	"strings"
	"testing"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/distribution/distribution/v3/internal/dcontext"
	storagedriver "github.com/distribution/distribution/v3/registry/storage/driver"
	"github.com/distribution/distribution/v3/registry/storage/driver/testsuites"
)

var (
	s3DriverConstructor func(rootDirectory, storageClass string) (*Driver, error)
	skipCheck           func(tb testing.TB)
)

func init() {
	var (
		accessKey       = os.Getenv("AWS_ACCESS_KEY")
		secretKey       = os.Getenv("AWS_SECRET_KEY")
		bucket          = os.Getenv("S3_BUCKET")
		encrypt         = os.Getenv("S3_ENCRYPT")
		keyID           = os.Getenv("S3_KEY_ID")
		secure          = os.Getenv("S3_SECURE")
		skipVerify      = os.Getenv("S3_SKIP_VERIFY")
		v4Auth          = os.Getenv("S3_V4_AUTH")
		region          = os.Getenv("AWS_REGION")
		objectACL       = os.Getenv("S3_OBJECT_ACL")
		regionEndpoint  = os.Getenv("REGION_ENDPOINT")
		forcePathStyle  = os.Getenv("AWS_S3_FORCE_PATH_STYLE")
		sessionToken    = os.Getenv("AWS_SESSION_TOKEN")
		useDualStack    = os.Getenv("S3_USE_DUALSTACK")
		accelerate      = os.Getenv("S3_ACCELERATE")
		useFIPSEndpoint = os.Getenv("S3_USE_FIPS_ENDPOINT")
		logLevel        = os.Getenv("S3_LOGLEVEL")
	)

	var err error
	s3DriverConstructor = func(rootDirectory, storageClass string) (*Driver, error) {
		encryptBool := false
		if encrypt != "" {
			encryptBool, err = strconv.ParseBool(encrypt)
			if err != nil {
				return nil, err
			}
		}

		secureBool := true
		if secure != "" {
			secureBool, err = strconv.ParseBool(secure)
			if err != nil {
				return nil, err
			}
		}

		skipVerifyBool := false
		if skipVerify != "" {
			skipVerifyBool, err = strconv.ParseBool(skipVerify)
			if err != nil {
				return nil, err
			}
		}

		v4Bool := true
		if v4Auth != "" {
			v4Bool, err = strconv.ParseBool(v4Auth)
			if err != nil {
				return nil, err
			}
		}
		forcePathStyleBool := false
		if forcePathStyle != "" {
			forcePathStyleBool, err = strconv.ParseBool(forcePathStyle)
			if err != nil {
				return nil, err
			}
		}

		useDualStackBool := false
		if useDualStack != "" {
			useDualStackBool, err = strconv.ParseBool(useDualStack)
		}

		accelerateBool := true
		if accelerate != "" {
			accelerateBool, err = strconv.ParseBool(accelerate)
			if err != nil {
				return nil, err
			}
		}

		useFIPSEndpointBool := false
		if useFIPSEndpoint != "" {
			useFIPSEndpointBool, err = strconv.ParseBool(useFIPSEndpoint)
			if err != nil {
				return nil, err
			}
		}

		if objectACL == "" {
			objectACL = s3.ObjectCannedACLPrivate
		}

		parameters := DriverParameters{
			AccessKey:                   accessKey,
			SecretKey:                   secretKey,
			Bucket:                      bucket,
			Region:                      region,
			RegionEndpoint:              regionEndpoint,
			ForcePathStyle:              forcePathStyleBool,
			Encrypt:                     encryptBool,
			KeyID:                       keyID,
			Secure:                      secureBool,
			SkipVerify:                  skipVerifyBool,
			V4Auth:                      v4Bool,
			ChunkSize:                   minChunkSize,
			MultipartCopyChunkSize:      defaultMultipartCopyChunkSize,
			MultipartCopyMaxConcurrency: defaultMultipartCopyMaxConcurrency,
			MultipartCopyThresholdSize:  defaultMultipartCopyThresholdSize,
			RootDirectory:               rootDirectory,
			StorageClass:                storageClass,
			UserAgent:                   driverName + "-test",
			ObjectACL:                   objectACL,
			SessionToken:                sessionToken,
			UseDualStack:                useDualStackBool,
			Accelerate:                  accelerateBool,
			UseFIPSEndpoint:             useFIPSEndpointBool,
			LogLevel:                    getS3LogLevelFromParam(logLevel),
		}

		return New(context.Background(), parameters)
	}

	// Skip S3 storage driver tests if environment variable parameters are not provided
	skipCheck = func(tb testing.TB) {
		tb.Helper()

		if accessKey == "" || secretKey == "" || region == "" || bucket == "" || encrypt == "" {
			tb.Skip("Must set AWS_ACCESS_KEY, AWS_SECRET_KEY, AWS_REGION, S3_BUCKET, and S3_ENCRYPT to run S3 tests")
		}
	}
}

func newDriverConstructor(tb testing.TB) testsuites.DriverConstructor {
	root := tb.TempDir()

	return func() (storagedriver.StorageDriver, error) {
		return s3DriverConstructor(root, s3.StorageClassStandard)
	}
}

func TestS3DriverSuite(t *testing.T) {
	skipCheck(t)
	skipVerify, err := strconv.ParseBool(os.Getenv("S3_SKIP_VERIFY"))
	if err != nil {
		skipVerify = false
	}
	testsuites.Driver(t, newDriverConstructor(t), skipVerify)
}

func BenchmarkS3DriverSuite(b *testing.B) {
	skipCheck(b)
	testsuites.BenchDriver(b, newDriverConstructor(b))
}

func TestGetS3LogLevelFromParam(t *testing.T) {
	tests := []struct {
		name     string
		param    any
		expected aws.LogLevelType
	}{
		// Nil case
		{
			name:     "nil parameter",
			param:    nil,
			expected: aws.LogOff,
		},

		// Boolean cases - YAML converts "off" to false
		{
			name:     "boolean false (from YAML off)",
			param:    false,
			expected: aws.LogOff,
		},
		{
			name:     "boolean true",
			param:    true,
			expected: aws.LogOff,
		},

		// String cases - valid values
		{
			name:     "string off",
			param:    "off",
			expected: aws.LogOff,
		},
		{
			name:     "string OFF uppercase",
			param:    "OFF",
			expected: aws.LogOff,
		},
		{
			name:     "string debug",
			param:    "debug",
			expected: aws.LogDebug,
		},
		{
			name:     "string DEBUG uppercase",
			param:    "DEBUG",
			expected: aws.LogDebug,
		},
		{
			name:     "string debugwithsigning",
			param:    "debugwithsigning",
			expected: aws.LogDebugWithSigning,
		},
		{
			name:     "string debugwithhttpbody",
			param:    "debugwithhttpbody",
			expected: aws.LogDebugWithHTTPBody,
		},
		{
			name:     "string debugwithrequestretries",
			param:    "debugwithrequestretries",
			expected: aws.LogDebugWithRequestRetries,
		},
		{
			name:     "string debugwithrequesterrors",
			param:    "debugwithrequesterrors",
			expected: aws.LogDebugWithRequestErrors,
		},
		{
			name:     "string debugwitheventstreambody",
			param:    "debugwitheventstreambody",
			expected: aws.LogDebugWithEventStreamBody,
		},

		// Invalid cases
		{
			name:     "invalid string value",
			param:    "invalid",
			expected: aws.LogOff,
		},
		{
			name:     "empty string",
			param:    "",
			expected: aws.LogOff,
		},
		{
			name:     "integer value",
			param:    42,
			expected: aws.LogOff,
		},
		{
			name:     "float value",
			param:    42.0,
			expected: aws.LogOff,
		},
		{
			name:     "slice value",
			param:    []string{"off"},
			expected: aws.LogOff,
		},
		{
			name:     "map value",
			param:    map[string]string{"level": "off"},
			expected: aws.LogOff,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := getS3LogLevelFromParam(tt.param)
			if result != tt.expected {
				t.Errorf("getS3LogLevelFromParam(%v) = %v, want %v", tt.param, result, tt.expected)
			}
		})
	}
}

func TestEmptyRootList(t *testing.T) {
	skipCheck(t)

	validRoot := t.TempDir()
	rootedDriver, err := s3DriverConstructor(validRoot, s3.StorageClassStandard)
	if err != nil {
		t.Fatalf("unexpected error creating rooted driver: %v", err)
	}

	emptyRootDriver, err := s3DriverConstructor("", s3.StorageClassStandard)
	if err != nil {
		t.Fatalf("unexpected error creating empty root driver: %v", err)
	}

	slashRootDriver, err := s3DriverConstructor("/", s3.StorageClassStandard)
	if err != nil {
		t.Fatalf("unexpected error creating slash root driver: %v", err)
	}

	filename := "/test"
	contents := []byte("contents")
	ctx := dcontext.Background()
	err = rootedDriver.PutContent(ctx, filename, contents)
	if err != nil {
		t.Fatalf("unexpected error creating content: %v", err)
	}
	// nolint:errcheck
	defer rootedDriver.Delete(ctx, filename)

	keys, _ := emptyRootDriver.List(ctx, "/")
	for _, path := range keys {
		if !storagedriver.PathRegexp.MatchString(path) {
			t.Fatalf("unexpected string in path: %q != %q", path, storagedriver.PathRegexp)
		}
	}

	keys, _ = slashRootDriver.List(ctx, "/")
	for _, p := range keys {
		if !storagedriver.PathRegexp.MatchString(p) {
			t.Fatalf("unexpected string in path: %q != %q", p, storagedriver.PathRegexp)
		}
	}
}

func TestClientTransport(t *testing.T) {
	skipCheck(t)

	testCases := []struct {
		skipverify bool
	}{
		{true},
		{false},
	}

	for _, tc := range testCases {
		// NOTE(milosgajdos): we cannot simply reuse s3DriverConstructor
		// because s3DriverConstructor is initialized in init() using the process
		// env vars: we can not override S3_SKIP_VERIFY env var with t.Setenv
		params := map[string]any{
			"region":     os.Getenv("AWS_REGION"),
			"bucket":     os.Getenv("S3_BUCKET"),
			"skipverify": tc.skipverify,
		}
		t.Run(fmt.Sprintf("SkipVerify %v", tc.skipverify), func(t *testing.T) {
			drv, err := FromParameters(context.TODO(), params)
			if err != nil {
				t.Fatalf("failed to create driver: %v", err)
			}

			s3drv := drv.baseEmbed.Base.StorageDriver.(*driver)
			if tc.skipverify {
				tr, ok := s3drv.S3.Client.Config.HTTPClient.Transport.(*http.Transport)
				if !ok {
					t.Fatal("unexpected driver transport")
				}
				if !tr.TLSClientConfig.InsecureSkipVerify {
					t.Errorf("unexpected TLS Config. Expected InsecureSkipVerify: %v, got %v",
						tc.skipverify,
						tr.TLSClientConfig.InsecureSkipVerify)
				}
				// make sure the proxy is always set
				if tr.Proxy == nil {
					t.Fatal("missing HTTP transport proxy config")
				}
				return
			}
			// if tc.skipverify is false we do not override the driver
			// HTTP client transport and leave it to the AWS SDK.
			if s3drv.S3.Client.Config.HTTPClient.Transport != nil {
				t.Errorf("unexpected S3 driver client transport")
			}
		})
	}
}

func TestStorageClass(t *testing.T) {
	skipCheck(t)

	rootDir := t.TempDir()
	contents := []byte("contents")
	ctx := dcontext.Background()

	// We don't need to test all the storage classes, just that its selectable.
	// The first 3 are common to AWS and MinIO, so use those.
	for _, storageClass := range s3StorageClasses[:3] {
		filename := "/test-" + storageClass
		s3Driver, err := s3DriverConstructor(rootDir, storageClass)
		if err != nil {
			t.Fatalf("unexpected error creating driver with storage class %v: %v", storageClass, err)
		}

		// Can only test outposts if using s3 outposts
		if storageClass == s3.StorageClassOutposts {
			continue
		}

		err = s3Driver.PutContent(ctx, filename, contents)
		if err != nil {
			t.Fatalf("unexpected error creating content with storage class %v: %v", storageClass, err)
		}
		// nolint:errcheck
		defer s3Driver.Delete(ctx, filename)

		driverUnwrapped := s3Driver.Base.StorageDriver.(*driver)
		resp, err := driverUnwrapped.S3.GetObject(&s3.GetObjectInput{
			Bucket: aws.String(driverUnwrapped.Bucket),
			Key:    aws.String(driverUnwrapped.s3Path(filename)),
		})
		if err != nil {
			t.Fatalf("unexpected error retrieving file with storage class %v: %v", storageClass, err)
		}
		defer resp.Body.Close()
		// Amazon only populates this header value for non-standard storage classes
		if storageClass == noStorageClass {
			// We haven't specified a storage class so we can't confirm what it is
		} else if storageClass == s3.StorageClassStandard && resp.StorageClass != nil {
			t.Fatalf(
				"unexpected response storage class for file with storage class %v: %v",
				storageClass,
				*resp.StorageClass,
			)
		} else if storageClass != s3.StorageClassStandard && resp.StorageClass == nil {
			t.Fatalf(
				"unexpected response storage class for file with storage class %v: %v",
				storageClass,
				s3.StorageClassStandard,
			)
		} else if storageClass != s3.StorageClassStandard && storageClass != *resp.StorageClass {
			t.Fatalf(
				"unexpected response storage class for file with storage class %v: %v",
				storageClass,
				*resp.StorageClass,
			)
		}
	}
}

func TestDelete(t *testing.T) {
	skipCheck(t)

	rootDir := t.TempDir()

	drvr, err := s3DriverConstructor(rootDir, s3.StorageClassStandard)
	if err != nil {
		t.Fatalf("unexpected error creating driver with standard storage: %v", err)
	}

	type errFn func(error) bool
	type testCase struct {
		name     string
		delete   string
		expected []string
		// error validation function
		err errFn
	}

	errPathNotFound := func(err error) bool {
		if err == nil {
			return false
		}
		switch err.(type) {
		case storagedriver.PathNotFoundError:
			return true
		}
		return false
	}
	errInvalidPath := func(err error) bool {
		if err == nil {
			return false
		}
		switch err.(type) {
		case storagedriver.InvalidPathError:
			return true
		}
		return false
	}

	objs := []string{
		"/file1",
		"/file1-2",
		"/folder1/file1",
		"/folder2/file1",
		"/folder3/file1",
		"/folder3/subfolder1/subfolder1/file1",
		"/folder3/subfolder2/subfolder1/file1",
		"/folder4/file1",
		"/folder1-v2/file1",
		"/folder1-v2/subfolder1/file1",
	}

	tcs := []testCase{ //nolint:prealloc
		{
			name:   "delete folder1",
			delete: "/folder1",
			expected: []string{
				"/folder1/file1",
			},
		},
		{
			name:   "delete folder2",
			delete: "/folder2",
			expected: []string{
				"/folder2/file1",
			},
		},
		{
			name:   "delete folder3",
			delete: "/folder3",
			expected: []string{
				"/folder3/file1",
				"/folder3/subfolder1/subfolder1/file1",
				"/folder3/subfolder2/subfolder1/file1",
			},
		},
		{
			name:     "delete path that doesn't exist",
			delete:   "/path/does/not/exist",
			expected: []string{},
			err:      errPathNotFound,
		},
		{
			name:     "delete path invalid: trailing slash",
			delete:   "/path/is/invalid/",
			expected: []string{},
			err:      errInvalidPath,
		},
		{
			name:     "delete path invalid: trailing special character",
			delete:   "/path/is/invalid*",
			expected: []string{},
			err:      errInvalidPath,
		},
	}

	// create a test case for each file
	for _, p := range objs {
		tcs = append(tcs, testCase{
			name:     fmt.Sprintf("delete path:'%s'", p),
			delete:   p,
			expected: []string{p},
		})
	}

	init := func() []string {
		// init file structure matching objs
		var created []string
		for _, p := range objs {
			err := drvr.PutContent(dcontext.Background(), p, []byte("content "+p))
			if err != nil {
				fmt.Printf("unable to init file %s: %s\n", p, err)
				continue
			}
			created = append(created, p)
		}
		return created
	}

	cleanup := func(objs []string) {
		var lastErr error
		for _, p := range objs {
			err := drvr.Delete(dcontext.Background(), p)
			if err != nil {
				switch err.(type) {
				case storagedriver.PathNotFoundError:
					continue
				}
				lastErr = err
			}
		}
		if lastErr != nil {
			t.Fatalf("cleanup failed: %s", lastErr)
		}
	}
	defer cleanup(objs)

	for _, tc := range tcs {
		t.Run(tc.name, func(t *testing.T) {
			objs := init()

			err := drvr.Delete(dcontext.Background(), tc.delete)

			if tc.err != nil {
				if err == nil {
					t.Fatal("expected error")
				}
				if !tc.err(err) {
					t.Fatalf("error does not match expected: %s", err)
				}
			}
			if tc.err == nil && err != nil {
				t.Fatalf("unexpected error: %s", err)
			}

			var issues []string

			// validate all files expected to be deleted are deleted
			// and all files not marked for deletion still remain
			expected := tc.expected
			isExpected := func(path string) bool {
				return slices.Contains(expected, path)
			}
			for _, path := range objs {
				stat, err := drvr.Stat(dcontext.Background(), path)
				if err != nil {
					switch err.(type) {
					case storagedriver.PathNotFoundError:
						if !isExpected(path) {
							issues = append(issues, fmt.Sprintf("unexpected path was deleted: %s", path))
						}
						// path was deleted & was supposed to be
						continue
					}
					t.Fatalf("stat: %s", err)
				}
				if stat.IsDir() {
					// for special cases where an object path has subpaths (eg /file1)
					// once /file1 is deleted it's now a directory according to stat
					continue
				}
				if isExpected(path) {
					issues = append(issues, fmt.Sprintf("expected path was not deleted: %s", path))
				}
			}

			if len(issues) > 0 {
				t.Fatal(strings.Join(issues, "; \n\t"))
			}
		})
	}
}

func TestWalkEmptyUploadsDir(t *testing.T) {
	skipCheck(t)

	ctx := dcontext.Background()

	drvr, err := s3DriverConstructor("s3walktest", s3.StorageClassStandard)
	if err != nil {
		t.Fatalf("unexpected error creating driver with standard storage: %v", err)
	}

	fileset := []string{
		"/docker/registry/v2/blobs/sha256/04/046909",
		"/docker/registry/v2/blobs/sha256/07/071a45",
		"/docker/registry/v2/repositories/testns/testimg/_layers/sha256/2a43dc",
		"/docker/registry/v2/repositories/testns/testimg/_layers/sha256/3ae7e8",
		"/docker/registry/v2/repositories/testns/testimg/_manifests/revisions/sha256/3ae7e8",
		"/docker/registry/v2/repositories/testns/testimg/_uploads/",
	}

	// create file structure matching fileset above.
	// we use the s3 sdk directly because the driver doesn't allow creation
	// of empty directories, which we need to simulate cases when purgeuploads
	// leaves behind empty directories.
	created := make([]string, 0, len(fileset))
	d := drvr.baseEmbed.Base.StorageDriver.(*driver)
	for _, p := range fileset {
		_, err := d.S3.PutObjectWithContext(ctx, &s3.PutObjectInput{
			Bucket:               aws.String(d.Bucket),
			Key:                  aws.String(d.s3Path(p)),
			ContentType:          d.getContentType(),
			ACL:                  d.getACL(),
			ServerSideEncryption: d.getEncryptionMode(),
			SSEKMSKeyId:          d.getSSEKMSKeyID(),
			StorageClass:         d.getStorageClass(),
			Body:                 bytes.NewReader([]byte("content " + p)),
		})
		if err != nil {
			fmt.Printf("unable to create file %s: %s\n", p, err)
			continue
		}
		created = append(created, p)
	}

	// use a custom cleanup here because we create an empty dir during this test's
	// setup, and the regular driver.Delete will error when trying to delete it.
	defer func() {
		s3Objects := make([]*s3.ObjectIdentifier, 0, len(fileset))
		for _, p := range created {
			s3Objects = append(s3Objects, &s3.ObjectIdentifier{
				Key: aws.String(d.s3Path(p)),
			})
		}
		resp, err := d.S3.DeleteObjectsWithContext(ctx, &s3.DeleteObjectsInput{
			Bucket: aws.String(d.Bucket),
			Delete: &s3.Delete{
				Objects: s3Objects,
				Quiet:   aws.Bool(false),
			},
		})
		if err != nil {
			t.Logf("DeleteObjectsWithContext resp: %+v", resp)
			t.Fatalf("cleanup failed: %s", err)
		}
	}()

	err = drvr.Walk(ctx, "/docker/registry/v2", func(fileInfo storagedriver.FileInfo) error {
		// attempt to split filepath into dir and filename, just like purgeuploads would.
		filePath := fileInfo.Path()
		_, file := path.Split(filePath)
		if len(file) == 0 {
			t.Logf("fileInfo.Path(): %s", fileInfo.Path())
			t.Fatalf("File part of fileInfo.Path() had zero length, this shouldn't happen.")
		}
		return nil
	}, func(*storagedriver.WalkOptions) {})
	if err != nil {
		t.Fatalf("driver.Walk failed: %s", err)
	}
}

func TestWalk(t *testing.T) {
	skipCheck(t)

	rootDir := t.TempDir()

	drvr, err := s3DriverConstructor(rootDir, s3.StorageClassStandard)
	if err != nil {
		t.Fatalf("unexpected error creating driver with standard storage: %v", err)
	}

	fileset := []string{
		"/file1",
		"/folder1-suffix/file1",
		"/folder1/file1",
		"/folder2/file1",
		"/folder3/subfolder1/subfolder1/file1",
		"/folder3/subfolder2/subfolder1/file1",
		"/folder4/file1",
	}

	// create file structure matching fileset above
	created := make([]string, 0, len(fileset))
	for _, p := range fileset {
		err := drvr.PutContent(dcontext.Background(), p, []byte("content "+p))
		if err != nil {
			fmt.Printf("unable to create file %s: %s\n", p, err)
			continue
		}
		created = append(created, p)
	}

	// cleanup
	defer func() {
		var lastErr error
		for _, p := range created {
			err := drvr.Delete(dcontext.Background(), p)
			if err != nil {
				_ = fmt.Errorf("cleanup failed for path %s: %s", p, err)
				lastErr = err
			}
		}
		if lastErr != nil {
			t.Fatalf("cleanup failed: %s", err)
		}
	}()

	noopFn := func(fileInfo storagedriver.FileInfo) error { return nil }

	tcs := []struct {
		name     string
		fn       storagedriver.WalkFn
		from     string
		options  []func(*storagedriver.WalkOptions)
		expected []string
		err      bool
	}{
		{
			name: "walk all",
			fn:   noopFn,
			expected: []string{
				"/file1",
				"/folder1-suffix",
				"/folder1-suffix/file1",
				"/folder1",
				"/folder1/file1",
				"/folder2",
				"/folder2/file1",
				"/folder3",
				"/folder3/subfolder1",
				"/folder3/subfolder1/subfolder1",
				"/folder3/subfolder1/subfolder1/file1",
				"/folder3/subfolder2",
				"/folder3/subfolder2/subfolder1",
				"/folder3/subfolder2/subfolder1/file1",
				"/folder4",
				"/folder4/file1",
			},
		},
		{
			name: "skip directory",
			fn: func(fileInfo storagedriver.FileInfo) error {
				if fileInfo.Path() == "/folder3" {
					return storagedriver.ErrSkipDir
				}
				if strings.Contains(fileInfo.Path(), "/folder3") {
					t.Fatalf("skipped dir %s and should not walk %s", "/folder3", fileInfo.Path())
				}
				return nil
			},
			expected: []string{
				"/file1",
				"/folder1-suffix",
				"/folder1-suffix/file1",
				"/folder1",
				"/folder1/file1",
				"/folder2",
				"/folder2/file1",
				"/folder3",
				// folder 3 contents skipped
				"/folder4",
				"/folder4/file1",
			},
		},
		{
			name: "start late without from",
			fn:   noopFn,
			options: []func(*storagedriver.WalkOptions){
				storagedriver.WithStartAfterHint("/folder3/subfolder1/subfolder1/file1"),
			},
			expected: []string{
				// start late
				"/folder3",
				"/folder3/subfolder2",
				"/folder3/subfolder2/subfolder1",
				"/folder3/subfolder2/subfolder1/file1",
				"/folder4",
				"/folder4/file1",
			},
			err: false,
		},
		{
			name: "start late with from",
			fn:   noopFn,
			from: "/folder3",
			options: []func(*storagedriver.WalkOptions){
				storagedriver.WithStartAfterHint("/folder3/subfolder1/subfolder1/file1"),
			},
			expected: []string{
				// start late
				"/folder3/subfolder2",
				"/folder3/subfolder2/subfolder1",
				"/folder3/subfolder2/subfolder1/file1",
			},
			err: false,
		},
		{
			name: "start after from",
			fn:   noopFn,
			from: "/folder1",
			options: []func(*storagedriver.WalkOptions){
				storagedriver.WithStartAfterHint("/folder2"),
			},
			expected: []string{},
			err:      false,
		},
		{
			name: "start matches from",
			fn:   noopFn,
			from: "/folder3",
			options: []func(*storagedriver.WalkOptions){
				storagedriver.WithStartAfterHint("/folder3"),
			},
			expected: []string{
				"/folder3/subfolder1",
				"/folder3/subfolder1/subfolder1",
				"/folder3/subfolder1/subfolder1/file1",
				"/folder3/subfolder2",
				"/folder3/subfolder2/subfolder1",
				"/folder3/subfolder2/subfolder1/file1",
			},
			err: false,
		},
		{
			name: "start doesn't exist",
			fn:   noopFn,
			from: "/folder3",
			options: []func(*storagedriver.WalkOptions){
				storagedriver.WithStartAfterHint("/folder3/notafolder/notafile"),
			},
			expected: []string{
				"/folder3/subfolder1",
				"/folder3/subfolder1/subfolder1",
				"/folder3/subfolder1/subfolder1/file1",
				"/folder3/subfolder2",
				"/folder3/subfolder2/subfolder1",
				"/folder3/subfolder2/subfolder1/file1",
			},
			err: false,
		},
		{
			name: "stop early",
			fn: func(fileInfo storagedriver.FileInfo) error {
				if fileInfo.Path() == "/folder1/file1" {
					return storagedriver.ErrFilledBuffer
				}
				return nil
			},
			expected: []string{
				"/file1",
				"/folder1-suffix",
				"/folder1-suffix/file1",
				"/folder1",
				"/folder1/file1",
				// stop early
			},
			err: false,
		},

		{
			name: "error",
			fn: func(fileInfo storagedriver.FileInfo) error {
				return errors.New("foo")
			},
			expected: []string{
				"/file1",
			},
			err: true,
		},
		{
			name: "from folder",
			fn:   noopFn,
			expected: []string{
				"/folder1/file1",
			},
			from: "/folder1",
		},
	}

	for _, tc := range tcs {
		var walked []string
		if tc.from == "" {
			tc.from = "/"
		}
		t.Run(tc.name, func(t *testing.T) {
			err := drvr.Walk(dcontext.Background(), tc.from, func(fileInfo storagedriver.FileInfo) error {
				walked = append(walked, fileInfo.Path())
				return tc.fn(fileInfo)
			}, tc.options...)
			if tc.err && err == nil {
				t.Fatal("expected err")
			}
			if !tc.err && err != nil {
				t.Fatal(err)
			}
			compareWalked(t, tc.expected, walked)
		})
	}
}

func TestOverThousandBlobs(t *testing.T) {
	skipCheck(t)

	rootDir := t.TempDir()
	standardDriver, err := s3DriverConstructor(rootDir, s3.StorageClassStandard)
	if err != nil {
		t.Fatalf("unexpected error creating driver with standard storage: %v", err)
	}

	ctx := dcontext.Background()
	for i := range 1005 {
		filename := "/thousandfiletest/file" + strconv.Itoa(i)
		contents := []byte("contents")
		err = standardDriver.PutContent(ctx, filename, contents)
		if err != nil {
			t.Fatalf("unexpected error creating content: %v", err)
		}
	}

	// cant actually verify deletion because read-after-delete is inconsistent, but can ensure no errors
	err = standardDriver.Delete(ctx, "/thousandfiletest")
	if err != nil {
		t.Fatalf("unexpected error deleting thousand files: %v", err)
	}
}

func TestMoveWithMultipartCopy(t *testing.T) {
	skipCheck(t)

	rootDir := t.TempDir()
	d, err := s3DriverConstructor(rootDir, s3.StorageClassStandard)
	if err != nil {
		t.Fatalf("unexpected error creating driver: %v", err)
	}

	ctx := dcontext.Background()
	sourcePath := "/source"
	destPath := "/dest"

	// nolint:errcheck
	defer d.Delete(ctx, sourcePath)
	// nolint:errcheck
	defer d.Delete(ctx, destPath)

	// An object larger than d's MultipartCopyThresholdSize will cause d.Move() to perform a multipart copy.
	multipartCopyThresholdSize := d.baseEmbed.Base.StorageDriver.(*driver).MultipartCopyThresholdSize
	contents := make([]byte, 2*multipartCopyThresholdSize)
	if _, err := rand.Read(contents); err != nil {
		t.Fatalf("unexpected error creating content: %v", err)
	}

	err = d.PutContent(ctx, sourcePath, contents)
	if err != nil {
		t.Fatalf("unexpected error writing content: %v", err)
	}

	err = d.Move(ctx, sourcePath, destPath)
	if err != nil {
		t.Fatalf("unexpected error moving file: %v", err)
	}

	received, err := d.GetContent(ctx, destPath)
	if err != nil {
		t.Fatalf("unexpected error getting content: %v", err)
	}
	if !bytes.Equal(contents, received) {
		t.Fatal("content differs")
	}

	_, err = d.GetContent(ctx, sourcePath)
	switch err.(type) {
	case storagedriver.PathNotFoundError:
	default:
		t.Fatalf("unexpected error getting content: %v", err)
	}
}

func TestListObjectsV2(t *testing.T) {
	skipCheck(t)

	rootDir := t.TempDir()
	d, err := s3DriverConstructor(rootDir, s3.StorageClassStandard)
	if err != nil {
		t.Fatalf("unexpected error creating driver: %v", err)
	}

	ctx := dcontext.Background()
	n := 6
	prefix := "/test-list-objects-v2"
	filePaths := make([]string, 0, n)
	for i := range n {
		filePaths = append(filePaths, fmt.Sprintf("%s/%d", prefix, i))
	}
	for _, p := range filePaths {
		if err := d.PutContent(ctx, p, []byte(p)); err != nil {
			t.Fatalf("unexpected error putting content: %v", err)
		}
	}

	info, err := d.Stat(ctx, filePaths[0])
	if err != nil {
		t.Fatalf("unexpected error stating: %v", err)
	}

	if info.IsDir() || info.Size() != int64(len(filePaths[0])) || info.Path() != filePaths[0] {
		t.Fatal("unexcepted state info")
	}

	subDirPath := prefix + "/sub/0"
	if err := d.PutContent(ctx, subDirPath, []byte(subDirPath)); err != nil {
		t.Fatalf("unexpected error putting content: %v", err)
	}

	subPaths := append(filePaths, path.Dir(subDirPath))

	result, err := d.List(ctx, prefix)
	if err != nil {
		t.Fatalf("unexpected error listing: %v", err)
	}

	sort.Strings(subPaths)
	sort.Strings(result)
	if !reflect.DeepEqual(subPaths, result) {
		t.Fatal("unexpected list result")
	}

	var walkPaths []string
	if err := d.Walk(ctx, prefix, func(fileInfo storagedriver.FileInfo) error {
		walkPaths = append(walkPaths, fileInfo.Path())
		if fileInfo.Path() == path.Dir(subDirPath) {
			if !fileInfo.IsDir() {
				t.Fatal("unexpected walking file info")
			}
		} else {
			if fileInfo.IsDir() || fileInfo.Size() != int64(len(fileInfo.Path())) {
				t.Fatal("unexpected walking file info")
			}
		}
		return nil
	}); err != nil {
		t.Fatalf("unexpected error walking: %v", err)
	}

	subPaths = append(subPaths, subDirPath)
	sort.Strings(walkPaths)
	sort.Strings(subPaths)
	if !reflect.DeepEqual(subPaths, walkPaths) {
		t.Fatal("unexpected walking paths")
	}

	if err := d.Delete(ctx, prefix); err != nil {
		t.Fatalf("unexpected error deleting: %v", err)
	}
}

func compareWalked(t *testing.T, expected, walked []string) {
	if len(walked) != len(expected) {
		t.Fatalf("Mismatch number of fileInfo walked %d expected %d; walked %s; expected %s;", len(walked), len(expected), walked, expected)
	}
	for i := range walked {
		if walked[i] != expected[i] {
			t.Fatalf("walked in unexpected order: expected %s; walked %s", expected, walked)
		}
	}
}
