package core_test

import (
	"math/big"
	"testing"

	"git.marketally.com/tutus-one/tutus-chain/pkg/config"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/dao"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/interop"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/native"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/native/nativeids"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/native/nativenames"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/state"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/transaction"
	"git.marketally.com/tutus-one/tutus-chain/pkg/crypto/keys"
	"git.marketally.com/tutus-one/tutus-chain/pkg/tutustest"
	"git.marketally.com/tutus-one/tutus-chain/pkg/tutustest/chain"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract/callflag"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract/manifest"
	"git.marketally.com/tutus-one/tutus-chain/pkg/util"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/stackitem"
)

var (
	_ = (native.ILub)(&gas{})
	_ = (native.ITutus)(&neo{})
	_ = (native.IPolicy)(&policy{})
)

// A set of custom native contract stubs that are used to override default native
// contract implementations. These subs don't have any state and intentionally
// designed to be as simple as possible.
type (
	// gas is a custom native utility token contract implementation that doesn't track
	// any balance changes.
	gas struct {
		interop.ContractMD // obligatory field for proper Blockchain functioning.

		policy native.IPolicy // optional field that is presented here as an example of cross-native contract interaction.
	}
	// neo is a custom native governing token implementation that doesn't maintain any
	// balance changes and always use constant committee list.
	neo struct {
		interop.ContractMD

		validators keys.PublicKeys // optional field that is presented here as an example of state cache that may be required for contract functioning.
	}
	// policy is a custom native Policy contract implementation that doesn't have any
	// state and always use constant policies.
	policy struct {
		interop.ContractMD
	}
)

