package accessbox

import (
	"encoding/hex"
	"testing"

	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
	frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
	"github.com/google/uuid"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"github.com/stretchr/testify/require"
)

func assertBearerToken(t *testing.T, exp, act bearer.Token) {
	// compare binary representations since deep equal is not guaranteed
	require.Equal(t, exp.Marshal(), act.Marshal())
}

func TestTokensEncryptDecrypt(t *testing.T) {
	var (
		tkn  bearer.Token
		tkn2 bearer.Token
	)
	sec, err := keys.NewPrivateKey()
	require.NoError(t, err)

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

	tkn.SetEACLTable(*eacl.NewTable())
	require.NoError(t, tkn.Sign(sec.PrivateKey))

	data, err := encrypt(cred, cred.PublicKey(), tkn.Marshal())
	require.NoError(t, err)

	rawTkn2, err := decrypt(cred, cred.PublicKey(), data)
	require.NoError(t, err)

	err = tkn2.Unmarshal(rawTkn2)
	require.NoError(t, err)

	assertBearerToken(t, tkn, tkn2)
}

func TestBearerTokenInAccessBox(t *testing.T) {
	var (
		box  *AccessBox
		box2 AccessBox
		tkn  bearer.Token
	)

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

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

	tkn.SetEACLTable(*eacl.NewTable())
	require.NoError(t, tkn.Sign(sec.PrivateKey))

	gate := NewGateData(cred.PublicKey(), &tkn)
	box, _, err = PackTokens([]*GateData{gate}, nil)
	require.NoError(t, err)

	data, err := box.Marshal()
	require.NoError(t, err)

	err = box2.Unmarshal(data)
	require.NoError(t, err)

	tkns, err := box2.GetTokens(cred)
	require.NoError(t, err)

	assertBearerToken(t, tkn, *tkns.BearerToken)
}

func TestSessionTokenInAccessBox(t *testing.T) {
	var (
		box  *AccessBox
		box2 AccessBox
		tkn  = new(session.Container)
	)

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

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

	tkn.SetID(uuid.New())
	tkn.SetAuthKey((*frostfsecdsa.PublicKey)(sec.PublicKey()))
	require.NoError(t, tkn.Sign(sec.PrivateKey))

	var newTkn bearer.Token
	gate := NewGateData(cred.PublicKey(), &newTkn)
	gate.SessionTokens = []*session.Container{tkn}
	box, _, err = PackTokens([]*GateData{gate}, nil)
	require.NoError(t, err)

	data, err := box.Marshal()
	require.NoError(t, err)

	err = box2.Unmarshal(data)
	require.NoError(t, err)

	tkns, err := box2.GetTokens(cred)
	require.NoError(t, err)

	require.Equal(t, []*session.Container{tkn}, tkns.SessionTokens)
}

func TestAccessboxMultipleKeys(t *testing.T) {
	var (
		box *AccessBox
		tkn bearer.Token
	)

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

	tkn.SetEACLTable(*eacl.NewTable())
	require.NoError(t, tkn.Sign(sec.PrivateKey))

	count := 10
	gates := make([]*GateData, 0, count)
	privateKeys := make([]*keys.PrivateKey, 0, count)
	{ // generate keys
		for i := 0; i < count; i++ {
			cred, err := keys.NewPrivateKey()
			require.NoError(t, err)

			gates = append(gates, NewGateData(cred.PublicKey(), &tkn))
			privateKeys = append(privateKeys, cred)
		}
	}

	box, _, err = PackTokens(gates, nil)
	require.NoError(t, err)

	for i, k := range privateKeys {
		tkns, err := box.GetTokens(k)
		require.NoError(t, err, "key #%d: %s failed", i, k)
		assertBearerToken(t, tkn, *tkns.BearerToken)
	}
}

