package stategen

import (
	"context"
	"fmt"
	"sort"
	"testing"

	"github.com/pkg/errors"
	"github.com/prysmaticlabs/prysm/v4/beacon-chain/core/blocks"
	"github.com/prysmaticlabs/prysm/v4/beacon-chain/core/helpers"
	"github.com/prysmaticlabs/prysm/v4/beacon-chain/core/transition"
	"github.com/prysmaticlabs/prysm/v4/beacon-chain/db"
	"github.com/prysmaticlabs/prysm/v4/beacon-chain/state"
	consensusblocks "github.com/prysmaticlabs/prysm/v4/consensus-types/blocks"
	blocktest "github.com/prysmaticlabs/prysm/v4/consensus-types/blocks/testing"
	"github.com/prysmaticlabs/prysm/v4/consensus-types/interfaces"
	"github.com/prysmaticlabs/prysm/v4/consensus-types/primitives"
	"github.com/prysmaticlabs/prysm/v4/testing/require"
	"github.com/prysmaticlabs/prysm/v4/testing/util"
)

func TestMockHistoryStates(t *testing.T) {
	ctx := context.Background()
	var begin, middle, end primitives.Slot = 100, 150, 155
	specs := []mockHistorySpec{
		{slot: begin},
		{slot: middle, savedState: true},
		{slot: end, canonicalBlock: true},
	}
	hist := newMockHistory(t, specs, end+1)
	// we should have 2 "saved" states, genesis and "middle" (savedState == true)
	require.Equal(t, 2, len(hist.states))
	genesisRoot := hist.slotMap[0]
	st, err := hist.StateOrError(ctx, genesisRoot)
	require.NoError(t, err)
	require.DeepEqual(t, hist.states[genesisRoot], st)
	require.Equal(t, primitives.Slot(0), st.Slot())

	shouldExist, err := hist.StateOrError(ctx, hist.slotMap[middle])
	require.NoError(t, err)
	require.DeepEqual(t, hist.states[hist.slotMap[middle]], shouldExist)
	require.Equal(t, middle, shouldExist.Slot())

	cantExist, err := hist.StateOrError(ctx, hist.slotMap[end])
	require.ErrorIs(t, err, db.ErrNotFoundState)
	require.Equal(t, nil, cantExist)

	cantExist, err = hist.StateOrError(ctx, hist.slotMap[begin])
	require.ErrorIs(t, err, db.ErrNotFoundState)
	require.Equal(t, nil, cantExist)
}

func TestMockHistoryParentRoot(t *testing.T) {
	ctx := context.Background()
	var begin, middle, end primitives.Slot = 100, 150, 155
	specs := []mockHistorySpec{
		{slot: begin},
		{slot: middle, savedState: true},
		{slot: end, canonicalBlock: true},
	}
	hist := newMockHistory(t, specs, end+1)
	endRoot := hist.slotMap[end]
	endBlock, err := hist.Block(ctx, endRoot)
	require.NoError(t, err)
	// middle should be the parent of end, compare the middle root to endBlock's parent root
	require.Equal(t, hist.slotMap[middle], endBlock.Block().ParentRoot())
}

type mockHistorySpec struct {
	slot           primitives.Slot
	savedState     bool
	canonicalBlock bool
}

type mockHistory struct {
	blocks                         map[[32]byte]interfaces.ReadOnlySignedBeaconBlock
	slotMap                        map[primitives.Slot][32]byte
	slotIndex                      slotList
	canonical                      map[[32]byte]bool
	states                         map[[32]byte]state.BeaconState
	hiddenStates                   map[[32]byte]state.BeaconState
	current                        primitives.Slot
	overrideHighestSlotBlocksBelow func(context.Context, primitives.Slot) (primitives.Slot, [][32]byte, error)
}

type slotList []primitives.Slot

func (m slotList) Len() int {
	return len(m)
}

func (m slotList) Less(i, j int) bool {
	return m[i] < m[j]
}

func (m slotList) Swap(i, j int) {
	m[i], m[j] = m[j], m[i]
}

var errFallThroughOverride = errors.New("override yielding control back to real HighestRootsBelowSlot")

func (m *mockHistory) HighestRootsBelowSlot(_ context.Context, slot primitives.Slot) (primitives.Slot, [][32]byte, error) {
	if m.overrideHighestSlotBlocksBelow != nil {
		s, r, err := m.overrideHighestSlotBlocksBelow(context.Background(), slot)
		if !errors.Is(err, errFallThroughOverride) {
			return s, r, err
		}
	}
	if len(m.slotIndex) == 0 && len(m.slotMap) > 0 {
		for k := range m.slotMap {
			m.slotIndex = append(m.slotIndex, k)
		}
		sort.Sort(sort.Reverse(m.slotIndex))
	}
	for _, s := range m.slotIndex {
		if s < slot {
			return s, [][32]byte{m.slotMap[s]}, nil
		}
	}
	return 0, [][32]byte{}, nil
}

var errGenesisBlockNotFound = errors.New("canonical genesis block not found in db")

func (m *mockHistory) GenesisBlockRoot(_ context.Context) ([32]byte, error) {
	genesisRoot, ok := m.slotMap[0]
	if !ok {
		return [32]byte{}, errGenesisBlockNotFound
	}
	return genesisRoot, nil
}

