package protoarray

import (
	"context"
	"testing"

	types "github.com/prysmaticlabs/eth2-types"
	"github.com/prysmaticlabs/prysm/config/params"
	"github.com/prysmaticlabs/prysm/encoding/bytesutil"
	"github.com/prysmaticlabs/prysm/testing/require"
)

func slicesEqual(a, b [][32]byte) bool {
	if len(a) != len(b) {
		return false
	}

	mapA := make(map[[32]byte]bool, len(a))
	for _, root := range a {
		mapA[root] = true
	}
	for _, root := range b {
		_, ok := mapA[root]
		if !ok {
			return false
		}
	}
	return true
}

func TestOptimistic_Outside_ForkChoice(t *testing.T) {
	root0 := bytesutil.ToBytes32([]byte("hello0"))

	nodeA := &Node{
		slot:      types.Slot(100),
		root:      bytesutil.ToBytes32([]byte("helloA")),
		bestChild: 1,
		status:    valid,
	}
	nodes := []*Node{
		nodeA,
	}
	ni := map[[32]byte]uint64{
		nodeA.root: 0,
	}

	s := &Store{
		nodes:        nodes,
		nodesIndices: ni,
	}

	f := &ForkChoice{
		store: s,
	}
	_, err := f.IsOptimistic(root0)
	require.ErrorIs(t, ErrUnknownNodeRoot, err)
}

// This tests the algorithm to update optimistic Status
// We start with the following diagram
//
//                E -- F
//               /
//         C -- D
//        /      \
//  A -- B        G -- H -- I
//        \        \
//         J        -- K -- L
//
// The Chain A -- B -- C -- D -- E is VALID.
//
func TestSetOptimisticToValid(t *testing.T) {
	ctx := context.Background()
	tests := []struct {
		root             [32]byte // the root of the new VALID block
		testRoot         [32]byte // root of the node we will test optimistic status
		wantedOptimistic bool     // wanted optimistic status for tested node
		wantedErr        error    // wanted error message
	}{
		{
			[32]byte{'i'},
			[32]byte{'i'},
			false,
			nil,
		},
		{
			[32]byte{'i'},
			[32]byte{'f'},
			true,
			nil,
		},
		{
			[32]byte{'i'},
			[32]byte{'b'},
			false,
			nil,
		},
		{
			[32]byte{'i'},
			[32]byte{'h'},
			false,
			nil,
		},
		{
			[32]byte{'b'},
			[32]byte{'b'},
			false,
			nil,
		},
		{
			[32]byte{'b'},
			[32]byte{'h'},
			true,
			nil,
		},
		{
			[32]byte{'b'},
			[32]byte{'a'},
			false,
			nil,
		},
		{
			[32]byte{'k'},
			[32]byte{'k'},
			false,
			nil,
		},
		{
			[32]byte{'k'},
			[32]byte{'l'},
			true,
			nil,
		},
		{
			[32]byte{'p'},
			[32]byte{},
			false,
			ErrUnknownNodeRoot,
		},
	}
	for _, tc := range tests {
		f := setup(1, 1)

		require.NoError(t, f.InsertOptimisticBlock(ctx, 100, [32]byte{'a'}, params.BeaconConfig().ZeroHash, [32]byte{'A'}, 1, 1))
		require.NoError(t, f.InsertOptimisticBlock(ctx, 101, [32]byte{'b'}, [32]byte{'a'}, [32]byte{'B'}, 1, 1))
		require.NoError(t, f.InsertOptimisticBlock(ctx, 102, [32]byte{'c'}, [32]byte{'b'}, [32]byte{'C'}, 1, 1))
		require.NoError(t, f.InsertOptimisticBlock(ctx, 102, [32]byte{'j'}, [32]byte{'b'}, [32]byte{'J'}, 1, 1))
		require.NoError(t, f.InsertOptimisticBlock(ctx, 103, [32]byte{'d'}, [32]byte{'c'}, [32]byte{'D'}, 1, 1))
		require.NoError(t, f.InsertOptimisticBlock(ctx, 104, [32]byte{'e'}, [32]byte{'d'}, [32]byte{'E'}, 1, 1))
		require.NoError(t, f.InsertOptimisticBlock(ctx, 104, [32]byte{'g'}, [32]byte{'d'}, [32]byte{'G'}, 1, 1))
		require.NoError(t, f.InsertOptimisticBlock(ctx, 105, [32]byte{'f'}, [32]byte{'e'}, [32]byte{'F'}, 1, 1))
		require.NoError(t, f.InsertOptimisticBlock(ctx, 105, [32]byte{'h'}, [32]byte{'g'}, [32]byte{'H'}, 1, 1))
		require.NoError(t, f.InsertOptimisticBlock(ctx, 105, [32]byte{'k'}, [32]byte{'g'}, [32]byte{'K'}, 1, 1))
		require.NoError(t, f.InsertOptimisticBlock(ctx, 106, [32]byte{'i'}, [32]byte{'h'}, [32]byte{'I'}, 1, 1))
		require.NoError(t, f.InsertOptimisticBlock(ctx, 106, [32]byte{'l'}, [32]byte{'k'}, [32]byte{'L'}, 1, 1))
		require.NoError(t, f.SetOptimisticToValid(context.Background(), [32]byte{'e'}))
		optimistic, err := f.IsOptimistic([32]byte{'b'})
		require.NoError(t, err)
		require.Equal(t, false, optimistic)

		err = f.SetOptimisticToValid(context.Background(), tc.root)
		if tc.wantedErr != nil {
			require.ErrorIs(t, err, tc.wantedErr)
		} else {
			require.NoError(t, err)
			optimistic, err := f.IsOptimistic(tc.testRoot)
			require.NoError(t, err)
			require.Equal(t, tc.wantedOptimistic, optimistic)
		}
	}
}

