package pbmsg

import (
	"bytes"
	"testing"

	"github.com/golang/protobuf/proto"

	"code.justin.tv/release/trace/api"
	"code.justin.tv/release/trace/common"
	"code.justin.tv/release/trace/pbmsg/iana"
)

func TestDowngrade(t *testing.T) {
	in := &api.Transaction{
		TransactionId: []uint64{1, 2},
		Root: &api.Call{
			Path: []uint32{},
			Svc: &api.Service{
				Name: "code.justin.tv/release/trace/cmd/service",
				Host: "trace.internal.justin.tv",
				Pid:  123,
				Peer: "localhost:12345",
			},
			Params: &api.RPCParams{
				Http: &api.HTTPParams{
					UriPath: "/somewhere",
					Status:  200,
					Method:  common.Method_GET,
				},
			},
			ServiceTimestamps: []*api.Timestamp{
				&api.Timestamp{
					Time: 1,
					Kind: common.Kind_REQUEST_HEAD_RECEIVED,
				},
				&api.Timestamp{
					Time: 10,
					Kind: common.Kind_RESPONSE_BODY_SENT,
				},
			},
			Subcalls: []*api.Call{
				&api.Call{
					Path: []uint32{0},
					Svc: &api.Service{
						Name: "code.justin.tv/release/trace/cmd/backend",
						Host: "trace-backend.internal.justin.tv",
						Pid:  456,
					},
					Params: &api.RPCParams{
						Http: &api.HTTPParams{
							UriPath: "/somewhere_else",
							Status:  304,
							Method:  common.Method_OPTIONS,
						},
					},
					ClientTimestamps: []*api.Timestamp{
						&api.Timestamp{
							Time: 2,
							Kind: common.Kind_REQUEST_HEAD_PREPARED,
						},
						&api.Timestamp{
							Time: 5,
							Kind: common.Kind_RESPONSE_BODY_RECEIVED,
						},
					},
					ServiceTimestamps: []*api.Timestamp{
						&api.Timestamp{
							Time: 3,
							Kind: common.Kind_REQUEST_HEAD_RECEIVED,
						},
						&api.Timestamp{
							Time: 4,
							Kind: common.Kind_RESPONSE_BODY_SENT,
						},
					},
				},
				&api.Call{
					Path: []uint32{1},
					Svc: &api.Service{
						Name: "code.justin.tv/release/trace/cmd/db",
						Host: "trace-db.internal.justin.tv",
						Pid:  789,
					},
					Params: &api.RPCParams{
						Sql: &api.SQLParams{
							Dbname:        "prod",
							Dbuser:        "user",
							StrippedQuery: "select * from users",
							Tables:        []string{"users"},
						},
					},
					ClientTimestamps: []*api.Timestamp{
						&api.Timestamp{
							Time: 6,
							Kind: common.Kind_REQUEST_HEAD_PREPARED,
						},
						&api.Timestamp{
							Time: 9,
							Kind: common.Kind_RESPONSE_BODY_RECEIVED,
						},
					},
					ServiceTimestamps: []*api.Timestamp{
						&api.Timestamp{
							Time: 7,
							Kind: common.Kind_REQUEST_HEAD_RECEIVED,
						},
						&api.Timestamp{
							Time: 8,
							Kind: common.Kind_RESPONSE_BODY_SENT,
						},
					},
				},
			},
		},
		Client: &api.Service{},
	}

	want := &Transaction{
		TransactionId: []uint64{1, 2},
		Root: &Call{
			Path: []uint32{},
			Svc: &Service{
				Name: proto.String("code.justin.tv/release/trace/cmd/service"),
				Host: proto.String("trace.internal.justin.tv"),
				Pid:  proto.Int32(123),
				Peer: proto.String("localhost:12345"),
			},
			Params: &RPCParams{
				Http: &HTTPParams{
					UriPath: proto.String("/somewhere"),
					Status:  proto.Int32(200),
					Method:  iana.HTTPMethod_GET.Enum(),
				},
			},
			ServiceTimestamps: []*Timestamp{
				&Timestamp{
					Time: proto.Int64(1),
					Kind: Event_REQUEST_HEAD_RECEIVED.Enum(),
				},
				&Timestamp{
					Time: proto.Int64(10),
					Kind: Event_RESPONSE_BODY_SENT.Enum(),
				},
			},
			Subcalls: []*Call{
				&Call{
					Path: []uint32{0},
					Svc: &Service{
						Name: proto.String("code.justin.tv/release/trace/cmd/backend"),
						Host: proto.String("trace-backend.internal.justin.tv"),
						Pid:  proto.Int32(456),
					},
					Params: &RPCParams{
						Http: &HTTPParams{
							UriPath: proto.String("/somewhere_else"),
							Status:  proto.Int32(304),
							Method:  iana.HTTPMethod_OPTIONS.Enum(),
						},
					},
					ClientTimestamps: []*Timestamp{
						&Timestamp{
							Time: proto.Int64(2),
							Kind: Event_REQUEST_HEAD_PREPARED.Enum(),
						},
						&Timestamp{
							Time: proto.Int64(5),
							Kind: Event_RESPONSE_BODY_RECEIVED.Enum(),
						},
					},
					ServiceTimestamps: []*Timestamp{
						&Timestamp{
							Time: proto.Int64(3),
							Kind: Event_REQUEST_HEAD_RECEIVED.Enum(),
						},
						&Timestamp{
							Time: proto.Int64(4),
							Kind: Event_RESPONSE_BODY_SENT.Enum(),
						},
					},
				},
				&Call{
					Path: []uint32{1},
					Svc: &Service{
						Name: proto.String("code.justin.tv/release/trace/cmd/db"),
						Host: proto.String("trace-db.internal.justin.tv"),
						Pid:  proto.Int32(789),
					},
					Params: &RPCParams{
						Sql: &SQLParams{
							Dbname:        proto.String("prod"),
							Dbuser:        proto.String("user"),
							StrippedQuery: proto.String("select * from users"),
							Tables:        []string{"users"},
						},
					},
					ClientTimestamps: []*Timestamp{
						&Timestamp{
							Time: proto.Int64(6),
							Kind: Event_REQUEST_HEAD_PREPARED.Enum(),
						},
						&Timestamp{
							Time: proto.Int64(9),
							Kind: Event_RESPONSE_BODY_RECEIVED.Enum(),
						},
					},
					ServiceTimestamps: []*Timestamp{
						&Timestamp{
							Time: proto.Int64(7),
							Kind: Event_REQUEST_HEAD_RECEIVED.Enum(),
						},
						&Timestamp{
							Time: proto.Int64(8),
							Kind: Event_RESPONSE_BODY_SENT.Enum(),
						},
					},
				},
			},
		},
		Client: &Service{},
	}

	have := DowngradeTransaction(in)

	// To compare, make sure the marshaled representation is
	// byte-equal. Can't use reflect.DeepEqual because the pointers to
	// basic types (strings and ints) will be different, which fools
	// reflect.DeepEqual.
	haveBytes, err := proto.Marshal(have)
	if err != nil {
		t.Fatalf("proto marshal have err=%q", err)
	}

	wantBytes, err := proto.Marshal(want)
	if err != nil {
		t.Fatalf("proto marshal want err=%q", err)
	}

	if !bytes.Equal(haveBytes, wantBytes) {
		t.Errorf("Failed to downgrade transaction")
		t.Logf("Have: %s", proto.MarshalTextString(have))
		t.Logf("Want: %s", proto.MarshalTextString(want))
	}
}

