// Copyright 2018, OpenCensus Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package metric

import (
	"fmt"
	"go.opencensus.io/metric/metricdata"
	"sort"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
)

func TestGauge(t *testing.T) {
	r := NewRegistry()

	f, _ := r.AddFloat64Gauge("TestGauge",
		WithLabelKeys("k1", "k2"))
	e, _ := f.GetEntry(metricdata.LabelValue{}, metricdata.LabelValue{})
	e.Set(5)
	e, _ = f.GetEntry(metricdata.NewLabelValue("k1v1"), metricdata.LabelValue{})
	e.Add(1)
	e, _ = f.GetEntry(metricdata.NewLabelValue("k1v1"), metricdata.LabelValue{})
	e.Add(1)
	e, _ = f.GetEntry(metricdata.NewLabelValue("k1v2"), metricdata.NewLabelValue("k2v2"))
	e.Add(1)
	m := r.Read()
	want := []*metricdata.Metric{
		{
			Descriptor: metricdata.Descriptor{
				Name:      "TestGauge",
				LabelKeys: []string{"k1", "k2"},
				Type:      metricdata.TypeGaugeFloat64,
			},
			TimeSeries: []*metricdata.TimeSeries{
				{
					LabelValues: []metricdata.LabelValue{
						{}, {},
					},
					Points: []metricdata.Point{
						metricdata.NewFloat64Point(time.Time{}, 5),
					},
				},
				{
					LabelValues: []metricdata.LabelValue{
						metricdata.NewLabelValue("k1v1"),
						{},
					},
					Points: []metricdata.Point{
						metricdata.NewFloat64Point(time.Time{}, 2),
					},
				},
				{
					LabelValues: []metricdata.LabelValue{
						metricdata.NewLabelValue("k1v2"),
						metricdata.NewLabelValue("k2v2"),
					},
					Points: []metricdata.Point{
						metricdata.NewFloat64Point(time.Time{}, 1),
					},
				},
			},
		},
	}
	canonicalize(m)
	canonicalize(want)
	if diff := cmp.Diff(m, want, cmp.Comparer(ignoreTimes)); diff != "" {
		t.Errorf("-got +want: %s", diff)
	}
}

func TestGaugeMetricDescriptor(t *testing.T) {
	r := NewRegistry()

	gf, _ := r.AddFloat64Gauge("float64_gauge")
	compareType(gf.g.desc.Type, metricdata.TypeGaugeFloat64, t)
	gi, _ := r.AddInt64Gauge("int64_gauge")
	compareType(gi.g.desc.Type, metricdata.TypeGaugeInt64, t)
	dgf, _ := r.AddFloat64DerivedGauge("derived_float64_gauge")
	compareType(dgf.g.desc.Type, metricdata.TypeGaugeFloat64, t)
	dgi, _ := r.AddInt64DerivedGauge("derived_int64_gauge")
	compareType(dgi.g.desc.Type, metricdata.TypeGaugeInt64, t)
}

func compareType(got, want metricdata.Type, t *testing.T) {
	if got != want {
		t.Errorf("metricdata type: got %v, want %v\n", got, want)
	}
}

func TestGaugeMetricOptionDesc(t *testing.T) {
	r := NewRegistry()
	name := "testOptDesc"
	gf, _ := r.AddFloat64Gauge(name, WithDescription("test"))
	want := metricdata.Descriptor{
		Name:        name,
		Description: "test",
		Type:        metricdata.TypeGaugeFloat64,
	}
	got := gf.g.desc
	if !cmp.Equal(got, want) {
		t.Errorf("metric option description: got %v, want %v\n", got, want)
	}
}

func TestGaugeMetricOptionUnit(t *testing.T) {
	r := NewRegistry()
	name := "testOptUnit"
	gf, _ := r.AddFloat64Gauge(name, WithUnit(metricdata.UnitMilliseconds))
	want := metricdata.Descriptor{
		Name: name,
		Unit: metricdata.UnitMilliseconds,
		Type: metricdata.TypeGaugeFloat64,
	}
	got := gf.g.desc
	if !cmp.Equal(got, want) {
		t.Errorf("metric descriptor: got %v, want %v\n", got, want)
	}
}

