// Copyright 2020-2021 InfluxData, Inc. All rights reserved.
// Use of this source code is governed by MIT
// license that can be found in the LICENSE file.

package write

import (
	"bytes"
	"fmt"
	"math"
	"math/rand"
	"reflect"
	"strings"
	"testing"
	"time"

	lp "github.com/influxdata/line-protocol"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

var points []*Point

func init() {
	points = make([]*Point, 5000)
	rand.Seed(321)

	t := time.Now()
	for i := 0; i < len(points); i++ {
		points[i] = NewPoint(
			"test",
			map[string]string{
				"id":       fmt.Sprintf("rack_%v", i%100),
				"vendor":   "AWS",
				"hostname": fmt.Sprintf("host_%v", i%10),
			},
			map[string]interface{}{
				"temperature": rand.Float64() * 80.0,
				"disk_free":   rand.Float64() * 1000.0,
				"disk_total":  (i/10 + 1) * 1000000,
				"mem_total":   (i/100 + 1) * 10000000,
				"mem_free":    rand.Float64() * 10000000.0,
			},
			t)
		if i%10 == 0 {
			t = t.Add(time.Second)
		}
	}
}

type ia int

type st struct {
	d float64
	b bool
}

func (s st) String() string {
	return fmt.Sprintf("%.2f d %v", s.d, s.b)
}

func TestConvert(t *testing.T) {
	obj := []struct {
		val       interface{}
		targetVal interface{}
	}{
		{int(-5), int64(-5)},
		{int8(5), int64(5)},
		{int16(-51), int64(-51)},
		{int32(5), int64(5)},
		{int64(555), int64(555)},
		{uint(5), uint64(5)},
		{uint8(55), uint64(55)},
		{uint16(51), uint64(51)},
		{uint32(555), uint64(555)},
		{uint64(5555), uint64(5555)},
		{"a", "a"},
		{true, true},
		{float32(1.2), float64(1.2)},
		{float64(2.2), float64(2.2)},
		{ia(4), "4"},
		{[]string{"a", "b"}, "[a b]"},
		{map[int]string{1: "a", 2: "b"}, "map[1:a 2:b]"},
		{struct {
			a int
			s string
		}{0, "x"}, "{0 x}"},
		{st{1.22, true}, "1.22 d true"},
	}
	for _, tv := range obj {
		t.Run(reflect.TypeOf(tv.val).String(), func(t *testing.T) {
			v := convertField(tv.val)
			assert.Equal(t, reflect.TypeOf(tv.targetVal), reflect.TypeOf(v))
			if f, ok := tv.targetVal.(float64); ok {
				val := reflect.ValueOf(tv.val)
				ft := reflect.TypeOf(float64(0))
				assert.True(t, val.Type().ConvertibleTo(ft))
				valf := val.Convert(ft)
				assert.True(t, math.Abs(f-valf.Float()) < 1e-6)
			} else {
				assert.EqualValues(t, tv.targetVal, v)
			}
		})
	}
}
func TestNewPoint(t *testing.T) {
	p := NewPoint(
		"test",
		map[string]string{
			"id":        "10ad=",
			"ven=dor":   "AWS",
			`host"name`: `ho\st "a"`,
			`x\" x`:     "a b",
		},
		map[string]interface{}{
			"float64":  80.1234567,
			"float32":  float32(80.0),
			"int":      -1234567890,
			"int8":     int8(-34),
			"int16":    int16(-3456),
			"int32":    int32(-34567),
			"int64":    int64(-1234567890),
			"uint":     uint(12345677890),
			"uint8":    uint8(34),
			"uint16":   uint16(3456),
			"uint32":   uint32(34578),
			"uint 64":  uint64(41234567890),
			"bo\\ol":   false,
			`"string"`: `six, "seven", eight`,
			"stri=ng":  `six=seven\, eight`,
			"time":     time.Date(2020, time.March, 20, 10, 30, 23, 123456789, time.UTC),
			"duration": 4*time.Hour + 24*time.Minute + 3*time.Second,
		},
		time.Unix(60, 70))
	verifyPoint(t, p)
}

func verifyPoint(t *testing.T, p *Point) {
	assert.Equal(t, p.Name(), "test")
	require.Len(t, p.TagList(), 4)
	assert.Equal(t, p.TagList(), []*lp.Tag{
		{Key: `host"name`, Value: "ho\\st \"a\""},
		{Key: "id", Value: "10ad="},
		{Key: `ven=dor`, Value: "AWS"},
		{Key: "x\\\" x", Value: "a b"}})
	require.Len(t, p.FieldList(), 17)
	assert.Equal(t, p.FieldList(), []*lp.Field{
		{Key: `"string"`, Value: `six, "seven", eight`},
		{Key: "bo\\ol", Value: false},
		{Key: "duration", Value: "4h24m3s"},
		{Key: "float32", Value: 80.0},
		{Key: "float64", Value: 80.1234567},
		{Key: "int", Value: int64(-1234567890)},
		{Key: "int16", Value: int64(-3456)},
		{Key: "int32", Value: int64(-34567)},
		{Key: "int64", Value: int64(-1234567890)},
		{Key: "int8", Value: int64(-34)},
		{Key: "stri=ng", Value: `six=seven\, eight`},
		{Key: "time", Value: "2020-03-20T10:30:23.123456789Z"},
		{Key: "uint", Value: uint64(12345677890)},
		{Key: "uint 64", Value: uint64(41234567890)},
		{Key: "uint16", Value: uint64(3456)},
		{Key: "uint32", Value: uint64(34578)},
		{Key: "uint8", Value: uint64(34)},
	})
	line := PointToLineProtocol(p, time.Nanosecond)
	assert.True(t, strings.HasSuffix(line, "\n"))
	//cut off last \n char
	line = line[:len(line)-1]
	assert.Equal(t, line, `test,host"name=ho\st\ "a",id=10ad\=,ven\=dor=AWS,x\"\ x=a\ b "string"="six, \"seven\", eight",bo\ol=false,duration="4h24m3s",float32=80,float64=80.1234567,int=-1234567890i,int16=-3456i,int32=-34567i,int64=-1234567890i,int8=-34i,stri\=ng="six=seven\\, eight",time="2020-03-20T10:30:23.123456789Z",uint=12345677890u,uint\ 64=41234567890u,uint16=3456u,uint32=34578u,uint8=34u 60000000070`)
}

func TestPointAdd(t *testing.T) {
	p := NewPointWithMeasurement("test")
	p.AddTag("id", "10ad=")
	p.AddTag("ven=dor", "AWS")
	p.AddTag(`host"name`, "host_a")
	//test re-setting same tag
	p.AddTag(`host"name`, `ho\st "a"`)
	p.AddTag(`x\" x`, "a b")
	p.SortTags()

	p.AddField("float64", 80.1234567)
	p.AddField("float32", float32(80.0))
	p.AddField("int", -1234567890)
	p.AddField("int8", int8(-34))
	p.AddField("int16", int16(-3456))
	p.AddField("int32", int32(-34567))
	p.AddField("int64", int64(-1234567890))
	p.AddField("uint", uint(12345677890))
	p.AddField("uint8", uint8(34))
	p.AddField("uint16", uint16(3456))
	p.AddField("uint32", uint32(34578))
	p.AddField("uint 64", uint64(0))
	// test re-setting same field
	p.AddField("uint 64", uint64(41234567890))
	p.AddField("bo\\ol", false)
	p.AddField(`"string"`, `six, "seven", eight`)
	p.AddField("stri=ng", `six=seven\, eight`)
	p.AddField("time", time.Date(2020, time.March, 20, 10, 30, 23, 123456789, time.UTC))
	p.AddField("duration", time.Duration(4*time.Hour+24*time.Minute+3*time.Second))
	p.SortFields()

	p.SetTime(time.Unix(60, 70))
	verifyPoint(t, p)
}

func TestPointFluent(t *testing.T) {
	p := NewPointWithMeasurement("test").
		AddTag("id", "10ad=").
		AddTag("ven=dor", "AWS").
		AddTag(`host"name`, "host_a").
		//test re-setting same tag
		AddTag(`host"name`, `ho\st "a"`).
		AddTag(`x\" x`, "a b").
		SortTags().
		AddField("float64", 80.1234567).
		AddField("float32", float32(80.0)).
		AddField("int", -1234567890).
		AddField("int8", int8(-34)).
		AddField("int16", int16(-3456)).
		AddField("int32", int32(-34567)).
		AddField("int64", int64(-1234567890)).
		AddField("uint", uint(12345677890)).
		AddField("uint8", uint8(34)).
		AddField("uint16", uint16(3456)).
		AddField("uint32", uint32(34578)).
		AddField("uint 64", uint64(0)).
		// test re-setting same field
		AddField("uint 64", uint64(41234567890)).
		AddField("bo\\ol", false).
		AddField(`"string"`, `six, "seven", eight`).
		AddField("stri=ng", `six=seven\, eight`).
		AddField("time", time.Date(2020, time.March, 20, 10, 30, 23, 123456789, time.UTC)).
		AddField("duration", time.Duration(4*time.Hour+24*time.Minute+3*time.Second)).
		SortFields().
		SetTime(time.Unix(60, 70))

	verifyPoint(t, p)
}

func TestPrecision(t *testing.T) {
	p := NewPointWithMeasurement("test")
	p.AddTag("id", "10")
	p.AddField("float64", 80.1234567)

	p.SetTime(time.Unix(60, 89))
	line := PointToLineProtocol(p, time.Nanosecond)
	assert.Equal(t, line, "test,id=10 float64=80.1234567 60000000089\n")

	p.SetTime(time.Unix(60, 56789))
	line = PointToLineProtocol(p, time.Microsecond)
	assert.Equal(t, line, "test,id=10 float64=80.1234567 60000056\n")

	p.SetTime(time.Unix(60, 123456789))
	line = PointToLineProtocol(p, time.Millisecond)
	assert.Equal(t, line, "test,id=10 float64=80.1234567 60123\n")

	p.SetTime(time.Unix(60, 123456789))
	line = PointToLineProtocol(p, time.Second)
	assert.Equal(t, line, "test,id=10 float64=80.1234567 60\n")
}

func TestTagEscapingKeyAndValue(t *testing.T) {
	p := NewPointWithMeasurement("h\n2\no\t_data")
	p.AddTag("new\nline", "new\nline")
	p.AddTag("carriage\rreturn", "carriage\rreturn")
	p.AddTag("t\tab", "t\tab")
	p.AddField("level", 2)

	line := PointToLineProtocol(p, time.Nanosecond)
	assert.Equal(t, "h\\\\n2\\\\no\\\\t_data,new\\\\nline=new\\\\nline,carriage\\\\rreturn=carriage\\\\rreturn,t\\\\tab=t\\\\tab level=2i\n", line)
}

func TestEqualSignEscaping(t *testing.T) {
	p := NewPointWithMeasurement("h=2o")
	p.AddTag("l=ocation", "e=urope")
	p.AddField("l=evel", 2)

	line := PointToLineProtocol(p, time.Nanosecond)
	assert.Equal(t, "h=2o,l\\=ocation=e\\=urope l\\=evel=2i\n", line)
}

var s string

func BenchmarkPointEncoderSingle(b *testing.B) {
	for n := 0; n < b.N; n++ {
		var buff strings.Builder
		for _, p := range points {
			var buffer bytes.Buffer
			e := lp.NewEncoder(&buffer)
			_, _ = e.Encode(p)
			buff.WriteString(buffer.String())
		}
		s = buff.String()
	}
}

func BenchmarkPointEncoderMulti(b *testing.B) {
	for n := 0; n < b.N; n++ {
		var buffer bytes.Buffer
		e := lp.NewEncoder(&buffer)
		for _, p := range points {
			_, _ = e.Encode(p)
		}
		s = buffer.String()
	}
}

func BenchmarkPointStringSingle(b *testing.B) {
	for n := 0; n < b.N; n++ {
		var buff strings.Builder
		for _, p := range points {
			buff.WriteString(PointToLineProtocol(p, time.Nanosecond))
		}
		s = buff.String()
	}
}

func BenchmarkPointStringMulti(b *testing.B) {
	for n := 0; n < b.N; n++ {
		var buff strings.Builder
		for _, p := range points {
			PointToLineProtocolBuffer(p, &buff, time.Nanosecond)
		}
		s = buff.String()
	}
}