// Downgrading an empty transaction shouldn't panic
func TestDowngradeEmpty(t *testing.T) {
	if panics(func() {
		var transaction = &api.Transaction{}
		DowngradeTransaction(transaction)
	}) {
		t.Error("downgrading an empty transaction shouldn't panic")
	}

	if panics(func() {
		var call = &api.Call{}
		downgradeCall(call)
	}) {
		t.Error("downgrading an empty call shouldn't panic")
	}

	if panics(func() {
		var service = &api.Service{}
		downgradeService(service)
	}) {
		t.Error("downgrading an empty service shouldn't panic")
	}

	if panics(func() {
		var timestamps = []*api.Timestamp{}
		downgradeTimestamps(timestamps)
	}) {
		t.Error("downgrading an empty timestamp slice shouldn't panic")
	}

	if panics(func() {
		var RPCParams = &api.RPCParams{}
		downgradeParams(RPCParams)
	}) {
		t.Error("downgrading an empty RPCParams shouldn't panic")
	}

	if panics(func() {
		var HTTPParams = &api.HTTPParams{}
		downgradeHTTPParams(HTTPParams)
	}) {
		t.Error("downgrading an empty HTTPParams shouldn't panic")
	}

	if panics(func() {
		var SQLParams = &api.SQLParams{}
		downgradeSQLParams(SQLParams)
	}) {
		t.Error("downgrading an empty SQLParams shouldn't panic")
	}
}

func panics(f func()) (panicked bool) {
	defer func() {
		if recover() != nil {
			panicked = true
		}
	}()
	f()
	return false
}
