/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package config_test

import (
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"testing"

	"github.com/hyperledger/fabric-protos-go-apiv2/common"
	"github.com/hyperledger/fabric-protos-go-apiv2/discovery"
	"github.com/hyperledger/fabric-protos-go-apiv2/msp"
	"github.com/hyperledger/fabric/common/channelconfig"
	"github.com/hyperledger/fabric/common/configtx/test"
	"github.com/hyperledger/fabric/discovery/support/config"
	"github.com/hyperledger/fabric/discovery/support/mocks"
	"github.com/hyperledger/fabric/internal/configtxgen/encoder"
	"github.com/hyperledger/fabric/internal/configtxgen/genesisconfig"
	"github.com/hyperledger/fabric/protoutil"
	"github.com/onsi/gomega/gexec"
	"github.com/stretchr/testify/require"
	"google.golang.org/protobuf/proto"
)

func TestMSPIDMapping(t *testing.T) {
	randString := func() string {
		buff := make([]byte, 10)
		rand.Read(buff)
		return hex.EncodeToString(buff)
	}

	dir := filepath.Join(os.TempDir(), fmt.Sprintf("TestMSPIDMapping_%s", randString()))
	os.Mkdir(dir, 0o700)
	defer os.RemoveAll(dir)

	cryptogen, err := gexec.Build("github.com/hyperledger/fabric/cmd/cryptogen")
	require.NoError(t, err)
	defer os.Remove(cryptogen)

	idemixgen, err := gexec.Build("github.com/IBM/idemix/tools/idemixgen", "-mod=mod")
	require.NoError(t, err)
	defer os.Remove(idemixgen)

	cryptoConfigDir := filepath.Join(dir, "crypto-config")
	b, err := exec.Command(cryptogen, "generate", fmt.Sprintf("--output=%s", cryptoConfigDir)).CombinedOutput()
	require.NoError(t, err, string(b))

	idemixConfigDir := filepath.Join(dir, "crypto-config", "idemix")
	b, err = exec.Command(idemixgen, "ca-keygen", fmt.Sprintf("--output=%s", idemixConfigDir)).CombinedOutput()
	require.NoError(t, err, string(b))

	profileConfig := genesisconfig.Load("TwoOrgsChannel", "testdata/")
	ordererConfig := genesisconfig.Load("TwoOrgsOrdererGenesis", "testdata/")
	profileConfig.Orderer = ordererConfig.Orderer

	// Override the MSP directory with our randomly generated and populated path
	for _, org := range ordererConfig.Orderer.Organizations {
		org.MSPDir = filepath.Join(cryptoConfigDir, "ordererOrganizations", "example.com", "msp")
		org.Name = randString()
		org.OrdererEndpoints = []string{"foo:7050", "bar:8050"}
	}

	// Randomize organization names
	for _, org := range profileConfig.Application.Organizations {
		org.Name = randString()
		// Non bccsp-msp orgs don't have the crypto material produced by cryptogen,
		// we need to use the idemix crypto folder instead.
		if org.MSPType != "bccsp" {
			org.MSPDir = filepath.Join(idemixConfigDir)
			continue
		}
		org.MSPDir = filepath.Join(cryptoConfigDir, "peerOrganizations", "org1.example.com", "msp")
	}

	channelGroup, err := encoder.NewChannelGroup(profileConfig)
	require.NoError(t, err)
	fakeConfigGetter := &mocks.ConfigGetter{}
	fakeConfigGetter.GetCurrConfigReturnsOnCall(
		0,
		&common.Config{
			ChannelGroup: channelGroup,
		},
	)

	cs := config.NewDiscoverySupport(fakeConfigGetter)
	res, err := cs.Config("mychannel")
	require.NoError(t, err)

	actualKeys := make(map[string]struct{})
	for key := range res.Orderers {
		actualKeys[key] = struct{}{}
	}

	for key := range res.Msps {
		actualKeys[key] = struct{}{}
	}

	// Note that Org3MSP is an idemix org, but it shouldn't be listed here
	// because peers can't have idemix credentials
	expected := map[string]struct{}{
		"OrdererMSP": {},
		"Org1MSP":    {},
		"Org2MSP":    {},
	}
	require.Equal(t, expected, actualKeys)
}

