// Copyright 2013 Ooyala, Inc.

package statsd

import (
	"fmt"
	"io"
	"net"
	"reflect"
	"strings"
	"testing"
	"time"
)

var dogstatsdTests = []struct {
	GlobalNamespace string
	GlobalTags      []string
	Method          string
	Metric          string
	Value           interface{}
	Tags            []string
	Rate            float64
	Expected        string
}{
	{"", nil, "Gauge", "test.gauge", 1.0, nil, 1.0, "test.gauge:1.000000|g"},
	{"", nil, "Gauge", "test.gauge", 1.0, nil, 0.999999, "test.gauge:1.000000|g|@0.999999"},
	{"", nil, "Gauge", "test.gauge", 1.0, []string{"tagA"}, 1.0, "test.gauge:1.000000|g|#tagA"},
	{"", nil, "Gauge", "test.gauge", 1.0, []string{"tagA", "tagB"}, 1.0, "test.gauge:1.000000|g|#tagA,tagB"},
	{"", nil, "Gauge", "test.gauge", 1.0, []string{"tagA"}, 0.999999, "test.gauge:1.000000|g|@0.999999|#tagA"},
	{"", nil, "Count", "test.count", int64(1), []string{"tagA"}, 1.0, "test.count:1|c|#tagA"},
	{"", nil, "Count", "test.count", int64(-1), []string{"tagA"}, 1.0, "test.count:-1|c|#tagA"},
	{"", nil, "Histogram", "test.histogram", 2.3, []string{"tagA"}, 1.0, "test.histogram:2.300000|h|#tagA"},
	{"", nil, "Set", "test.set", "uuid", []string{"tagA"}, 1.0, "test.set:uuid|s|#tagA"},
	{"flubber.", nil, "Set", "test.set", "uuid", []string{"tagA"}, 1.0, "flubber.test.set:uuid|s|#tagA"},
	{"", []string{"tagC"}, "Set", "test.set", "uuid", []string{"tagA"}, 1.0, "test.set:uuid|s|#tagC,tagA"},
	{"", nil, "Count", "test.count", int64(1), []string{"hello\nworld"}, 1.0, "test.count:1|c|#helloworld"},
}

func assertNotPanics(t *testing.T, f func()) {
	defer func() {
		if r := recover(); r != nil {
			t.Fatal(r)
		}
	}()
	f()
}

func TestClient(t *testing.T) {
	addr := "localhost:1201"
	udpAddr, err := net.ResolveUDPAddr("udp", addr)
	if err != nil {
		t.Fatal(err)
	}

	server, err := net.ListenUDP("udp", udpAddr)
	if err != nil {
		t.Fatal(err)
	}
	defer server.Close()

	client, err := New(addr)
	if err != nil {
		t.Fatal(err)
	}

	for _, tt := range dogstatsdTests {
		client.Namespace = tt.GlobalNamespace
		client.Tags = tt.GlobalTags
		method := reflect.ValueOf(client).MethodByName(tt.Method)
		e := method.Call([]reflect.Value{
			reflect.ValueOf(tt.Metric),
			reflect.ValueOf(tt.Value),
			reflect.ValueOf(tt.Tags),
			reflect.ValueOf(tt.Rate)})[0]
		errInter := e.Interface()
		if errInter != nil {
			t.Fatal(errInter.(error))
		}

		bytes := make([]byte, 1024)
		n, err := server.Read(bytes)
		if err != nil {
			t.Fatal(err)
		}
		message := bytes[:n]
		if string(message) != tt.Expected {
			t.Errorf("Expected: %s. Actual: %s", tt.Expected, string(message))
		}
	}
}

