package watcher

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

	"github.com/agiledragon/gomonkey/v2"
	"github.com/scroll-tech/go-ethereum"
	"github.com/scroll-tech/go-ethereum/accounts/abi"
	"github.com/scroll-tech/go-ethereum/common"
	"github.com/scroll-tech/go-ethereum/core/types"
	"github.com/scroll-tech/go-ethereum/ethclient"
	"github.com/scroll-tech/go-ethereum/rpc"
	"github.com/smartystreets/goconvey/convey"
	"github.com/stretchr/testify/assert"
	"gorm.io/gorm"

	"scroll-tech/common/database"
	commonTypes "scroll-tech/common/types"

	bridgeAbi "scroll-tech/rollup/abi"
	"scroll-tech/rollup/internal/orm"
	"scroll-tech/rollup/internal/utils"
)

func setupL1Watcher(t *testing.T) (*L1WatcherClient, *gorm.DB) {
	db := setupDB(t)
	client, err := testApps.GetL1GethClient()
	assert.NoError(t, err)
	l1Cfg := cfg.L1Config
	watcher := NewL1WatcherClient(context.Background(), client, l1Cfg.StartHeight, l1Cfg.Confirmations, l1Cfg.L1MessageQueueAddress, l1Cfg.RelayerConfig.RollupContractAddress, db, nil)
	assert.NoError(t, watcher.FetchContractEvent())
	return watcher, db
}

func testFetchContractEvent(t *testing.T) {
	watcher, db := setupL1Watcher(t)
	defer database.CloseDB(db)
	assert.NoError(t, watcher.FetchContractEvent())
}

func testL1WatcherClientFetchBlockHeader(t *testing.T) {
	watcher, db := setupL1Watcher(t)
	defer database.CloseDB(db)
	convey.Convey("test toBlock < fromBlock", t, func() {
		var blockHeight uint64
		if watcher.ProcessedBlockHeight() <= 0 {
			blockHeight = 0
		} else {
			blockHeight = watcher.ProcessedBlockHeight() - 1
		}
		err := watcher.FetchBlockHeader(blockHeight)
		assert.NoError(t, err)
	})

	convey.Convey("test get header from client error", t, func() {
		var c *ethclient.Client
		patchGuard := gomonkey.ApplyMethodFunc(c, "HeaderByNumber", func(ctx context.Context, height *big.Int) (*types.Header, error) {
			return nil, ethereum.NotFound
		})
		defer patchGuard.Reset()

		var blockHeight uint64 = 10
		err := watcher.FetchBlockHeader(blockHeight)
		assert.Error(t, err)
	})

	var l1BlockOrm *orm.L1Block
	convey.Convey("insert l1 block error", t, func() {
		var c *ethclient.Client
		patchGuard := gomonkey.ApplyMethodFunc(c, "HeaderByNumber", func(ctx context.Context, height *big.Int) (*types.Header, error) {
			if height == nil {
				height = big.NewInt(100)
			}
			t.Log(height)
			return &types.Header{
				BaseFee: big.NewInt(100),
			}, nil
		})
		defer patchGuard.Reset()

		patchGuard.ApplyMethodFunc(l1BlockOrm, "InsertL1Blocks", func(ctx context.Context, blocks []orm.L1Block) error {
			return errors.New("insert failed")
		})

		var blockHeight uint64 = 10
		err := watcher.FetchBlockHeader(blockHeight)
		assert.Error(t, err)
	})

	convey.Convey("fetch block header success", t, func() {
		var c *ethclient.Client
		patchGuard := gomonkey.ApplyMethodFunc(c, "HeaderByNumber", func(ctx context.Context, height *big.Int) (*types.Header, error) {
			if height == nil {
				height = big.NewInt(100)
			}
			t.Log(height)
			return &types.Header{
				BaseFee: big.NewInt(100),
			}, nil
		})
		defer patchGuard.Reset()

		patchGuard.ApplyMethodFunc(l1BlockOrm, "InsertL1Blocks", func(ctx context.Context, blocks []orm.L1Block) error {
			return nil
		})

		var blockHeight uint64 = 10
		err := watcher.FetchBlockHeader(blockHeight)
		assert.NoError(t, err)
	})
}

