package api

import (
	"bytes"
	"encoding/json"
	"encoding/xml"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strconv"
	"testing"
	"time"

	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
	apiErrors "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
	s3middleware "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/metrics"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
	engineiam "git.frostfs.info/TrueCloudLab/policy-engine/iam"
	"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
	"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
	"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine/inmemory"
	"git.frostfs.info/TrueCloudLab/policy-engine/schema/common"
	"git.frostfs.info/TrueCloudLab/policy-engine/schema/s3"
	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap/zaptest"
)

type routerMock struct {
	t                  *testing.T
	router             *chi.Mux
	cfg                Config
	middlewareSettings *middlewareSettingsMock
	policyChecker      engine.LocalOverrideEngine
	handler            *handlerMock
}

func (m *routerMock) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	m.router.ServeHTTP(w, r)
}

type option func(*Config)

func frostFSIDValidation(flag bool) option {
	return func(cfg *Config) {
		cfg.FrostFSIDValidation = flag
	}
}

func prepareRouter(t *testing.T, opts ...option) *routerMock {
	middlewareSettings := &middlewareSettingsMock{}
	policyChecker := inmemory.NewInMemoryLocalOverrides()

	logger := zaptest.NewLogger(t)

	metricsConfig := metrics.AppMetricsConfig{
		Logger:         logger,
		PoolStatistics: &poolStatisticMock{},
		Registerer:     prometheus.NewRegistry(),
		Enabled:        true,
	}

	handlerTestMock := &handlerMock{t: t, cfg: middlewareSettings, buckets: map[string]*data.BucketInfo{}}

	cfg := Config{
		Throttle: middleware.ThrottleOpts{
			Limit:          10,
			BacklogTimeout: 30 * time.Second,
		},
		Handler:            handlerTestMock,
		Center:             &centerMock{t: t},
		Log:                logger,
		Metrics:            metrics.NewAppMetrics(metricsConfig),
		MiddlewareSettings: middlewareSettings,
		PolicyChecker:      policyChecker,
		Domains:            []string{"domain1", "domain2"},
		FrostfsID:          &frostFSIDMock{},
		XMLDecoder:         &xmlMock{},
		Tagging:            &resourceTaggingMock{},
	}

	for _, o := range opts {
		o(&cfg)
	}

	return &routerMock{
		t:                  t,
		router:             NewRouter(cfg),
		cfg:                cfg,
		middlewareSettings: middlewareSettings,
		policyChecker:      policyChecker,
		handler:            handlerTestMock,
	}
}

func TestRouterUploadPart(t *testing.T) {
	chiRouter := prepareRouter(t)

	createBucket(chiRouter, "", "dkirillov")

	w := httptest.NewRecorder()
	r := httptest.NewRequest(http.MethodPut, "/dkirillov/fix-object", nil)
	query := make(url.Values)
	query.Set("uploadId", "some-id")
	query.Set("partNumber", "1")
	r.URL.RawQuery = query.Encode()

	chiRouter.ServeHTTP(w, r)
	resp := readResponse(t, w)
	require.Equal(t, "UploadPart", resp.Method)
}

func TestRouterListMultipartUploads(t *testing.T) {
	chiRouter := prepareRouter(t)

	createBucket(chiRouter, "", "test-bucket")

	w := httptest.NewRecorder()
	r := httptest.NewRequest(http.MethodGet, "/test-bucket", nil)
	query := make(url.Values)
	query.Set("uploads", "")
	r.URL.RawQuery = query.Encode()

	chiRouter.ServeHTTP(w, r)
	resp := readResponse(t, w)
	require.Equal(t, "ListMultipartUploads", resp.Method)
}

func TestRouterObjectWithSlashes(t *testing.T) {
	chiRouter := prepareRouter(t)

	ns, bktName, objName := "", "dkirillov", "/fix/object"

	createBucket(chiRouter, ns, bktName)
	resp := putObject(chiRouter, ns, bktName, objName, nil)
	require.Equal(t, objName, resp.ReqInfo.ObjectName)
}

