// Copyright 2015 The go-ethereum Authors
// (original work)
// Copyright 2024 The Erigon Authors
// (modifications)
// This file is part of Erigon.
//
// Erigon is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Erigon is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with Erigon. If not, see <http://www.gnu.org/licenses/>.

package blockgen_test

import (
	"math/big"
	"testing"

	"github.com/holiman/uint256"

	"github.com/erigontech/erigon/common"
	"github.com/erigontech/erigon/common/crypto"
	"github.com/erigontech/erigon/common/log/v3"
	"github.com/erigontech/erigon/execution/chain"
	"github.com/erigontech/erigon/execution/execmodule/execmoduletester"
	"github.com/erigontech/erigon/execution/protocol/params"
	"github.com/erigontech/erigon/execution/rlp"
	"github.com/erigontech/erigon/execution/state"
	"github.com/erigontech/erigon/execution/tests/blockgen"
	"github.com/erigontech/erigon/execution/types"
	"github.com/erigontech/erigon/execution/types/accounts"
	"github.com/erigontech/erigon/node/gointerfaces/sentryproto"
	"github.com/erigontech/erigon/p2p/protocols/eth"
)

func TestGenerateChain(t *testing.T) {
	t.Parallel()
	var (
		key1, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
		key2, _ = crypto.HexToECDSA("8a1f9a8f95be41cd7ccb6168179afb4504aefe388d1e14474d32c45c72ce7b7a")
		key3, _ = crypto.HexToECDSA("49a7b37aa6f6645917e7b807e9d1c00d4fa71f18343b0d4122a4d2df64dd6fee")
		addr1   = crypto.PubkeyToAddress(key1.PublicKey)
		addr2   = crypto.PubkeyToAddress(key2.PublicKey)
		addr3   = crypto.PubkeyToAddress(key3.PublicKey)
	)

	h := log.Root().GetHandler()
	defer func() {
		log.Root().SetHandler(h)
	}()
	log.Root().SetHandler(log.DiscardHandler())

	// Ensure that key1 has some funds in the genesis block.
	gspec := &types.Genesis{
		Config:     &chain.Config{HomesteadBlock: new(big.Int), ChainID: big.NewInt(1)},
		Difficulty: uint256.NewInt(0),
		Alloc:      types.GenesisAlloc{addr1: {Balance: big.NewInt(1000000)}},
	}
	m := execmoduletester.New(t, execmoduletester.WithGenesisSpec(gspec), execmoduletester.WithKey(key1))

	// This call generates a chain of 5 blocks. The function runs for
	// each block and adds different features to gen based on the
	// block index.
	signer := types.LatestSignerForChainID(nil)
	chain, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 5, func(i int, gen *blockgen.BlockGen) {
		switch i {
		case 0:
			// In block 1, addr1 sends addr2 some ether.
			tx, _ := types.SignTx(types.NewTransaction(gen.TxNonce(addr1), addr2, uint256.NewInt(10000), params.TxGas, nil, nil), *signer, key1)
			gen.AddTx(tx)
		case 1:
			// In block 2, addr1 sends some more ether to addr2.
			// addr2 passes it on to addr3.
			tx1, _ := types.SignTx(types.NewTransaction(gen.TxNonce(addr1), addr2, uint256.NewInt(1000), params.TxGas, nil, nil), *signer, key1)
			tx2, _ := types.SignTx(types.NewTransaction(gen.TxNonce(addr2), addr3, uint256.NewInt(1000), params.TxGas, nil, nil), *signer, key2)
			gen.AddTx(tx1)
			gen.AddTx(tx2)
		case 2:
			// Block 3 is empty but was mined by addr3.
			gen.SetCoinbase(addr3)
			gen.SetExtra([]byte("yeehaw"))
		case 3:
			// Block 4 includes blocks 2 and 3 as uncle headers (with modified extra data).
			b2 := gen.PrevBlock(1).Header()
			b2.Extra = []byte("foo")
			gen.AddUncle(b2)
			b3 := gen.PrevBlock(2).Header()
			b3.Extra = []byte("foo")
			gen.AddUncle(b3)
		}
	})
	if err != nil {
		t.Errorf("generate chain: %v\n", err)
	}

	// Import the chain. This runs all block validation rules.
	if err := m.InsertChain(chain); err != nil {
		t.Errorf("insert error%v\n", err)
	}

	tx, err := m.DB.BeginTemporalRw(m.Ctx)
	if err != nil {
		t.Errorf("beginro error%v\n", err)
	}
	defer tx.Rollback()

	st := state.New(m.NewStateReader(tx))

	if m.Current(tx).NumberU64() != 5 {
		t.Errorf("wrong block number: %d", m.Current(tx).Number())
	}
	balance, err := st.GetBalance(accounts.InternAddress(addr1))
	if err != nil {
		t.Error(err)
	}
	if !uint256.NewInt(989000).Eq(&balance) {
		t.Errorf("wrong balance of addr1: %s", &balance)
	}
	balance, err = st.GetBalance(accounts.InternAddress(addr2))
	if err != nil {
		t.Error(err)
	}
	if !uint256.NewInt(10000).Eq(&balance) {
		t.Errorf("wrong balance of addr2: %s", &balance)
	}
	balance, err = st.GetBalance(accounts.InternAddress(addr3))
	if err != nil {
		t.Error(err)
	}
	if balance.String() != "19687500000000001000" { //nolint
		t.Errorf("wrong balance of addr3: %s", &balance)
	}

	// Test of receipts
	hashPacket := make([]common.Hash, 0, len(chain.Blocks))
	for _, block := range chain.Blocks {
		hashPacket = append(hashPacket, block.Hash())
	}

	b, err := rlp.EncodeToBytes(&eth.GetReceiptsPacket66{
		RequestId:         1,
		GetReceiptsPacket: hashPacket,
	})
	if err != nil {
		t.Fatal(err)
	}

	m.ReceiveWg.Add(1)
	for _, err = range m.Send(&sentryproto.InboundMessage{Id: sentryproto.MessageId_GET_RECEIPTS_66, Data: b, PeerId: m.PeerId}) {
		if err != nil {
			t.Fatal(err)
		}
	}

	m.ReceiveWg.Wait()

	msg, err := m.SentMessage(0)
	if err != nil {
		t.Fatal(err)
	}

	if sentryproto.MessageId_RECEIPTS_66 != msg.Id {
		t.Errorf("receipt id %d do not match the expected id %d", msg.Id, sentryproto.MessageId_RECEIPTS_66)
	}

	// Decode the response and verify receipt counts per block (structure test).
	// Block 1 has 1 tx, block 2 has 2 txs, blocks 3-5 have no txs with receipts.
	var resp eth.ReceiptsRLPPacket66
	if err = rlp.DecodeBytes(msg.GetData(), &resp); err != nil {
		t.Fatalf("failed to decode receipts response: %v", err)
	}
	if resp.RequestId != 1 {
		t.Errorf("wrong request id: got %d, want 1", resp.RequestId)
	}
	if len(resp.ReceiptsRLPPacket) != 5 {
		t.Fatalf("expected 5 receipt entries, got %d", len(resp.ReceiptsRLPPacket))
	}
	var block1Receipts, block2Receipts types.Receipts
	if err = rlp.DecodeBytes(resp.ReceiptsRLPPacket[0], &block1Receipts); err != nil {
		t.Fatalf("failed to decode block 1 receipts: %v", err)
	}
	if err = rlp.DecodeBytes(resp.ReceiptsRLPPacket[1], &block2Receipts); err != nil {
		t.Fatalf("failed to decode block 2 receipts: %v", err)
	}
	if len(block1Receipts) != 1 {
		t.Errorf("block 1: expected 1 receipt, got %d", len(block1Receipts))
	}
	if len(block2Receipts) != 2 {
		t.Errorf("block 2: expected 2 receipts, got %d", len(block2Receipts))
	}
	if len(block1Receipts) > 0 && block1Receipts[0].CumulativeGasUsed != 21000 {
		t.Errorf("block 1 receipt cumulative gas: got %d, want 21000", block1Receipts[0].CumulativeGasUsed)
	}
	if len(block2Receipts) > 1 && block2Receipts[1].CumulativeGasUsed != 42000 {
		t.Errorf("block 2 receipt 2 cumulative gas: got %d, want 42000", block2Receipts[1].CumulativeGasUsed)
	}

}
