package remote

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"strconv"
	"testing"

	"github.com/golang/mock/gomock"
	"github.com/prysmaticlabs/prysm/async/event"
	fieldparams "github.com/prysmaticlabs/prysm/config/fieldparams"
	"github.com/prysmaticlabs/prysm/config/params"
	"github.com/prysmaticlabs/prysm/crypto/bls"
	"github.com/prysmaticlabs/prysm/encoding/bytesutil"
	validatorpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1/validator-client"
	"github.com/prysmaticlabs/prysm/testing/assert"
	"github.com/prysmaticlabs/prysm/testing/mock"
	"github.com/prysmaticlabs/prysm/testing/require"
	"github.com/prysmaticlabs/prysm/validator/keymanager"
	logTest "github.com/sirupsen/logrus/hooks/test"
)

var validClientCert = `-----BEGIN CERTIFICATE-----
MIIEITCCAgmgAwIBAgIQXUJWQZgVO4IX+zlWGI1/mTANBgkqhkiG9w0BAQsFADAU
MRIwEAYDVQQDEwlBdHRlc3RhbnQwHhcNMjAwMzE3MDgwNjU3WhcNMjEwOTE3MDc1
OTUyWjASMRAwDgYDVQQDEwdjbGllbnQxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEAsc977g16Tan2j7YuA+zQOlDntb4Bkfs4sDOznOEvnozHwRZOgfcP
jVcA9AS5eZOGIRrsTssptrgVNDPoIHWoKk7LAKyyLM3dGp5PWeyMBoQA5cq+yPAT
4JkJpDnBFfwxXB99osJH0z3jSTRa62CSVvPRBisK4B9AlLQfcleEQlKJugy9tOAj
G7zodwEi+J4AYQHmOiwL38ZsKq9We5y4HMQ0E7de0FoU5QHrtuPNrTuwVwrq825l
cEAAFey6Btngx+sziysPHWHYOq4xOZ1UPBApeaAFLguzusc/4VwM7kzRNr4VOD8a
eC3CtKLhBBVVxHI5ZlaHS+YylNGYD4+FxQIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC
A7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBQDGCE0
3k4rHzB+Ycf3pt1MzeDPgzAfBgNVHSMEGDAWgBScIYZa4dQBIW/gVwR0ctGCuHhe
9jANBgkqhkiG9w0BAQsFAAOCAgEAHG/EfvqIwbhYfci+zRCYC7aQPuvhivJblBwN
mbXo2qsxvje1hcKm0ptJLOy/cjJzeLJYREhQlXDPRJC/xgELnbXRjgag82r35+pf
wVJwP6Yw53VCM3o0QKsUrKyMm4sAijOBrJyqpB5untAieZsry5Bfj0S4YobbtdJa
VsEioU07fVVczf5lYN0XrLgRnXq3LMkTiZ6drFiqLkwmXQZVxNujmcaFSm7yCALl
EdhYNmaqedS5me5UOGxwPacrsZwWF9dvMsl3OswgTcaGdsUtx2/q+S2vbZUAM/Gw
qaTanDfvVtVTF7KzVN9hiqKe4mO0HHHK2HWJYBLdRJjInOgRW+53hCmUhLxD+Dq+
31jLKxn/Y4hyH9E+55b1sJHCFpsbEtVD53fojiH2C/uLbhq4Wr1PXgOoxzf2KeSQ
B3ENu8C4b6AlNhqOnz5zeDcx8Ug0vMfVDAwf6RAYMG5b/MoWNKcLNXhk8H1nbVkt
16ppjh6I27JqfNqfP2J/p3BF++ZugZuWfN9DRaJ6UPz+yyF7eW8fyDAQNl7LS0Kh
8PlF5cYvyIIKVHe38Mn8ZAWboKUs0xNv2vhA9V/4Q1ZzAEkXjmbk8H26sjGvJnvg
Lgm/+6LVWR4EnUlU8aEWASEpTWq2lSRF3ZOvNstHnufyiDfcwDcl/IKKQiVQQ3mX
tw8Jf74=
-----END CERTIFICATE-----`

