package chain

import (
	"context"
	"fmt"
	"testing"
	"time"

	"github.com/filecoin-project/lotus/chain/types"
	"github.com/filecoin-project/lotus/chain/types/mock"
)

var genTs = mock.TipSet(mock.MkBlock(nil, 0, 0))

type syncOp struct {
	ts   *types.TipSet
	done func()
}

func runSyncMgrTest(t *testing.T, tname string, thresh int, tf func(*testing.T, *syncManager, chan *syncOp)) {
	syncTargets := make(chan *syncOp)
	sm := NewSyncManager(func(ctx context.Context, ts *types.TipSet) error {
		ch := make(chan struct{})
		syncTargets <- &syncOp{
			ts:   ts,
			done: func() { close(ch) },
		}
		<-ch
		return nil
	}).(*syncManager)
	sm.bspThresh = thresh

	sm.Start()
	defer sm.Stop()
	t.Run(tname+fmt.Sprintf("-%d", thresh), func(t *testing.T) {
		tf(t, sm, syncTargets)
	})
}

func assertTsEqual(t *testing.T, actual, expected *types.TipSet) {
	t.Helper()
	if !actual.Equals(expected) {
		t.Fatalf("got unexpected tipset %s (expected: %s)", actual.Cids(), expected.Cids())
	}
}

func assertNoOp(t *testing.T, c chan *syncOp) {
	t.Helper()
	select {
	case <-time.After(time.Millisecond * 20):
	case <-c:
		t.Fatal("shouldnt have gotten any sync operations yet")
	}
}

func assertGetSyncOp(t *testing.T, c chan *syncOp, ts *types.TipSet) {
	t.Helper()

	select {
	case <-time.After(time.Millisecond * 100):
		t.Fatal("expected sync manager to try and sync to our target")
	case op := <-c:
		op.done()
		if !op.ts.Equals(ts) {
			t.Fatalf("somehow got wrong tipset from syncer (got %s, expected %s)", op.ts.Cids(), ts.Cids())
		}
	}
}

func TestSyncManager(t *testing.T) {
	ctx := context.Background()

	a := mock.TipSet(mock.MkBlock(genTs, 1, 1))
	b := mock.TipSet(mock.MkBlock(a, 1, 2))
	c1 := mock.TipSet(mock.MkBlock(b, 1, 3))
	c2 := mock.TipSet(mock.MkBlock(b, 2, 4))
	c3 := mock.TipSet(mock.MkBlock(b, 3, 5))
	d := mock.TipSet(mock.MkBlock(c1, 4, 5))

	runSyncMgrTest(t, "testBootstrap", 1, func(t *testing.T, sm *syncManager, stc chan *syncOp) {
		sm.SetPeerHead(ctx, "peer1", c1)
		assertGetSyncOp(t, stc, c1)
	})

	runSyncMgrTest(t, "testBootstrap", 2, func(t *testing.T, sm *syncManager, stc chan *syncOp) {
		sm.SetPeerHead(ctx, "peer1", c1)
		assertNoOp(t, stc)

		sm.SetPeerHead(ctx, "peer2", c1)
		assertGetSyncOp(t, stc, c1)
	})

	runSyncMgrTest(t, "testSyncAfterBootstrap", 1, func(t *testing.T, sm *syncManager, stc chan *syncOp) {
		sm.SetPeerHead(ctx, "peer1", b)
		assertGetSyncOp(t, stc, b)

		sm.SetPeerHead(ctx, "peer2", c1)
		assertGetSyncOp(t, stc, c1)

		sm.SetPeerHead(ctx, "peer2", c2)
		assertGetSyncOp(t, stc, c2)
	})

	runSyncMgrTest(t, "testCoalescing", 1, func(t *testing.T, sm *syncManager, stc chan *syncOp) {
		sm.SetPeerHead(ctx, "peer1", a)
		assertGetSyncOp(t, stc, a)

		sm.SetPeerHead(ctx, "peer2", b)
		op := <-stc

		sm.SetPeerHead(ctx, "peer2", c1)
		sm.SetPeerHead(ctx, "peer2", c2)
		sm.SetPeerHead(ctx, "peer2", d)

		assertTsEqual(t, op.ts, b)

		// need a better way to 'wait until syncmgr is idle'
		time.Sleep(time.Millisecond * 20)

		op.done()

		assertGetSyncOp(t, stc, d)
	})

	runSyncMgrTest(t, "testSyncIncomingTipset", 1, func(t *testing.T, sm *syncManager, stc chan *syncOp) {
		sm.SetPeerHead(ctx, "peer1", a)
		assertGetSyncOp(t, stc, a)

		sm.SetPeerHead(ctx, "peer2", b)
		op := <-stc
		op.done()

		sm.SetPeerHead(ctx, "peer2", c1)
		op1 := <-stc
		fmt.Println("op1: ", op1.ts.Cids())

		sm.SetPeerHead(ctx, "peer2", c2)
		sm.SetPeerHead(ctx, "peer2", c3)

		op1.done()

		op2 := <-stc
		fmt.Println("op2: ", op2.ts.Cids())
		op2.done()

		op3 := <-stc
		fmt.Println("op3: ", op3.ts.Cids())
		op3.done()
	})
}