/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package etcdraft

import (
	"crypto/rand"
	"crypto/x509"
	"encoding/base64"
	"encoding/pem"
	"fmt"
	"os"
	"path/filepath"
	"testing"
	"time"

	"github.com/hyperledger/fabric-lib-go/bccsp/sw"
	"github.com/hyperledger/fabric-lib-go/common/flogging"
	"github.com/hyperledger/fabric-protos-go-apiv2/common"
	"github.com/hyperledger/fabric-protos-go-apiv2/orderer"
	etcdraftproto "github.com/hyperledger/fabric-protos-go-apiv2/orderer/etcdraft"
	"github.com/hyperledger/fabric/common/crypto/tlsgen"
	"github.com/hyperledger/fabric/orderer/common/cluster"
	"github.com/hyperledger/fabric/protoutil"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"google.golang.org/protobuf/proto"
)

func TestIsConsenterOfChannel(t *testing.T) {
	certInsideConfigBlock, err := base64.StdEncoding.DecodeString("LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNmekNDQWlhZ0F3SUJBZ0l" +
		"SQUo4bjFLYTVzS1ZaTXRMTHJ1dldERDB3Q2dZSUtvWkl6ajBFQXdJd2JERUwKTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnVENrTmhiR" +
		"2xtYjNKdWFXRXhGakFVQmdOVkJBY1REVk5oYmlCRwpjbUZ1WTJselkyOHhGREFTQmdOVkJBb1RDMlY0WVcxd2JHVXVZMjl0TVJvd0dBWUR" +
		"WUVFERXhGMGJITmpZUzVsCmVHRnRjR3hsTG1OdmJUQWVGdzB4T0RFeE1EWXdPVFE1TURCYUZ3MHlPREV4TURNd09UUTVNREJhTUZreEN6QU" +
		"oKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJRXdwRFlXeHBabTl5Ym1saE1SWXdGQVlEVlFRSEV3MVRZVzRnUm5KaApibU5wYzJOdk1SMHdH" +
		"d1lEVlFRREV4UnZjbVJsY21WeU1TNWxlR0Z0Y0d4bExtTnZiVEJaTUJNR0J5cUdTTTQ5CkFnRUdDQ3FHU000OUF3RUhBMElBQkRUVlFZc0" +
		"ZKZWxUcFZDMDFsek5DSkx6OENRMFFGVDBvN1BmSnBwSkl2SXgKUCtRVjQvRGRCSnRqQ0cvcGsvMGFxZXRpSjhZRUFMYmMrOUhmWnExN2tJ" +
		"Q2pnYnN3Z2Jnd0RnWURWUjBQQVFILwpCQVFEQWdXZ01CMEdBMVVkSlFRV01CUUdDQ3NHQVFVRkJ3TUJCZ2dyQmdFRkJRY0RBakFNQmdOV" +
		"khSTUJBZjhFCkFqQUFNQ3NHQTFVZEl3UWtNQ0tBSUVBOHFrSVJRTVBuWkxBR2g0TXZla2gzZFpHTmNxcEhZZWlXdzE3Rmw0ZlMKTUV3R0" +
		"ExVWRFUVJGTUVPQ0ZHOXlaR1Z5WlhJeExtVjRZVzF3YkdVdVkyOXRnZ2h2Y21SbGNtVnlNWUlKYkc5agpZV3hvYjNOMGh3Ui9BQUFCaHh" +
		"BQUFBQUFBQUFBQUFBQUFBQUFBQUFCTUFvR0NDcUdTTTQ5QkFNQ0EwY0FNRVFDCklFckJZRFVzV0JwOHB0ZVFSaTZyNjNVelhJQi81Sn" +
		"YxK0RlTkRIUHc3aDljQWlCakYrM3V5TzBvMEdRclB4MEUKUWptYlI5T3BVREN2LzlEUkNXWU9GZitkVlE9PQotLS0tLUVORCBDRVJUSU" +
		"ZJQ0FURS0tLS0tCg==")
	require.NoError(t, err)

	ca, err := tlsgen.NewCA()
	require.NoError(t, err)

	kp, err := ca.NewClientCertKeyPair()
	require.NoError(t, err)

	validBlock := func() *common.Block {
		b, err := os.ReadFile(filepath.Join("testdata", "etcdraftgenesis.block"))
		require.NoError(t, err)
		block := &common.Block{}
		err = proto.Unmarshal(b, block)
		require.NoError(t, err)
		return block
	}
	for _, testCase := range []struct {
		name          string
		expectedError string
		configBlock   *common.Block
		certificate   []byte
	}{
		{
			name:          "nil block",
			expectedError: "nil block or nil header",
		},
		{
			name:          "nil header",
			expectedError: "nil block or nil header",
			configBlock:   &common.Block{},
		},
		{
			name:          "no block data",
			expectedError: "block data is nil",
			configBlock:   &common.Block{Header: &common.BlockHeader{}},
		},
		{
			name:          "invalid envelope inside block",
			expectedError: "failed to unmarshal payload from envelope:",
			configBlock: &common.Block{
				Header: &common.BlockHeader{},
				Data: &common.BlockData{
					Data: [][]byte{protoutil.MarshalOrPanic(&common.Envelope{
						Payload: []byte{1, 2, 3},
					})},
				},
			},
		},
		{
			name:          "valid config block with cert mismatch",
			configBlock:   validBlock(),
			certificate:   kp.Cert,
			expectedError: cluster.ErrNotInChannel.Error(),
		},
		{
			name:        "valid config block with matching cert",
			configBlock: validBlock(),
			certificate: certInsideConfigBlock,
		},
	} {
		t.Run(testCase.name, func(t *testing.T) {
			cryptoProvider, err := sw.NewDefaultSecurityLevelWithKeystore(sw.NewDummyKeyStore())
			require.NoError(t, err)

			consenterCertificate := &ConsenterCertificate{
				Logger:               flogging.MustGetLogger("test"),
				ConsenterCertificate: testCase.certificate,
				CryptoProvider:       cryptoProvider,
			}
			err = consenterCertificate.IsConsenterOfChannel(testCase.configBlock)
			if testCase.expectedError != "" {
				require.ErrorContains(t, err, testCase.expectedError)
			} else {
				require.NoError(t, err)
			}
		})
	}
}