// skipcq: SCT-1000
var validClientKey = `-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAsc977g16Tan2j7YuA+zQOlDntb4Bkfs4sDOznOEvnozHwRZO
gfcPjVcA9AS5eZOGIRrsTssptrgVNDPoIHWoKk7LAKyyLM3dGp5PWeyMBoQA5cq+
yPAT4JkJpDnBFfwxXB99osJH0z3jSTRa62CSVvPRBisK4B9AlLQfcleEQlKJugy9
tOAjG7zodwEi+J4AYQHmOiwL38ZsKq9We5y4HMQ0E7de0FoU5QHrtuPNrTuwVwrq
825lcEAAFey6Btngx+sziysPHWHYOq4xOZ1UPBApeaAFLguzusc/4VwM7kzRNr4V
OD8aeC3CtKLhBBVVxHI5ZlaHS+YylNGYD4+FxQIDAQABAoIBAQCjV2MVcDQmHDhw
FH95A5bVu3TgM8flfs64rwYU25iPIexuqDs+kOMsh/xMLfrkgGz7BGyIhYGwZLK1
3ekjyHHPS8qYuAyFtCelSEDE7tRDOAhLEFDq7gCUloGQ561EsQP3CMa1OZwZpgSh
PwM2ruRAFIK0E95NvOfqsv0gYN0Svo7hYjNsvW6ok/ZGMyN2ikcRR04wGOFOGjfT
xTmfURc9ejnOjHAOqLTpToPwM1/gWWR2iMQefC4njy4MO2BXqOPUmHxmmR4PYhu2
8EcKbyRs+/fvL3GgD3VAlOe5vnkfBzssQhHmexgSk5lHZrcSxUGXYGrYKPAeV2mk
5HRBWp0RAoGBAOUn5w+NCAugcTGP0hfNlyGXsXqUZvnMyFWvUcxgzgPlJyEyDnKn
aIb1DFOF2HckCfLZdrHqqgaF6K3TDvW9BgSKIsvISpo1S95ZPD6DKUo6YQ10CQRW
q/ZZVbxtFksVgFRGYpCVmPNULmx7CiXDT1b/suwNMAwCZwiNPTSvKQVLAoGBAMaj
zDo1/eepRslqnz5s8hh7dGEjfG/ZJcLgAJAxCyAgnIP4Tls7QkNhCVp9LcN6i1bc
CnT6AIuZRXSJWEdp4k2QnVFUmh9Q5MGgwrKYSY5M/1puTISlF1yQ8J6FX8BlDVmy
4dyaSyC0RIvgBzF9/KBDxxmJcHgGQ0awLeeyl4cvAoGBAN83FS3itLmOmXQrofyp
uNNyDeFXeU9OmL5OPqGUkljc+Favib9JLtp3DIC3WfoD0uUJy0LXULN18QaRFnts
mtYFMIvMGE9KJxL5XWOPI8M4Rp1yL+5X9r3Km2cl45dT5GMzBIPOFOTBVU86MtJC
A6C9Bi5FUk4AcRi1a69MB+stAoGAWNiwoyS9IV38dGCFQ4W1LzAg2MXnhZuJoUVR
2yykfkU33Gs2mOXDeKGxblDpJDLumfYnkzSzA72VbE92NdLtTqYtR1Bg8zraZqTC
EOG+nLBh0o/dF8ND1LpbdXvQXRyVwRYaofI9Qi5/LlUQwplIYmKObiSkMnsSok5w
6d5emi8CgYBjtUihOFaAmgqkTHOn4j4eKS1O7/H8QQSVe5M0bocmAIbgJ4At3GnI
E1JcIY2SZtSwAWs6aQPGE42gwsNCCsQWdJNtViO23JbCwlcPToC4aDfc0JJNaYqp
oVV7C5jmJh9VRd2tXIXIZMMNOfThfNf2qDQuJ1S2t5KugozFiRsHUg==
-----END RSA PRIVATE KEY-----`