func TestGaugeMetricOptionLabelKeys(t *testing.T) {
	r := NewRegistry()
	name := "testOptUnit"
	gf, _ := r.AddFloat64Gauge(name, WithLabelKeys("k1", "k3"))
	want := metricdata.Descriptor{
		Name:      name,
		LabelKeys: []string{"k1", "k3"},
		Type:      metricdata.TypeGaugeFloat64,
	}
	got := gf.g.desc
	if !cmp.Equal(got, want) {
		t.Errorf("metric descriptor: got %v, want %v\n", got, want)
	}
}

func TestGaugeMetricOptionDefault(t *testing.T) {
	r := NewRegistry()
	name := "testOptUnit"
	gf, _ := r.AddFloat64Gauge(name)
	want := metricdata.Descriptor{
		Name: name,
		Type: metricdata.TypeGaugeFloat64,
	}
	got := gf.g.desc
	if !cmp.Equal(got, want) {
		t.Errorf("metric descriptor: got %v, want %v\n", got, want)
	}
}

func TestFloat64Entry_Add(t *testing.T) {
	r := NewRegistry()
	g, _ := r.AddFloat64Gauge("g")
	e, _ := g.GetEntry()
	e.Add(0)
	ms := r.Read()
	if got, want := ms[0].TimeSeries[0].Points[0].Value.(float64), 0.0; got != want {
		t.Errorf("value = %v, want %v", got, want)
	}
	e, _ = g.GetEntry()
	e.Add(1)
	ms = r.Read()
	if got, want := ms[0].TimeSeries[0].Points[0].Value.(float64), 1.0; got != want {
		t.Errorf("value = %v, want %v", got, want)
	}
	e, _ = g.GetEntry()
	e.Add(-1)
	ms = r.Read()
	if got, want := ms[0].TimeSeries[0].Points[0].Value.(float64), 0.0; got != want {
		t.Errorf("value = %v, want %v", got, want)
	}
}

func TestFloat64Gauge_Add_NegativeTotals(t *testing.T) {
	r := NewRegistry()
	g, _ := r.AddFloat64Gauge("g")
	e, _ := g.GetEntry()
	e.Add(-1.0)
	ms := r.Read()
	if got, want := ms[0].TimeSeries[0].Points[0].Value.(float64), float64(0); got != want {
		t.Errorf("value = %v, want %v", got, want)
	}
}

func TestInt64GaugeEntry_Add(t *testing.T) {
	r := NewRegistry()
	g, _ := r.AddInt64Gauge("g")
	e, _ := g.GetEntry()
	e.Add(0)
	ms := r.Read()
	if got, want := ms[0].TimeSeries[0].Points[0].Value.(int64), int64(0); got != want {
		t.Errorf("value = %v, want %v", got, want)
	}
	e, _ = g.GetEntry()
	e.Add(1)
	ms = r.Read()
	if got, want := ms[0].TimeSeries[0].Points[0].Value.(int64), int64(1); got != want {
		t.Errorf("value = %v, want %v", got, want)
	}
}

func TestInt64Gauge_Add_NegativeTotals(t *testing.T) {
	r := NewRegistry()
	g, _ := r.AddInt64Gauge("g")
	e, _ := g.GetEntry()
	e.Add(-1)
	ms := r.Read()
	if got, want := ms[0].TimeSeries[0].Points[0].Value.(int64), int64(0); got != want {
		t.Errorf("value = %v, want %v", got, want)
	}
}

func TestGaugeWithSameNameDiffType(t *testing.T) {
	r := NewRegistry()
	r.AddInt64Gauge("g")
	_, gotErr := r.AddFloat64Gauge("g")
	if gotErr == nil {
		t.Errorf("got: nil, want error: %v", errGaugeExistsWithDiffType)
	}
	_, gotErr = r.AddInt64DerivedGauge("g")
	if gotErr == nil {
		t.Errorf("got: nil, want error: %v", errGaugeExistsWithDiffType)
	}
	_, gotErr = r.AddFloat64DerivedGauge("g")
	if gotErr == nil {
		t.Errorf("got: nil, want error: %v", errGaugeExistsWithDiffType)
	}
}

