// Copyright 2019, 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 view

import (
	"context"
	"testing"
	"time"

	"encoding/json"

	"github.com/google/go-cmp/cmp"
	"go.opencensus.io/metric/metricdata"
	"go.opencensus.io/metric/metricexport"
	"go.opencensus.io/stats"
	"go.opencensus.io/tag"
)

type recordValWithTag struct {
	tags  []tag.Tag
	value interface{}
}
type testToMetrics struct {
	vi          *viewInternal
	view        *View
	recordValue []recordValWithTag
	wantMetric  *metricdata.Metric
}

var (
	// tag objects.
	tk1   tag.Key
	tk2   tag.Key
	tk3   tag.Key
	tk1v1 tag.Tag
	tk2v2 tag.Tag
	tags  []tag.Tag

	labelValues      []metricdata.LabelValue
	emptyLabelValues []metricdata.LabelValue

	labelKeys []metricdata.LabelKey

	recordsInt64        []recordValWithTag
	recordsFloat64      []recordValWithTag
	recordsFloat64WoTag []recordValWithTag

	// distribution objects.
	aggDist *Aggregation
	aggCnt  *Aggregation
	aggS    *Aggregation
	aggL    *Aggregation
	buckOpt *metricdata.BucketOptions

	// exemplar objects.
	attachments metricdata.Attachments

	// views and descriptors
	viewTypeFloat64Distribution         *View
	viewTypeInt64Distribution           *View
	viewTypeInt64Count                  *View
	viewTypeFloat64Count                *View
	viewTypeFloat64Sum                  *View
	viewTypeInt64Sum                    *View
	viewTypeFloat64LastValue            *View
	viewTypeInt64LastValue              *View
	viewRecordWithoutLabel              *View
	mdTypeFloat64CumulativeDistribution metricdata.Descriptor
	mdTypeInt64CumulativeDistribution   metricdata.Descriptor
	mdTypeInt64CumulativeCount          metricdata.Descriptor
	mdTypeFloat64CumulativeCount        metricdata.Descriptor
	mdTypeInt64CumulativeSum            metricdata.Descriptor
	mdTypeFloat64CumulativeSum          metricdata.Descriptor
	mdTypeInt64CumulativeLastValue      metricdata.Descriptor
	mdTypeFloat64CumulativeLastValue    metricdata.Descriptor
	mdTypeRecordWithoutLabel            metricdata.Descriptor
)

const (
	nameInt64DistM1        = "viewToMetricTest_Int64_Distribution/m1"
	nameFloat64DistM1      = "viewToMetricTest_Float64_Distribution/m1"
	nameInt64CountM1       = "viewToMetricTest_Int64_Count/m1"
	nameFloat64CountM1     = "viewToMetricTest_Float64_Count/m1"
	nameInt64SumM1         = "viewToMetricTest_Int64_Sum/m1"
	nameFloat64SumM1       = "viewToMetricTest_Float64_Sum/m1"
	nameInt64LastValueM1   = "viewToMetricTest_Int64_LastValue/m1"
	nameFloat64LastValueM1 = "viewToMetricTest_Float64_LastValue/m1"
	nameRecordWithoutLabel = "viewToMetricTest_RecordWithoutLabel/m1"
	v1                     = "v1"
	v2                     = "v2"
)

func init() {
	initTags()
	initAgg()
	initViews()
	initMetricDescriptors()

}

func initTags() {
	tk1 = tag.MustNewKey("k1")
	tk2 = tag.MustNewKey("k2")
	tk3 = tag.MustNewKey("k3")
	tk1v1 = tag.Tag{Key: tk1, Value: v1}
	tk2v2 = tag.Tag{Key: tk2, Value: v2}

	tags = []tag.Tag{tk1v1, tk2v2}
	labelValues = []metricdata.LabelValue{
		{Value: v1, Present: true},
		{Value: v2, Present: true},
	}
	emptyLabelValues = []metricdata.LabelValue{
		{Value: "", Present: false},
		{Value: "", Present: false},
	}
	labelKeys = []metricdata.LabelKey{
		{Key: tk1.Name()},
		{Key: tk2.Name()},
	}

	recordsInt64 = []recordValWithTag{
		{tags: tags, value: int64(2)},
		{tags: tags, value: int64(4)},
	}
	recordsFloat64 = []recordValWithTag{
		{tags: tags, value: float64(1.5)},
		{tags: tags, value: float64(5.4)},
	}
	recordsFloat64WoTag = []recordValWithTag{
		{value: float64(1.5)},
		{value: float64(5.4)},
	}
}