func TestSupportGreenPath(t *testing.T) {
	fakeConfigGetter := &mocks.ConfigGetter{}
	fakeConfigGetter.GetCurrConfigReturnsOnCall(0, nil)

	cs := config.NewDiscoverySupport(fakeConfigGetter)
	res, err := cs.Config("test")
	require.Nil(t, res)
	require.Equal(t, "could not get last config for channel test", err.Error())

	config, err := test.MakeChannelConfig("test")
	require.NoError(t, err)
	require.NotNil(t, config)

	fakeConfigGetter.GetCurrConfigReturnsOnCall(1, config)
	res, err = cs.Config("test")
	require.NoError(t, err)
	require.NotNil(t, res)
}

func TestValidateConfig(t *testing.T) {
	tests := []struct {
		name          string
		config        *common.Config
		containsError string
	}{
		{
			name:          "nil Config field",
			config:        &common.Config{},
			containsError: "field Config.ChannelGroup is nil",
		},
		{
			name: "nil Groups field",
			config: &common.Config{
				ChannelGroup: &common.ConfigGroup{},
			},
			containsError: "field Config.ChannelGroup.Groups is nil",
		},
		{
			name: "no orderer group key",
			config: &common.Config{
				ChannelGroup: &common.ConfigGroup{
					Groups: map[string]*common.ConfigGroup{
						channelconfig.ApplicationGroupKey: {},
					},
				},
			},
			containsError: "key Config.ChannelGroup.Groups[Orderer] is missing",
		},
		{
			name: "no application group key",
			config: &common.Config{
				ChannelGroup: &common.ConfigGroup{
					Groups: map[string]*common.ConfigGroup{
						channelconfig.OrdererGroupKey: {
							Groups: map[string]*common.ConfigGroup{},
						},
					},
				},
			},
			containsError: "key Config.ChannelGroup.Groups[Application] is missing",
		},
		{
			name: "no groups key in orderer group",
			config: &common.Config{
				ChannelGroup: &common.ConfigGroup{
					Groups: map[string]*common.ConfigGroup{
						channelconfig.ApplicationGroupKey: {
							Groups: map[string]*common.ConfigGroup{},
						},
						channelconfig.OrdererGroupKey: {},
					},
				},
			},
			containsError: "key Config.ChannelGroup.Groups[Orderer].Groups is nil",
		},
		{
			name: "no groups key in application group",
			config: &common.Config{
				ChannelGroup: &common.ConfigGroup{
					Groups: map[string]*common.ConfigGroup{
						channelconfig.ApplicationGroupKey: {},
						channelconfig.OrdererGroupKey: {
							Groups: map[string]*common.ConfigGroup{},
						},
					},
				},
			},
			containsError: "key Config.ChannelGroup.Groups[Application].Groups is nil",
		},
		{
			name: "no Values in ChannelGroup",
			config: &common.Config{
				ChannelGroup: &common.ConfigGroup{
					Groups: map[string]*common.ConfigGroup{
						channelconfig.ApplicationGroupKey: {
							Groups: map[string]*common.ConfigGroup{},
						},
						channelconfig.OrdererGroupKey: {
							Groups: map[string]*common.ConfigGroup{},
						},
					},
				},
			},
			containsError: "field Config.ChannelGroup.Values is nil",
		},
	}

	for _, test := range tests {
		test := test
		t.Run(test.name, func(t *testing.T) {
			err := config.ValidateConfig(test.config)
			require.Contains(t, test.containsError, err.Error())
		})
	}
}

