package v2

import (
	"fmt"
	"math"
	"sort"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/tendermint/tendermint/p2p"
	"github.com/tendermint/tendermint/state"
	"github.com/tendermint/tendermint/types"
)

type scTestParams struct {
	peers         map[string]*scPeer
	initHeight    int64
	height        int64
	allB          []int64
	pending       map[int64]p2p.NodeID
	pendingTime   map[int64]time.Time
	received      map[int64]p2p.NodeID
	peerTimeout   time.Duration
	minRecvRate   int64
	targetPending int
	startTime     time.Time
	syncTimeout   time.Duration
}

func verifyScheduler(sc *scheduler) {
	missing := 0
	if sc.maxHeight() >= sc.height {
		missing = int(math.Min(float64(sc.targetPending), float64(sc.maxHeight()-sc.height+1)))
	}
	if len(sc.blockStates) != missing {
		panic(fmt.Sprintf("scheduler block length %d different than target %d", len(sc.blockStates), missing))
	}
}

func newTestScheduler(params scTestParams) *scheduler {
	peers := make(map[p2p.NodeID]*scPeer)
	var maxHeight int64

	initHeight := params.initHeight
	if initHeight == 0 {
		initHeight = 1
	}
	sc := newScheduler(initHeight, params.startTime)
	if params.height != 0 {
		sc.height = params.height
	}

	for id, peer := range params.peers {
		peer.peerID = p2p.NodeID(id)
		peers[p2p.NodeID(id)] = peer
		if maxHeight < peer.height {
			maxHeight = peer.height
		}
	}
	for _, h := range params.allB {
		sc.blockStates[h] = blockStateNew
	}
	for h, pid := range params.pending {
		sc.blockStates[h] = blockStatePending
		sc.pendingBlocks[h] = pid
	}
	for h, tm := range params.pendingTime {
		sc.pendingTime[h] = tm
	}
	for h, pid := range params.received {
		sc.blockStates[h] = blockStateReceived
		sc.receivedBlocks[h] = pid
	}

	sc.peers = peers
	sc.peerTimeout = params.peerTimeout
	if params.syncTimeout == 0 {
		sc.syncTimeout = 10 * time.Second
	} else {
		sc.syncTimeout = params.syncTimeout
	}

	if params.targetPending == 0 {
		sc.targetPending = 10
	} else {
		sc.targetPending = params.targetPending
	}

	sc.minRecvRate = params.minRecvRate

	verifyScheduler(sc)

	return sc
}

func TestScInit(t *testing.T) {
	var (
		initHeight int64 = 5
		sc               = newScheduler(initHeight, time.Now())
	)
	assert.Equal(t, blockStateProcessed, sc.getStateAtHeight(initHeight-1))
	assert.Equal(t, blockStateUnknown, sc.getStateAtHeight(initHeight))
	assert.Equal(t, blockStateUnknown, sc.getStateAtHeight(initHeight+1))
}

func TestScMaxHeights(t *testing.T) {

	tests := []struct {
		name    string
		sc      scheduler
		wantMax int64
	}{
		{
			name:    "no peers",
			sc:      scheduler{height: 11},
			wantMax: 10,
		},
		{
			name: "one ready peer",
			sc: scheduler{
				height: 3,
				peers:  map[p2p.NodeID]*scPeer{"P1": {height: 6, state: peerStateReady}},
			},
			wantMax: 6,
		},
		{
			name: "ready and removed peers",
			sc: scheduler{
				height: 1,
				peers: map[p2p.NodeID]*scPeer{
					"P1": {height: 4, state: peerStateReady},
					"P2": {height: 10, state: peerStateRemoved}},
			},
			wantMax: 4,
		},
		{
			name: "removed peers",
			sc: scheduler{
				height: 1,
				peers: map[p2p.NodeID]*scPeer{
					"P1": {height: 4, state: peerStateRemoved},
					"P2": {height: 10, state: peerStateRemoved}},
			},
			wantMax: 0,
		},
		{
			name: "new peers",
			sc: scheduler{
				height: 1,
				peers: map[p2p.NodeID]*scPeer{
					"P1": {base: -1, height: -1, state: peerStateNew},
					"P2": {base: -1, height: -1, state: peerStateNew}},
			},
			wantMax: 0,
		},
		{
			name: "mixed peers",
			sc: scheduler{
				height: 1,
				peers: map[p2p.NodeID]*scPeer{
					"P1": {height: -1, state: peerStateNew},
					"P2": {height: 10, state: peerStateReady},
					"P3": {height: 20, state: peerStateRemoved},
					"P4": {height: 22, state: peerStateReady},
				},
			},
			wantMax: 22,
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			// maxHeight() should not mutate the scheduler
			wantSc := tt.sc

			resMax := tt.sc.maxHeight()
			assert.Equal(t, tt.wantMax, resMax)
			assert.Equal(t, wantSc, tt.sc)
		})
	}
}

func TestScEnsurePeer(t *testing.T) {

	type args struct {
		peerID p2p.NodeID
	}
	tests := []struct {
		name       string
		fields     scTestParams
		args       args
		wantFields scTestParams
	}{
		{
			name:       "add first peer",
			fields:     scTestParams{},
			args:       args{peerID: "P1"},
			wantFields: scTestParams{peers: map[string]*scPeer{"P1": {base: -1, height: -1, state: peerStateNew}}},
		},
		{
			name:   "add second peer",
			fields: scTestParams{peers: map[string]*scPeer{"P1": {base: -1, height: -1, state: peerStateNew}}},
			args:   args{peerID: "P2"},
			wantFields: scTestParams{peers: map[string]*scPeer{
				"P1": {base: -1, height: -1, state: peerStateNew},
				"P2": {base: -1, height: -1, state: peerStateNew}}},
		},
		{
			name:       "add duplicate peer is fine",
			fields:     scTestParams{peers: map[string]*scPeer{"P1": {height: -1}}},
			args:       args{peerID: "P1"},
			wantFields: scTestParams{peers: map[string]*scPeer{"P1": {height: -1}}},
		},
		{
			name: "add duplicate peer with existing peer in Ready state is noop",
			fields: scTestParams{
				peers: map[string]*scPeer{"P1": {state: peerStateReady, height: 3}},
				allB:  []int64{1, 2, 3},
			},
			args: args{peerID: "P1"},
			wantFields: scTestParams{
				peers: map[string]*scPeer{"P1": {state: peerStateReady, height: 3}},
				allB:  []int64{1, 2, 3},
			},
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			sc := newTestScheduler(tt.fields)
			sc.ensurePeer(tt.args.peerID)
			wantSc := newTestScheduler(tt.wantFields)
			assert.Equal(t, wantSc, sc, "wanted peers %v, got %v", wantSc.peers, sc.peers)
		})
	}
}

func TestScTouchPeer(t *testing.T) {
	now := time.Now()

	type args struct {
		peerID p2p.NodeID
		time   time.Time
	}

	tests := []struct {
		name       string
		fields     scTestParams
		args       args
		wantFields scTestParams
		wantErr    bool
	}{
		{
			name: "attempt to touch non existing peer",
			fields: scTestParams{
				peers: map[string]*scPeer{"P1": {state: peerStateReady, height: 5}},
				allB:  []int64{1, 2, 3, 4, 5},
			},
			args: args{peerID: "P2", time: now},
			wantFields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateReady, height: 5}},
				allB: []int64{1, 2, 3, 4, 5},
			},
			wantErr: true,
		},
		{
			name:       "attempt to touch peer in state New",
			fields:     scTestParams{peers: map[string]*scPeer{"P1": {}}},
			args:       args{peerID: "P1", time: now},
			wantFields: scTestParams{peers: map[string]*scPeer{"P1": {}}},
			wantErr:    true,
		},
		{
			name:       "attempt to touch peer in state Removed",
			fields:     scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateRemoved}, "P2": {state: peerStateReady}}},
			args:       args{peerID: "P1", time: now},
			wantFields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateRemoved}, "P2": {state: peerStateReady}}},
			wantErr:    true,
		},
		{
			name:   "touch peer in state Ready",
			fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateReady, lastTouched: now}}},
			args:   args{peerID: "P1", time: now.Add(3 * time.Second)},
			wantFields: scTestParams{peers: map[string]*scPeer{
				"P1": {state: peerStateReady, lastTouched: now.Add(3 * time.Second)}}},
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			sc := newTestScheduler(tt.fields)
			if err := sc.touchPeer(tt.args.peerID, tt.args.time); (err != nil) != tt.wantErr {
				t.Errorf("touchPeer() wantErr %v, error = %v", tt.wantErr, err)
			}
			wantSc := newTestScheduler(tt.wantFields)
			assert.Equal(t, wantSc, sc, "wanted peers %v, got %v", wantSc.peers, sc.peers)
		})
	}
}

