package morph

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

	"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 = "../../../../../../frostfs-contract/frostfs-contract-v0.16.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, maxAlphabetNodes)
	})
	t.Run("too many nodes", func(t *testing.T) {
		require.ErrorIs(t, generateTestData(t, t.TempDir(), maxAlphabetNodes+1), ErrTooManyAlphabetNodes)
	})
}

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

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

	// Set to the path or remove the next statement to download from the network.
	require.NoError(t, initCmd.Flags().Set(contractsInitFlag, contractsPath))
	v.Set(localDumpFlag, filepath.Join(testdataDir, "out"))
	v.Set(alphabetWalletsFlag, testdataDir)
	v.Set(epochDurationInitFlag, 1)
	v.Set(maxObjectSizeInitFlag, 1024)

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

	t.Run("force-new-epoch", func(t *testing.T) {
		require.NoError(t, forceNewEpochCmd(forceNewEpoch, nil))
	})
	t.Run("set-config", func(t *testing.T) {
		require.NoError(t, setConfigCmd(setConfig, []string{"MaintenanceModeAllowed=true"}))
	})
	t.Run("set-policy", func(t *testing.T) {
		require.NoError(t, setPolicyCmd(setPolicy, []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, removeNodesCmd(removeNodes, []string{pub}))
	})
}

func generateTestData(t *testing.T, dir string, size int) error {
	v := viper.GetViper()
	v.Set(alphabetWalletsFlag, dir)

	sizeStr := strconv.FormatUint(uint64(size), 10)
	if err := generateAlphabetCmd.Flags().Set(alphabetSizeFlag, sizeStr); err != nil {
		return err
	}

	setTestCredentials(v, size)
	if err := generateAlphabetCreds(generateAlphabetCmd, nil); err != nil {
		return err
	}

	var pubs []string
	for i := 0; i < size; i++ {
		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 == 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 := 0; i < size; i++ {
		v.Set("credentials."+innerring.GlagoliticLetter(i).String(), strconv.FormatUint(uint64(i), 10))
	}
	v.Set("credentials.contract", testContractPassword)
}