package tests

import (
	"context"
	"crypto/rand"
	"math/big"
	"net/http"
	"os"
	"strconv"
	"strings"
	"testing"
	"time"

	"scroll-tech/database/migrate"

	"scroll-tech/common/database"
	dockercompose "scroll-tech/common/docker-compose/l1"
	tc "scroll-tech/common/testcontainers"
	"scroll-tech/common/utils"

	"github.com/gin-gonic/gin"
	"github.com/scroll-tech/go-ethereum/accounts/abi/bind"
	"github.com/scroll-tech/go-ethereum/common"
	"github.com/scroll-tech/go-ethereum/core/types"
	gethTypes "github.com/scroll-tech/go-ethereum/core/types"
	"github.com/scroll-tech/go-ethereum/crypto"
	"github.com/scroll-tech/go-ethereum/ethclient"
	"github.com/scroll-tech/go-ethereum/log"
	"github.com/stretchr/testify/assert"
	"gorm.io/gorm"

	bcmd "scroll-tech/rollup/cmd"
	"scroll-tech/rollup/mock_bridge"
)

var (
	testApps     *tc.TestcontainerApps
	rollupApp    *bcmd.MockApp
	posL1TestEnv *dockercompose.PoSL1TestEnv

	// clients
	l1Client *ethclient.Client
	l2Client *ethclient.Client

	// l1Auth
	l1Auth *bind.TransactOpts
)

func setupDB(t *testing.T) *gorm.DB {
	dsn, err := testApps.GetDBEndPoint()
	assert.NoError(t, err)

	cfg := &database.Config{
		DSN:        dsn,
		DriverName: "postgres",
		MaxOpenNum: 200,
		MaxIdleNum: 20,
	}
	db, err := database.InitDB(cfg)
	assert.NoError(t, err)
	sqlDB, err := db.DB()
	assert.NoError(t, err)
	assert.NoError(t, migrate.ResetDB(sqlDB))
	return db
}

func TestMain(m *testing.M) {
	defer func() {
		if testApps != nil {
			testApps.Free()
		}
		if rollupApp != nil {
			rollupApp.Free()
		}
		if posL1TestEnv != nil {
			posL1TestEnv.Stop()
		}
	}()
	m.Run()
}

func setupEnv(t *testing.T) {
	glogger := log.NewGlogHandler(log.StreamHandler(os.Stderr, log.LogfmtFormat()))
	glogger.Verbosity(log.LvlInfo)
	log.Root().SetHandler(glogger)

	var (
		err           error
		l1GethChainID *big.Int
	)

	posL1TestEnv, err = dockercompose.NewPoSL1TestEnv()
	assert.NoError(t, err, "failed to create PoS L1 test environment")
	assert.NoError(t, posL1TestEnv.Start(), "failed to start PoS L1 test environment")

	testApps = tc.NewTestcontainerApps()
	assert.NoError(t, testApps.StartPostgresContainer())
	assert.NoError(t, testApps.StartL1GethContainer())
	assert.NoError(t, testApps.StartL2GethContainer())
	rollupApp = bcmd.NewRollupApp(testApps, "../conf/config.json")

	l1Client, err = posL1TestEnv.L1Client()
	assert.NoError(t, err)
	l2Client, err = testApps.GetL2GethClient()
	assert.NoError(t, err)
	l1GethChainID, err = l1Client.ChainID(context.Background())
	assert.NoError(t, err)

	l1Cfg, l2Cfg := rollupApp.Config.L1Config, rollupApp.Config.L2Config
	l1Cfg.Confirmations = 0
	l1Cfg.RelayerConfig.SenderConfig.Confirmations = 0
	l2Cfg.Confirmations = 0
	l2Cfg.RelayerConfig.SenderConfig.Confirmations = 0

	l1Auth, err = bind.NewKeyedTransactorWithChainID(rollupApp.Config.L2Config.RelayerConfig.CommitSenderPrivateKey, l1GethChainID)
	assert.NoError(t, err)
	rollupApp.Config.L1Config.Endpoint = posL1TestEnv.Endpoint()
	rollupApp.Config.L2Config.RelayerConfig.SenderConfig.Endpoint = posL1TestEnv.Endpoint()

	port, err := rand.Int(rand.Reader, big.NewInt(10000))
	assert.NoError(t, err)
	svrPort := strconv.FormatInt(port.Int64()+40000, 10)
	rollupApp.Config.L2Config.RelayerConfig.ChainMonitor.BaseURL = "http://localhost:" + svrPort
}

