package handler

import (
	"crypto/md5"
	"crypto/tls"
	"encoding/base64"
	"encoding/xml"
	"net/http"
	"net/url"
	"strconv"
	"testing"

	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/encryption"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
	"github.com/stretchr/testify/require"
)

type CopyMeta struct {
	TaggingDirective  string
	Tags              map[string]string
	MetadataDirective string
	Metadata          map[string]string
	Headers           map[string]string
}

func TestCopyWithTaggingDirective(t *testing.T) {
	tc := prepareHandlerContext(t)

	bktName, objName := "bucket-for-copy", "object-from-copy"
	objToCopy, objToCopy2 := "object-to-copy", "object-to-copy-2"
	createBucketAndObject(tc, bktName, objName)

	putObjectTagging(t, tc, bktName, objName, map[string]string{"key": "val"})

	copyMeta := CopyMeta{
		Tags: map[string]string{"key2": "val"},
	}
	copyObject(tc, bktName, objName, objToCopy, copyMeta, http.StatusOK)
	tagging := getObjectTagging(t, tc, bktName, objToCopy, emptyVersion)
	require.Len(t, tagging.TagSet, 1)
	require.Equal(t, "key", tagging.TagSet[0].Key)
	require.Equal(t, "val", tagging.TagSet[0].Value)

	copyMeta.TaggingDirective = replaceDirective
	copyObject(tc, bktName, objName, objToCopy2, copyMeta, http.StatusOK)
	tagging = getObjectTagging(t, tc, bktName, objToCopy2, emptyVersion)
	require.Len(t, tagging.TagSet, 1)
	require.Equal(t, "key2", tagging.TagSet[0].Key)
	require.Equal(t, "val", tagging.TagSet[0].Value)
}

func TestCopyToItself(t *testing.T) {
	tc := prepareHandlerContext(t)

	bktName, objName := "bucket-for-copy", "object-for-copy"
	createBucketAndObject(tc, bktName, objName)

	copyMeta := CopyMeta{MetadataDirective: replaceDirective}

	copyObject(tc, bktName, objName, objName, CopyMeta{}, http.StatusBadRequest)
	copyObject(tc, bktName, objName, objName, copyMeta, http.StatusOK)

	putBucketVersioning(t, tc, bktName, true)
	copyObject(tc, bktName, objName, objName, CopyMeta{}, http.StatusOK)
	copyObject(tc, bktName, objName, objName, copyMeta, http.StatusOK)

	putBucketVersioning(t, tc, bktName, false)
	copyObject(tc, bktName, objName, objName, CopyMeta{}, http.StatusOK)
	copyObject(tc, bktName, objName, objName, copyMeta, http.StatusOK)
}

func TestCopyMultipart(t *testing.T) {
	hc := prepareHandlerContext(t)

	bktName, objName := "bucket-for-copy", "object-for-copy"
	createTestBucket(hc, bktName)

	partSize := layer.UploadMinSize
	objLen := 6 * partSize
	headers := map[string]string{}

	data := multipartUpload(hc, bktName, objName, headers, objLen, partSize)
	require.Equal(t, objLen, len(data))

	objToCopy := "copy-target"
	var copyMeta CopyMeta
	copyObject(hc, bktName, objName, objToCopy, copyMeta, http.StatusOK)

	copiedData, _ := getObject(hc, bktName, objToCopy)
	equalDataSlices(t, data, copiedData)

	result := getObjectAttributes(hc, bktName, objToCopy, objectParts)
	require.NotNil(t, result.ObjectParts)

	objToCopy2 := "copy-target2"
	copyMeta.MetadataDirective = replaceDirective
	copyObject(hc, bktName, objName, objToCopy2, copyMeta, http.StatusOK)

	result = getObjectAttributes(hc, bktName, objToCopy2, objectParts)
	require.Nil(t, result.ObjectParts)

	copiedData, _ = getObject(hc, bktName, objToCopy2)
	equalDataSlices(t, data, copiedData)
}

