package metrics

import (
	"context"
	"testing"

	"go.opencensus.io/plugin/ocgrpc"
	"go.opencensus.io/stats/view"
	"google.golang.org/grpc"
	"google.golang.org/grpc/stats"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/wrapperspb"
)

var statsHandler = &ocgrpc.ServerHandler{}

type testInvoker struct {
	invokeResult error
	statsHandler stats.Handler
}

func (t testInvoker) UnaryInvoke(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, opts ...grpc.CallOption) error {
	r := reply.(*wrapperspb.StringValue)
	r.Value = "hello"

	ctx = t.statsHandler.TagRPC(ctx, &stats.RPCTagInfo{FullMethodName: method})
	t.statsHandler.HandleRPC(ctx, &stats.InPayload{Client: true, Length: len(r.Value)})
	t.statsHandler.HandleRPC(ctx, &stats.OutPayload{Client: true, Length: len(r.Value)})
	t.statsHandler.HandleRPC(ctx, &stats.End{Client: true, Error: t.invokeResult})

	return t.invokeResult
}

func newTestCC(t *testing.T) *grpc.ClientConn {
	testCC, err := grpc.Dial("dns:localhost:9999", grpc.WithInsecure())
	if err != nil {
		t.Fatalf("Failed to create testCC: %s", err)
	}
	return testCC
}

func Test_GRPCClientInterceptor(t *testing.T) {
	interceptor := GRPCClientInterceptor("test_service")

	tests := []struct {
		name                          string
		method                        string
		errorCode                     error
		wantgrpcClientResponseSize    string
		wantgrpcClientRequestDuration string
		wantgrpcClientRequestCount    string
		wantgrpcClientRequestSize     string
	}{
		{
			name:                          "ok authorize",
			method:                        "/authorize.Authorizer/Authorize",
			errorCode:                     nil,
			wantgrpcClientResponseSize:    "{ { {grpc_client_status OK}{grpc_method Authorize}{grpc_service authorize.Authorizer}{host dns:localhost:9999}{service test_service} }&{1 5 5 5 0 [0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0]",
			wantgrpcClientRequestDuration: "{ { {grpc_client_status OK}{grpc_method Authorize}{grpc_service authorize.Authorizer}{host dns:localhost:9999}{service test_service} }",
			wantgrpcClientRequestCount:    "{ { {grpc_client_status OK}{grpc_method Authorize}{grpc_service authorize.Authorizer}{host dns:localhost:9999}{service test_service} }",
			wantgrpcClientRequestSize:     "{ { {grpc_client_status OK}{grpc_method Authorize}{grpc_service authorize.Authorizer}{host dns:localhost:9999}{service test_service} }&{1 5 5 5 0 [0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0]",
		},
		{
			name:                          "unknown validate",
			method:                        "/authenticate.Authenticator/Validate",
			errorCode:                     status.Error(14, ""),
			wantgrpcClientResponseSize:    "{ { {grpc_client_status UNAVAILABLE}{grpc_method Validate}{grpc_service authenticate.Authenticator}{host dns:localhost:9999}{service test_service} }&{1 5 5 5 0 [0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0]",
			wantgrpcClientRequestDuration: "{ { {grpc_client_status UNAVAILABLE}{grpc_method Validate}{grpc_service authenticate.Authenticator}{host dns:localhost:9999}{service test_service} }",
			wantgrpcClientRequestCount:    "{ { {grpc_client_status UNAVAILABLE}{grpc_method Validate}{grpc_service authenticate.Authenticator}{host dns:localhost:9999}{service test_service} }",
			wantgrpcClientRequestSize:     "{ { {grpc_client_status UNAVAILABLE}{grpc_method Validate}{grpc_service authenticate.Authenticator}{host dns:localhost:9999}{service test_service} }&{1 5 5 5 0 [0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0]",
		},
		{
			name:                          "broken method parsing",
			method:                        "f",
			errorCode:                     status.Error(14, ""),
			wantgrpcClientResponseSize:    "{ { {grpc_client_status UNAVAILABLE}{host dns:localhost:9999}{service test_service} }&{1 5 5 5 0 [0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0]",
			wantgrpcClientRequestDuration: "{ { {grpc_client_status UNAVAILABLE}{host dns:localhost:9999}{service test_service} }",
			wantgrpcClientRequestCount:    "{ { {grpc_client_status UNAVAILABLE}{host dns:localhost:9999}{service test_service} }",
			wantgrpcClientRequestSize:     "{ { {grpc_client_status UNAVAILABLE}{host dns:localhost:9999}{service test_service} }&{1 5 5 5 0 [0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0]",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			view.Unregister(GRPCClientViews...)
			view.Register(GRPCClientViews...)

			invoker := testInvoker{
				invokeResult: tt.errorCode,
				statsHandler: &ocgrpc.ClientHandler{},
			}
			var reply wrapperspb.StringValue

			interceptor(context.Background(), tt.method, nil, &reply, newTestCC(t), invoker.UnaryInvoke)

			testDataRetrieval(GRPCClientResponseSizeView, t, tt.wantgrpcClientResponseSize)
			testDataRetrieval(GRPCClientRequestDurationView, t, tt.wantgrpcClientRequestDuration)
			testDataRetrieval(GRPCClientRequestCountView, t, tt.wantgrpcClientRequestCount)
			testDataRetrieval(GRPCClientRequestSizeView, t, tt.wantgrpcClientRequestSize)
		})
	}
}