func initAgg() {
	aggDist = Distribution(2.0)
	aggCnt = Count()
	aggS = Sum()
	aggL = LastValue()
	buckOpt = &metricdata.BucketOptions{Bounds: []float64{2.0}}
}

func initViews() {
	// View objects
	viewTypeInt64Distribution = &View{
		Name:        nameInt64DistM1,
		TagKeys:     []tag.Key{tk1, tk2},
		Measure:     stats.Int64(nameInt64DistM1, "", stats.UnitDimensionless),
		Aggregation: aggDist,
	}
	viewTypeFloat64Distribution = &View{
		Name:        nameFloat64DistM1,
		TagKeys:     []tag.Key{tk1, tk2},
		Measure:     stats.Float64(nameFloat64DistM1, "", stats.UnitDimensionless),
		Aggregation: aggDist,
	}
	viewTypeInt64Count = &View{
		Name:        nameInt64CountM1,
		TagKeys:     []tag.Key{tk1, tk2},
		Measure:     stats.Int64(nameInt64CountM1, "", stats.UnitDimensionless),
		Aggregation: aggCnt,
	}
	viewTypeFloat64Count = &View{
		Name:        nameFloat64CountM1,
		TagKeys:     []tag.Key{tk1, tk2},
		Measure:     stats.Float64(nameFloat64CountM1, "", stats.UnitDimensionless),
		Aggregation: aggCnt,
	}
	viewTypeInt64Sum = &View{
		Name:        nameInt64SumM1,
		TagKeys:     []tag.Key{tk1, tk2},
		Measure:     stats.Int64(nameInt64SumM1, "", stats.UnitBytes),
		Aggregation: aggS,
	}
	viewTypeFloat64Sum = &View{
		Name:        nameFloat64SumM1,
		TagKeys:     []tag.Key{tk1, tk2},
		Measure:     stats.Float64(nameFloat64SumM1, "", stats.UnitMilliseconds),
		Aggregation: aggS,
	}
	viewTypeInt64LastValue = &View{
		Name:        nameInt64LastValueM1,
		TagKeys:     []tag.Key{tk1, tk2},
		Measure:     stats.Int64(nameInt64LastValueM1, "", stats.UnitDimensionless),
		Aggregation: aggL,
	}
	viewTypeFloat64LastValue = &View{
		Name:        nameFloat64LastValueM1,
		TagKeys:     []tag.Key{tk1, tk2},
		Measure:     stats.Float64(nameFloat64LastValueM1, "", stats.UnitDimensionless),
		Aggregation: aggL,
	}
	viewRecordWithoutLabel = &View{
		Name:        nameRecordWithoutLabel,
		TagKeys:     []tag.Key{tk1, tk2},
		Measure:     stats.Float64(nameRecordWithoutLabel, "", stats.UnitDimensionless),
		Aggregation: aggL,
	}
}

func initMetricDescriptors() {
	// Metric objects
	mdTypeFloat64CumulativeDistribution = metricdata.Descriptor{
		Name: nameFloat64DistM1, Description: "", Unit: metricdata.UnitDimensionless,
		Type: metricdata.TypeCumulativeDistribution, LabelKeys: labelKeys,
	}
	mdTypeInt64CumulativeDistribution = metricdata.Descriptor{
		Name: nameInt64DistM1, Description: "", Unit: metricdata.UnitDimensionless,
		Type: metricdata.TypeCumulativeDistribution, LabelKeys: labelKeys,
	}
	mdTypeInt64CumulativeCount = metricdata.Descriptor{
		Name: nameInt64CountM1, Description: "", Unit: metricdata.UnitDimensionless,
		Type: metricdata.TypeCumulativeInt64, LabelKeys: labelKeys,
	}
	mdTypeFloat64CumulativeCount = metricdata.Descriptor{
		Name: nameFloat64CountM1, Description: "", Unit: metricdata.UnitDimensionless,
		Type: metricdata.TypeCumulativeInt64, LabelKeys: labelKeys,
	}
	mdTypeInt64CumulativeSum = metricdata.Descriptor{
		Name: nameInt64SumM1, Description: "", Unit: metricdata.UnitBytes,
		Type: metricdata.TypeCumulativeInt64, LabelKeys: labelKeys,
	}
	mdTypeFloat64CumulativeSum = metricdata.Descriptor{
		Name: nameFloat64SumM1, Description: "", Unit: metricdata.UnitMilliseconds,
		Type: metricdata.TypeCumulativeFloat64, LabelKeys: labelKeys,
	}
	mdTypeInt64CumulativeLastValue = metricdata.Descriptor{
		Name: nameInt64LastValueM1, Description: "", Unit: metricdata.UnitDimensionless,
		Type: metricdata.TypeGaugeInt64, LabelKeys: labelKeys,
	}
	mdTypeFloat64CumulativeLastValue = metricdata.Descriptor{
		Name: nameFloat64LastValueM1, Description: "", Unit: metricdata.UnitDimensionless,
		Type: metricdata.TypeGaugeFloat64, LabelKeys: labelKeys,
	}
	mdTypeRecordWithoutLabel = metricdata.Descriptor{
		Name: nameRecordWithoutLabel, Description: "", Unit: metricdata.UnitDimensionless,
		Type: metricdata.TypeGaugeFloat64, LabelKeys: labelKeys,
	}
}