func TestRouterObjectEscaping(t *testing.T) {
	chiRouter := prepareRouter(t)

	ns, bktName := "", "dkirillov"
	createBucket(chiRouter, ns, bktName)

	for _, tc := range []struct {
		name            string
		expectedObjName string
		objName         string
	}{
		{
			name:            "simple",
			expectedObjName: "object",
			objName:         "object",
		},
		{
			name:            "with slashes",
			expectedObjName: "fix/object",
			objName:         "fix/object",
		},
		{
			name:            "with slash escaped",
			expectedObjName: "/foo/bar",
			objName:         "/foo%2fbar",
		},
		{
			name:            "with percentage escaped",
			expectedObjName: "fix/object%ac",
			objName:         "fix/object%25ac",
		},
		{
			name:            "with awful mint name",
			expectedObjName: "äöüex ®©µÄÆÐÕæŒƕƩDž 01000000 0x40 \u0040 amȡȹɆple&0a!-_.*'()&$@=;:+,?<>.pdf",
			objName:         "%C3%A4%C3%B6%C3%BCex%20%C2%AE%C2%A9%C2%B5%C3%84%C3%86%C3%90%C3%95%C3%A6%C5%92%C6%95%C6%A9%C7%85%2001000000%200x40%20%40%20am%C8%A1%C8%B9%C9%86ple%260a%21-_.%2A%27%28%29%26%24%40%3D%3B%3A%2B%2C%3F%3C%3E.pdf",
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			resp := putObject(chiRouter, ns, bktName, tc.objName, nil)
			require.Equal(t, tc.expectedObjName, resp.ReqInfo.ObjectName)
		})
	}
}

func TestPolicyChecker(t *testing.T) {
	chiRouter := prepareRouter(t)
	ns1, bktName1, objName1 := "", "bucket", "object"
	ns2, bktName2, objName2 := "custom-ns", "other-bucket", "object"

	createBucket(chiRouter, ns1, bktName1)
	createBucket(chiRouter, ns2, bktName1)
	createBucket(chiRouter, ns2, bktName2)

	ruleChain := &chain.Chain{
		ID: chain.ID("id"),
		Rules: []chain.Rule{{
			Status:    chain.AccessDenied,
			Actions:   chain.Actions{Names: []string{"*"}},
			Resources: chain.Resources{Names: []string{fmt.Sprintf(s3.ResourceFormatS3BucketObjects, bktName1)}},
		}},
	}

	_, _, err := chiRouter.policyChecker.MorphRuleChainStorage().AddMorphRuleChain(chain.S3, engine.NamespaceTarget(ns2), ruleChain)
	require.NoError(t, err)

	// check we can access 'bucket' in default namespace
	putObject(chiRouter, ns1, bktName1, objName1, nil)
	deleteObject(chiRouter, ns1, bktName1, objName1, nil)

	// check we can access 'other-bucket' in custom namespace
	putObject(chiRouter, ns2, bktName2, objName2, nil)
	deleteObject(chiRouter, ns2, bktName2, objName2, nil)

	// check we cannot access 'bucket' in custom namespace
	putObjectErr(chiRouter, ns2, bktName1, objName2, nil, apiErrors.ErrAccessDenied)
	deleteObjectErr(chiRouter, ns2, bktName1, objName2, nil, apiErrors.ErrAccessDenied)
}

func TestPolicyCheckerError(t *testing.T) {
	chiRouter := prepareRouter(t)
	ns1, bktName1, objName1 := "", "bucket", "object"
	putObjectErr(chiRouter, ns1, bktName1, objName1, nil, apiErrors.ErrNoSuchBucket)

	chiRouter = prepareRouter(t)
	chiRouter.cfg.FrostfsID.(*frostFSIDMock).userGroupsError = true
	putObjectErr(chiRouter, ns1, bktName1, objName1, nil, apiErrors.ErrInternalError)
}

func TestPolicyCheckerReqTypeDetermination(t *testing.T) {
	chiRouter := prepareRouter(t)
	bktName, objName := "bucket", "object"
	createBucket(chiRouter, "", bktName)

	policy := engineiam.Policy{
		Version: "2012-10-17",
		Statement: []engineiam.Statement{{
			Principal: map[engineiam.PrincipalType][]string{engineiam.Wildcard: {}},
			Effect:    engineiam.AllowEffect,
			Action:    engineiam.Action{"s3:*"},
			Resource:  engineiam.Resource{fmt.Sprintf(s3.ResourceFormatS3All)},
		}},
	}

	ruleChain, err := engineiam.ConvertToS3Chain(policy, nil)
	require.NoError(t, err)

	_, _, err = chiRouter.policyChecker.MorphRuleChainStorage().AddMorphRuleChain(chain.S3, engine.NamespaceTarget(""), ruleChain)
	require.NoError(t, err)

	createBucket(chiRouter, "", bktName)

	chiRouter.middlewareSettings.denyByDefault = true
	t.Run("can list buckets", func(t *testing.T) {
		w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/", nil)
		chiRouter.ServeHTTP(w, r)
		resp := readResponse(t, w)
		require.Equal(t, s3middleware.ListBucketsOperation, resp.Method)
	})

	t.Run("can head 'bucket'", func(t *testing.T) {
		w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodHead, "/"+bktName, nil)
		chiRouter.ServeHTTP(w, r)
		resp := readResponse(t, w)
		require.Equal(t, s3middleware.HeadBucketOperation, resp.Method)
	})

	t.Run("can put object into 'bucket'", func(t *testing.T) {
		w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodPut, fmt.Sprintf("/%s/%s", bktName, objName), nil)
		chiRouter.ServeHTTP(w, r)
		resp := readResponse(t, w)
		require.Equal(t, s3middleware.PutObjectOperation, resp.Method)
	})
}