func mockServerRPCHandle(metricsHandler *GRPCServerMetricsHandler, method string, errorCode error) {
	message := "hello"
	ctx := statsHandler.TagRPC(context.Background(), &stats.RPCTagInfo{FullMethodName: method})
	ctx = metricsHandler.TagRPC(ctx, &stats.RPCTagInfo{FullMethodName: method})

	statsHandler.HandleRPC(ctx, &stats.InPayload{Client: false, Length: len(message)})
	statsHandler.HandleRPC(ctx, &stats.OutPayload{Client: false, Length: len(message)})
	statsHandler.HandleRPC(ctx, &stats.End{Client: false, Error: errorCode})
}

func Test_GRPCServerMetricsHandler(t *testing.T) {
	tests := []struct {
		name                          string
		method                        string
		errorCode                     error
		wantgrpcServerResponseSize    string
		wantgrpcServerRequestDuration string
		wantgrpcServerRequestCount    string
		wantgrpcServerRequestSizeView string
	}{
		{
			name:                          "ok authorize",
			method:                        "/authorize.Authorizer/Authorize",
			errorCode:                     nil,
			wantgrpcServerResponseSize:    "{ { {grpc_method Authorize}{grpc_server_status OK}{grpc_service authorize.Authorizer}{service test_service} }&{1 5 5 5 0 [0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0]",
			wantgrpcServerRequestDuration: "{ { {grpc_method Authorize}{grpc_server_status OK}{grpc_service authorize.Authorizer}{service test_service} }",
			wantgrpcServerRequestCount:    "{ { {grpc_method Authorize}{grpc_server_status OK}{grpc_service authorize.Authorizer}{service test_service} }",
			wantgrpcServerRequestSizeView: "{ { {grpc_method Authorize}{grpc_server_status OK}{grpc_service authorize.Authorizer}{service test_service} }&{1 5 5 5 0 [0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0]",
		},
		{
			name:                          "unknown validate",
			method:                        "/authenticate.Authenticator/Validate",
			errorCode:                     status.Error(14, ""),
			wantgrpcServerResponseSize:    "{ { {grpc_method Validate}{grpc_server_status UNAVAILABLE}{grpc_service authenticate.Authenticator}{service test_service} }&{1 5 5 5 0 [0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0]",
			wantgrpcServerRequestDuration: "{ { {grpc_method Validate}{grpc_server_status UNAVAILABLE}{grpc_service authenticate.Authenticator}{service test_service} }",
			wantgrpcServerRequestCount:    "{ { {grpc_method Validate}{grpc_server_status UNAVAILABLE}{grpc_service authenticate.Authenticator}{service test_service} }",
			wantgrpcServerRequestSizeView: "{ { {grpc_method Validate}{grpc_server_status UNAVAILABLE}{grpc_service authenticate.Authenticator}{service test_service} }&{1 5 5 5 0 [0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0]",
		},
		{
			name:                          "broken method parsing",
			method:                        "f",
			errorCode:                     status.Error(14, ""),
			wantgrpcServerResponseSize:    "{ { {grpc_server_status UNAVAILABLE}{service test_service} }&{1 5 5 5 0 [0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0]",
			wantgrpcServerRequestDuration: "{ { {grpc_server_status UNAVAILABLE}{service test_service} }",
			wantgrpcServerRequestCount:    "{ { {grpc_server_status UNAVAILABLE}{service test_service} }",
			wantgrpcServerRequestSizeView: "{ { {grpc_server_status UNAVAILABLE}{service test_service} }&{1 5 5 5 0 [0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0]",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			view.Unregister(GRPCServerViews...)
			view.Register(GRPCServerViews...)

			metricsHandler := NewGRPCServerMetricsHandler("test_service")
			mockServerRPCHandle(metricsHandler, tt.method, tt.errorCode)

			testDataRetrieval(GRPCServerResponseSizeView, t, tt.wantgrpcServerResponseSize)
			testDataRetrieval(GRPCServerRequestDurationView, t, tt.wantgrpcServerRequestDuration)
			testDataRetrieval(GRPCServerRequestCountView, t, tt.wantgrpcServerRequestCount)
			testDataRetrieval(GRPCServerRequestSizeView, t, tt.wantgrpcServerRequestSizeView)
		})
	}
}