package v2

import (
	"testing"

	"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
	sessionV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
	sigutilV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/util/signature"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
	cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test"
	sessionSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
	"github.com/google/uuid"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"github.com/stretchr/testify/require"
)

func TestRequestOwner(t *testing.T) {
	containerOwner, err := keys.NewPrivateKey()
	require.NoError(t, err)

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

	var userID user.ID
	user.IDFromKey(&userID, userPk.PrivateKey.PublicKey)

	var userSignature refs.Signature
	userSignature.SetKey(userPk.PublicKey().Bytes())

	vh := new(sessionV2.RequestVerificationHeader)
	vh.SetBodySignature(&userSignature)

	t.Run("empty verification header", func(t *testing.T) {
		req := MetaWithToken{}
		checkOwner(t, req, nil, errEmptyVerificationHeader)
	})
	t.Run("empty verification header signature", func(t *testing.T) {
		req := MetaWithToken{
			vheader: new(sessionV2.RequestVerificationHeader),
		}
		checkOwner(t, req, nil, errEmptyBodySig)
	})
	t.Run("no tokens", func(t *testing.T) {
		req := MetaWithToken{
			vheader: vh,
		}
		checkOwner(t, req, userPk.PublicKey(), nil)
	})

	t.Run("bearer without impersonate, no session", func(t *testing.T) {
		req := MetaWithToken{
			vheader: vh,
			bearer:  newBearer(t, containerOwner, userID, false),
		}
		checkOwner(t, req, userPk.PublicKey(), nil)
	})
	t.Run("bearer with impersonate, no session", func(t *testing.T) {
		req := MetaWithToken{
			vheader: vh,
			bearer:  newBearer(t, containerOwner, userID, true),
		}
		checkOwner(t, req, containerOwner.PublicKey(), nil)
	})
	t.Run("bearer with impersonate, with session", func(t *testing.T) {
		// To check that bearer token takes priority, use different key to sign session token.
		pk, err := keys.NewPrivateKey()
		require.NoError(t, err)

		req := MetaWithToken{
			vheader: vh,
			bearer:  newBearer(t, containerOwner, userID, true),
			token:   newSession(t, pk),
		}
		checkOwner(t, req, containerOwner.PublicKey(), nil)
	})
	t.Run("with session", func(t *testing.T) {
		req := MetaWithToken{
			vheader: vh,
			token:   newSession(t, containerOwner),
		}
		checkOwner(t, req, containerOwner.PublicKey(), nil)
	})
	t.Run("malformed session token", func(t *testing.T) {
		// This test is tricky: session token has issuer field and signature, which must correspond to each other.
		// SDK prevents constructing such token in the first place, but it is still possible via API.
		// Thus, construct v2 token, convert it to SDK one and pass to our function.
		pk, err := keys.NewPrivateKey()
		require.NoError(t, err)

		var user1 user.ID
		user.IDFromKey(&user1, pk.PrivateKey.PublicKey)

		var id refs.OwnerID
		id.SetValue(user1.WalletBytes())

		raw, err := uuid.New().MarshalBinary()
		require.NoError(t, err)

		var cidV2 refs.ContainerID
		cidtest.ID().WriteToV2(&cidV2)

		sessionCtx := new(sessionV2.ObjectSessionContext)
		sessionCtx.SetTarget(&cidV2)

		var body sessionV2.TokenBody
		body.SetOwnerID(&id)
		body.SetID(raw)
		body.SetLifetime(new(sessionV2.TokenLifetime))
		body.SetSessionKey(pk.PublicKey().Bytes())
		body.SetContext(sessionCtx)

		var tokV2 sessionV2.Token
		tokV2.SetBody(&body)
		require.NoError(t, sigutilV2.SignData(&containerOwner.PrivateKey, smWrapper{Token: &tokV2}))
		require.NoError(t, sigutilV2.VerifyData(smWrapper{Token: &tokV2}))

		var tok sessionSDK.Object
		require.NoError(t, tok.ReadFromV2(tokV2))

		req := MetaWithToken{
			vheader: vh,
			token:   &tok,
		}
		checkOwner(t, req, nil, errInvalidSessionOwner)
	})
}

type smWrapper struct {
	*sessionV2.Token
}

func (s smWrapper) ReadSignedData(data []byte) ([]byte, error) {
	return s.Token.GetBody().StableMarshal(data), nil
}

func (s smWrapper) SignedDataSize() int {
	return s.Token.GetBody().StableSize()
}

func newSession(t *testing.T, pk *keys.PrivateKey) *sessionSDK.Object {
	var tok sessionSDK.Object
	require.NoError(t, tok.Sign(pk.PrivateKey))
	return &tok
}

func newBearer(t *testing.T, pk *keys.PrivateKey, user user.ID, impersonate bool) *bearer.Token {
	var tok bearer.Token
	tok.SetImpersonate(impersonate)
	tok.ForUser(user)
	require.NoError(t, tok.Sign(pk.PrivateKey))
	return &tok
}

func checkOwner(t *testing.T, req MetaWithToken, expected *keys.PublicKey, expectedErr error) {
	_, actual, err := req.RequestOwner()
	if expectedErr != nil {
		require.ErrorIs(t, err, expectedErr)
		return
	}

	require.NoError(t, err)
	require.Equal(t, expected, actual)
}