package ape

import (
	"context"
	"fmt"
	"net"
	"testing"

	objectV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
	aperequest "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/ape/request"
	checksumtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/checksum/test"
	objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
	usertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user/test"
	commonschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/common"
	nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
	"github.com/stretchr/testify/require"
	"google.golang.org/grpc/peer"
)

const (
	testOwnerID = "FPPtmAi9TCX329"

	incomingIP = "192.92.33.1"
)

func ctxWithPeerInfo() context.Context {
	return peer.NewContext(context.Background(), &peer.Peer{
		Addr: &net.TCPAddr{
			IP:   net.ParseIP(incomingIP),
			Port: 41111,
		},
	})
}

func TestObjectProperties(t *testing.T) {
	for _, test := range []struct {
		name      string
		container string
		object    *string
		header    *headerObjectSDKParams
	}{
		{
			name:      "fully filled header",
			container: containerID,
			object:    stringPtr(objectID),
			header: &headerObjectSDKParams{
				majorVersion:           1,
				minorVersion:           1,
				owner:                  usertest.ID(),
				epoch:                  3,
				payloadSize:            1000,
				typ:                    objectSDK.TypeRegular,
				payloadChecksum:        checksumtest.Checksum(),
				payloadHomomorphicHash: checksumtest.Checksum(),
				attributes: []struct {
					key string
					val string
				}{
					{
						key: "attr1",
						val: "val1",
					},
					{
						key: "attr2",
						val: "val2",
					},
				},
			},
		},
		{
			name:      "partially filled header",
			container: containerID,
			header: &headerObjectSDKParams{
				majorVersion: 1,
				minorVersion: 1,
				owner:        usertest.ID(),
				epoch:        3,
				attributes: []struct {
					key string
					val string
				}{
					{
						key: "attr1",
						val: "val1",
					},
				},
			},
		},
		{
			name:      "only address paramaters set in header",
			container: containerID,
			object:    stringPtr(objectID),
		},
		{
			name:      "only container set in header",
			container: containerID,
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			cnr := newContainerIDSDK(t, test.container)
			obj := newObjectIDSDK(t, test.object)
			header := newHeaderObjectSDK(cnr, obj, test.header)

			var testCnrOwner user.ID
			require.NoError(t, testCnrOwner.DecodeString(testOwnerID))

			props := objectProperties(cnr, obj, testCnrOwner, header.ToV2().GetHeader())
			require.Equal(t, test.container, props[nativeschema.PropertyKeyObjectContainerID])
			require.Equal(t, testOwnerID, props[nativeschema.PropertyKeyContainerOwnerID])

			if obj != nil {
				require.Equal(t, *test.object, props[nativeschema.PropertyKeyObjectID])
			}

			if test.header != nil {
				require.Equal(t,
					fmt.Sprintf("v%d.%d", test.header.majorVersion, test.header.minorVersion),
					props[nativeschema.PropertyKeyObjectVersion],
				)
				require.Equal(t, test.header.owner.EncodeToString(), props[nativeschema.PropertyKeyObjectOwnerID])
				require.Equal(t, fmt.Sprintf("%d", test.header.epoch), props[nativeschema.PropertyKeyObjectCreationEpoch])
				require.Equal(t, fmt.Sprintf("%d", test.header.payloadSize), props[nativeschema.PropertyKeyObjectPayloadLength])
				require.Equal(t, test.header.typ.String(), props[nativeschema.PropertyKeyObjectType])
				require.Equal(t, test.header.payloadChecksum.String(), props[nativeschema.PropertyKeyObjectPayloadHash])
				require.Equal(t, test.header.payloadHomomorphicHash.String(), props[nativeschema.PropertyKeyObjectHomomorphicHash])

				for _, attr := range test.header.attributes {
					require.Equal(t, attr.val, props[attr.key])
				}
			}
		})
	}
}