func newGAS() *gas {
	g := &gas{}
	defer g.BuildHFSpecificMD(nil) // obligatory call to properly initialize MD cache.

	g.ContractMD = *interop.NewContractMD( // obligatory call for proper allocate MD cache.
		nativenames.Lub, // obligatory name for proper Blockchain functioning.
		nativeids.Lub,   // obligatory ID even if some default native contracts are missing.
		func(m *manifest.Manifest, hf config.Hardfork) {
			m.SupportedStandards = []string{manifest.NEP17StandardName} // not really for this stub, but let's pretend to show how to setup standards.
		})

	desc := native.NewDescriptor("getInt", smartcontract.IntegerType,
		manifest.NewParameter("someBoolParam", smartcontract.BoolType))
	md := native.NewMethodAndPrice(g.getInt, 1<<10, callflag.ReadStates)
	g.AddMethod(md, desc)

	eDesc := native.NewEventDescriptor("MyPrettyEvent",
		manifest.NewParameter("pubkey", smartcontract.PublicKeyType),
		manifest.NewParameter("someBool", smartcontract.BoolType),
	)
	eMD := native.NewEvent(eDesc)
	g.AddEvent(eMD)

	return g
}
func (g *gas) Initialize(*interop.Context, *config.Hardfork, *interop.HFSpecificContractMD) error {
	return nil
}
func (g *gas) ActiveIn() *config.Hardfork { return nil }
func (g *gas) InitializeCache(isHardforkEnabled interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error {
	return nil
}
func (g *gas) Metadata() *interop.ContractMD {
	return &g.ContractMD
}
func (g *gas) OnPersist(*interop.Context) error {
	return nil
}
func (g *gas) PostPersist(*interop.Context) error {
	return nil
}
func (g *gas) BalanceOf(d *dao.Simple, acc util.Uint160) *big.Int {
	return big.NewInt(1)
}
func (g *gas) Burn(ic *interop.Context, h util.Uint160, amount *big.Int)                     {}
func (g *gas) Mint(ic *interop.Context, h util.Uint160, amount *big.Int, callOnPayment bool) {}
func (g *gas) getInt(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	// An example of how cross-native methods may be used:
	if g.policy.IsBlocked(ic.DAO, ic.Container.(*transaction.Transaction).Sender()) {
		panic("blocked sender")
	}

	return stackitem.NewBigInteger(big.NewInt(1))
}

func newNEO(validator *keys.PublicKey) *neo {
	n := &neo{
		validators: keys.PublicKeys{validator},
	}
	defer n.BuildHFSpecificMD(nil)

	n.ContractMD = *interop.NewContractMD(nativenames.Tutus, nativeids.Tutus)

	desc := native.NewDescriptor("getBool", smartcontract.BoolType)
	md := native.NewMethodAndPrice(n.getBool, 1<<10, callflag.ReadStates)
	n.AddMethod(md, desc)

	return n
}
func (n *neo) Initialize(*interop.Context, *config.Hardfork, *interop.HFSpecificContractMD) error {
	return nil
}
func (n *neo) ActiveIn() *config.Hardfork { return nil }
func (n *neo) InitializeCache(isHardforkEnabled interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error {
	return nil
}
func (n *neo) Metadata() *interop.ContractMD {
	return &n.ContractMD
}
func (n *neo) OnPersist(*interop.Context) error               { return nil }
func (n *neo) PostPersist(*interop.Context) error             { return nil }
func (n *neo) GetCommitteeAddress(d *dao.Simple) util.Uint160 { return util.Uint160{1, 2, 3} }
func (n *neo) GetNextBlockValidatorsInternal(d *dao.Simple) keys.PublicKeys {
	return n.validators
}
func (n *neo) BalanceOf(d *dao.Simple, acc util.Uint160) (*big.Int, uint32) {
	return big.NewInt(100500), 0
}
func (n *neo) CalculateBonus(ic *interop.Context, acc util.Uint160, endHeight uint32) (*big.Int, error) {
	return big.NewInt(5), nil
}
func (n *neo) GetCommitteeMembers(d *dao.Simple) keys.PublicKeys {
	return n.validators
}
func (n *neo) ComputeNextBlockValidators(d *dao.Simple) keys.PublicKeys {
	return n.validators
}
func (n *neo) GetCandidates(d *dao.Simple) ([]state.Validator, error) {
	return []state.Validator{
		{
			Key:   n.validators[0],
			Votes: big.NewInt(100),
		},
	}, nil
}
func (n *neo) CheckCommittee(ic *interop.Context) bool {
	return true
}
func (n *neo) getBool(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	return stackitem.NewBool(true)
}
func (n *neo) RevokeVotes(ic *interop.Context, acc util.Uint160) error { return nil }

func newPolicy() *policy {
	p := &policy{}
	defer p.BuildHFSpecificMD(nil)

	p.ContractMD = *interop.NewContractMD(nativenames.Policy, nativeids.PolicyContract)

	// Methods that are required for inter-contract interaction should follow below. It's
	// not obligatory to include all native.IPolicy methods to the contract manifest.
	desc := native.NewDescriptor("getTimePerBlock", smartcontract.IntegerType)
	md := native.NewMethodAndPrice(p.getTimePerBlock, 1<<10, callflag.ReadStates)
	p.AddMethod(md, desc)

	return p
}
func (p *policy) Initialize(*interop.Context, *config.Hardfork, *interop.HFSpecificContractMD) error {
	return nil
}
func (p *policy) ActiveIn() *config.Hardfork { return nil }
func (p *policy) InitializeCache(isHardforkEnabled interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error {
	return nil
}
func (p *policy) Metadata() *interop.ContractMD                                { return &p.ContractMD }
func (p *policy) OnPersist(*interop.Context) error                             { return nil }
func (p *policy) PostPersist(*interop.Context) error                           { return nil }
func (p *policy) GetStoragePriceInternal(d *dao.Simple) int64                  { return 5 }
func (p *policy) GetMaxVerificationGas(d *dao.Simple) int64                    { return 10_0000_0000 }
func (p *policy) GetExecFeeFactorInternal(d *dao.Simple) int64                 { return 1 }
func (p *policy) GetMaxTraceableBlocksInternal(d *dao.Simple) uint32           { return 100 }
func (p *policy) GetMillisecondsPerBlockInternal(d *dao.Simple) uint32         { return 1000 }
func (p *policy) GetMaxValidUntilBlockIncrementFromCache(d *dao.Simple) uint32 { return 2 }
func (p *policy) GetAttributeFeeInternal(d *dao.Simple, attrType transaction.AttrType) int64 {
	return 1
}
func (p *policy) CheckPolicy(d *dao.Simple, tx *transaction.Transaction) error      { return nil }
func (p *policy) GetFeePerByteInternal(d *dao.Simple) int64                         { return 1 }
func (p *policy) BlockAccountInternal(ic *interop.Context, hash util.Uint160) bool  { return false }
func (p *policy) IsBlocked(dao *dao.Simple, hash util.Uint160) bool                 { return false }
func (p *policy) WhitelistedFee(d *dao.Simple, hash util.Uint160, offset int) int64 { return -1 }
func (p *policy) CleanWhitelist(ic *interop.Context, cs *state.Contract) error      { return nil }
func (p *policy) GetMaxValidUntilBlockIncrementInternal(ic *interop.Context) uint32 { return 2 }
func (p *policy) getTimePerBlock(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	return stackitem.NewBigInteger(big.NewInt(1000))
}

func newCustomNatives(cfg config.ProtocolConfiguration) []interop.Contract {
	// Use default ContractManagement and Ledger implementations:
	mgmt := native.NewManagement()
	ledger := native.NewLedger()

	// Don't even create CryptoLib, StdLib, Notary, Oracle and Treasury contracts since they are useless.

	// Use custom GasToken, NeoToken and Policy implementations:
	g := newGAS()
	pk, _ := keys.NewPrivateKey()
	n := newNEO(pk.PublicKey())
	p := newPolicy()

	// An example of dependent native initialisation:
	g.policy = p

	// If default ContractManagement and Ledger implementations are used, then it's
	// mandatory to properly initialize their dependent natives:
	mgmt.Tutus = n
	mgmt.Policy = p
	ledger.Policy = p

	// Use default RoleManagement implementation (and properly initialize links to dependent natives):
	desig := native.NewDesignate(cfg.Genesis.Roles)
	desig.Tutus = n

	// If needed, some additional custom native may be added to the list of contracts. Use ID that
	// follows Notary contract ID.

	// Return the resulting list of native contracts that is different from the default one:
	return []interop.Contract{
		mgmt,
		ledger,
		n,
		g,
		p,
		desig,
	}
}

func TestBlockchain_CustomNatives(t *testing.T) {
	t.Skip("Custom natives not supported with Tutus-specific contracts - requires Vita and dependencies")
	// Create chain with some natives.
	bc, acc := chain.NewSingleWithOptions(t, &chain.Options{
		NewNatives: newCustomNatives,
	})

	e := tutustest.NewExecutor(t, bc, acc, acc)
	e.AddNewBlock(t)
}