func Test_ViewToMetric(t *testing.T) {
	now := time.Now()
	tests := []*testToMetrics{
		{
			view:        viewTypeInt64Distribution,
			recordValue: recordsInt64,
			wantMetric: &metricdata.Metric{
				Descriptor: mdTypeInt64CumulativeDistribution,
				TimeSeries: []*metricdata.TimeSeries{
					{Points: []metricdata.Point{
						{Value: &metricdata.Distribution{
							Count:                 2,
							Sum:                   6.0,
							SumOfSquaredDeviation: 2,
							BucketOptions:         buckOpt,
							Buckets: []metricdata.Bucket{
								{Count: 0, Exemplar: nil},
								{Count: 2, Exemplar: nil},
							},
						},
							Time: now,
						},
					},
						LabelValues: labelValues,
						StartTime:   now,
					},
				},
			},
		},
		{
			view:        viewTypeFloat64Distribution,
			recordValue: recordsFloat64,
			wantMetric: &metricdata.Metric{
				Descriptor: mdTypeFloat64CumulativeDistribution,
				TimeSeries: []*metricdata.TimeSeries{
					{
						Points: []metricdata.Point{
							{
								Value: &metricdata.Distribution{
									Count:                 2,
									Sum:                   6.9,
									SumOfSquaredDeviation: 7.605000000000001,
									BucketOptions:         buckOpt,
									Buckets: []metricdata.Bucket{
										{Count: 1, Exemplar: nil}, // TODO: [rghetia] add exemplar test.
										{Count: 1, Exemplar: nil},
									},
								},
								Time: now,
							},
						},
						LabelValues: labelValues,
						StartTime:   now,
					},
				},
			},
		},
		{
			view:        viewTypeInt64Count,
			recordValue: recordsInt64,
			wantMetric: &metricdata.Metric{
				Descriptor: mdTypeInt64CumulativeCount,
				TimeSeries: []*metricdata.TimeSeries{
					{Points: []metricdata.Point{
						metricdata.NewInt64Point(now, 2),
					},
						LabelValues: labelValues,
						StartTime:   now,
					},
				},
			},
		},
		{
			view:        viewTypeFloat64Count,
			recordValue: recordsFloat64,
			wantMetric: &metricdata.Metric{
				Descriptor: mdTypeFloat64CumulativeCount,
				TimeSeries: []*metricdata.TimeSeries{
					{Points: []metricdata.Point{
						metricdata.NewInt64Point(now, 2),
					},
						LabelValues: labelValues,
						StartTime:   now,
					},
				},
			},
		},
		{
			view:        viewTypeInt64Sum,
			recordValue: recordsInt64,
			wantMetric: &metricdata.Metric{
				Descriptor: mdTypeInt64CumulativeSum,
				TimeSeries: []*metricdata.TimeSeries{
					{Points: []metricdata.Point{
						metricdata.NewInt64Point(now, 6),
					},
						LabelValues: labelValues,
						StartTime:   now,
					},
				},
			},
		},
		{
			view:        viewTypeFloat64Sum,
			recordValue: recordsFloat64,
			wantMetric: &metricdata.Metric{
				Descriptor: mdTypeFloat64CumulativeSum,
				TimeSeries: []*metricdata.TimeSeries{
					{Points: []metricdata.Point{
						metricdata.NewFloat64Point(now, 6.9),
					},
						LabelValues: labelValues,
						StartTime:   now,
					},
				},
			},
		},
		{
			view:        viewTypeInt64LastValue,
			recordValue: recordsInt64,
			wantMetric: &metricdata.Metric{
				Descriptor: mdTypeInt64CumulativeLastValue,
				TimeSeries: []*metricdata.TimeSeries{
					{Points: []metricdata.Point{
						metricdata.NewInt64Point(now, 4),
					},
						LabelValues: labelValues,
						StartTime:   time.Time{},
					},
				},
			},
		},
		{
			view:        viewTypeFloat64LastValue,
			recordValue: recordsFloat64,
			wantMetric: &metricdata.Metric{
				Descriptor: mdTypeFloat64CumulativeLastValue,
				TimeSeries: []*metricdata.TimeSeries{
					{Points: []metricdata.Point{
						metricdata.NewFloat64Point(now, 5.4),
					},
						LabelValues: labelValues,
						StartTime:   time.Time{},
					},
				},
			},
		},
		{
			view:        viewRecordWithoutLabel,
			recordValue: recordsFloat64WoTag,
			wantMetric: &metricdata.Metric{
				Descriptor: mdTypeRecordWithoutLabel,
				TimeSeries: []*metricdata.TimeSeries{
					{Points: []metricdata.Point{
						metricdata.NewFloat64Point(now, 5.4),
					},
						LabelValues: emptyLabelValues,
						StartTime:   time.Time{},
					},
				},
			},
		},
	}

	for _, tc := range tests {
		tc.vi, _ = defaultWorker.tryRegisterView(tc.view)
		tc.vi.clearRows()
		tc.vi.subscribe()
	}

	for i, tc := range tests {
		for _, r := range tc.recordValue {
			mods := []tag.Mutator{}
			for _, tg := range r.tags {
				mods = append(mods, tag.Insert(tg.Key, tg.Value))
			}
			ctx, err := tag.New(context.Background(), mods...)
			if err != nil {
				t.Errorf("%v: New = %v", tc.view.Name, err)
			}
			var v float64
			switch i := r.value.(type) {
			case float64:
				v = float64(i)
			case int64:
				v = float64(i)
			default:
				t.Errorf("unexpected value type %v", r.tags)
			}
			tc.vi.addSample(tag.FromContext(ctx), v, nil, now)
		}

		gotMetric := viewToMetric(tc.vi, nil, now)
		if !cmp.Equal(gotMetric, tc.wantMetric) {
			// JSON format is strictly for checking the content when test fails. Do not use JSON
			// format to determine if the two values are same as it doesn't differentiate between
			// int64(2) and float64(2.0)
			t.Errorf("#%d: Unmatched \nGot:\n\t%v\nWant:\n\t%v\nGot Serialized:%s\nWant Serialized:%s\n",
				i, gotMetric, tc.wantMetric, serializeAsJSON(gotMetric), serializeAsJSON(tc.wantMetric))
		}
	}
}

