// Copyright 2017, 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 stackdriver

import (
	"context"
	"fmt"
	"math/big"
	"reflect"
	"sort"
	"strings"
	"testing"
	"time"

	"cloud.google.com/go/trace/apiv2/tracepb"
	timestamppb "github.com/golang/protobuf/ptypes/timestamp"
	wrapperspb "github.com/golang/protobuf/ptypes/wrappers"
	"go.opencensus.io/trace"
	monitoredrespb "google.golang.org/genproto/googleapis/api/monitoredres"
	codepb "google.golang.org/genproto/googleapis/rpc/code"
	statuspb "google.golang.org/genproto/googleapis/rpc/status"
	"google.golang.org/protobuf/encoding/prototext"
)

var (
	traceID = trace.TraceID{0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f}
	spanID  = trace.SpanID{0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1}
)

type spans []*tracepb.Span

func (s spans) Len() int           { return len(s) }
func (s spans) Less(x, y int) bool { return s[x].DisplayName.Value < s[y].DisplayName.Value }
func (s spans) Swap(x, y int)      { s[x], s[y] = s[y], s[x] }

type testExporter struct {
	spans []*trace.SpanData
}

func (t *testExporter) ExportSpan(s *trace.SpanData) {
	t.spans = append(t.spans, s)
}

func generateSpan() {
	ctx := context.Background()
	ctx, span0 := trace.StartSpanWithRemoteParent(
		ctx,
		"span0",
		trace.SpanContext{
			TraceID:      traceID,
			SpanID:       spanID,
			TraceOptions: 1,
		},
	)
	{
		ctx1, span1 := trace.StartSpan(ctx, "span1")
		{
			_, span2 := trace.StartSpan(ctx1, "span2")
			span2.AddMessageSendEvent(0x123, 1024, 512)
			span2.Annotatef(nil, "in span%d", 2)
			span2.Annotate(nil, big.NewRat(2, 4).String())
			span2.AddAttributes(
				trace.StringAttribute("key1", "value1"),
				trace.StringAttribute("key2", "value2"),
				trace.StringAttribute(agentLabel, "custom-agent"))
			span2.AddAttributes(
				trace.Int64Attribute("key1", 100),
				// TODO [rghetia]: uncomment the test case after go.opencensus.io/trace@v0.20.0 is released.
				// trace.Float64Attribute("key3", 100.001),
			)
			span2.End()
		}
		{
			ctx3, span3 := trace.StartSpan(ctx1, "span3")
			span3.Annotate(nil, "in span3")
			span3.AddMessageReceiveEvent(0x456, 2048, 1536)
			span3.SetStatus(trace.Status{Code: int32(codepb.Code_UNAVAILABLE)})
			span3.End()
			{
				_, span4 := trace.StartSpan(ctx3, "span4")
				x := 42
				a1 := []trace.Attribute{trace.StringAttribute("k1", "v1")}
				a2 := []trace.Attribute{trace.StringAttribute("k2", "v2")}
				a3 := []trace.Attribute{trace.StringAttribute("k3", "v3")}
				a4 := map[string]interface{}{"k4": "v4"}
				r := big.NewRat(2, 4)
				span4.Annotate(a1, r.String())
				span4.Annotatef(a2, "foo %d", x)
				span4.Annotate(a3, "in span4")
				span4.AddLink(trace.Link{TraceID: trace.TraceID{1, 2}, SpanID: trace.SpanID{3}, Type: trace.LinkTypeParent, Attributes: a4})
				span4.End()
			}
		}
		span1.End()
	}
	span0.End()
}

