/*
Copyright IBM Corp. 2017 All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package genesisconfig

import (
	"testing"

	"github.com/hyperledger/fabric-protos-go-apiv2/orderer/etcdraft"
	"github.com/hyperledger/fabric/common/viperutil"
	"github.com/hyperledger/fabric/core/config/configtest"
	"github.com/stretchr/testify/require"
)

func TestLoadProfile(t *testing.T) {
	configtest.SetDevFabricConfigPath(t)

	pNames := []string{
		SampleDevModeSoloProfile,
		SampleSingleMSPChannelProfile,
		SampleSingleMSPSoloProfile,
		SampleDevModeEtcdRaftProfile,
		SampleAppChannelEtcdRaftProfile,
	}
	for _, pName := range pNames {
		t.Run(pName, func(t *testing.T) {
			p := Load(pName)
			require.NotNil(t, p, "profile should not be nil")
		})
	}
}

func TestLoadProfileWithPath(t *testing.T) {
	devConfigDir := configtest.GetDevConfigDir()

	pNames := []string{
		SampleDevModeSoloProfile,
		SampleSingleMSPChannelProfile,
		SampleSingleMSPSoloProfile,
		SampleDevModeEtcdRaftProfile,
		SampleAppChannelEtcdRaftProfile,
	}
	for _, pName := range pNames {
		t.Run(pName, func(t *testing.T) {
			p := Load(pName, devConfigDir)
			require.NotNil(t, p, "profile should not be nil")
		})
	}
}

func TestLoadTopLevel(t *testing.T) {
	configtest.SetDevFabricConfigPath(t)

	topLevel := LoadTopLevel()
	require.NotNil(t, topLevel.Application, "application should not be nil")
	require.NotNil(t, topLevel.Capabilities, "capabilities should not be nil")
	require.NotNil(t, topLevel.Orderer, "orderer should not be nil")
	require.NotNil(t, topLevel.Organizations, "organizations should not be nil")
	require.NotNil(t, topLevel.Profiles, "profiles should not be nil")
}

func TestLoadTopLevelWithPath(t *testing.T) {
	devConfigDir := configtest.GetDevConfigDir()

	topLevel := LoadTopLevel(devConfigDir)
	require.NotNil(t, topLevel.Application, "application should not be nil")
	require.NotNil(t, topLevel.Capabilities, "capabilities should not be nil")
	require.NotNil(t, topLevel.Orderer, "orderer should not be nil")
	require.NotNil(t, topLevel.Organizations, "organizations should not be nil")
	require.NotNil(t, topLevel.Profiles, "profiles should not be nil")
}

func TestConsensusSpecificInit(t *testing.T) {
	configtest.SetDevFabricConfigPath(t)

	devConfigDir := configtest.GetDevConfigDir()

	t.Run("nil orderer type", func(t *testing.T) {
		profile := &Profile{
			Orderer: &Orderer{
				OrdererType: "",
			},
		}
		profile.completeInitialization(devConfigDir)

		require.Equal(t, profile.Orderer.OrdererType, genesisDefaults.Orderer.OrdererType)
	})

	t.Run("unknown orderer type", func(t *testing.T) {
		profile := &Profile{
			Orderer: &Orderer{
				OrdererType: "unknown",
			},
		}

		require.Panics(t, func() {
			profile.completeInitialization(devConfigDir)
		})
	})

	t.Run("solo", func(t *testing.T) {
		profile := &Profile{
			Orderer: &Orderer{
				OrdererType: "solo",
			},
		}
		profile.completeInitialization(devConfigDir)
		require.NotNil(t, profile.Orderer.BatchSize)
	})

	t.Run("raft", func(t *testing.T) {
		makeProfile := func(consenters []*etcdraft.Consenter, options *etcdraft.Options) *Profile {
			return &Profile{
				Orderer: &Orderer{
					OrdererType: "etcdraft",
					EtcdRaft: &etcdraft.ConfigMetadata{
						Consenters: consenters,
						Options:    options,
					},
				},
			}
		}
		t.Run("EtcdRaft section not specified in profile", func(t *testing.T) {
			profile := &Profile{
				Orderer: &Orderer{
					OrdererType: "etcdraft",
				},
			}

			require.Panics(t, func() {
				profile.completeInitialization(devConfigDir)
			})
		})

		t.Run("nil consenter set", func(t *testing.T) { // should panic
			profile := makeProfile(nil, nil)

			require.Panics(t, func() {
				profile.completeInitialization(devConfigDir)
			})
		})

		t.Run("single consenter", func(t *testing.T) {
			consenters := []*etcdraft.Consenter{
				{
					Host:          "node-1.example.com",
					Port:          7050,
					ClientTlsCert: []byte("path/to/client/cert"),
					ServerTlsCert: []byte("path/to/server/cert"),
				},
			}

			t.Run("invalid consenters specification", func(t *testing.T) {
				failingConsenterSpecifications := []*etcdraft.Consenter{
					{ // missing Host
						Port:          7050,
						ClientTlsCert: []byte("path/to/client/cert"),
						ServerTlsCert: []byte("path/to/server/cert"),
					},
					{ // missing Port
						Host:          "node-1.example.com",
						ClientTlsCert: []byte("path/to/client/cert"),
						ServerTlsCert: []byte("path/to/server/cert"),
					},
					{ // missing ClientTlsCert
						Host:          "node-1.example.com",
						Port:          7050,
						ServerTlsCert: []byte("path/to/server/cert"),
					},
					{ // missing ServerTlsCert
						Host:          "node-1.example.com",
						Port:          7050,
						ClientTlsCert: []byte("path/to/client/cert"),
					},
				}

				for _, consenter := range failingConsenterSpecifications {
					profile := makeProfile([]*etcdraft.Consenter{consenter}, nil)

					require.Panics(t, func() {
						profile.completeInitialization(devConfigDir)
					})
				}
			})

			t.Run("nil Options", func(t *testing.T) {
				profile := makeProfile(consenters, nil)
				profile.completeInitialization(devConfigDir)

				// need not be tested in subsequent tests
				require.NotNil(t, profile.Orderer.EtcdRaft, "EtcdRaft config settings should be set")
				require.Equal(t, profile.Orderer.EtcdRaft.Consenters[0].ClientTlsCert, consenters[0].ClientTlsCert,
					"Client TLS cert path should be correctly set")

				// specific assertion for this test context
				require.Equal(t, profile.Orderer.EtcdRaft.Options, genesisDefaults.Orderer.EtcdRaft.Options,
					"Options should be set to the default value")
			})

			t.Run("heartbeat tick specified in Options", func(t *testing.T) {
				heartbeatTick := uint32(2)
				options := &etcdraft.Options{ // partially set so that we can check that the other members are set to defaults
					HeartbeatTick: heartbeatTick,
				}
				profile := makeProfile(consenters, options)
				profile.completeInitialization(devConfigDir)

				// specific assertions for this test context
				require.Equal(t, profile.Orderer.EtcdRaft.Options.HeartbeatTick, heartbeatTick,
					"HeartbeatTick should be set to the specified value")
				require.Equal(t, profile.Orderer.EtcdRaft.Options.ElectionTick, genesisDefaults.Orderer.EtcdRaft.Options.ElectionTick,
					"ElectionTick should be set to the default value")
			})

			t.Run("election tick specified in Options", func(t *testing.T) {
				electionTick := uint32(20)
				options := &etcdraft.Options{ // partially set so that we can check that the other members are set to defaults
					ElectionTick: electionTick,
				}
				profile := makeProfile(consenters, options)
				profile.completeInitialization(devConfigDir)

				// specific assertions for this test context
				require.Equal(t, profile.Orderer.EtcdRaft.Options.ElectionTick, electionTick,
					"ElectionTick should be set to the specified value")
				require.Equal(t, profile.Orderer.EtcdRaft.Options.HeartbeatTick, genesisDefaults.Orderer.EtcdRaft.Options.HeartbeatTick,
					"HeartbeatTick should be set to the default value")
			})

			t.Run("panic on invalid Heartbeat and Election tick", func(t *testing.T) {
				options := &etcdraft.Options{
					HeartbeatTick: 2,
					ElectionTick:  1,
				}
				profile := makeProfile(consenters, options)

				require.Panics(t, func() {
					profile.completeInitialization(devConfigDir)
				})
			})

			t.Run("panic on invalid TickInterval", func(t *testing.T) {
				options := &etcdraft.Options{
					TickInterval: "500",
				}
				profile := makeProfile(consenters, options)

				require.Panics(t, func() {
					profile.completeInitialization(devConfigDir)
				})
			})
		})
	})
}

func TestLoadConfigCache(t *testing.T) {
	configtest.SetDevFabricConfigPath(t)

	cfg := viperutil.New()
	devConfigDir := configtest.GetDevConfigDir()
	cfg.AddConfigPaths(devConfigDir)
	cfg.SetConfigName("configtx")
	err := cfg.ReadInConfig()
	require.NoError(t, err)

	configPath := cfg.ConfigFileUsed()
	c := &configCache{
		cache: make(map[string][]byte),
	}

	// Load the initial config, update the environment, and load again.
	// With the caching behavior, the update should not be reflected.
	initial, err := c.load(cfg, configPath)
	require.NoError(t, err)
	updated, err := c.load(cfg, configPath)
	require.NoError(t, err)
	require.Equal(t, initial, updated, "expected %#v to equal %#v", updated, initial)

	// Change the configuration we got back and load again.
	// The  new value should not contain the updated to the initial
	initial.Orderer.OrdererType = "bad-Orderer-Type"
	updated, err = c.load(cfg, configPath)
	require.NoError(t, err)
	require.NotEqual(t, initial, updated, "expected %#v to not equal %#v", updated, initial)
}