func TestScPrunablePeers(t *testing.T) {
	now := time.Now()

	type args struct {
		threshold time.Duration
		time      time.Time
		minSpeed  int64
	}

	tests := []struct {
		name       string
		fields     scTestParams
		args       args
		wantResult []p2p.NodeID
	}{
		{
			name:       "no peers",
			fields:     scTestParams{peers: map[string]*scPeer{}},
			args:       args{threshold: time.Second, time: now.Add(time.Second + time.Millisecond), minSpeed: 100},
			wantResult: []p2p.NodeID{},
		},
		{
			name: "mixed peers",
			fields: scTestParams{peers: map[string]*scPeer{
				// X - removed, active, fast
				"P1": {state: peerStateRemoved, lastTouched: now.Add(time.Second), lastRate: 101},
				// X - ready, active, fast
				"P2": {state: peerStateReady, lastTouched: now.Add(time.Second), lastRate: 101},
				// X - removed, active, equal
				"P3": {state: peerStateRemoved, lastTouched: now.Add(time.Second), lastRate: 100},
				// V - ready, inactive, equal
				"P4": {state: peerStateReady, lastTouched: now, lastRate: 100},
				// V - ready, inactive, slow
				"P5": {state: peerStateReady, lastTouched: now, lastRate: 99},
				//  V - ready, active, slow
				"P6": {state: peerStateReady, lastTouched: now.Add(time.Second), lastRate: 90},
			}},
			args:       args{threshold: time.Second, time: now.Add(time.Second + time.Millisecond), minSpeed: 100},
			wantResult: []p2p.NodeID{"P4", "P5", "P6"},
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			sc := newTestScheduler(tt.fields)
			// peersSlowerThan should not mutate the scheduler
			wantSc := sc
			res := sc.prunablePeers(tt.args.threshold, tt.args.minSpeed, tt.args.time)
			assert.Equal(t, tt.wantResult, res)
			assert.Equal(t, wantSc, sc)
		})
	}
}

func TestScRemovePeer(t *testing.T) {

	type args struct {
		peerID p2p.NodeID
	}
	tests := []struct {
		name       string
		fields     scTestParams
		args       args
		wantFields scTestParams
		wantErr    bool
	}{
		{
			name:       "remove non existing peer",
			fields:     scTestParams{peers: map[string]*scPeer{"P1": {height: -1}}},
			args:       args{peerID: "P2"},
			wantFields: scTestParams{peers: map[string]*scPeer{"P1": {height: -1}}},
		},
		{
			name:       "remove single New peer",
			fields:     scTestParams{peers: map[string]*scPeer{"P1": {height: -1}}},
			args:       args{peerID: "P1"},
			wantFields: scTestParams{peers: map[string]*scPeer{"P1": {height: -1, state: peerStateRemoved}}},
		},
		{
			name:       "remove one of two New peers",
			fields:     scTestParams{peers: map[string]*scPeer{"P1": {height: -1}, "P2": {height: -1}}},
			args:       args{peerID: "P1"},
			wantFields: scTestParams{peers: map[string]*scPeer{"P1": {height: -1, state: peerStateRemoved}, "P2": {height: -1}}},
		},
		{
			name: "remove one Ready peer, all peers removed",
			fields: scTestParams{
				peers: map[string]*scPeer{
					"P1": {height: 10, state: peerStateRemoved},
					"P2": {height: 5, state: peerStateReady}},
				allB: []int64{1, 2, 3, 4, 5},
			},
			args: args{peerID: "P2"},
			wantFields: scTestParams{peers: map[string]*scPeer{
				"P1": {height: 10, state: peerStateRemoved},
				"P2": {height: 5, state: peerStateRemoved}},
			},
		},
		{
			name: "attempt to remove already removed peer",
			fields: scTestParams{
				height: 8,
				peers: map[string]*scPeer{
					"P1": {height: 10, state: peerStateRemoved},
					"P2": {height: 11, state: peerStateReady}},
				allB: []int64{8, 9, 10, 11},
			},
			args: args{peerID: "P1"},
			wantFields: scTestParams{
				height: 8,
				peers: map[string]*scPeer{
					"P1": {height: 10, state: peerStateRemoved},
					"P2": {height: 11, state: peerStateReady}},
				allB: []int64{8, 9, 10, 11}},
		},
		{
			name: "remove Ready peer with blocks requested",
			fields: scTestParams{
				peers:   map[string]*scPeer{"P1": {height: 3, state: peerStateReady}},
				allB:    []int64{1, 2, 3},
				pending: map[int64]p2p.NodeID{1: "P1"},
			},
			args: args{peerID: "P1"},
			wantFields: scTestParams{
				peers:   map[string]*scPeer{"P1": {height: 3, state: peerStateRemoved}},
				allB:    []int64{},
				pending: map[int64]p2p.NodeID{},
			},
		},
		{
			name: "remove Ready peer with blocks received",
			fields: scTestParams{
				peers:    map[string]*scPeer{"P1": {height: 3, state: peerStateReady}},
				allB:     []int64{1, 2, 3},
				received: map[int64]p2p.NodeID{1: "P1"},
			},
			args: args{peerID: "P1"},
			wantFields: scTestParams{
				peers:    map[string]*scPeer{"P1": {height: 3, state: peerStateRemoved}},
				allB:     []int64{},
				received: map[int64]p2p.NodeID{},
			},
		},
		{
			name: "remove Ready peer with blocks received and requested (not yet received)",
			fields: scTestParams{
				peers:    map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
				allB:     []int64{1, 2, 3, 4},
				pending:  map[int64]p2p.NodeID{1: "P1", 3: "P1"},
				received: map[int64]p2p.NodeID{2: "P1", 4: "P1"},
			},
			args: args{peerID: "P1"},
			wantFields: scTestParams{
				peers:    map[string]*scPeer{"P1": {height: 4, state: peerStateRemoved}},
				allB:     []int64{},
				pending:  map[int64]p2p.NodeID{},
				received: map[int64]p2p.NodeID{},
			},
		},
		{
			name: "remove Ready peer from multiple peers set, with blocks received and requested (not yet received)",
			fields: scTestParams{
				peers: map[string]*scPeer{
					"P1": {height: 6, state: peerStateReady},
					"P2": {height: 6, state: peerStateReady},
				},
				allB:     []int64{1, 2, 3, 4, 5, 6},
				pending:  map[int64]p2p.NodeID{1: "P1", 3: "P2", 6: "P1"},
				received: map[int64]p2p.NodeID{2: "P1", 4: "P2", 5: "P2"},
			},
			args: args{peerID: "P1"},
			wantFields: scTestParams{
				peers: map[string]*scPeer{
					"P1": {height: 6, state: peerStateRemoved},
					"P2": {height: 6, state: peerStateReady},
				},
				allB:     []int64{1, 2, 3, 4, 5, 6},
				pending:  map[int64]p2p.NodeID{3: "P2"},
				received: map[int64]p2p.NodeID{4: "P2", 5: "P2"},
			},
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			sc := newTestScheduler(tt.fields)
			sc.removePeer(tt.args.peerID)
			wantSc := newTestScheduler(tt.wantFields)
			assert.Equal(t, wantSc, sc, "wanted peers %v, got %v", wantSc.peers, sc.peers)
		})
	}
}

func TestScSetPeerRange(t *testing.T) {

	type args struct {
		peerID p2p.NodeID
		base   int64
		height int64
	}
	tests := []struct {
		name       string
		fields     scTestParams
		args       args
		wantFields scTestParams
		wantErr    bool
	}{
		{
			name: "change height of non existing peer",
			fields: scTestParams{
				peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
				allB:  []int64{1, 2}},
			args: args{peerID: "P2", height: 4},
			wantFields: scTestParams{
				peers: map[string]*scPeer{
					"P1": {height: 2, state: peerStateReady},
					"P2": {height: 4, state: peerStateReady},
				},
				allB: []int64{1, 2, 3, 4}},
		},
		{
			name: "increase height of removed peer",
			fields: scTestParams{
				peers: map[string]*scPeer{"P1": {height: 2, state: peerStateRemoved}}},
			args:       args{peerID: "P1", height: 4},
			wantFields: scTestParams{peers: map[string]*scPeer{"P1": {height: 2, state: peerStateRemoved}}},
		},
		{
			name: "decrease height of single peer",
			fields: scTestParams{
				peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
				allB:  []int64{1, 2, 3, 4}},
			args: args{peerID: "P1", height: 2},
			wantFields: scTestParams{
				peers: map[string]*scPeer{"P1": {height: 4, state: peerStateRemoved}},
				allB:  []int64{}},
			wantErr: true,
		},
		{
			name: "increase height of single peer",
			fields: scTestParams{
				peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
				allB:  []int64{1, 2}},
			args: args{peerID: "P1", height: 4},
			wantFields: scTestParams{
				peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
				allB:  []int64{1, 2, 3, 4}},
		},
		{
			name: "noop height change of single peer",
			fields: scTestParams{
				peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
				allB:  []int64{1, 2, 3, 4}},
			args: args{peerID: "P1", height: 4},
			wantFields: scTestParams{
				peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
				allB:  []int64{1, 2, 3, 4}},
		},
		{
			name: "add peer with huge height 10**10 ",
			fields: scTestParams{
				peers:         map[string]*scPeer{"P2": {height: -1, state: peerStateNew}},
				targetPending: 4,
			},
			args: args{peerID: "P2", height: 10000000000},
			wantFields: scTestParams{
				targetPending: 4,
				peers:         map[string]*scPeer{"P2": {height: 10000000000, state: peerStateReady}},
				allB:          []int64{1, 2, 3, 4}},
		},
		{
			name: "add peer with base > height should error",
			fields: scTestParams{
				peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
				allB:  []int64{1, 2, 3, 4}},
			args: args{peerID: "P1", base: 6, height: 5},
			wantFields: scTestParams{
				peers: map[string]*scPeer{"P1": {height: 4, state: peerStateRemoved}}},
			wantErr: true,
		},
		{
			name: "add peer with base == height is fine",
			fields: scTestParams{
				peers:         map[string]*scPeer{"P1": {height: 4, state: peerStateNew}},
				targetPending: 4,
			},
			args: args{peerID: "P1", base: 6, height: 6},
			wantFields: scTestParams{
				targetPending: 4,
				peers:         map[string]*scPeer{"P1": {base: 6, height: 6, state: peerStateReady}},
				allB:          []int64{1, 2, 3, 4}},
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			sc := newTestScheduler(tt.fields)
			err := sc.setPeerRange(tt.args.peerID, tt.args.base, tt.args.height)
			if (err != nil) != tt.wantErr {
				t.Errorf("setPeerHeight() wantErr %v, error = %v", tt.wantErr, err)
			}
			wantSc := newTestScheduler(tt.wantFields)
			assert.Equal(t, wantSc, sc, "wanted peers %v, got %v", wantSc.peers, sc.peers)
		})
	}
}