func TestCopyEncryptedToUnencrypted(t *testing.T) {
	tc := prepareHandlerContext(t)

	bktName, srcObjName := "bucket-for-copy", "object-for-copy"
	key1 := []byte("firstencriptionkeyofsourceobject")
	key1Md5 := md5.Sum(key1)
	key2 := []byte("anotherencriptionkeysourceobject")
	key2Md5 := md5.Sum(key2)
	bktInfo := createTestBucket(tc, bktName)

	srcEnc, err := encryption.NewParams(key1)
	require.NoError(t, err)
	srcObjInfo := createTestObject(tc, bktInfo, srcObjName, *srcEnc)
	require.True(t, containEncryptionMetadataHeaders(srcObjInfo.Headers))

	dstObjName := "copy-object"

	// empty copy-source-sse headers
	w, r := prepareTestRequest(tc, bktName, dstObjName, nil)
	r.TLS = &tls.ConnectionState{}
	r.Header.Set(api.AmzCopySource, bktName+"/"+srcObjName)
	tc.Handler().CopyObjectHandler(w, r)

	assertStatus(t, w, http.StatusBadRequest)
	assertS3Error(t, w, errors.GetAPIError(errors.ErrSSEEncryptedObject))

	// empty copy-source-sse-custom-key
	w, r = prepareTestRequest(tc, bktName, dstObjName, nil)
	r.TLS = &tls.ConnectionState{}
	r.Header.Set(api.AmzCopySource, bktName+"/"+srcObjName)
	r.Header.Set(api.AmzCopySourceServerSideEncryptionCustomerAlgorithm, layer.AESEncryptionAlgorithm)
	tc.Handler().CopyObjectHandler(w, r)

	assertStatus(t, w, http.StatusBadRequest)
	assertS3Error(t, w, errors.GetAPIError(errors.ErrMissingSSECustomerKey))

	// empty copy-source-sse-custom-algorithm
	w, r = prepareTestRequest(tc, bktName, dstObjName, nil)
	r.TLS = &tls.ConnectionState{}
	r.Header.Set(api.AmzCopySource, bktName+"/"+srcObjName)
	r.Header.Set(api.AmzCopySourceServerSideEncryptionCustomerKey, base64.StdEncoding.EncodeToString(key1))
	tc.Handler().CopyObjectHandler(w, r)

	assertStatus(t, w, http.StatusBadRequest)
	assertS3Error(t, w, errors.GetAPIError(errors.ErrMissingSSECustomerAlgorithm))

	// invalid copy-source-sse-custom-key
	w, r = prepareTestRequest(tc, bktName, dstObjName, nil)
	r.TLS = &tls.ConnectionState{}
	r.Header.Set(api.AmzCopySource, bktName+"/"+srcObjName)
	r.Header.Set(api.AmzCopySourceServerSideEncryptionCustomerAlgorithm, layer.AESEncryptionAlgorithm)
	r.Header.Set(api.AmzCopySourceServerSideEncryptionCustomerKey, base64.StdEncoding.EncodeToString(key2))
	r.Header.Set(api.AmzCopySourceServerSideEncryptionCustomerKeyMD5, base64.StdEncoding.EncodeToString(key2Md5[:]))
	tc.Handler().CopyObjectHandler(w, r)

	assertStatus(t, w, http.StatusBadRequest)
	assertS3Error(t, w, errors.GetAPIError(errors.ErrInvalidSSECustomerParameters))

	// success copy
	w, r = prepareTestRequest(tc, bktName, dstObjName, nil)
	r.TLS = &tls.ConnectionState{}
	r.Header.Set(api.AmzCopySource, bktName+"/"+srcObjName)
	r.Header.Set(api.AmzCopySourceServerSideEncryptionCustomerAlgorithm, layer.AESEncryptionAlgorithm)
	r.Header.Set(api.AmzCopySourceServerSideEncryptionCustomerKey, base64.StdEncoding.EncodeToString(key1))
	r.Header.Set(api.AmzCopySourceServerSideEncryptionCustomerKeyMD5, base64.StdEncoding.EncodeToString(key1Md5[:]))
	tc.Handler().CopyObjectHandler(w, r)

	assertStatus(t, w, http.StatusOK)
	dstObjInfo, err := tc.Layer().GetObjectInfo(tc.Context(), &layer.HeadObjectParams{BktInfo: bktInfo, Object: dstObjName})
	require.NoError(t, err)
	require.Equal(t, srcObjInfo.Headers[layer.AttributeDecryptedSize], strconv.Itoa(int(dstObjInfo.Size)))
	require.False(t, containEncryptionMetadataHeaders(dstObjInfo.Headers))
}

