package connmgr

import (
	"os"
	"testing"
	"time"

	"github.com/libp2p/go-libp2p/core/connmgr"
	"github.com/libp2p/go-libp2p/core/peer"
	tu "github.com/libp2p/go-libp2p/core/test"

	"github.com/benbjohnson/clock"
	"github.com/stretchr/testify/require"
)

const TestResolution = 50 * time.Millisecond

func waitForTag(t *testing.T, mgr *BasicConnMgr, id peer.ID) {
	t.Helper()
	require.Eventually(t, func() bool { return mgr.GetTagInfo(id) != nil }, 500*time.Millisecond, 10*time.Millisecond)
}

func TestDecayExpire(t *testing.T) {
	id := tu.RandPeerIDFatal(t)
	mgr, decay, mockClock := testDecayTracker(t)

	tag, err := decay.RegisterDecayingTag("pop", 250*time.Millisecond, connmgr.DecayExpireWhenInactive(1*time.Second), connmgr.BumpSumUnbounded())
	require.NoError(t, err)
	require.NoError(t, tag.Bump(id, 10))

	waitForTag(t, mgr, id)
	require.Equal(t, 10, mgr.GetTagInfo(id).Value)

	mockClock.Add(250 * time.Millisecond)
	mockClock.Add(250 * time.Millisecond)
	mockClock.Add(250 * time.Millisecond)
	mockClock.Add(250 * time.Millisecond)

	eventuallyEqual(t, func() int { return mgr.GetTagInfo(id).Value }, 0)
}

func TestMultipleBumps(t *testing.T) {
	id := tu.RandPeerIDFatal(t)
	mgr, decay, _ := testDecayTracker(t)

	tag, err := decay.RegisterDecayingTag("pop", 250*time.Millisecond, connmgr.DecayExpireWhenInactive(1*time.Second), connmgr.BumpSumBounded(10, 20))
	require.NoError(t, err)

	require.NoError(t, tag.Bump(id, 5))

	waitForTag(t, mgr, id)
	require.Equal(t, 10, mgr.GetTagInfo(id).Value)

	require.NoError(t, tag.Bump(id, 100))
	require.Eventually(t, func() bool { return mgr.GetTagInfo(id).Value == 20 }, 100*time.Millisecond, 10*time.Millisecond, "expected tag value to decay to 20")
}

func TestMultipleTagsNoDecay(t *testing.T) {
	id := tu.RandPeerIDFatal(t)
	mgr, decay, _ := testDecayTracker(t)

	tag1, err := decay.RegisterDecayingTag("beep", 250*time.Millisecond, connmgr.DecayNone(), connmgr.BumpSumBounded(0, 100))
	require.NoError(t, err)
	tag2, err := decay.RegisterDecayingTag("bop", 250*time.Millisecond, connmgr.DecayNone(), connmgr.BumpSumBounded(0, 100))
	require.NoError(t, err)
	tag3, err := decay.RegisterDecayingTag("foo", 250*time.Millisecond, connmgr.DecayNone(), connmgr.BumpSumBounded(0, 100))
	require.NoError(t, err)

	_ = tag1.Bump(id, 100)
	_ = tag2.Bump(id, 100)
	_ = tag3.Bump(id, 100)
	_ = tag1.Bump(id, 100)
	_ = tag2.Bump(id, 100)
	_ = tag3.Bump(id, 100)

	waitForTag(t, mgr, id)

	// all tags are upper-bounded, so the score must be 300
	ti := mgr.GetTagInfo(id)
	require.Equal(t, 300, ti.Value)

	for _, s := range []string{"beep", "bop", "foo"} {
		if v, ok := ti.Tags[s]; !ok || v != 100 {
			t.Fatalf("expected tag %s to be 100; was = %d", s, v)
		}
	}
}

func TestCustomFunctions(t *testing.T) {
	id := tu.RandPeerIDFatal(t)
	mgr, decay, mockClock := testDecayTracker(t)

	tag1, err := decay.RegisterDecayingTag("beep", 250*time.Millisecond, connmgr.DecayFixed(10), connmgr.BumpSumUnbounded())
	require.NoError(t, err)
	tag2, err := decay.RegisterDecayingTag("bop", 100*time.Millisecond, connmgr.DecayFixed(5), connmgr.BumpSumUnbounded())
	require.NoError(t, err)
	tag3, err := decay.RegisterDecayingTag("foo", 50*time.Millisecond, connmgr.DecayFixed(1), connmgr.BumpSumUnbounded())
	require.NoError(t, err)

	_ = tag1.Bump(id, 1000)
	_ = tag2.Bump(id, 1000)
	_ = tag3.Bump(id, 1000)

	waitForTag(t, mgr, id)

	// no decay has occurred yet, so score must be 3000.
	require.Equal(t, 3000, mgr.GetTagInfo(id).Value)

	// only tag3 should tick.
	mockClock.Add(50 * time.Millisecond)
	eventuallyEqual(t, func() int { return mgr.GetTagInfo(id).Value }, 2999)

	// tag3 will tick thrice, tag2 will tick twice.
	mockClock.Add(150 * time.Millisecond)
	eventuallyEqual(t, func() int { return mgr.GetTagInfo(id).Value }, 2986)

	// tag3 will tick once, tag1 will tick once.
	mockClock.Add(50 * time.Millisecond)
	eventuallyEqual(t, func() int { return mgr.GetTagInfo(id).Value }, 2975)
}