func TestDefaultBehaviorPolicyChecker(t *testing.T) {
	chiRouter := prepareRouter(t)
	ns, bktName := "", "bucket"

	// check we can access bucket if rules not found
	createBucket(chiRouter, ns, bktName)

	// check we cannot access if rules not found when settings is enabled
	chiRouter.middlewareSettings.denyByDefault = true
	createBucketErr(chiRouter, ns, bktName, nil, apiErrors.ErrAccessDenied)
}

func TestDefaultPolicyCheckerWithUserTags(t *testing.T) {
	router := prepareRouter(t)
	ns, bktName := "", "bucket"
	router.middlewareSettings.denyByDefault = true

	allowOperations(router, ns, []string{"s3:CreateBucket"}, engineiam.Conditions{
		engineiam.CondStringEquals: engineiam.Condition{fmt.Sprintf(common.PropertyKeyFormatFrostFSIDUserClaim, "tag-test"): []string{"test"}},
	})
	createBucketErr(router, ns, bktName, nil, apiErrors.ErrAccessDenied)

	tags := make(map[string]string)
	tags["tag-test"] = "test"
	router.cfg.FrostfsID.(*frostFSIDMock).tags = tags
	createBucket(router, ns, bktName)
}

func TestRequestParametersCheck(t *testing.T) {
	t.Run("prefix parameter, allow specific value", func(t *testing.T) {
		router := prepareRouter(t)

		ns, bktName, prefix := "", "bucket", "prefix"
		router.middlewareSettings.denyByDefault = true

		allowOperations(router, ns, []string{"s3:CreateBucket"}, nil)
		createBucket(router, ns, bktName)

		// Add policies and check
		denyOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{
			engineiam.CondStringNotEquals: engineiam.Condition{s3.PropertyKeyPrefix: []string{prefix}},
		})
		allowOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{
			engineiam.CondStringEquals: engineiam.Condition{s3.PropertyKeyPrefix: []string{prefix}},
		})

		listObjectsV1(router, ns, bktName, prefix, "", "")
		listObjectsV1Err(router, ns, bktName, "", "", "", apiErrors.ErrAccessDenied)
		listObjectsV1Err(router, ns, bktName, "invalid", "", "", apiErrors.ErrAccessDenied)
	})

	t.Run("delimiter parameter, prohibit specific value", func(t *testing.T) {
		router := prepareRouter(t)

		ns, bktName, delimiter := "", "bucket", "delimiter"
		router.middlewareSettings.denyByDefault = true

		allowOperations(router, ns, []string{"s3:CreateBucket"}, nil)
		createBucket(router, ns, bktName)

		// Add policies and check
		denyOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{
			engineiam.CondStringEquals: engineiam.Condition{s3.PropertyKeyDelimiter: []string{delimiter}},
		})
		allowOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{
			engineiam.CondStringNotEquals: engineiam.Condition{s3.PropertyKeyDelimiter: []string{delimiter}},
		})

		listObjectsV1(router, ns, bktName, "", "", "")
		listObjectsV1(router, ns, bktName, "", "some-delimiter", "")
		listObjectsV1Err(router, ns, bktName, "", delimiter, "", apiErrors.ErrAccessDenied)
	})

	t.Run("max-keys parameter, allow specific value", func(t *testing.T) {
		router := prepareRouter(t)

		ns, bktName, maxKeys := "", "bucket", 10
		router.middlewareSettings.denyByDefault = true

		allowOperations(router, ns, []string{"s3:CreateBucket"}, nil)
		createBucket(router, ns, bktName)

		// Add policies and check
		denyOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{
			engineiam.CondNumericNotEquals: engineiam.Condition{s3.PropertyKeyMaxKeys: []string{strconv.Itoa(maxKeys)}},
		})
		allowOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{
			engineiam.CondNumericEquals: engineiam.Condition{s3.PropertyKeyMaxKeys: []string{strconv.Itoa(maxKeys)}},
		})

		listObjectsV1(router, ns, bktName, "", "", strconv.Itoa(maxKeys))
		listObjectsV1Err(router, ns, bktName, "", "", "", apiErrors.ErrAccessDenied)
		listObjectsV1Err(router, ns, bktName, "", "", strconv.Itoa(maxKeys-1), apiErrors.ErrAccessDenied)
		listObjectsV1Err(router, ns, bktName, "", "", "invalid", apiErrors.ErrAccessDenied)
	})

	t.Run("max-keys parameter, allow range of values", func(t *testing.T) {
		router := prepareRouter(t)

		ns, bktName, maxKeys := "", "bucket", 10
		router.middlewareSettings.denyByDefault = true

		allowOperations(router, ns, []string{"s3:CreateBucket"}, nil)
		createBucket(router, ns, bktName)

		// Add policies and check
		denyOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{
			engineiam.CondNumericGreaterThan: engineiam.Condition{s3.PropertyKeyMaxKeys: []string{strconv.Itoa(maxKeys)}},
		})
		allowOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{
			engineiam.CondNumericLessThanEquals: engineiam.Condition{s3.PropertyKeyMaxKeys: []string{strconv.Itoa(maxKeys)}},
		})

		listObjectsV1(router, ns, bktName, "", "", strconv.Itoa(maxKeys))
		listObjectsV1(router, ns, bktName, "", "", strconv.Itoa(maxKeys-1))
		listObjectsV1Err(router, ns, bktName, "", "", strconv.Itoa(maxKeys+1), apiErrors.ErrAccessDenied)
	})

	t.Run("max-keys parameter, prohibit specific value", func(t *testing.T) {
		router := prepareRouter(t)

		ns, bktName, maxKeys := "", "bucket", 10
		router.middlewareSettings.denyByDefault = true

		allowOperations(router, ns, []string{"s3:CreateBucket"}, nil)
		createBucket(router, ns, bktName)

		// Add policies and check
		denyOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{
			engineiam.CondNumericEquals: engineiam.Condition{s3.PropertyKeyMaxKeys: []string{strconv.Itoa(maxKeys)}},
		})
		allowOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{
			engineiam.CondNumericNotEquals: engineiam.Condition{s3.PropertyKeyMaxKeys: []string{strconv.Itoa(maxKeys)}},
		})

		listObjectsV1(router, ns, bktName, "", "", "")
		listObjectsV1(router, ns, bktName, "", "", strconv.Itoa(maxKeys-1))
		listObjectsV1Err(router, ns, bktName, "", "", strconv.Itoa(maxKeys), apiErrors.ErrAccessDenied)
	})
}

