// Copyright 2024 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library 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.
//
// The go-ethereum library 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 the go-ethereum library. If not, see <http://www.gnu.org/licenses/>

package core

import (
	"math/big"
	"testing"

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

// TestTxIndexer tests the functionalities for managing transaction indexes.
func TestTxIndexer(t *testing.T) {
	var (
		testBankKey, _  = crypto.GenerateKey()
		testBankAddress = crypto.PubkeyToAddress(testBankKey.PublicKey)
		testBankFunds   = big.NewInt(1000000000000000000)

		gspec = &Genesis{
			Config:  params.TestChainConfig,
			Alloc:   types.GenesisAlloc{testBankAddress: {Balance: testBankFunds}},
			BaseFee: big.NewInt(params.InitialBaseFee),
		}
		engine    = ethash.NewFaker()
		nonce     = uint64(0)
		chainHead = uint64(128)
	)
	_, blocks, receipts := GenerateChainWithGenesis(gspec, engine, int(chainHead), func(i int, gen *BlockGen) {
		tx, _ := types.SignTx(types.NewTransaction(nonce, common.HexToAddress("0xdeadbeef"), big.NewInt(1000), params.TxGas, big.NewInt(10*params.InitialBaseFee), nil), types.HomesteadSigner{}, testBankKey)
		gen.AddTx(tx)
		nonce += 1
	})

	// verifyIndexes checks if the transaction indexes are present or not
	// of the specified block.
	verifyIndexes := func(db ethdb.Database, number uint64, exist bool) {
		if number == 0 {
			return
		}
		block := blocks[number-1]
		for _, tx := range block.Transactions() {
			lookup := rawdb.ReadTxLookupEntry(db, tx.Hash())
			if exist && lookup == nil {
				t.Fatalf("missing %d %x", number, tx.Hash().Hex())
			}
			if !exist && lookup != nil {
				t.Fatalf("unexpected %d %x", number, tx.Hash().Hex())
			}
		}
	}
	verify := func(db ethdb.Database, expTail uint64, indexer *txIndexer) {
		tail := rawdb.ReadTxIndexTail(db)
		if tail == nil {
			t.Fatal("Failed to write tx index tail")
		}
		if *tail != expTail {
			t.Fatalf("Unexpected tx index tail, want %v, got %d", expTail, *tail)
		}
		if *tail != 0 {
			for number := uint64(0); number < *tail; number += 1 {
				verifyIndexes(db, number, false)
			}
		}
		for number := *tail; number <= chainHead; number += 1 {
			verifyIndexes(db, number, true)
		}
		progress := indexer.report(chainHead, tail)
		if !progress.Done() {
			t.Fatalf("Expect fully indexed")
		}
	}

	var cases = []struct {
		limitA uint64
		tailA  uint64
		limitB uint64
		tailB  uint64
		limitC uint64
		tailC  uint64
	}{
		{
			// LimitA: 0
			// TailA:  0
			//
			// all blocks are indexed
			limitA: 0,
			tailA:  0,

			// LimitB: 1
			// TailB:  128
			//
			// block-128 is indexed
			limitB: 1,
			tailB:  128,

			// LimitB: 64
			// TailB:  65
			//
			// block [65, 128] are indexed
			limitC: 64,
			tailC:  65,
		},
		{
			// LimitA: 64
			// TailA:  65
			//
			// block [65, 128] are indexed
			limitA: 64,
			tailA:  65,

			// LimitB: 1
			// TailB:  128
			//
			// block-128 is indexed
			limitB: 1,
			tailB:  128,

			// LimitB: 64
			// TailB:  65
			//
			// block [65, 128] are indexed
			limitC: 64,
			tailC:  65,
		},
		{
			// LimitA: 127
			// TailA:  2
			//
			// block [2, 128] are indexed
			limitA: 127,
			tailA:  2,

			// LimitB: 1
			// TailB:  128
			//
			// block-128 is indexed
			limitB: 1,
			tailB:  128,

			// LimitB: 64
			// TailB:  65
			//
			// block [65, 128] are indexed
			limitC: 64,
			tailC:  65,
		},
		{
			// LimitA: 128
			// TailA:  1
			//
			// block [2, 128] are indexed
			limitA: 128,
			tailA:  1,

			// LimitB: 1
			// TailB:  128
			//
			// block-128 is indexed
			limitB: 1,
			tailB:  128,

			// LimitB: 64
			// TailB:  65
			//
			// block [65, 128] are indexed
			limitC: 64,
			tailC:  65,
		},
		{
			// LimitA: 129
			// TailA:  0
			//
			// block [0, 128] are indexed
			limitA: 129,
			tailA:  0,

			// LimitB: 1
			// TailB:  128
			//
			// block-128 is indexed
			limitB: 1,
			tailB:  128,

			// LimitB: 64
			// TailB:  65
			//
			// block [65, 128] are indexed
			limitC: 64,
			tailC:  65,
		},
	}
	for _, c := range cases {
		db, _ := rawdb.NewDatabaseWithFreezer(rawdb.NewMemoryDatabase(), "", "", false)
		rawdb.WriteAncientBlocks(db, append([]*types.Block{gspec.ToBlock()}, blocks...), append([]types.Receipts{{}}, receipts...), big.NewInt(0))

		// Index the initial blocks from ancient store
		indexer := &txIndexer{
			limit:    c.limitA,
			db:       db,
			progress: make(chan chan TxIndexProgress),
		}
		indexer.run(nil, 128, make(chan struct{}), make(chan struct{}))
		verify(db, c.tailA, indexer)

		indexer.limit = c.limitB
		indexer.run(rawdb.ReadTxIndexTail(db), 128, make(chan struct{}), make(chan struct{}))
		verify(db, c.tailB, indexer)

		indexer.limit = c.limitC
		indexer.run(rawdb.ReadTxIndexTail(db), 128, make(chan struct{}), make(chan struct{}))
		verify(db, c.tailC, indexer)

		// Recover all indexes
		indexer.limit = 0
		indexer.run(rawdb.ReadTxIndexTail(db), 128, make(chan struct{}), make(chan struct{}))
		verify(db, 0, indexer)

		db.Close()
	}
}
