package handler

import (
	"bytes"
	"context"
	"crypto/rand"
	"encoding/hex"
	"encoding/xml"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strconv"
	"testing"
	"time"

	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/cache"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
	"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/layer/frostfs"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/resolver"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/tree"
	bearertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer/test"
	cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
	oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
	"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
	"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"github.com/panjf2000/ants/v2"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap"
	"go.uber.org/zap/zaptest"
	"golang.org/x/exp/slices"
)

type handlerContext struct {
	*handlerContextBase
	t *testing.T
}

type handlerContextBase struct {
	owner     user.ID
	h         *handler
	tp        *layer.TestFrostFS
	tree      *tree.Tree
	context   context.Context
	config    *configMock
	corsCnrID cid.ID

	layerFeatures *layer.FeatureSettingsMock
	treeMock      *tree.ServiceClientMemory
	cache         *layer.Cache
}

func (hc *handlerContextBase) Handler() *handler {
	return hc.h
}

func (hc *handlerContextBase) MockedPool() *layer.TestFrostFS {
	return hc.tp
}

func (hc *handlerContextBase) Layer() *layer.Layer {
	return hc.h.obj
}

func (hc *handlerContextBase) Context() context.Context {
	return hc.context
}

type configMock struct {
	defaultPolicy                 netmap.PlacementPolicy
	placementPolicies             map[string]netmap.PlacementPolicy
	copiesNumbers                 map[string][]uint32
	defaultCopiesNumbers          []uint32
	bypassContentEncodingInChunks bool
	md5Enabled                    bool
	tlsTerminationHeader          string
	useDefaultXMLNS               bool
}

func (c *configMock) DefaultPlacementPolicy(_ string) netmap.PlacementPolicy {
	return c.defaultPolicy
}

func (c *configMock) PlacementPolicy(_, constraint string) (netmap.PlacementPolicy, bool) {
	policy, ok := c.placementPolicies[constraint]
	return policy, ok
}

func (c *configMock) CopiesNumbers(_, locationConstraint string) ([]uint32, bool) {
	result, ok := c.copiesNumbers[locationConstraint]
	return result, ok
}

func (c *configMock) DefaultCopiesNumbers(_ string) []uint32 {
	return c.defaultCopiesNumbers
}

func (c *configMock) NewXMLDecoder(r io.Reader, _ string) *xml.Decoder {
	dec := xml.NewDecoder(r)
	if c.useDefaultXMLNS {
		dec.DefaultSpace = "http://s3.amazonaws.com/doc/2006-03-01/"
	}
	return dec
}

func (c *configMock) BypassContentEncodingInChunks(_ string) bool {
	return c.bypassContentEncodingInChunks
}

func (c *configMock) DefaultMaxAge() int {
	return 0
}

func (c *configMock) ResolveZoneList() []string {
	return []string{}
}

func (c *configMock) IsResolveListAllow() bool {
	return false
}

func (c *configMock) CompleteMultipartKeepalive() time.Duration {
	return time.Duration(0)
}

func (c *configMock) MD5Enabled() bool {
	return c.md5Enabled
}

func (c *configMock) ResolveNamespaceAlias(ns string) string {
	return ns
}

func (c *configMock) RetryMaxAttempts() int {
	return 1
}

func (c *configMock) RetryMaxBackoff() time.Duration {
	return 0
}

func (c *configMock) RetryStrategy() RetryStrategy {
	return RetryStrategyConstant
}

func (c *configMock) TLSTerminationHeader() string {
	return c.tlsTerminationHeader
}

func (c *configMock) ListingKeepaliveThrottle() time.Duration {
	return 0
}

func (c *configMock) putLocationConstraint(constraint string) {
	c.placementPolicies[constraint] = c.defaultPolicy
}

type handlerConfig struct {
	cacheCfg    *layer.CachesConfig
	withoutCORS bool
}

func prepareHandlerContext(t *testing.T) *handlerContext {
	log := zaptest.NewLogger(t)
	hc, err := prepareHandlerContextBase(&handlerConfig{cacheCfg: layer.DefaultCachesConfigs(log)}, log)
	require.NoError(t, err)
	return &handlerContext{
		handlerContextBase: hc,
		t:                  t,
	}
}

func prepareWithoutCORSHandlerContext(t *testing.T) *handlerContext {
	log := zaptest.NewLogger(t)
	hc, err := prepareHandlerContextBase(&handlerConfig{
		cacheCfg:    layer.DefaultCachesConfigs(log),
		withoutCORS: true,
	}, log)
	require.NoError(t, err)
	return &handlerContext{
		handlerContextBase: hc,
		t:                  t,
	}
}

