package container

import (
	"crypto/ecdsa"
	"encoding/hex"
	"fmt"
	"testing"
	"time"

	frostfsidclient "git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid/client"
	containercore "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container"
	cntClient "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/container"
	containerEvent "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/event/container"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/logger/test"
	apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
	containerSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl"
	cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
	frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
	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/netmap"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/version"
	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"github.com/nspcc-dev/neo-go/pkg/network/payload"
	"github.com/nspcc-dev/neo-go/pkg/util"
	"github.com/stretchr/testify/require"
)

func TestPutEvent(t *testing.T) {
	t.Parallel()
	nst := &testNetworkState{
		homHashDisabled: true,
		epoch:           100,
	}
	mc := &testMorphClient{}

	proc, err := New(&Params{
		Log:             test.NewLogger(t, true),
		PoolSize:        2,
		AlphabetState:   &testAlphabetState{isAlphabet: true},
		NetworkState:    nst,
		ContainerClient: &testContainerClient{},
		MorphClient:     mc,
		FrostFSIDClient: &testFrostFSIDClient{},
	})
	require.NoError(t, err, "failed to create processor")

	p, err := keys.NewPrivateKey()
	require.NoError(t, err)
	var usr user.ID
	user.IDFromKey(&usr, (ecdsa.PublicKey)(*p.PublicKey()))

	var pp netmap.PlacementPolicy
	pp.AddReplicas(netmap.ReplicaDescriptor{})

	var cnr containerSDK.Container
	cnr.Init()
	cnr.SetOwner(usr)
	cnr.SetPlacementPolicy(pp)
	cnr.SetBasicACL(acl.Private)
	containerSDK.DisableHomomorphicHashing(&cnr)

	nr := &payload.P2PNotaryRequest{
		MainTransaction: &transaction.Transaction{},
	}

	event := &testPutEvent{
		cnr: &cnr,
		pk:  p,
		st:  nil,
		nr:  nr,
	}

	proc.handlePut(event)

	for proc.pool.Running() > 0 {
		time.Sleep(10 * time.Millisecond)
	}

	require.EqualValues(t, []*transaction.Transaction{nr.MainTransaction}, mc.transactions, "invalid notary requests")
}

func TestDeleteEvent(t *testing.T) {
	t.Parallel()
	nst := &testNetworkState{
		homHashDisabled: true,
		epoch:           100,
	}
	cc := &testContainerClient{
		get: make(map[string]*containercore.Container),
	}

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

	mc := &testMorphClient{}

	proc, err := New(&Params{
		Log:             test.NewLogger(t, true),
		PoolSize:        2,
		AlphabetState:   &testAlphabetState{isAlphabet: true},
		NetworkState:    nst,
		ContainerClient: cc,
		MorphClient:     mc,
		FrostFSIDClient: &testFrostFSIDClient{},
	})
	require.NoError(t, err, "failed to create processor")

	var usr user.ID
	user.IDFromKey(&usr, (ecdsa.PublicKey)(*p.PublicKey()))

	var pp netmap.PlacementPolicy
	pp.AddReplicas(netmap.ReplicaDescriptor{})

	var cnr containerSDK.Container
	cnr.Init()
	cnr.SetOwner(usr)
	cnr.SetPlacementPolicy(pp)
	cnr.SetBasicACL(acl.Private)
	containerSDK.DisableHomomorphicHashing(&cnr)

	var cid cid.ID
	containerSDK.CalculateID(&cid, cnr)
	cidBin := make([]byte, 32)
	cid.Encode(cidBin)

	nr := &payload.P2PNotaryRequest{
		MainTransaction: &transaction.Transaction{},
	}

	ev := containerEvent.Delete{
		ContainerIDValue:   cidBin,
		SignatureValue:     p.Sign(cidBin),
		NotaryRequestValue: nr,
		PublicKeyValue:     p.PublicKey().Bytes(),
	}

	var signature frostfscrypto.Signature
	signer := frostfsecdsa.Signer(p.PrivateKey)
	require.NoError(t, signature.Calculate(signer, ev.ContainerID()), "failed to calculate signature")
	cc.get[hex.EncodeToString(ev.ContainerID())] = &containercore.Container{
		Value:     cnr,
		Signature: signature,
	}

	proc.handleDelete(ev)

	for proc.pool.Running() > 0 {
		time.Sleep(10 * time.Millisecond)
	}

	var expectedDelete cntClient.DeletePrm
	expectedDelete.SetCID(ev.ContainerID())
	expectedDelete.SetSignature(ev.Signature())

	require.EqualValues(t, []*transaction.Transaction{nr.MainTransaction}, mc.transactions, "invalid notary requests")
}