func TestRequestTagsCheck(t *testing.T) {
	t.Run("put bucket tagging", func(t *testing.T) {
		router := prepareRouter(t)

		ns, bktName, tagKey, tagValue := "", "bucket", "tag", "value"
		router.middlewareSettings.denyByDefault = true

		allowOperations(router, ns, []string{"s3:CreateBucket"}, nil)
		createBucket(router, ns, bktName)

		// Add policies and check
		allowOperations(router, ns, []string{"s3:PutBucketTagging"}, engineiam.Conditions{
			engineiam.CondStringEquals: engineiam.Condition{fmt.Sprintf(s3.PropertyKeyFormatRequestTag, tagKey): []string{tagValue}},
		})
		denyOperations(router, ns, []string{"s3:PutBucketTagging"}, engineiam.Conditions{
			engineiam.CondStringNotEquals: engineiam.Condition{fmt.Sprintf(s3.PropertyKeyFormatRequestTag, tagKey): []string{tagValue}},
		})

		tagging, err := xml.Marshal(data.Tagging{TagSet: []data.Tag{{Key: tagKey, Value: tagValue}}})
		require.NoError(t, err)
		putBucketTagging(router, ns, bktName, tagging)

		tagging, err = xml.Marshal(data.Tagging{TagSet: []data.Tag{{Key: "key", Value: tagValue}}})
		require.NoError(t, err)
		putBucketTaggingErr(router, ns, bktName, tagging, apiErrors.ErrAccessDenied)

		tagging = nil
		putBucketTaggingErr(router, ns, bktName, tagging, apiErrors.ErrMalformedXML)
	})

	t.Run("put object with tag", func(t *testing.T) {
		router := prepareRouter(t)

		ns, bktName, objName, tagKey, tagValue := "", "bucket", "object", "tag", "value"
		router.middlewareSettings.denyByDefault = true

		allowOperations(router, ns, []string{"s3:CreateBucket"}, nil)
		createBucket(router, ns, bktName)

		// Add policies and check
		allowOperations(router, ns, []string{"s3:PutObject"}, engineiam.Conditions{
			engineiam.CondStringEquals: engineiam.Condition{fmt.Sprintf(s3.PropertyKeyFormatRequestTag, tagKey): []string{tagValue}},
		})
		denyOperations(router, ns, []string{"s3:PutObject"}, engineiam.Conditions{
			engineiam.CondStringNotEquals: engineiam.Condition{fmt.Sprintf(s3.PropertyKeyFormatRequestTag, tagKey): []string{tagValue}},
		})

		putObject(router, ns, bktName, objName, &data.Tag{Key: tagKey, Value: tagValue})

		putObjectErr(router, ns, bktName, objName, &data.Tag{Key: "key", Value: tagValue}, apiErrors.ErrAccessDenied)
	})
}