func TestOrdererEndpoints(t *testing.T) {
	t.Run("Global endpoints", func(t *testing.T) {
		channelConfig, err := test.MakeChannelConfig("mychannel")
		require.NoError(t, err)

		fakeConfigGetter := &mocks.ConfigGetter{}
		cs := config.NewDiscoverySupport(fakeConfigGetter)

		fakeConfigGetter.GetCurrConfigReturnsOnCall(0, channelConfig)

		injectGlobalOrdererEndpoint(t, channelConfig, "globalEndpoint:7050")

		res, err := cs.Config("test")
		require.NoError(t, err)
		require.Equal(t, map[string]*discovery.Endpoints{
			"SampleOrg": {Endpoint: []*discovery.Endpoint{{Host: "globalEndpoint", Port: 7050}}},
		}, res.Orderers)
	})

	t.Run("Per org endpoints alongside global endpoints", func(t *testing.T) {
		channelConfig, err := test.MakeChannelConfig("mychannel")
		require.NoError(t, err)

		fakeConfigGetter := &mocks.ConfigGetter{}
		cs := config.NewDiscoverySupport(fakeConfigGetter)

		fakeConfigGetter.GetCurrConfigReturnsOnCall(0, channelConfig)

		injectAdditionalEndpointPair(t, channelConfig, "perOrgEndpoint:7050", "anotherOrg")
		injectAdditionalEndpointPair(t, channelConfig, "endpointWithoutAPortName", "aBadOrg")

		res, err := cs.Config("test")
		require.NoError(t, err)
		require.Equal(t, map[string]*discovery.Endpoints{
			"SampleOrg":  {Endpoint: []*discovery.Endpoint{{Host: "127.0.0.1", Port: 7050}}},
			"anotherOrg": {Endpoint: []*discovery.Endpoint{{Host: "perOrgEndpoint", Port: 7050}}},
			"aBadOrg":    {},
		}, res.Orderers)
	})

	t.Run("Per org endpoints without global endpoints", func(t *testing.T) {
		channelConfig, err := test.MakeChannelConfig("mychannel")
		require.NoError(t, err)

		fakeConfigGetter := &mocks.ConfigGetter{}
		cs := config.NewDiscoverySupport(fakeConfigGetter)

		fakeConfigGetter.GetCurrConfigReturnsOnCall(0, channelConfig)

		removeGlobalEndpoints(t, channelConfig)
		injectAdditionalEndpointPair(t, channelConfig, "perOrgEndpoint:7050", "SampleOrg")
		injectAdditionalEndpointPair(t, channelConfig, "endpointWithoutAPortName", "aBadOrg")

		res, err := cs.Config("test")
		require.NoError(t, err)
		require.Equal(t, map[string]*discovery.Endpoints{
			"SampleOrg": {Endpoint: []*discovery.Endpoint{{Host: "perOrgEndpoint", Port: 7050}}},
			"aBadOrg":   {},
		}, res.Orderers)
	})
}

func removeGlobalEndpoints(t *testing.T, config *common.Config) {
	// Remove the orderer addresses
	delete(config.ChannelGroup.Values, channelconfig.OrdererAddressesKey)
}

func injectGlobalOrdererEndpoint(t *testing.T, config *common.Config, endpoint string) {
	ordererAddresses := channelconfig.OrdererAddressesValue([]string{endpoint})
	// Replace the orderer addresses
	config.ChannelGroup.Values[ordererAddresses.Key()] = &common.ConfigValue{
		Value:     protoutil.MarshalOrPanic(ordererAddresses.Value()),
		ModPolicy: "/Channel/Orderer/Admins",
	}
	// Remove the per org addresses, if applicable
	ordererGrps := config.ChannelGroup.Groups[channelconfig.OrdererGroupKey].Groups
	for _, grp := range ordererGrps {
		if grp.Values[channelconfig.EndpointsKey] == nil {
			continue
		}
		grp.Values[channelconfig.EndpointsKey].Value = nil
	}
}

func injectAdditionalEndpointPair(t *testing.T, config *common.Config, endpoint string, orgName string) {
	ordererGrp := config.ChannelGroup.Groups[channelconfig.OrdererGroupKey].Groups
	// Get the first orderer org config
	var firstOrdererConfig *common.ConfigGroup
	for _, grp := range ordererGrp {
		firstOrdererConfig = grp
		break
	}
	// Duplicate it.
	secondOrdererConfig := proto.Clone(firstOrdererConfig).(*common.ConfigGroup)
	ordererGrp[orgName] = secondOrdererConfig
	// Reach the FabricMSPConfig buried in it.
	mspConfig := &msp.MSPConfig{}
	err := proto.Unmarshal(secondOrdererConfig.Values[channelconfig.MSPKey].Value, mspConfig)
	require.NoError(t, err)

	fabricConfig := &msp.FabricMSPConfig{}
	err = proto.Unmarshal(mspConfig.Config, fabricConfig)
	require.NoError(t, err)

	// Rename it.
	fabricConfig.Name = orgName

	// Pack the MSP config back into the config
	secondOrdererConfig.Values[channelconfig.MSPKey].Value = protoutil.MarshalOrPanic(&msp.MSPConfig{
		Config: protoutil.MarshalOrPanic(fabricConfig),
		Type:   mspConfig.Type,
	})

	// Inject the endpoint
	ordererOrgProtos := &common.OrdererAddresses{
		Addresses: []string{endpoint},
	}
	secondOrdererConfig.Values[channelconfig.EndpointsKey].Value = protoutil.MarshalOrPanic(ordererOrgProtos)
}