func TestScGetPeersWithHeight(t *testing.T) {

	type args struct {
		height int64
	}
	tests := []struct {
		name       string
		fields     scTestParams
		args       args
		wantResult []p2p.NodeID
	}{
		{
			name:       "no peers",
			fields:     scTestParams{peers: map[string]*scPeer{}},
			args:       args{height: 10},
			wantResult: []p2p.NodeID{},
		},
		{
			name:       "only new peers",
			fields:     scTestParams{peers: map[string]*scPeer{"P1": {height: -1, state: peerStateNew}}},
			args:       args{height: 10},
			wantResult: []p2p.NodeID{},
		},
		{
			name:       "only Removed peers",
			fields:     scTestParams{peers: map[string]*scPeer{"P1": {height: 4, state: peerStateRemoved}}},
			args:       args{height: 2},
			wantResult: []p2p.NodeID{},
		},
		{
			name: "one Ready shorter peer",
			fields: scTestParams{
				peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
				allB:  []int64{1, 2, 3, 4},
			},
			args:       args{height: 5},
			wantResult: []p2p.NodeID{},
		},
		{
			name: "one Ready equal peer",
			fields: scTestParams{
				peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
				allB:  []int64{1, 2, 3, 4},
			},
			args:       args{height: 4},
			wantResult: []p2p.NodeID{"P1"},
		},
		{
			name: "one Ready higher peer",
			fields: scTestParams{
				targetPending: 4,
				peers:         map[string]*scPeer{"P1": {height: 20, state: peerStateReady}},
				allB:          []int64{1, 2, 3, 4},
			},
			args:       args{height: 4},
			wantResult: []p2p.NodeID{"P1"},
		},
		{
			name: "one Ready higher peer at base",
			fields: scTestParams{
				targetPending: 4,
				peers:         map[string]*scPeer{"P1": {base: 4, height: 20, state: peerStateReady}},
				allB:          []int64{1, 2, 3, 4},
			},
			args:       args{height: 4},
			wantResult: []p2p.NodeID{"P1"},
		},
		{
			name: "one Ready higher peer with higher base",
			fields: scTestParams{
				targetPending: 4,
				peers:         map[string]*scPeer{"P1": {base: 10, height: 20, state: peerStateReady}},
				allB:          []int64{1, 2, 3, 4},
			},
			args:       args{height: 4},
			wantResult: []p2p.NodeID{},
		},
		{
			name: "multiple mixed peers",
			fields: scTestParams{
				height: 8,
				peers: map[string]*scPeer{
					"P1": {height: -1, state: peerStateNew},
					"P2": {height: 10, state: peerStateReady},
					"P3": {height: 5, state: peerStateReady},
					"P4": {height: 20, state: peerStateRemoved},
					"P5": {height: 11, state: peerStateReady}},
				allB: []int64{8, 9, 10, 11},
			},
			args:       args{height: 8},
			wantResult: []p2p.NodeID{"P2", "P5"},
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			sc := newTestScheduler(tt.fields)
			// getPeersWithHeight should not mutate the scheduler
			wantSc := sc
			res := sc.getPeersWithHeight(tt.args.height)
			sort.Sort(PeerByID(res))
			assert.Equal(t, tt.wantResult, res)
			assert.Equal(t, wantSc, sc)
		})
	}
}

func TestScMarkPending(t *testing.T) {
	now := time.Now()

	type args struct {
		peerID p2p.NodeID
		height int64
		tm     time.Time
	}
	tests := []struct {
		name       string
		fields     scTestParams
		args       args
		wantFields scTestParams
		wantErr    bool
	}{
		{
			name: "attempt mark pending an unknown block above height",
			fields: scTestParams{
				peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
				allB:  []int64{1, 2}},
			args: args{peerID: "P1", height: 3, tm: now},
			wantFields: scTestParams{
				peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
				allB:  []int64{1, 2}},
			wantErr: true,
		},
		{
			name: "attempt mark pending an unknown block below base",
			fields: scTestParams{
				peers: map[string]*scPeer{"P1": {base: 4, height: 6, state: peerStateReady}},
				allB:  []int64{1, 2, 3, 4, 5, 6}},
			args: args{peerID: "P1", height: 3, tm: now},
			wantFields: scTestParams{
				peers: map[string]*scPeer{"P1": {base: 4, height: 6, state: peerStateReady}},
				allB:  []int64{1, 2, 3, 4, 5, 6}},
			wantErr: true,
		},
		{
			name: "attempt mark pending from non existing peer",
			fields: scTestParams{
				peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
				allB:  []int64{1, 2}},
			args: args{peerID: "P2", height: 1, tm: now},
			wantFields: scTestParams{
				peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
				allB:  []int64{1, 2}},
			wantErr: true,
		},
		{
			name: "mark pending from Removed peer",
			fields: scTestParams{
				peers: map[string]*scPeer{"P1": {height: 2, state: peerStateRemoved}}},
			args: args{peerID: "P1", height: 1, tm: now},
			wantFields: scTestParams{
				peers: map[string]*scPeer{"P1": {height: 2, state: peerStateRemoved}}},
			wantErr: true,
		},
		{
			name: "mark pending from New peer",
			fields: scTestParams{
				peers: map[string]*scPeer{
					"P1": {height: 4, state: peerStateReady},
					"P2": {height: 4, state: peerStateNew},
				},
				allB: []int64{1, 2, 3, 4},
			},
			args: args{peerID: "P2", height: 2, tm: now},
			wantFields: scTestParams{
				peers: map[string]*scPeer{
					"P1": {height: 4, state: peerStateReady},
					"P2": {height: 4, state: peerStateNew},
				},
				allB: []int64{1, 2, 3, 4},
			},
			wantErr: true,
		},
		{
			name: "mark pending from short peer",
			fields: scTestParams{
				peers: map[string]*scPeer{
					"P1": {height: 4, state: peerStateReady},
					"P2": {height: 2, state: peerStateReady},
				},
				allB: []int64{1, 2, 3, 4},
			},
			args: args{peerID: "P2", height: 3, tm: now},
			wantFields: scTestParams{
				peers: map[string]*scPeer{
					"P1": {height: 4, state: peerStateReady},
					"P2": {height: 2, state: peerStateReady},
				},
				allB: []int64{1, 2, 3, 4},
			},
			wantErr: true,
		},
		{
			name: "mark pending all good",
			fields: scTestParams{
				peers:       map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
				allB:        []int64{1, 2},
				pending:     map[int64]p2p.NodeID{1: "P1"},
				pendingTime: map[int64]time.Time{1: now},
			},
			args: args{peerID: "P1", height: 2, tm: now.Add(time.Millisecond)},
			wantFields: scTestParams{
				peers:       map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
				allB:        []int64{1, 2},
				pending:     map[int64]p2p.NodeID{1: "P1", 2: "P1"},
				pendingTime: map[int64]time.Time{1: now, 2: now.Add(time.Millisecond)},
			},
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			sc := newTestScheduler(tt.fields)
			if err := sc.markPending(tt.args.peerID, tt.args.height, tt.args.tm); (err != nil) != tt.wantErr {
				t.Errorf("markPending() wantErr %v, error = %v", tt.wantErr, err)
			}
			wantSc := newTestScheduler(tt.wantFields)
			assert.Equal(t, wantSc, sc)
		})
	}
}