func TestResourceTagsCheck(t *testing.T) {
	t.Run("bucket tagging", func(t *testing.T) {
		router := prepareRouter(t)

		ns, bktName, tagKey, tagValue := "", "bucket", "tag", "value"
		router.middlewareSettings.denyByDefault = true

		allowOperations(router, ns, []string{"s3:CreateBucket"}, nil)
		createBucket(router, ns, bktName)

		// Add policies and check
		allowOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{
			engineiam.CondStringEquals: engineiam.Condition{fmt.Sprintf(s3.PropertyKeyFormatResourceTag, tagKey): []string{tagValue}},
		})
		denyOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{
			engineiam.CondStringNotEquals: engineiam.Condition{fmt.Sprintf(s3.PropertyKeyFormatResourceTag, tagKey): []string{tagValue}},
		})

		router.cfg.Tagging.(*resourceTaggingMock).bucketTags = map[string]string{tagKey: tagValue}
		listObjectsV1(router, ns, bktName, "", "", "")

		router.cfg.Tagging.(*resourceTaggingMock).bucketTags = map[string]string{}
		listObjectsV1Err(router, ns, bktName, "", "", "", apiErrors.ErrAccessDenied)
	})

	t.Run("object tagging", func(t *testing.T) {
		router := prepareRouter(t)

		ns, bktName, objName, tagKey, tagValue := "", "bucket", "object", "tag", "value"
		router.middlewareSettings.denyByDefault = true

		allowOperations(router, ns, []string{"s3:CreateBucket", "s3:PutObject"}, nil)
		createBucket(router, ns, bktName)
		putObject(router, ns, bktName, objName, nil)

		// Add policies and check
		allowOperations(router, ns, []string{"s3:GetObject"}, engineiam.Conditions{
			engineiam.CondStringEquals: engineiam.Condition{fmt.Sprintf(s3.PropertyKeyFormatResourceTag, tagKey): []string{tagValue}},
		})
		denyOperations(router, ns, []string{"s3:GetObject"}, engineiam.Conditions{
			engineiam.CondStringNotEquals: engineiam.Condition{fmt.Sprintf(s3.PropertyKeyFormatResourceTag, tagKey): []string{tagValue}},
		})

		router.cfg.Tagging.(*resourceTaggingMock).objectTags = map[string]string{tagKey: tagValue}
		getObject(router, ns, bktName, objName)

		router.cfg.Tagging.(*resourceTaggingMock).objectTags = map[string]string{}
		getObjectErr(router, ns, bktName, objName, apiErrors.ErrAccessDenied)
	})

	t.Run("non-existent resources", func(t *testing.T) {
		router := prepareRouter(t)
		ns, bktName, objName := "", "bucket", "object"

		listObjectsV1Err(router, ns, bktName, "", "", "", apiErrors.ErrNoSuchBucket)

		router.cfg.Tagging.(*resourceTaggingMock).noSuchBucketKey = true
		createBucket(router, ns, bktName)
		getBucketErr(router, ns, bktName, apiErrors.ErrNoSuchKey)

		router.cfg.Tagging.(*resourceTaggingMock).noSuchObjectKey = true
		createBucket(router, ns, bktName)
		getObjectErr(router, ns, bktName, objName, apiErrors.ErrNoSuchKey)
	})
}

func TestAccessBoxAttributesCheck(t *testing.T) {
	router := prepareRouter(t)

	ns, bktName, attrKey, attrValue := "", "bucket", "key", "true"
	router.middlewareSettings.denyByDefault = true

	allowOperations(router, ns, []string{"s3:CreateBucket"}, nil)
	createBucket(router, ns, bktName)

	// Add policy and check
	allowOperations(router, ns, []string{"s3:ListBucket"}, engineiam.Conditions{
		engineiam.CondBool: engineiam.Condition{fmt.Sprintf(s3.PropertyKeyFormatAccessBoxAttr, attrKey): []string{attrValue}},
	})

	listObjectsV1Err(router, ns, bktName, "", "", "", apiErrors.ErrAccessDenied)

	var attr object.Attribute
	attr.SetKey(attrKey)
	attr.SetValue(attrValue)
	router.cfg.Center.(*centerMock).attrs = []object.Attribute{attr}
	listObjectsV1(router, ns, bktName, "", "", "")
}