func testL1WatcherClientFetchContractEvent(t *testing.T) {
	watcher, db := setupL1Watcher(t)
	defer database.CloseDB(db)

	watcher.SetConfirmations(rpc.SafeBlockNumber)
	convey.Convey("get latest confirmed block number failure", t, func() {
		var c *ethclient.Client
		patchGuard := gomonkey.ApplyMethodFunc(c, "HeaderByNumber", func(ctx context.Context, height *big.Int) (*types.Header, error) {
			return nil, ethereum.NotFound
		})
		defer patchGuard.Reset()
		err := watcher.FetchContractEvent()
		assert.Error(t, err)
	})

	var c *ethclient.Client
	patchGuard := gomonkey.ApplyMethodFunc(c, "HeaderByNumber", func(ctx context.Context, height *big.Int) (*types.Header, error) {
		if height == nil {
			height = big.NewInt(100)
		}
		t.Log(height)
		return &types.Header{
			Number:  big.NewInt(100),
			BaseFee: big.NewInt(100),
		}, nil
	})
	defer patchGuard.Reset()

	convey.Convey("filter logs failure", t, func() {
		targetErr := errors.New("call filter failure")
		patchGuard.ApplyMethodFunc(c, "FilterLogs", func(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) {
			return nil, targetErr
		})
		err := watcher.FetchContractEvent()
		assert.EqualError(t, err, targetErr.Error())
	})

	patchGuard.ApplyMethodFunc(c, "FilterLogs", func(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) {
		return []types.Log{
			{
				Address: common.HexToAddress("0x0000000000000000000000000000000000000000"),
			},
		}, nil
	})

	convey.Convey("parse bridge event logs failure", t, func() {
		targetErr := errors.New("parse log failure")
		patchGuard.ApplyPrivateMethod(watcher, "parseBridgeEventLogs", func(*L1WatcherClient, []types.Log) ([]*orm.L1Message, []rollupEvent, error) {
			return nil, nil, targetErr
		})
		err := watcher.FetchContractEvent()
		assert.EqualError(t, err, targetErr.Error())
	})

	patchGuard.ApplyPrivateMethod(watcher, "parseBridgeEventLogs", func(*L1WatcherClient, []types.Log) ([]*orm.L1Message, []rollupEvent, error) {
		rollupEvents := []rollupEvent{
			{
				batchHash: common.HexToHash("0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"),
				txHash:    common.HexToHash("0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5"),
				status:    commonTypes.RollupFinalized,
			},
			{
				batchHash: common.HexToHash("0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5"),
				txHash:    common.HexToHash("0xb4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30"),
				status:    commonTypes.RollupCommitted,
			},
		}
		return nil, rollupEvents, nil
	})

	var batchOrm *orm.Batch
	convey.Convey("db get rollup status by hash list failure", t, func() {
		targetErr := errors.New("get db failure")
		patchGuard.ApplyMethodFunc(batchOrm, "GetRollupStatusByHashList", func(context.Context, []string) ([]commonTypes.RollupStatus, error) {
			return nil, targetErr
		})
		err := watcher.FetchContractEvent()
		assert.Equal(t, err.Error(), targetErr.Error())
	})

	convey.Convey("rollup status mismatch batch hashes length", t, func() {
		patchGuard.ApplyMethodFunc(batchOrm, "GetRollupStatusByHashList", func(context.Context, []string) ([]commonTypes.RollupStatus, error) {
			s := []commonTypes.RollupStatus{
				commonTypes.RollupFinalized,
			}
			return s, nil
		})
		err := watcher.FetchContractEvent()
		assert.NoError(t, err)
	})

	patchGuard.ApplyMethodFunc(batchOrm, "GetRollupStatusByHashList", func(context.Context, []string) ([]commonTypes.RollupStatus, error) {
		s := []commonTypes.RollupStatus{
			commonTypes.RollupPending,
			commonTypes.RollupCommitting,
		}
		return s, nil
	})

	convey.Convey("db update RollupFinalized status failure", t, func() {
		targetErr := errors.New("UpdateFinalizeTxHashAndRollupStatus RollupFinalized failure")
		patchGuard.ApplyMethodFunc(batchOrm, "UpdateFinalizeTxHashAndRollupStatus", func(context.Context, string, string, commonTypes.RollupStatus) error {
			return targetErr
		})
		err := watcher.FetchContractEvent()
		assert.Equal(t, targetErr.Error(), err.Error())
	})

	patchGuard.ApplyMethodFunc(batchOrm, "UpdateFinalizeTxHashAndRollupStatus", func(context.Context, string, string, commonTypes.RollupStatus) error {
		return nil
	})

	convey.Convey("db update RollupCommitted status failure", t, func() {
		targetErr := errors.New("UpdateCommitTxHashAndRollupStatus RollupCommitted failure")
		patchGuard.ApplyMethodFunc(batchOrm, "UpdateCommitTxHashAndRollupStatus", func(context.Context, string, string, commonTypes.RollupStatus) error {
			return targetErr
		})
		err := watcher.FetchContractEvent()
		assert.Equal(t, targetErr.Error(), err.Error())
	})

	patchGuard.ApplyMethodFunc(batchOrm, "UpdateCommitTxHashAndRollupStatus", func(context.Context, string, string, commonTypes.RollupStatus) error {
		return nil
	})

	var l1MessageOrm *orm.L1Message
	convey.Convey("db save l1 message failure", t, func() {
		targetErr := errors.New("SaveL1Messages failure")
		patchGuard.ApplyMethodFunc(l1MessageOrm, "SaveL1Messages", func(context.Context, []*orm.L1Message) error {
			return targetErr
		})
		err := watcher.FetchContractEvent()
		assert.Equal(t, targetErr.Error(), err.Error())
	})

	patchGuard.ApplyMethodFunc(l1MessageOrm, "SaveL1Messages", func(context.Context, []*orm.L1Message) error {
		return nil
	})

	convey.Convey("FetchContractEvent success", t, func() {
		err := watcher.FetchContractEvent()
		assert.NoError(t, err)
	})
}

