package core

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"math/big"
	"runtime/pprof"
	"slices"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"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/state"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/crypto"
	"github.com/ethereum/go-ethereum/params"
	"github.com/ethereum/go-ethereum/triedb"

	"github.com/google/pprof/profile"
)

func TestPrefetchLeaking(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	var (
		gendb   = rawdb.NewMemoryDatabase()
		key, _  = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
		address = crypto.PubkeyToAddress(key.PublicKey)
		funds   = big.NewInt(100000000000000000)
		gspec   = &Genesis{
			Config:  params.TestChainConfig,
			Alloc:   GenesisAlloc{address: {Balance: funds}},
			BaseFee: big.NewInt(params.InitialBaseFee),
		}
		genesis = gspec.MustCommit(gendb, triedb.NewDatabase(gendb, nil))
		signer  = types.LatestSigner(gspec.Config)
	)
	blocks, _ := GenerateChain(gspec.Config, genesis, ethash.NewFaker(), gendb, 1, func(i int, block *BlockGen) {
		block.SetCoinbase(common.Address{0x00})
		for j := 0; j < 100; j++ {
			tx, err := types.SignTx(types.NewTransaction(block.TxNonce(address), common.Address{0x00}, big.NewInt(1000), params.TxGas, block.header.BaseFee, nil), signer, key)
			if err != nil {
				panic(err)
			}
			block.AddTx(tx)
		}
	})
	archiveDb := rawdb.NewMemoryDatabase()
	gspec.MustCommit(archiveDb, triedb.NewDatabase(archiveDb, nil))
	archive, _ := NewBlockChain(archiveDb, gspec, ethash.NewFaker(), nil)
	defer archive.Stop()

	block := blocks[0]
	parent := archive.GetHeader(block.ParentHash(), block.NumberU64()-1)
	statedb, _ := state.New(parent.Root, archive.statedb)
	var inter atomic.Bool

	Track(ctx, t, func(ctx context.Context) {
		defer inter.Store(true)
		go archive.prefetcher.Prefetch(block.Transactions(), block.Header(), block.GasLimit(), statedb, archive.cfg.VmConfig, &inter)
		time.Sleep(1 * time.Second)
	})
}

func Track(ctx context.Context, t *testing.T, fn func(context.Context)) {
	label := t.Name()
	pprof.Do(ctx, pprof.Labels("test", label), fn)
	if err := CheckNoGoroutines("test", label); err != nil {
		t.Fatal("Leaked goroutines\n", err)
	}
}

func CheckNoGoroutines(key, value string) error {
	var pb bytes.Buffer
	profiler := pprof.Lookup("goroutine")
	if profiler == nil {
		return errors.New("unable to find profile")
	}
	err := profiler.WriteTo(&pb, 0)
	if err != nil {
		return fmt.Errorf("unable to read profile: %w", err)
	}

	p, err := profile.ParseData(pb.Bytes())
	if err != nil {
		return fmt.Errorf("unable to parse profile: %w", err)
	}

	return summarizeGoroutines(p, key, value)
}

func summarizeGoroutines(p *profile.Profile, key, expectedValue string) error {
	var b strings.Builder

	for _, sample := range p.Sample {
		if !matchesLabel(sample, key, expectedValue) {
			continue
		}

		fmt.Fprintf(&b, "count %d @", sample.Value[0])
		// format the stack trace for each goroutine
		for _, loc := range sample.Location {
			for i, ln := range loc.Line {
				if i == 0 {
					fmt.Fprintf(&b, "#   %#8x", loc.Address)
					if loc.IsFolded {
						fmt.Fprint(&b, " [F]")
					}
				} else {
					fmt.Fprint(&b, "#           ")
				}
				if fn := ln.Function; fn != nil {
					fmt.Fprintf(&b, " %-50s %s:%d", fn.Name, fn.Filename, ln.Line)
				} else {
					fmt.Fprintf(&b, " ???")
				}
				fmt.Fprintf(&b, "\n")
			}
		}
		fmt.Fprintf(&b, "\n")
	}

	if b.Len() == 0 {
		return nil
	}

	return errors.New(b.String())
}

func matchesLabel(sample *profile.Sample, key, expectedValue string) bool {
	values, hasLabel := sample.Label[key]
	if !hasLabel {
		return false
	}

	return slices.Contains(values, expectedValue)
}
