package crossdomain_test

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

	"github.com/ethereum-optimism/optimism/op-bindings/bindings"
	"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
	"github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
	"github.com/ethereum-optimism/optimism/op-chain-ops/state"
	"github.com/stretchr/testify/require"

	"github.com/ethereum/go-ethereum/accounts/abi/bind"
	"github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/common/hexutil"
	"github.com/ethereum/go-ethereum/core/vm"
	"github.com/ethereum/go-ethereum/crypto"
)

var (
	// testKey is the same test key that geth uses
	testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
	// chainID is the chain id used for simulated backends
	chainID = big.NewInt(1337)
	// testAccount represents the sender account for tests
	testAccount = crypto.PubkeyToAddress(testKey.PublicKey)
)

// sendMessageArgs represents the input to `SendMessage`. The value
// is excluded specifically here because we want to simulate v0 messages
// as closely as possible.
type sendMessageArgs struct {
	Target      common.Address
	Message     []byte
	MinGasLimit uint32
}

// setL1CrossDomainMessenger will set the L1CrossDomainMessenger into
// a state db that represents L1. It accepts a list of "successfulMessages"
// to be placed into the state. This allows for a subset of messages that
// were withdrawn on L2 to be placed into the L1 state to simulate
// a set of withdrawals that are not finalized on L1
func setL1CrossDomainMessenger(db vm.StateDB, successful []common.Hash) error {
	bytecode, err := bindings.GetDeployedBytecode("L1CrossDomainMessenger")
	if err != nil {
		return err
	}

	db.CreateAccount(predeploys.DevL1CrossDomainMessengerAddr)
	db.SetCode(predeploys.DevL1CrossDomainMessengerAddr, bytecode)

	msgs := make(map[any]any)
	for _, hash := range successful {
		msgs[hash] = true
	}

	return state.SetStorage(
		"L1CrossDomainMessenger",
		predeploys.DevL1CrossDomainMessengerAddr,
		state.StorageValues{
			"successfulMessages": msgs,
		},
		db,
	)
}

// setL2CrossDomainMessenger will set the L2CrossDomainMessenger into
// a state db that represents L2. It does not set any state as the only
// function called in this test is "sendMessage" which calls a hardcoded
// address that represents the L2ToL1MessagePasser
func setL2CrossDomainMessenger(db vm.StateDB) error {
	bytecode, err := bindings.GetDeployedBytecode("L2CrossDomainMessenger")
	if err != nil {
		return err
	}

	db.CreateAccount(predeploys.L2CrossDomainMessengerAddr)
	db.SetCode(predeploys.L2CrossDomainMessengerAddr, bytecode)

	return state.SetStorage(
		"L2CrossDomainMessenger",
		predeploys.L2CrossDomainMessengerAddr,
		state.StorageValues{
			"successfulMessages": map[any]any{},
		},
		db,
	)
}

// setL2ToL1MessagePasser will set the L2ToL1MessagePasser into a state
// db that represents L2. This must be set so the L2CrossDomainMessenger
// can call it as part of "sendMessage"
func setL2ToL1MessagePasser(db vm.StateDB) error {
	bytecode, err := bindings.GetDeployedBytecode("L2ToL1MessagePasser")
	if err != nil {
		return err
	}

	db.CreateAccount(predeploys.L2ToL1MessagePasserAddr)
	db.SetCode(predeploys.L2ToL1MessagePasserAddr, bytecode)

	return state.SetStorage(
		"L2ToL1MessagePasser",
		predeploys.L2ToL1MessagePasserAddr,
		state.StorageValues{},
		db,
	)

}

// sendCrossDomainMessage will send a L2 to L1 cross domain message.
// The state cannot just be set because logs must be generated by
// transaction execution
func sendCrossDomainMessage(
	l2xdm *bindings.L2CrossDomainMessenger,
	backend *backends.SimulatedBackend,
	message *sendMessageArgs,
	t *testing.T,
) *crossdomain.CrossDomainMessage {
	opts, err := bind.NewKeyedTransactorWithChainID(testKey, chainID)
	require.Nil(t, err)

	tx, err := l2xdm.SendMessage(opts, message.Target, message.Message, message.MinGasLimit)
	require.Nil(t, err)
	backend.Commit()

	receipt, err := backend.TransactionReceipt(context.Background(), tx.Hash())
	require.Nil(t, err)

	abi, _ := bindings.L2CrossDomainMessengerMetaData.GetAbi()
	var msg crossdomain.CrossDomainMessage

	// Ensure that we see the event so that a default CrossDomainMessage
	// is not returned
	seen := false

	// Assume there is only 1 deposit per transaction
	for _, log := range receipt.Logs {
		event, _ := abi.EventByID(log.Topics[0])
		// Not the event we are looking for
		if event == nil {
			continue
		}
		// Parse the legacy event
		if event.Name == "SentMessage" {
			e, _ := l2xdm.ParseSentMessage(*log)
			msg.Target = e.Target
			msg.Sender = e.Sender
			msg.Data = e.Message
			msg.Nonce = e.MessageNonce
			msg.GasLimit = e.GasLimit

			// Set seen to true to ensure that this event
			// was observed
			seen = true
		}
		// Parse the new extension event
		if event.Name == "SentMessageExtension1" {
			e, _ := l2xdm.ParseSentMessageExtension1(*log)
			msg.Value = e.Value
		}
	}

	require.True(t, seen)
	return &msg
}