func TestNewRemoteKeymanager(t *testing.T) {
	tests := []struct {
		name       string
		opts       *KeymanagerOpts
		clientCert string
		clientKey  string
		caCert     string
		err        string
	}{
		{
			name: "NoCertificates",
			opts: &KeymanagerOpts{
				RemoteCertificate: nil,
			},
			err: "certificate configuration is missing",
		},
		{
			name: "NoClientCertificate",
			opts: &KeymanagerOpts{
				RemoteCertificate: &CertificateConfig{
					RequireTls: true,
				},
			},
			err: "client certificate is required",
		},
		{
			name: "NoClientKey",
			opts: &KeymanagerOpts{
				RemoteCertificate: &CertificateConfig{
					RequireTls:     true,
					ClientCertPath: "/foo/client.crt",
					ClientKeyPath:  "",
				},
			},
			err: "client key is required",
		},
		{
			name: "MissingClientKey",
			opts: &KeymanagerOpts{
				RemoteCertificate: &CertificateConfig{
					RequireTls:     true,
					ClientCertPath: "/foo/client.crt",
					ClientKeyPath:  "/foo/client.key",
					CACertPath:     "",
				},
			},
			err: "failed to obtain client's certificate and/or key",
		},
		{
			name:       "BadClientCert",
			clientCert: `bad`,
			clientKey:  validClientKey,
			opts: &KeymanagerOpts{
				RemoteCertificate: &CertificateConfig{
					RequireTls: true,
				},
			},
			err: "failed to obtain client's certificate and/or key: tls: failed to find any PEM data in certificate input",
		},
		{
			name:       "BadClientKey",
			clientCert: validClientCert,
			clientKey:  `bad`,
			opts: &KeymanagerOpts{
				RemoteCertificate: &CertificateConfig{
					RequireTls: true,
				},
			},
			err: "failed to obtain client's certificate and/or key: tls: failed to find any PEM data in key input",
		},
		{
			name:       "MissingCACert",
			clientCert: validClientCert,
			clientKey:  validClientKey,
			opts: &KeymanagerOpts{
				RemoteCertificate: &CertificateConfig{
					RequireTls: true,
					CACertPath: `bad`,
				},
			},
			err: "failed to obtain server's CA certificate: open bad: no such file or directory",
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			if test.caCert != "" || test.clientCert != "" || test.clientKey != "" {
				dir := fmt.Sprintf("%s/%s", t.TempDir(), test.name)
				require.NoError(t, os.MkdirAll(dir, 0777))
				if test.caCert != "" {
					caCertPath := fmt.Sprintf("%s/ca.crt", dir)
					err := ioutil.WriteFile(caCertPath, []byte(test.caCert), params.BeaconIoConfig().ReadWritePermissions)
					require.NoError(t, err, "Failed to write CA certificate")
					test.opts.RemoteCertificate.CACertPath = caCertPath
				}
				if test.clientCert != "" {
					clientCertPath := fmt.Sprintf("%s/client.crt", dir)
					err := ioutil.WriteFile(clientCertPath, []byte(test.clientCert), params.BeaconIoConfig().ReadWritePermissions)
					require.NoError(t, err, "Failed to write client certificate")
					test.opts.RemoteCertificate.ClientCertPath = clientCertPath
				}
				if test.clientKey != "" {
					clientKeyPath := fmt.Sprintf("%s/client.key", dir)
					err := ioutil.WriteFile(clientKeyPath, []byte(test.clientKey), params.BeaconIoConfig().ReadWritePermissions)
					require.NoError(t, err, "Failed to write client key")
					test.opts.RemoteCertificate.ClientKeyPath = clientKeyPath
				}
			}
			_, err := NewKeymanager(context.Background(), &SetupConfig{Opts: test.opts, MaxMessageSize: 1})
			if test.err == "" {
				require.NoError(t, err)
			} else {
				require.ErrorContains(t, test.err, err)
			}
		})
	}
}

func TestNewRemoteKeymanager_TlsDisabled(t *testing.T) {
	opts := &KeymanagerOpts{
		RemoteCertificate: &CertificateConfig{
			RequireTls: false,
		},
	}
	_, err := NewKeymanager(context.Background(), &SetupConfig{Opts: opts, MaxMessageSize: 1})
	assert.NoError(t, err)
}

