/*
Copyright 2019 The Knative 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 metrics

import (
	"context"
	"path"
	"testing"
	"time"

	sd "contrib.go.opencensus.io/exporter/stackdriver"
	"github.com/google/go-cmp/cmp"
	"go.opencensus.io/metric/metricdata"
	"go.opencensus.io/metric/metricexport"
	"go.opencensus.io/resource"
	"go.opencensus.io/stats"
	"go.opencensus.io/stats/view"
	"go.opencensus.io/tag"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	. "knative.dev/pkg/logging/testing"
	"knative.dev/pkg/metrics/metricskey"
)

// TODO UTs should move to eventing and serving, as appropriate.
// 	See https://github.com/knative/pkg/issues/608

var testGcpMetadata = gcpMetadata{
	project:  "test-project",
	location: "test-location",
	cluster:  "test-cluster",
}

func fakeGcpMetadataFunc() *gcpMetadata {
	// the caller of this function could modify the struct, so we need a copy if we don't want the original modified.
	newTestGCPMetadata := testGcpMetadata
	return &newTestGCPMetadata
}

type fakeExporter struct{}

func (fe *fakeExporter) ExportView(vd *view.Data) {}
func (fe *fakeExporter) Flush()                   {}

func newFakeExporter(o sd.Options) (view.Exporter, error) {
	return &fakeExporter{}, nil
}

func makeResourceLabels(kv ...string) map[string]string {
	retval := map[string]string{
		metricskey.LabelProject:       testGcpMetadata.project,
		metricskey.LabelLocation:      testGcpMetadata.location,
		metricskey.LabelClusterName:   testGcpMetadata.cluster,
		metricskey.LabelNamespaceName: testNS,
	}
	for i := 0; i+1 < len(kv); i += 2 {
		retval[kv[i]] = kv[i+1]
	}
	return retval
}

type metricExtractor struct {
	data []*metricdata.Metric
}

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

func TestSdRecordWithResources(t *testing.T) {
	testCases := []struct {
		name               string
		domain             string
		component          string
		metricName         string
		allowCustomMetrics bool
		metricTags         map[string]string
		resource           resource.Resource
		expectedLabels     map[string]string
		expectedResource   map[string]string
	}{{
		name:       "Serving resource and metric labels",
		domain:     internalServingDomain,
		component:  "activator",
		metricName: "request_count",
		metricTags: map[string]string{
			metricskey.ContainerName:          testContainer,
			metricskey.PodName:                testPod,
			metricskey.LabelResponseCodeClass: "2xx",
			metricskey.LabelResponseCode:      "200",
		},
		resource: resource.Resource{
			Labels: map[string]string{
				metricskey.LabelConfigurationName: testConfiguration,
				metricskey.LabelNamespaceName:     testNS,
				metricskey.LabelServiceName:       testService,
				metricskey.LabelRevisionName:      testRevision,
			},
		},
		expectedLabels: map[string]string{
			metricskey.ContainerName:          testContainer,
			metricskey.PodName:                testPod,
			metricskey.LabelResponseCodeClass: "2xx",
			metricskey.LabelResponseCode:      "200",
		},
		expectedResource: makeResourceLabels(metricskey.LabelServiceName, testService,
			metricskey.LabelConfigurationName, testConfiguration,
			metricskey.LabelRevisionName, testRevision),
	}, {
		name:       "Serving only resource labels",
		domain:     internalServingDomain,
		component:  "activator",
		metricName: "request_count",
		resource: resource.Resource{Labels: map[string]string{
			metricskey.LabelConfigurationName: testConfiguration,
			metricskey.LabelNamespaceName:     testNS,
			metricskey.LabelServiceName:       testService,
			metricskey.LabelRevisionName:      testRevision,
		}},
		expectedResource: makeResourceLabels(metricskey.LabelServiceName, testService,
			metricskey.LabelConfigurationName, testConfiguration,
			metricskey.LabelRevisionName, testRevision),
	}, {
		name:       "Serving resource labels overwrite metric labels",
		domain:     internalServingDomain,
		component:  "activator",
		metricName: "request_count",
		metricTags: map[string]string{
			metricskey.LabelNamespaceName: testNS,
			metricskey.LabelServiceName:   testService,
		},
		resource: resource.Resource{Labels: map[string]string{
			metricskey.LabelNamespaceName: "foo",
			metricskey.LabelServiceName:   "bar",
			metricskey.LabelRevisionName:  testRevision,
		}},
		expectedResource: makeResourceLabels(metricskey.LabelNamespaceName, "foo",
			metricskey.LabelServiceName, "bar",
			metricskey.LabelConfigurationName, metricskey.ValueUnknown,
			metricskey.LabelRevisionName, testRevision),
	}, {
		name:       "Serving only metric labels",
		domain:     internalServingDomain,
		component:  "activator",
		metricName: "request_count",
		metricTags: map[string]string{
			metricskey.LabelNamespaceName:     testNS,
			metricskey.LabelServiceName:       testService,
			metricskey.LabelRevisionName:      testRevision,
			metricskey.ContainerName:          testContainer,
			metricskey.PodName:                testPod,
			metricskey.LabelResponseCodeClass: "2xx",
			metricskey.LabelResponseCode:      "200",
		},
		expectedLabels: map[string]string{
			metricskey.ContainerName:          testContainer,
			metricskey.PodName:                testPod,
			metricskey.LabelResponseCodeClass: "2xx",
			metricskey.LabelResponseCode:      "200",
		},
		expectedResource: makeResourceLabels(metricskey.LabelServiceName, testService,
			metricskey.LabelConfigurationName, metricskey.ValueUnknown,
			metricskey.LabelRevisionName, testRevision),
	}, {
		name:               "Serving only metric labels with allowCustomMetrics",
		domain:             internalServingDomain,
		component:          "activator",
		metricName:         "request_count",
		allowCustomMetrics: true,
		metricTags: map[string]string{
			metricskey.LabelNamespaceName:     testNS,
			metricskey.LabelServiceName:       testService,
			metricskey.LabelRevisionName:      testRevision,
			metricskey.ContainerName:          testContainer,
			metricskey.PodName:                testPod,
			metricskey.LabelResponseCodeClass: "2xx",
			metricskey.LabelResponseCode:      "200",
		},
		expectedLabels: map[string]string{
			metricskey.ContainerName:          testContainer,
			metricskey.PodName:                testPod,
			metricskey.LabelResponseCodeClass: "2xx",
			metricskey.LabelResponseCode:      "200",
		},
		expectedResource: makeResourceLabels(metricskey.LabelServiceName, testService,
			metricskey.LabelConfigurationName, metricskey.ValueUnknown,
			metricskey.LabelRevisionName, testRevision),
	}, {
		name:       "Eventing broker metrics",
		domain:     internalEventingDomain,
		component:  "broker",
		metricName: "event_count",
	}, {
		name:       "Eventing trigger metrics",
		domain:     internalEventingDomain,
		component:  "trigger",
		metricName: "event_processing_latencies",
	}, {
		name:       "Eventing source metrics",
		domain:     eventingDomain,
		component:  "source",
		metricName: "event_count",
	}}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			recordFunc := sdCustomMetricsRecorder(metricsConfig{
				stackdriverMetricTypePrefix: path.Join(tc.domain, tc.component),
			}, tc.allowCustomMetrics)
			m := stats.Int64(tc.metricName, "", "1")
			v := &view.View{
				Name:    "test_" + tc.metricName,
				Measure: m,

				Aggregation: view.Count(),
			}
			for k := range tc.metricTags {
				v.TagKeys = append(v.TagKeys, tag.MustNewKey(k))
			}
			if err := RegisterResourceView(v); err != nil {
				t.Error("Unable to register view:", err)
			}
			defer UnregisterResourceView(v)

			ctx := context.Background()
			ctx = metricskey.WithResource(ctx, tc.resource)
			tags := make([]tag.Mutator, 0, len(tc.metricTags))
			for k, v := range tc.metricTags {
				tags = append(tags, tag.Upsert(tag.MustNewKey(k), v))
			}
			ctx, err := tag.New(ctx, tags...)
			if err != nil {
				t.Error("Unable to set tags:", err)
			}

			if err := recordFunc(ctx, []stats.Measurement{m.M(1)}); err != nil {
				t.Errorf("Record %q failed: %v", tc.metricName, err)
			}

			// We need to sleep for a moment because stats.Record happens on a
			// background thread, and ReadAndExport happens on the local thread.
			// (This is probably an opencensus bug!)
			time.Sleep(1 * time.Millisecond)

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

			if len(me.data) != 1 {
				t.Fatalf("Expected exactly one metric: %+v", me.data)
			}
			if len(me.data[0].TimeSeries) != 1 {
				t.Errorf("Expected exactly one row: %+v", me.data[0].TimeSeries)
			}

			if tc.expectedResource != nil {
				if diff := cmp.Diff(tc.expectedResource, me.data[0].Resource.Labels); diff != "" {
					t.Errorf("Wrong resource for %s (-want +got):\n%s", tc.name, diff)
				}
			}

			if tc.expectedLabels != nil {
				labels := make(map[string]string, len(me.data[0].Descriptor.LabelKeys))
				for i, k := range me.data[0].Descriptor.LabelKeys {
					if me.data[0].TimeSeries[0].LabelValues[i].Present {
						labels[k.Key] = me.data[0].TimeSeries[0].LabelValues[i].Value
					}
				}
				if diff := cmp.Diff(tc.expectedLabels, labels); diff != "" {
					t.Errorf("Wrong labels for %s (-want + got):\n%s\n\n%+v", tc.name, diff, me.data[0].Resource.Labels)
				}
			}
		})
	}
}

func TestGetMetricPrefixFunc_UseKnativeDomain(t *testing.T) {
	testCases := []struct {
		name       string
		domain     string
		component  string
		metricName string
	}{{
		name:       "both resource and metric labels",
		domain:     internalServingDomain,
		component:  "activator",
		metricName: "request_count",
	}, {
		name:       "only resource labels",
		domain:     internalServingDomain,
		component:  "activator",
		metricName: "request_count",
	}, {
		name:       "resource labels overwrite metric labels",
		domain:     internalServingDomain,
		component:  "activator",
		metricName: "request_count",
	}, {
		name:       "only metric labels",
		domain:     internalServingDomain,
		component:  "activator",
		metricName: "request_count",
	}}
	for _, testCase := range testCases {
		t.Run(testCase.name, func(t *testing.T) {
			knativePrefix := path.Join(testCase.domain, testCase.component)
			customPrefix := path.Join(defaultCustomMetricSubDomain, testCase.component)
			mpf := getMetricPrefixFunc(knativePrefix, customPrefix)

			if got, want := mpf(testCase.metricName), knativePrefix; got != want {
				t.Fatalf("getMetricPrefixFunc=%v, want %v", got, want)
			}
		})
	}
}

func TestGetMetricPrefixFunc_UseCustomDomain(t *testing.T) {
	testCases := []struct {
		name       string
		domain     string
		component  string
		metricName string
	}{{
		name:       "unsupported domain",
		domain:     "unsupported",
		component:  "activator",
		metricName: "request_count",
	}, {
		name:       "unsupported component",
		domain:     servingDomain,
		component:  "unsupported",
		metricName: "request_count",
	}, {
		name:       "unsupported metric",
		domain:     servingDomain,
		component:  "activator",
		metricName: "unsupported",
	}, {
		name:       "unsupported component",
		domain:     internalEventingDomain,
		component:  "unsupported",
		metricName: "event_count",
	}, {
		name:       "unsupported metric",
		domain:     internalEventingDomain,
		component:  "broker",
		metricName: "unsupported",
	}}
	for _, testCase := range testCases {
		t.Run(testCase.name, func(t *testing.T) {
			knativePrefix := path.Join(testCase.domain, testCase.component)
			customPrefix := path.Join(defaultCustomMetricSubDomain, testCase.component)
			mpf := getMetricPrefixFunc(knativePrefix, customPrefix)

			if got, want := mpf(testCase.metricName), customPrefix; got != want {
				t.Fatalf("getMetricPrefixFunc=%v, want %v", got, want)
			}
		})
	}
}

func TestNewStackdriverExporterWithMetadata(t *testing.T) {
	tests := []struct {
		name          string
		config        *metricsConfig
		expectSuccess bool
	}{{
		name: "standardCase",
		config: &metricsConfig{
			domain:             servingDomain,
			component:          "autoscaler",
			backendDestination: stackdriver,
			stackdriverClientConfig: StackdriverClientConfig{
				ProjectID: testProj,
			},
		},
		expectSuccess: true,
	}, {
		name: "stackdriverClientConfigOnly",
		config: &metricsConfig{
			stackdriverClientConfig: StackdriverClientConfig{
				ProjectID:   "project",
				GCPLocation: "us-west1",
				ClusterName: "cluster",
				UseSecret:   true,
			},
		},
		expectSuccess: true,
	}, {
		name: "fullValidConfig",
		config: &metricsConfig{
			domain:                            servingDomain,
			component:                         testComponent,
			backendDestination:                stackdriver,
			reportingPeriod:                   60 * time.Second,
			isStackdriverBackend:              true,
			stackdriverMetricTypePrefix:       path.Join(servingDomain, testComponent),
			stackdriverCustomMetricTypePrefix: path.Join(customMetricTypePrefix, defaultCustomMetricSubDomain, testComponent),
			stackdriverClientConfig: StackdriverClientConfig{
				ProjectID:   "project",
				GCPLocation: "us-west1",
				ClusterName: "cluster",
				UseSecret:   true,
			},
		},
		expectSuccess: true,
	}, {
		name: "invalidStackdriverGcpLocation",
		config: &metricsConfig{
			domain:                            servingDomain,
			component:                         testComponent,
			backendDestination:                stackdriver,
			reportingPeriod:                   60 * time.Second,
			isStackdriverBackend:              true,
			stackdriverMetricTypePrefix:       path.Join(servingDomain, testComponent),
			stackdriverCustomMetricTypePrefix: path.Join(customMetricTypePrefix, defaultCustomMetricSubDomain, testComponent),
			stackdriverClientConfig: StackdriverClientConfig{
				ProjectID:   "project",
				GCPLocation: "narnia",
				ClusterName: "cluster",
				UseSecret:   true,
			},
		},
		expectSuccess: true,
	}, {
		name: "missingProjectID",
		config: &metricsConfig{
			domain:                            servingDomain,
			component:                         testComponent,
			backendDestination:                stackdriver,
			reportingPeriod:                   60 * time.Second,
			isStackdriverBackend:              true,
			stackdriverMetricTypePrefix:       path.Join(servingDomain, testComponent),
			stackdriverCustomMetricTypePrefix: path.Join(customMetricTypePrefix, defaultCustomMetricSubDomain, testComponent),
			stackdriverClientConfig: StackdriverClientConfig{
				GCPLocation: "narnia",
				ClusterName: "cluster",
				UseSecret:   true,
			},
		},
		expectSuccess: true,
	}, {
		name: "partialStackdriverConfig",
		config: &metricsConfig{
			domain:                            servingDomain,
			component:                         testComponent,
			backendDestination:                stackdriver,
			reportingPeriod:                   60 * time.Second,
			isStackdriverBackend:              true,
			stackdriverMetricTypePrefix:       path.Join(servingDomain, testComponent),
			stackdriverCustomMetricTypePrefix: path.Join(customMetricTypePrefix, defaultCustomMetricSubDomain, testComponent),
			stackdriverClientConfig: StackdriverClientConfig{
				ProjectID: "project",
			},
		},
		expectSuccess: true,
	}}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			e, _, err := newStackdriverExporter(test.config, TestLogger(t))

			succeeded := e != nil && err == nil
			if test.expectSuccess != succeeded {
				t.Errorf("Unexpected test result. Expected success? [%v]. Error: [%v]", test.expectSuccess, err)
			}
		})
	}
}

func TestEnsureKubeClient(t *testing.T) {
	// Even though ensureKubeclient uses sync.Once, make sure if the first run failed, it returns an error on subsequent calls.
	for i := 0; i < 3; i++ {
		err := ensureKubeclient()
		if err == nil {
			t.Error("Expected ensureKubeclient to fail due to not being in a Kubernetes cluster. Did the function run?")
		}
	}
}

func assertStringsEqual(t *testing.T, description string, expected string, actual string) {
	if expected != actual {
		t.Errorf("Expected %v to be set correctly. Want [%v], Got [%v]", description, expected, actual)
	}
}

func TestSetStackdriverSecretLocation(t *testing.T) {
	// Prevent pollution from other tests
	useStackdriverSecretEnabled = false
	// Reset global state after test
	defer func() {
		secretName = StackdriverSecretNameDefault
		secretNamespace = StackdriverSecretNamespaceDefault
		useStackdriverSecretEnabled = false
	}()

	const testName, testNamespace = "test-name", "test-namespace"
	secretFetcher := func(name string) (*corev1.Secret, error) {
		return &corev1.Secret{
			ObjectMeta: metav1.ObjectMeta{
				Name:      testName,
				Namespace: testNamespace,
			},
		}, nil
	}

	ctx := context.Background()

	// Sanity checks
	assertStringsEqual(t, "DefaultSecretName", secretName, StackdriverSecretNameDefault)
	assertStringsEqual(t, "DefaultSecretNamespace", secretNamespace, StackdriverSecretNamespaceDefault)
	sec, err := getStackdriverSecret(ctx, secretFetcher)
	if err != nil {
		t.Error("Got unexpected error when getting secret:", err)
	}
	if sec != nil {
		t.Errorf("Stackdriver secret should not be fetched unless SetStackdriverSecretLocation has been called")
	}

	// Once SetStackdriverSecretLocation has been called, attempts to get the secret should complete.
	SetStackdriverSecretLocation(testName, testNamespace)
	sec, err = getStackdriverSecret(ctx, secretFetcher)
	if err != nil {
		t.Error("Got unexpected error when getting secret:", err)
	}
	if sec == nil {
		t.Error("expected secret to be non-nil if there is no error and SetStackdriverSecretLocation has been called")
	}
	assertStringsEqual(t, "secretName", secretName, testName)
	assertStringsEqual(t, "secretNamespace", secretNamespace, testNamespace)
}