func TestVerifyConfigMetadata(t *testing.T) {
	tlsCA, err := tlsgen.NewCA()
	require.NoError(t, err, "failed to create CA")

	caRootCert, err := parseCertificateFromBytes(tlsCA.CertBytes())
	require.NoError(t, err, "failed to parse CA certificate")

	serverPair, err := tlsCA.NewServerCertKeyPair("localhost")
	require.NoError(t, err, "failed to create server key pair")

	clientPair, err := tlsCA.NewClientCertKeyPair()
	require.NoError(t, err, "failed to create client key pair")

	unknownTlsCA, err := tlsgen.NewCA()
	require.NoError(t, err, "failed to create unknown CA")

	unknownServerPair, err := unknownTlsCA.NewServerCertKeyPair("unknownhost")
	require.NoError(t, err, "failed to create unknown server key pair")

	unknownServerCert, err := parseCertificateFromBytes(unknownServerPair.Cert)
	require.NoError(t, err, "failed to parse unknown server certificate")

	unknownClientPair, err := unknownTlsCA.NewClientCertKeyPair()
	require.NoError(t, err, "failed to create unknown client key pair")

	unknownClientCert, err := parseCertificateFromBytes(unknownClientPair.Cert)
	require.NoError(t, err, "failed to parse unknown client certificate")

	validOptions := &etcdraftproto.Options{
		TickInterval:         "500ms",
		ElectionTick:         10,
		HeartbeatTick:        1,
		MaxInflightBlocks:    5,
		SnapshotIntervalSize: 20 * 1024 * 1024, // 20 MB
	}
	singleConsenter := &etcdraftproto.Consenter{
		Host:          "host1",
		Port:          10001,
		ClientTlsCert: clientPair.Cert,
		ServerTlsCert: serverPair.Cert,
	}

	rootCertPool := x509.NewCertPool()
	rootCertPool.AddCert(caRootCert)
	goodVerifyingOpts := x509.VerifyOptions{
		Roots: rootCertPool,
		KeyUsages: []x509.ExtKeyUsage{
			x509.ExtKeyUsageClientAuth,
			x509.ExtKeyUsageServerAuth,
		},
	}

	// valid metadata should give nil error
	goodMetadata := &etcdraftproto.ConfigMetadata{
		Options: validOptions,
		Consenters: []*etcdraftproto.Consenter{
			singleConsenter,
		},
	}
	assert.Nil(t, VerifyConfigMetadata(goodMetadata, goodVerifyingOpts))

	// test variety of bad metadata
	for _, testCase := range []struct {
		description string
		metadata    *etcdraftproto.ConfigMetadata
		verifyOpts  x509.VerifyOptions
		errRegex    string
	}{
		{
			description: "nil metadata",
			metadata:    nil,
			errRegex:    "nil Raft config metadata",
			verifyOpts:  goodVerifyingOpts,
		},
		{
			description: "nil options",
			metadata:    &etcdraftproto.ConfigMetadata{},
			verifyOpts:  goodVerifyingOpts,
			errRegex:    "nil Raft config metadata options",
		},
		{
			description: "HeartbeatTick is 0",
			metadata: &etcdraftproto.ConfigMetadata{
				Options: &etcdraftproto.Options{
					HeartbeatTick: 0,
				},
			},
			verifyOpts: goodVerifyingOpts,
			errRegex:   "none of HeartbeatTick .* can be zero",
		},
		{
			description: "ElectionTick is 0",
			metadata: &etcdraftproto.ConfigMetadata{
				Options: &etcdraftproto.Options{
					HeartbeatTick: validOptions.HeartbeatTick,
					ElectionTick:  0,
				},
			},
			verifyOpts: goodVerifyingOpts,
			errRegex:   "none of .* ElectionTick .* can be zero",
		},
		{
			description: "MaxInflightBlocks is 0",
			metadata: &etcdraftproto.ConfigMetadata{
				Options: &etcdraftproto.Options{
					HeartbeatTick:     validOptions.HeartbeatTick,
					ElectionTick:      validOptions.ElectionTick,
					MaxInflightBlocks: 0,
				},
			},
			verifyOpts: goodVerifyingOpts,
			errRegex:   "none of .* MaxInflightBlocks .* can be zero",
		},
		{
			description: "ElectionTick is less than HeartbeatTick",
			metadata: &etcdraftproto.ConfigMetadata{
				Options: &etcdraftproto.Options{
					HeartbeatTick:     10,
					ElectionTick:      1,
					MaxInflightBlocks: validOptions.MaxInflightBlocks,
				},
			},
			verifyOpts: goodVerifyingOpts,
			errRegex:   "ElectionTick .* must be greater than HeartbeatTick",
		},
		{
			description: "TickInterval is not parsable",
			metadata: &etcdraftproto.ConfigMetadata{
				Options: &etcdraftproto.Options{
					HeartbeatTick:     validOptions.HeartbeatTick,
					ElectionTick:      validOptions.ElectionTick,
					MaxInflightBlocks: validOptions.MaxInflightBlocks,
					TickInterval:      "abcd",
				},
			},
			verifyOpts: goodVerifyingOpts,
			errRegex:   "failed to parse TickInterval .* to time duration",
		},
		{
			description: "TickInterval is 0",
			metadata: &etcdraftproto.ConfigMetadata{
				Options: &etcdraftproto.Options{
					HeartbeatTick:     validOptions.HeartbeatTick,
					ElectionTick:      validOptions.ElectionTick,
					MaxInflightBlocks: validOptions.MaxInflightBlocks,
					TickInterval:      "0s",
				},
			},
			verifyOpts: goodVerifyingOpts,
			errRegex:   "TickInterval cannot be zero",
		},
		{
			description: "consenter set is empty",
			metadata: &etcdraftproto.ConfigMetadata{
				Options:    validOptions,
				Consenters: []*etcdraftproto.Consenter{},
			},
			verifyOpts: goodVerifyingOpts,
			errRegex:   "empty consenter set",
		},
		{
			description: "metadata has nil consenter",
			metadata: &etcdraftproto.ConfigMetadata{
				Options: validOptions,
				Consenters: []*etcdraftproto.Consenter{
					nil,
				},
			},
			verifyOpts: goodVerifyingOpts,
			errRegex:   "metadata has nil consenter",
		},
		{
			description: "consenter has invalid server cert",
			metadata: &etcdraftproto.ConfigMetadata{
				Options: validOptions,
				Consenters: []*etcdraftproto.Consenter{
					{
						ServerTlsCert: []byte("invalid"),
						ClientTlsCert: clientPair.Cert,
					},
				},
			},
			verifyOpts: goodVerifyingOpts,
			errRegex:   "no PEM data found in cert",
		},
		{
			description: "consenter has invalid client cert",
			metadata: &etcdraftproto.ConfigMetadata{
				Options: validOptions,
				Consenters: []*etcdraftproto.Consenter{
					{
						ServerTlsCert: serverPair.Cert,
						ClientTlsCert: []byte("invalid"),
					},
				},
			},
			verifyOpts: goodVerifyingOpts,
			errRegex:   "no PEM data found in cert",
		},
		{
			description: "metadata has duplicate consenters",
			metadata: &etcdraftproto.ConfigMetadata{
				Options: validOptions,
				Consenters: []*etcdraftproto.Consenter{
					singleConsenter,
					singleConsenter,
				},
			},
			verifyOpts: goodVerifyingOpts,
			errRegex:   "duplicate consenter",
		},
		{
			description: "consenter has client cert signed by unknown authority",
			metadata: &etcdraftproto.ConfigMetadata{
				Options: validOptions,
				Consenters: []*etcdraftproto.Consenter{
					{
						ClientTlsCert: unknownClientPair.Cert,
						ServerTlsCert: serverPair.Cert,
					},
				},
			},
			verifyOpts: goodVerifyingOpts,
			errRegex:   fmt.Sprintf("verifying tls client cert with serial number %d: x509: certificate signed by unknown authority", unknownClientCert.SerialNumber),
		},
		{
			description: "consenter has server cert signed by unknown authority",
			metadata: &etcdraftproto.ConfigMetadata{
				Options: validOptions,
				Consenters: []*etcdraftproto.Consenter{
					{
						ServerTlsCert: unknownServerPair.Cert,
						ClientTlsCert: clientPair.Cert,
					},
				},
			},
			verifyOpts: goodVerifyingOpts,
			errRegex:   fmt.Sprintf("verifying tls server cert with serial number %d: x509: certificate signed by unknown authority", unknownServerCert.SerialNumber),
		},
	} {
		t.Run(testCase.description, func(t *testing.T) {
			err := VerifyConfigMetadata(testCase.metadata, testCase.verifyOpts)
			require.NotNil(t, err, testCase.description)
			require.Regexp(t, testCase.errRegex, err)
		})
	}

	t.Run("ExpiredCertificate", func(t *testing.T) {
		clientPair, err := tlsCA.NewClientCertKeyPair()
		require.NoError(t, err, "failed to create client key pair")

		clientCert := clientPair.TLSCert
		clientCert.NotAfter = time.Now().Add(-24 * time.Hour)
		clientCertBytes, err := x509.CreateCertificate(rand.Reader, clientCert, caRootCert, clientPair.Signer.Public(), tlsCA.Signer())
		require.NoError(t, err, "failed to create expired certificate")

		clientCert, err = x509.ParseCertificate(clientCertBytes)
		require.NoError(t, err, "failed to parse expired certificate")

		_, err = clientCert.Verify(goodVerifyingOpts)
		require.Error(t, err, "expected certificate verification to fail")
		cie, ok := err.(x509.CertificateInvalidError)
		require.True(t, ok, "expected an x509.CertificateInvalidError but got %T", err)
		require.Equal(t, x509.Expired, cie.Reason)

		consenterWithExpiredCerts := &etcdraftproto.Consenter{
			Host:          "host1",
			Port:          10001,
			ClientTlsCert: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: clientCertBytes}),
			ServerTlsCert: serverPair.Cert,
		}

		metadataWithExpiredConsenter := &etcdraftproto.ConfigMetadata{
			Options: &etcdraftproto.Options{
				TickInterval:         "500ms",
				ElectionTick:         10,
				HeartbeatTick:        1,
				MaxInflightBlocks:    5,
				SnapshotIntervalSize: 20 * 1024 * 1024, // 20 MB
			},
			Consenters: []*etcdraftproto.Consenter{
				consenterWithExpiredCerts,
			},
		}

		require.Nil(t, VerifyConfigMetadata(metadataWithExpiredConsenter, goodVerifyingOpts))
	})
}

// This test checks that MetadataFromConfigValue, which reads and translates configuration updates from config value,
// returns an empty raft metadata when the consensus type is BFT.
func TestMetadataFromConfigValue(t *testing.T) {
	configValue := &common.ConfigValue{
		Value: protoutil.MarshalOrPanic(&orderer.ConsensusType{
			Type:     "BFT",
			Metadata: []byte{1, 2},
		}),
	}

	metadata, consensusType, err := MetadataFromConfigValue(configValue)
	require.Nil(t, metadata)
	require.Nil(t, err)
	require.NotNil(t, consensusType)
	require.Equal(t, consensusType.Type, "BFT")
}
