package middleware

import (
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/stretchr/testify/require"
)

func TestReqTypeDetermination(t *testing.T) {
	bkt, obj, domain := "test-bucket", "test-object", "domain"

	for _, tc := range []struct {
		name            string
		target          string
		host            string
		domains         []string
		expectedType    ReqType
		expectedBktName string
		expectedObjName string
	}{
		{
			name:            "bucket request, path-style",
			target:          "/" + bkt,
			expectedType:    bucketType,
			expectedBktName: bkt,
		},
		{
			name:            "bucket request with slash, path-style",
			target:          "/" + bkt + "/",
			expectedType:    bucketType,
			expectedBktName: bkt,
		},
		{
			name:            "object request, path-style",
			target:          "/" + bkt + "/" + obj,
			expectedType:    objectType,
			expectedBktName: bkt,
			expectedObjName: obj,
		},
		{
			name:            "object request with slash, path-style",
			target:          "/" + bkt + "/" + obj + "/",
			expectedType:    objectType,
			expectedBktName: bkt,
			expectedObjName: obj + "/",
		},
		{
			name:         "none type request",
			target:       "/",
			expectedType: noneType,
		},
		{
			name:            "bucket request, virtual-hosted style",
			target:          "/",
			host:            bkt + "." + domain,
			domains:         []string{"some-domain", domain},
			expectedType:    bucketType,
			expectedBktName: bkt,
		},
		{
			name:            "object request, virtual-hosted style",
			target:          "/" + obj,
			host:            bkt + "." + domain,
			domains:         []string{"some-domain", domain},
			expectedType:    objectType,
			expectedBktName: bkt,
			expectedObjName: obj,
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			r := httptest.NewRequest(http.MethodPut, tc.target, nil)
			r.Host = tc.host

			reqType, bktName, objName := getBucketObject(r, tc.domains)
			require.Equal(t, tc.expectedType, reqType)
			require.Equal(t, tc.expectedBktName, bktName)
			require.Equal(t, tc.expectedObjName, objName)
		})
	}
}