func TestBufferedClient(t *testing.T) {
	addr := "localhost:1201"
	udpAddr, err := net.ResolveUDPAddr("udp", addr)
	if err != nil {
		t.Fatal(err)
	}

	server, err := net.ListenUDP("udp", udpAddr)
	if err != nil {
		t.Fatal(err)
	}
	defer server.Close()

	conn, err := net.DialUDP("udp", nil, udpAddr)
	if err != nil {
		t.Fatal(err)
	}

	bufferLength := 8
	client := &Client{
		conn:         conn,
		commands:     make([]string, 0, bufferLength),
		bufferLength: bufferLength,
	}

	client.Namespace = "foo."
	client.Tags = []string{"dd:2"}

	dur, _ := time.ParseDuration("123us")

	client.Incr("ic", nil, 1)
	client.Decr("dc", nil, 1)
	client.Count("cc", 1, nil, 1)
	client.Gauge("gg", 10, nil, 1)
	client.Histogram("hh", 1, nil, 1)
	client.Timing("tt", dur, nil, 1)
	client.Set("ss", "ss", nil, 1)

	if len(client.commands) != 7 {
		t.Errorf("Expected client to have buffered 7 commands, but found %d\n", len(client.commands))
	}

	client.Set("ss", "xx", nil, 1)
	err = client.flush()
	if err != nil {
		t.Errorf("Error sending: %s", err)
	}

	if len(client.commands) != 0 {
		t.Errorf("Expecting send to flush commands, but found %d\n", len(client.commands))
	}

	buffer := make([]byte, 4096)
	n, err := io.ReadAtLeast(server, buffer, 1)
	result := string(buffer[:n])

	if err != nil {
		t.Error(err)
	}

	expected := []string{
		`foo.ic:1|c|#dd:2`,
		`foo.dc:-1|c|#dd:2`,
		`foo.cc:1|c|#dd:2`,
		`foo.gg:10.000000|g|#dd:2`,
		`foo.hh:1.000000|h|#dd:2`,
		`foo.tt:0.123000|ms|#dd:2`,
		`foo.ss:ss|s|#dd:2`,
		`foo.ss:xx|s|#dd:2`,
	}

	for i, res := range strings.Split(result, "\n") {
		if res != expected[i] {
			t.Errorf("Got `%s`, expected `%s`", res, expected[i])
		}
	}

	client.Event(&Event{Title: "title1", Text: "text1", Priority: Normal, AlertType: Success, Tags: []string{"tagg"}})
	client.SimpleEvent("event1", "text1")

	if len(client.commands) != 2 {
		t.Errorf("Expected to find %d commands, but found %d\n", 2, len(client.commands))
	}

	err = client.flush()

	if err != nil {
		t.Errorf("Error sending: %s", err)
	}

	if len(client.commands) != 0 {
		t.Errorf("Expecting send to flush commands, but found %d\n", len(client.commands))
	}

	buffer = make([]byte, 1024)
	n, err = io.ReadAtLeast(server, buffer, 1)
	result = string(buffer[:n])

	if err != nil {
		t.Error(err)
	}

	if n == 0 {
		t.Errorf("Read 0 bytes but expected more.")
	}

	expected = []string{
		`_e{6,5}:title1|text1|p:normal|t:success|#dd:2,tagg`,
		`_e{6,5}:event1|text1|#dd:2`,
	}

	for i, res := range strings.Split(result, "\n") {
		if res != expected[i] {
			t.Errorf("Got `%s`, expected `%s`", res, expected[i])
		}
	}

}

