package s3

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"os"
	"path"
	"path/filepath"
	"reflect"
	"strings"
	"testing"
	"text/template"
	"time"

	"github.com/k3s-io/k3s/pkg/daemons/config"
	"github.com/k3s-io/k3s/pkg/etcd/snapshot"
	"github.com/k3s-io/k3s/pkg/util"
	"github.com/k3s-io/k3s/pkg/util/mux"
	"github.com/k3s-io/k3s/tests/mock"
	"github.com/rancher/dynamiclistener/cert"
	"github.com/rancher/wrangler/v3/pkg/generated/controllers/core"
	"github.com/sirupsen/logrus"
	"go.uber.org/mock/gomock"
	v1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

var gmt = time.FixedZone("GMT", 0)

func Test_UnitControllerGetClient(t *testing.T) {
	logrus.SetLevel(logrus.DebugLevel)

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

	// Dummy server with http and https listeners as a simple S3 mock
	server := &http.Server{Handler: s3Router(t)}

	// Create temp cert/key
	cert, key, _ := cert.GenerateSelfSignedCertKey("localhost", []net.IP{net.ParseIP("::1"), net.ParseIP("127.0.0.1")}, nil)
	tempDir := t.TempDir()
	certFile := filepath.Join(tempDir, "test.crt")
	keyFile := filepath.Join(tempDir, "test.key")
	os.WriteFile(certFile, cert, 0600)
	os.WriteFile(keyFile, key, 0600)

	listener, _ := net.Listen("tcp", ":0")
	listenerTLS, _ := net.Listen("tcp", ":0")

	_, port, _ := net.SplitHostPort(listener.Addr().String())
	listenerAddr := net.JoinHostPort("localhost", port)
	_, port, _ = net.SplitHostPort(listenerTLS.Addr().String())
	listenerTLSAddr := net.JoinHostPort("localhost", port)

	go server.Serve(listener)
	go server.ServeTLS(listenerTLS, certFile, keyFile)
	go func() {
		<-ctx.Done()
		server.Close()
	}()

	type fields struct {
		clusterID   string
		tokenHash   string
		nodeName    string
		clientCache *util.Cache[*Client]
	}
	type args struct {
		ctx    context.Context
		etcdS3 *config.EtcdS3
	}
	tests := []struct {
		name    string
		fields  fields
		args    args
		setup   func(t *testing.T, a args, f fields, c *Client) (core.Interface, error)
		want    *Client
		wantErr bool
	}{
		{
			name: "Fail to get client with nil config",
			args: args{
				ctx: ctx,
			},
			fields: fields{
				clusterID:   "1234",
				tokenHash:   "abcd",
				nodeName:    "server01",
				clientCache: util.NewCache[*Client](5),
			},
			wantErr: true,
			setup: func(t *testing.T, a args, f fields, c *Client) (core.Interface, error) {
				coreMock := mock.NewCore(gomock.NewController(t))
				return coreMock, nil
			},
		},
		{
			name: "Fail to get client when bucket not set",
			args: args{
				ctx: ctx,
				etcdS3: &config.EtcdS3{
					AccessKey: "test",
					Endpoint:  listenerAddr,
					Insecure:  true,
					Region:    defaultEtcdS3.Region,
					Retention: defaultEtcdS3.Retention,
					Timeout:   *defaultEtcdS3.Timeout.DeepCopy(),
				},
			},
			fields: fields{
				clusterID:   "1234",
				tokenHash:   "abcd",
				nodeName:    "server01",
				clientCache: util.NewCache[*Client](5),
			},
			wantErr: true,
			setup: func(t *testing.T, a args, f fields, c *Client) (core.Interface, error) {
				coreMock := mock.NewCore(gomock.NewController(t))
				return coreMock, nil
			},
		},
		{
			name: "Fail to get client when bucket does not exist",
			args: args{
				ctx: ctx,
				etcdS3: &config.EtcdS3{
					AccessKey: "test",
					Bucket:    "badbucket",
					Endpoint:  listenerAddr,
					Insecure:  true,
					Region:    defaultEtcdS3.Region,
					Retention: defaultEtcdS3.Retention,
					Timeout:   *defaultEtcdS3.Timeout.DeepCopy(),
				},
			},
			fields: fields{
				clusterID:   "1234",
				tokenHash:   "abcd",
				nodeName:    "server01",
				clientCache: util.NewCache[*Client](5),
			},
			wantErr: true,
			setup: func(t *testing.T, a args, f fields, c *Client) (core.Interface, error) {
				coreMock := mock.NewCore(gomock.NewController(t))
				return coreMock, nil
			},
		},
		{
			name: "Fail to get client with missing Secret",
			args: args{
				ctx: ctx,
				etcdS3: &config.EtcdS3{
					Endpoint:     defaultEtcdS3.Endpoint,
					Region:       defaultEtcdS3.Region,
					ConfigSecret: "my-etcd-s3-config-secret",
					Retention:    defaultEtcdS3.Retention,
					Timeout:      *defaultEtcdS3.Timeout.DeepCopy(),
				},
			},
			fields: fields{
				clusterID:   "1234",
				tokenHash:   "abcd",
				nodeName:    "server01",
				clientCache: util.NewCache[*Client](5),
			},
			wantErr: true,
			setup: func(t *testing.T, a args, f fields, c *Client) (core.Interface, error) {
				coreMock := mock.NewCore(gomock.NewController(t))
				coreMock.V1Mock.SecretMock.EXPECT().Get(metav1.NamespaceSystem, "my-etcd-s3-config-secret", gomock.Any()).AnyTimes().DoAndReturn(func(namespace, name string, _ metav1.GetOptions) (*v1.Secret, error) {
					return nil, mock.ErrorNotFound("secret", name)
				})
				return coreMock, nil
			},
		},
		{
			name: "Create client for config from secret",
			args: args{
				ctx: ctx,
				etcdS3: &config.EtcdS3{
					Endpoint:     defaultEtcdS3.Endpoint,
					Region:       defaultEtcdS3.Region,
					ConfigSecret: "my-etcd-s3-config-secret",
					Retention:    defaultEtcdS3.Retention,
					Timeout:      *defaultEtcdS3.Timeout.DeepCopy(),
				},
			},
			fields: fields{
				clusterID:   "1234",
				tokenHash:   "abcd",
				nodeName:    "server01",
				clientCache: util.NewCache[*Client](5),
			},
			setup: func(t *testing.T, a args, f fields, c *Client) (core.Interface, error) {
				coreMock := mock.NewCore(gomock.NewController(t))
				coreMock.V1Mock.SecretMock.EXPECT().Get(metav1.NamespaceSystem, "my-etcd-s3-config-secret", gomock.Any()).AnyTimes().DoAndReturn(func(namespace, name string, _ metav1.GetOptions) (*v1.Secret, error) {
					return &v1.Secret{
						ObjectMeta: metav1.ObjectMeta{
							Name:      name,
							Namespace: namespace,
						},
						Type: v1.SecretTypeOpaque,
						Data: map[string][]byte{
							"etcd-s3-access-key":  []byte("test"),
							"etcd-s3-bucket":      []byte("testbucket"),
							"etcd-s3-endpoint":    []byte(listenerTLSAddr),
							"etcd-s3-region":      []byte("us-west-2"),
							"etcd-s3-timeout":     []byte("1m"),
							"etcd-s3-endpoint-ca": cert,
							"etcd-s3-retention":   []byte("5"),
						},
					}, nil
				})
				return coreMock, nil
			},
		},
		{
			name: "Create client for config from secret with CA in configmap",
			args: args{
				ctx: ctx,
				etcdS3: &config.EtcdS3{
					Endpoint:     defaultEtcdS3.Endpoint,
					Region:       defaultEtcdS3.Region,
					ConfigSecret: "my-etcd-s3-config-secret",
					Retention:    defaultEtcdS3.Retention,
					Timeout:      *defaultEtcdS3.Timeout.DeepCopy(),
				},
			},
			fields: fields{
				clusterID:   "1234",
				tokenHash:   "abcd",
				nodeName:    "server01",
				clientCache: util.NewCache[*Client](5),
			},
			setup: func(t *testing.T, a args, f fields, c *Client) (core.Interface, error) {
				coreMock := mock.NewCore(gomock.NewController(t))
				coreMock.V1Mock.SecretMock.EXPECT().Get(metav1.NamespaceSystem, "my-etcd-s3-config-secret", gomock.Any()).AnyTimes().DoAndReturn(func(namespace, name string, _ metav1.GetOptions) (*v1.Secret, error) {
					return &v1.Secret{
						ObjectMeta: metav1.ObjectMeta{
							Name:      name,
							Namespace: namespace,
						},
						Type: v1.SecretTypeOpaque,
						Data: map[string][]byte{
							"etcd-s3-access-key":       []byte("test"),
							"etcd-s3-bucket":           []byte("testbucket"),
							"etcd-s3-endpoint":         []byte(listenerTLSAddr),
							"etcd-s3-region":           []byte("us-west-2"),
							"etcd-s3-timeout":          []byte("1m"),
							"etcd-s3-endpoint-ca-name": []byte("my-etcd-s3-ca"),
							"etcd-s3-skip-ssl-verify":  []byte("false"),
							"etcd-s3-retention":        []byte("5"),
						},
					}, nil
				})
				coreMock.V1Mock.ConfigMapMock.EXPECT().Get(metav1.NamespaceSystem, "my-etcd-s3-ca", gomock.Any()).AnyTimes().DoAndReturn(func(namespace, name string, _ metav1.GetOptions) (*v1.ConfigMap, error) {
					return &v1.ConfigMap{
						ObjectMeta: metav1.ObjectMeta{
							Name:      name,
							Namespace: namespace,
						},
						Data: map[string]string{
							"dummy-ca": string(cert),
						},
						BinaryData: map[string][]byte{
							"dummy-ca-binary": cert,
						},
					}, nil
				})
				return coreMock, nil
			},
		},
		{
			name: "Fail to create client for config from secret with CA in missing configmap",
			args: args{
				ctx: ctx,
				etcdS3: &config.EtcdS3{
					Endpoint:     defaultEtcdS3.Endpoint,
					Region:       defaultEtcdS3.Region,
					ConfigSecret: "my-etcd-s3-config-secret",
					Retention:    defaultEtcdS3.Retention,
					Timeout:      *defaultEtcdS3.Timeout.DeepCopy(),
				},
			},
			fields: fields{
				clusterID:   "1234",
				tokenHash:   "abcd",
				nodeName:    "server01",
				clientCache: util.NewCache[*Client](5),
			},
			wantErr: true,
			setup: func(t *testing.T, a args, f fields, c *Client) (core.Interface, error) {
				coreMock := mock.NewCore(gomock.NewController(t))
				coreMock.V1Mock.SecretMock.EXPECT().Get(metav1.NamespaceSystem, "my-etcd-s3-config-secret", gomock.Any()).AnyTimes().DoAndReturn(func(namespace, name string, _ metav1.GetOptions) (*v1.Secret, error) {
					return &v1.Secret{
						ObjectMeta: metav1.ObjectMeta{
							Name:      name,
							Namespace: namespace,
						},
						Type: v1.SecretTypeOpaque,
						Data: map[string][]byte{
							"etcd-s3-access-key":       []byte("test"),
							"etcd-s3-bucket":           []byte("testbucket"),
							"etcd-s3-endpoint":         []byte(listenerTLSAddr),
							"etcd-s3-region":           []byte("us-west-2"),
							"etcd-s3-timeout":          []byte("invalid"),
							"etcd-s3-endpoint-ca":      []byte("invalid"),
							"etcd-s3-endpoint-ca-name": []byte("my-etcd-s3-ca"),
							"etcd-s3-skip-ssl-verify":  []byte("invalid"),
							"etcd-s3-insecure":         []byte("invalid"),
							"etcd-s3-retention":        []byte("5"),
						},
					}, nil
				})
				coreMock.V1Mock.ConfigMapMock.EXPECT().Get(metav1.NamespaceSystem, "my-etcd-s3-ca", gomock.Any()).AnyTimes().DoAndReturn(func(namespace, name string, _ metav1.GetOptions) (*v1.ConfigMap, error) {
					return nil, mock.ErrorNotFound("configmap", name)
				})
				return coreMock, nil
			},
		},
		{
			name: "Create insecure client for config from cli when secret is also set",
			args: args{
				ctx: ctx,
				etcdS3: &config.EtcdS3{
					AccessKey:    "test",
					Bucket:       "testbucket",
					Region:       "us-west-2",
					ConfigSecret: "my-etcd-s3-config-secret",
					Endpoint:     listenerAddr,
					Insecure:     true,
					Retention:    defaultEtcdS3.Retention,
					Timeout:      *defaultEtcdS3.Timeout.DeepCopy(),
				},
			},
			fields: fields{
				clusterID:   "1234",
				tokenHash:   "abcd",
				nodeName:    "server01",
				clientCache: util.NewCache[*Client](5),
			},
			setup: func(t *testing.T, a args, f fields, c *Client) (core.Interface, error) {
				coreMock := mock.NewCore(gomock.NewController(t))
				return coreMock, nil
			},
		},
		{
			name: "Create skip-ssl-verify client for config from cli when secret is also set",
			args: args{
				ctx: ctx,
				etcdS3: &config.EtcdS3{
					AccessKey:     "test",
					Bucket:        "testbucket",
					Region:        "us-west-2",
					ConfigSecret:  "my-etcd-s3-config-secret",
					Endpoint:      listenerTLSAddr,
					SkipSSLVerify: true,
					Retention:     defaultEtcdS3.Retention,
					Timeout:       *defaultEtcdS3.Timeout.DeepCopy(),
				},
			},
			fields: fields{
				clusterID:   "1234",
				tokenHash:   "abcd",
				nodeName:    "server01",
				clientCache: util.NewCache[*Client](5),
			},
			setup: func(t *testing.T, a args, f fields, c *Client) (core.Interface, error) {
				coreMock := mock.NewCore(gomock.NewController(t))
				return coreMock, nil
			},
		},
		{
			name: "Create client for config from cli when secret is not set",
			args: args{
				ctx: ctx,
				etcdS3: &config.EtcdS3{
					AccessKey: "test",
					Bucket:    "testbucket",
					Region:    "us-west-2",
					Endpoint:  listenerAddr,
					Insecure:  true,
					Retention: defaultEtcdS3.Retention,
					Timeout:   *defaultEtcdS3.Timeout.DeepCopy(),
				},
			},
			fields: fields{
				clusterID:   "1234",
				tokenHash:   "abcd",
				nodeName:    "server01",
				clientCache: util.NewCache[*Client](5),
			},
			setup: func(t *testing.T, a args, f fields, c *Client) (core.Interface, error) {
				coreMock := mock.NewCore(gomock.NewController(t))
				return coreMock, nil
			},
		},
		{
			name: "Get cached client for config from secret",
			args: args{
				ctx: ctx,
				etcdS3: &config.EtcdS3{
					Endpoint:     defaultEtcdS3.Endpoint,
					Region:       defaultEtcdS3.Region,
					ConfigSecret: "my-etcd-s3-config-secret",
					Retention:    defaultEtcdS3.Retention,
					Timeout:      *defaultEtcdS3.Timeout.DeepCopy(),
				},
			},
			fields: fields{
				clusterID:   "1234",
				tokenHash:   "abcd",
				nodeName:    "server01",
				clientCache: util.NewCache[*Client](5),
			},
			want: &Client{},
			setup: func(t *testing.T, a args, f fields, c *Client) (core.Interface, error) {
				c.etcdS3 = &config.EtcdS3{
					AccessKey: "test",
					Bucket:    "testbucket",
					Endpoint:  listenerAddr,
					Insecure:  true,
					Region:    defaultEtcdS3.Region,
					Retention: defaultEtcdS3.Retention,
					Timeout:   *defaultEtcdS3.Timeout.DeepCopy(),
				}
				f.clientCache.Add(*c.etcdS3, c)
				coreMock := mock.NewCore(gomock.NewController(t))
				coreMock.V1Mock.SecretMock.EXPECT().Get(metav1.NamespaceSystem, "my-etcd-s3-config-secret", gomock.Any()).AnyTimes().DoAndReturn(func(namespace, name string, _ metav1.GetOptions) (*v1.Secret, error) {
					return &v1.Secret{
						ObjectMeta: metav1.ObjectMeta{
							Name:      name,
							Namespace: namespace,
						},
						Type: v1.SecretTypeOpaque,
						Data: map[string][]byte{
							"etcd-s3-access-key": []byte("test"),
							"etcd-s3-bucket":     []byte("testbucket"),
							"etcd-s3-endpoint":   []byte(listenerAddr),
							"etcd-s3-insecure":   []byte("true"),
							"etcd-s3-retention":  []byte("5"),
						},
					}, nil
				})
				return coreMock, nil
			},
		},
		{
			name: "Get cached client for config from cli",
			args: args{
				ctx: ctx,
				etcdS3: &config.EtcdS3{
					AccessKey:    "test",
					Bucket:       "testbucket",
					Region:       "us-west-2",
					ConfigSecret: "my-etcd-s3-config-secret",
					Endpoint:     listenerAddr,
					Insecure:     true,
					Retention:    defaultEtcdS3.Retention,
					Timeout:      *defaultEtcdS3.Timeout.DeepCopy(),
				},
			},
			fields: fields{
				clusterID:   "1234",
				tokenHash:   "abcd",
				nodeName:    "server01",
				clientCache: util.NewCache[*Client](5),
			},
			want: &Client{},
			setup: func(t *testing.T, a args, f fields, c *Client) (core.Interface, error) {
				c.etcdS3 = a.etcdS3
				f.clientCache.Add(*c.etcdS3, c)
				coreMock := mock.NewCore(gomock.NewController(t))
				return coreMock, nil
			},
		},
		{
			name: "Create client for config from cli with proxy",
			args: args{
				ctx: ctx,
				etcdS3: &config.EtcdS3{
					AccessKey:    "test",
					Bucket:       "testbucket",
					Region:       "us-west-2",
					ConfigSecret: "my-etcd-s3-config-secret",
					Endpoint:     listenerAddr,
					Insecure:     true,
					Retention:    defaultEtcdS3.Retention,
					Timeout:      *defaultEtcdS3.Timeout.DeepCopy(),
					Proxy:        "http://" + listenerAddr,
				},
			},
			fields: fields{
				clusterID:   "1234",
				tokenHash:   "abcd",
				nodeName:    "server01",
				clientCache: util.NewCache[*Client](5),
			},
			setup: func(t *testing.T, a args, f fields, c *Client) (core.Interface, error) {
				coreMock := mock.NewCore(gomock.NewController(t))
				return coreMock, nil
			},
		},
		{
			name: "Fail to create client for config from cli with invalid proxy",
			args: args{
				ctx: ctx,
				etcdS3: &config.EtcdS3{
					AccessKey:    "test",
					Bucket:       "testbucket",
					Region:       "us-west-2",
					ConfigSecret: "my-etcd-s3-config-secret",
					Endpoint:     listenerAddr,
					Insecure:     true,
					Retention:    defaultEtcdS3.Retention,
					Timeout:      *defaultEtcdS3.Timeout.DeepCopy(),
					Proxy:        "http://%invalid",
				},
			},
			fields: fields{
				clusterID:   "1234",
				tokenHash:   "abcd",
				nodeName:    "server01",
				clientCache: util.NewCache[*Client](5),
			},
			wantErr: true,
			setup: func(t *testing.T, a args, f fields, c *Client) (core.Interface, error) {
				coreMock := mock.NewCore(gomock.NewController(t))
				return coreMock, nil
			},
		},
		{
			name: "Fail to create client for config from cli with no proxy scheme",
			args: args{
				ctx: ctx,
				etcdS3: &config.EtcdS3{
					AccessKey:    "test",
					Bucket:       "testbucket",
					Region:       "us-west-2",
					ConfigSecret: "my-etcd-s3-config-secret",
					Endpoint:     listenerAddr,
					Insecure:     true,
					Retention:    defaultEtcdS3.Retention,
					Timeout:      *defaultEtcdS3.Timeout.DeepCopy(),
					Proxy:        "/proxy",
				},
			},
			fields: fields{
				clusterID:   "1234",
				tokenHash:   "abcd",
				nodeName:    "server01",
				clientCache: util.NewCache[*Client](5),
			},
			wantErr: true,
			setup: func(t *testing.T, a args, f fields, c *Client) (core.Interface, error) {
				coreMock := mock.NewCore(gomock.NewController(t))
				return coreMock, nil
			},
		},
		{
			name: "Create client for config from cli with CA path",
			args: args{
				ctx: ctx,
				etcdS3: &config.EtcdS3{
					AccessKey:    "test",
					Bucket:       "testbucket",
					Region:       "us-west-2",
					ConfigSecret: "my-etcd-s3-config-secret",
					Endpoint:     listenerTLSAddr,
					EndpointCA:   certFile,
					Retention:    defaultEtcdS3.Retention,
					Timeout:      *defaultEtcdS3.Timeout.DeepCopy(),
				},
			},
			fields: fields{
				clusterID:   "1234",
				tokenHash:   "abcd",
				nodeName:    "server01",
				clientCache: util.NewCache[*Client](5),
			},
			setup: func(t *testing.T, a args, f fields, c *Client) (core.Interface, error) {
				coreMock := mock.NewCore(gomock.NewController(t))
				return coreMock, nil
			},
		},
		{
			name: "Fail to create client for config from cli with invalid CA path",
			args: args{
				ctx: ctx,
				etcdS3: &config.EtcdS3{
					AccessKey:    "test",
					Bucket:       "testbucket",
					Region:       "us-west-2",
					ConfigSecret: "my-etcd-s3-config-secret",
					Endpoint:     listenerTLSAddr,
					EndpointCA:   "/does/not/exist",
					Retention:    defaultEtcdS3.Retention,
					Timeout:      *defaultEtcdS3.Timeout.DeepCopy(),
				},
			},
			fields: fields{
				clusterID:   "1234",
				tokenHash:   "abcd",
				nodeName:    "server01",
				clientCache: util.NewCache[*Client](5),
			},
			wantErr: true,
			setup: func(t *testing.T, a args, f fields, c *Client) (core.Interface, error) {
				coreMock := mock.NewCore(gomock.NewController(t))
				return coreMock, nil
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			core, err := tt.setup(t, tt.args, tt.fields, tt.want)
			if err != nil {
				t.Errorf("Setup for Controller.GetClient() failed = %v", err)
				return
			}
			c := &Controller{
				clusterID:   tt.fields.clusterID,
				tokenHash:   tt.fields.tokenHash,
				nodeName:    tt.fields.nodeName,
				clientCache: tt.fields.clientCache,
				core:        core,
			}
			got, err := c.GetClient(tt.args.ctx, tt.args.etcdS3)
			t.Logf("Got client=%#v err=%v", got, err)
			if (err != nil) != tt.wantErr {
				t.Errorf("Controller.GetClient() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if tt.want != nil && !reflect.DeepEqual(got, tt.want) {
				t.Errorf("Controller.GetClient() = %+v\nWant = %+v", got, tt.want)
			}
		})
	}
}

func Test_UnitClientUpload(t *testing.T) {
	logrus.SetLevel(logrus.DebugLevel)

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

	// Dummy server with http listener as a simple S3 mock
	server := &http.Server{Handler: s3Router(t)}

	listener, _ := net.Listen("tcp", ":0")

	_, port, _ := net.SplitHostPort(listener.Addr().String())
	listenerAddr := net.JoinHostPort("localhost", port)

	go server.Serve(listener)
	go func() {
		<-ctx.Done()
		server.Close()
	}()

	controller, err := Start(ctx, &config.Control{ClusterReset: true})
	if err != nil {
		t.Errorf("Start() for Client.Upload() failed = %v", err)
		return
	}

	tempDir := t.TempDir()
	metadataDir := filepath.Join(tempDir, ".metadata")
	snapshotDir := filepath.Join(tempDir, "snapshots")
	snapshotPath := filepath.Join(snapshotDir, "snapshot-01")
	metadataPath := filepath.Join(metadataDir, "snapshot-01")
	if err := os.Mkdir(snapshotDir, 0700); err != nil {
		t.Errorf("Mkdir() failed = %v", err)
		return
	}
	if err := os.Mkdir(metadataDir, 0700); err != nil {
		t.Errorf("Mkdir() failed = %v", err)
		return
	}
	if err := os.WriteFile(snapshotPath, []byte("test snapshot file\n"), 0600); err != nil {
		t.Errorf("WriteFile() failed = %v", err)
		return
	}
	if err := os.WriteFile(metadataPath, []byte("test snapshot metadata\n"), 0600); err != nil {
		t.Errorf("WriteFile() failed = %v", err)
		return
	}

	t.Logf("Using snapshot = %s, metadata = %s", snapshotPath, metadataPath)

	type fields struct {
		controller *Controller
		etcdS3     *config.EtcdS3
	}
	type args struct {
		ctx           context.Context
		snapshotPath  string
		extraMetadata *v1.ConfigMap
		now           time.Time
	}
	tests := []struct {
		name    string
		fields  fields
		args    args
		want    *snapshot.File
		wantErr bool
	}{
		{
			name: "Successful Upload",
			fields: fields{
				controller: controller,
				etcdS3: &config.EtcdS3{
					AccessKey: "test",
					Bucket:    "testbucket",
					Endpoint:  listenerAddr,
					Insecure:  true,
					Region:    defaultEtcdS3.Region,
					Retention: defaultEtcdS3.Retention,
					Timeout:   *defaultEtcdS3.Timeout.DeepCopy(),
				},
			},
			args: args{
				ctx:           ctx,
				snapshotPath:  snapshotPath,
				extraMetadata: &v1.ConfigMap{Data: map[string]string{"foo": "bar"}},
				now:           time.Now(),
			},
		},
		{
			name: "Successful Upload with Prefix",
			fields: fields{
				controller: controller,
				etcdS3: &config.EtcdS3{
					AccessKey: "test",
					Bucket:    "testbucket",
					Endpoint:  listenerAddr,
					Folder:    "testfolder",
					Insecure:  true,
					Region:    defaultEtcdS3.Region,
					Retention: defaultEtcdS3.Retention,
					Timeout:   *defaultEtcdS3.Timeout.DeepCopy(),
				},
			},
			args: args{
				ctx:           ctx,
				snapshotPath:  snapshotPath,
				extraMetadata: &v1.ConfigMap{Data: map[string]string{"foo": "bar"}},
				now:           time.Now(),
			},
		},
		{
			name: "Fails Upload to Nonexistent Bucket",
			fields: fields{
				controller: controller,
				etcdS3: &config.EtcdS3{
					AccessKey: "test",
					Bucket:    "badbucket",
					Endpoint:  listenerAddr,
					Insecure:  true,
					Region:    defaultEtcdS3.Region,
					Retention: defaultEtcdS3.Retention,
					Timeout:   *defaultEtcdS3.Timeout.DeepCopy(),
				},
			},
			args: args{
				ctx:           ctx,
				snapshotPath:  snapshotPath,
				extraMetadata: &v1.ConfigMap{Data: map[string]string{"foo": "bar"}},
				now:           time.Now(),
			},
			wantErr: true,
		},
		{
			name: "Fails Upload to Unauthorized Bucket",
			fields: fields{
				controller: controller,
				etcdS3: &config.EtcdS3{
					AccessKey: "test",
					Bucket:    "authbucket",
					Endpoint:  listenerAddr,
					Insecure:  true,
					Region:    defaultEtcdS3.Region,
					Retention: defaultEtcdS3.Retention,
					Timeout:   *defaultEtcdS3.Timeout.DeepCopy(),
				},
			},
			args: args{
				ctx:           ctx,
				snapshotPath:  snapshotPath,
				extraMetadata: &v1.ConfigMap{Data: map[string]string{"foo": "bar"}},
				now:           time.Now(),
			},
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			c, err := tt.fields.controller.GetClient(tt.args.ctx, tt.fields.etcdS3)
			if err != nil {
				if !tt.wantErr {
					t.Errorf("GetClient for Client.Upload() error = %v, wantErr %v", err, tt.wantErr)
				}
				return
			}
			got, err := c.Upload(tt.args.ctx, tt.args.snapshotPath, tt.args.extraMetadata, tt.args.now)
			t.Logf("Got File=%#v err=%v", got, err)
			if (err != nil) != tt.wantErr {
				t.Errorf("Client.Upload() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if tt.want != nil && !reflect.DeepEqual(got, tt.want) {
				t.Errorf("Client.Upload() = %+v\nWant = %+v", got, tt.want)
			}
		})
	}
}

func Test_UnitClientDownload(t *testing.T) {
	logrus.SetLevel(logrus.DebugLevel)

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

	// Dummy server with http listener as a simple S3 mock
	server := &http.Server{Handler: s3Router(t)}

	listener, _ := net.Listen("tcp", ":0")

	_, port, _ := net.SplitHostPort(listener.Addr().String())
	listenerAddr := net.JoinHostPort("localhost", port)

	go server.Serve(listener)
	go func() {
		<-ctx.Done()
		server.Close()
	}()

	controller, err := Start(ctx, &config.Control{ClusterReset: true})
	if err != nil {
		t.Errorf("Start() for Client.Download() failed = %v", err)
		return
	}

	snapshotName := "snapshot-01"
	tempDir := t.TempDir()
	metadataDir := filepath.Join(tempDir, ".metadata")
	snapshotDir := filepath.Join(tempDir, "snapshots")
	if err := os.Mkdir(snapshotDir, 0700); err != nil {
		t.Errorf("Mkdir() failed = %v", err)
		return
	}
	if err := os.Mkdir(metadataDir, 0700); err != nil {
		t.Errorf("Mkdir() failed = %v", err)
		return
	}

	type fields struct {
		etcdS3     *config.EtcdS3
		controller *Controller
	}
	type args struct {
		ctx          context.Context
		snapshotName string
		snapshotDir  string
	}
	tests := []struct {
		name    string
		fields  fields
		args    args
		want    string
		wantErr bool
	}{
		{
			name: "Successful Download",
			fields: fields{
				controller: controller,
				etcdS3: &config.EtcdS3{
					AccessKey: "test",
					Bucket:    "testbucket",
					Endpoint:  listenerAddr,
					Insecure:  true,
					Region:    defaultEtcdS3.Region,
					Retention: defaultEtcdS3.Retention,
					Timeout:   *defaultEtcdS3.Timeout.DeepCopy(),
				},
			},
			args: args{
				ctx:          ctx,
				snapshotName: snapshotName,
				snapshotDir:  snapshotDir,
			},
		},
		{
			name: "Unauthorizied Download",
			fields: fields{
				controller: controller,
				etcdS3: &config.EtcdS3{
					AccessKey: "test",
					Bucket:    "authbucket",
					Endpoint:  listenerAddr,
					Insecure:  true,
					Region:    defaultEtcdS3.Region,
					Retention: defaultEtcdS3.Retention,
					Timeout:   *defaultEtcdS3.Timeout.DeepCopy(),
				},
			},
			args: args{
				ctx:          ctx,
				snapshotName: snapshotName,
				snapshotDir:  snapshotDir,
			},
			wantErr: true,
		},
		{
			name: "Nonexistent Bucket",
			fields: fields{
				controller: controller,
				etcdS3: &config.EtcdS3{
					AccessKey: "test",
					Bucket:    "badbucket",
					Endpoint:  listenerAddr,
					Insecure:  true,
					Region:    defaultEtcdS3.Region,
					Retention: defaultEtcdS3.Retention,
					Timeout:   *defaultEtcdS3.Timeout.DeepCopy(),
				},
			},
			args: args{
				ctx:          ctx,
				snapshotName: snapshotName,
				snapshotDir:  snapshotDir,
			},
			wantErr: true,
		},
		{
			name: "Nonexistent Snapshot",
			fields: fields{
				controller: controller,
				etcdS3: &config.EtcdS3{
					AccessKey: "test",
					Bucket:    "testbucket",
					Endpoint:  listenerAddr,
					Insecure:  true,
					Region:    defaultEtcdS3.Region,
					Retention: defaultEtcdS3.Retention,
					Timeout:   *defaultEtcdS3.Timeout.DeepCopy(),
				},
			},
			args: args{
				ctx:          ctx,
				snapshotName: "badfile-1",
				snapshotDir:  snapshotDir,
			},
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			c, err := tt.fields.controller.GetClient(tt.args.ctx, tt.fields.etcdS3)
			if err != nil {
				if !tt.wantErr {
					t.Errorf("GetClient for Client.Upload() error = %v, wantErr %v", err, tt.wantErr)
				}
				return
			}
			got, err := c.Download(tt.args.ctx, tt.args.snapshotName, tt.args.snapshotDir)
			t.Logf("Got snapshotPath=%#v err=%v", got, err)
			if (err != nil) != tt.wantErr {
				t.Errorf("Client.Download() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if tt.want != "" && got != tt.want {
				t.Errorf("Client.Download() = %+v\nWant = %+v", got, tt.want)
			}
		})
	}
}

func Test_UnitClientListSnapshots(t *testing.T) {
	logrus.SetLevel(logrus.DebugLevel)

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

	// Dummy server with http listener as a simple S3 mock
	server := &http.Server{Handler: s3Router(t)}

	listener, _ := net.Listen("tcp", ":0")

	_, port, _ := net.SplitHostPort(listener.Addr().String())
	listenerAddr := net.JoinHostPort("localhost", port)

	go server.Serve(listener)
	go func() {
		<-ctx.Done()
		server.Close()
	}()

	controller, err := Start(ctx, &config.Control{ClusterReset: true})
	if err != nil {
		t.Errorf("Start() for Client.Download() failed = %v", err)
		return
	}

	type fields struct {
		etcdS3     *config.EtcdS3
		controller *Controller
	}
	type args struct {
		ctx context.Context
	}
	tests := []struct {
		name    string
		fields  fields
		args    args
		want    map[string]snapshot.File
		wantErr bool
	}{
		{
			name: "List Snapshots",
			fields: fields{
				etcdS3: &config.EtcdS3{
					AccessKey: "test",
					Bucket:    "testbucket",
					Endpoint:  listenerAddr,
					Insecure:  true,
					Region:    defaultEtcdS3.Region,
					Retention: defaultEtcdS3.Retention,
					Timeout:   *defaultEtcdS3.Timeout.DeepCopy(),
				},
				controller: controller,
			},
			args: args{
				ctx: ctx,
			},
		},
		{
			name: "List Snapshots with Prefix",
			fields: fields{
				etcdS3: &config.EtcdS3{
					AccessKey: "test",
					Bucket:    "testbucket",
					Endpoint:  listenerAddr,
					Folder:    "testfolder",
					Insecure:  true,
					Region:    defaultEtcdS3.Region,
					Retention: defaultEtcdS3.Retention,
					Timeout:   *defaultEtcdS3.Timeout.DeepCopy(),
				},
				controller: controller,
			},
			args: args{
				ctx: ctx,
			},
		},
		{
			name: "Fail to List Snapshots from Nonexistent Bucket",
			fields: fields{
				etcdS3: &config.EtcdS3{
					AccessKey: "test",
					Bucket:    "badbucket",
					Endpoint:  listenerAddr,
					Insecure:  true,
					Region:    defaultEtcdS3.Region,
					Retention: defaultEtcdS3.Retention,
					Timeout:   *defaultEtcdS3.Timeout.DeepCopy(),
				},
				controller: controller,
			},
			args: args{
				ctx: ctx,
			},
			wantErr: true,
		},
		{
			name: "Fail to List Snapshots from Unauthorized Bucket",
			fields: fields{
				etcdS3: &config.EtcdS3{
					AccessKey: "test",
					Bucket:    "authbucket",
					Endpoint:  listenerAddr,
					Insecure:  true,
					Region:    defaultEtcdS3.Region,
					Retention: defaultEtcdS3.Retention,
					Timeout:   *defaultEtcdS3.Timeout.DeepCopy(),
				},
				controller: controller,
			},
			args: args{
				ctx: ctx,
			},
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			c, err := tt.fields.controller.GetClient(tt.args.ctx, tt.fields.etcdS3)
			if err != nil {
				if !tt.wantErr {
					t.Errorf("GetClient for Client.Upload() error = %v, wantErr %v", err, tt.wantErr)
				}
				return
			}
			got, err := c.ListSnapshots(tt.args.ctx)
			t.Logf("Got snapshots=%#v err=%v", got, err)
			if (err != nil) != tt.wantErr {
				t.Errorf("Client.ListSnapshots() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if tt.want != nil && !reflect.DeepEqual(got, tt.want) {
				t.Errorf("Client.ListSnapshots() = %+v\nWant = %+v", got, tt.want)
			}
		})
	}
}

func Test_UnitClientDeleteSnapshot(t *testing.T) {
	logrus.SetLevel(logrus.DebugLevel)

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

	// Dummy server with http listener as a simple S3 mock
	server := &http.Server{Handler: s3Router(t)}

	listener, _ := net.Listen("tcp", ":0")

	_, port, _ := net.SplitHostPort(listener.Addr().String())
	listenerAddr := net.JoinHostPort("localhost", port)

	go server.Serve(listener)
	go func() {
		<-ctx.Done()
		server.Close()
	}()

	controller, err := Start(ctx, &config.Control{ClusterReset: true})
	if err != nil {
		t.Errorf("Start() for Client.Download() failed = %v", err)
		return
	}

	type fields struct {
		etcdS3     *config.EtcdS3
		controller *Controller
	}
	type args struct {
		ctx context.Context
		key string
	}
	tests := []struct {
		name    string
		fields  fields
		args    args
		wantErr bool
	}{
		{
			name: "Delete Snapshot",
			fields: fields{
				etcdS3: &config.EtcdS3{
					AccessKey: "test",
					Bucket:    "testbucket",
					Endpoint:  listenerAddr,
					Insecure:  true,
					Region:    defaultEtcdS3.Region,
					Retention: defaultEtcdS3.Retention,
					Timeout:   *defaultEtcdS3.Timeout.DeepCopy(),
				},
				controller: controller,
			},
			args: args{
				ctx: ctx,
				key: "snapshot-01",
			},
		},
		{
			name: "Fails to Delete from Nonexistent Bucket",
			fields: fields{
				etcdS3: &config.EtcdS3{
					AccessKey: "test",
					Bucket:    "badbucket",
					Endpoint:  listenerAddr,
					Insecure:  true,
					Region:    defaultEtcdS3.Region,
					Retention: defaultEtcdS3.Retention,
					Timeout:   *defaultEtcdS3.Timeout.DeepCopy(),
				},
				controller: controller,
			},
			args: args{
				ctx: ctx,
				key: "snapshot-01",
			},
			wantErr: true,
		},
		{
			name: "Fails to Delete from Unauthorized Bucket",
			fields: fields{
				etcdS3: &config.EtcdS3{
					AccessKey: "test",
					Bucket:    "authbucket",
					Endpoint:  listenerAddr,
					Insecure:  true,
					Region:    defaultEtcdS3.Region,
					Retention: defaultEtcdS3.Retention,
					Timeout:   *defaultEtcdS3.Timeout.DeepCopy(),
				},
				controller: controller,
			},
			args: args{
				ctx: ctx,
				key: "snapshot-01",
			},
			wantErr: true,
		},
		{
			name: "Fails to Delete Nonexistent Snapshot",
			fields: fields{
				etcdS3: &config.EtcdS3{
					AccessKey: "test",
					Bucket:    "testbucket",
					Endpoint:  listenerAddr,
					Insecure:  true,
					Region:    defaultEtcdS3.Region,
					Retention: defaultEtcdS3.Retention,
					Timeout:   *defaultEtcdS3.Timeout.DeepCopy(),
				},
				controller: controller,
			},
			args: args{
				ctx: ctx,
				key: "badfile-1",
			},
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			c, err := tt.fields.controller.GetClient(tt.args.ctx, tt.fields.etcdS3)
			if err != nil {
				if !tt.wantErr {
					t.Errorf("GetClient for Client.DeleteSnapshot() error = %v, wantErr %v", err, tt.wantErr)
				}
				return
			}
			err = c.DeleteSnapshot(tt.args.ctx, tt.args.key)
			t.Logf("DeleteSnapshot got error=%v", err)
			if (err != nil) != tt.wantErr {
				t.Errorf("Client.DeleteSnapshot() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

func Test_UnitClientSnapshotRetention(t *testing.T) {
	logrus.SetLevel(logrus.DebugLevel)

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

	// Dummy server with http listener as a simple S3 mock
	server := &http.Server{Handler: s3Router(t)}

	listener, _ := net.Listen("tcp", ":0")

	_, port, _ := net.SplitHostPort(listener.Addr().String())
	listenerAddr := net.JoinHostPort("localhost", port)

	go server.Serve(listener)
	go func() {
		<-ctx.Done()
		server.Close()
	}()

	controller, err := Start(ctx, &config.Control{ClusterReset: true})
	if err != nil {
		t.Errorf("Start() for Client.Download() failed = %v", err)
		return
	}

	type fields struct {
		etcdS3     *config.EtcdS3
		controller *Controller
	}
	type args struct {
		ctx    context.Context
		prefix string
	}
	tests := []struct {
		name    string
		fields  fields
		args    args
		want    []string
		wantErr bool
	}{
		{
			name: "Prune Snapshots - keep all, no folder",
			fields: fields{
				etcdS3: &config.EtcdS3{
					AccessKey: "test",
					Bucket:    "testbucket",
					Endpoint:  listenerAddr,
					Insecure:  true,
					Region:    defaultEtcdS3.Region,
					Retention: 10,
					Timeout:   *defaultEtcdS3.Timeout.DeepCopy(),
				},
				controller: controller,
			},
			args: args{
				ctx:    ctx,
				prefix: "snapshot-",
			},
		},
		{
			name: "Prune Snapshots keep 2 of 3, no folder",
			fields: fields{
				etcdS3: &config.EtcdS3{
					AccessKey: "test",
					Bucket:    "testbucket",
					Endpoint:  listenerAddr,
					Insecure:  true,
					Region:    defaultEtcdS3.Region,
					Retention: 2,
					Timeout:   *defaultEtcdS3.Timeout.DeepCopy(),
				},
				controller: controller,
			},
			args: args{
				ctx:    ctx,
				prefix: "snapshot-",
			},
			want: []string{"snapshot-03"},
		},
		{
			name: "Prune Snapshots - keep 1 of 3, no folder",
			fields: fields{
				etcdS3: &config.EtcdS3{
					AccessKey: "test",
					Bucket:    "testbucket",
					Endpoint:  listenerAddr,
					Insecure:  true,
					Region:    defaultEtcdS3.Region,
					Retention: 1,
					Timeout:   *defaultEtcdS3.Timeout.DeepCopy(),
				},
				controller: controller,
			},
			args: args{
				ctx:    ctx,
				prefix: "snapshot-",
			},
			want: []string{"snapshot-02", "snapshot-03"},
		},
		{
			name: "Prune Snapshots - keep all, with folder",
			fields: fields{
				etcdS3: &config.EtcdS3{
					AccessKey: "test",
					Bucket:    "testbucket",
					Endpoint:  listenerAddr,
					Folder:    "testfolder",
					Insecure:  true,
					Region:    defaultEtcdS3.Region,
					Retention: 10,
					Timeout:   *defaultEtcdS3.Timeout.DeepCopy(),
				},
				controller: controller,
			},
			args: args{
				ctx:    ctx,
				prefix: "snapshot-",
			},
		},
		{
			name: "Prune Snapshots keep 2 of 3, with folder",
			fields: fields{
				etcdS3: &config.EtcdS3{
					AccessKey: "test",
					Bucket:    "testbucket",
					Endpoint:  listenerAddr,
					Folder:    "testfolder",
					Insecure:  true,
					Region:    defaultEtcdS3.Region,
					Retention: 2,
					Timeout:   *defaultEtcdS3.Timeout.DeepCopy(),
				},
				controller: controller,
			},
			args: args{
				ctx:    ctx,
				prefix: "snapshot-",
			},
			want: []string{"snapshot-06"},
		},
		{
			name: "Prune Snapshots - keep 1 of 3, with folder",
			fields: fields{
				etcdS3: &config.EtcdS3{
					AccessKey: "test",
					Bucket:    "testbucket",
					Endpoint:  listenerAddr,
					Folder:    "testfolder",
					Insecure:  true,
					Region:    defaultEtcdS3.Region,
					Retention: 1,
					Timeout:   *defaultEtcdS3.Timeout.DeepCopy(),
				},
				controller: controller,
			},
			args: args{
				ctx:    ctx,
				prefix: "snapshot-",
			},
			want: []string{"snapshot-05", "snapshot-06"},
		},
		{
			name: "Fail to Prune from Unauthorized Bucket",
			fields: fields{
				etcdS3: &config.EtcdS3{
					AccessKey: "test",
					Bucket:    "authbucket",
					Endpoint:  listenerAddr,
					Insecure:  true,
					Region:    defaultEtcdS3.Region,
					Retention: 1,
					Timeout:   *defaultEtcdS3.Timeout.DeepCopy(),
				},
				controller: controller,
			},
			args: args{
				ctx:    ctx,
				prefix: "snapshot-",
			},
			wantErr: true,
		},
		{
			name: "Fail to Prune from Nonexistent Bucket",
			fields: fields{
				etcdS3: &config.EtcdS3{
					AccessKey: "test",
					Bucket:    "badbucket",
					Endpoint:  listenerAddr,
					Insecure:  true,
					Region:    defaultEtcdS3.Region,
					Retention: 1,
					Timeout:   *defaultEtcdS3.Timeout.DeepCopy(),
				},
				controller: controller,
			},
			args: args{
				ctx:    ctx,
				prefix: "snapshot-",
			},
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			c, err := tt.fields.controller.GetClient(tt.args.ctx, tt.fields.etcdS3)
			if err != nil {
				if !tt.wantErr {
					t.Errorf("GetClient for Client.SnapshotRetention() error = %v, wantErr %v", err, tt.wantErr)
				}
				return
			}
			got, err := c.SnapshotRetention(tt.args.ctx, tt.args.prefix)
			t.Logf("Got snapshots=%#v err=%v", got, err)
			if (err != nil) != tt.wantErr {
				t.Errorf("Client.SnapshotRetention() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("Client.SnapshotRetention() = %+v\nWant = %+v", got, tt.want)
			}
		})
	}
}

//
//  ListObjects response body template
//

var listObjectsV2ResponseTemplate = `
{{- /* */ -}}
{{ with $b := . -}}
<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Name>{{$b.Name}}</Name>
	{{ if $b.Prefix }}<Prefix>{{$b.Prefix}}</Prefix>{{ else }}<Prefix/>{{ end }}
  <KeyCount>{{ len $b.Objects }}</KeyCount>
  <MaxKeys>1000</MaxKeys>
  <Delimiter/>
  <IsTruncated>false</IsTruncated>
  {{- range $o := $b.Objects }}
  <Contents>
    <Key>{{ $o.Key }}</Key>
    <LastModified>{{ $o.LastModified }}</LastModified>
    <ETag>{{ printf "%q" $o.ETag }}</ETag>
    <Size>{{ $o.Size }}</Size>
    <Owner>
      <ID>0</ID>
      <DisplayName>test</DisplayName>
    </Owner>
    <StorageClass>STANDARD</StorageClass>
  </Contents>
  {{- end }}
  <EncodingType>url</EncodingType>
</ListBucketResult>
{{- end }}
`

func s3Router(t *testing.T) http.Handler {
	var listResponse = template.Must(template.New("listObjectsV2").Parse(listObjectsV2ResponseTemplate))

	type object struct {
		Key          string
		LastModified string
		ETag         string
		Size         int
	}

	type bucket struct {
		Name    string
		Prefix  string
		Objects []object
	}

	snapshotId := 0
	objects := []object{}
	timestamp := time.Now().Format(time.RFC3339)
	for _, prefix := range []string{"", "testfolder", "testfolder/netsted", "otherfolder"} {
		for idx := range []int{0, 1, 2} {
			snapshotId++
			objects = append(objects, object{
				Key:          path.Join(prefix, fmt.Sprintf("snapshot-%02d", snapshotId)),
				LastModified: timestamp,
				ETag:         "0000",
				Size:         100,
			})
			if idx != 0 {
				objects = append(objects, object{
					Key:          path.Join(prefix, fmt.Sprintf(".metadata/snapshot-%02d", snapshotId)),
					LastModified: timestamp,
					ETag:         "0000",
					Size:         10,
				})
			}
		}
	}

	// badbucket returns 404 for all requests
	// authbucket returns 200 for HeadBucket, 403 for all others
	// others return 200 for objects with name prefix snapshot, 404 for all others
	router := mux.NewRouter()
	// HeadBucket
	// ListObjectsV2
	router.HandleFunc("GET /{bucket}/{$}", func(rw http.ResponseWriter, r *http.Request) {
		switch r.Method {
		case http.MethodHead:
			if r.PathValue("bucket") == "badbucket" {
				rw.WriteHeader(http.StatusNotFound)
			}
		case http.MethodGet:
			switch r.PathValue("bucket") {
			case "badbucket":
				rw.WriteHeader(http.StatusNotFound)
			case "authbucket":
				rw.WriteHeader(http.StatusForbidden)
			default:
				prefix := r.URL.Query().Get("prefix")
				filtered := []object{}
				for _, object := range objects {
					if strings.HasPrefix(object.Key, prefix) {
						filtered = append(filtered, object)
					}
				}
				if err := listResponse.Execute(rw, bucket{Name: r.PathValue("bucket"), Prefix: prefix, Objects: filtered}); err != nil {
					t.Errorf("Failed to generate ListObjectsV2 response, error = %v", err)
					rw.WriteHeader(http.StatusInternalServerError)
				}
			}
		}
	})
	// HeadObject
	// GetObject
	router.HandleFunc("GET /{bucket}/{object...}", func(rw http.ResponseWriter, r *http.Request) {
		switch r.Method {
		case http.MethodHead:
			switch r.PathValue("bucket") {
			case "badbucket":
				rw.WriteHeader(http.StatusNotFound)
			case "authbucket":
				rw.WriteHeader(http.StatusForbidden)
			default:
				if strings.Contains(r.PathValue("object"), "bad") {
					rw.WriteHeader(http.StatusNotFound)
				} else {
					rw.Header().Add("last-modified", time.Now().In(gmt).Format(time.RFC1123))
				}
			}
		case http.MethodGet:
			switch r.PathValue("bucket") {
			case "badbucket":
				rw.WriteHeader(http.StatusNotFound)
			case "authbucket":
				rw.WriteHeader(http.StatusForbidden)
			default:
				if strings.Contains(r.PathValue("object"), "bad") {
					rw.WriteHeader(http.StatusNotFound)
				} else {
					rw.Header().Add("last-modified", time.Now().In(gmt).Format(time.RFC1123))
					rw.Write([]byte("test snapshot file\n"))
				}
			}
		}
	})
	// PutObject
	router.HandleFunc("PUT /{bucket}/{object...}", func(rw http.ResponseWriter, r *http.Request) {
		switch r.PathValue("bucket") {
		case "badbucket":
			rw.WriteHeader(http.StatusNotFound)
		case "authbucket":
			rw.WriteHeader(http.StatusForbidden)
		default:
			if r.Method == http.MethodDelete {
				rw.WriteHeader(http.StatusNoContent)
			}
		}
	})
	// DeleteObject
	router.HandleFunc("DELETE /{bucket}/{object...}", func(rw http.ResponseWriter, r *http.Request) {
		switch r.PathValue("bucket") {
		case "badbucket":
			rw.WriteHeader(http.StatusNotFound)
		case "authbucket":
			rw.WriteHeader(http.StatusForbidden)
		default:
			if r.Method == http.MethodDelete {
				if strings.Contains(r.PathValue("object"), "bad") {
					rw.WriteHeader(http.StatusNotFound)
				} else {
					rw.WriteHeader(http.StatusNoContent)
				}
			}
		}
	})
	router.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
		logrus.Errorf("Failed to match %q", r.URL)
		rw.WriteHeader(http.StatusInternalServerError)
	})
	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
		scheme := "http"
		if r.TLS != nil {
			scheme = "https"
		}
		logrus.Infof("%s %s://%s %s", r.Method, scheme, r.Host, r.URL)
		router.ServeHTTP(rw, r)
	})
}