// Test to verify that a metric converted from a view with Aggregation Count should always
// have Dimensionless unit.
func TestUnitConversionForAggCount(t *testing.T) {
	now := time.Now()
	tests := []*struct {
		name     string
		vi       *viewInternal
		v        *View
		wantUnit metricdata.Unit
	}{
		{
			name: "View with Count Aggregation on Latency measurement",
			v: &View{
				Name:        "request_count1",
				Measure:     stats.Int64("request_latency", "", stats.UnitMilliseconds),
				Aggregation: aggCnt,
			},
			wantUnit: metricdata.UnitDimensionless,
		},
		{
			name: "View with Count Aggregation on bytes measurement",
			v: &View{
				Name:        "request_count2",
				Measure:     stats.Int64("request_bytes", "", stats.UnitBytes),
				Aggregation: aggCnt,
			},
			wantUnit: metricdata.UnitDimensionless,
		},
		{
			name: "View with aggregation other than Count Aggregation on Latency measurement",
			v: &View{
				Name:        "request_latency",
				Measure:     stats.Int64("request_latency", "", stats.UnitMilliseconds),
				Aggregation: aggSum,
			},
			wantUnit: metricdata.UnitMilliseconds,
		},
	}
	var err error
	for _, tc := range tests {
		tc.vi, err = defaultWorker.tryRegisterView(tc.v)
		if err != nil {
			t.Fatalf("error registering view: %v, err: %v\n", tc.v, err)
		}
		tc.vi.clearRows()
		tc.vi.subscribe()
	}

	for _, tc := range tests {
		tc.vi.addSample(tag.FromContext(context.Background()), 5.0, nil, now)
		gotMetric := viewToMetric(tc.vi, nil, now)
		gotUnit := gotMetric.Descriptor.Unit
		if !cmp.Equal(gotUnit, tc.wantUnit) {
			t.Errorf("Verify Unit: %s: Got:%v Want:%v", tc.name, gotUnit, tc.wantUnit)
		}
	}
}