// We test the algorithm to update a node from SYNCING to INVALID
// We start with the same diagram as above:
//
//                         E(2) -- F(1)
//                        /
//             C(7) -- D(6)
//            /           \
//  A(10) -- B(9)          G(3) -- H(1) -- I(0)
//            \               \
//             J(1)             -- K(1) -- L(0)
//
// And the chain A -- B -- C -- D -- E has been fully validated. The numbers in parentheses are
// the weights of the nodes.
//
func TestSetOptimisticToInvalid(t *testing.T) {
	tests := []struct {
		name              string   // test description
		root              [32]byte // the root of the new INVALID block
		payload           [32]byte // the payload of the last valid hash
		newBestChild      uint64
		newBestDescendant uint64
		newParentWeight   uint64
		returnedRoots     [][32]byte
	}{
		{
			"Remove tip, parent was valid",
			[32]byte{'j'},
			[32]byte{'B'},
			3,
			12,
			8,
			[][32]byte{[32]byte{'j'}},
		},
		{
			"Remove tip, parent was optimistic",
			[32]byte{'i'},
			[32]byte{'H'},
			NonExistentNode,
			NonExistentNode,
			1,
			[][32]byte{[32]byte{'i'}},
		},
		{
			"Remove tip, lvh is inner and valid",
			[32]byte{'i'},
			[32]byte{'D'},
			6,
			8,
			3,
			[][32]byte{[32]byte{'g'}, [32]byte{'h'}, [32]byte{'k'}, [32]byte{'i'}, [32]byte{'l'}},
		},
		{
			"Remove inner, lvh is inner and optimistic",
			[32]byte{'h'},
			[32]byte{'G'},
			10,
			12,
			2,
			[][32]byte{[32]byte{'h'}, [32]byte{'i'}},
		},
		{
			"Remove tip, lvh is inner and optimistic",
			[32]byte{'l'},
			[32]byte{'G'},
			9,
			11,
			2,
			[][32]byte{[32]byte{'k'}, [32]byte{'l'}},
		},
		{
			"Remove tip, lvh is not an ancestor",
			[32]byte{'j'},
			[32]byte{'C'},
			5,
			12,
			7,
			[][32]byte{[32]byte{'j'}},
		},
		{
			"Remove inner, lvh is not an ancestor",
			[32]byte{'g'},
			[32]byte{'J'},
			NonExistentNode,
			NonExistentNode,
			1,
			[][32]byte{[32]byte{'g'}, [32]byte{'h'}, [32]byte{'k'}, [32]byte{'i'}, [32]byte{'l'}},
		},
	}
	for _, tc := range tests {
		ctx := context.Background()
		f := setup(1, 1)

		require.NoError(t, f.InsertOptimisticBlock(ctx, 100, [32]byte{'a'}, params.BeaconConfig().ZeroHash, params.BeaconConfig().ZeroHash, 1, 1))
		require.NoError(t, f.InsertOptimisticBlock(ctx, 101, [32]byte{'b'}, [32]byte{'a'}, [32]byte{'B'}, 1, 1))
		require.NoError(t, f.InsertOptimisticBlock(ctx, 102, [32]byte{'c'}, [32]byte{'b'}, [32]byte{'C'}, 1, 1))
		require.NoError(t, f.InsertOptimisticBlock(ctx, 102, [32]byte{'j'}, [32]byte{'b'}, [32]byte{'J'}, 1, 1))
		require.NoError(t, f.InsertOptimisticBlock(ctx, 103, [32]byte{'d'}, [32]byte{'c'}, [32]byte{'D'}, 1, 1))
		require.NoError(t, f.InsertOptimisticBlock(ctx, 104, [32]byte{'e'}, [32]byte{'d'}, [32]byte{'E'}, 1, 1))
		require.NoError(t, f.InsertOptimisticBlock(ctx, 104, [32]byte{'g'}, [32]byte{'d'}, [32]byte{'G'}, 1, 1))
		require.NoError(t, f.InsertOptimisticBlock(ctx, 105, [32]byte{'f'}, [32]byte{'e'}, [32]byte{'F'}, 1, 1))
		require.NoError(t, f.InsertOptimisticBlock(ctx, 105, [32]byte{'h'}, [32]byte{'g'}, [32]byte{'H'}, 1, 1))
		require.NoError(t, f.InsertOptimisticBlock(ctx, 105, [32]byte{'k'}, [32]byte{'g'}, [32]byte{'K'}, 1, 1))
		require.NoError(t, f.InsertOptimisticBlock(ctx, 106, [32]byte{'i'}, [32]byte{'h'}, [32]byte{'I'}, 1, 1))
		require.NoError(t, f.InsertOptimisticBlock(ctx, 106, [32]byte{'l'}, [32]byte{'k'}, [32]byte{'L'}, 1, 1))
		weights := []uint64{10, 10, 9, 7, 1, 6, 2, 3, 1, 1, 1, 0, 0}
		f.store.nodesLock.Lock()
		for i, node := range f.store.nodes {
			node.weight = weights[i]
		}
		f.store.nodesLock.Unlock()
		require.NoError(t, f.SetOptimisticToValid(ctx, [32]byte{'e'}))
		roots, err := f.SetOptimisticToInvalid(ctx, tc.root, tc.payload)
		require.NoError(t, err)
		f.store.nodesLock.RLock()
		_, ok := f.store.nodesIndices[tc.root]
		require.Equal(t, false, ok)
		lvh := f.store.nodes[f.store.payloadIndices[tc.payload]]
		require.Equal(t, true, slicesEqual(tc.returnedRoots, roots))
		require.Equal(t, tc.newBestChild, lvh.bestChild)
		require.Equal(t, tc.newBestDescendant, lvh.bestDescendant)
		require.Equal(t, tc.newParentWeight, lvh.weight)
		require.Equal(t, syncing, f.store.nodes[8].status /* F */)
		require.Equal(t, valid, f.store.nodes[5].status /* E */)
		f.store.nodesLock.RUnlock()
	}
}

