package main

import (
	"bytes"
	"context"
	"encoding/binary"
	"fmt"
	"io/ioutil"
	"testing"
	"time"

	"github.com/ethereum/go-ethereum"
	"github.com/ethereum/go-ethereum/common"
	ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1"
	"github.com/prysmaticlabs/go-ssz"
	contracts "github.com/prysmaticlabs/prysm/contracts/deposit-contract"
	"github.com/prysmaticlabs/prysm/shared/interop"
	"github.com/prysmaticlabs/prysm/shared/params"
	"github.com/prysmaticlabs/prysm/shared/testutil"
	"github.com/prysmaticlabs/prysm/shared/trieutil"
	"github.com/sirupsen/logrus"
)

func init() {
	logrus.SetLevel(logrus.DebugLevel)
	logrus.SetOutput(ioutil.Discard)
}

func sendDeposits(t *testing.T, testAcc *contracts.TestAccount,
	numberOfDeposits, numberOfValidators uint64) []*ethpb.Deposit {

	deposits := make([]*ethpb.Deposit, 0, numberOfValidators)
	depositDelay := int64(1)
	depositContractAddrStr := testAcc.ContractAddr.Hex()

	privKeys, pubKeys, err := interop.DeterministicallyGenerateKeys(0, numberOfValidators)
	if err != nil {
		t.Fatalf("Unable to generate keys: %v", err)
	}

	depositData, depositDataRoots, err := interop.DepositDataFromKeys(privKeys, pubKeys)
	if err != nil {
		t.Fatalf("Unable to generate deposit data from keys: %v", err)
	}

	for i, data := range depositData {
		dataRoot := [32]byte{}
		copy(dataRoot[:], depositDataRoots[i])

		pubKey := pubKeys[i]

		deposits = append(deposits, &ethpb.Deposit{
			Data: data,
		})

		for j := uint64(0); j < numberOfDeposits; j++ {
			tx, err := testAcc.Contract.Deposit(testAcc.TxOpts, data.PublicKey, data.WithdrawalCredentials, data.Signature, dataRoot)
			if err != nil {
				t.Fatalf("unable to send transaction to contract: %v", err)
			}

			testAcc.Backend.Commit()

			log.WithFields(logrus.Fields{
				"Transaction Hash": fmt.Sprintf("%#x", tx.Hash()),
			}).Infof("Deposit %d sent to contract address %v for validator with a public key %#x", j, depositContractAddrStr, pubKey.Marshal())

			time.Sleep(time.Duration(depositDelay) * time.Second)
		}
	}
	return deposits
}

func TestEndtoEndDeposits(t *testing.T) {
	testutil.ResetCache()
	testAcc, err := contracts.Setup()
	if err != nil {
		t.Fatalf("Unable to set up simulated backend %v", err)
	}

	testAcc.Backend.Commit()

	testAcc.TxOpts.Value = contracts.Amount32Eth()
	testAcc.TxOpts.GasLimit = 1000000

	numberOfValidators := uint64(2)
	numberOfDeposits := uint64(5)
	deposits := sendDeposits(t, testAcc, numberOfDeposits, numberOfValidators)

	query := ethereum.FilterQuery{
		Addresses: []common.Address{
			testAcc.ContractAddr,
		},
	}

	logs, err := testAcc.Backend.FilterLogs(context.Background(), query)
	if err != nil {
		t.Fatalf("Unable to retrieve logs %v", err)
	}
	if len(logs) == 0 {
		t.Fatal("no logs")
	}

	if len(logs) != int(numberOfDeposits*numberOfValidators) {
		t.Fatal("No sufficient number of logs")
	}

	j := 0
	for i, log := range logs {
		loggedPubkey, withCreds, _, loggedSig, index, err := contracts.UnpackDepositLogData(log.Data)
		if err != nil {
			t.Fatalf("Unable to unpack logs %v", err)
		}

		if binary.LittleEndian.Uint64(index) != uint64(i) {
			t.Errorf("Retrieved merkle tree index is incorrect %d", index)
		}

		if !bytes.Equal(loggedPubkey, deposits[j].Data.PublicKey) {
			t.Errorf("Pubkey is not the same as the data that was put in %v, i: %d", loggedPubkey, i)
		}

		if !bytes.Equal(loggedSig, deposits[j].Data.Signature) {
			t.Errorf("Proof of Possession is not the same as the data that was put in %v, i: %d", loggedSig, i)
		}

		if !bytes.Equal(withCreds, deposits[j].Data.WithdrawalCredentials) {
			t.Errorf("Withdrawal Credentials is not the same as the data that was put in %v, i: %d", withCreds, i)
		}

		if i == int(numberOfDeposits)-1 {
			j++
		}
	}

	encodedDeposits := make([][]byte, numberOfValidators*numberOfDeposits)
	for i := 0; i < int(numberOfValidators); i++ {
		hashedDeposit, err := ssz.HashTreeRoot(deposits[i].Data)
		if err != nil {
			t.Fatalf("could not tree hash deposit data: %v", err)
		}
		for j := 0; j < int(numberOfDeposits); j++ {
			encodedDeposits[i*int(numberOfDeposits)+j] = hashedDeposit[:]
		}
	}

	depositTrie, err := trieutil.GenerateTrieFromItems(encodedDeposits, int(params.BeaconConfig().DepositContractTreeDepth))
	if err != nil {
		t.Fatalf("Could not generate trie: %v", err)
	}

	root := depositTrie.Root()

	for i, encodedDeposit := range encodedDeposits {
		proof, err := depositTrie.MerkleProof(i)
		if err != nil {
			t.Fatalf("Could not generate proof: %v", err)
		}
		if ok := trieutil.VerifyMerkleBranch(
			root[:],
			encodedDeposit,
			i,
			proof,
		); !ok {
			t.Fatalf(
				"Unable verify deposit merkle branch of deposit root for root: %#x, encodedDeposit: %#x, i : %d",
				root[:], encodedDeposit, i)
		}
	}
}