func TestGaugeWithLabelMismatch(t *testing.T) {
	r := NewRegistry()
	g, _ := r.AddInt64Gauge("g", WithLabelKeys("k1"))
	_, gotErr := g.GetEntry(metricdata.NewLabelValue("k1v2"), metricdata.NewLabelValue("k2v2"))
	if gotErr == nil {
		t.Errorf("got: nil, want error: %v", errKeyValueMismatch)
	}
}

func TestMapKey(t *testing.T) {
	cases := [][]metricdata.LabelValue{
		{},
		{metricdata.LabelValue{}},
		{metricdata.NewLabelValue("")},
		{metricdata.NewLabelValue("-")},
		{metricdata.NewLabelValue(",")},
		{metricdata.NewLabelValue("v1"), metricdata.NewLabelValue("v2")},
		{metricdata.NewLabelValue("v1"), metricdata.LabelValue{}},
		{metricdata.NewLabelValue("v1"), metricdata.LabelValue{}, metricdata.NewLabelValue(string([]byte{0}))},
		{metricdata.LabelValue{}, metricdata.LabelValue{}},
	}
	for i, tc := range cases {
		t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
			g := &gauge{
				keys: make([]string, len(tc)),
			}
			mk := g.mapKey(tc)
			vals := g.labelValues(mk)
			if diff := cmp.Diff(vals, tc); diff != "" {
				t.Errorf("values differ after serialization -got +want: %s", diff)
			}
		})
	}
}

func TestRaceCondition(t *testing.T) {
	r := NewRegistry()

	// start reader before adding Gauge metric.
	var ms = []*metricdata.Metric{}
	for i := 0; i < 5; i++ {
		go func(k int) {
			for j := 0; j < 5; j++ {
				g, _ := r.AddInt64Gauge(fmt.Sprintf("g%d%d", k, j))
				e, _ := g.GetEntry()
				e.Add(1)
			}
		}(i)
	}
	time.Sleep(1 * time.Second)
	ms = r.Read()
	if got, want := ms[0].TimeSeries[0].Points[0].Value.(int64), int64(1); got != want {
		t.Errorf("value = %v, want %v", got, want)
	}
}

func ignoreTimes(_, _ time.Time) bool {
	return true
}

func canonicalize(ms []*metricdata.Metric) {
	for _, m := range ms {
		sort.Slice(m.TimeSeries, func(i, j int) bool {
			// sort time series by their label values
			iLabels := m.TimeSeries[i].LabelValues
			jLabels := m.TimeSeries[j].LabelValues
			for k := 0; k < len(iLabels); k++ {
				if !iLabels[k].Present {
					if jLabels[k].Present {
						return true
					}
				} else if !jLabels[k].Present {
					return false
				} else {
					return iLabels[k].Value < jLabels[k].Value
				}
			}
			panic("should have returned")
		})
	}
}

type queueInt64 struct {
	size int64
}

func (q *queueInt64) ToInt64() int64 {
	return q.size
}

func TestInt64DerivedGaugeEntry_Add(t *testing.T) {
	r := NewRegistry()
	q := &queueInt64{3}
	g, _ := r.AddInt64DerivedGauge("g", WithLabelKeys("k1", "k2"))
	err := g.UpsertEntry(q.ToInt64, metricdata.NewLabelValue("k1v1"), metricdata.LabelValue{})
	if err != nil {
		t.Errorf("want: nil, got: %v", err)
	}
	ms := r.Read()
	if got, want := ms[0].TimeSeries[0].Points[0].Value.(int64), int64(3); got != want {
		t.Errorf("value = %v, want %v", got, want)
	}
	q.size = 5
	ms = r.Read()
	if got, want := ms[0].TimeSeries[0].Points[0].Value.(int64), int64(5); got != want {
		t.Errorf("value = %v, want %v", got, want)
	}
}

func TestInt64DerivedGaugeEntry_AddWithNilObj(t *testing.T) {
	r := NewRegistry()
	g, _ := r.AddInt64DerivedGauge("g", WithLabelKeys("k1", "k2"))
	gotErr := g.UpsertEntry(nil, metricdata.NewLabelValue("k1v1"), metricdata.LabelValue{})
	if gotErr == nil {
		t.Errorf("expected error but got nil")
	}
}