func TestMultiplePeers(t *testing.T) {
	ids := []peer.ID{tu.RandPeerIDFatal(t), tu.RandPeerIDFatal(t), tu.RandPeerIDFatal(t)}
	mgr, decay, mockClock := testDecayTracker(t)

	tag1, err := decay.RegisterDecayingTag("beep", 250*time.Millisecond, connmgr.DecayFixed(10), connmgr.BumpSumUnbounded())
	require.NoError(t, err)
	tag2, err := decay.RegisterDecayingTag("bop", 100*time.Millisecond, connmgr.DecayFixed(5), connmgr.BumpSumUnbounded())
	require.NoError(t, err)
	tag3, err := decay.RegisterDecayingTag("foo", 50*time.Millisecond, connmgr.DecayFixed(1), connmgr.BumpSumUnbounded())
	require.NoError(t, err)

	_ = tag1.Bump(ids[0], 1000)
	_ = tag2.Bump(ids[0], 1000)
	_ = tag3.Bump(ids[0], 1000)

	_ = tag1.Bump(ids[1], 500)
	_ = tag2.Bump(ids[1], 500)
	_ = tag3.Bump(ids[1], 500)

	_ = tag1.Bump(ids[2], 100)
	_ = tag2.Bump(ids[2], 100)
	_ = tag3.Bump(ids[2], 100)

	// allow the background goroutine to process bumps.
	waitFor := 100 * time.Millisecond
	tick := 10 * time.Millisecond
	if os.Getenv("CI") != "" {
		waitFor *= 10
		tick *= 10
	}

	require.Eventually(t, func() bool {
		return mgr.GetTagInfo(ids[0]) != nil && mgr.GetTagInfo(ids[1]) != nil && mgr.GetTagInfo(ids[2]) != nil
	}, waitFor, tick)

	mockClock.Add(3 * time.Second)

	waitFor = 500 * time.Millisecond
	if os.Getenv("CI") != "" {
		waitFor *= 10
	}

	require.Eventually(t, func() bool { return mgr.GetTagInfo(ids[0]).Value == 2670 }, waitFor, tick)
	require.Equal(t, 1170, mgr.GetTagInfo(ids[1]).Value)
	require.Equal(t, 40, mgr.GetTagInfo(ids[2]).Value)
}

func eventuallyEqual(t *testing.T, f func() int, val int) {
	t.Helper()
	require.Eventually(t, func() bool {
		v := f()
		if v == val {
			return true
		}
		t.Log("f() was", v, "expected", val, "retrying...")
		return false
	}, 1*time.Second, 10*time.Millisecond)
}

func TestLinearDecayOverwrite(t *testing.T) {
	id := tu.RandPeerIDFatal(t)
	mgr, decay, mockClock := testDecayTracker(t)

	tag1, err := decay.RegisterDecayingTag("beep", 250*time.Millisecond, connmgr.DecayLinear(0.5), connmgr.BumpOverwrite())
	require.NoError(t, err)

	_ = tag1.Bump(id, 1000)
	waitForTag(t, mgr, id)

	eventuallyEqual(t, func() int { return mgr.GetTagInfo(id).Value }, 1000)
	mockClock.Add(250 * time.Millisecond)
	eventuallyEqual(t, func() int { return mgr.GetTagInfo(id).Value }, 500)

	mockClock.Add(250 * time.Millisecond)
	eventuallyEqual(t, func() int { return mgr.GetTagInfo(id).Value }, 250)

	_ = tag1.Bump(id, 1000)
	eventuallyEqual(t, func() int { return mgr.GetTagInfo(id).Value }, 1000)
}

func TestResolutionMisaligned(t *testing.T) {
	var (
		id                    = tu.RandPeerIDFatal(t)
		mgr, decay, mockClock = testDecayTracker(t)
		require               = require.New(t)
	)

	tag1, err := decay.RegisterDecayingTag("beep", time.Duration(float64(TestResolution)*1.4), connmgr.DecayFixed(1), connmgr.BumpOverwrite())
	require.NoError(err)

	tag2, err := decay.RegisterDecayingTag("bop", time.Duration(float64(TestResolution)*2.4), connmgr.DecayFixed(1), connmgr.BumpOverwrite())
	require.NoError(err)

	_ = tag1.Bump(id, 1000)
	_ = tag2.Bump(id, 1000)
	// allow the background goroutine to process bumps.
	<-time.After(500 * time.Millisecond)

	// first tick.
	mockClock.Add(TestResolution)
	eventuallyEqual(t, func() int { return mgr.GetTagInfo(id).Tags["beep"] }, 1000)
	require.Equal(1000, mgr.GetTagInfo(id).Tags["bop"])

	// next tick; tag1 would've ticked.
	mockClock.Add(TestResolution)
	eventuallyEqual(t, func() int { return mgr.GetTagInfo(id).Tags["beep"] }, 999)
	require.Equal(1000, mgr.GetTagInfo(id).Tags["bop"])

	// next tick; tag1 would've ticked twice, tag2 once.
	mockClock.Add(TestResolution)
	eventuallyEqual(t, func() int { return mgr.GetTagInfo(id).Tags["beep"] }, 998)
	require.Equal(999, mgr.GetTagInfo(id).Tags["bop"])

	require.Equal(1997, mgr.GetTagInfo(id).Value)
}

