package eth

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

	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/common/hexutil"
	"github.com/ethereum/go-ethereum/common/math"
	"github.com/ethereum/go-ethereum/consensus/ethash"
	"github.com/ethereum/go-ethereum/consensus/taiko"
	"github.com/ethereum/go-ethereum/core"
	"github.com/ethereum/go-ethereum/core/rawdb"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/ethdb"
	"github.com/ethereum/go-ethereum/params"
)

func TestShastaProposalIDFromExtraData(t *testing.T) {
	extra := []byte{0x2a, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06}
	proposalID, err := core.DecodeShastaProposalID(extra)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	expected := new(big.Int).SetBytes([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06})
	if proposalID.Cmp(expected) != 0 {
		t.Fatalf("expected %s, got %s", expected.String(), proposalID.String())
	}
}

func TestShastaBasefeeSharingPctgFromExtraData(t *testing.T) {
	extra := []byte{0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
	if pctg := core.DecodeShastaBasefeeSharingPctg(extra); pctg != 0x64 {
		t.Fatalf("expected 0x64, got %d", pctg)
	}
	if pctg := core.DecodeShastaBasefeeSharingPctg(nil); pctg != 0 {
		t.Fatalf("expected 0, got %d", pctg)
	}
}

func TestShastaProposalIDFromExtraDataInvalid(t *testing.T) {
	if _, err := core.DecodeShastaProposalID([]byte{0x01}); err == nil {
		t.Fatal("expected error for short extradata")
	}
}

func TestTaikoAuthBackendExposesBatchLookupMethods(t *testing.T) {
	backendType := reflect.TypeOf(&TaikoAuthAPIBackend{})
	for _, name := range []string{
		"LastL1OriginByBatchID",
		"LastBlockIDByBatchID",
		"LastCertainBlockIDByBatchID",
		"LastCertainL1OriginByBatchID",
	} {
		if _, ok := backendType.MethodByName(name); !ok {
			t.Fatalf("expected TaikoAuthAPIBackend to expose %s", name)
		}
	}
}

func TestTaikoAPIBackendHidesBatchLookupMethods(t *testing.T) {
	backendType := reflect.TypeOf(&TaikoAPIBackend{})
	for _, name := range []string{
		"LastL1OriginByBatchID",
		"LastBlockIDByBatchID",
		"LastCertainBlockIDByBatchID",
		"LastCertainL1OriginByBatchID",
	} {
		if _, ok := backendType.MethodByName(name); ok {
			t.Fatalf("expected TaikoAPIBackend to hide %s", name)
		}
	}
}

func TestGetLastBlockByBatchIdNoHeadL1Origin(t *testing.T) {
	db, chain, proposalID, _ := newShastaTestChain(t)
	backend := &TaikoAuthAPIBackend{eth: &Ethereum{blockchain: chain, chainDb: db}}

	blockID, err := backend.getLastBlockByBatchId(proposalID)
	if !errors.Is(err, ErrProposalLastBlockUncertain) {
		t.Fatalf("expected ErrProposalLastBlockUncertain, got %v", err)
	}
	if blockID != nil {
		t.Fatalf("expected nil blockID, got %v", blockID)
	}
}

func TestGetLastBlockByBatchIdUncertainAtHead(t *testing.T) {
	db, chain, proposalID, blocks := newShastaTestChain(t)
	backend := &TaikoAuthAPIBackend{eth: &Ethereum{blockchain: chain, chainDb: db}}
	headBlock := blocks[len(blocks)-1]
	rawdb.WriteHeadL1Origin(db, headBlock.Number())

	blockID, err := backend.getLastBlockByBatchId(proposalID)
	if !errors.Is(err, ErrProposalLastBlockUncertain) {
		t.Fatalf("expected ErrProposalLastBlockUncertain, got %v", err)
	}
	if blockID != nil {
		t.Fatalf("expected nil blockID, got %v", blockID)
	}
}

func TestMaxBatchLookupBlocks(t *testing.T) {
	if maxBatchLookupBlocks != 768*21_600 {
		t.Fatalf("expected maxBatchLookupBlocks %d, got %d", 768*21_600, maxBatchLookupBlocks)
	}
}

func TestGetLastBlockByBatchIdLookbackLimit(t *testing.T) {
	const maxLookback = 3
	chainLength := maxLookback + 2

	proposalBytes := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}
	proposalID := new(big.Int).SetBytes(proposalBytes)
	matchExtra := append([]byte{0x00}, proposalBytes...)
	otherExtra := append([]byte{0x00}, []byte{0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}...)
	data := make([]byte, len(taiko.AnchorV4Selector))
	copy(data, taiko.AnchorV4Selector)

	genesis := &core.Genesis{
		Config: params.TestChainConfig,
		Alloc: types.GenesisAlloc{
			testAddr: {Balance: big.NewInt(1_000_000_000_000_000_000)},
		},
	}
	engine := ethash.NewFaker()

	db := rawdb.NewMemoryDatabase()
	chain, err := core.NewBlockChain(db, genesis, engine, nil)
	if err != nil {
		t.Fatalf("failed to create chain: %v", err)
	}
	genesisBlock := chain.Genesis()
	if genesisBlock == nil {
		t.Fatal("missing genesis block")
	}

	tx := types.NewTx(&types.LegacyTx{
		Nonce:    0,
		To:       &common.Address{1},
		Value:    big.NewInt(0),
		Gas:      50_000,
		GasPrice: big.NewInt(1),
		Data:     data,
	})

	parentHash := genesisBlock.Hash()
	var headBlock *types.Block
	for i := 1; i <= chainLength; i++ {
		extra := otherExtra
		if i == 1 {
			extra = matchExtra
		}
		header := &types.Header{
			ParentHash: parentHash,
			Number:     new(big.Int).SetUint64(uint64(i)),
			Time:       uint64(i),
			Difficulty: big.NewInt(1),
			GasLimit:   30_000_000,
			GasUsed:    0,
			BaseFee:    big.NewInt(0),
			Extra:      extra,
		}
		block := types.NewBlockWithHeader(header).WithBody(types.Body{
			Transactions: types.Transactions{tx},
		})
		rawdb.WriteBlock(db, block)
		rawdb.WriteCanonicalHash(db, block.Hash(), block.NumberU64())
		parentHash = block.Hash()
		headBlock = block
	}
	if headBlock == nil {
		t.Fatal("failed to build test chain")
	}

	backend := &TaikoAuthAPIBackend{eth: &Ethereum{blockchain: chain, chainDb: db}}
	rawdb.WriteL1Origin(db, headBlock.Number(), &rawdb.L1Origin{
		BlockID:       headBlock.Number(),
		L2BlockHash:   headBlock.Hash(),
		L1BlockHeight: big.NewInt(1),
	})
	rawdb.WriteHeadL1Origin(db, headBlock.Number())
	chain.HeaderChain().SetCurrentHeader(headBlock.Header())

	blockID, err := backend.getLastBlockByBatchIdWithLimit(proposalID, maxLookback)
	if !errors.Is(err, ErrProposalLastBlockLookbackExceeded) {
		t.Fatalf("expected ErrProposalLastBlockLookbackExceeded, got %v", err)
	}
	if blockID != nil {
		t.Fatalf("expected nil blockID, got %v", blockID)
	}
}

