// Copyright 2024 The Erigon Authors
// 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 aura_test

import (
	"context"
	"math/big"
	"strings"
	"testing"

	"github.com/holiman/uint256"
	"github.com/stretchr/testify/require"

	"github.com/erigontech/erigon/common"
	"github.com/erigontech/erigon/common/empty"
	"github.com/erigontech/erigon/common/log/v3"
	"github.com/erigontech/erigon/db/datadir"
	"github.com/erigontech/erigon/db/kv/dbcfg"
	"github.com/erigontech/erigon/db/kv/memdb"
	"github.com/erigontech/erigon/db/snapshotsync/freezeblocks"
	"github.com/erigontech/erigon/execution/abi"
	"github.com/erigontech/erigon/execution/builder"
	"github.com/erigontech/erigon/execution/chain"
	chainspec "github.com/erigontech/erigon/execution/chain/spec"
	"github.com/erigontech/erigon/execution/commitment/trie"
	"github.com/erigontech/erigon/execution/execmodule/execmoduletester"
	"github.com/erigontech/erigon/execution/protocol"
	"github.com/erigontech/erigon/execution/protocol/params"
	"github.com/erigontech/erigon/execution/protocol/rules/aura"
	"github.com/erigontech/erigon/execution/stagedsync"
	"github.com/erigontech/erigon/execution/state"
	"github.com/erigontech/erigon/execution/state/genesiswrite"
	"github.com/erigontech/erigon/execution/tests/blockgen"
	"github.com/erigontech/erigon/execution/tests/testutil"
	"github.com/erigontech/erigon/execution/types"
	"github.com/erigontech/erigon/execution/types/accounts"
	"github.com/erigontech/erigon/node/rulesconfig"
)

// Check that the first block of Gnosis Chain, which doesn't have any transactions,
// does not change the state root.
func TestEmptyBlock(t *testing.T) {
	require := require.New(t)
	genesis := chainspec.GnosisGenesisBlock()
	genesisBlock, _, err := genesiswrite.GenesisToBlock(t, genesis, datadir.New(t.TempDir()), log.Root())
	require.NoError(err)

	genesis.Config.TerminalTotalDifficultyPassed = false

	chainConfig := genesis.Config
	auraDB := memdb.NewTestDB(t, dbcfg.ChainDB)
	engine, err := aura.NewAuRa(chainConfig.Aura, auraDB)
	require.NoError(err)
	m := execmoduletester.New(t, execmoduletester.WithGenesisSpec(genesis), execmoduletester.WithEngine(engine))

	time := uint64(1539016985)
	header := builder.MakeEmptyHeader(genesisBlock.Header(), chainConfig, time, nil)
	header.UncleHash = empty.UncleHash
	header.TxHash = trie.EmptyRoot
	header.ReceiptHash = trie.EmptyRoot
	header.Coinbase = common.HexToAddress("0xcace5b3c29211740e595850e80478416ee77ca21")
	header.Difficulty = engine.CalcDifficulty(nil, time,
		0,
		genesisBlock.Difficulty(),
		genesisBlock.NumberU64(),
		genesisBlock.Hash(),
		genesisBlock.UncleHash(),
		genesisBlock.Header().AuRaStep,
	)

	block := types.NewBlockWithHeader(header)

	headers, blocks, receipts := make([]*types.Header, 1), make(types.Blocks, 1), make([]types.Receipts, 1)
	headers[0] = header
	blocks[0] = block

	chain := &blockgen.ChainPack{Headers: headers, Blocks: blocks, Receipts: receipts, TopBlock: block}
	err = m.InsertChain(chain)
	require.NoError(err)
}