func createExpectedSpans() spans {
	ua := trunc(defaultUserAgent, len(defaultUserAgent))
	expectedSpans := spans{
		&tracepb.Span{
			DisplayName:             trunc("span0", 128),
			SameProcessAsParentSpan: &wrapperspb.BoolValue{Value: false},
			Attributes: &tracepb.Span_Attributes{
				AttributeMap: map[string]*tracepb.AttributeValue{
					agentLabel: {Value: &tracepb.AttributeValue_StringValue{StringValue: ua}},
				},
			},
		},
		&tracepb.Span{
			DisplayName:             trunc("span1", 128),
			SameProcessAsParentSpan: &wrapperspb.BoolValue{Value: true},
			Attributes: &tracepb.Span_Attributes{
				AttributeMap: map[string]*tracepb.AttributeValue{
					agentLabel: {Value: &tracepb.AttributeValue_StringValue{StringValue: ua}},
				},
			},
		},
		&tracepb.Span{
			DisplayName: trunc("span2", 128),
			Attributes: &tracepb.Span_Attributes{
				AttributeMap: map[string]*tracepb.AttributeValue{
					"key2": {Value: &tracepb.AttributeValue_StringValue{StringValue: trunc("value2", 256)}},
					"key1": {Value: &tracepb.AttributeValue_IntValue{IntValue: 100}},
					// TODO [rghetia]: uncomment the test case after go.opencensus.io/trace@v0.20.0 is released.
					//"key3": {Value: &tracepb.AttributeValue_StringValue{StringValue: trunc("100.001", 256)}},
					agentLabel: {Value: &tracepb.AttributeValue_StringValue{StringValue: trunc("custom-agent", 256)}},
				},
			},
			TimeEvents: &tracepb.Span_TimeEvents{
				TimeEvent: []*tracepb.Span_TimeEvent{
					{
						Value: &tracepb.Span_TimeEvent_Annotation_{
							Annotation: &tracepb.Span_TimeEvent_Annotation{
								Description: trunc("in span2", 256),
							},
						},
					},
					{
						Value: &tracepb.Span_TimeEvent_Annotation_{
							Annotation: &tracepb.Span_TimeEvent_Annotation{
								Description: trunc("1/2", 256),
							},
						},
					},
					{
						Value: &tracepb.Span_TimeEvent_MessageEvent_{
							MessageEvent: &tracepb.Span_TimeEvent_MessageEvent{
								Type:                  tracepb.Span_TimeEvent_MessageEvent_SENT,
								Id:                    0x123,
								UncompressedSizeBytes: 1024,
								CompressedSizeBytes:   512,
							},
						},
					},
				},
			},
			SameProcessAsParentSpan: &wrapperspb.BoolValue{Value: true},
		},
		&tracepb.Span{
			DisplayName: trunc("span3", 128),
			Attributes: &tracepb.Span_Attributes{
				AttributeMap: map[string]*tracepb.AttributeValue{
					agentLabel: {Value: &tracepb.AttributeValue_StringValue{StringValue: ua}},
				},
			},
			TimeEvents: &tracepb.Span_TimeEvents{
				TimeEvent: []*tracepb.Span_TimeEvent{
					{
						Value: &tracepb.Span_TimeEvent_Annotation_{
							Annotation: &tracepb.Span_TimeEvent_Annotation{
								Description: trunc("in span3", 256),
							},
						},
					},
					{
						Value: &tracepb.Span_TimeEvent_MessageEvent_{
							MessageEvent: &tracepb.Span_TimeEvent_MessageEvent{
								Type:                  tracepb.Span_TimeEvent_MessageEvent_RECEIVED,
								Id:                    0x456,
								UncompressedSizeBytes: 2048,
								CompressedSizeBytes:   1536,
							},
						},
					},
				},
			},
			Status: &statuspb.Status{
				Code: 14,
			},
			SameProcessAsParentSpan: &wrapperspb.BoolValue{Value: true},
		},
		&tracepb.Span{
			DisplayName: trunc("span4", 128),
			Attributes: &tracepb.Span_Attributes{
				AttributeMap: map[string]*tracepb.AttributeValue{
					agentLabel: {Value: &tracepb.AttributeValue_StringValue{StringValue: ua}},
				},
			},
			TimeEvents: &tracepb.Span_TimeEvents{
				TimeEvent: []*tracepb.Span_TimeEvent{
					{
						Value: &tracepb.Span_TimeEvent_Annotation_{
							Annotation: &tracepb.Span_TimeEvent_Annotation{
								Description: trunc("1/2", 256),
								Attributes: &tracepb.Span_Attributes{
									AttributeMap: map[string]*tracepb.AttributeValue{
										"k1": {Value: &tracepb.AttributeValue_StringValue{StringValue: trunc("v1", 256)}},
									},
								},
							},
						},
					},
					{
						Value: &tracepb.Span_TimeEvent_Annotation_{
							Annotation: &tracepb.Span_TimeEvent_Annotation{
								Description: trunc("foo 42", 256),
								Attributes: &tracepb.Span_Attributes{
									AttributeMap: map[string]*tracepb.AttributeValue{
										"k2": {Value: &tracepb.AttributeValue_StringValue{StringValue: trunc("v2", 256)}},
									},
								},
							},
						},
					},
					{
						Value: &tracepb.Span_TimeEvent_Annotation_{
							Annotation: &tracepb.Span_TimeEvent_Annotation{
								Description: trunc("in span4", 256),
								Attributes: &tracepb.Span_Attributes{
									AttributeMap: map[string]*tracepb.AttributeValue{
										"k3": {Value: &tracepb.AttributeValue_StringValue{StringValue: trunc("v3", 256)}},
									},
								},
							},
						},
					},
				},
			},
			Links: &tracepb.Span_Links{
				Link: []*tracepb.Span_Link{
					{
						TraceId: "01020000000000000000000000000000",
						SpanId:  "0300000000000000",
						Type:    tracepb.Span_Link_PARENT_LINKED_SPAN,
						Attributes: &tracepb.Span_Attributes{
							AttributeMap: map[string]*tracepb.AttributeValue{
								"k4": {Value: &tracepb.AttributeValue_StringValue{StringValue: trunc("v4", 256)}},
							},
						},
					},
				},
			},
			SameProcessAsParentSpan: &wrapperspb.BoolValue{Value: true},
		},
	}
	return expectedSpans
}