func TestLastCertainL1OriginByBatchID(t *testing.T) {
	db := rawdb.NewMemoryDatabase()
	backend := &TaikoAuthAPIBackend{eth: &Ethereum{chainDb: db}}
	batchID := (*hexutil.Big)(big.NewInt(1))

	l1Origin, err := backend.LastCertainL1OriginByBatchID((*math.HexOrDecimal256)(batchID))
	if err != nil {
		t.Fatalf("expected nil error, got %v", err)
	}
	if l1Origin != nil {
		t.Fatalf("expected nil l1Origin, got %v", l1Origin)
	}

	blockID := big.NewInt(2)
	expected := &rawdb.L1Origin{
		BlockID:       blockID,
		L2BlockHash:   common.HexToHash("0x1"),
		L1BlockHeight: big.NewInt(3),
		L1BlockHash:   common.HexToHash("0x2"),
	}
	rawdb.WriteBatchToLastBlockID(db, big.NewInt(1), blockID)
	rawdb.WriteL1Origin(db, blockID, expected)

	l1Origin, err = backend.LastCertainL1OriginByBatchID((*math.HexOrDecimal256)(batchID))
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if l1Origin == nil {
		t.Fatal("expected l1Origin, got nil")
	}
	if !reflect.DeepEqual(expected, l1Origin) {
		t.Fatalf("expected %v, got %v", expected, l1Origin)
	}
}