func TestTagRemoval(t *testing.T) {
	id1, id2 := tu.RandPeerIDFatal(t), tu.RandPeerIDFatal(t)
	mgr, decay, mockClock := testDecayTracker(t)

	tag1, err := decay.RegisterDecayingTag("beep", TestResolution, connmgr.DecayFixed(1), connmgr.BumpOverwrite())
	require.NoError(t, err)

	tag2, err := decay.RegisterDecayingTag("bop", TestResolution, connmgr.DecayFixed(1), connmgr.BumpOverwrite())
	require.NoError(t, err)

	// id1 has both tags; id2 only has the first tag.
	_ = tag1.Bump(id1, 1000)
	_ = tag2.Bump(id1, 1000)
	_ = tag1.Bump(id2, 1000)

	waitForTag(t, mgr, id1)
	waitForTag(t, mgr, id2)

	// first tick.
	mockClock.Add(TestResolution)
	eventuallyEqual(t, func() int { return mgr.GetTagInfo(id1).Tags["beep"] }, 999)
	require.Equal(t, 999, mgr.GetTagInfo(id1).Tags["bop"])
	require.Equal(t, 999, mgr.GetTagInfo(id2).Tags["beep"])

	require.Equal(t, 999*2, mgr.GetTagInfo(id1).Value)
	require.Equal(t, 999, mgr.GetTagInfo(id2).Value)

	// remove tag1 from p1.
	require.NoError(t, tag1.Remove(id1))

	// next tick. both peers only have 1 tag, both at 998 value.
	mockClock.Add(TestResolution)
	require.Eventually(t, func() bool { return mgr.GetTagInfo(id1).Tags["beep"] == 0 }, 500*time.Millisecond, 10*time.Millisecond)
	require.Equal(t, 998, mgr.GetTagInfo(id1).Tags["bop"])
	require.Equal(t, 998, mgr.GetTagInfo(id2).Tags["beep"])

	require.Equal(t, 998, mgr.GetTagInfo(id1).Value)
	require.Equal(t, 998, mgr.GetTagInfo(id2).Value)

	// remove tag1 from p1 again; no error.
	require.NoError(t, tag1.Remove(id1))
}

func TestTagClosure(t *testing.T) {
	id := tu.RandPeerIDFatal(t)
	mgr, decay, mockClock := testDecayTracker(t)

	tag1, err := decay.RegisterDecayingTag("beep", TestResolution, connmgr.DecayFixed(1), connmgr.BumpOverwrite())
	require.NoError(t, err)
	tag2, err := decay.RegisterDecayingTag("bop", TestResolution, connmgr.DecayFixed(1), connmgr.BumpOverwrite())
	require.NoError(t, err)

	_ = tag1.Bump(id, 1000)
	_ = tag2.Bump(id, 1000)
	waitForTag(t, mgr, id)

	// nothing has happened.
	mockClock.Add(TestResolution)
	eventuallyEqual(t, func() int { return mgr.GetTagInfo(id).Tags["beep"] }, 999)
	require.Equal(t, 999, mgr.GetTagInfo(id).Tags["bop"])
	require.Equal(t, 999*2, mgr.GetTagInfo(id).Value)

	// next tick; tag1 would've ticked.
	mockClock.Add(TestResolution)
	eventuallyEqual(t, func() int { return mgr.GetTagInfo(id).Tags["beep"] }, 998)
	require.Equal(t, 998, mgr.GetTagInfo(id).Tags["bop"])
	require.Equal(t, 998*2, mgr.GetTagInfo(id).Value)

	// close the tag.
	require.NoError(t, tag1.Close())

	// allow the background goroutine to process the closure.
	require.Eventually(t, func() bool { return mgr.GetTagInfo(id).Value == 998 }, 500*time.Millisecond, 10*time.Millisecond)

	// a second closure should not error.
	require.NoError(t, tag1.Close())

	// bumping a tag after it's been closed should error.
	require.Error(t, tag1.Bump(id, 5))
}

func testDecayTracker(tb testing.TB) (*BasicConnMgr, connmgr.Decayer, *clock.Mock) {
	mockClock := clock.NewMock()
	cfg := &DecayerCfg{
		Resolution: TestResolution,
		Clock:      mockClock,
	}

	mgr, err := NewConnManager(10, 10, WithGracePeriod(time.Second), DecayerConfig(cfg))
	require.NoError(tb, err)
	decay, ok := connmgr.SupportsDecay(mgr)
	if !ok {
		tb.Fatalf("connmgr does not support decay")
	}
	tb.Cleanup(func() {
		mgr.Close()
		decay.Close()
	})

	return mgr, decay, mockClock
}