func TestCopyUnencryptedToEncrypted(t *testing.T) {
	tc := prepareHandlerContext(t)

	bktName, srcObjName := "bucket-for-copy", "object-for-copy"
	key := []byte("firstencriptionkeyofsourceobject")
	keyMd5 := md5.Sum(key)
	bktInfo := createTestBucket(tc, bktName)

	srcObjInfo := createTestObject(tc, bktInfo, srcObjName, encryption.Params{})
	require.False(t, containEncryptionMetadataHeaders(srcObjInfo.Headers))

	dstObjName := "copy-object"

	// invalid copy-source-sse headers
	w, r := prepareTestRequest(tc, bktName, dstObjName, nil)
	r.TLS = &tls.ConnectionState{}
	r.Header.Set(api.AmzCopySource, bktName+"/"+srcObjName)
	r.Header.Set(api.AmzCopySourceServerSideEncryptionCustomerAlgorithm, layer.AESEncryptionAlgorithm)
	r.Header.Set(api.AmzCopySourceServerSideEncryptionCustomerKey, base64.StdEncoding.EncodeToString(key))
	r.Header.Set(api.AmzCopySourceServerSideEncryptionCustomerKeyMD5, base64.StdEncoding.EncodeToString(keyMd5[:]))
	tc.Handler().CopyObjectHandler(w, r)

	assertStatus(t, w, http.StatusBadRequest)
	assertS3Error(t, w, errors.GetAPIError(errors.ErrInvalidEncryptionParameters))

	// success copy
	w, r = prepareTestRequest(tc, bktName, dstObjName, nil)
	r.TLS = &tls.ConnectionState{}
	r.Header.Set(api.AmzCopySource, bktName+"/"+srcObjName)
	r.Header.Set(api.AmzServerSideEncryptionCustomerAlgorithm, layer.AESEncryptionAlgorithm)
	r.Header.Set(api.AmzServerSideEncryptionCustomerKey, base64.StdEncoding.EncodeToString(key))
	r.Header.Set(api.AmzServerSideEncryptionCustomerKeyMD5, base64.StdEncoding.EncodeToString(keyMd5[:]))
	tc.Handler().CopyObjectHandler(w, r)

	assertStatus(t, w, http.StatusOK)
	dstObjInfo, err := tc.Layer().GetObjectInfo(tc.Context(), &layer.HeadObjectParams{BktInfo: bktInfo, Object: dstObjName})
	require.NoError(t, err)
	require.True(t, containEncryptionMetadataHeaders(dstObjInfo.Headers))
	require.Equal(t, strconv.Itoa(int(srcObjInfo.Size)), dstObjInfo.Headers[layer.AttributeDecryptedSize])
}

func TestCopyEncryptedToEncryptedWithAnotherKey(t *testing.T) {
	tc := prepareHandlerContext(t)

	bktName, srcObjName := "bucket-for-copy", "object-for-copy"
	key1 := []byte("firstencriptionkeyofsourceobject")
	key1Md5 := md5.Sum(key1)
	key2 := []byte("anotherencriptionkeysourceobject")
	key2Md5 := md5.Sum(key2)
	bktInfo := createTestBucket(tc, bktName)

	srcEnc, err := encryption.NewParams(key1)
	require.NoError(t, err)
	srcObjInfo := createTestObject(tc, bktInfo, srcObjName, *srcEnc)
	require.True(t, containEncryptionMetadataHeaders(srcObjInfo.Headers))

	dstObjName := "copy-object"

	w, r := prepareTestRequest(tc, bktName, dstObjName, nil)
	r.TLS = &tls.ConnectionState{}
	r.Header.Set(api.AmzCopySource, bktName+"/"+srcObjName)
	r.Header.Set(api.AmzCopySourceServerSideEncryptionCustomerAlgorithm, layer.AESEncryptionAlgorithm)
	r.Header.Set(api.AmzCopySourceServerSideEncryptionCustomerKey, base64.StdEncoding.EncodeToString(key1))
	r.Header.Set(api.AmzCopySourceServerSideEncryptionCustomerKeyMD5, base64.StdEncoding.EncodeToString(key1Md5[:]))
	r.Header.Set(api.AmzServerSideEncryptionCustomerAlgorithm, layer.AESEncryptionAlgorithm)
	r.Header.Set(api.AmzServerSideEncryptionCustomerKey, base64.StdEncoding.EncodeToString(key2))
	r.Header.Set(api.AmzServerSideEncryptionCustomerKeyMD5, base64.StdEncoding.EncodeToString(key2Md5[:]))
	tc.Handler().CopyObjectHandler(w, r)

	assertStatus(t, w, http.StatusOK)
	dstObjInfo, err := tc.Layer().GetObjectInfo(tc.Context(), &layer.HeadObjectParams{BktInfo: bktInfo, Object: dstObjName})
	require.NoError(t, err)
	require.True(t, containEncryptionMetadataHeaders(dstObjInfo.Headers))
	require.Equal(t, srcObjInfo.Headers[layer.AttributeDecryptedSize], dstObjInfo.Headers[layer.AttributeDecryptedSize])
}