func TestSourceIPCheck(t *testing.T) {
	router := prepareRouter(t)

	ns, bktName, hdr := "", "bucket", "Source-Ip"
	router.middlewareSettings.denyByDefault = true

	// Add policy and check
	allowOperations(router, ns, []string{"s3:CreateBucket"}, engineiam.Conditions{
		engineiam.CondIPAddress: engineiam.Condition{"aws:SourceIp": []string{"192.0.2.0/24"}},
	})

	router.middlewareSettings.sourceIPHeader = hdr
	header := map[string][]string{hdr: {"192.0.3.0"}}
	createBucketErr(router, ns, bktName, header, apiErrors.ErrAccessDenied)

	router.middlewareSettings.sourceIPHeader = ""
	createBucket(router, ns, bktName)
}

func TestMFAPolicy(t *testing.T) {
	router := prepareRouter(t)

	ns, bktName := "", "bucket"
	router.middlewareSettings.denyByDefault = true

	allowOperations(router, ns, []string{"s3:CreateBucket"}, nil)
	denyOperations(router, ns, []string{"s3:CreateBucket"}, engineiam.Conditions{
		engineiam.CondBool: engineiam.Condition{s3.PropertyKeyAccessBoxAttrMFA: []string{"false"}},
	})
	createBucketErr(router, ns, bktName, nil, apiErrors.ErrAccessDenied)

	var attr object.Attribute
	attr.SetKey("IAM-MFA")
	attr.SetValue("true")
	router.cfg.Center.(*centerMock).attrs = []object.Attribute{attr}

	createBucket(router, ns, bktName)
}

func allowOperations(router *routerMock, ns string, operations []string, conditions engineiam.Conditions) {
	addPolicy(router, ns, "allow", engineiam.AllowEffect, operations, conditions)
}

func denyOperations(router *routerMock, ns string, operations []string, conditions engineiam.Conditions) {
	addPolicy(router, ns, "deny", engineiam.DenyEffect, operations, conditions)
}

func addPolicy(router *routerMock, ns string, id string, effect engineiam.Effect, operations []string, conditions engineiam.Conditions) {
	policy := engineiam.Policy{
		Version: "2012-10-17",
		Statement: []engineiam.Statement{{
			Principal:  map[engineiam.PrincipalType][]string{engineiam.Wildcard: {}},
			Effect:     effect,
			Action:     engineiam.Action(operations),
			Resource:   engineiam.Resource{fmt.Sprintf(s3.ResourceFormatS3All)},
			Conditions: conditions,
		}},
	}

	ruleChain, err := engineiam.ConvertToS3Chain(policy, nil)
	require.NoError(router.t, err)
	ruleChain.ID = chain.ID(id)

	_, _, err = router.policyChecker.MorphRuleChainStorage().AddMorphRuleChain(chain.S3, engine.NamespaceTarget(ns), ruleChain)
	require.NoError(router.t, err)
}

func createBucket(router *routerMock, namespace, bktName string) {
	w := createBucketBase(router, namespace, bktName, nil)
	resp := readResponse(router.t, w)
	require.Equal(router.t, s3middleware.CreateBucketOperation, resp.Method)
}

func createBucketErr(router *routerMock, namespace, bktName string, header http.Header, errCode apiErrors.ErrorCode) {
	w := createBucketBase(router, namespace, bktName, header)
	assertAPIError(router.t, w, errCode)
}

func createBucketBase(router *routerMock, namespace, bktName string, header http.Header) *httptest.ResponseRecorder {
	w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodPut, "/"+bktName, nil)
	r.Header.Set(FrostfsNamespaceHeader, namespace)
	for key := range header {
		r.Header.Set(key, header.Get(key))
	}

	router.ServeHTTP(w, r)
	return w
}

func getBucketErr(router *routerMock, namespace, bktName string, errCode apiErrors.ErrorCode) {
	w := getBucketBase(router, namespace, bktName)
	assertAPIError(router.t, w, errCode)
}

func getBucketBase(router *routerMock, namespace, bktName string) *httptest.ResponseRecorder {
	w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/"+bktName, nil)
	r.Header.Set(FrostfsNamespaceHeader, namespace)
	router.ServeHTTP(w, r)
	return w
}