func TestScMarkReceived(t *testing.T) {
	now := time.Now()

	type args struct {
		peerID p2p.NodeID
		height int64
		size   int64
		tm     time.Time
	}
	tests := []struct {
		name       string
		fields     scTestParams
		args       args
		wantFields scTestParams
		wantErr    bool
	}{
		{
			name: "received from non existing peer",
			fields: scTestParams{
				peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
				allB:  []int64{1, 2}},
			args: args{peerID: "P2", height: 1, size: 1000, tm: now},
			wantFields: scTestParams{
				peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
				allB:  []int64{1, 2}},
			wantErr: true,
		},
		{
			name: "received from removed peer",
			fields: scTestParams{
				peers: map[string]*scPeer{"P1": {height: 2, state: peerStateRemoved}}},
			args: args{peerID: "P1", height: 1, size: 1000, tm: now},
			wantFields: scTestParams{
				peers: map[string]*scPeer{"P1": {height: 2, state: peerStateRemoved}}},
			wantErr: true,
		},
		{
			name: "received from unsolicited peer",
			fields: scTestParams{
				peers: map[string]*scPeer{
					"P1": {height: 4, state: peerStateReady},
					"P2": {height: 4, state: peerStateReady},
				},
				allB:    []int64{1, 2, 3, 4},
				pending: map[int64]p2p.NodeID{1: "P1", 2: "P2", 3: "P2", 4: "P1"},
			},
			args: args{peerID: "P1", height: 2, size: 1000, tm: now},
			wantFields: scTestParams{
				peers: map[string]*scPeer{
					"P1": {height: 4, state: peerStateReady},
					"P2": {height: 4, state: peerStateReady},
				},
				allB:    []int64{1, 2, 3, 4},
				pending: map[int64]p2p.NodeID{1: "P1", 2: "P2", 3: "P2", 4: "P1"},
			},
			wantErr: true,
		},
		{
			name: "received but blockRequest not sent",
			fields: scTestParams{
				peers:   map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
				allB:    []int64{1, 2, 3, 4},
				pending: map[int64]p2p.NodeID{},
			},
			args: args{peerID: "P1", height: 2, size: 1000, tm: now},
			wantFields: scTestParams{
				peers:   map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
				allB:    []int64{1, 2, 3, 4},
				pending: map[int64]p2p.NodeID{},
			},
			wantErr: true,
		},
		{
			name: "received with bad timestamp",
			fields: scTestParams{
				peers:       map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
				allB:        []int64{1, 2},
				pending:     map[int64]p2p.NodeID{1: "P1", 2: "P1"},
				pendingTime: map[int64]time.Time{1: now, 2: now.Add(time.Second)},
			},
			args: args{peerID: "P1", height: 2, size: 1000, tm: now},
			wantFields: scTestParams{
				peers:       map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
				allB:        []int64{1, 2},
				pending:     map[int64]p2p.NodeID{1: "P1", 2: "P1"},
				pendingTime: map[int64]time.Time{1: now, 2: now.Add(time.Second)},
			},
			wantErr: true,
		},
		{
			name: "received all good",
			fields: scTestParams{
				peers:       map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
				allB:        []int64{1, 2},
				pending:     map[int64]p2p.NodeID{1: "P1", 2: "P1"},
				pendingTime: map[int64]time.Time{1: now, 2: now},
			},
			args: args{peerID: "P1", height: 2, size: 1000, tm: now.Add(time.Millisecond)},
			wantFields: scTestParams{
				peers:       map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
				allB:        []int64{1, 2},
				pending:     map[int64]p2p.NodeID{1: "P1"},
				pendingTime: map[int64]time.Time{1: now},
				received:    map[int64]p2p.NodeID{2: "P1"},
			},
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			sc := newTestScheduler(tt.fields)
			if err := sc.markReceived(
				tt.args.peerID,
				tt.args.height,
				tt.args.size,
				now.Add(time.Second)); (err != nil) != tt.wantErr {
				t.Errorf("markReceived() wantErr %v, error = %v", tt.wantErr, err)
			}
			wantSc := newTestScheduler(tt.wantFields)
			assert.Equal(t, wantSc, sc)
		})
	}
}

func TestScMarkProcessed(t *testing.T) {
	now := time.Now()

	type args struct {
		height int64
	}
	tests := []struct {
		name       string
		fields     scTestParams
		args       args
		wantFields scTestParams
		wantErr    bool
	}{
		{
			name: "processed an unreceived block",
			fields: scTestParams{
				height:        2,
				peers:         map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
				allB:          []int64{2},
				pending:       map[int64]p2p.NodeID{2: "P1"},
				pendingTime:   map[int64]time.Time{2: now},
				targetPending: 1,
			},
			args: args{height: 2},
			wantFields: scTestParams{
				height:        3,
				peers:         map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
				allB:          []int64{3},
				targetPending: 1,
			},
		},
		{
			name: "mark processed success",
			fields: scTestParams{
				height:      1,
				peers:       map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
				allB:        []int64{1, 2},
				pending:     map[int64]p2p.NodeID{2: "P1"},
				pendingTime: map[int64]time.Time{2: now},
				received:    map[int64]p2p.NodeID{1: "P1"}},
			args: args{height: 1},
			wantFields: scTestParams{
				height:      2,
				peers:       map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
				allB:        []int64{2},
				pending:     map[int64]p2p.NodeID{2: "P1"},
				pendingTime: map[int64]time.Time{2: now}},
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			sc := newTestScheduler(tt.fields)
			oldBlockState := sc.getStateAtHeight(tt.args.height)
			if err := sc.markProcessed(tt.args.height); (err != nil) != tt.wantErr {
				t.Errorf("markProcessed() wantErr %v, error = %v", tt.wantErr, err)
			}
			if tt.wantErr {
				assert.Equal(t, oldBlockState, sc.getStateAtHeight(tt.args.height))
			} else {
				assert.Equal(t, blockStateProcessed, sc.getStateAtHeight(tt.args.height))
			}
			wantSc := newTestScheduler(tt.wantFields)
			checkSameScheduler(t, wantSc, sc)
		})
	}
}

func TestScResetState(t *testing.T) {
	tests := []struct {
		name       string
		fields     scTestParams
		state      state.State
		wantFields scTestParams
	}{
		{
			name: "updates height and initHeight",
			fields: scTestParams{
				height:     0,
				initHeight: 0,
			},
			state: state.State{LastBlockHeight: 7},
			wantFields: scTestParams{
				height:     8,
				initHeight: 8,
			},
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			sc := newTestScheduler(tt.fields)
			e, err := sc.handleResetState(bcResetState{state: tt.state})
			require.NoError(t, err)
			assert.Equal(t, e, noOp)
			wantSc := newTestScheduler(tt.wantFields)
			checkSameScheduler(t, wantSc, sc)
		})
	}
}

func TestScAllBlocksProcessed(t *testing.T) {
	now := time.Now()

	tests := []struct {
		name       string
		fields     scTestParams
		wantResult bool
	}{
		{
			name:       "no blocks, no peers",
			fields:     scTestParams{},
			wantResult: false,
		},
		{
			name: "only New blocks",
			fields: scTestParams{
				peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
				allB:  []int64{1, 2, 3, 4},
			},
			wantResult: false,
		},
		{
			name: "only Pending blocks",
			fields: scTestParams{
				peers:       map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
				allB:        []int64{1, 2, 3, 4},
				pending:     map[int64]p2p.NodeID{1: "P1", 2: "P1", 3: "P1", 4: "P1"},
				pendingTime: map[int64]time.Time{1: now, 2: now, 3: now, 4: now},
			},
			wantResult: false,
		},
		{
			name: "only Received blocks",
			fields: scTestParams{
				peers:    map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
				allB:     []int64{1, 2, 3, 4},
				received: map[int64]p2p.NodeID{1: "P1", 2: "P1", 3: "P1", 4: "P1"},
			},
			wantResult: false,
		},
		{
			name: "only Processed blocks plus highest is received",
			fields: scTestParams{
				height: 4,
				peers: map[string]*scPeer{
					"P1": {height: 4, state: peerStateReady}},
				allB:     []int64{4},
				received: map[int64]p2p.NodeID{4: "P1"},
			},
			wantResult: true,
		},
		{
			name: "mixed block states",
			fields: scTestParams{
				peers:       map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
				allB:        []int64{1, 2, 3, 4},
				pending:     map[int64]p2p.NodeID{2: "P1", 4: "P1"},
				pendingTime: map[int64]time.Time{2: now, 4: now},
			},
			wantResult: false,
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			sc := newTestScheduler(tt.fields)
			// allBlocksProcessed() should not mutate the scheduler
			wantSc := sc
			res := sc.allBlocksProcessed()
			assert.Equal(t, tt.wantResult, res)
			checkSameScheduler(t, wantSc, sc)
		})
	}
}

