// Copyright The OpenTelemetry 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 jaeger

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"testing"

	commonpb "github.com/census-instrumentation/opencensus-proto/gen-go/agent/common/v1"
	tracepb "github.com/census-instrumentation/opencensus-proto/gen-go/trace/v1"
	"github.com/jaegertracing/jaeger/thrift-gen/jaeger"
	"github.com/stretchr/testify/require"

	"go.opentelemetry.io/collector/consumer/consumerdata"
	"go.opentelemetry.io/collector/testutil"
	tracetranslator "go.opentelemetry.io/collector/translator/trace"
)

func TestThriftBatchToOCProto(t *testing.T) {
	const numOfFiles = 2
	for i := 1; i <= numOfFiles; i++ {
		thriftInFile := fmt.Sprintf("./testdata/thrift_batch_%02d.json", i)
		jb := &jaeger.Batch{}
		if err := loadFromJSON(thriftInFile, jb); err != nil {
			t.Errorf("Failed load Jaeger Thrift from %q. Error: %v", thriftInFile, err)
			continue
		}

		td, err := ThriftBatchToOCProto(jb)
		if err != nil {
			t.Errorf("Failed to handled Jaeger Thrift Batch from %q. Error: %v", thriftInFile, err)
			continue
		}

		wantSpanCount, gotSpanCount := len(jb.Spans), len(td.Spans)
		if wantSpanCount != gotSpanCount {
			t.Errorf("Different number of spans in the batches on pass #%d (want %d, got %d)", i, wantSpanCount, gotSpanCount)
			continue
		}

		gb, err := json.MarshalIndent(td, "", "  ")
		if err != nil {
			t.Errorf("Failed to convert received OC proto to json. Error: %v", err)
			continue
		}

		protoFile := fmt.Sprintf("./testdata/ocproto_batch_%02d.json", i)
		wb, err := ioutil.ReadFile(protoFile)
		if err != nil {
			t.Errorf("Failed to read file %q with expected OC proto in JSON format. Error: %v", protoFile, err)
			continue
		}

		gj, wj := testutil.GenerateNormalizedJSON(t, string(gb)), testutil.GenerateNormalizedJSON(t, string(wb))
		if gj != wj {
			t.Errorf("The roundtrip JSON doesn't match the JSON that we want\nGot:\n%s\nWant:\n%s", gj, wj)
		}
	}
}

func loadFromJSON(file string, obj interface{}) error {
	blob, err := ioutil.ReadFile(file)
	if err == nil {
		err = json.Unmarshal(blob, obj)
	}

	return err
}