func TestRemoteKeymanager_Sign(t *testing.T) {
	ctrl := gomock.NewController(t)
	m := mock.NewMockRemoteSignerClient(ctrl)
	k := &Keymanager{
		client: m,
	}

	// Expect error handling to work.
	m.EXPECT().Sign(
		gomock.Any(), // ctx
		gomock.Any(), // epoch
	).Return(nil, errors.New("could not sign"))
	_, err := k.Sign(context.Background(), nil)
	require.ErrorContains(t, "could not sign", err)

	// Expected proper error handling for signing response statuses.
	m.EXPECT().Sign(
		gomock.Any(), // ctx
		gomock.Any(), // epoch
	).Return(&validatorpb.SignResponse{
		Status: validatorpb.SignResponse_FAILED,
	}, nil /*err*/)
	_, err = k.Sign(context.Background(), nil)
	if err == nil {
		t.Fatal(err)
	}
	if err != ErrSigningFailed {
		t.Errorf("Expected %v, received %v", ErrSigningFailed, err)
	}
	m.EXPECT().Sign(
		gomock.Any(), // ctx
		gomock.Any(), // epoch
	).Return(&validatorpb.SignResponse{
		Status: validatorpb.SignResponse_DENIED,
	}, nil /*err*/)
	_, err = k.Sign(context.Background(), nil)
	if err == nil {
		t.Fatal(err)
	}
	if err != ErrSigningDenied {
		t.Errorf("Expected %v, received %v", ErrSigningDenied, err)
	}

	// Expected signing success.
	randKey, err := bls.RandKey()
	require.NoError(t, err)
	data := []byte("hello-world")
	sig := randKey.Sign(data)
	m.EXPECT().Sign(
		gomock.Any(), // ctx
		gomock.Any(), // epoch
	).Return(&validatorpb.SignResponse{
		Status:    validatorpb.SignResponse_SUCCEEDED,
		Signature: sig.Marshal(),
	}, nil /*err*/)
	resp, err := k.Sign(context.Background(), nil)
	require.NoError(t, err)
	assert.DeepEqual(t, sig.Marshal(), resp.Marshal())
}

func TestRemoteKeymanager_FetchValidatingPublicKeys(t *testing.T) {
	ctrl := gomock.NewController(t)
	m := mock.NewMockRemoteSignerClient(ctrl)
	k := &Keymanager{
		client:              m,
		accountsChangedFeed: new(event.Feed),
	}

	// Expect error handling to work.
	m.EXPECT().ListValidatingPublicKeys(
		gomock.Any(), // ctx
		gomock.Any(), // epoch
	).Return(nil, errors.New("could not fetch keys"))
	_, err := k.FetchValidatingPublicKeys(context.Background())
	require.ErrorContains(t, "could not fetch keys", err)

	// Expect an empty response to return empty keys.
	m.EXPECT().ListValidatingPublicKeys(
		gomock.Any(), // ctx
		gomock.Any(), // epoch
	).Return(&validatorpb.ListPublicKeysResponse{
		ValidatingPublicKeys: make([][]byte, 0),
	}, nil /*err*/)
	keys, err := k.FetchValidatingPublicKeys(context.Background())
	require.NoError(t, err)
	assert.Equal(t, 0, len(keys), "Expected empty response")

	numKeys := 10
	pubKeys := make([][]byte, numKeys)
	for i := 0; i < numKeys; i++ {
		key := make([]byte, 48)
		copy(key, strconv.Itoa(i))
		pubKeys[i] = key
	}
	m.EXPECT().ListValidatingPublicKeys(
		gomock.Any(), // ctx
		gomock.Any(), // epoch
	).Return(&validatorpb.ListPublicKeysResponse{
		ValidatingPublicKeys: pubKeys,
	}, nil /*err*/)
	keys, err = k.FetchValidatingPublicKeys(context.Background())
	require.NoError(t, err)
	rawKeys := make([][]byte, len(keys))
	for i := 0; i < len(rawKeys); i++ {
		rawKeys[i] = keys[i][:]
	}
	assert.DeepEqual(t, pubKeys, rawKeys)
}