func TestScNextHeightToSchedule(t *testing.T) {
	now := time.Now()

	tests := []struct {
		name       string
		fields     scTestParams
		wantHeight int64
	}{
		{
			name:       "no blocks",
			fields:     scTestParams{initHeight: 11, height: 11},
			wantHeight: -1,
		},
		{
			name: "only New blocks",
			fields: scTestParams{
				initHeight: 3,
				peers:      map[string]*scPeer{"P1": {height: 6, state: peerStateReady}},
				allB:       []int64{3, 4, 5, 6},
			},
			wantHeight: 3,
		},
		{
			name: "only Pending blocks",
			fields: scTestParams{
				initHeight:  1,
				peers:       map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
				allB:        []int64{1, 2, 3, 4},
				pending:     map[int64]p2p.NodeID{1: "P1", 2: "P1", 3: "P1", 4: "P1"},
				pendingTime: map[int64]time.Time{1: now, 2: now, 3: now, 4: now},
			},
			wantHeight: -1,
		},
		{
			name: "only Received blocks",
			fields: scTestParams{
				initHeight: 1,
				peers:      map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
				allB:       []int64{1, 2, 3, 4},
				received:   map[int64]p2p.NodeID{1: "P1", 2: "P1", 3: "P1", 4: "P1"},
			},
			wantHeight: -1,
		},
		{
			name: "only Processed blocks",
			fields: scTestParams{
				initHeight: 1,
				peers:      map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
				allB:       []int64{1, 2, 3, 4},
			},
			wantHeight: 1,
		},
		{
			name: "mixed block states",
			fields: scTestParams{
				initHeight:  1,
				peers:       map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
				allB:        []int64{1, 2, 3, 4},
				pending:     map[int64]p2p.NodeID{2: "P1"},
				pendingTime: map[int64]time.Time{2: now},
			},
			wantHeight: 1,
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			sc := newTestScheduler(tt.fields)
			// nextHeightToSchedule() should not mutate the scheduler
			wantSc := sc

			resMin := sc.nextHeightToSchedule()
			assert.Equal(t, tt.wantHeight, resMin)
			checkSameScheduler(t, wantSc, sc)
		})
	}
}

func TestScSelectPeer(t *testing.T) {

	type args struct {
		height int64
	}
	tests := []struct {
		name       string
		fields     scTestParams
		args       args
		wantResult p2p.NodeID
		wantError  bool
	}{
		{
			name:       "no peers",
			fields:     scTestParams{peers: map[string]*scPeer{}},
			args:       args{height: 10},
			wantResult: "",
			wantError:  true,
		},
		{
			name:       "only new peers",
			fields:     scTestParams{peers: map[string]*scPeer{"P1": {height: -1, state: peerStateNew}}},
			args:       args{height: 10},
			wantResult: "",
			wantError:  true,
		},
		{
			name:       "only Removed peers",
			fields:     scTestParams{peers: map[string]*scPeer{"P1": {height: 4, state: peerStateRemoved}}},
			args:       args{height: 2},
			wantResult: "",
			wantError:  true,
		},
		{
			name: "one Ready shorter peer",
			fields: scTestParams{
				peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
				allB:  []int64{1, 2, 3, 4},
			},
			args:       args{height: 5},
			wantResult: "",
			wantError:  true,
		},
		{
			name: "one Ready equal peer",
			fields: scTestParams{peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
				allB: []int64{1, 2, 3, 4},
			},
			args:       args{height: 4},
			wantResult: "P1",
		},
		{
			name: "one Ready higher peer",
			fields: scTestParams{peers: map[string]*scPeer{"P1": {height: 6, state: peerStateReady}},
				allB: []int64{1, 2, 3, 4, 5, 6},
			},
			args:       args{height: 4},
			wantResult: "P1",
		},
		{
			name: "one Ready higher peer with higher base",
			fields: scTestParams{
				peers: map[string]*scPeer{"P1": {base: 4, height: 6, state: peerStateReady}},
				allB:  []int64{1, 2, 3, 4, 5, 6},
			},
			args:       args{height: 3},
			wantResult: "",
			wantError:  true,
		},
		{
			name: "many Ready higher peers with different number of pending requests",
			fields: scTestParams{
				height: 4,
				peers: map[string]*scPeer{
					"P1": {height: 8, state: peerStateReady},
					"P2": {height: 9, state: peerStateReady}},
				allB: []int64{4, 5, 6, 7, 8, 9},
				pending: map[int64]p2p.NodeID{
					4: "P1", 6: "P1",
					5: "P2",
				},
			},
			args:       args{height: 4},
			wantResult: "P2",
		},
		{
			name: "many Ready higher peers with same number of pending requests",
			fields: scTestParams{
				peers: map[string]*scPeer{
					"P2": {height: 20, state: peerStateReady},
					"P1": {height: 15, state: peerStateReady},
					"P3": {height: 15, state: peerStateReady}},
				allB: []int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
				pending: map[int64]p2p.NodeID{
					1: "P1", 2: "P1",
					3: "P3", 4: "P3",
					5: "P2", 6: "P2",
				},
			},
			args:       args{height: 7},
			wantResult: "P1",
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			sc := newTestScheduler(tt.fields)
			// selectPeer should not mutate the scheduler
			wantSc := sc
			res, err := sc.selectPeer(tt.args.height)
			assert.Equal(t, tt.wantResult, res)
			assert.Equal(t, tt.wantError, err != nil)
			checkSameScheduler(t, wantSc, sc)
		})
	}
}

// makeScBlock makes an empty block.
func makeScBlock(height int64) *types.Block {
	return &types.Block{Header: types.Header{Height: height}}
}

// used in place of assert.Equal(t, want, actual) to avoid failures due to
// scheduler.lastAdvanced timestamp inequalities.
func checkSameScheduler(t *testing.T, want *scheduler, actual *scheduler) {
	assert.Equal(t, want.initHeight, actual.initHeight)
	assert.Equal(t, want.height, actual.height)
	assert.Equal(t, want.peers, actual.peers)
	assert.Equal(t, want.blockStates, actual.blockStates)
	assert.Equal(t, want.pendingBlocks, actual.pendingBlocks)
	assert.Equal(t, want.pendingTime, actual.pendingTime)
	assert.Equal(t, want.blockStates, actual.blockStates)
	assert.Equal(t, want.receivedBlocks, actual.receivedBlocks)
	assert.Equal(t, want.blockStates, actual.blockStates)
}

// checkScResults checks scheduler handler test results
func checkScResults(t *testing.T, wantErr bool, err error, wantEvent Event, event Event) {
	if (err != nil) != wantErr {
		t.Errorf("error = %v, wantErr %v", err, wantErr)
		return
	}
	if !assert.IsType(t, wantEvent, event) {
		t.Log(fmt.Sprintf("Wrong type received, got: %v", event))
	}
	switch wantEvent := wantEvent.(type) {
	case scPeerError:
		assert.Equal(t, wantEvent.peerID, event.(scPeerError).peerID)
		assert.Equal(t, wantEvent.reason != nil, event.(scPeerError).reason != nil)
	case scBlockReceived:
		assert.Equal(t, wantEvent.peerID, event.(scBlockReceived).peerID)
		assert.Equal(t, wantEvent.block, event.(scBlockReceived).block)
	case scSchedulerFail:
		assert.Equal(t, wantEvent.reason != nil, event.(scSchedulerFail).reason != nil)
	}
}

func TestScHandleBlockResponse(t *testing.T) {
	now := time.Now()
	block6FromP1 := bcBlockResponse{
		time:   now.Add(time.Millisecond),
		peerID: p2p.NodeID("P1"),
		size:   100,
		block:  makeScBlock(6),
	}

	type args struct {
		event bcBlockResponse
	}

	tests := []struct {
		name      string
		fields    scTestParams
		args      args
		wantEvent Event
		wantErr   bool
	}{
		{
			name:      "empty scheduler",
			fields:    scTestParams{},
			args:      args{event: block6FromP1},
			wantEvent: noOpEvent{},
		},
		{
			name:      "block from removed peer",
			fields:    scTestParams{peers: map[string]*scPeer{"P1": {height: 8, state: peerStateRemoved}}},
			args:      args{event: block6FromP1},
			wantEvent: noOpEvent{},
		},
		{
			name: "block we haven't asked for",
			fields: scTestParams{
				peers: map[string]*scPeer{"P1": {height: 8, state: peerStateReady}},
				allB:  []int64{1, 2, 3, 4, 5, 6, 7, 8}},
			args:      args{event: block6FromP1},
			wantEvent: scPeerError{peerID: "P1", reason: fmt.Errorf("some error")},
		},
		{
			name: "block from wrong peer",
			fields: scTestParams{
				peers:       map[string]*scPeer{"P2": {height: 8, state: peerStateReady}},
				allB:        []int64{1, 2, 3, 4, 5, 6, 7, 8},
				pending:     map[int64]p2p.NodeID{6: "P2"},
				pendingTime: map[int64]time.Time{6: now},
			},
			args:      args{event: block6FromP1},
			wantEvent: noOpEvent{},
		},
		{
			name: "block with bad timestamp",
			fields: scTestParams{
				peers:       map[string]*scPeer{"P1": {height: 8, state: peerStateReady}},
				allB:        []int64{1, 2, 3, 4, 5, 6, 7, 8},
				pending:     map[int64]p2p.NodeID{6: "P1"},
				pendingTime: map[int64]time.Time{6: now.Add(time.Second)},
			},
			args:      args{event: block6FromP1},
			wantEvent: scPeerError{peerID: "P1", reason: fmt.Errorf("some error")},
		},
		{
			name: "good block, accept",
			fields: scTestParams{
				peers:       map[string]*scPeer{"P1": {height: 8, state: peerStateReady}},
				allB:        []int64{1, 2, 3, 4, 5, 6, 7, 8},
				pending:     map[int64]p2p.NodeID{6: "P1"},
				pendingTime: map[int64]time.Time{6: now},
			},
			args:      args{event: block6FromP1},
			wantEvent: scBlockReceived{peerID: "P1", block: block6FromP1.block},
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			sc := newTestScheduler(tt.fields)
			event, err := sc.handleBlockResponse(tt.args.event)
			checkScResults(t, tt.wantErr, err, tt.wantEvent, event)
		})
	}
}