func putObject(router *routerMock, namespace, bktName, objName string, tag *data.Tag) handlerResult {
	w := putObjectBase(router, namespace, bktName, objName, tag)
	resp := readResponse(router.t, w)
	require.Equal(router.t, s3middleware.PutObjectOperation, resp.Method)
	return resp
}

func putObjectErr(router *routerMock, namespace, bktName, objName string, tag *data.Tag, errCode apiErrors.ErrorCode) {
	w := putObjectBase(router, namespace, bktName, objName, tag)
	assertAPIError(router.t, w, errCode)
}

func putObjectBase(router *routerMock, namespace, bktName, objName string, tag *data.Tag) *httptest.ResponseRecorder {
	w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodPut, "/"+bktName+"/"+objName, nil)
	if tag != nil {
		queries := url.Values{
			tag.Key: []string{tag.Value},
		}
		r.Header.Set(AmzTagging, queries.Encode())
	}
	r.Header.Set(FrostfsNamespaceHeader, namespace)
	router.ServeHTTP(w, r)
	return w
}

func deleteObject(router *routerMock, namespace, bktName, objName string, tag *data.Tag) handlerResult {
	w := deleteObjectBase(router, namespace, bktName, objName, tag)
	resp := readResponse(router.t, w)
	require.Equal(router.t, s3middleware.DeleteObjectOperation, resp.Method)
	return resp
}

func deleteObjectErr(router *routerMock, namespace, bktName, objName string, tag *data.Tag, errCode apiErrors.ErrorCode) {
	w := deleteObjectBase(router, namespace, bktName, objName, tag)
	assertAPIError(router.t, w, errCode)
}

func deleteObjectBase(router *routerMock, namespace, bktName, objName string, tag *data.Tag) *httptest.ResponseRecorder {
	w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodDelete, "/"+bktName+"/"+objName, nil)
	if tag != nil {
		queries := url.Values{
			tag.Key: []string{tag.Value},
		}
		r.Header.Set(AmzTagging, queries.Encode())
	}
	r.Header.Set(FrostfsNamespaceHeader, namespace)
	router.ServeHTTP(w, r)
	return w
}

func putBucketTagging(router *routerMock, namespace, bktName string, tagging []byte) handlerResult {
	w := putBucketTaggingBase(router, namespace, bktName, tagging)
	resp := readResponse(router.t, w)
	require.Equal(router.t, s3middleware.PutBucketTaggingOperation, resp.Method)
	return resp
}

func putBucketTaggingErr(router *routerMock, namespace, bktName string, tagging []byte, errCode apiErrors.ErrorCode) {
	w := putBucketTaggingBase(router, namespace, bktName, tagging)
	assertAPIError(router.t, w, errCode)
}

func putBucketTaggingBase(router *routerMock, namespace, bktName string, tagging []byte) *httptest.ResponseRecorder {
	queries := url.Values{}
	queries.Add(s3middleware.TaggingQuery, "")

	w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodPut, "/"+bktName, bytes.NewBuffer(tagging))
	r.URL.RawQuery = queries.Encode()
	r.Header.Set(FrostfsNamespaceHeader, namespace)
	router.ServeHTTP(w, r)
	return w
}

func getObject(router *routerMock, namespace, bktName, objName string) handlerResult {
	w := getObjectBase(router, namespace, bktName, objName)
	resp := readResponse(router.t, w)
	require.Equal(router.t, s3middleware.GetObjectOperation, resp.Method)
	return resp
}

func getObjectErr(router *routerMock, namespace, bktName, objName string, errCode apiErrors.ErrorCode) {
	w := getObjectBase(router, namespace, bktName, objName)
	assertAPIError(router.t, w, errCode)
}

func getObjectBase(router *routerMock, namespace, bktName, objName string) *httptest.ResponseRecorder {
	w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/"+bktName+"/"+objName, nil)
	r.Header.Set(FrostfsNamespaceHeader, namespace)
	router.ServeHTTP(w, r)
	return w
}

func listObjectsV1(router *routerMock, namespace, bktName, prefix, delimiter, maxKeys string) handlerResult {
	w := listObjectsV1Base(router, namespace, bktName, prefix, delimiter, maxKeys)
	resp := readResponse(router.t, w)
	require.Equal(router.t, s3middleware.ListObjectsV1Operation, resp.Method)
	return resp
}

func listObjectsV1Err(router *routerMock, namespace, bktName, prefix, delimiter, maxKeys string, errCode apiErrors.ErrorCode) {
	w := listObjectsV1Base(router, namespace, bktName, prefix, delimiter, maxKeys)
	assertAPIError(router.t, w, errCode)
}