// This test ensures that we conservatively allocate, only creating memory when necessary.
func TestConservativeConversions(t *testing.T) {
	batches := []*jaeger.Batch{
		{
			Process: nil,
			Spans: []*jaeger.Span{
				{}, // Blank span
			},
		},
		{
			Process: &jaeger.Process{
				ServiceName: "testHere",
				Tags: []*jaeger.Tag{
					{
						Key:   "storage_version",
						VLong: func() *int64 { v := int64(13); return &v }(),
						VType: jaeger.TagType_LONG,
					},
				},
			},
			Spans: []*jaeger.Span{
				{}, // Blank span
			},
		},
		{
			Process: &jaeger.Process{
				ServiceName: "test2",
			},
			Spans: []*jaeger.Span{
				{TraceIdLow: 0, TraceIdHigh: 0},
				{TraceIdLow: 0x01111111FFFFFFFF, TraceIdHigh: 0x0011121314111111},
				{
					OperationName: "HTTP call",
					TraceIdLow:    0x01111111FFFFFFFF, TraceIdHigh: 0x0011121314111111,
					Tags: []*jaeger.Tag{
						{
							Key:   "http.status_code",
							VLong: func() *int64 { v := int64(403); return &v }(),
							VType: jaeger.TagType_LONG,
						},
						{
							Key:   "http.status_message",
							VStr:  func() *string { v := "Forbidden"; return &v }(),
							VType: jaeger.TagType_STRING,
						},
					},
				},
				{
					OperationName: "RPC call",
					TraceIdLow:    0x01111111FFFFFFFF, TraceIdHigh: 0x0011121314111111,
					Tags: []*jaeger.Tag{
						{
							Key:   "status.code",
							VLong: func() *int64 { v := int64(13); return &v }(),
							VType: jaeger.TagType_LONG,
						},
						{
							Key:   "status.message",
							VStr:  func() *string { v := "proxy crashed"; return &v }(),
							VType: jaeger.TagType_STRING,
						},
					},
				},
			},
		},
		{
			Spans: []*jaeger.Span{
				{
					OperationName: "ThisOne",
					TraceIdLow:    0x1001021314151617,
					TraceIdHigh:   0x100102F3F4F5F6F7,
					SpanId:        0x1011121314151617,
					ParentSpanId:  0x10F1F2F3F4F5F6F7,
					Tags: []*jaeger.Tag{
						{
							Key:   "cache_miss",
							VBool: func() *bool { v := true; return &v }(),
							VType: jaeger.TagType_BOOL,
						},
					},
				},
			},
		},
	}

	got := make([]consumerdata.TraceData, 0, len(batches))
	for i, batch := range batches {
		gb, err := ThriftBatchToOCProto(batch)
		if err != nil {
			t.Errorf("#%d: Unexpected error: %v", i, err)
			continue
		}
		got = append(got, gb)
	}

	want := []consumerdata.TraceData{
		{
			// The conversion returns a slice with capacity equals to the number of elements in the
			// jager batch spans, even if the element is nil.
			Spans: make([]*tracepb.Span, 0, 1),
		},
		{
			Node: &commonpb.Node{
				ServiceInfo: &commonpb.ServiceInfo{Name: "testHere"},
				LibraryInfo: new(commonpb.LibraryInfo),
				Identifier:  new(commonpb.ProcessIdentifier),
				Attributes: map[string]string{
					"storage_version": "13",
				},
			},
			// The conversion returns a slice with capacity equals to the number of elements in the
			// jager batch spans, even if the element is nil.
			Spans: make([]*tracepb.Span, 0, 1),
		},
		{
			Node: &commonpb.Node{
				ServiceInfo: &commonpb.ServiceInfo{Name: "test2"},
				LibraryInfo: new(commonpb.LibraryInfo),
				Identifier:  new(commonpb.ProcessIdentifier),
			},
			Spans: []*tracepb.Span{
				// The first span should be missing
				{
					TraceId: []byte{0x00, 0x11, 0x12, 0x13, 0x14, 0x11, 0x11, 0x11, 0x01, 0x11, 0x11, 0x11, 0xFF, 0xFF, 0xFF, 0xFF},
					SpanId:  []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
				},
				{
					Name:    &tracepb.TruncatableString{Value: "HTTP call"},
					TraceId: []byte{0x00, 0x11, 0x12, 0x13, 0x14, 0x11, 0x11, 0x11, 0x01, 0x11, 0x11, 0x11, 0xFF, 0xFF, 0xFF, 0xFF},
					SpanId:  []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
					// Ensure that the status code was properly translated
					Status: &tracepb.Status{
						Code:    7,
						Message: "Forbidden",
					},
					Attributes: &tracepb.Span_Attributes{
						AttributeMap: map[string]*tracepb.AttributeValue{
							"http.status_code": {
								Value: &tracepb.AttributeValue_IntValue{
									IntValue: 403,
								},
							},
							"http.status_message": {
								Value: &tracepb.AttributeValue_StringValue{
									StringValue: &tracepb.TruncatableString{Value: "Forbidden"},
								},
							},
						},
					},
				},
				{
					Name:    &tracepb.TruncatableString{Value: "RPC call"},
					TraceId: []byte{0x00, 0x11, 0x12, 0x13, 0x14, 0x11, 0x11, 0x11, 0x01, 0x11, 0x11, 0x11, 0xFF, 0xFF, 0xFF, 0xFF},
					SpanId:  []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
					// Ensure that the status code was properly translated
					Status: &tracepb.Status{
						Code:    13,
						Message: "proxy crashed",
					},
					Attributes: nil,
				},
			},
		},
		{
			Spans: []*tracepb.Span{
				{
					Name:         &tracepb.TruncatableString{Value: "ThisOne"},
					SpanId:       []byte{0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17},
					ParentSpanId: []byte{0x10, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7},
					TraceId:      []byte{0x10, 0x01, 0x02, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0x10, 0x01, 0x02, 0x13, 0x14, 0x15, 0x16, 0x17},
					Attributes: &tracepb.Span_Attributes{
						AttributeMap: map[string]*tracepb.AttributeValue{
							"cache_miss": {
								Value: &tracepb.AttributeValue_BoolValue{
									BoolValue: true,
								},
							},
						},
					},
				},
			},
		},
	}

	require.Equal(t, want, got, "Unsuccessful conversion")
}