func TestUnmarshalOptionsFile_DefaultRequireTls(t *testing.T) {
	optsWithoutTls := struct {
		RemoteCertificate struct {
			ClientCertPath string
			ClientKeyPath  string
			CACertPath     string
		}
	}{}
	var buffer bytes.Buffer
	b, err := json.Marshal(optsWithoutTls)
	require.NoError(t, err)
	_, err = buffer.Write(b)
	require.NoError(t, err)
	r := ioutil.NopCloser(&buffer)

	opts, err := UnmarshalOptionsFile(r)
	assert.NoError(t, err)
	assert.Equal(t, true, opts.RemoteCertificate.RequireTls)
}

func TestReloadPublicKeys(t *testing.T) {
	hook := logTest.NewGlobal()
	ctx := context.Background()
	ctrl := gomock.NewController(t)
	m := mock.NewMockRemoteSignerClient(ctrl)

	k := &Keymanager{
		client:              m,
		accountsChangedFeed: new(event.Feed),
		orderedPubKeys:      [][fieldparams.BLSPubkeyLength]byte{bytesutil.ToBytes48([]byte("100"))},
	}

	// Add key
	m.EXPECT().ListValidatingPublicKeys(
		gomock.Any(), // ctx
		gomock.Any(), // epoch
	).Return(&validatorpb.ListPublicKeysResponse{
		// Return keys in reverse order to verify ordering
		ValidatingPublicKeys: [][]byte{[]byte("200"), []byte("100")},
	}, nil /* err */)

	keys, err := k.ReloadPublicKeys(ctx)
	require.NoError(t, err)
	assert.DeepEqual(t, [][fieldparams.BLSPubkeyLength]byte{bytesutil.ToBytes48([]byte("100")), bytesutil.ToBytes48([]byte("200"))}, k.orderedPubKeys)
	assert.DeepEqual(t, keys, k.orderedPubKeys)
	assert.LogsContain(t, hook, keymanager.KeysReloaded)

	hook.Reset()

	// Remove key
	m.EXPECT().ListValidatingPublicKeys(
		gomock.Any(), // ctx
		gomock.Any(), // epoch
	).Return(&validatorpb.ListPublicKeysResponse{
		ValidatingPublicKeys: [][]byte{[]byte("200")},
	}, nil /* err */)

	keys, err = k.ReloadPublicKeys(ctx)
	require.NoError(t, err)
	assert.DeepEqual(t, [][fieldparams.BLSPubkeyLength]byte{bytesutil.ToBytes48([]byte("200"))}, k.orderedPubKeys)
	assert.DeepEqual(t, keys, k.orderedPubKeys)
	assert.LogsContain(t, hook, keymanager.KeysReloaded)

	hook.Reset()

	// Change key
	m.EXPECT().ListValidatingPublicKeys(
		gomock.Any(), // ctx
		gomock.Any(), // epoch
	).Return(&validatorpb.ListPublicKeysResponse{
		ValidatingPublicKeys: [][]byte{[]byte("300")},
	}, nil /* err */)

	keys, err = k.ReloadPublicKeys(ctx)
	require.NoError(t, err)
	assert.DeepEqual(t, [][fieldparams.BLSPubkeyLength]byte{bytesutil.ToBytes48([]byte("300"))}, k.orderedPubKeys)
	assert.DeepEqual(t, keys, k.orderedPubKeys)
	assert.LogsContain(t, hook, keymanager.KeysReloaded)

	hook.Reset()

	// No change
	m.EXPECT().ListValidatingPublicKeys(
		gomock.Any(), // ctx
		gomock.Any(), // epoch
	).Return(&validatorpb.ListPublicKeysResponse{
		ValidatingPublicKeys: [][]byte{[]byte("300")},
	}, nil /* err */)

	keys, err = k.ReloadPublicKeys(ctx)
	require.NoError(t, err)
	assert.DeepEqual(t, [][fieldparams.BLSPubkeyLength]byte{bytesutil.ToBytes48([]byte("300"))}, k.orderedPubKeys)
	assert.DeepEqual(t, keys, k.orderedPubKeys)
	assert.LogsDoNotContain(t, hook, keymanager.KeysReloaded)
}