func listObjectsV1Base(router *routerMock, namespace, bktName, prefix, delimiter, maxKeys string) *httptest.ResponseRecorder {
	queries := url.Values{}
	if len(prefix) > 0 {
		queries.Add(s3middleware.QueryPrefix, prefix)
	}
	if len(delimiter) > 0 {
		queries.Add(s3middleware.QueryDelimiter, delimiter)
	}
	if len(maxKeys) > 0 {
		queries.Add(s3middleware.QueryMaxKeys, maxKeys)
	}
	encoded := queries.Encode()

	w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/"+bktName, nil)
	r.URL.RawQuery = encoded
	r.Header.Set(FrostfsNamespaceHeader, namespace)
	router.ServeHTTP(w, r)
	return w
}

func TestOwnerIDRetrieving(t *testing.T) {
	chiRouter := prepareRouter(t)

	ns, bktName, objName := "", "test-bucket", "test-object"

	createBucket(chiRouter, ns, bktName)

	resp := putObject(chiRouter, ns, bktName, objName, nil)
	require.NotEqual(t, "anon", resp.ReqInfo.User)

	chiRouter.cfg.Center.(*centerMock).anon = true
	resp = putObject(chiRouter, ns, bktName, objName, nil)
	require.Equal(t, "anon", resp.ReqInfo.User)
}

func TestBillingMetrics(t *testing.T) {
	chiRouter := prepareRouter(t)

	ns, bktName, objName := "", "test-bucket", "test-object"

	createBucket(chiRouter, ns, bktName)
	dump := chiRouter.cfg.Metrics.UsersAPIStats().DumpMetrics()
	require.Len(t, dump.Requests, 1)
	require.NotEqual(t, "anon", dump.Requests[0].User)
	require.Equal(t, metrics.PUTRequest, dump.Requests[0].Operation)
	require.Equal(t, bktName, dump.Requests[0].Bucket)
	require.Equal(t, 1, dump.Requests[0].Requests)

	chiRouter.cfg.Center.(*centerMock).anon = true
	putObject(chiRouter, ns, bktName, objName, nil)
	dump = chiRouter.cfg.Metrics.UsersAPIStats().DumpMetrics()
	require.Len(t, dump.Requests, 1)
	require.Equal(t, "anon", dump.Requests[0].User)
}

func TestAuthenticate(t *testing.T) {
	chiRouter := prepareRouter(t)
	createBucket(chiRouter, "", "bkt-1")

	chiRouter = prepareRouter(t)
	chiRouter.cfg.Center.(*centerMock).noAuthHeader = true
	createBucket(chiRouter, "", "bkt-2")

	chiRouter = prepareRouter(t)
	chiRouter.cfg.Center.(*centerMock).isError = true
	createBucketErr(chiRouter, "", "bkt-3", nil, apiErrors.ErrAccessDenied)
}

func TestFrostFSIDValidation(t *testing.T) {
	// successful frostFSID validation
	chiRouter := prepareRouter(t, frostFSIDValidation(true))
	createBucket(chiRouter, "", "bkt-1")

	// anon request, skip frostFSID validation
	chiRouter = prepareRouter(t, frostFSIDValidation(true))
	chiRouter.cfg.Center.(*centerMock).anon = true
	createBucket(chiRouter, "", "bkt-2")

	// frostFSID validation failed
	chiRouter = prepareRouter(t, frostFSIDValidation(true))
	chiRouter.cfg.FrostfsID.(*frostFSIDMock).validateError = true
	createBucketErr(chiRouter, "", "bkt-3", nil, apiErrors.ErrInternalError)
}

func readResponse(t *testing.T, w *httptest.ResponseRecorder) handlerResult {
	var res handlerResult

	resData, err := io.ReadAll(w.Result().Body)
	require.NoError(t, err)

	err = json.Unmarshal(resData, &res)
	require.NoErrorf(t, err, "actual body: '%s'", string(resData))
	return res
}

func assertAPIError(t *testing.T, w *httptest.ResponseRecorder, expectedErrorCode apiErrors.ErrorCode) {
	actualErrorResponse := &s3middleware.ErrorResponse{}
	err := xml.NewDecoder(w.Result().Body).Decode(actualErrorResponse)
	require.NoError(t, err)

	expectedError := apiErrors.GetAPIError(expectedErrorCode)

	require.Equal(t, expectedError.HTTPStatusCode, w.Code)
	require.Equal(t, expectedError.Code, actualErrorResponse.Code)

	if expectedError.ErrCode != apiErrors.ErrInternalError {
		require.Contains(t, actualErrorResponse.Message, expectedError.Description)
	}
}