package handler

import (
	"net/http"
	"testing"
	"time"

	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
	s3errors "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
	apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
	cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"github.com/stretchr/testify/require"
)

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

	bktName, objName := "bucket-for-conditional", "object"
	_, objInfo := createBucketAndObject(tc, bktName, objName)

	w, r := prepareTestRequest(tc, bktName, objName, nil)
	tc.Handler().HeadObjectHandler(w, r)
	assertStatus(t, w, http.StatusOK)
	etag := w.Result().Header.Get(api.ETag)
	etagQuoted := "\"" + etag + "\""

	headers := map[string]string{api.IfMatch: etag}
	headObject(t, tc, bktName, objName, headers, http.StatusOK)

	headers = map[string]string{api.IfMatch: etagQuoted}
	headObject(t, tc, bktName, objName, headers, http.StatusOK)

	headers = map[string]string{api.IfMatch: "etag"}
	headObject(t, tc, bktName, objName, headers, http.StatusPreconditionFailed)

	headers = map[string]string{api.IfUnmodifiedSince: objInfo.Created.Add(time.Minute).Format(http.TimeFormat)}
	headObject(t, tc, bktName, objName, headers, http.StatusOK)

	var zeroTime time.Time
	headers = map[string]string{api.IfUnmodifiedSince: zeroTime.UTC().Format(http.TimeFormat)}
	headObject(t, tc, bktName, objName, headers, http.StatusPreconditionFailed)

	headers = map[string]string{
		api.IfMatch:           etag,
		api.IfUnmodifiedSince: zeroTime.UTC().Format(http.TimeFormat),
	}
	headObject(t, tc, bktName, objName, headers, http.StatusOK)

	headers = map[string]string{api.IfNoneMatch: etag}
	headObject(t, tc, bktName, objName, headers, http.StatusNotModified)

	headers = map[string]string{api.IfNoneMatch: etagQuoted}
	headObject(t, tc, bktName, objName, headers, http.StatusNotModified)

	headers = map[string]string{api.IfNoneMatch: "etag"}
	headObject(t, tc, bktName, objName, headers, http.StatusOK)

	headers = map[string]string{api.IfModifiedSince: zeroTime.UTC().Format(http.TimeFormat)}
	headObject(t, tc, bktName, objName, headers, http.StatusOK)

	headers = map[string]string{api.IfModifiedSince: time.Now().Add(time.Minute).UTC().Format(http.TimeFormat)}
	headObject(t, tc, bktName, objName, headers, http.StatusNotModified)

	headers = map[string]string{
		api.IfNoneMatch:     etag,
		api.IfModifiedSince: zeroTime.UTC().Format(http.TimeFormat),
	}
	headObject(t, tc, bktName, objName, headers, http.StatusNotModified)
}

func headObject(t *testing.T, tc *handlerContext, bktName, objName string, headers map[string]string, status int) {
	w, r := prepareTestRequest(tc, bktName, objName, nil)

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

	tc.Handler().HeadObjectHandler(w, r)
	assertStatus(t, w, status)
}

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

	bktName, objName := "bucket-for-cache", "obj-for-cache"
	bktInfo, _ := createBucketAndObject(hc, bktName, objName)
	setContainerEACL(hc, bktInfo.CID)

	headObject(t, hc, bktName, objName, nil, http.StatusOK)

	w, r := prepareTestRequest(hc, bktName, objName, nil)
	hc.Handler().HeadObjectHandler(w, r.WithContext(middleware.SetBoxData(r.Context(), newTestAccessBox(t, nil))))
	assertStatus(t, w, http.StatusForbidden)
}

func setContainerEACL(hc *handlerContext, cnrID cid.ID) {
	table := eacl.NewTable()
	table.SetCID(cnrID)
	for _, op := range fullOps {
		table.AddRecord(getOthersRecord(op, eacl.ActionDeny))
	}

	err := hc.MockedPool().SetContainerEACL(hc.Context(), *table, nil)
	require.NoError(hc.t, err)
}

func TestHeadObject(t *testing.T) {
	hc := prepareHandlerContextWithMinCache(t)
	bktName, objName := "bucket", "obj"
	bktInfo, objInfo := createVersionedBucketAndObject(hc.t, hc, bktName, objName)

	putObject(hc.t, hc, bktName, objName)

	checkFound(hc.t, hc, bktName, objName, objInfo.VersionID())
	checkFound(hc.t, hc, bktName, objName, emptyVersion)

	addr := getAddressOfLastVersion(hc, bktInfo, objName)
	hc.tp.SetObjectError(addr, apistatus.ObjectNotFound{})
	hc.tp.SetObjectError(objInfo.Address(), apistatus.ObjectNotFound{})

	headObjectAssertS3Error(hc, bktName, objName, objInfo.VersionID(), s3errors.ErrNoSuchVersion)
	headObjectAssertS3Error(hc, bktName, objName, emptyVersion, s3errors.ErrNoSuchKey)
}

func TestIsAvailableToResolve(t *testing.T) {
	list := []string{"container", "s3"}

	for i, testCase := range [...]struct {
		isAllowList bool
		list        []string
		zone        string
		expected    bool
	}{
		{isAllowList: true, list: list, zone: "container", expected: true},
		{isAllowList: true, list: list, zone: "sftp", expected: false},
		{isAllowList: false, list: list, zone: "s3", expected: false},
		{isAllowList: false, list: list, zone: "system", expected: true},
		{isAllowList: true, list: list, zone: "", expected: false},
	} {
		result := isAvailableToResolve(testCase.zone, testCase.list, testCase.isAllowList)
		require.Equal(t, testCase.expected, result, "case %d", i+1)
	}
}

func newTestAccessBox(t *testing.T, key *keys.PrivateKey) *accessbox.Box {
	var err error
	if key == nil {
		key, err = keys.NewPrivateKey()
		require.NoError(t, err)
	}

	var btoken bearer.Token
	btoken.SetEACLTable(*eacl.NewTable())
	err = btoken.Sign(key.PrivateKey)
	require.NoError(t, err)

	return &accessbox.Box{
		Gate: &accessbox.GateData{
			BearerToken: &btoken,
		},
	}
}