package statsd

import (
	"bytes"
	"math"
	"math/rand"
	"testing"
	"time"
)

var testConf = StatsConfig{
	Rate:   1.0,
	Prefix: "",
}

var unsampledTestConf = StatsConfig{
	Rate:   0.0,
	Prefix: "",
}

func assertEquals(t *testing.T, got, expected interface{}) {
	if expected != got {
		t.Errorf("expected '%v', got '%v'", expected, got)
	}

}

func setUp(t *testing.T, conf StatsConfig) (*bytes.Buffer, Stats) {
	b := &bytes.Buffer{}
	s := New(b, conf)

	return b, s
}

func TestSafeDial(t *testing.T) {
	s := SafeDial("there is no spoon", "", testConf)

	assertEquals(t, s, noopStats)
}

func TestMustDial(t *testing.T) {
	defer func() { recover() }()

	_ = MustDial("there is no spoon", "", testConf)

	t.Error("Expected panic()")
}

func TestIncrBy(t *testing.T) {
	b, s := setUp(t, testConf)

	s.IncrBy("gorets", 13)

	assertEquals(t, "gorets:13|c", b.String())
}

func TestIncr(t *testing.T) {
	b, s := setUp(t, testConf)

	s.Incr("goreta")

	assertEquals(t, "goreta:1|c", b.String())
}

func TestIncrByUnsampled(t *testing.T) {
	b, s := setUp(t, testConf)

	s.IncrBy("gorets", 987)
	assertEquals(t, "gorets:987|c", b.String())

	b, s = setUp(t, unsampledTestConf)

	s.IncrBy("gorets", 987)
	assertEquals(t, "", b.String())
	s.IncrByUnsampled("gorets", 987)
	assertEquals(t, "gorets:987|c", b.String())
}

func TestIncrUnsampled(t *testing.T) {
	b, s := setUp(t, testConf)

	s.Incr("goreta")
	assertEquals(t, "goreta:1|c", b.String())

	b, s = setUp(t, unsampledTestConf)

	s.Incr("goreta")
	assertEquals(t, "", b.String())
	s.IncrUnsampled("goreta")
	assertEquals(t, "goreta:1|c", b.String())
}

func TestTiming(t *testing.T) {
	b, s := setUp(t, testConf)

	s.Timing("glork", 320*time.Millisecond)

	assertEquals(t, "glork:320|ms", b.String())
}

func TestTimingUnsampled(t *testing.T) {
	b, s := setUp(t, unsampledTestConf)

	s.Timing("glork", 320*time.Millisecond)
	assertEquals(t, "", b.String())
	s.TimingUnsampled("glork", 320*time.Millisecond)
	assertEquals(t, "glork:320|ms", b.String())
}

func TestIntegerTiming(t *testing.T) {
	b, s := setUp(t, testConf)

	s.IntegerTiming("xylophone", 387563)
	assertEquals(t, "xylophone:387563|ms", b.String())
}

func TestIntegerTimingUnsampled(t *testing.T) {
	b, s := setUp(t, unsampledTestConf)

	s.IntegerTiming("xylophone", 387563)
	assertEquals(t, "", b.String())
	s.IntegerTimingUnsampled("xylophone", 387563)
	assertEquals(t, "xylophone:387563|ms", b.String())
}

func TestGaugeSet(t *testing.T) {
	b, s := setUp(t, testConf)

	s.GaugeSet("gaugor", 333)

	assertEquals(t, "gaugor:333|g", b.String())
}

func TestGaugeSetUnsampled(t *testing.T) {
	b, s := setUp(t, testConf)

	s.GaugeSetUnsampled("gaugor", 333)
	assertEquals(t, "gaugor:333|g", b.String())

	b, s = setUp(t, unsampledTestConf)

	s.GaugeSet("gaugor", 333)
	assertEquals(t, "", b.String())
	s.GaugeSetUnsampled("gaugor", 333)
	assertEquals(t, "gaugor:333|g", b.String())
}

func TestGaugeChangeUnsampled(t *testing.T) {
	b, s := setUp(t, testConf)

	s.GaugeChangeUnsampled("gaugor", 333)
	assertEquals(t, "gaugor:+333|g", b.String())

	b, s = setUp(t, unsampledTestConf)

	s.GaugeChange("gaugor", 333)
	assertEquals(t, "", b.String())
	s.GaugeChangeUnsampled("gaugor", 333)
	assertEquals(t, "gaugor:+333|g", b.String())
}

func TestGaugeChange(t *testing.T) {
	b, s := setUp(t, testConf)

	s.GaugeChange("gaugor", 333)

	assertEquals(t, "gaugor:+333|g", b.String())
}

func TestUnique(t *testing.T) {
	b, s := setUp(t, testConf)

	s.Unique("gaugor", "case")

	assertEquals(t, "gaugor:case|s", b.String())
}

func TestSamplingZero(t *testing.T) {
	b, s := setUp(t, StatsConfig{Rate: 0, Prefix: ""})

	s.Incr("nobucket") // should never succeed

	assertEquals(t, "", b.String())
}

func TestPrefix(t *testing.T) {
	b, s := setUp(t, StatsConfig{Rate: 1.0, Prefix: "root"})

	s.Incr("gorets")

	assertEquals(t, "root.gorets:1|c", b.String())
}

func checkIfHit(s Stats, b *bytes.Buffer) bool {
	b.Reset()
	s.Incr("hit")
	return b.Len() != 0
}

func TestRate(t *testing.T) {
	for i := 0; i < 10; i++ {
		rand.Seed(int64(i))
		doTestRate(t)
	}
}

func doTestRate(t *testing.T) {
	const GRANULITY = 50
	const TRIES = GRANULITY * 100

	rate := float32(rand.Int31n(GRANULITY)) / GRANULITY
	b, s := setUp(t, StatsConfig{Rate: rate, Prefix: ""})

	hitCount := 0
	for count := 0; count < TRIES; count++ {
		if checkIfHit(s, b) {
			hitCount++
		}
	}

	realRate := float32(hitCount) / TRIES

	if math.Abs(float64(realRate-rate)) > (1.0 / GRANULITY) {
		t.Errorf("Expected rate %v, got %v", rate, realRate)
	}
}

func TestSampledTimer(t *testing.T) {
	b, s := setUp(t, StatsConfig{Rate: 0.9, Prefix: ""})
	for b.Len() == 0 {
		s.Timing("sampled", 30*time.Millisecond)
	}
	if got, exp := b.String(), "sampled:30|ms|@0.900000"; got != exp {
		t.Errorf("incorrect format for sampled stats: have %#v, want %#v", got, exp)
	}
}