func TestBatchLookupMethodsReturnNilBelowNetworkThreshold(t *testing.T) {
	const networkID = 999
	batchID := big.NewInt(1)
	blockID := big.NewInt(2)
	threshold := uint64(3)

	originalThreshold, hadThreshold := batchLookupBlockThresholds[networkID]
	batchLookupBlockThresholds[networkID] = threshold
	defer func() {
		if hadThreshold {
			batchLookupBlockThresholds[networkID] = originalThreshold
		} else {
			delete(batchLookupBlockThresholds, networkID)
		}
	}()

	db := rawdb.NewMemoryDatabase()
	backend := &TaikoAuthAPIBackend{eth: &Ethereum{chainDb: db, networkID: networkID}}
	expectedOrigin := &rawdb.L1Origin{
		BlockID:       blockID,
		L2BlockHash:   common.HexToHash("0x1"),
		L1BlockHeight: big.NewInt(3),
		L1BlockHash:   common.HexToHash("0x2"),
	}
	rawdb.WriteBatchToLastBlockID(db, batchID, blockID)
	rawdb.WriteL1Origin(db, blockID, expectedOrigin)

	l1Origin, err := backend.LastL1OriginByBatchID((*math.HexOrDecimal256)(batchID))
	if err != nil {
		t.Fatalf("expected nil error, got %v", err)
	}
	if l1Origin != nil {
		t.Fatalf("expected nil l1Origin, got %v", l1Origin)
	}

	lastBlockID, err := backend.LastBlockIDByBatchID((*math.HexOrDecimal256)(batchID))
	if err != nil {
		t.Fatalf("expected nil error, got %v", err)
	}
	if lastBlockID != nil {
		t.Fatalf("expected nil blockID, got %v", lastBlockID)
	}

	certainBlockID, err := backend.LastCertainBlockIDByBatchID((*math.HexOrDecimal256)(batchID))
	if err != nil {
		t.Fatalf("expected nil error, got %v", err)
	}
	if certainBlockID != nil {
		t.Fatalf("expected nil certain blockID, got %v", certainBlockID)
	}

	certainL1Origin, err := backend.LastCertainL1OriginByBatchID((*math.HexOrDecimal256)(batchID))
	if err != nil {
		t.Fatalf("expected nil error, got %v", err)
	}
	if certainL1Origin != nil {
		t.Fatalf("expected nil certain l1Origin, got %v", certainL1Origin)
	}
}

func newShastaTestChain(t *testing.T) (ethdb.Database, *core.BlockChain, *big.Int, []*types.Block) {
	t.Helper()

	proposalBytes := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}
	proposalID := new(big.Int).SetBytes(proposalBytes)
	extra := append([]byte{0x00}, proposalBytes...)
	data := make([]byte, len(taiko.AnchorV4Selector))
	copy(data, taiko.AnchorV4Selector)

	genesis := &core.Genesis{
		Config: params.TestChainConfig,
		Alloc: types.GenesisAlloc{
			testAddr: {Balance: big.NewInt(1_000_000_000_000_000_000)},
		},
	}
	engine := ethash.NewFaker()

	_, blocks, _ := core.GenerateChainWithGenesis(genesis, engine, 1, func(i int, b *core.BlockGen) {
		b.SetExtra(extra)
		tx := types.NewTx(&types.LegacyTx{
			Nonce:    0,
			To:       &common.Address{1},
			Value:    big.NewInt(0),
			Gas:      50_000,
			GasPrice: b.BaseFee(),
			Data:     data,
		})
		signed, err := types.SignTx(tx, types.HomesteadSigner{}, testKey)
		if err != nil {
			t.Fatalf("failed to sign tx: %v", err)
		}
		b.AddTx(signed)
	})

	db := rawdb.NewMemoryDatabase()
	chain, err := core.NewBlockChain(db, genesis, engine, nil)
	if err != nil {
		t.Fatalf("failed to create chain: %v", err)
	}
	if _, err := chain.InsertChain(blocks); err != nil {
		t.Fatalf("failed to insert chain: %v", err)
	}

	return db, chain, proposalID, blocks
}
