package core

import (
	"fmt"
	"math/big"
	"testing"

	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/crypto"
)

// callmsg is the message type used for call transactions in the private state test
type callmsg struct {
	addr       common.Address
	to         *common.Address
	gas        uint64
	gasPrice   *big.Int
	value      *big.Int
	data       []byte
	accessList types.AccessList // EIP-2930 access list.
}

// accessor boilerplate to implement core.Message
func (m callmsg) From() common.Address         { return m.addr }
func (m callmsg) FromFrontier() common.Address { return m.addr }
func (m callmsg) Nonce() uint64                { return 0 }
func (m callmsg) To() *common.Address          { return m.to }
func (m callmsg) GasPrice() *big.Int           { return m.gasPrice }
func (m callmsg) Gas() uint64                  { return m.gas }
func (m callmsg) Value() *big.Int              { return m.value }
func (m callmsg) Data() []byte                 { return m.data }
func (m callmsg) CheckNonce() bool             { return true }
func (m callmsg) AccessList() types.AccessList { return m.accessList }

func ExampleMakeCallHelper() {
	var (
		// setup new pair of keys for the calls
		key, _ = crypto.GenerateKey()
		// create a new helper
		helper = MakeCallHelper()
	)
	// Private contract address
	prvContractAddr := common.Address{1}
	// Initialise custom code for private contract
	helper.PrivateState.SetCode(prvContractAddr, common.Hex2Bytes("600a60005500"))
	// Public contract address
	pubContractAddr := common.Address{2}
	// Initialise custom code for public contract
	helper.PublicState.SetCode(pubContractAddr, common.Hex2Bytes("601460005500"))

	// Make a call to the private contract
	err := helper.MakeCall(true, key, prvContractAddr, nil)
	if err != nil {
		fmt.Println(err)
	}
	// Make a call to the public contract
	err = helper.MakeCall(false, key, pubContractAddr, nil)
	if err != nil {
		fmt.Println(err)
	}

	// Output:
	// Private: 10
	// Public: 20
	fmt.Println("Private:", helper.PrivateState.GetState(prvContractAddr, common.Hash{}).Big())
	fmt.Println("Public:", helper.PublicState.GetState(pubContractAddr, common.Hash{}).Big())
}

// 600a600055600060006001a1
// 60 0a, 60 00, 55,  60 00, 60 00, 60 01,  a1
// [1] (0x60) PUSH1 0x0a (store value)
// [3] (0x60) PUSH1 0x00 (store addr)
// [4] (0x55) SSTORE  (Store (k-00,v-a))

// [6] (0x60) PUSH1 0x00
// [8] (0x60) PUSH1 0x00
// [10](0x60) PUSH1 0x01
// [11](0xa1) LOG1 offset(0x01), len(0x00), topic(0x00)
//
// Store then log
func TestPrivateTransaction(t *testing.T) {
	var (
		key, _       = crypto.GenerateKey()
		helper       = MakeCallHelper()
		privateState = helper.PrivateState
		publicState  = helper.PublicState
	)

	prvContractAddr := common.Address{1}
	pubContractAddr := common.Address{2}
	// SSTORE (K,V) SSTORE(0, 10): 600a600055
	// +
	// LOG1 OFFSET LEN TOPIC,  LOG1 (a1) 01, 00, 00: 600060006001a1
	privateState.SetCode(prvContractAddr, common.Hex2Bytes("600a600055600060006001a1"))
	// SSTORE (K,V) SSTORE(0, 14): 6014600055
	publicState.SetCode(pubContractAddr, common.Hex2Bytes("6014600055"))

	if publicState.Exist(prvContractAddr) {
		t.Error("didn't expect private contract address to exist on public state")
	}

	// Private transaction 1
	err := helper.MakeCall(true, key, prvContractAddr, nil)

	if err != nil {
		t.Fatal(err)
	}
	stateEntry := privateState.GetState(prvContractAddr, common.Hash{}).Big()
	if stateEntry.Cmp(big.NewInt(10)) != 0 {
		t.Error("expected state to have 10, got", stateEntry)
	}
	if len(privateState.Logs()) != 1 {
		t.Error("expected private state to have 1 log, got", len(privateState.Logs()))
	}
	if len(publicState.Logs()) != 0 {
		t.Error("expected public state to have 0 logs, got", len(publicState.Logs()))
	}
	if publicState.Exist(prvContractAddr) {
		t.Error("didn't expect private contract address to exist on public state")
	}
	if !privateState.Exist(prvContractAddr) {
		t.Error("expected private contract address to exist on private state")
	}

	// Public transaction 1
	err = helper.MakeCall(false, key, pubContractAddr, nil)
	if err != nil {
		t.Fatal(err)
	}
	stateEntry = publicState.GetState(pubContractAddr, common.Hash{}).Big()
	if stateEntry.Cmp(big.NewInt(20)) != 0 {
		t.Error("expected state to have 20, got", stateEntry)
	}

	// Private transaction 2
	helper.MakeCall(true, key, prvContractAddr, nil)
	stateEntry = privateState.GetState(prvContractAddr, common.Hash{}).Big()
	if stateEntry.Cmp(big.NewInt(10)) != 0 {
		t.Error("expected state to have 10, got", stateEntry)
	}

	if publicState.Exist(prvContractAddr) {
		t.Error("didn't expect private contract address to exist on public state")
	}
	if privateState.Exist(pubContractAddr) {
		t.Error("didn't expect public contract address to exist on private state")
	}
}