func prepareHandlerContextWithMinCache(t *testing.T) *handlerContext {
	log := zaptest.NewLogger(t)
	hc, err := prepareHandlerContextBase(&handlerConfig{cacheCfg: getMinCacheConfig(log)}, log)
	require.NoError(t, err)
	return &handlerContext{
		handlerContextBase: hc,
		t:                  t,
	}
}

func prepareHandlerContextBase(config *handlerConfig, log *zap.Logger) (*handlerContextBase, error) {
	key, err := keys.NewPrivateKey()
	if err != nil {
		return nil, err
	}

	tp := layer.NewTestFrostFS(key)

	testResolver := &resolver.Resolver{Name: "test_resolver"}
	testResolver.SetResolveFunc(func(_ context.Context, _, name string) (cid.ID, error) {
		return tp.ContainerID(name)
	})

	var owner user.ID
	user.IDFromKey(&owner, key.PrivateKey.PublicKey)

	memCli, err := tree.NewTreeServiceClientMemory()
	if err != nil {
		return nil, err
	}

	treeMock := tree.NewTree(memCli, log)

	features := &layer.FeatureSettingsMock{}

	pool, err := ants.NewPool(1)
	if err != nil {
		return nil, err
	}

	layerCfg := &layer.Config{
		Cache:       layer.NewCache(config.cacheCfg),
		AnonKey:     layer.AnonymousKey{Key: key},
		Resolver:    testResolver,
		TreeService: treeMock,
		Features:    features,
		GateOwner:   owner,
		GateKey:     key,
		WorkerPool:  pool,
	}

	if !config.withoutCORS {
		layerCfg.CORSCnrInfo, err = createCORSContainer(key, tp)
		if err != nil {
			return nil, err
		}
	}

	var pp netmap.PlacementPolicy
	err = pp.DecodeString("REP 1")
	if err != nil {
		return nil, err
	}

	cfg := &configMock{
		defaultPolicy:     pp,
		placementPolicies: make(map[string]netmap.PlacementPolicy),
	}
	h := &handler{
		log:       log,
		obj:       layer.NewLayer(log, tp, layerCfg),
		cfg:       cfg,
		ape:       newAPEMock(),
		frostfsid: newFrostfsIDMock(),
	}

	accessBox, err := newTestAccessBox(key)
	if err != nil {
		return nil, err
	}

	hc := &handlerContextBase{
		owner:   owner,
		h:       h,
		tp:      tp,
		tree:    treeMock,
		context: middleware.SetBox(context.Background(), &middleware.Box{AccessBox: accessBox}),
		config:  cfg,

		layerFeatures: features,
		treeMock:      memCli,
		cache:         layerCfg.Cache,
	}

	if layerCfg.CORSCnrInfo != nil {
		hc.corsCnrID = layerCfg.CORSCnrInfo.CID
	}

	return hc, nil
}

func createCORSContainer(key *keys.PrivateKey, tp *layer.TestFrostFS) (*data.BucketInfo, error) {
	bearerToken := bearertest.Token()
	err := bearerToken.Sign(key.PrivateKey)
	if err != nil {
		return nil, err
	}

	bktName := "cors"
	res, err := tp.CreateContainer(middleware.SetBox(context.Background(), &middleware.Box{AccessBox: &accessbox.Box{
		Gate: &accessbox.GateData{
			BearerToken: &bearerToken,
			GateKey:     key.PublicKey(),
		},
	}}), frostfs.PrmContainerCreate{
		Name:   bktName,
		Policy: getPlacementPolicy(),
	})
	if err != nil {
		return nil, err
	}

	var owner user.ID
	user.IDFromKey(&owner, key.PrivateKey.PublicKey)

	return &data.BucketInfo{
		Name:                    bktName,
		Owner:                   owner,
		CID:                     res.ContainerID,
		HomomorphicHashDisabled: res.HomomorphicHashDisabled,
	}, nil
}

func getMinCacheConfig(logger *zap.Logger) *layer.CachesConfig {
	minCacheCfg := &cache.Config{
		Size:     1,
		Lifetime: 1,
		Logger:   logger,
	}
	return &layer.CachesConfig{
		Logger:        logger,
		Objects:       minCacheCfg,
		ObjectsList:   minCacheCfg,
		SessionList:   minCacheCfg,
		Names:         minCacheCfg,
		Buckets:       minCacheCfg,
		System:        minCacheCfg,
		AccessControl: minCacheCfg,
		Network:       &cache.NetworkCacheConfig{Lifetime: minCacheCfg.Lifetime},
	}
}

type apeMock struct {
	chainMap  map[engine.Target][]*chain.Chain
	policyMap map[string][]byte
	err       error
}