func TestScHandleNoBlockResponse(t *testing.T) {
	now := time.Now()
	noBlock6FromP1 := bcNoBlockResponse{
		time:   now.Add(time.Millisecond),
		peerID: p2p.NodeID("P1"),
		height: 6,
	}

	tests := []struct {
		name       string
		fields     scTestParams
		wantEvent  Event
		wantFields scTestParams
		wantErr    bool
	}{
		{
			name:       "empty scheduler",
			fields:     scTestParams{},
			wantEvent:  noOpEvent{},
			wantFields: scTestParams{},
		},
		{
			name:       "noBlock from removed peer",
			fields:     scTestParams{peers: map[string]*scPeer{"P1": {height: 8, state: peerStateRemoved}}},
			wantEvent:  noOpEvent{},
			wantFields: scTestParams{peers: map[string]*scPeer{"P1": {height: 8, state: peerStateRemoved}}},
		},
		{
			name: "for block we haven't asked for",
			fields: scTestParams{
				peers: map[string]*scPeer{"P1": {height: 8, state: peerStateReady}},
				allB:  []int64{1, 2, 3, 4, 5, 6, 7, 8}},
			wantEvent:  scPeerError{peerID: "P1", reason: fmt.Errorf("some error")},
			wantFields: scTestParams{peers: map[string]*scPeer{"P1": {height: 8, state: peerStateRemoved}}},
		},
		{
			name: "noBlock from peer we don't have",
			fields: scTestParams{
				peers:       map[string]*scPeer{"P2": {height: 8, state: peerStateReady}},
				allB:        []int64{1, 2, 3, 4, 5, 6, 7, 8},
				pending:     map[int64]p2p.NodeID{6: "P2"},
				pendingTime: map[int64]time.Time{6: now},
			},
			wantEvent: noOpEvent{},
			wantFields: scTestParams{
				peers:       map[string]*scPeer{"P2": {height: 8, state: peerStateReady}},
				allB:        []int64{1, 2, 3, 4, 5, 6, 7, 8},
				pending:     map[int64]p2p.NodeID{6: "P2"},
				pendingTime: map[int64]time.Time{6: now},
			},
		},
		{
			name: "noBlock from existing peer",
			fields: scTestParams{
				peers:       map[string]*scPeer{"P1": {height: 8, state: peerStateReady}},
				allB:        []int64{1, 2, 3, 4, 5, 6, 7, 8},
				pending:     map[int64]p2p.NodeID{6: "P1"},
				pendingTime: map[int64]time.Time{6: now},
			},
			wantEvent:  scPeerError{peerID: "P1", reason: fmt.Errorf("some error")},
			wantFields: scTestParams{peers: map[string]*scPeer{"P1": {height: 8, state: peerStateRemoved}}},
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			sc := newTestScheduler(tt.fields)
			event, err := sc.handleNoBlockResponse(noBlock6FromP1)
			checkScResults(t, tt.wantErr, err, tt.wantEvent, event)
			wantSc := newTestScheduler(tt.wantFields)
			assert.Equal(t, wantSc, sc)
		})
	}
}

func TestScHandleBlockProcessed(t *testing.T) {
	now := time.Now()
	processed6FromP1 := pcBlockProcessed{
		peerID: p2p.NodeID("P1"),
		height: 6,
	}

	type args struct {
		event pcBlockProcessed
	}

	tests := []struct {
		name      string
		fields    scTestParams
		args      args
		wantEvent Event
		wantErr   bool
	}{
		{
			name:      "empty scheduler",
			fields:    scTestParams{height: 6},
			args:      args{event: processed6FromP1},
			wantEvent: noOpEvent{},
		},
		{
			name: "processed block we don't have",
			fields: scTestParams{
				initHeight:  6,
				peers:       map[string]*scPeer{"P1": {height: 8, state: peerStateReady}},
				allB:        []int64{6, 7, 8},
				pending:     map[int64]p2p.NodeID{6: "P1"},
				pendingTime: map[int64]time.Time{6: now},
			},
			args:      args{event: processed6FromP1},
			wantEvent: noOpEvent{},
		},
		{
			name: "processed block ok, we processed all blocks",
			fields: scTestParams{
				initHeight: 6,
				peers:      map[string]*scPeer{"P1": {height: 7, state: peerStateReady}},
				allB:       []int64{6, 7},
				received:   map[int64]p2p.NodeID{6: "P1", 7: "P1"},
			},
			args:      args{event: processed6FromP1},
			wantEvent: scFinishedEv{},
		},
		{
			name: "processed block ok, we still have blocks to process",
			fields: scTestParams{
				initHeight: 6,
				peers:      map[string]*scPeer{"P1": {height: 8, state: peerStateReady}},
				allB:       []int64{6, 7, 8},
				pending:    map[int64]p2p.NodeID{7: "P1", 8: "P1"},
				received:   map[int64]p2p.NodeID{6: "P1"},
			},
			args:      args{event: processed6FromP1},
			wantEvent: noOpEvent{},
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			sc := newTestScheduler(tt.fields)
			event, err := sc.handleBlockProcessed(tt.args.event)
			checkScResults(t, tt.wantErr, err, tt.wantEvent, event)
		})
	}
}

func TestScHandleBlockVerificationFailure(t *testing.T) {
	now := time.Now()

	type args struct {
		event pcBlockVerificationFailure
	}

	tests := []struct {
		name      string
		fields    scTestParams
		args      args
		wantEvent Event
		wantErr   bool
	}{
		{
			name:      "empty scheduler",
			fields:    scTestParams{},
			args:      args{event: pcBlockVerificationFailure{height: 10, firstPeerID: "P1", secondPeerID: "P1"}},
			wantEvent: noOpEvent{},
		},
		{
			name: "failed block we don't have, single peer is still removed",
			fields: scTestParams{
				initHeight:  6,
				peers:       map[string]*scPeer{"P1": {height: 8, state: peerStateReady}},
				allB:        []int64{6, 7, 8},
				pending:     map[int64]p2p.NodeID{6: "P1"},
				pendingTime: map[int64]time.Time{6: now},
			},
			args:      args{event: pcBlockVerificationFailure{height: 10, firstPeerID: "P1", secondPeerID: "P1"}},
			wantEvent: scFinishedEv{},
		},
		{
			name: "failed block we don't have, one of two peers are removed",
			fields: scTestParams{
				initHeight:  6,
				peers:       map[string]*scPeer{"P1": {height: 8, state: peerStateReady}, "P2": {height: 8, state: peerStateReady}},
				allB:        []int64{6, 7, 8},
				pending:     map[int64]p2p.NodeID{6: "P1"},
				pendingTime: map[int64]time.Time{6: now},
			},
			args:      args{event: pcBlockVerificationFailure{height: 10, firstPeerID: "P1", secondPeerID: "P1"}},
			wantEvent: noOpEvent{},
		},
		{
			name: "failed block, all blocks are processed after removal",
			fields: scTestParams{
				initHeight: 6,
				peers:      map[string]*scPeer{"P1": {height: 7, state: peerStateReady}},
				allB:       []int64{6, 7},
				received:   map[int64]p2p.NodeID{6: "P1", 7: "P1"},
			},
			args:      args{event: pcBlockVerificationFailure{height: 7, firstPeerID: "P1", secondPeerID: "P1"}},
			wantEvent: scFinishedEv{},
		},
		{
			name: "failed block, we still have blocks to process",
			fields: scTestParams{
				initHeight: 5,
				peers:      map[string]*scPeer{"P1": {height: 8, state: peerStateReady}, "P2": {height: 8, state: peerStateReady}},
				allB:       []int64{5, 6, 7, 8},
				pending:    map[int64]p2p.NodeID{7: "P1", 8: "P1"},
				received:   map[int64]p2p.NodeID{5: "P1", 6: "P1"},
			},
			args:      args{event: pcBlockVerificationFailure{height: 5, firstPeerID: "P1", secondPeerID: "P1"}},
			wantEvent: noOpEvent{},
		},
		{
			name: "failed block, H+1 and H+2 delivered by different peers, we still have blocks to process",
			fields: scTestParams{
				initHeight: 5,
				peers: map[string]*scPeer{
					"P1": {height: 8, state: peerStateReady},
					"P2": {height: 8, state: peerStateReady},
					"P3": {height: 8, state: peerStateReady},
				},
				allB:     []int64{5, 6, 7, 8},
				pending:  map[int64]p2p.NodeID{7: "P1", 8: "P1"},
				received: map[int64]p2p.NodeID{5: "P1", 6: "P1"},
			},
			args:      args{event: pcBlockVerificationFailure{height: 5, firstPeerID: "P1", secondPeerID: "P2"}},
			wantEvent: noOpEvent{},
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			sc := newTestScheduler(tt.fields)
			event, err := sc.handleBlockProcessError(tt.args.event)
			checkScResults(t, tt.wantErr, err, tt.wantEvent, event)
		})
	}
}