func TestSetOptimisticToInvalid_InvalidRoots(t *testing.T) {
	ctx := context.Background()
	f := setup(1, 1)

	require.NoError(t, f.InsertOptimisticBlock(ctx, 100, [32]byte{'a'}, params.BeaconConfig().ZeroHash, params.BeaconConfig().ZeroHash, 1, 1))
	require.NoError(t, f.InsertOptimisticBlock(ctx, 101, [32]byte{'b'}, [32]byte{'a'}, [32]byte{'B'}, 1, 1))
	_, err := f.SetOptimisticToInvalid(ctx, [32]byte{'p'}, [32]byte{'B'})
	require.ErrorIs(t, ErrUnknownNodeRoot, err)
	_, err = f.SetOptimisticToInvalid(ctx, [32]byte{'a'}, [32]byte{'p'})
	require.ErrorIs(t, errInvalidFinalizedNode, err)
}

// This is a regression test (10445)
func TestSetOptimisticToInvalid_ProposerBoost(t *testing.T) {
	ctx := context.Background()
	f := setup(1, 1)

	require.NoError(t, f.InsertOptimisticBlock(ctx, 100, [32]byte{'a'}, params.BeaconConfig().ZeroHash, [32]byte{'A'}, 1, 1))
	require.NoError(t, f.InsertOptimisticBlock(ctx, 101, [32]byte{'b'}, [32]byte{'a'}, [32]byte{'B'}, 1, 1))
	require.NoError(t, f.InsertOptimisticBlock(ctx, 101, [32]byte{'c'}, [32]byte{'b'}, [32]byte{'C'}, 1, 1))
	f.store.proposerBoostLock.Lock()
	f.store.proposerBoostRoot = [32]byte{'c'}
	f.store.previousProposerBoostScore = 10
	f.store.previousProposerBoostRoot = [32]byte{'b'}
	f.store.proposerBoostLock.Unlock()

	_, err := f.SetOptimisticToInvalid(ctx, [32]byte{'c'}, [32]byte{'A'})
	require.NoError(t, err)
	f.store.proposerBoostLock.RLock()
	require.Equal(t, uint64(0), f.store.previousProposerBoostScore)
	require.DeepEqual(t, [32]byte{}, f.store.proposerBoostRoot)
	require.DeepEqual(t, params.BeaconConfig().ZeroHash, f.store.previousProposerBoostRoot)
	f.store.proposerBoostLock.RUnlock()
}