func TestJoinMaxSize(t *testing.T) {
	c := Client{}
	elements := []string{"abc", "abcd", "ab", "xyz", "foobaz", "x", "wwxxyyzz"}
	res, n := c.joinMaxSize(elements, " ", 8)

	if len(res) != len(n) && len(res) != 4 {
		t.Errorf("Was expecting 4 frames to flush but got: %v - %v", n, res)
	}
	if n[0] != 2 {
		t.Errorf("Was expecting 2 elements in first frame but got: %v", n[0])
	}
	if string(res[0]) != "abc abcd" {
		t.Errorf("Join should have returned \"abc abcd\" in frame, but found: %s", res[0])
	}
	if n[1] != 2 {
		t.Errorf("Was expecting 2 elements in second frame but got: %v - %v", n[1], n)
	}
	if string(res[1]) != "ab xyz" {
		t.Errorf("Join should have returned \"ab xyz\" in frame, but found: %s", res[1])
	}
	if n[2] != 2 {
		t.Errorf("Was expecting 2 elements in third frame but got: %v - %v", n[2], n)
	}
	if string(res[2]) != "foobaz x" {
		t.Errorf("Join should have returned \"foobaz x\" in frame, but found: %s", res[2])
	}
	if n[3] != 1 {
		t.Errorf("Was expecting 1 element in fourth frame but got: %v - %v", n[3], n)
	}
	if string(res[3]) != "wwxxyyzz" {
		t.Errorf("Join should have returned \"wwxxyyzz\" in frame, but found: %s", res[3])
	}

	res, n = c.joinMaxSize(elements, " ", 11)

	if len(res) != len(n) && len(res) != 3 {
		t.Errorf("Was expecting 3 frames to flush but got: %v - %v", n, res)
	}
	if n[0] != 3 {
		t.Errorf("Was expecting 3 elements in first frame but got: %v", n[0])
	}
	if string(res[0]) != "abc abcd ab" {
		t.Errorf("Join should have returned \"abc abcd ab\" in frame, but got: %s", res[0])
	}
	if n[1] != 2 {
		t.Errorf("Was expecting 2 elements in second frame but got: %v", n[1])
	}
	if string(res[1]) != "xyz foobaz" {
		t.Errorf("Join should have returned \"xyz foobaz\" in frame, but got: %s", res[1])
	}
	if n[2] != 2 {
		t.Errorf("Was expecting 2 elements in third frame but got: %v", n[2])
	}
	if string(res[2]) != "x wwxxyyzz" {
		t.Errorf("Join should have returned \"x wwxxyyzz\" in frame, but got: %s", res[2])
	}

	res, n = c.joinMaxSize(elements, "    ", 8)

	if len(res) != len(n) && len(res) != 7 {
		t.Errorf("Was expecting 7 frames to flush but got: %v - %v", n, res)
	}
	if n[0] != 1 {
		t.Errorf("Separator is long, expected a single element in frame but got: %d - %v", n[0], res)
	}
	if string(res[0]) != "abc" {
		t.Errorf("Join should have returned \"abc\" in first frame, but got: %s", res)
	}
	if n[1] != 1 {
		t.Errorf("Separator is long, expected a single element in frame but got: %d - %v", n[1], res)
	}
	if string(res[1]) != "abcd" {
		t.Errorf("Join should have returned \"abcd\" in second frame, but got: %s", res[1])
	}
	if n[2] != 1 {
		t.Errorf("Separator is long, expected a single element in third frame but got: %d - %v", n[2], res)
	}
	if string(res[2]) != "ab" {
		t.Errorf("Join should have returned \"ab\" in third frame, but got: %s", res[2])
	}
	if n[3] != 1 {
		t.Errorf("Separator is long, expected a single element in fourth frame but got: %d - %v", n[3], res)
	}
	if string(res[3]) != "xyz" {
		t.Errorf("Join should have returned \"xyz\" in fourth frame, but got: %s", res[3])
	}
	if n[4] != 1 {
		t.Errorf("Separator is long, expected a single element in fifth frame but got: %d - %v", n[4], res)
	}
	if string(res[4]) != "foobaz" {
		t.Errorf("Join should have returned \"foobaz\" in fifth frame, but got: %s", res[4])
	}
	if n[5] != 1 {
		t.Errorf("Separator is long, expected a single element in sixth frame but got: %d - %v", n[5], res)
	}
	if string(res[5]) != "x" {
		t.Errorf("Join should have returned \"x\" in sixth frame, but got: %s", res[5])
	}
	if n[6] != 1 {
		t.Errorf("Separator is long, expected a single element in seventh frame but got: %d - %v", n[6], res)
	}
	if string(res[6]) != "wwxxyyzz" {
		t.Errorf("Join should have returned \"wwxxyyzz\" in seventh frame, but got: %s", res[6])
	}

	res, n = c.joinMaxSize(elements[4:], " ", 6)
	if len(res) != len(n) && len(res) != 3 {
		t.Errorf("Was expecting 3 frames to flush but got: %v - %v", n, res)

	}
	if n[0] != 1 {
		t.Errorf("Element should just fit in frame - expected single element in frame: %d - %v", n[0], res)
	}
	if string(res[0]) != "foobaz" {
		t.Errorf("Join should have returned \"foobaz\" in first frame, but got: %s", res[0])
	}
	if n[1] != 1 {
		t.Errorf("Single element expected in frame, but got. %d - %v", n[1], res)
	}
	if string(res[1]) != "x" {
		t.Errorf("Join should' have returned \"x\" in second frame, but got: %s", res[1])
	}
	if n[2] != 1 {
		t.Errorf("Even though element is greater then max size we still try to send it. %d - %v", n[2], res)
	}
	if string(res[2]) != "wwxxyyzz" {
		t.Errorf("Join should have returned \"wwxxyyzz\" in third frame, but got: %s", res[2])
	}
}