func TestJaegerStatusTagsToOCStatus(t *testing.T) {
	type test struct {
		haveTags       []*jaeger.Tag
		wantAttributes *tracepb.Span_Attributes
		wantStatus     *tracepb.Status
	}

	cases := []test{
		// only status.code tag
		{
			haveTags: []*jaeger.Tag{
				{
					Key:   "status.code",
					VLong: func() *int64 { v := int64(13); return &v }(),
					VType: jaeger.TagType_LONG,
				},
			},
			wantAttributes: nil,
			wantStatus: &tracepb.Status{
				Code: 13,
			},
		},
		// only status.message tag
		{
			haveTags: []*jaeger.Tag{
				{
					Key:   "status.message",
					VStr:  func() *string { v := "Forbidden"; return &v }(),
					VType: jaeger.TagType_STRING,
				},
			},
			wantAttributes: nil,
			wantStatus:     nil,
		},
		// both status.code and status.message
		{
			haveTags: []*jaeger.Tag{
				{
					Key:   "status.code",
					VLong: func() *int64 { v := int64(13); return &v }(),
					VType: jaeger.TagType_LONG,
				},
				{
					Key:   "status.message",
					VStr:  func() *string { v := "Forbidden"; return &v }(),
					VType: jaeger.TagType_STRING,
				},
			},
			wantAttributes: nil,
			wantStatus: &tracepb.Status{
				Code:    13,
				Message: "Forbidden",
			},
		},

		// http status.code
		{
			haveTags: []*jaeger.Tag{
				{
					Key:   "http.status_code",
					VLong: func() *int64 { v := int64(404); return &v }(),
					VType: jaeger.TagType_LONG,
				},
				{
					Key:   "http.status_message",
					VStr:  func() *string { v := "NotFound"; return &v }(),
					VType: jaeger.TagType_STRING,
				},
			},
			wantAttributes: &tracepb.Span_Attributes{
				AttributeMap: map[string]*tracepb.AttributeValue{
					tracetranslator.TagHTTPStatusCode: {
						Value: &tracepb.AttributeValue_IntValue{
							IntValue: 404,
						},
					},
					tracetranslator.TagHTTPStatusMsg: {
						Value: &tracepb.AttributeValue_StringValue{
							StringValue: &tracepb.TruncatableString{Value: "NotFound"},
						},
					},
				},
			},
			wantStatus: &tracepb.Status{
				Code:    5,
				Message: "NotFound",
			},
		},

		// http and oc
		{
			haveTags: []*jaeger.Tag{
				{
					Key:   "http.status_code",
					VLong: func() *int64 { v := int64(404); return &v }(),
					VType: jaeger.TagType_LONG,
				},
				{
					Key:   "http.status_message",
					VStr:  func() *string { v := "NotFound"; return &v }(),
					VType: jaeger.TagType_STRING,
				},
				{
					Key:   "status.code",
					VLong: func() *int64 { v := int64(13); return &v }(),
					VType: jaeger.TagType_LONG,
				},
				{
					Key:   "status.message",
					VStr:  func() *string { v := "Forbidden"; return &v }(),
					VType: jaeger.TagType_STRING,
				},
			},
			wantAttributes: &tracepb.Span_Attributes{
				AttributeMap: map[string]*tracepb.AttributeValue{
					tracetranslator.TagHTTPStatusCode: {
						Value: &tracepb.AttributeValue_IntValue{
							IntValue: 404,
						},
					},
					tracetranslator.TagHTTPStatusMsg: {
						Value: &tracepb.AttributeValue_StringValue{
							StringValue: &tracepb.TruncatableString{Value: "NotFound"},
						},
					},
				},
			},
			wantStatus: &tracepb.Status{
				Code:    13,
				Message: "Forbidden",
			},
		},
		// http and only oc code
		{
			haveTags: []*jaeger.Tag{
				{
					Key:   "http.status_code",
					VLong: func() *int64 { v := int64(404); return &v }(),
					VType: jaeger.TagType_LONG,
				},
				{
					Key:   "http.status_message",
					VStr:  func() *string { v := "NotFound"; return &v }(),
					VType: jaeger.TagType_STRING,
				},
				{
					Key:   "status.code",
					VLong: func() *int64 { v := int64(14); return &v }(),
					VType: jaeger.TagType_LONG,
				},
			},
			wantAttributes: &tracepb.Span_Attributes{
				AttributeMap: map[string]*tracepb.AttributeValue{
					tracetranslator.TagHTTPStatusCode: {
						Value: &tracepb.AttributeValue_IntValue{
							IntValue: 404,
						},
					},
					tracetranslator.TagHTTPStatusMsg: {
						Value: &tracepb.AttributeValue_StringValue{
							StringValue: &tracepb.TruncatableString{Value: "NotFound"},
						},
					},
				},
			},
			wantStatus: &tracepb.Status{
				Code: 14,
			},
		},
		// http and only oc message
		{
			haveTags: []*jaeger.Tag{
				{
					Key:   "http.status_code",
					VLong: func() *int64 { v := int64(404); return &v }(),
					VType: jaeger.TagType_LONG,
				},
				{
					Key:   "http.status_message",
					VStr:  func() *string { v := "NotFound"; return &v }(),
					VType: jaeger.TagType_STRING,
				},
				{
					Key:   "status.message",
					VStr:  func() *string { v := "Forbidden"; return &v }(),
					VType: jaeger.TagType_STRING,
				},
			},
			wantAttributes: &tracepb.Span_Attributes{
				AttributeMap: map[string]*tracepb.AttributeValue{
					tracetranslator.TagHTTPStatusCode: {
						Value: &tracepb.AttributeValue_IntValue{
							IntValue: 404,
						},
					},
					tracetranslator.TagHTTPStatusMsg: {
						Value: &tracepb.AttributeValue_StringValue{
							StringValue: &tracepb.TruncatableString{Value: "NotFound"},
						},
					},
				},
			},
			wantStatus: &tracepb.Status{
				Code:    5,
				Message: "NotFound",
			},
		},
	}

	for i, c := range cases {
		gb, err := ThriftBatchToOCProto(&jaeger.Batch{
			Process: nil,
			Spans: []*jaeger.Span{{
				TraceIdLow:  0x1001021314151617,
				TraceIdHigh: 0x100102F3F4F5F6F7,
				SpanId:      0x1011121314151617,
				Tags:        c.haveTags,
			}},
		})
		if err != nil {
			t.Errorf("#%d: Unexpected error: %v", i, err)
			continue
		}
		gs := gb.Spans[0]
		require.Equal(t, c.wantAttributes, gs.Attributes, "Unsuccessful conversion %d", i)
		require.Equal(t, c.wantStatus, gs.Status, "Unsuccessful conversion %d", i)
	}
}

func TestHTTPToGRPCStatusCode(t *testing.T) {
	for i := int64(100); i <= 600; i++ {
		wantStatus := tracetranslator.OCStatusCodeFromHTTP(int32(i))
		gb, err := ThriftBatchToOCProto(&jaeger.Batch{
			Process: nil,
			Spans: []*jaeger.Span{{
				TraceIdLow:  0x1001021314151617,
				TraceIdHigh: 0x100102F3F4F5F6F7,
				SpanId:      0x1011121314151617,
				Tags: []*jaeger.Tag{{
					Key:   "http.status_code",
					VLong: &i,
					VType: jaeger.TagType_LONG,
				}},
			}},
		})
		if err != nil {
			t.Errorf("#%d: Unexpected error: %v", i, err)
			continue
		}
		gs := gb.Spans[0]
		require.Equal(t, wantStatus, gs.Status.Code, "Unsuccessful conversion %d", i)
	}
}
