package tokens

import (
	"context"
	"encoding/hex"
	"errors"
	"testing"
	"time"

	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/cache"
	"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"
	cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
	oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
	oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap/zaptest"
)

type frostfsMock struct {
	objects map[oid.Address][]*object.Object
	errors  map[oid.Address]error
}

func newFrostfsMock() *frostfsMock {
	return &frostfsMock{
		objects: map[oid.Address][]*object.Object{},
		errors:  map[oid.Address]error{},
	}
}

func (f *frostfsMock) CreateObject(_ context.Context, prm PrmObjectCreate) (oid.ID, error) {
	var obj object.Object
	obj.SetPayload(prm.Payload)
	obj.SetOwnerID(prm.Creator)
	obj.SetContainerID(prm.Container)

	a := object.NewAttribute()
	a.SetKey(object.AttributeFilePath)
	a.SetValue(prm.Filepath)
	prm.CustomAttributes = append(prm.CustomAttributes, *a)
	obj.SetAttributes(prm.CustomAttributes...)

	if prm.NewVersionFor != nil {
		var addr oid.Address
		addr.SetObject(*prm.NewVersionFor)
		addr.SetContainer(prm.Container)

		_, ok := f.objects[addr]
		if !ok {
			return oid.ID{}, errors.New("not found")
		}

		objID := oidtest.ID()
		obj.SetID(objID)
		f.objects[addr] = append(f.objects[addr], &obj)

		return objID, nil
	}

	objID := oidtest.ID()
	obj.SetID(objID)

	var addr oid.Address
	addr.SetObject(objID)
	addr.SetContainer(prm.Container)
	f.objects[addr] = []*object.Object{&obj}

	return objID, nil
}

func (f *frostfsMock) GetCredsObject(_ context.Context, address oid.Address) (*object.Object, error) {
	if err := f.errors[address]; err != nil {
		return nil, err
	}

	objects, ok := f.objects[address]
	if !ok {
		return nil, errors.New("not found")
	}

	return objects[len(objects)-1], nil
}

func TestRemovingAccessBox(t *testing.T) {
	ctx := context.Background()

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

	gateData := []*accessbox.GateData{{
		BearerToken: &bearer.Token{},
		GateKey:     key.PublicKey(),
	}}

	secretKey := "713d0a0b9efc7d22923e17b0402a6a89b4273bc711c8bacb2da1b643d0006aeb"
	sk, err := hex.DecodeString(secretKey)
	require.NoError(t, err)

	accessBox, _, err := accessbox.PackTokens(gateData, sk)
	require.NoError(t, err)
	data, err := accessBox.Marshal()
	require.NoError(t, err)

	var obj object.Object
	obj.SetPayload(data)
	addr := oidtest.Address()
	obj.SetID(addr.Object())
	obj.SetContainerID(addr.Container())

	frostfs := &frostfsMock{
		objects: map[oid.Address][]*object.Object{addr: {&obj}},
		errors:  map[oid.Address]error{},
	}

	cfg := Config{
		FrostFS: frostfs,
		Key:     key,
		CacheConfig: &cache.Config{
			Size:     10,
			Lifetime: 24 * time.Hour,
			Logger:   zaptest.NewLogger(t),
		},
		RemovingCheckAfterDurations: 0, // means check always
	}

	creds := New(cfg)

	_, _, err = creds.GetBox(ctx, addr)
	require.NoError(t, err)

	frostfs.errors[addr] = errors.New("network error")
	_, _, err = creds.GetBox(ctx, addr)
	require.NoError(t, err)

	frostfs.errors[addr] = &apistatus.ObjectAlreadyRemoved{}
	_, _, err = creds.GetBox(ctx, addr)
	require.Error(t, err)
}