func TestExportTrace(t *testing.T) {
	var te testExporter
	trace.RegisterExporter(&te)
	defer trace.UnregisterExporter(&te)

	generateSpan()

	if len(te.spans) != 5 {
		t.Errorf("got %d exported spans, want 5", len(te.spans))
	}

	var spbs spans
	for _, s := range te.spans {
		spbs = append(spbs, protoFromSpanData(s, "testproject", nil, defaultUserAgent))
	}
	sort.Sort(spbs)

	for i, want := range []string{
		spanID.String(),
		spbs[0].SpanId,
		spbs[1].SpanId,
		spbs[1].SpanId,
		spbs[3].SpanId,
	} {
		if got := spbs[i].ParentSpanId; got != want {
			t.Errorf("span %d: got ParentSpanID %q want %q", i, got, want)
		}
	}
	checkTime := func(ts **timestamppb.Timestamp) {
		if *ts == nil {
			t.Error("expected timestamp")
		}
		*ts = nil
	}
	for _, span := range spbs {
		checkTime(&span.StartTime)
		checkTime(&span.EndTime)
		if span.TimeEvents != nil {
			for _, te := range span.TimeEvents.TimeEvent {
				checkTime(&te.Time)
			}
		}
		if want := fmt.Sprintf("projects/testproject/traces/%s/spans/%s", traceID, span.SpanId); span.Name != want {
			t.Errorf("got span name %q want %q", span.Name, want)
		}
		span.Name, span.SpanId, span.ParentSpanId = "", "", ""
	}

	expectedSpans := createExpectedSpans()

	if !reflect.DeepEqual(spbs, expectedSpans) {
		var got, want []string
		for _, s := range spbs {
			bytes, err := prototext.Marshal(s)
			if err != nil {
				t.Fatalf("Error marshaling span: %s", err)
			}
			got = append(got, string(bytes))
		}
		for _, s := range expectedSpans {
			bytes, err := prototext.Marshal(s)
			if err != nil {
				t.Fatalf("Error marshaling span: %s", err)
			}
			want = append(want, string(bytes))
		}
		t.Errorf("got spans:\n%s\nwant:\n%s", strings.Join(got, "\n"), strings.Join(want, "\n"))
	}
}

func checkExepectedMonitoredResourceKV(k string, v string, spb *tracepb.Span, t *testing.T) {
	found := spb.Attributes.AttributeMap[k].GetStringValue().GetValue()
	if found != v {
		t.Errorf("Monitored Resource Attributes: got %s want %s for attribute %s", found, v, k)
	}
}

func createAWSEC2MonitoredResource() *monitoredrespb.MonitoredResource {
	mr := &monitoredrespb.MonitoredResource{
		Type: "aws_ec2_instance",
		Labels: map[string]string{
			"instance_id": "i-092676e3abbde2959",
			"aws_account": "999999999999",
			"region":      "aws:us-west-2",
		},
	}
	return mr
}

func createGCEInstanceMonitoredResource() *monitoredrespb.MonitoredResource {
	mr := &monitoredrespb.MonitoredResource{
		Type: "gce_instance",
		Labels: map[string]string{
			"instance_id": "8586409804775703315",
			"project_id":  "project-test",
			"zone":        "us-central1-c",
		},
	}
	return mr
}

func createGKEContainerMonitoredResource() *monitoredrespb.MonitoredResource {
	mr := &monitoredrespb.MonitoredResource{
		Type: "gke_container",
		Labels: map[string]string{
			"instance_id":    "8586409804775703315",
			"project_id":     "project-test",
			"zone":           "us-central1-c",
			"cluster_name":   "cluster",
			"namespace_id":   "namespace",
			"pod_id":         "pod",
			"container_name": "container",
		},
	}
	return mr
}

