package initialize

import (
	"encoding/hex"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"testing"
	"time"

	"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags"
	cmdConfig "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/config"
	"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants"
	"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/generate"
	"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/helper"
	"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/netmap"
	"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/node"
	"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/policy"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/innerring"
	"github.com/nspcc-dev/neo-go/pkg/config"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"github.com/nspcc-dev/neo-go/pkg/vm"
	"github.com/nspcc-dev/neo-go/pkg/wallet"
	"github.com/spf13/viper"
	"github.com/stretchr/testify/require"
	"gopkg.in/yaml.v3"
)

const (
	contractsPath = "../../../../../../contract/frostfs-contract-v0.18.0.tar.gz"
	protoFileName = "proto.yml"
)

func TestInitialize(t *testing.T) {
	// This test needs frostfs-contract tarball, so it is skipped by default.
	// It is here for performing local testing after the changes.
	t.Skip()

	t.Run("1 nodes", func(t *testing.T) {
		testInitialize(t, 1)
	})
	t.Run("4 nodes", func(t *testing.T) {
		testInitialize(t, 4)
	})
	t.Run("7 nodes", func(t *testing.T) {
		testInitialize(t, 7)
	})
	t.Run("16 nodes", func(t *testing.T) {
		testInitialize(t, 16)
	})
	t.Run("max nodes", func(t *testing.T) {
		testInitialize(t, constants.MaxAlphabetNodes)
	})
	t.Run("too many nodes", func(t *testing.T) {
		require.ErrorIs(t, generateTestData(t.TempDir(), constants.MaxAlphabetNodes+1), helper.ErrTooManyAlphabetNodes)
	})
}

func testInitialize(t *testing.T, committeeSize int) {
	testdataDir := t.TempDir()
	v := viper.GetViper()

	require.NoError(t, generateTestData(testdataDir, committeeSize))
	v.Set(commonflags.ProtoConfigPath, filepath.Join(testdataDir, protoFileName))

	// Set to the path or remove the next statement to download from the network.
	require.NoError(t, Cmd.Flags().Set(commonflags.ContractsInitFlag, contractsPath))

	dumpPath := filepath.Join(testdataDir, "out")
	require.NoError(t, Cmd.Flags().Set(commonflags.LocalDumpFlag, dumpPath))
	v.Set(commonflags.AlphabetWalletsFlag, testdataDir)
	v.Set(commonflags.EpochDurationInitFlag, 1)
	v.Set(commonflags.MaxObjectSizeInitFlag, 1024)

	setTestCredentials(v, committeeSize)
	require.NoError(t, initializeSideChainCmd(Cmd, nil))

	t.Run("force-new-epoch", func(t *testing.T) {
		require.NoError(t, netmap.ForceNewEpoch.Flags().Set(commonflags.LocalDumpFlag, dumpPath))
		require.NoError(t, netmap.ForceNewEpochCmd(netmap.ForceNewEpoch, nil))
	})
	t.Run("set-config", func(t *testing.T) {
		require.NoError(t, cmdConfig.SetCmd.Flags().Set(commonflags.LocalDumpFlag, dumpPath))
		require.NoError(t, cmdConfig.SetConfigCmd(cmdConfig.SetCmd, []string{"MaintenanceModeAllowed=true"}))
	})
	t.Run("set-policy", func(t *testing.T) {
		require.NoError(t, policy.Set.Flags().Set(commonflags.LocalDumpFlag, dumpPath))
		require.NoError(t, policy.SetPolicyCmd(policy.Set, []string{"ExecFeeFactor=1"}))
	})
	t.Run("remove-node", func(t *testing.T) {
		pk, err := keys.NewPrivateKey()
		require.NoError(t, err)

		pub := hex.EncodeToString(pk.PublicKey().Bytes())
		require.NoError(t, node.RemoveCmd.Flags().Set(commonflags.LocalDumpFlag, dumpPath))
		require.NoError(t, node.RemoveNodesCmd(node.RemoveCmd, []string{pub}))
	})
}

func generateTestData(dir string, size int) error {
	v := viper.GetViper()
	v.Set(commonflags.AlphabetWalletsFlag, dir)

	sizeStr := strconv.FormatUint(uint64(size), 10)
	if err := generate.GenerateAlphabetCmd.Flags().Set(commonflags.AlphabetSizeFlag, sizeStr); err != nil {
		return err
	}

	setTestCredentials(v, size)
	if err := generate.AlphabetCreds(generate.GenerateAlphabetCmd, nil); err != nil {
		return err
	}

	var pubs []string
	for i := range size {
		p := filepath.Join(dir, innerring.GlagoliticLetter(i).String()+".json")
		w, err := wallet.NewWalletFromFile(p)
		if err != nil {
			return fmt.Errorf("wallet doesn't exist: %w", err)
		}
		for _, acc := range w.Accounts {
			if acc.Label == constants.SingleAccountName {
				pub, ok := vm.ParseSignatureContract(acc.Contract.Script)
				if !ok {
					return fmt.Errorf("could not parse signature script for %s", acc.Address)
				}
				pubs = append(pubs, hex.EncodeToString(pub))
				continue
			}
		}
	}

	cfg := config.Config{}
	cfg.ProtocolConfiguration.Magic = 12345
	cfg.ProtocolConfiguration.ValidatorsCount = uint32(size)
	cfg.ProtocolConfiguration.TimePerBlock = time.Second
	cfg.ProtocolConfiguration.StandbyCommittee = pubs // sorted by glagolic letters
	cfg.ProtocolConfiguration.P2PSigExtensions = true
	cfg.ProtocolConfiguration.VerifyTransactions = true
	data, err := yaml.Marshal(cfg)
	if err != nil {
		return err
	}

	protoPath := filepath.Join(dir, protoFileName)
	return os.WriteFile(protoPath, data, os.ModePerm)
}

func setTestCredentials(v *viper.Viper, size int) {
	for i := range size {
		v.Set("credentials."+innerring.GlagoliticLetter(i).String(), strconv.FormatUint(uint64(i), 10))
	}
	v.Set("credentials.contract", constants.TestContractPassword)
}