package mock

import (
	"crypto/ecdsa"
	"fmt"
	"math/big"
	"strings"

	"github.com/OffchainLabs/prysm/v7/contracts/deposit"
	"github.com/ethereum/go-ethereum/accounts/abi"
	"github.com/ethereum/go-ethereum/accounts/abi/bind"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/crypto"
	"github.com/ethereum/go-ethereum/ethclient/simulated"
)

var (
	amount32Eth        = "32000000000000000000"
	amountLessThan1Eth = "500000000000000000"
	depositContractBin = "[encoded data, 13268 characters]"
)

// TestAccount represents a test account in the simulated backend,
// through which we can perform actions on the eth1.0 chain.
type TestAccount struct {
	Addr         common.Address
	ContractAddr common.Address
	Contract     *deposit.DepositContract
	Backend      *simulated.Backend
	TxOpts       *bind.TransactOpts
}

// Setup creates the simulated backend with the deposit contract deployed
func Setup() (*TestAccount, error) {
	genesis := make(types.GenesisAlloc)
	privKey, err := crypto.GenerateKey()
	if err != nil {
		return nil, err
	}
	pubKeyECDSA, ok := privKey.Public().(*ecdsa.PublicKey)
	if !ok {
		return nil, fmt.Errorf("error casting public key to ECDSA")
	}

	// strip off the 0x and the first 2 characters 04 which is always the EC prefix and is not required.
	publicKeyBytes := crypto.FromECDSAPub(pubKeyECDSA)[4:]
	var pubKey = make([]byte, 48)
	copy(pubKey, publicKeyBytes)

	addr := crypto.PubkeyToAddress(privKey.PublicKey)
	txOpts, err := bind.NewKeyedTransactorWithChainID(privKey, big.NewInt(1337))
	if err != nil {
		return nil, err
	}
	startingBalance, _ := new(big.Int).SetString("100000000000000000000000000000000000000", 10)
	genesis[addr] = types.Account{Balance: startingBalance}
	backend := simulated.NewBackend(genesis, simulated.WithBlockGasLimit(210000000000))

	contractAddr, _, contract, err := DeployDepositContract(txOpts, backend.Client())
	if err != nil {
		return nil, err
	}
	backend.Commit()

	return &TestAccount{addr, contractAddr, contract, backend, txOpts}, nil
}

// Amount32Eth returns 32Eth(in wei) in terms of the big.Int type.
func Amount32Eth() *big.Int {
	amount, _ := new(big.Int).SetString(amount32Eth, 10)
	return amount
}

// LessThan1Eth returns less than 1 Eth(in wei) in terms of the big.Int type.
func LessThan1Eth() *big.Int {
	amount, _ := new(big.Int).SetString(amountLessThan1Eth, 10)
	return amount
}

// DeployDepositContract deploys a new Ethereum contract, binding an instance of DepositContract to it.
func DeployDepositContract(auth *bind.TransactOpts, backend bind.ContractBackend) (common.Address, *types.Transaction, *deposit.DepositContract, error) {
	parsed, err := abi.JSON(strings.NewReader(deposit.DepositContractABI))
	if err != nil {
		return common.Address{}, nil, nil, err
	}

	address, tx, contract, err := bind.DeployContract(auth, parsed, common.FromHex(depositContractBin), backend)
	if err != nil {
		return common.Address{}, nil, nil, err
	}
	return address, tx, &deposit.DepositContract{
		DepositContractCaller:     deposit.NewDepositContractCallerFromBoundContract(contract),
		DepositContractTransactor: deposit.NewDepositContractTransactorFromBoundContract(contract),
		DepositContractFilterer:   deposit.NewDepositContractFiltererFromBoundContract(contract),
	}, nil
}