func TestGetBox(t *testing.T) {
	ctx := context.Background()

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

	gateData := []*accessbox.GateData{{
		BearerToken: &bearer.Token{},
		GateKey:     key.PublicKey(),
	}}

	secret := []byte("secret")
	accessBox, _, err := accessbox.PackTokens(gateData, secret)
	require.NoError(t, err)
	data, err := accessBox.Marshal()
	require.NoError(t, err)

	var attr object.Attribute
	attr.SetKey("key")
	attr.SetValue("value")
	attrs := []object.Attribute{attr}

	cfg := Config{
		CacheConfig: &cache.Config{
			Size:     10,
			Lifetime: 24 * time.Hour,
			Logger:   zaptest.NewLogger(t),
		},
	}

	t.Run("no removing check, accessbox from cache", func(t *testing.T) {
		frostfs := newFrostfsMock()
		cfg.FrostFS = frostfs
		cfg.RemovingCheckAfterDurations = time.Hour
		cfg.Key = key
		creds := New(cfg)

		cnrID := cidtest.ID()
		addr, err := creds.Put(ctx, cnrID, CredentialsParam{Keys: keys.PublicKeys{key.PublicKey()}, AccessBox: accessBox})
		require.NoError(t, err)

		_, _, err = creds.GetBox(ctx, addr)
		require.NoError(t, err)

		frostfs.errors[addr] = &apistatus.ObjectAlreadyRemoved{}
		_, _, err = creds.GetBox(ctx, addr)
		require.NoError(t, err)
	})

	t.Run("error while getting box from frostfs", func(t *testing.T) {
		frostfs := newFrostfsMock()
		cfg.FrostFS = frostfs
		cfg.RemovingCheckAfterDurations = 0
		cfg.Key = key
		creds := New(cfg)

		cnrID := cidtest.ID()
		addr, err := creds.Put(ctx, cnrID, CredentialsParam{Keys: keys.PublicKeys{key.PublicKey()}, AccessBox: accessBox})
		require.NoError(t, err)

		frostfs.errors[addr] = errors.New("network error")
		_, _, err = creds.GetBox(ctx, addr)
		require.Error(t, err)
	})

	t.Run("invalid key", func(t *testing.T) {
		frostfs := newFrostfsMock()

		var obj object.Object
		obj.SetPayload(data)
		addr := oidtest.Address()
		frostfs.objects[addr] = []*object.Object{&obj}

		cfg.FrostFS = frostfs
		cfg.RemovingCheckAfterDurations = 0
		cfg.Key = &keys.PrivateKey{}
		creds := New(cfg)

		_, _, err = creds.GetBox(ctx, addr)
		require.Error(t, err)
	})

	t.Run("invalid payload", func(t *testing.T) {
		frostfs := newFrostfsMock()

		var obj object.Object
		obj.SetPayload([]byte("invalid"))
		addr := oidtest.Address()
		frostfs.objects[addr] = []*object.Object{&obj}

		cfg.FrostFS = frostfs
		cfg.RemovingCheckAfterDurations = 0
		cfg.Key = key
		creds := New(cfg)

		_, _, err = creds.GetBox(ctx, addr)
		require.Error(t, err)
	})

	t.Run("check attributes update", func(t *testing.T) {
		frostfs := newFrostfsMock()
		cfg.FrostFS = frostfs
		cfg.RemovingCheckAfterDurations = 0
		cfg.Key = key
		creds := New(cfg)

		cnrID := cidtest.ID()
		addr, err := creds.Put(ctx, cnrID, CredentialsParam{Keys: keys.PublicKeys{key.PublicKey()}, AccessBox: accessBox})
		require.NoError(t, err)

		_, boxAttrs, err := creds.GetBox(ctx, addr)
		require.NoError(t, err)

		_, err = creds.Update(ctx, addr, CredentialsParam{Keys: keys.PublicKeys{key.PublicKey()}, AccessBox: accessBox, CustomAttributes: attrs})
		require.NoError(t, err)

		_, newBoxAttrs, err := creds.GetBox(ctx, addr)
		require.NoError(t, err)
		require.Equal(t, len(boxAttrs)+1, len(newBoxAttrs))
	})

	t.Run("check accessbox update", func(t *testing.T) {
		frostfs := newFrostfsMock()
		cfg.FrostFS = frostfs
		cfg.RemovingCheckAfterDurations = 0
		cfg.Key = key
		creds := New(cfg)

		cnrID := cidtest.ID()
		addr, err := creds.Put(ctx, cnrID, CredentialsParam{Keys: keys.PublicKeys{key.PublicKey()}, AccessBox: accessBox})
		require.NoError(t, err)

		box, _, err := creds.GetBox(ctx, addr)
		require.NoError(t, err)
		require.Equal(t, hex.EncodeToString(secret), box.Gate.SecretKey)

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

		newGateData := []*accessbox.GateData{{
			BearerToken: &bearer.Token{},
			GateKey:     newKey.PublicKey(),
		}}

		newSecret := []byte("new-secret")
		newAccessBox, _, err := accessbox.PackTokens(newGateData, newSecret)
		require.NoError(t, err)

		_, err = creds.Update(ctx, addr, CredentialsParam{Keys: keys.PublicKeys{newKey.PublicKey()}, AccessBox: newAccessBox})
		require.NoError(t, err)

		_, _, err = creds.GetBox(ctx, addr)
		require.Error(t, err)

		cfg.Key = newKey
		newCreds := New(cfg)

		box, _, err = newCreds.GetBox(ctx, addr)
		require.NoError(t, err)
		require.Equal(t, hex.EncodeToString(newSecret), box.Gate.SecretKey)
	})

	t.Run("empty keys", func(t *testing.T) {
		frostfs := newFrostfsMock()
		cfg.FrostFS = frostfs
		cfg.RemovingCheckAfterDurations = 0
		cfg.Key = key
		creds := New(cfg)

		cnrID := cidtest.ID()
		_, err = creds.Put(ctx, cnrID, CredentialsParam{AccessBox: accessBox})
		require.ErrorIs(t, err, ErrEmptyPublicKeys)
	})

	t.Run("empty accessbox", func(t *testing.T) {
		frostfs := newFrostfsMock()
		cfg.FrostFS = frostfs
		cfg.RemovingCheckAfterDurations = 0
		cfg.Key = key
		creds := New(cfg)

		cnrID := cidtest.ID()
		_, err = creds.Put(ctx, cnrID, CredentialsParam{Keys: keys.PublicKeys{key.PublicKey()}})
		require.ErrorIs(t, err, ErrEmptyBearerToken)
	})
}