func TestExportTraceWithMonitoredResource(t *testing.T) {
	var te testExporter
	trace.RegisterExporter(&te)
	defer trace.UnregisterExporter(&te)

	generateSpan()

	// Test GCE Insatance Type monitored resources
	var gceSpbs spans
	mr := createGCEInstanceMonitoredResource()

	for _, s := range te.spans {
		gceSpbs = append(gceSpbs, protoFromSpanData(s, "testproject", mr, defaultUserAgent))
	}

	for _, span := range gceSpbs {
		checkExepectedMonitoredResourceKV("g.co/r/gce_instance/project_id", "project-test", span, t)
		checkExepectedMonitoredResourceKV("g.co/r/gce_instance/instance_id", "8586409804775703315", span, t)
		checkExepectedMonitoredResourceKV("g.co/r/gce_instance/zone", "us-central1-c", span, t)
	}

	// Test GKE Container Type monitored resources
	var gkeSpbs spans
	mr = createGKEContainerMonitoredResource()

	for _, s := range te.spans {
		gkeSpbs = append(gkeSpbs, protoFromSpanData(s, "testproject", mr, defaultUserAgent))
	}

	for _, span := range gkeSpbs {
		checkExepectedMonitoredResourceKV("g.co/r/gke_container/project_id", "project-test", span, t)
		checkExepectedMonitoredResourceKV("g.co/r/gke_container/instance_id", "8586409804775703315", span, t)
		checkExepectedMonitoredResourceKV("g.co/r/gke_container/zone", "us-central1-c", span, t)
		checkExepectedMonitoredResourceKV("g.co/r/gke_container/container_name", "container", span, t)
		checkExepectedMonitoredResourceKV("g.co/r/gke_container/cluster_name", "cluster", span, t)
		checkExepectedMonitoredResourceKV("g.co/r/gke_container/pod_id", "pod", span, t)
		checkExepectedMonitoredResourceKV("g.co/r/gke_container/namespace_id", "namespace", span, t)
	}

	// Test AWS EC2 Instance type monitored resources
	var awsEc2Spbs spans
	mr = createAWSEC2MonitoredResource()
	for _, s := range te.spans {
		awsEc2Spbs = append(awsEc2Spbs, protoFromSpanData(s, "testproject", mr, defaultUserAgent))
	}

	for _, span := range awsEc2Spbs {
		checkExepectedMonitoredResourceKV("g.co/r/aws_ec2_instance/aws_account", "999999999999", span, t)
		checkExepectedMonitoredResourceKV("g.co/r/aws_ec2_instance/instance_id", "i-092676e3abbde2959", span, t)
		checkExepectedMonitoredResourceKV("g.co/r/aws_ec2_instance/region", "aws:us-west-2", span, t)
	}
}

func TestEnums(t *testing.T) {
	for _, test := range []struct {
		x trace.LinkType
		y tracepb.Span_Link_Type
	}{
		{trace.LinkTypeUnspecified, tracepb.Span_Link_TYPE_UNSPECIFIED},
		{trace.LinkTypeChild, tracepb.Span_Link_CHILD_LINKED_SPAN},
		{trace.LinkTypeParent, tracepb.Span_Link_PARENT_LINKED_SPAN},
	} {
		if test.x != trace.LinkType(test.y) {
			t.Errorf("got link type values %d and %d, want equal", test.x, test.y)
		}
	}

	for _, test := range []struct {
		x trace.MessageEventType
		y tracepb.Span_TimeEvent_MessageEvent_Type
	}{
		{trace.MessageEventTypeUnspecified, tracepb.Span_TimeEvent_MessageEvent_TYPE_UNSPECIFIED},
		{trace.MessageEventTypeSent, tracepb.Span_TimeEvent_MessageEvent_SENT},
		{trace.MessageEventTypeRecv, tracepb.Span_TimeEvent_MessageEvent_RECEIVED},
	} {
		if test.x != trace.MessageEventType(test.y) {
			t.Errorf("got network event type values %d and %d, want equal", test.x, test.y)
		}
	}
}

func BenchmarkProto(b *testing.B) {
	sd := &trace.SpanData{
		SpanContext: trace.SpanContext{
			TraceID: traceID,
			SpanID:  spanID,
		},
		Name:       "foo",
		StartTime:  time.Now().Add(-time.Second),
		EndTime:    time.Now(),
		Attributes: map[string]interface{}{"foo": "bar"},
		Annotations: []trace.Annotation{
			{
				Time:       time.Now().Add(-time.Millisecond),
				Message:    "hello, world",
				Attributes: map[string]interface{}{"foo": "bar"},
			},
		},
		MessageEvents: []trace.MessageEvent{
			{
				Time:                 time.Now().Add(-time.Microsecond),
				EventType:            1,
				MessageID:            2,
				UncompressedByteSize: 4,
				CompressedByteSize:   3,
			},
		},
		Status: trace.Status{
			Code:    42,
			Message: "failed",
		},
		HasRemoteParent: true,
	}
	var x int
	for i := 0; i < b.N; i++ {
		s := protoFromSpanData(sd, `testproject`, nil, defaultUserAgent)
		x += len(s.Name)
	}
	if x == 0 {
		fmt.Println(x)
	}
}