func TestInt64DerivedGaugeEntry_AddWithInvalidLabels(t *testing.T) {
	r := NewRegistry()
	q := &queueInt64{3}
	g, _ := r.AddInt64DerivedGauge("g", WithLabelKeys("k1", "k2"))
	gotErr := g.UpsertEntry(q.ToInt64, metricdata.NewLabelValue("k1v1"))
	if gotErr == nil {
		t.Errorf("expected error but got nil")
	}
}

func TestInt64DerivedGaugeEntry_Update(t *testing.T) {
	r := NewRegistry()
	q := &queueInt64{3}
	q2 := &queueInt64{5}
	g, _ := r.AddInt64DerivedGauge("g", WithLabelKeys("k1", "k2"))
	g.UpsertEntry(q.ToInt64, metricdata.NewLabelValue("k1v1"), metricdata.LabelValue{})
	gotErr := g.UpsertEntry(q2.ToInt64, metricdata.NewLabelValue("k1v1"), metricdata.LabelValue{})
	if gotErr != nil {
		t.Errorf("got: %v, want: nil", gotErr)
	}
	ms := r.Read()
	if got, want := ms[0].TimeSeries[0].Points[0].Value.(int64), int64(5); got != want {
		t.Errorf("value = %v, want %v", got, want)
	}
}

type queueFloat64 struct {
	size float64
}

func (q *queueFloat64) ToFloat64() float64 {
	return q.size
}

func TestFloat64DerivedGaugeEntry_Add(t *testing.T) {
	r := NewRegistry()
	q := &queueFloat64{5.0}
	g, _ := r.AddFloat64DerivedGauge("g", WithLabelKeys("k1", "k2"))
	err := g.UpsertEntry(q.ToFloat64, metricdata.NewLabelValue("k1v1"), metricdata.LabelValue{})
	if err != nil {
		t.Errorf("want: nil, got: %v", err)
	}
	ms := r.Read()
	if got, want := ms[0].TimeSeries[0].Points[0].Value.(float64), float64(5.0); got != want {
		t.Errorf("value = %v, want %v", got, want)
	}
	q.size = 5
	ms = r.Read()
	if got, want := ms[0].TimeSeries[0].Points[0].Value.(float64), float64(5.0); got != want {
		t.Errorf("value = %v, want %v", got, want)
	}
}

func TestFloat64DerivedGaugeEntry_AddWithNilObj(t *testing.T) {
	r := NewRegistry()
	g, _ := r.AddFloat64DerivedGauge("g", WithLabelKeys("k1", "k2"))
	gotErr := g.UpsertEntry(nil, metricdata.NewLabelValue("k1v1"), metricdata.LabelValue{})
	if gotErr == nil {
		t.Errorf("expected error but got nil")
	}
}

func TestFloat64DerivedGaugeEntry_AddWithInvalidLabels(t *testing.T) {
	r := NewRegistry()
	q := &queueFloat64{3}
	g, _ := r.AddFloat64DerivedGauge("g", WithLabelKeys("k1", "k2"))
	gotErr := g.UpsertEntry(q.ToFloat64, metricdata.NewLabelValue("k1v1"))
	if gotErr == nil {
		t.Errorf("expected error but got nil")
	}
}

func TestFloat64DerivedGaugeEntry_Update(t *testing.T) {
	r := NewRegistry()
	q := &queueFloat64{3.0}
	q2 := &queueFloat64{5.0}
	g, _ := r.AddFloat64DerivedGauge("g", WithLabelKeys("k1", "k2"))
	g.UpsertEntry(q.ToFloat64, metricdata.NewLabelValue("k1v1"), metricdata.LabelValue{})
	gotErr := g.UpsertEntry(q2.ToFloat64, metricdata.NewLabelValue("k1v1"), metricdata.LabelValue{})
	if gotErr != nil {
		t.Errorf("got: %v, want: nil", gotErr)
	}
	ms := r.Read()
	if got, want := ms[0].TimeSeries[0].Points[0].Value.(float64), float64(5.0); got != want {
		t.Errorf("value = %v, want %v", got, want)
	}
}