func TestSetEACLEvent(t *testing.T) {
	t.Parallel()
	nst := &testNetworkState{
		homHashDisabled: true,
		epoch:           100,
	}
	cc := &testContainerClient{
		get: make(map[string]*containercore.Container),
	}
	mc := &testMorphClient{}

	proc, err := New(&Params{
		Log:             test.NewLogger(t, true),
		PoolSize:        2,
		AlphabetState:   &testAlphabetState{isAlphabet: true},
		NetworkState:    nst,
		ContainerClient: cc,
		MorphClient:     mc,
		FrostFSIDClient: &testFrostFSIDClient{},
	})
	require.NoError(t, err, "failed to create processor")

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

	var usr user.ID
	user.IDFromKey(&usr, (ecdsa.PublicKey)(*p.PublicKey()))

	var pp netmap.PlacementPolicy
	pp.AddReplicas(netmap.ReplicaDescriptor{})

	var cnr containerSDK.Container
	cnr.Init()
	cnr.SetOwner(usr)
	cnr.SetPlacementPolicy(pp)
	cnr.SetBasicACL(acl.PrivateExtended)
	containerSDK.DisableHomomorphicHashing(&cnr)

	var cid cid.ID
	containerSDK.CalculateID(&cid, cnr)
	cidBytes := make([]byte, 32)
	cid.Encode(cidBytes)

	var signature frostfscrypto.Signature
	signer := frostfsecdsa.Signer(p.PrivateKey)
	require.NoError(t, signature.Calculate(signer, cidBytes), "failed to calculate signature")

	cc.get[hex.EncodeToString(cidBytes)] = &containercore.Container{
		Value:     cnr,
		Signature: signature,
	}

	table := eacl.NewTable()
	table.SetCID(cid)
	table.SetVersion(version.Current())

	r := &eacl.Record{}
	r.AddObjectContainerIDFilter(eacl.MatchStringEqual, cid)

	table.AddRecord(r)

	nr := &payload.P2PNotaryRequest{
		MainTransaction: &transaction.Transaction{},
	}
	event := containerEvent.SetEACL{
		TableValue:         table.ToV2().StableMarshal(nil),
		PublicKeyValue:     p.PublicKey().Bytes(),
		SignatureValue:     p.Sign(table.ToV2().StableMarshal(nil)),
		NotaryRequestValue: nr,
	}

	proc.handleSetEACL(event)

	for proc.pool.Running() > 0 {
		time.Sleep(10 * time.Millisecond)
	}

	var expectedPutEACL cntClient.PutEACLPrm
	expectedPutEACL.SetTable(table.ToV2().StableMarshal(nil))
	expectedPutEACL.SetKey(p.PublicKey().Bytes())
	expectedPutEACL.SetSignature(p.Sign(table.ToV2().StableMarshal(nil)))

	require.EqualValues(t, []*transaction.Transaction{nr.MainTransaction}, mc.transactions, "invalid notary requests")
}

type testAlphabetState struct {
	isAlphabet bool
}

func (s *testAlphabetState) IsAlphabet() bool {
	return s.isAlphabet
}

type testNetworkState struct {
	homHashDisabled bool
	epoch           uint64
}

func (s *testNetworkState) HomomorphicHashDisabled() (bool, error) {
	return s.homHashDisabled, nil
}

func (s *testNetworkState) Epoch() (uint64, error) {
	return s.epoch, nil
}

type testContainerClient struct {
	contractAddress util.Uint160
	get             map[string]*containercore.Container
}

func (c *testContainerClient) ContractAddress() util.Uint160 {
	return c.contractAddress
}

func (c *testContainerClient) Get(cid []byte) (*containercore.Container, error) {
	key := hex.EncodeToString(cid)
	if cont, found := c.get[key]; found {
		return cont, nil
	}
	return nil, new(apistatus.ContainerNotFound)
}

var _ putEvent = &testPutEvent{}

type testPutEvent struct {
	cnr *containerSDK.Container
	pk  *keys.PrivateKey
	st  []byte
	nr  *payload.P2PNotaryRequest
}

func (e *testPutEvent) MorphEvent() {}

func (e *testPutEvent) Container() []byte {
	return e.cnr.Marshal()
}

func (e *testPutEvent) PublicKey() []byte {
	return e.pk.PublicKey().Bytes()
}

func (e *testPutEvent) Signature() []byte {
	return e.pk.Sign(e.cnr.Marshal())
}

func (e *testPutEvent) SessionToken() []byte {
	return e.st
}

func (e *testPutEvent) NotaryRequest() *payload.P2PNotaryRequest {
	return e.nr
}

type testMorphClient struct {
	transactions []*transaction.Transaction
}

func (c *testMorphClient) NotarySignAndInvokeTX(mainTx *transaction.Transaction) error {
	c.transactions = append(c.transactions, mainTx)
	return nil
}

type testFrostFSIDClient struct{}

func (c *testFrostFSIDClient) GetSubject(addr util.Uint160) (*frostfsidclient.Subject, error) {
	return nil, fmt.Errorf("subject not found")
}