func TestScHandleAddNewPeer(t *testing.T) {
	addP1 := bcAddNewPeer{
		peerID: p2p.NodeID("P1"),
	}
	type args struct {
		event bcAddNewPeer
	}

	tests := []struct {
		name      string
		fields    scTestParams
		args      args
		wantEvent Event
		wantErr   bool
	}{
		{
			name:      "add P1 to empty scheduler",
			fields:    scTestParams{},
			args:      args{event: addP1},
			wantEvent: noOpEvent{},
		},
		{
			name: "add duplicate peer",
			fields: scTestParams{
				initHeight: 6,
				peers:      map[string]*scPeer{"P1": {height: 8, state: peerStateReady}},
				allB:       []int64{6, 7, 8},
			},
			args:      args{event: addP1},
			wantEvent: noOpEvent{},
		},
		{
			name: "add P1 to non empty scheduler",
			fields: scTestParams{
				initHeight: 6,
				peers:      map[string]*scPeer{"P2": {height: 8, state: peerStateReady}},
				allB:       []int64{6, 7, 8},
			},
			args:      args{event: addP1},
			wantEvent: noOpEvent{},
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			sc := newTestScheduler(tt.fields)
			event, err := sc.handleAddNewPeer(tt.args.event)
			checkScResults(t, tt.wantErr, err, tt.wantEvent, event)
		})
	}
}

func TestScHandleTryPrunePeer(t *testing.T) {
	now := time.Now()

	pruneEv := rTryPrunePeer{
		time: now.Add(time.Second + time.Millisecond),
	}
	type args struct {
		event rTryPrunePeer
	}

	tests := []struct {
		name      string
		fields    scTestParams
		args      args
		wantEvent Event
		wantErr   bool
	}{
		{
			name:      "no peers",
			fields:    scTestParams{},
			args:      args{event: pruneEv},
			wantEvent: noOpEvent{},
		},
		{
			name: "no prunable peers",
			fields: scTestParams{
				minRecvRate: 100,
				peers: map[string]*scPeer{
					// X - removed, active, fast
					"P1": {state: peerStateRemoved, lastTouched: now.Add(time.Second), lastRate: 101},
					// X - ready, active, fast
					"P2": {state: peerStateReady, lastTouched: now.Add(time.Second), lastRate: 101},
					// X - removed, active, equal
					"P3": {state: peerStateRemoved, lastTouched: now.Add(time.Second), lastRate: 100}},
				peerTimeout: time.Second,
			},
			args:      args{event: pruneEv},
			wantEvent: noOpEvent{},
		},
		{
			name: "mixed peers",
			fields: scTestParams{
				minRecvRate: 100,
				peers: map[string]*scPeer{
					// X - removed, active, fast
					"P1": {state: peerStateRemoved, lastTouched: now.Add(time.Second), lastRate: 101, height: 5},
					// X - ready, active, fast
					"P2": {state: peerStateReady, lastTouched: now.Add(time.Second), lastRate: 101, height: 5},
					// X - removed, active, equal
					"P3": {state: peerStateRemoved, lastTouched: now.Add(time.Second), lastRate: 100, height: 5},
					// V - ready, inactive, equal
					"P4": {state: peerStateReady, lastTouched: now, lastRate: 100, height: 7},
					// V - ready, inactive, slow
					"P5": {state: peerStateReady, lastTouched: now, lastRate: 99, height: 7},
					//  V - ready, active, slow
					"P6": {state: peerStateReady, lastTouched: now.Add(time.Second), lastRate: 90, height: 7},
				},
				allB:        []int64{1, 2, 3, 4, 5, 6, 7},
				peerTimeout: time.Second},
			args:      args{event: pruneEv},
			wantEvent: scPeersPruned{peers: []p2p.NodeID{"P4", "P5", "P6"}},
		},
		{
			name: "mixed peers, finish after pruning",
			fields: scTestParams{
				minRecvRate: 100,
				height:      6,
				peers: map[string]*scPeer{
					// X - removed, active, fast
					"P1": {state: peerStateRemoved, lastTouched: now.Add(time.Second), lastRate: 101, height: 5},
					// X - ready, active, fast
					"P2": {state: peerStateReady, lastTouched: now.Add(time.Second), lastRate: 101, height: 5},
					// X - removed, active, equal
					"P3": {state: peerStateRemoved, lastTouched: now.Add(time.Second), lastRate: 100, height: 5},
					// V - ready, inactive, equal
					"P4": {state: peerStateReady, lastTouched: now, lastRate: 100, height: 7},
					// V - ready, inactive, slow
					"P5": {state: peerStateReady, lastTouched: now, lastRate: 99, height: 7},
					//  V - ready, active, slow
					"P6": {state: peerStateReady, lastTouched: now.Add(time.Second), lastRate: 90, height: 7},
				},
				allB:        []int64{6, 7},
				peerTimeout: time.Second},
			args:      args{event: pruneEv},
			wantEvent: scFinishedEv{},
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			sc := newTestScheduler(tt.fields)
			event, err := sc.handleTryPrunePeer(tt.args.event)
			checkScResults(t, tt.wantErr, err, tt.wantEvent, event)
		})
	}
}

func TestScHandleTrySchedule(t *testing.T) {
	now := time.Now()
	tryEv := rTrySchedule{
		time: now.Add(time.Second + time.Millisecond),
	}

	type args struct {
		event rTrySchedule
	}
	tests := []struct {
		name      string
		fields    scTestParams
		args      args
		wantEvent Event
		wantErr   bool
	}{
		{
			name:      "no peers",
			fields:    scTestParams{startTime: now, peers: map[string]*scPeer{}},
			args:      args{event: tryEv},
			wantEvent: noOpEvent{},
		},
		{
			name:      "only new peers",
			fields:    scTestParams{startTime: now, peers: map[string]*scPeer{"P1": {height: -1, state: peerStateNew}}},
			args:      args{event: tryEv},
			wantEvent: noOpEvent{},
		},
		{
			name:      "only Removed peers",
			fields:    scTestParams{startTime: now, peers: map[string]*scPeer{"P1": {height: 4, state: peerStateRemoved}}},
			args:      args{event: tryEv},
			wantEvent: noOpEvent{},
		},
		{
			name: "one Ready shorter peer",
			fields: scTestParams{
				startTime: now,
				height:    6,
				peers:     map[string]*scPeer{"P1": {height: 4, state: peerStateReady}}},
			args:      args{event: tryEv},
			wantEvent: noOpEvent{},
		},
		{
			name: "one Ready equal peer",
			fields: scTestParams{
				startTime: now,
				peers:     map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
				allB:      []int64{1, 2, 3, 4}},
			args:      args{event: tryEv},
			wantEvent: scBlockRequest{peerID: "P1", height: 1},
		},
		{
			name: "many Ready higher peers with different number of pending requests",
			fields: scTestParams{
				startTime: now,
				peers: map[string]*scPeer{
					"P1": {height: 4, state: peerStateReady},
					"P2": {height: 5, state: peerStateReady}},
				allB: []int64{1, 2, 3, 4, 5},
				pending: map[int64]p2p.NodeID{
					1: "P1", 2: "P1",
					3: "P2",
				},
			},
			args:      args{event: tryEv},
			wantEvent: scBlockRequest{peerID: "P2", height: 4},
		},

		{
			name: "many Ready higher peers with same number of pending requests",
			fields: scTestParams{
				startTime: now,
				peers: map[string]*scPeer{
					"P2": {height: 8, state: peerStateReady},
					"P1": {height: 8, state: peerStateReady},
					"P3": {height: 8, state: peerStateReady}},
				allB: []int64{1, 2, 3, 4, 5, 6, 7, 8},
				pending: map[int64]p2p.NodeID{
					1: "P1", 2: "P1",
					3: "P3", 4: "P3",
					5: "P2", 6: "P2",
				},
			},
			args:      args{event: tryEv},
			wantEvent: scBlockRequest{peerID: "P1", height: 7},
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			sc := newTestScheduler(tt.fields)
			event, err := sc.handleTrySchedule(tt.args.event)
			checkScResults(t, tt.wantErr, err, tt.wantEvent, event)
		})
	}
}

func TestScHandleStatusResponse(t *testing.T) {
	now := time.Now()
	statusRespP1Ev := bcStatusResponse{
		time:   now.Add(time.Second + time.Millisecond),
		peerID: "P1",
		height: 6,
	}

	type args struct {
		event bcStatusResponse
	}
	tests := []struct {
		name      string
		fields    scTestParams
		args      args
		wantEvent Event
		wantErr   bool
	}{
		{
			name: "change height of non existing peer",
			fields: scTestParams{
				peers: map[string]*scPeer{"P2": {height: 2, state: peerStateReady}},
				allB:  []int64{1, 2},
			},
			args:      args{event: statusRespP1Ev},
			wantEvent: noOpEvent{},
		},

		{
			name:      "increase height of removed peer",
			fields:    scTestParams{peers: map[string]*scPeer{"P1": {height: 2, state: peerStateRemoved}}},
			args:      args{event: statusRespP1Ev},
			wantEvent: noOpEvent{},
		},

		{
			name: "decrease height of single peer",
			fields: scTestParams{
				height: 5,
				peers:  map[string]*scPeer{"P1": {height: 10, state: peerStateReady}},
				allB:   []int64{5, 6, 7, 8, 9, 10},
			},
			args:      args{event: statusRespP1Ev},
			wantEvent: scPeerError{peerID: "P1", reason: fmt.Errorf("some error")},
		},

		{
			name: "increase height of single peer",
			fields: scTestParams{
				peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
				allB:  []int64{1, 2}},
			args:      args{event: statusRespP1Ev},
			wantEvent: noOpEvent{},
		},
		{
			name: "noop height change of single peer",
			fields: scTestParams{
				peers: map[string]*scPeer{"P1": {height: 6, state: peerStateReady}},
				allB:  []int64{1, 2, 3, 4, 5, 6}},
			args:      args{event: statusRespP1Ev},
			wantEvent: noOpEvent{},
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			sc := newTestScheduler(tt.fields)
			event, err := sc.handleStatusResponse(tt.args.event)
			checkScResults(t, tt.wantErr, err, tt.wantEvent, event)
		})
	}
}

