// Copyright 2017 Monax Industries Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package integration

import (
	"context"
	"encoding/binary"
	"fmt"
	"os"
	"runtime"
	"strconv"
	"sync/atomic"
	"time"

	"github.com/hyperledger/burrow/acm"
	"github.com/hyperledger/burrow/acm/validator"
	"github.com/hyperledger/burrow/config"
	"github.com/hyperledger/burrow/consensus/tendermint"
	"github.com/hyperledger/burrow/core"
	"github.com/hyperledger/burrow/execution/evm/sha3"
	"github.com/hyperledger/burrow/genesis"
	"github.com/hyperledger/burrow/keys/mock"
	"github.com/hyperledger/burrow/logging"
	"github.com/hyperledger/burrow/logging/lifecycle"
	lConfig "github.com/hyperledger/burrow/logging/logconfig"
	"github.com/hyperledger/burrow/permission"
)

const (
	ChainName = "Integration_Test_Chain"
	testDir   = "./test_scratch/tm_test"
)

// Enable logger output during tests

// Starting point for assigning range of ports for tests
// Start at unprivileged port (hoping for the best)
const startingPort uint16 = 1024

// For each port claimant assign a bucket
const startingPortSeparation uint16 = 10
const startingPortBuckets = 1000

// Mutable port to assign to next claimant
var port = uint32(startingPort)

var node uint64 = 0

// We use this to wrap tests
func TestKernel(validatorAccount *acm.PrivateAccount, keysAccounts []*acm.PrivateAccount,
	testConfig *config.BurrowConfig, loggingConfig *lConfig.LoggingConfig) *core.Kernel {
	fmt.Println("Creating integration test Kernel...")

	logger := logging.NewNoopLogger()
	if loggingConfig != nil {
		var err error
		// Change config as needed
		logger, err = lifecycle.NewLoggerFromLoggingConfig(loggingConfig)
		if err != nil {
			panic(err)
		}
	}

	privValidator := tendermint.NewPrivValidatorMemory(validatorAccount, validatorAccount)
	keyClient := mock.NewKeyClient(keysAccounts...)
	kernel, err := core.NewKernel(context.Background(), keyClient, privValidator,
		testConfig.GenesisDoc,
		testConfig.Tendermint.TendermintConfig(),
		testConfig.RPC,
		testConfig.Keys,
		nil, nil, logger)
	if err != nil {
		panic(err)
	}

	return kernel
}

func EnterTestDirectory() (cleanup func()) {
	os.RemoveAll(testDir)
	os.MkdirAll(testDir, 0777)
	os.Chdir(testDir)
	os.MkdirAll("config", 0777)
	return func() { os.RemoveAll(testDir) }
}

func TestGenesisDoc(addressables []*acm.PrivateAccount) *genesis.GenesisDoc {
	accounts := make(map[string]acm.Account, len(addressables))
	for i, pa := range addressables {
		account := acm.FromAddressable(pa)
		account.AddToBalance(1 << 32)
		account.SetPermissions(permission.AllAccountPermissions.Clone())
		accounts[fmt.Sprintf("user_%v", i)] = account
	}
	genesisTime, err := time.Parse("02-01-2006", "27-10-2017")
	if err != nil {
		panic("could not parse test genesis time")
	}
	return genesis.MakeGenesisDocFromAccounts(ChainName, nil, genesisTime, accounts,
		map[string]validator.Validator{
			"genesis_validator": validator.FromAccount(accounts["user_0"], 1<<16),
		})
}

// Deterministic account generation helper. Pass number of accounts to make
func MakePrivateAccounts(n int) []*acm.PrivateAccount {
	accounts := make([]*acm.PrivateAccount, n)
	for i := 0; i < n; i++ {
		accounts[i] = acm.GeneratePrivateAccountFromSecret("mysecret" + strconv.Itoa(i))
	}
	return accounts
}

// Some helpers for setting Burrow's various ports in non-colliding ranges for tests
func ClaimPorts() uint16 {
	_, file, _, _ := runtime.Caller(1)
	startIndex := uint16(binary.LittleEndian.Uint16(sha3.Sha3([]byte(file)))) % startingPortBuckets
	newPort := startingPort + startIndex*startingPortSeparation
	// In case overflow
	if newPort < startingPort {
		newPort += startingPort
	}
	if !atomic.CompareAndSwapUint32(&port, uint32(startingPort), uint32(newPort)) {
		panic("GetPort() called before ClaimPorts() or ClaimPorts() called twice")
	}
	return uint16(atomic.LoadUint32(&port))
}

func GetPort() uint16 {
	return uint16(atomic.AddUint32(&port, 1))
}

// Gets an name based on an incrementing counter for running multiple nodes
func GetName() string {
	nodeNumber := atomic.AddUint64(&node, 1)
	return fmt.Sprintf("node_%03d", nodeNumber)
}

func GetLocalAddress() string {
	return fmt.Sprintf("127.0.0.1:%v", GetPort())
}

func GetTCPLocalAddress() string {
	return fmt.Sprintf("tcp://127.0.0.1:%v", GetPort())
}

func NewTestConfig(genesisDoc *genesis.GenesisDoc) *config.BurrowConfig {
	name := GetName()
	cnf := config.DefaultBurrowConfig()
	cnf.GenesisDoc = genesisDoc
	cnf.Tendermint.Moniker = name
	cnf.Tendermint.TendermintRoot = fmt.Sprintf(".burrow_%s", name)
	cnf.Tendermint.ListenAddress = GetTCPLocalAddress()
	cnf.Tendermint.ExternalAddress = cnf.Tendermint.ListenAddress
	cnf.RPC.GRPC.ListenAddress = GetLocalAddress()
	cnf.RPC.Metrics.ListenAddress = GetTCPLocalAddress()
	cnf.RPC.TM.ListenAddress = GetTCPLocalAddress()
	cnf.Keys.RemoteAddress = ""
	return cnf
}