func newAPEMock() *apeMock {
	return &apeMock{
		chainMap:  map[engine.Target][]*chain.Chain{},
		policyMap: map[string][]byte{},
	}
}

func (a *apeMock) AddChain(target engine.Target, c *chain.Chain) error {
	list := a.chainMap[target]

	ind := slices.IndexFunc(list, func(item *chain.Chain) bool { return bytes.Equal(item.ID, c.ID) })
	if ind != -1 {
		list[ind] = c
	} else {
		list = append(list, c)
	}

	a.chainMap[target] = list
	return nil
}

func (a *apeMock) RemoveChain(target engine.Target, chainID chain.ID) error {
	a.chainMap[target] = slices.DeleteFunc(a.chainMap[target], func(item *chain.Chain) bool { return bytes.Equal(item.ID, chainID) })
	return nil
}

func (a *apeMock) ListChains(target engine.Target) ([]*chain.Chain, error) {
	return a.chainMap[target], nil
}

func (a *apeMock) PutPolicy(namespace string, cnrID cid.ID, policy []byte) error {
	a.policyMap[namespace+cnrID.EncodeToString()] = policy
	return nil
}

func (a *apeMock) DeletePolicy(namespace string, cnrID cid.ID) error {
	delete(a.policyMap, namespace+cnrID.EncodeToString())
	return nil
}

func (a *apeMock) PutBucketPolicy(ns string, cnrID cid.ID, policy []byte, chain []*chain.Chain) error {
	if a.err != nil {
		return a.err
	}

	if err := a.PutPolicy(ns, cnrID, policy); err != nil {
		return err
	}

	for i := range chain {
		if err := a.AddChain(engine.ContainerTarget(cnrID.EncodeToString()), chain[i]); err != nil {
			return err
		}
	}

	return nil
}

func (a *apeMock) DeleteBucketPolicy(ns string, cnrID cid.ID, chainIDs []chain.ID) error {
	if a.err != nil {
		return a.err
	}

	if err := a.DeletePolicy(ns, cnrID); err != nil {
		return err
	}
	for i := range chainIDs {
		if err := a.RemoveChain(engine.ContainerTarget(cnrID.EncodeToString()), chainIDs[i]); err != nil {
			return err
		}
	}

	return nil
}

func (a *apeMock) GetBucketPolicy(ns string, cnrID cid.ID) ([]byte, error) {
	if a.err != nil {
		return nil, a.err
	}

	policy, ok := a.policyMap[ns+cnrID.EncodeToString()]
	if !ok {
		return nil, errors.New("not found")
	}

	return policy, nil
}

func (a *apeMock) SaveACLChains(cid string, chains []*chain.Chain) error {
	if a.err != nil {
		return a.err
	}

	for i := range chains {
		if err := a.AddChain(engine.ContainerTarget(cid), chains[i]); err != nil {
			return err
		}
	}

	return nil
}

type frostfsidMock struct {
	data map[string]*keys.PublicKey
}

func newFrostfsIDMock() *frostfsidMock {
	return &frostfsidMock{data: map[string]*keys.PublicKey{}}
}

func (f *frostfsidMock) GetUserAddress(account, user string) (string, error) {
	res, ok := f.data[account+user]
	if !ok {
		return "", fmt.Errorf("not found")
	}

	return res.Address(), nil
}

func (f *frostfsidMock) GetUserKey(account, user string) (string, error) {
	res, ok := f.data[account+user]
	if !ok {
		return "", fmt.Errorf("not found")
	}

	return hex.EncodeToString(res.Bytes()), nil
}

func createTestBucket(hc *handlerContext, bktName string) *data.BucketInfo {
	info := createBucket(hc, bktName)
	return info.BktInfo
}

func createTestBucketWithLock(hc *handlerContext, bktName string, conf *data.ObjectLockConfiguration) *data.BucketInfo {
	res, err := hc.MockedPool().CreateContainer(hc.Context(), frostfs.PrmContainerCreate{
		Creator:              hc.owner,
		Name:                 bktName,
		AdditionalAttributes: [][2]string{{layer.AttributeLockEnabled, "true"}},
		Policy:               getPlacementPolicy(),
	})
	require.NoError(hc.t, err)

	var ownerID user.ID

	bktInfo := &data.BucketInfo{
		CID:                     res.ContainerID,
		Name:                    bktName,
		ObjectLockEnabled:       true,
		Owner:                   ownerID,
		HomomorphicHashDisabled: res.HomomorphicHashDisabled,
		PlacementPolicy:         getPlacementPolicy(),
	}

	key, err := keys.NewPrivateKey()
	require.NoError(hc.t, err)

	sp := &layer.PutSettingsParams{
		BktInfo: bktInfo,
		Settings: &data.BucketSettings{
			Versioning:        data.VersioningEnabled,
			LockConfiguration: conf,
			OwnerKey:          key.PublicKey(),
		},
	}

	err = hc.Layer().PutBucketSettings(hc.Context(), sp)
	require.NoError(hc.t, err)

	return bktInfo
}