func (m *mockHistory) Block(_ context.Context, blockRoot [32]byte) (interfaces.ReadOnlySignedBeaconBlock, error) {
	if b, ok := m.blocks[blockRoot]; ok {
		return b, nil
	}
	return nil, nil
}

func (m *mockHistory) StateOrError(_ context.Context, blockRoot [32]byte) (state.BeaconState, error) {
	if s, ok := m.states[blockRoot]; ok {
		return s.Copy(), nil
	}
	return nil, db.ErrNotFoundState
}

func (m *mockHistory) IsCanonical(_ context.Context, blockRoot [32]byte) (bool, error) {
	canon, ok := m.canonical[blockRoot]
	return ok && canon, nil
}

func (m *mockHistory) CurrentSlot() primitives.Slot {
	return m.current
}

func (h *mockHistory) addBlock(root [32]byte, b interfaces.ReadOnlySignedBeaconBlock, canon bool) {
	h.blocks[root] = b
	h.slotMap[b.Block().Slot()] = root
	h.canonical[root] = canon
}

func (h *mockHistory) addState(root [32]byte, s state.BeaconState) {
	h.states[root] = s
}

func (h *mockHistory) hideState(root [32]byte, s state.BeaconState) {
	h.hiddenStates[root] = s
}

func (h *mockHistory) validateRoots() error {
	uniqParentRoots := make(map[[32]byte]primitives.Slot)
	for s, root := range h.slotMap {
		b := h.blocks[root]
		htr, err := b.Block().HashTreeRoot()
		if err != nil {
			return errors.Wrap(err, fmt.Sprintf("error computing htr for block at slot %d", s))
		}
		if htr != root {
			return fmt.Errorf("htr mismatch, expected=%#x, actual=%#x", root, htr)
		}
		if ps, ok := uniqParentRoots[htr]; ok {
			return fmt.Errorf("duplicate parent_root %#x seen at slots %d, %d", htr, ps, s)
		}
		uniqParentRoots[htr] = s
	}
	return nil
}

func newMockHistory(t *testing.T, hist []mockHistorySpec, current primitives.Slot) *mockHistory {
	ctx := context.Background()
	mh := &mockHistory{
		blocks:       map[[32]byte]interfaces.ReadOnlySignedBeaconBlock{},
		canonical:    map[[32]byte]bool{},
		states:       map[[32]byte]state.BeaconState{},
		hiddenStates: map[[32]byte]state.BeaconState{},
		slotMap:      map[primitives.Slot][32]byte{},
		slotIndex:    slotList{},
		current:      current,
	}

	// genesis state for history
	gs, _ := util.DeterministicGenesisState(t, 32)
	gsr, err := gs.HashTreeRoot(ctx)
	require.NoError(t, err)

	// generate new genesis block using the root of the deterministic state
	gb, err := consensusblocks.NewSignedBeaconBlock(blocks.NewGenesisBlock(gsr[:]))
	require.NoError(t, err)
	pr, err := gb.Block().HashTreeRoot()
	require.NoError(t, err)

	// add genesis block as canonical
	mh.addBlock(pr, gb, true)
	// add genesis state, indexed by unapplied genesis block - genesis block is never really processed...
	mh.addState(pr, gs.Copy())

	ps := gs.Copy()
	for _, spec := range hist {
		// call process_slots and process_block separately, because process_slots updates values used in randao mix
		// which influences proposer_index.
		s, err := ReplayProcessSlots(ctx, ps, spec.slot)
		require.NoError(t, err)

		// create proposer block, setting values in the order seen in the validator.md spec
		b, err := consensusblocks.NewSignedBeaconBlock(util.NewBeaconBlock())
		require.NoError(t, err)

		// set slot to mock history spec value
		b, err = blocktest.SetBlockSlot(b, spec.slot)
		require.NoError(t, err)

		// set the correct proposer_index in the "proposal" block
		// so that it will pass validation in process_block. important that we do this
		// after process_slots!
		idx, err := helpers.BeaconProposerIndex(ctx, s)
		require.NoError(t, err)
		b, err = blocktest.SetProposerIndex(b, idx)
		require.NoError(t, err)

		// set parent root
		b, err = blocktest.SetBlockParentRoot(b, pr)
		require.NoError(t, err)

		// now do process_block
		s, err = transition.ProcessBlockForStateRoot(ctx, s, b)
		require.NoError(t, err)

		sr, err := s.HashTreeRoot(ctx)
		require.NoError(t, err)
		b, err = blocktest.SetBlockStateRoot(b, sr)
		require.NoError(t, err)

		pr, err = b.Block().HashTreeRoot()
		require.NoError(t, err)
		if spec.savedState {
			mh.addState(pr, s)
		} else {
			mh.hideState(pr, s)
		}
		mh.addBlock(pr, b, spec.canonicalBlock)
		ps = s.Copy()
	}

	require.NoError(t, mh.validateRoots())
	return mh
}

var _ HistoryAccessor = &mockHistory{}
var _ CanonicalChecker = &mockHistory{}
var _ CurrentSlotter = &mockHistory{}

type mockCachedGetter struct {
	cache map[[32]byte]state.BeaconState
}

func (m mockCachedGetter) ByBlockRoot(root [32]byte) (state.BeaconState, error) {
	st, ok := m.cache[root]
	if !ok {
		return nil, ErrNotInCache
	}
	return st, nil
}

var _ CachedGetter = &mockCachedGetter{}