func TestAuRaSkipGasLimit(t *testing.T) {
	require := require.New(t)
	genesis := chainspec.GnosisGenesisBlock()
	genesis.Config.TerminalTotalDifficultyPassed = false
	genesis.Config.Aura.BlockGasLimitContractTransitions = map[uint64]common.Address{0: common.HexToAddress("0x4000000000000000000000000000000000000001")}

	chainConfig := genesis.Config
	auraDB := memdb.NewTestDB(t, dbcfg.ChainDB)
	engine, err := aura.NewAuRa(chainConfig.Aura, auraDB)
	require.NoError(err)
	m := execmoduletester.New(t, execmoduletester.WithGenesisSpec(genesis), execmoduletester.WithEngine(engine))

	difficulty := uint256.MustFromDecimal("340282366920938463463374607431768211454")
	//Populate a sample valid header for a Pre-merge block
	// - actually sampled from 5000th block in chiado
	validPreMergeHeader := &types.Header{
		ParentHash:  common.HexToHash("0x102482332de853f2f8967263e77e71d4fddf68fd5d84b750b2ddb7e501052097"),
		UncleHash:   common.HexToHash("0x0"),
		Coinbase:    common.HexToAddress("0x14747a698Ec1227e6753026C08B29b4d5D3bC484"),
		Root:        common.HexToHash("0x0"),
		TxHash:      common.HexToHash("0x0"),
		ReceiptHash: common.HexToHash("0x0"),
		Bloom:       types.BytesToBloom(nil),
		Difficulty:  *difficulty,
		Number:      *uint256.NewInt(5000),
		GasLimit:    12500000,
		GasUsed:     0,
		Time:        1664049551,
		Extra:       []byte{},
		Nonce:       [8]byte{0, 0, 0, 0, 0, 0, 0, 0},
	}

	syscallCustom := func(accounts.Address, []byte, *state.IntraBlockState, *types.Header, bool) ([]byte, error) {
		//Packing as constructor gives the same effect as unpacking the returned value
		json := `[{"inputs": [{"internalType": "uint256","name": "blockGasLimit","type": "uint256"}],"stateMutability": "nonpayable","type": "constructor"}]`
		fakeAbi, err := abi.JSON(strings.NewReader(json))
		require.NoError(err)

		fakeVal, err := fakeAbi.Pack("", big.NewInt(12500000))
		return fakeVal, err
	}
	require.NoError(m.Engine.Initialize(chainConfig, &blockgen.FakeChainReader{}, validPreMergeHeader, nil, syscallCustom, nil, nil))

	invalidPreMergeHeader := validPreMergeHeader
	invalidPreMergeHeader.GasLimit = 12_123456 //a different, wrong gasLimit
	require.Error(m.Engine.Initialize(chainConfig, &blockgen.FakeChainReader{}, invalidPreMergeHeader, nil, syscallCustom, nil, nil))

	invalidPostMergeHeader := invalidPreMergeHeader
	invalidPostMergeHeader.Difficulty.Clear() //zero difficulty detected as PoS
	require.NoError(m.Engine.Initialize(chainConfig, &blockgen.FakeChainReader{}, invalidPostMergeHeader, nil, syscallCustom, nil, nil))
}

func TestEmptySystemAccountCreation(t *testing.T) {
	require := require.New(t)
	genesis := chainspec.ChiadoGenesisBlock()
	ctx := context.Background()
	logger := log.New()
	dirs := datadir.New(t.TempDir())
	db := testutil.TemporalDBWithDirs(t, dirs)
	tx, domains := testutil.TemporalTxSD(t, db)

	// Replay genesis block the same way exec3 does (see txtask.go):
	// GenesisToBlock populates an IntraBlockState with the alloc,
	// then MakeWriteSet writes it into the real shared domains.
	genesisBlock, genesisIbs, err := genesiswrite.GenesisToBlock(t, genesis, dirs, logger)
	require.NoError(err)
	domainWriter := state.NewWriter(domains.AsPutDel(tx), nil, 1)
	err = genesisIbs.MakeWriteSet(&chain.Rules{}, domainWriter)
	require.NoError(err)

	// Write block headers so ChainReader can look up the genesis header.
	err = genesiswrite.WriteGenesisBesideState(genesisBlock, tx, genesis)
	require.NoError(err)

	config := genesis.Config
	blockReader := freezeblocks.NewBlockReader(db.(freezeblocks.HasBlockFiles).DebugBlockFiles(), nil)
	chainRdr := stagedsync.ChainReader{Cfg: config, Db: tx, BlockReader: blockReader}
	engine := rulesconfig.CreateRulesEngineBareBones(ctx, config, logger)
	time := uint64(1)
	header := builder.MakeEmptyHeader(genesisBlock.Header(), config, time, nil)
	header.Difficulty = engine.CalcDifficulty(chainRdr, time,
		0,
		genesisBlock.Difficulty(),
		genesisBlock.NumberU64(),
		genesisBlock.Hash(),
		genesisBlock.UncleHash(),
		genesisBlock.Header().AuRaStep,
	)
	header.GasLimit = 12500000

	// Set up block 1 state the same way exec3 does for TxIndex == -1:
	// engine.Initialize + FinalizeTx via InitializeBlockExecution.
	rs := state.NewStateV3Buffered(state.NewStateV3(domains, false, logger))
	reader := state.NewBufferedReader(rs, state.NewReaderV3(rs.Domains().AsGetter(tx)))
	writer := state.NewVersionedWriteCollector(rs)
	ibs := state.New(reader)
	defer ibs.Release(false)
	err = protocol.InitializeBlockExecution(engine, chainRdr, header, config, ibs, writer, logger, nil)
	require.NoError(err)
	account, err := reader.ReadAccountData(params.SystemAddress)
	require.NoError(err)
	require.NotNil(account)
	require.True(account.Empty())
}