func mockChainMonitorServer(baseURL string) (*http.Server, error) {
	router := gin.New()
	r := router.Group("/v1")
	r.GET("/batch_status", func(ctx *gin.Context) {
		ctx.JSON(http.StatusOK, struct {
			ErrCode int    `json:"errcode"`
			ErrMsg  string `json:"errmsg"`
			Data    bool   `json:"data"`
		}{
			ErrCode: 0,
			ErrMsg:  "",
			Data:    true,
		})
	})
	return utils.StartHTTPServer(strings.Split(baseURL, "//")[1], router)
}

func prepareContracts(t *testing.T) {
	// L1 ScrolChain contract
	nonce, err := l1Client.PendingNonceAt(context.Background(), l1Auth.From)
	assert.NoError(t, err)
	scrollChainAddress := crypto.CreateAddress(l1Auth.From, nonce)
	tx := types.NewContractCreation(nonce, big.NewInt(0), 10000000, big.NewInt(1000000000), common.FromHex(mock_bridge.MockBridgeMetaData.Bin))
	signedTx, err := l1Auth.Signer(l1Auth.From, tx)
	assert.NoError(t, err)
	err = l1Client.SendTransaction(context.Background(), signedTx)
	assert.NoError(t, err)

	assert.Eventually(t, func() bool {
		_, isPending, err := l1Client.TransactionByHash(context.Background(), signedTx.Hash())
		return err == nil && !isPending
	}, 30*time.Second, time.Second)

	assert.Eventually(t, func() bool {
		receipt, err := l1Client.TransactionReceipt(context.Background(), signedTx.Hash())
		return err == nil && receipt.Status == gethTypes.ReceiptStatusSuccessful
	}, 30*time.Second, time.Second)

	assert.Eventually(t, func() bool {
		code, err := l1Client.CodeAt(context.Background(), scrollChainAddress, nil)
		return err == nil && len(code) > 0
	}, 30*time.Second, time.Second)

	l1Config, l2Config := rollupApp.Config.L1Config, rollupApp.Config.L2Config
	l1Config.ScrollChainContractAddress = scrollChainAddress
	l2Config.RelayerConfig.RollupContractAddress = scrollChainAddress
}

func TestFunction(t *testing.T) {
	setupEnv(t)
	srv, err := mockChainMonitorServer(rollupApp.Config.L2Config.RelayerConfig.ChainMonitor.BaseURL)
	assert.NoError(t, err)
	defer srv.Close()

	// process start test
	t.Run("TestProcessStart", testProcessStart)
	t.Run("TestProcessStartEnableMetrics", testProcessStartEnableMetrics)

	// l1 rollup and watch rollup events
	t.Run("TestCommitAndFinalizeGenesisBatch", testCommitAndFinalizeGenesisBatch)
	t.Run("TestCommitBatchAndFinalizeBatch", testCommitBatchAndFinalizeBatch)
	t.Run("TestCommitBatchAndFinalizeBatch4844", testCommitBatchAndFinalizeBatch4844)
	t.Run("TestCommitBatchAndFinalizeBatchBeforeAndPost4844", testCommitBatchAndFinalizeBatchBeforeAndPost4844)

	// l1/l2 gas oracle
	t.Run("TestImportL1GasPrice", testImportL1GasPrice)
	t.Run("TestImportL2GasPrice", testImportL2GasPrice)
}