func TestUnknownKey(t *testing.T) {
	var (
		box *AccessBox
		tkn bearer.Token
	)

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

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

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

	tkn.SetEACLTable(*eacl.NewTable())
	require.NoError(t, tkn.Sign(sec.PrivateKey))

	gate := NewGateData(cred.PublicKey(), &tkn)
	box, _, err = PackTokens([]*GateData{gate}, nil)
	require.NoError(t, err)

	_, err = box.GetTokens(wrongCred)
	require.Error(t, err)
}

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

	var tkn bearer.Token
	gate := NewGateData(cred.PublicKey(), &tkn)
	require.Equal(t, cred.PublicKey(), gate.GateKey)
	assertBearerToken(t, tkn, *gate.BearerToken)

	t.Run("session token for put", func(t *testing.T) {
		gate.SessionTokens = []*session.Container{}
		sessionTkn := gate.SessionTokenForPut()
		require.Nil(t, sessionTkn)

		sessionTknPut := new(session.Container)
		sessionTknPut.ForVerb(session.VerbContainerPut)
		gate.SessionTokens = []*session.Container{sessionTknPut}
		sessionTkn = gate.SessionTokenForPut()
		require.Equal(t, sessionTknPut, sessionTkn)
	})

	t.Run("session token for delete", func(t *testing.T) {
		gate.SessionTokens = []*session.Container{}
		sessionTkn := gate.SessionTokenForDelete()
		require.Nil(t, sessionTkn)

		sessionTknDelete := new(session.Container)
		sessionTknDelete.ForVerb(session.VerbContainerDelete)
		gate.SessionTokens = []*session.Container{sessionTknDelete}
		sessionTkn = gate.SessionTokenForDelete()
		require.Equal(t, sessionTknDelete, sessionTkn)
	})

	t.Run("session token", func(t *testing.T) {
		gate.SessionTokens = []*session.Container{}
		sessionTkn := gate.SessionToken()
		require.Nil(t, sessionTkn)

		sessionTknPut := new(session.Container)
		sessionTknPut.ForVerb(session.VerbContainerPut)
		gate.SessionTokens = []*session.Container{sessionTknPut}
		sessionTkn = gate.SessionToken()
		require.Equal(t, sessionTkn, sessionTknPut)
	})
}

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

	var tkn bearer.Token
	gate := NewGateData(cred.PublicKey(), &tkn)

	secret := []byte("secret")
	accessBox, _, err := PackTokens([]*GateData{gate}, secret)
	require.NoError(t, err)

	box, err := accessBox.GetBox(cred)
	require.NoError(t, err)
	require.Equal(t, hex.EncodeToString(secret), box.Gate.SecretKey)
}

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

	var tkn bearer.Token
	gate := NewGateData(cred.PublicKey(), &tkn)

	accessBox, _, err := PackTokens([]*GateData{gate}, nil)
	require.NoError(t, err)

	t.Run("invalid owner", func(t *testing.T) {
		randomKey, err := keys.NewPrivateKey()
		require.NoError(t, err)

		_, err = accessBox.GetTokens(randomKey)
		require.Error(t, err)

		_, err = accessBox.GetBox(randomKey)
		require.Error(t, err)
	})

	t.Run("empty placement policy", func(t *testing.T) {
		policy, err := accessBox.GetPlacementPolicy()
		require.NoError(t, err)
		require.Nil(t, policy)
	})

	t.Run("get correct placement policy", func(t *testing.T) {
		policy := &AccessBox_ContainerPolicy{LocationConstraint: "locationConstraint"}
		accessBox.ContainerPolicy = []*AccessBox_ContainerPolicy{policy}

		policies, err := accessBox.GetPlacementPolicy()
		require.NoError(t, err)
		require.Len(t, policies, 1)
		require.Equal(t, policy.LocationConstraint, policies[0].LocationConstraint)
	})

	t.Run("get incorrect placement policy", func(t *testing.T) {
		policy := &AccessBox_ContainerPolicy{
			LocationConstraint: "locationConstraint",
			Policy:             []byte("policy"),
		}
		accessBox.ContainerPolicy = []*AccessBox_ContainerPolicy{policy}

		_, err = accessBox.GetPlacementPolicy()
		require.Error(t, err)

		_, err = accessBox.GetBox(cred)
		require.Error(t, err)
	})

	t.Run("empty seed key", func(t *testing.T) {
		accessBox.SeedKey = nil

		_, err = accessBox.GetTokens(cred)
		require.Error(t, err)

		_, err = accessBox.GetBox(cred)
		require.Error(t, err)
	})

	t.Run("invalid gate key", func(t *testing.T) {
		gate = &GateData{
			BearerToken: &tkn,
			GateKey:     &keys.PublicKey{},
		}
		_, _, err = PackTokens([]*GateData{gate}, nil)
		require.Error(t, err)
	})
}