func TestSendMsg(t *testing.T) {
	addr := "localhost:1201"
	udpAddr, err := net.ResolveUDPAddr("udp", addr)
	if err != nil {
		t.Fatal(err)
	}

	server, err := net.ListenUDP("udp", udpAddr)
	if err != nil {
		t.Fatal(err)
	}
	defer server.Close()

	conn, err := net.DialUDP("udp", nil, udpAddr)
	if err != nil {
		t.Fatal(err)
	}
	defer conn.Close()

	client := &Client{
		conn:         conn,
		bufferLength: 0,
	}

	err = client.sendMsg(strings.Repeat("x", MaxUDPPayloadSize+1))
	if err == nil {
		t.Error("Expected error to be returned if message size is bigger than MaxUDPPayloadSize")
	}

	longMsg := strings.Repeat("x", MaxUDPPayloadSize)

	err = client.sendMsg(longMsg)
	if err != nil {
		t.Errorf("Expected no error to be returned if message size is smaller or equal to MaxUDPPayloadSize, got: %s", err.Error())
	}

	buffer := make([]byte, MaxUDPPayloadSize+1)
	n, err := io.ReadAtLeast(server, buffer, 1)

	if err != nil {
		t.Fatalf("Expected no error to be returned reading the buffer, got: %s", err.Error())
	}

	if n != MaxUDPPayloadSize {
		t.Fatalf("Failed to read full message from buffer. Got size `%d` expected `%d`", n, MaxUDPPayloadSize)
	}

	if string(buffer[:n]) != longMsg {
		t.Fatalf("The received message did not match what we expect.")
	}

	client = &Client{
		conn:         conn,
		commands:     make([]string, 0, 1),
		bufferLength: 1,
	}

	err = client.sendMsg(strings.Repeat("x", MaxUDPPayloadSize+1))
	if err == nil {
		t.Error("Expected error to be returned if message size is bigger than MaxUDPPayloadSize")
	}

	err = client.sendMsg(longMsg)
	if err != nil {
		t.Errorf("Expected no error to be returned if message size is smaller or equal to MaxUDPPayloadSize, got: %s", err.Error())
	}

	client.Lock()
	err = client.flush()
	client.Unlock()

	if err != nil {
		t.Fatalf("Expected no error to be returned flushing the client, got: %s", err.Error())
	}

	buffer = make([]byte, MaxUDPPayloadSize+1)
	n, err = io.ReadAtLeast(server, buffer, 1)

	if err != nil {
		t.Fatalf("Expected no error to be returned reading the buffer, got: %s", err.Error())
	}

	if n != MaxUDPPayloadSize {
		t.Fatalf("Failed to read full message from buffer. Got size `%d` expected `%d`", n, MaxUDPPayloadSize)
	}

	if string(buffer[:n]) != longMsg {
		t.Fatalf("The received message did not match what we expect.")
	}
}