func TestDetermineBucketOperation(t *testing.T) {
	const defaultValue = "value"

	for _, tc := range []struct {
		name       string
		method     string
		queryParam map[string]string
		expected   string
	}{
		{
			name:     "OptionsBucketOperation",
			method:   http.MethodOptions,
			expected: OptionsBucketOperation,
		},
		{
			name:     "HeadBucketOperation",
			method:   http.MethodHead,
			expected: HeadBucketOperation,
		},
		{
			name:       "ListMultipartUploadsOperation",
			method:     http.MethodGet,
			queryParam: map[string]string{UploadsQuery: defaultValue},
			expected:   ListMultipartUploadsOperation,
		},
		{
			name:       "GetBucketLocationOperation",
			method:     http.MethodGet,
			queryParam: map[string]string{LocationQuery: defaultValue},
			expected:   GetBucketLocationOperation,
		},
		{
			name:       "GetBucketPolicyOperation",
			method:     http.MethodGet,
			queryParam: map[string]string{PolicyQuery: defaultValue},
			expected:   GetBucketPolicyOperation,
		},
		{
			name:       "GetBucketLifecycleOperation",
			method:     http.MethodGet,
			queryParam: map[string]string{LifecycleQuery: defaultValue},
			expected:   GetBucketLifecycleOperation,
		},
		{
			name:       "GetBucketEncryptionOperation",
			method:     http.MethodGet,
			queryParam: map[string]string{EncryptionQuery: defaultValue},
			expected:   GetBucketEncryptionOperation,
		},
		{
			name:       "GetBucketCorsOperation",
			method:     http.MethodGet,
			queryParam: map[string]string{CorsQuery: defaultValue},
			expected:   GetBucketCorsOperation,
		},
		{
			name:       "GetBucketACLOperation",
			method:     http.MethodGet,
			queryParam: map[string]string{ACLQuery: defaultValue},
			expected:   GetBucketACLOperation,
		},
		{
			name:       "GetBucketWebsiteOperation",
			method:     http.MethodGet,
			queryParam: map[string]string{WebsiteQuery: defaultValue},
			expected:   GetBucketWebsiteOperation,
		},
		{
			name:       "GetBucketAccelerateOperation",
			method:     http.MethodGet,
			queryParam: map[string]string{AccelerateQuery: defaultValue},
			expected:   GetBucketAccelerateOperation,
		},
		{
			name:       "GetBucketRequestPaymentOperation",
			method:     http.MethodGet,
			queryParam: map[string]string{RequestPaymentQuery: defaultValue},
			expected:   GetBucketRequestPaymentOperation,
		},
		{
			name:       "GetBucketLoggingOperation",
			method:     http.MethodGet,
			queryParam: map[string]string{LoggingQuery: defaultValue},
			expected:   GetBucketLoggingOperation,
		},
		{
			name:       "GetBucketReplicationOperation",
			method:     http.MethodGet,
			queryParam: map[string]string{ReplicationQuery: defaultValue},
			expected:   GetBucketReplicationOperation,
		},
		{
			name:       "GetBucketTaggingOperation",
			method:     http.MethodGet,
			queryParam: map[string]string{TaggingQuery: defaultValue},
			expected:   GetBucketTaggingOperation,
		},
		{
			name:       "GetBucketObjectLockConfigOperation",
			method:     http.MethodGet,
			queryParam: map[string]string{ObjectLockQuery: defaultValue},
			expected:   GetBucketObjectLockConfigOperation,
		},
		{
			name:       "GetBucketVersioningOperation",
			method:     http.MethodGet,
			queryParam: map[string]string{VersioningQuery: defaultValue},
			expected:   GetBucketVersioningOperation,
		},
		{
			name:       "GetBucketNotificationOperation",
			method:     http.MethodGet,
			queryParam: map[string]string{NotificationQuery: defaultValue},
			expected:   GetBucketNotificationOperation,
		},
		{
			name:       "ListenBucketNotificationOperation",
			method:     http.MethodGet,
			queryParam: map[string]string{EventsQuery: defaultValue},
			expected:   ListenBucketNotificationOperation,
		},
		{
			name:       "ListBucketObjectVersionsOperation",
			method:     http.MethodGet,
			queryParam: map[string]string{VersionsQuery: defaultValue},
			expected:   ListBucketObjectVersionsOperation,
		},
		{
			name:       "ListObjectsV2MOperation",
			method:     http.MethodGet,
			queryParam: map[string]string{ListTypeQuery: "2", MetadataQuery: "true"},
			expected:   ListObjectsV2MOperation,
		},
		{
			name:       "ListObjectsV2Operation",
			method:     http.MethodGet,
			queryParam: map[string]string{ListTypeQuery: "2"},
			expected:   ListObjectsV2Operation,
		},
		{
			name:     "ListObjectsV1Operation",
			method:   http.MethodGet,
			expected: ListObjectsV1Operation,
		},
		{
			name:       "PutBucketCorsOperation",
			method:     http.MethodPut,
			queryParam: map[string]string{CorsQuery: defaultValue},
			expected:   PutBucketCorsOperation,
		},
		{
			name:       "PutBucketACLOperation",
			method:     http.MethodPut,
			queryParam: map[string]string{ACLQuery: defaultValue},
			expected:   PutBucketACLOperation,
		},
		{
			name:       "PutBucketLifecycleOperation",
			method:     http.MethodPut,
			queryParam: map[string]string{LifecycleQuery: defaultValue},
			expected:   PutBucketLifecycleOperation,
		},
		{
			name:       "PutBucketEncryptionOperation",
			method:     http.MethodPut,
			queryParam: map[string]string{EncryptionQuery: defaultValue},
			expected:   PutBucketEncryptionOperation,
		},
		{
			name:       "PutBucketPolicyOperation",
			method:     http.MethodPut,
			queryParam: map[string]string{PolicyQuery: defaultValue},
			expected:   PutBucketPolicyOperation,
		},
		{
			name:       "PutBucketObjectLockConfigOperation",
			method:     http.MethodPut,
			queryParam: map[string]string{ObjectLockQuery: defaultValue},
			expected:   PutBucketObjectLockConfigOperation,
		},
		{
			name:       "PutBucketTaggingOperation",
			method:     http.MethodPut,
			queryParam: map[string]string{TaggingQuery: defaultValue},
			expected:   PutBucketTaggingOperation,
		},
		{
			name:       "PutBucketVersioningOperation",
			method:     http.MethodPut,
			queryParam: map[string]string{VersioningQuery: defaultValue},
			expected:   PutBucketVersioningOperation,
		},
		{
			name:       "PutBucketNotificationOperation",
			method:     http.MethodPut,
			queryParam: map[string]string{NotificationQuery: defaultValue},
			expected:   PutBucketNotificationOperation,
		},
		{
			name:     "CreateBucketOperation",
			method:   http.MethodPut,
			expected: CreateBucketOperation,
		},
		{
			name:       "DeleteMultipleObjectsOperation",
			method:     http.MethodPost,
			queryParam: map[string]string{DeleteQuery: defaultValue},
			expected:   DeleteMultipleObjectsOperation,
		},
		{
			name:     "PostObjectOperation",
			method:   http.MethodPost,
			expected: PostObjectOperation,
		},
		{
			name:       "DeleteBucketCorsOperation",
			method:     http.MethodDelete,
			queryParam: map[string]string{CorsQuery: defaultValue},
			expected:   DeleteBucketCorsOperation,
		},
		{
			name:       "DeleteBucketWebsiteOperation",
			method:     http.MethodDelete,
			queryParam: map[string]string{WebsiteQuery: defaultValue},
			expected:   DeleteBucketWebsiteOperation,
		},
		{
			name:       "DeleteBucketTaggingOperation",
			method:     http.MethodDelete,
			queryParam: map[string]string{TaggingQuery: defaultValue},
			expected:   DeleteBucketTaggingOperation,
		},
		{
			name:       "DeleteBucketPolicyOperation",
			method:     http.MethodDelete,
			queryParam: map[string]string{PolicyQuery: defaultValue},
			expected:   DeleteBucketPolicyOperation,
		},
		{
			name:       "DeleteBucketLifecycleOperation",
			method:     http.MethodDelete,
			queryParam: map[string]string{LifecycleQuery: defaultValue},
			expected:   DeleteBucketLifecycleOperation,
		},
		{
			name:       "DeleteBucketEncryptionOperation",
			method:     http.MethodDelete,
			queryParam: map[string]string{EncryptionQuery: defaultValue},
			expected:   DeleteBucketEncryptionOperation,
		},
		{
			name:     "DeleteBucketOperation",
			method:   http.MethodDelete,
			expected: DeleteBucketOperation,
		},
		{
			name:     "UnmatchedBucketOperation",
			method:   "invalid-method",
			expected: "UnmatchedBucketOperation",
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			req := httptest.NewRequest(tc.method, "/test", nil)
			if tc.queryParam != nil {
				addQueryParams(req, tc.queryParam)
			}

			actual := determineBucketOperation(req)
			require.Equal(t, tc.expected, actual)
		})
	}
}