type mockExp struct {
	metrics []*metricdata.Metric
}

func (me *mockExp) ExportMetrics(ctx context.Context, metrics []*metricdata.Metric) error {
	me.metrics = append(me.metrics, metrics...)
	return nil
}

var _ metricexport.Exporter = (*mockExp)(nil)

func TestViewToMetric_OutOfOrderWithZeroBuckets(t *testing.T) {
	m := stats.Int64("OutOfOrderWithZeroBuckets", "", "")
	now := time.Now()
	tts := []struct {
		v *View
		m *metricdata.Metric
	}{
		{
			v: &View{
				Name:        m.Name() + "_order1",
				Measure:     m,
				Aggregation: Distribution(10, 0, 2),
			},
			m: &metricdata.Metric{
				Descriptor: metricdata.Descriptor{
					Name:      "OutOfOrderWithZeroBuckets_order1",
					Unit:      metricdata.UnitDimensionless,
					Type:      metricdata.TypeCumulativeDistribution,
					LabelKeys: []metricdata.LabelKey{},
				},
				TimeSeries: []*metricdata.TimeSeries{
					{Points: []metricdata.Point{
						{Value: &metricdata.Distribution{
							Count:                 3,
							Sum:                   9.0,
							SumOfSquaredDeviation: 8,
							BucketOptions: &metricdata.BucketOptions{
								Bounds: []float64{2, 10},
							},
							Buckets: []metricdata.Bucket{
								{Count: 1, Exemplar: nil},
								{Count: 2, Exemplar: nil},
								{Count: 0, Exemplar: nil},
							},
						},
							Time: now,
						},
					},
						StartTime:   now,
						LabelValues: []metricdata.LabelValue{},
					},
				},
			},
		},
		{
			v: &View{
				Name:        m.Name() + "_order2",
				Measure:     m,
				Aggregation: Distribution(0, 5, 10),
			},
			m: &metricdata.Metric{
				Descriptor: metricdata.Descriptor{
					Name:      "OutOfOrderWithZeroBuckets_order2",
					Unit:      metricdata.UnitDimensionless,
					Type:      metricdata.TypeCumulativeDistribution,
					LabelKeys: []metricdata.LabelKey{},
				},
				TimeSeries: []*metricdata.TimeSeries{
					{Points: []metricdata.Point{
						{Value: &metricdata.Distribution{
							Count:                 3,
							Sum:                   9.0,
							SumOfSquaredDeviation: 8,
							BucketOptions: &metricdata.BucketOptions{
								Bounds: []float64{5, 10},
							},
							Buckets: []metricdata.Bucket{
								{Count: 2, Exemplar: nil},
								{Count: 1, Exemplar: nil},
								{Count: 0, Exemplar: nil},
							},
						},
							Time: now,
						},
					},
						StartTime:   now,
						LabelValues: []metricdata.LabelValue{},
					},
				},
			},
		},
	}
	for _, tt := range tts {
		err := Register(tt.v)
		if err != nil {
			t.Fatalf("error registering view %v, err: %v", tt.v, err)
		}

	}

	stats.Record(context.Background(), m.M(5), m.M(1), m.M(3))
	time.Sleep(1 * time.Second)

	me := &mockExp{}
	reader := metricexport.NewReader()
	reader.ReadAndExport(me)

	var got *metricdata.Metric
	lookup := func(vname string, metrics []*metricdata.Metric) *metricdata.Metric {
		for _, m := range metrics {
			if m.Descriptor.Name == vname {
				return m
			}
		}
		return nil
	}

	for _, tt := range tts {
		got = lookup(tt.v.Name, me.metrics)
		if got == nil {
			t.Fatalf("metric %s not found in %v\n", tt.v.Name, me.metrics)
		}
		got.TimeSeries[0].Points[0].Time = now
		got.TimeSeries[0].StartTime = now

		want := tt.m
		if diff := cmp.Diff(got, want); diff != "" {
			t.Errorf("buckets differ -got +want: %s \n Serialized got %v\n, Serialized want %v\n", diff, serializeAsJSON(got), serializeAsJSON(want))
		}
	}
}

func serializeAsJSON(v interface{}) string {
	blob, _ := json.MarshalIndent(v, "", "  ")
	return string(blob)
}