func TestNewAPERequest(t *testing.T) {
	tests := []struct {
		name      string
		methods   []string
		namespace string
		container string
		object    *string
		header    testHeader
		expectErr error
	}{
		{
			name:      "oid required requests",
			methods:   methodsRequiredOID,
			namespace: namespace,
			container: containerID,
			object:    stringPtr(objectID),
			header: testHeader{
				headerObjSDK: &headerObjectSDKParams{
					majorVersion:           1,
					minorVersion:           1,
					owner:                  usertest.ID(),
					epoch:                  3,
					payloadSize:            1000,
					typ:                    objectSDK.TypeRegular,
					payloadChecksum:        checksumtest.Checksum(),
					payloadHomomorphicHash: checksumtest.Checksum(),
				},
				fromHeaderProvider: true,
			},
		},
		{
			name:      "oid required requests but header cannot be found locally",
			methods:   methodsRequiredOID,
			namespace: namespace,
			container: containerID,
			object:    stringPtr(objectID),
			header:    testHeader{},
		},
		{
			name:      "oid required requests missed oid",
			methods:   methodsRequiredOID,
			namespace: namespace,
			container: containerID,
			object:    nil,
			header:    testHeader{},
			expectErr: errMissingOID,
		},
		{
			name:      "response for oid required requests",
			methods:   methodsRequiredOID,
			namespace: namespace,
			container: containerID,
			object:    stringPtr(objectID),
			header: testHeader{
				headerObjSDK: &headerObjectSDKParams{
					majorVersion:           1,
					minorVersion:           1,
					owner:                  usertest.ID(),
					epoch:                  3,
					payloadSize:            1000,
					typ:                    objectSDK.TypeRegular,
					payloadChecksum:        checksumtest.Checksum(),
					payloadHomomorphicHash: checksumtest.Checksum(),
				},
				fromRequestResponseHeader: true,
			},
		},
		{
			name:      "oid not required methods request",
			methods:   methodsOptionalOID,
			namespace: namespace,
			container: containerID,
			object:    nil,
			header: testHeader{
				headerObjSDK: &headerObjectSDKParams{
					majorVersion: 6,
					minorVersion: 66,
					owner:        usertest.ID(),
					epoch:        3,
					typ:          objectSDK.TypeLock,
				},
				fromRequestResponseHeader: true,
			},
		},
		{
			name:      "oid not required methods request but no header",
			methods:   methodsOptionalOID,
			namespace: namespace,
			container: containerID,
			object:    nil,
			header:    testHeader{},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			for _, method := range test.methods {
				t.Run(method, func(t *testing.T) {
					cnr := newContainerIDSDK(t, test.container)
					obj := newObjectIDSDK(t, test.object)

					var testCnrOwner user.ID
					require.NoError(t, testCnrOwner.DecodeString(testOwnerID))

					prm := Prm{
						Namespace:      test.namespace,
						Method:         method,
						Container:      cnr,
						Object:         obj,
						Role:           role,
						SenderKey:      senderKey,
						ContainerOwner: testCnrOwner,
					}

					headerSource := newHeaderProviderMock()
					ffidProvider := newFrostfsIDProviderMock(t)

					var headerObjSDK *objectSDK.Object
					if test.header.headerObjSDK != nil {
						headerObjSDK = newHeaderObjectSDK(cnr, obj, test.header.headerObjSDK)
						if test.header.fromHeaderProvider {
							require.NotNil(t, obj, "oid is required if a header is expected to be found in header provider")
							headerSource.addHeader(cnr, *obj, headerObjSDK)
						} else if test.header.fromRequestResponseHeader {
							prm.Header = headerObjSDK.ToV2().GetHeader()
						}
					}

					c := checkerImpl{
						headerProvider:  headerSource,
						frostFSIDClient: ffidProvider,
					}

					r, err := c.newAPERequest(ctxWithPeerInfo(), prm)
					if test.expectErr != nil {
						require.Error(t, err)
						require.ErrorIs(t, err, test.expectErr)
						return
					}

					expectedRequest := aperequest.NewRequest(
						method,
						aperequest.NewResource(
							resourceName(cnr, obj, prm.Namespace),
							objectProperties(cnr, obj, testCnrOwner, func() *objectV2.Header {
								if headerObjSDK != nil {
									return headerObjSDK.ToV2().GetHeader()
								}
								return prm.Header
							}())),
						map[string]string{
							nativeschema.PropertyKeyActorPublicKey:                                     prm.SenderKey,
							nativeschema.PropertyKeyActorRole:                                          prm.Role,
							fmt.Sprintf(commonschema.PropertyKeyFormatFrostFSIDUserClaim, "tag-attr1"): "value1",
							fmt.Sprintf(commonschema.PropertyKeyFormatFrostFSIDUserClaim, "tag-attr2"): "value2",
							commonschema.PropertyKeyFrostFSIDGroupID:                                   "1",
							commonschema.PropertyKeyFrostFSSourceIP:                                    incomingIP,
						},
					)

					require.Equal(t, expectedRequest, r)
				})
			}
		})
	}
}

func TestResourceName(t *testing.T) {
	for _, test := range []struct {
		name      string
		namespace string
		container string
		object    *string
		expected  string
	}{
		{
			name:      "non-root namespace, CID",
			namespace: namespace,
			container: containerID,
			expected:  fmt.Sprintf("native:object/%s/%s/*", namespace, containerID),
		},
		{
			name:      "non-root namespace, CID, OID",
			namespace: namespace,
			container: containerID,
			object:    stringPtr(objectID),
			expected:  fmt.Sprintf("native:object/%s/%s/%s", namespace, containerID, objectID),
		},
		{
			name:      "empty namespace, CID",
			namespace: "",
			container: containerID,
			expected:  fmt.Sprintf("native:object//%s/*", containerID),
		},
		{
			name:      "empty namespace, CID, OID",
			namespace: "",
			container: containerID,
			object:    stringPtr(objectID),
			expected:  fmt.Sprintf("native:object//%s/%s", containerID, objectID),
		},
		{
			name:      "root namespace, CID",
			namespace: "root",
			container: containerID,
			expected:  fmt.Sprintf("native:object//%s/*", containerID),
		},
		{
			name:      "root namespace, CID, OID",
			namespace: "root",
			container: containerID,
			object:    stringPtr(objectID),
			expected:  fmt.Sprintf("native:object//%s/%s", containerID, objectID),
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			cnr := newContainerIDSDK(t, test.container)
			obj := newObjectIDSDK(t, test.object)
			require.Equal(t, test.expected, resourceName(cnr, obj, test.namespace))
		})
	}
}