func TestNilSafe(t *testing.T) {
	var c *Client
	assertNotPanics(t, func() { c.Close() })
	assertNotPanics(t, func() { c.Count("", 0, nil, 1) })
	assertNotPanics(t, func() { c.Histogram("", 0, nil, 1) })
	assertNotPanics(t, func() { c.Gauge("", 0, nil, 1) })
	assertNotPanics(t, func() { c.Set("", "", nil, 1) })
	assertNotPanics(t, func() { c.send("", "", nil, 1) })
	assertNotPanics(t, func() { c.SimpleEvent("", "") })
}

func TestEvents(t *testing.T) {
	matrix := []struct {
		event   *Event
		encoded string
	}{
		{
			NewEvent("Hello", "Something happened to my event"),
			`_e{5,30}:Hello|Something happened to my event`,
		}, {
			&Event{Title: "hi", Text: "okay", AggregationKey: "foo"},
			`_e{2,4}:hi|okay|k:foo`,
		}, {
			&Event{Title: "hi", Text: "okay", AggregationKey: "foo", AlertType: Info},
			`_e{2,4}:hi|okay|k:foo|t:info`,
		}, {
			&Event{Title: "hi", Text: "w/e", AlertType: Error, Priority: Normal},
			`_e{2,3}:hi|w/e|p:normal|t:error`,
		}, {
			&Event{Title: "hi", Text: "uh", Tags: []string{"host:foo", "app:bar"}},
			`_e{2,2}:hi|uh|#host:foo,app:bar`,
		}, {
			&Event{Title: "hi", Text: "line1\nline2", Tags: []string{"hello\nworld"}},
			`_e{2,12}:hi|line1\nline2|#helloworld`,
		},
	}

	for _, m := range matrix {
		r, err := m.event.Encode()
		if err != nil {
			t.Errorf("Error encoding: %s\n", err)
			continue
		}
		if r != m.encoded {
			t.Errorf("Expected `%s`, got `%s`\n", m.encoded, r)
		}
	}

	e := NewEvent("", "hi")
	if _, err := e.Encode(); err == nil {
		t.Errorf("Expected error on empty Title.")
	}

	e = NewEvent("hi", "")
	if _, err := e.Encode(); err == nil {
		t.Errorf("Expected error on empty Text.")
	}

	e = NewEvent("hello", "world")
	s, err := e.Encode("tag1", "tag2")
	if err != nil {
		t.Error(err)
	}
	expected := "_e{5,5}:hello|world|#tag1,tag2"
	if s != expected {
		t.Errorf("Expected %s, got %s", expected, s)
	}
	if len(e.Tags) != 0 {
		t.Errorf("Modified event in place illegally.")
	}
}