func TestDetermineObjectOperation(t *testing.T) {
	const (
		amzCopySource = "X-Amz-Copy-Source"
		defaultValue  = "value"
	)

	for _, tc := range []struct {
		name       string
		method     string
		queryParam map[string]string
		headerKeys []string
		expected   string
	}{
		{
			name:     "OptionsObjectOperation",
			method:   http.MethodOptions,
			expected: OptionsObjectOperation,
		},
		{
			name:     "HeadObjectOperation",
			method:   http.MethodHead,
			expected: HeadObjectOperation,
		},
		{
			name:       "ListPartsOperation",
			method:     http.MethodGet,
			queryParam: map[string]string{UploadIDQuery: defaultValue},
			expected:   ListPartsOperation,
		},
		{
			name:       "GetObjectACLOperation",
			method:     http.MethodGet,
			queryParam: map[string]string{ACLQuery: defaultValue},
			expected:   GetObjectACLOperation,
		},
		{
			name:       "GetObjectTaggingOperation",
			method:     http.MethodGet,
			queryParam: map[string]string{TaggingQuery: defaultValue},
			expected:   GetObjectTaggingOperation,
		},
		{
			name:       "GetObjectRetentionOperation",
			method:     http.MethodGet,
			queryParam: map[string]string{RetentionQuery: defaultValue},
			expected:   GetObjectRetentionOperation,
		},
		{
			name:       "GetObjectLegalHoldOperation",
			method:     http.MethodGet,
			queryParam: map[string]string{LegalQuery: defaultValue},
			expected:   GetObjectLegalHoldOperation,
		},
		{
			name:       "GetObjectAttributesOperation",
			method:     http.MethodGet,
			queryParam: map[string]string{AttributesQuery: defaultValue},
			expected:   GetObjectAttributesOperation,
		},
		{
			name:     "GetObjectOperation",
			method:   http.MethodGet,
			expected: GetObjectOperation,
		},
		{
			name:       "UploadPartCopyOperation",
			method:     http.MethodPut,
			queryParam: map[string]string{PartNumberQuery: defaultValue, UploadIDQuery: defaultValue},
			headerKeys: []string{amzCopySource},
			expected:   UploadPartCopyOperation,
		},
		{
			name:       "UploadPartOperation",
			method:     http.MethodPut,
			queryParam: map[string]string{PartNumberQuery: defaultValue, UploadIDQuery: defaultValue},
			expected:   UploadPartOperation,
		},
		{
			name:       "PutObjectACLOperation",
			method:     http.MethodPut,
			queryParam: map[string]string{ACLQuery: defaultValue},
			expected:   PutObjectACLOperation,
		},
		{
			name:       "PutObjectTaggingOperation",
			method:     http.MethodPut,
			queryParam: map[string]string{TaggingQuery: defaultValue},
			expected:   PutObjectTaggingOperation,
		},
		{
			name:       "CopyObjectOperation",
			method:     http.MethodPut,
			headerKeys: []string{amzCopySource},
			expected:   CopyObjectOperation,
		},
		{
			name:       "PutObjectRetentionOperation",
			method:     http.MethodPut,
			queryParam: map[string]string{RetentionQuery: defaultValue},
			expected:   PutObjectRetentionOperation,
		},
		{
			name:       "PutObjectLegalHoldOperation",
			method:     http.MethodPut,
			queryParam: map[string]string{LegalHoldQuery: defaultValue},
			expected:   PutObjectLegalHoldOperation,
		},
		{
			name:     "PutObjectOperation",
			method:   http.MethodPut,
			expected: PutObjectOperation,
		},
		{
			name:       "CompleteMultipartUploadOperation",
			method:     http.MethodPost,
			queryParam: map[string]string{UploadIDQuery: defaultValue},
			expected:   CompleteMultipartUploadOperation,
		},
		{
			name:       "CreateMultipartUploadOperation",
			method:     http.MethodPost,
			queryParam: map[string]string{UploadsQuery: defaultValue},
			expected:   CreateMultipartUploadOperation,
		},
		{
			name:     "SelectObjectContentOperation",
			method:   http.MethodPost,
			expected: SelectObjectContentOperation,
		},
		{
			name:       "AbortMultipartUploadOperation",
			method:     http.MethodDelete,
			queryParam: map[string]string{UploadIDQuery: defaultValue},
			expected:   AbortMultipartUploadOperation,
		},
		{
			name:       "DeleteObjectTaggingOperation",
			method:     http.MethodDelete,
			queryParam: map[string]string{TaggingQuery: defaultValue},
			expected:   DeleteObjectTaggingOperation,
		},
		{
			name:     "DeleteObjectOperation",
			method:   http.MethodDelete,
			expected: DeleteObjectOperation,
		},
		{
			name:     "UnmatchedObjectOperation",
			method:   "invalid-method",
			expected: "UnmatchedObjectOperation",
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			req := httptest.NewRequest(tc.method, "/test", nil)
			if tc.queryParam != nil {
				addQueryParams(req, tc.queryParam)
			}
			if tc.headerKeys != nil {
				addHeaderParams(req, tc.headerKeys)
			}

			actual := determineObjectOperation(req)
			require.Equal(t, tc.expected, actual)
		})
	}
}

func addQueryParams(req *http.Request, pairs map[string]string) {
	values := req.URL.Query()
	for key, val := range pairs {
		values.Add(key, val)
	}
	req.URL.RawQuery = values.Encode()
}

func addHeaderParams(req *http.Request, keys []string) {
	for _, key := range keys {
		req.Header.Set(key, "val")
	}
}

func TestDetermineGeneralOperation(t *testing.T) {
	req := httptest.NewRequest(http.MethodGet, "/test", nil)
	actual := determineGeneralOperation(req)
	require.Equal(t, ListBucketsOperation, actual)

	req = httptest.NewRequest(http.MethodPost, "/test", nil)
	actual = determineGeneralOperation(req)
	require.Equal(t, "UnmatchedOperation", actual)
}