func containEncryptionMetadataHeaders(headers map[string]string) bool {
	for k := range headers {
		if _, ok := layer.EncryptionMetadata[k]; ok {
			return true
		}
	}
	return false
}

func copyObject(hc *handlerContext, bktName, fromObject, toObject string, copyMeta CopyMeta, statusCode int) {
	w, r := prepareTestRequest(hc, bktName, toObject, nil)
	r.Header.Set(api.AmzCopySource, bktName+"/"+fromObject)

	r.Header.Set(api.AmzMetadataDirective, copyMeta.MetadataDirective)
	for key, val := range copyMeta.Metadata {
		r.Header.Set(api.MetadataPrefix+key, val)
	}

	r.Header.Set(api.AmzTaggingDirective, copyMeta.TaggingDirective)
	tagsQuery := make(url.Values)
	for key, val := range copyMeta.Tags {
		tagsQuery.Set(key, val)
	}
	r.Header.Set(api.AmzTagging, tagsQuery.Encode())

	for key, val := range copyMeta.Headers {
		r.Header.Set(key, val)
	}

	hc.Handler().CopyObjectHandler(w, r)
	assertStatus(hc.t, w, statusCode)
}

func putObjectTagging(t *testing.T, tc *handlerContext, bktName, objName string, tags map[string]string) {
	body := &data.Tagging{
		TagSet: make([]data.Tag, 0, len(tags)),
	}

	for key, val := range tags {
		body.TagSet = append(body.TagSet, data.Tag{
			Key:   key,
			Value: val,
		})
	}

	w, r := prepareTestRequest(tc, bktName, objName, body)
	middleware.GetReqInfo(r.Context()).Tagging = body
	tc.Handler().PutObjectTaggingHandler(w, r)
	assertStatus(t, w, http.StatusOK)
}

func getObjectTagging(t *testing.T, tc *handlerContext, bktName, objName, version string) *data.Tagging {
	query := make(url.Values)
	query.Add(api.QueryVersionID, version)

	w, r := prepareTestFullRequest(tc, bktName, objName, query, nil)
	tc.Handler().GetObjectTaggingHandler(w, r)
	assertStatus(t, w, http.StatusOK)

	tagging := &data.Tagging{}
	err := xml.NewDecoder(w.Result().Body).Decode(tagging)
	require.NoError(t, err)
	return tagging
}

func TestSourceCopyRegexp(t *testing.T) {
	for _, tc := range []struct {
		path    string
		err     bool
		bktName string
		objName string
	}{
		{
			path:    "/bucket/object",
			err:     false,
			bktName: "bucket",
			objName: "object",
		},
		{
			path:    "bucket/object",
			err:     false,
			bktName: "bucket",
			objName: "object",
		},
		{
			path:    "sub-bucket/object",
			err:     false,
			bktName: "sub-bucket",
			objName: "object",
		},
		{
			path:    "bucket.domain/object",
			err:     false,
			bktName: "bucket.domain",
			objName: "object",
		},
		{
			path:    "bucket/object/deep",
			err:     false,
			bktName: "bucket",
			objName: "object/deep",
		},
		{
			path: "bucket",
			err:  true,
		},
		{
			path: "/bucket",
			err:  true,
		},
		{
			path: "invalid+bucket/object",
			err:  true,
		},
		{
			path: "invaliDBucket/object",
			err:  true,
		},
		{
			path: "i/object",
			err:  true,
		},
	} {
		t.Run("", func(t *testing.T) {
			bktName, objName, err := path2BucketObject(tc.path)
			if tc.err {
				require.Error(t, err)
				return
			}

			require.NoError(t, err)
			require.Equal(t, tc.bktName, bktName)
			require.Equal(t, tc.objName, objName)
		})
	}
}