func testParseBridgeEventLogsL1QueueTransactionEventSignature(t *testing.T) {
	watcher, db := setupL1Watcher(t)
	defer database.CloseDB(db)

	logs := []types.Log{
		{
			Topics:      []common.Hash{bridgeAbi.L1QueueTransactionEventSignature},
			BlockNumber: 100,
			TxHash:      common.HexToHash("0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"),
		},
	}

	convey.Convey("unpack QueueTransaction log failure", t, func() {
		targetErr := errors.New("UnpackLog QueueTransaction failure")
		patchGuard := gomonkey.ApplyFunc(utils.UnpackLog, func(c *abi.ABI, out interface{}, event string, log types.Log) error {
			return targetErr
		})
		defer patchGuard.Reset()

		l2Messages, rollupEvents, err := watcher.parseBridgeEventLogs(logs)
		assert.EqualError(t, err, targetErr.Error())
		assert.Empty(t, l2Messages)
		assert.Empty(t, rollupEvents)
	})

	convey.Convey("L1QueueTransactionEventSignature success", t, func() {
		patchGuard := gomonkey.ApplyFunc(utils.UnpackLog, func(c *abi.ABI, out interface{}, event string, log types.Log) error {
			tmpOut := out.(*bridgeAbi.L1QueueTransactionEvent)
			tmpOut.QueueIndex = 100
			tmpOut.Data = []byte("test data")
			tmpOut.Sender = common.HexToAddress("0xb4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30")
			tmpOut.Value = big.NewInt(1000)
			tmpOut.Target = common.HexToAddress("0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5")
			tmpOut.GasLimit = big.NewInt(10)
			return nil
		})
		defer patchGuard.Reset()

		l2Messages, rollupEvents, err := watcher.parseBridgeEventLogs(logs)
		assert.NoError(t, err)
		assert.Empty(t, rollupEvents)
		assert.Len(t, l2Messages, 1)
		assert.Equal(t, l2Messages[0].Value, big.NewInt(1000).String())
	})
}