func TestServiceChecks(t *testing.T) {
	matrix := []struct {
		serviceCheck *ServiceCheck
		encoded      string
	}{
		{
			NewServiceCheck("DataCatService", Ok),
			`_sc|DataCatService|0`,
		}, {
			NewServiceCheck("DataCatService", Warn),
			`_sc|DataCatService|1`,
		}, {
			NewServiceCheck("DataCatService", Critical),
			`_sc|DataCatService|2`,
		}, {
			NewServiceCheck("DataCatService", Unknown),
			`_sc|DataCatService|3`,
		}, {
			&ServiceCheck{Name: "DataCatService", Status: Ok, Hostname: "DataStation.Cat"},
			`_sc|DataCatService|0|h:DataStation.Cat`,
		}, {
			&ServiceCheck{Name: "DataCatService", Status: Ok, Hostname: "DataStation.Cat", Message: "Here goes valuable message"},
			`_sc|DataCatService|0|h:DataStation.Cat|m:Here goes valuable message`,
		}, {
			&ServiceCheck{Name: "DataCatService", Status: Ok, Hostname: "DataStation.Cat", Message: "Here are some cyrillic chars: к л м н о п р с т у ф х ц ч ш"},
			`_sc|DataCatService|0|h:DataStation.Cat|m:Here are some cyrillic chars: к л м н о п р с т у ф х ц ч ш`,
		}, {
			&ServiceCheck{Name: "DataCatService", Status: Ok, Hostname: "DataStation.Cat", Message: "Here goes valuable message", Tags: []string{"host:foo", "app:bar"}},
			`_sc|DataCatService|0|h:DataStation.Cat|#host:foo,app:bar|m:Here goes valuable message`,
		}, {
			&ServiceCheck{Name: "DataCatService", Status: Ok, Hostname: "DataStation.Cat", Message: "Here goes \n that should be escaped", Tags: []string{"host:foo", "app:b\nar"}},
			`_sc|DataCatService|0|h:DataStation.Cat|#host:foo,app:bar|m:Here goes \n that should be escaped`,
		}, {
			&ServiceCheck{Name: "DataCatService", Status: Ok, Hostname: "DataStation.Cat", Message: "Here goes m: that should be escaped", Tags: []string{"host:foo", "app:bar"}},
			`_sc|DataCatService|0|h:DataStation.Cat|#host:foo,app:bar|m:Here goes m\: that should be escaped`,
		},
	}

	for _, m := range matrix {
		r, err := m.serviceCheck.Encode()
		if err != nil {
			t.Errorf("Error encoding: %s\n", err)
			continue
		}
		if r != m.encoded {
			t.Errorf("Expected `%s`, got `%s`\n", m.encoded, r)
		}
	}

	sc := NewServiceCheck("", Ok)
	if _, err := sc.Encode(); err == nil {
		t.Errorf("Expected error on empty Name.")
	}

	sc = NewServiceCheck("sc", ServiceCheckStatus(5))
	if _, err := sc.Encode(); err == nil {
		t.Errorf("Expected error on invalid status value.")
	}

	sc = NewServiceCheck("hello", Warn)
	s, err := sc.Encode("tag1", "tag2")
	if err != nil {
		t.Error(err)
	}
	expected := "_sc|hello|1|#tag1,tag2"
	if s != expected {
		t.Errorf("Expected %s, got %s", expected, s)
	}
	if len(sc.Tags) != 0 {
		t.Errorf("Modified serviceCheck in place illegally.")
	}
}

// These benchmarks show that using a buffer instead of sprintf-ing together
// a bunch of intermediate strings is 4-5x faster

func BenchmarkFormatNew(b *testing.B) {
	b.StopTimer()
	c := &Client{}
	c.Namespace = "foo.bar."
	c.Tags = []string{"app:foo", "host:bar"}
	b.StartTimer()
	for i := 0; i < b.N; i++ {
		c.format("system.cpu.idle", "10", []string{"foo"}, 1)
		c.format("system.cpu.load", "0.1", nil, 0.9)
	}
}

// Old formatting function, added to client for tests
func (c *Client) formatOld(name, value string, tags []string, rate float64) string {
	if rate < 1 {
		value = fmt.Sprintf("%s|@%f", value, rate)
	}
	if c.Namespace != "" {
		name = fmt.Sprintf("%s%s", c.Namespace, name)
	}

	tags = append(c.Tags, tags...)
	if len(tags) > 0 {
		value = fmt.Sprintf("%s|#%s", value, strings.Join(tags, ","))
	}

	return fmt.Sprintf("%s:%s", name, value)

}

func BenchmarkFormatOld(b *testing.B) {
	b.StopTimer()
	c := &Client{}
	c.Namespace = "foo.bar."
	c.Tags = []string{"app:foo", "host:bar"}
	b.StartTimer()
	for i := 0; i < b.N; i++ {
		c.formatOld("system.cpu.idle", "10", []string{"foo"}, 1)
		c.formatOld("system.cpu.load", "0.1", nil, 0.9)
	}
}