// TestGetPendingWithdrawals tests the high level function used
// to fetch pending withdrawals
func TestGetPendingWithdrawals(t *testing.T) {
	// Create a L2 db
	L2db := state.NewMemoryStateDB(nil)
	// Set the test account and give it a large balance
	L2db.CreateAccount(testAccount)
	L2db.AddBalance(testAccount, big.NewInt(10000000000000000))
	// Set the L2ToL1MessagePasser in the L2 state
	err := setL2ToL1MessagePasser(L2db)
	require.Nil(t, err)
	// Set the L2CrossDomainMessenger in the L2 state
	err = setL2CrossDomainMessenger(L2db)
	require.Nil(t, err)

	L2 := backends.NewSimulatedBackend(
		L2db.Genesis().Alloc,
		15000000,
	)

	L2CrossDomainMessenger, err := bindings.NewL2CrossDomainMessenger(
		predeploys.L2CrossDomainMessengerAddr,
		L2,
	)
	require.Nil(t, err)

	// Create a set of test data that is made up of cross domain messages.
	// There is a total of 6 cross domain messages. 3 of them are set to be
	// finalized on L1 so 3 of them will be considered not finalized.
	msgs := []*sendMessageArgs{
		{
			Target:      common.Address{},
			Message:     []byte{},
			MinGasLimit: 0,
		},
		{
			Target:      common.Address{0x01},
			Message:     []byte{0x01},
			MinGasLimit: 0,
		},
		{
			Target:      common.Address{},
			Message:     []byte{},
			MinGasLimit: 100,
		},
		{
			Target:      common.Address{19: 0x01},
			Message:     []byte{0xaa, 0xbb},
			MinGasLimit: 10000,
		},
		{
			Target:      common.HexToAddress("0x4675C7e5BaAFBFFbca748158bEcBA61ef3b0a263"),
			Message:     hexutil.MustDecode("0x095ea7b3000000000000000000000000c92e8bdf79f0507f65a392b0ab4667716bfe01100000000000000000000000000000000000000000000000000000000000000000"),
			MinGasLimit: 50000,
		},
		{
			Target:      common.HexToAddress("0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5"),
			Message:     []byte{},
			MinGasLimit: 70511,
		},
	}

	// For each test cross domain message, call "sendMessage" on the
	// L2CrossDomainMessenger and compute the cross domain message hash
	hashes := make([]common.Hash, len(msgs))
	for i, msg := range msgs {
		sent := sendCrossDomainMessage(L2CrossDomainMessenger, L2, msg, t)
		hash, err := sent.Hash()
		require.Nil(t, err)
		hashes[i] = hash
	}

	// Create a L1 backend with a dev account
	L1db := state.NewMemoryStateDB(nil)
	L1db.CreateAccount(testAccount)
	L1db.AddBalance(testAccount, big.NewInt(10000000000000000))

	// Set the L1CrossDomainMessenger into the L1 state. Only set a subset
	// of the messages as finalized, the first 3.
	err = setL1CrossDomainMessenger(L1db, hashes[0:3])
	require.Nil(t, err)

	L1 := backends.NewSimulatedBackend(
		L1db.Genesis().Alloc,
		15000000,
	)

	backends := crossdomain.NewBackends(L1, L2)
	messengers, err := crossdomain.NewMessengers(backends, predeploys.DevL1CrossDomainMessengerAddr)
	require.Nil(t, err)

	// Fetch the pending withdrawals
	withdrawals, err := crossdomain.GetPendingWithdrawals(messengers, nil, 0, 100)
	require.Nil(t, err)

	// Since only half of the withdrawals were set as finalized on L1,
	// the number of pending withdrawals should be 3
	require.Equal(t, 3, len(withdrawals))

	// The final 3 test cross domain messages should be equal to the
	// fetched pending withdrawals. This shows that `GetPendingWithdrawals`
	// fetched the correct messages
	for i, msg := range msgs[3:] {
		withdrawal := withdrawals[i]

		require.Equal(t, msg.Target, withdrawal.XDomainTarget)
		require.Equal(t, msg.Message, []byte(withdrawal.XDomainData))
	}
}