func testParseBridgeEventLogsL1CommitBatchEventSignature(t *testing.T) {
	watcher, db := setupL1Watcher(t)
	defer database.CloseDB(db)
	logs := []types.Log{
		{
			Topics:      []common.Hash{bridgeAbi.L1CommitBatchEventSignature},
			BlockNumber: 100,
			TxHash:      common.HexToHash("0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"),
		},
	}

	convey.Convey("unpack CommitBatch log failure", t, func() {
		targetErr := errors.New("UnpackLog CommitBatch failure")
		patchGuard := gomonkey.ApplyFunc(utils.UnpackLog, func(c *abi.ABI, out interface{}, event string, log types.Log) error {
			return targetErr
		})
		defer patchGuard.Reset()

		l2Messages, rollupEvents, err := watcher.parseBridgeEventLogs(logs)
		assert.EqualError(t, err, targetErr.Error())
		assert.Empty(t, l2Messages)
		assert.Empty(t, rollupEvents)
	})

	convey.Convey("L1CommitBatchEventSignature success", t, func() {
		msgHash := common.HexToHash("0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5")
		patchGuard := gomonkey.ApplyFunc(utils.UnpackLog, func(c *abi.ABI, out interface{}, event string, log types.Log) error {
			tmpOut := out.(*bridgeAbi.L1CommitBatchEvent)
			tmpOut.BatchHash = msgHash
			return nil
		})
		defer patchGuard.Reset()

		l2Messages, rollupEvents, err := watcher.parseBridgeEventLogs(logs)
		assert.NoError(t, err)
		assert.Empty(t, l2Messages)
		assert.Len(t, rollupEvents, 1)
		assert.Equal(t, rollupEvents[0].batchHash, msgHash)
		assert.Equal(t, rollupEvents[0].status, commonTypes.RollupCommitted)
	})
}

func testParseBridgeEventLogsL1FinalizeBatchEventSignature(t *testing.T) {
	watcher, db := setupL1Watcher(t)
	defer database.CloseDB(db)
	logs := []types.Log{
		{
			Topics:      []common.Hash{bridgeAbi.L1FinalizeBatchEventSignature},
			BlockNumber: 100,
			TxHash:      common.HexToHash("0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"),
		},
	}

	convey.Convey("unpack FinalizeBatch log failure", t, func() {
		targetErr := errors.New("UnpackLog FinalizeBatch failure")
		patchGuard := gomonkey.ApplyFunc(utils.UnpackLog, func(c *abi.ABI, out interface{}, event string, log types.Log) error {
			return targetErr
		})
		defer patchGuard.Reset()

		l2Messages, rollupEvents, err := watcher.parseBridgeEventLogs(logs)
		assert.EqualError(t, err, targetErr.Error())
		assert.Empty(t, l2Messages)
		assert.Empty(t, rollupEvents)
	})

	convey.Convey("L1FinalizeBatchEventSignature success", t, func() {
		msgHash := common.HexToHash("0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5")
		patchGuard := gomonkey.ApplyFunc(utils.UnpackLog, func(c *abi.ABI, out interface{}, event string, log types.Log) error {
			tmpOut := out.(*bridgeAbi.L1FinalizeBatchEvent)
			tmpOut.BatchHash = msgHash
			return nil
		})
		defer patchGuard.Reset()

		l2Messages, rollupEvents, err := watcher.parseBridgeEventLogs(logs)
		assert.NoError(t, err)
		assert.Empty(t, l2Messages)
		assert.Len(t, rollupEvents, 1)
		assert.Equal(t, rollupEvents[0].batchHash, msgHash)
		assert.Equal(t, rollupEvents[0].status, commonTypes.RollupFinalized)
	})
}