func TestScHandle(t *testing.T) {
	now := time.Now()

	type unknownEv struct {
		priorityNormal
	}

	block1, block2, block3 := makeScBlock(1), makeScBlock(2), makeScBlock(3)

	t0 := time.Now()
	tick := make([]time.Time, 100)
	for i := range tick {
		tick[i] = t0.Add(time.Duration(i) * time.Millisecond)
	}

	type args struct {
		event Event
	}
	type scStep struct {
		currentSc *scTestParams
		args      args
		wantEvent Event
		wantErr   bool
		wantSc    *scTestParams
	}
	tests := []struct {
		name  string
		steps []scStep
	}{
		{
			name: "unknown event",
			steps: []scStep{
				{ // add P1
					currentSc: &scTestParams{},
					args:      args{event: unknownEv{}},
					wantEvent: scSchedulerFail{reason: fmt.Errorf("some error")},
					wantSc:    &scTestParams{},
				},
			},
		},
		{
			name: "single peer, sync 3 blocks",
			steps: []scStep{
				{ // add P1
					currentSc: &scTestParams{startTime: now, peers: map[string]*scPeer{}, height: 1},
					args:      args{event: bcAddNewPeer{peerID: "P1"}},
					wantEvent: noOpEvent{},
					wantSc: &scTestParams{startTime: now, peers: map[string]*scPeer{
						"P1": {base: -1, height: -1, state: peerStateNew}}, height: 1},
				},
				{ // set height of P1
					args:      args{event: bcStatusResponse{peerID: "P1", time: tick[0], height: 3}},
					wantEvent: noOpEvent{},
					wantSc: &scTestParams{
						startTime: now,
						peers:     map[string]*scPeer{"P1": {height: 3, state: peerStateReady}},
						allB:      []int64{1, 2, 3},
						height:    1,
					},
				},
				{ // schedule block 1
					args:      args{event: rTrySchedule{time: tick[1]}},
					wantEvent: scBlockRequest{peerID: "P1", height: 1},
					wantSc: &scTestParams{
						startTime:   now,
						peers:       map[string]*scPeer{"P1": {height: 3, state: peerStateReady}},
						allB:        []int64{1, 2, 3},
						pending:     map[int64]p2p.NodeID{1: "P1"},
						pendingTime: map[int64]time.Time{1: tick[1]},
						height:      1,
					},
				},
				{ // schedule block 2
					args:      args{event: rTrySchedule{time: tick[2]}},
					wantEvent: scBlockRequest{peerID: "P1", height: 2},
					wantSc: &scTestParams{
						startTime:   now,
						peers:       map[string]*scPeer{"P1": {height: 3, state: peerStateReady}},
						allB:        []int64{1, 2, 3},
						pending:     map[int64]p2p.NodeID{1: "P1", 2: "P1"},
						pendingTime: map[int64]time.Time{1: tick[1], 2: tick[2]},
						height:      1,
					},
				},
				{ // schedule block 3
					args:      args{event: rTrySchedule{time: tick[3]}},
					wantEvent: scBlockRequest{peerID: "P1", height: 3},
					wantSc: &scTestParams{
						startTime:   now,
						peers:       map[string]*scPeer{"P1": {height: 3, state: peerStateReady}},
						allB:        []int64{1, 2, 3},
						pending:     map[int64]p2p.NodeID{1: "P1", 2: "P1", 3: "P1"},
						pendingTime: map[int64]time.Time{1: tick[1], 2: tick[2], 3: tick[3]},
						height:      1,
					},
				},
				{ // block response 1
					args:      args{event: bcBlockResponse{peerID: "P1", time: tick[4], size: 100, block: block1}},
					wantEvent: scBlockReceived{peerID: "P1", block: block1},
					wantSc: &scTestParams{
						startTime:   now,
						peers:       map[string]*scPeer{"P1": {height: 3, state: peerStateReady, lastTouched: tick[4]}},
						allB:        []int64{1, 2, 3},
						pending:     map[int64]p2p.NodeID{2: "P1", 3: "P1"},
						pendingTime: map[int64]time.Time{2: tick[2], 3: tick[3]},
						received:    map[int64]p2p.NodeID{1: "P1"},
						height:      1,
					},
				},
				{ // block response 2
					args:      args{event: bcBlockResponse{peerID: "P1", time: tick[5], size: 100, block: block2}},
					wantEvent: scBlockReceived{peerID: "P1", block: block2},
					wantSc: &scTestParams{
						startTime:   now,
						peers:       map[string]*scPeer{"P1": {height: 3, state: peerStateReady, lastTouched: tick[5]}},
						allB:        []int64{1, 2, 3},
						pending:     map[int64]p2p.NodeID{3: "P1"},
						pendingTime: map[int64]time.Time{3: tick[3]},
						received:    map[int64]p2p.NodeID{1: "P1", 2: "P1"},
						height:      1,
					},
				},
				{ // block response 3
					args:      args{event: bcBlockResponse{peerID: "P1", time: tick[6], size: 100, block: block3}},
					wantEvent: scBlockReceived{peerID: "P1", block: block3},
					wantSc: &scTestParams{
						startTime: now,
						peers:     map[string]*scPeer{"P1": {height: 3, state: peerStateReady, lastTouched: tick[6]}},
						allB:      []int64{1, 2, 3},
						received:  map[int64]p2p.NodeID{1: "P1", 2: "P1", 3: "P1"},
						height:    1,
					},
				},
				{ // processed block 1
					args:      args{event: pcBlockProcessed{peerID: p2p.NodeID("P1"), height: 1}},
					wantEvent: noOpEvent{},
					wantSc: &scTestParams{
						startTime: now,
						peers:     map[string]*scPeer{"P1": {height: 3, state: peerStateReady, lastTouched: tick[6]}},
						allB:      []int64{2, 3},
						received:  map[int64]p2p.NodeID{2: "P1", 3: "P1"},
						height:    2,
					},
				},
				{ // processed block 2
					args:      args{event: pcBlockProcessed{peerID: p2p.NodeID("P1"), height: 2}},
					wantEvent: scFinishedEv{},
					wantSc: &scTestParams{
						startTime: now,
						peers:     map[string]*scPeer{"P1": {height: 3, state: peerStateReady, lastTouched: tick[6]}},
						allB:      []int64{3},
						received:  map[int64]p2p.NodeID{3: "P1"},
						height:    3,
					},
				},
			},
		},
		{
			name: "block verification failure",
			steps: []scStep{
				{ // failure processing block 1
					currentSc: &scTestParams{
						startTime: now,
						peers: map[string]*scPeer{
							"P1": {height: 4, state: peerStateReady, lastTouched: tick[6]},
							"P2": {height: 3, state: peerStateReady, lastTouched: tick[6]}},
						allB:     []int64{1, 2, 3, 4},
						received: map[int64]p2p.NodeID{1: "P1", 2: "P1", 3: "P1"},
						height:   1,
					},
					args:      args{event: pcBlockVerificationFailure{height: 1, firstPeerID: "P1", secondPeerID: "P1"}},
					wantEvent: noOpEvent{},
					wantSc: &scTestParams{
						startTime: now,
						peers: map[string]*scPeer{
							"P1": {height: 4, state: peerStateRemoved, lastTouched: tick[6]},
							"P2": {height: 3, state: peerStateReady, lastTouched: tick[6]}},
						allB:     []int64{1, 2, 3},
						received: map[int64]p2p.NodeID{},
						height:   1,
					},
				},
			},
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			var sc *scheduler
			for i, step := range tt.steps {
				// First step must always initialise the currentState as state.
				if step.currentSc != nil {
					sc = newTestScheduler(*step.currentSc)
				}
				if sc == nil {
					panic("Bad (initial?) step")
				}

				nextEvent, err := sc.handle(step.args.event)
				wantSc := newTestScheduler(*step.wantSc)

				t.Logf("step %d(%v): %s", i, step.args.event, sc)
				checkSameScheduler(t, wantSc, sc)

				checkScResults(t, step.wantErr, err, step.wantEvent, nextEvent)

				// Next step may use the wantedState as their currentState.
				sc = newTestScheduler(*step.wantSc)
			}
		})
	}
}