func createTestObject(hc *handlerContext, bktInfo *data.BucketInfo, objName string, encryption encryption.Params) *data.ObjectInfo {
	content := make([]byte, 1024)
	_, err := rand.Read(content)
	require.NoError(hc.t, err)

	header := map[string]string{
		object.AttributeTimestamp: strconv.FormatInt(time.Now().UTC().Unix(), 10),
	}

	extObjInfo, err := hc.Layer().PutObject(hc.Context(), &layer.PutObjectParams{
		BktInfo:    bktInfo,
		Object:     objName,
		Size:       ptr(uint64(len(content))),
		Reader:     bytes.NewReader(content),
		Header:     header,
		Encryption: encryption,
	})
	require.NoError(hc.t, err)

	return extObjInfo.ObjectInfo
}

func prepareTestRequest(hc *handlerContext, bktName, objName string, body interface{}) (*httptest.ResponseRecorder, *http.Request) {
	return prepareTestFullRequest(hc, bktName, objName, make(url.Values), body)
}

func prepareTestFullRequest(hc *handlerContext, bktName, objName string, query url.Values, body interface{}) (*httptest.ResponseRecorder, *http.Request) {
	rawBody, err := xml.Marshal(body)
	require.NoError(hc.t, err)

	return prepareTestRequestWithQuery(hc, bktName, objName, query, rawBody)
}

func prepareTestRequestWithQuery(hc *handlerContext, bktName, objName string, query url.Values, body []byte) (*httptest.ResponseRecorder, *http.Request) {
	w := httptest.NewRecorder()
	r := httptest.NewRequest(http.MethodPut, defaultURL, bytes.NewReader(body))
	r.URL.RawQuery = query.Encode()

	reqInfo := middleware.NewReqInfo(w, r, middleware.ObjectRequest{Bucket: bktName, Object: objName}, "")
	reqInfo.User = hc.owner.String()
	r = r.WithContext(middleware.SetReqInfo(hc.Context(), reqInfo))

	return w, r
}

func prepareTestPayloadRequest(hc *handlerContext, bktName, objName string, payload io.Reader) (*httptest.ResponseRecorder, *http.Request) {
	w := httptest.NewRecorder()
	r := httptest.NewRequest(http.MethodPut, defaultURL, payload)

	reqInfo := middleware.NewReqInfo(w, r, middleware.ObjectRequest{Bucket: bktName, Object: objName}, "")
	r = r.WithContext(middleware.SetReqInfo(hc.Context(), reqInfo))

	return w, r
}

func parseTestResponse(t *testing.T, response *httptest.ResponseRecorder, body interface{}) {
	assertStatus(t, response, http.StatusOK)
	err := xml.NewDecoder(response.Result().Body).Decode(body)
	require.NoError(t, err)
}

func existInMockedFrostFS(tc *handlerContext, bktInfo *data.BucketInfo, objInfo *data.ObjectInfo) bool {
	p := &layer.GetObjectParams{
		BucketInfo: bktInfo,
		ObjectInfo: objInfo,
	}

	objPayload, err := tc.Layer().GetObject(tc.Context(), p)
	if err != nil {
		return false
	}

	_, err = io.ReadAll(objPayload)
	require.NoError(tc.t, err)
	return true
}

func listOIDsFromMockedFrostFS(t *testing.T, tc *handlerContext, bktName string) []oid.ID {
	bktInfo, err := tc.Layer().GetBucketInfo(tc.Context(), bktName)
	require.NoError(t, err)

	return tc.MockedPool().AllObjects(bktInfo.CID)
}

func assertStatus(t *testing.T, w *httptest.ResponseRecorder, status int) {
	if w.Code != status {
		resp, err := io.ReadAll(w.Result().Body)
		require.NoError(t, err)
		require.Failf(t, "unexpected status", "expected: %d, actual: %d, resp: '%s'", status, w.Code, string(resp))
	}
}

func readResponse(t *testing.T, w *httptest.ResponseRecorder, status int, model interface{}) {
	assertStatus(t, w, status)
	if status == http.StatusOK {
		err := xml.NewDecoder(w.Result().Body).Decode(model)
		require.NoError(t, err)
	}
}

func getPlacementPolicy() (p netmap.PlacementPolicy) {
	var r netmap.ReplicaDescriptor
	r.SetNumberOfObjects(1)
	p.AddReplicas([]netmap.ReplicaDescriptor{r}...)
	return p
}