// Copyright 2013 The Prometheus 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 model

import (
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	dto "github.com/prometheus/client_model/go"
	"google.golang.org/protobuf/proto"
)

func testMetric(t testing.TB) {
	scenarios := []struct {
		input           LabelSet
		fingerprint     Fingerprint
		fastFingerprint Fingerprint
	}{
		{
			input:           LabelSet{},
			fingerprint:     14695981039346656037,
			fastFingerprint: 14695981039346656037,
		},
		{
			input: LabelSet{
				"first_name":   "electro",
				"occupation":   "robot",
				"manufacturer": "westinghouse",
			},
			fingerprint:     5911716720268894962,
			fastFingerprint: 11310079640881077873,
		},
		{
			input: LabelSet{
				"x": "y",
			},
			fingerprint:     8241431561484471700,
			fastFingerprint: 13948396922932177635,
		},
		{
			input: LabelSet{
				"a": "bb",
				"b": "c",
			},
			fingerprint:     3016285359649981711,
			fastFingerprint: 3198632812309449502,
		},
		{
			input: LabelSet{
				"a":  "b",
				"bb": "c",
			},
			fingerprint:     7122421792099404749,
			fastFingerprint: 5774953389407657638,
		},
	}

	for i, scenario := range scenarios {
		input := Metric(scenario.input)

		if scenario.fingerprint != input.Fingerprint() {
			t.Errorf("%d. expected %d, got %d", i, scenario.fingerprint, input.Fingerprint())
		}
		if scenario.fastFingerprint != input.FastFingerprint() {
			t.Errorf("%d. expected %d, got %d", i, scenario.fastFingerprint, input.FastFingerprint())
		}
	}
}

func TestMetric(t *testing.T) {
	testMetric(t)
}

func BenchmarkMetric(b *testing.B) {
	for i := 0; i < b.N; i++ {
		testMetric(b)
	}
}

func TestMetricNameIsLegacyValid(t *testing.T) {
	scenarios := []struct {
		mn          LabelValue
		legacyValid bool
		utf8Valid   bool
	}{
		{
			mn:          "Avalid_23name",
			legacyValid: true,
			utf8Valid:   true,
		},
		{
			mn:          "_Avalid_23name",
			legacyValid: true,
			utf8Valid:   true,
		},
		{
			mn:          "1valid_23name",
			legacyValid: false,
			utf8Valid:   true,
		},
		{
			mn:          "avalid_23name",
			legacyValid: true,
			utf8Valid:   true,
		},
		{
			mn:          "Ava:lid_23name",
			legacyValid: true,
			utf8Valid:   true,
		},
		{
			mn:          "a lid_23name",
			legacyValid: false,
			utf8Valid:   true,
		},
		{
			mn:          ":leading_colon",
			legacyValid: true,
			utf8Valid:   true,
		},
		{
			mn:          "colon:in:the:middle",
			legacyValid: true,
			utf8Valid:   true,
		},
		{
			mn:          "",
			legacyValid: false,
			utf8Valid:   false,
		},
		{
			mn:          "a\xc5z",
			legacyValid: false,
			utf8Valid:   false,
		},
	}

	for _, s := range scenarios {
		NameValidationScheme = LegacyValidation
		if IsValidMetricName(s.mn) != s.legacyValid {
			t.Errorf("Expected %v for %q using legacy IsValidMetricName method", s.legacyValid, s.mn)
		}
		if MetricNameRE.MatchString(string(s.mn)) != s.legacyValid {
			t.Errorf("Expected %v for %q using regexp matching", s.legacyValid, s.mn)
		}
		NameValidationScheme = UTF8Validation
		if IsValidMetricName(s.mn) != s.utf8Valid {
			t.Errorf("Expected %v for %q using utf-8 IsValidMetricName method", s.legacyValid, s.mn)
		}
	}
}

func TestMetricClone(t *testing.T) {
	m := Metric{
		"first_name":   "electro",
		"occupation":   "robot",
		"manufacturer": "westinghouse",
	}

	m2 := m.Clone()

	if len(m) != len(m2) {
		t.Errorf("expected the length of the cloned metric to be equal to the input metric")
	}

	for ln, lv := range m2 {
		expected := m[ln]
		if expected != lv {
			t.Errorf("expected label value %s but got %s for label name %s", expected, lv, ln)
		}
	}
}

func TestMetricToString(t *testing.T) {
	scenarios := []struct {
		name     string
		input    Metric
		expected string
	}{
		{
			name: "valid metric without __name__ label",
			input: Metric{
				"first_name":   "electro",
				"occupation":   "robot",
				"manufacturer": "westinghouse",
			},
			expected: `{first_name="electro", manufacturer="westinghouse", occupation="robot"}`,
		},
		{
			name: "valid metric with __name__ label",
			input: Metric{
				"__name__":     "electro",
				"occupation":   "robot",
				"manufacturer": "westinghouse",
			},
			expected: `electro{manufacturer="westinghouse", occupation="robot"}`,
		},
		{
			name: "empty metric with __name__ label",
			input: Metric{
				"__name__": "fooname",
			},
			expected: "fooname",
		},
		{
			name:     "empty metric",
			input:    Metric{},
			expected: "{}",
		},
	}

	for _, scenario := range scenarios {
		t.Run(scenario.name, func(t *testing.T) {
			actual := scenario.input.String()
			if actual != scenario.expected {
				t.Errorf("expected string output %s but got %s", scenario.expected, actual)
			}
		})
	}
}

func TestEscapeName(t *testing.T) {
	scenarios := []struct {
		name                  string
		input                 string
		expectedUnderscores   string
		expectedDots          string
		expectedUnescapedDots string
		expectedValue         string
	}{
		{
			name: "empty string",
		},
		{
			name:                "legacy valid name",
			input:               "no:escaping_required",
			expectedUnderscores: "no:escaping_required",
			// Dots escaping will escape underscores even though it's not strictly
			// necessary for compatibility.
			expectedDots:          "no:escaping__required",
			expectedUnescapedDots: "no:escaping_required",
			expectedValue:         "no:escaping_required",
		},
		{
			name:                  "name with dots",
			input:                 "mysystem.prod.west.cpu.load",
			expectedUnderscores:   "mysystem_prod_west_cpu_load",
			expectedDots:          "mysystem_dot_prod_dot_west_dot_cpu_dot_load",
			expectedUnescapedDots: "mysystem.prod.west.cpu.load",
			expectedValue:         "U__mysystem_2e_prod_2e_west_2e_cpu_2e_load",
		},
		{
			name:                  "name with dots and underscore",
			input:                 "mysystem.prod.west.cpu.load_total",
			expectedUnderscores:   "mysystem_prod_west_cpu_load_total",
			expectedDots:          "mysystem_dot_prod_dot_west_dot_cpu_dot_load__total",
			expectedUnescapedDots: "mysystem.prod.west.cpu.load_total",
			expectedValue:         "U__mysystem_2e_prod_2e_west_2e_cpu_2e_load__total",
		},
		{
			name:                  "name with dots and colon",
			input:                 "http.status:sum",
			expectedUnderscores:   "http_status:sum",
			expectedDots:          "http_dot_status:sum",
			expectedUnescapedDots: "http.status:sum",
			expectedValue:         "U__http_2e_status:sum",
		},
		{
			name:                  "name with spaces and emoji",
			input:                 "label with 😱",
			expectedUnderscores:   "label_with__",
			expectedDots:          "label__with____",
			expectedUnescapedDots: "label_with__",
			expectedValue:         "U__label_20_with_20__1f631_",
		},
		{
			name:                "name with unicode characters > 0x100",
			input:               "花火",
			expectedUnderscores: "__",
			expectedDots:        "____",
			// Dots-replacement does not know the difference between two replaced
			// characters and a single underscore.
			expectedUnescapedDots: "__",
			expectedValue:         "U___82b1__706b_",
		},
		{
			name:                  "name with spaces and edge-case value",
			input:                 "label with \u0100",
			expectedUnderscores:   "label_with__",
			expectedDots:          "label__with____",
			expectedUnescapedDots: "label_with__",
			expectedValue:         "U__label_20_with_20__100_",
		},
	}

	for _, scenario := range scenarios {
		t.Run(scenario.name, func(t *testing.T) {
			got := EscapeName(scenario.input, UnderscoreEscaping)
			if got != scenario.expectedUnderscores {
				t.Errorf("expected string output %s but got %s", scenario.expectedUnderscores, got)
			}
			// Unescaping with the underscore method is a noop.
			got = UnescapeName(got, UnderscoreEscaping)
			if got != scenario.expectedUnderscores {
				t.Errorf("expected unescaped string output %s but got %s", scenario.expectedUnderscores, got)
			}

			got = EscapeName(scenario.input, DotsEscaping)
			if got != scenario.expectedDots {
				t.Errorf("expected string output %s but got %s", scenario.expectedDots, got)
			}
			got = UnescapeName(got, DotsEscaping)
			if got != scenario.expectedUnescapedDots {
				t.Errorf("expected unescaped string output %s but got %s", scenario.expectedUnescapedDots, got)
			}

			got = EscapeName(scenario.input, ValueEncodingEscaping)
			if got != scenario.expectedValue {
				t.Errorf("expected string output %s but got %s", scenario.expectedValue, got)
			}
			// Unescaped result should always be identical to the original input.
			got = UnescapeName(got, ValueEncodingEscaping)
			if got != scenario.input {
				t.Errorf("expected unescaped string output %s but got %s", scenario.input, got)
			}
		})
	}
}

func TestValueUnescapeErrors(t *testing.T) {
	scenarios := []struct {
		name     string
		input    string
		expected string
	}{
		{
			name: "empty string",
		},
		{
			name:     "basic case, no error",
			input:    "U__no:unescapingrequired",
			expected: "no:unescapingrequired",
		},
		{
			name:     "capitals ok, no error",
			input:    "U__capitals_2E_ok",
			expected: "capitals.ok",
		},
		{
			name:     "underscores, no error",
			input:    "U__underscores__doubled__",
			expected: "underscores_doubled_",
		},
		{
			name:     "invalid single underscore",
			input:    "U__underscores_doubled_",
			expected: "U__underscores_doubled_",
		},
		{
			name:     "invalid single underscore, 2",
			input:    "U__underscores__doubled_",
			expected: "U__underscores__doubled_",
		},
		{
			name:     "giant fake utf-8 code",
			input:    "U__my__hack_2e_attempt_872348732fabdabbab_",
			expected: "U__my__hack_2e_attempt_872348732fabdabbab_",
		},
		{
			name:     "trailing utf-8",
			input:    "U__my__hack_2e",
			expected: "U__my__hack_2e",
		},
		{
			name:     "invalid utf-8 value",
			input:    "U__bad__utf_2eg_",
			expected: "U__bad__utf_2eg_",
		},
		{
			name:     "surrogate utf-8 value",
			input:    "U__bad__utf_D900_",
			expected: "U__bad__utf_D900_",
		},
	}

	for _, scenario := range scenarios {
		t.Run(scenario.name, func(t *testing.T) {
			got := UnescapeName(scenario.input, ValueEncodingEscaping)
			if got != scenario.expected {
				t.Errorf("expected unescaped string output %s but got %s", scenario.expected, got)
			}
		})
	}
}

func TestEscapeMetricFamily(t *testing.T) {
	scenarios := []struct {
		name     string
		input    *dto.MetricFamily
		scheme   EscapingScheme
		expected *dto.MetricFamily
	}{
		{
			name:     "empty",
			input:    &dto.MetricFamily{},
			scheme:   ValueEncodingEscaping,
			expected: &dto.MetricFamily{},
		},
		{
			name:   "simple, no escaping needed",
			scheme: ValueEncodingEscaping,
			input: &dto.MetricFamily{
				Name: proto.String("my_metric"),
				Help: proto.String("some help text"),
				Type: dto.MetricType_COUNTER.Enum(),
				Metric: []*dto.Metric{
					{
						Counter: &dto.Counter{
							Value: proto.Float64(34.2),
						},
						Label: []*dto.LabelPair{
							{
								Name:  proto.String("__name__"),
								Value: proto.String("my_metric"),
							},
							{
								Name:  proto.String("some_label"),
								Value: proto.String("labelvalue"),
							},
						},
					},
				},
			},
			expected: &dto.MetricFamily{
				Name: proto.String("my_metric"),
				Help: proto.String("some help text"),
				Type: dto.MetricType_COUNTER.Enum(),
				Metric: []*dto.Metric{
					{
						Counter: &dto.Counter{
							Value: proto.Float64(34.2),
						},
						Label: []*dto.LabelPair{
							{
								Name:  proto.String("__name__"),
								Value: proto.String("my_metric"),
							},
							{
								Name:  proto.String("some_label"),
								Value: proto.String("labelvalue"),
							},
						},
					},
				},
			},
		},
		{
			name:   "label name escaping needed",
			scheme: ValueEncodingEscaping,
			input: &dto.MetricFamily{
				Name: proto.String("my_metric"),
				Help: proto.String("some help text"),
				Type: dto.MetricType_COUNTER.Enum(),
				Metric: []*dto.Metric{
					{
						Counter: &dto.Counter{
							Value: proto.Float64(34.2),
						},
						Label: []*dto.LabelPair{
							{
								Name:  proto.String("__name__"),
								Value: proto.String("my_metric"),
							},
							{
								Name:  proto.String("some.label"),
								Value: proto.String("labelvalue"),
							},
						},
					},
				},
			},
			expected: &dto.MetricFamily{
				Name: proto.String("my_metric"),
				Help: proto.String("some help text"),
				Type: dto.MetricType_COUNTER.Enum(),
				Metric: []*dto.Metric{
					{
						Counter: &dto.Counter{
							Value: proto.Float64(34.2),
						},
						Label: []*dto.LabelPair{
							{
								Name:  proto.String("__name__"),
								Value: proto.String("my_metric"),
							},
							{
								Name:  proto.String("U__some_2e_label"),
								Value: proto.String("labelvalue"),
							},
						},
					},
				},
			},
		},
		{
			name:   "counter, escaping needed",
			scheme: ValueEncodingEscaping,
			input: &dto.MetricFamily{
				Name: proto.String("my.metric"),
				Help: proto.String("some help text"),
				Type: dto.MetricType_COUNTER.Enum(),
				Metric: []*dto.Metric{
					{
						Counter: &dto.Counter{
							Value: proto.Float64(34.2),
						},
						Label: []*dto.LabelPair{
							{
								Name:  proto.String("__name__"),
								Value: proto.String("my.metric"),
							},
							{
								Name:  proto.String("some?label"),
								Value: proto.String("label??value"),
							},
						},
					},
				},
			},
			expected: &dto.MetricFamily{
				Name: proto.String("U__my_2e_metric"),
				Help: proto.String("some help text"),
				Type: dto.MetricType_COUNTER.Enum(),
				Metric: []*dto.Metric{
					{
						Counter: &dto.Counter{
							Value: proto.Float64(34.2),
						},
						Label: []*dto.LabelPair{
							{
								Name:  proto.String("__name__"),
								Value: proto.String("U__my_2e_metric"),
							},
							{
								Name:  proto.String("U__some_3f_label"),
								Value: proto.String("label??value"),
							},
						},
					},
				},
			},
		},
		{
			name:   "gauge, escaping needed",
			scheme: DotsEscaping,
			input: &dto.MetricFamily{
				Name: proto.String("unicode.and.dots.花火"),
				Help: proto.String("some help text"),
				Type: dto.MetricType_GAUGE.Enum(),
				Metric: []*dto.Metric{
					{
						Gauge: &dto.Gauge{
							Value: proto.Float64(34.2),
						},
						Label: []*dto.LabelPair{
							{
								Name:  proto.String("__name__"),
								Value: proto.String("unicode.and.dots.花火"),
							},
							{
								Name:  proto.String("some_label"),
								Value: proto.String("label??value"),
							},
						},
					},
				},
			},
			expected: &dto.MetricFamily{
				Name: proto.String("unicode_dot_and_dot_dots_dot_____"),
				Help: proto.String("some help text"),
				Type: dto.MetricType_GAUGE.Enum(),
				Metric: []*dto.Metric{
					{
						Gauge: &dto.Gauge{
							Value: proto.Float64(34.2),
						},
						Label: []*dto.LabelPair{
							{
								Name:  proto.String("__name__"),
								Value: proto.String("unicode_dot_and_dot_dots_dot_____"),
							},
							{
								Name:  proto.String("some_label"),
								Value: proto.String("label??value"),
							},
						},
					},
				},
			},
		},
	}

	unexportList := []interface{}{dto.MetricFamily{}, dto.Metric{}, dto.LabelPair{}, dto.Counter{}, dto.Gauge{}}

	for _, scenario := range scenarios {
		t.Run(scenario.name, func(t *testing.T) {
			original := proto.Clone(scenario.input)
			got := EscapeMetricFamily(scenario.input, scenario.scheme)
			if !cmp.Equal(scenario.expected, got, cmpopts.IgnoreUnexported(unexportList...)) {
				t.Errorf("unexpected difference in escaped output:\n%s", cmp.Diff(scenario.expected, got, cmpopts.IgnoreUnexported(unexportList...)))
			}
			if !cmp.Equal(scenario.input, original, cmpopts.IgnoreUnexported(unexportList...)) {
				t.Errorf("input was mutated during escaping:\n%s", cmp.Diff(scenario.expected, got, cmpopts.IgnoreUnexported(unexportList...)))
			}
		})
	}
}

// TestProtoFormatUnchanged checks to see if the proto format changed, in which
// case EscapeMetricFamily will need to be updated.
func TestProtoFormatUnchanged(t *testing.T) {
	scenarios := []struct {
		name         string
		input        proto.Message
		expectFields []string
	}{
		{
			name:         "MetricFamily",
			input:        &dto.MetricFamily{},
			expectFields: []string{"name", "help", "type", "metric", "unit"},
		},
		{
			name:         "Metric",
			input:        &dto.Metric{},
			expectFields: []string{"label", "gauge", "counter", "summary", "untyped", "histogram", "timestamp_ms"},
		},
		{
			name:         "LabelPair",
			input:        &dto.LabelPair{},
			expectFields: []string{"name", "value"},
		},
	}

	for _, scenario := range scenarios {
		t.Run(scenario.name, func(t *testing.T) {
			desc := scenario.input.ProtoReflect().Descriptor()
			fields := desc.Fields()
			if fields.Len() != len(scenario.expectFields) {
				t.Errorf("dto.MetricFamily changed length, expected %d, got %d", len(scenario.expectFields), fields.Len())
			}

			for i := 0; i < fields.Len(); i++ {
				got := fields.Get(i).TextName()
				if got != scenario.expectFields[i] {
					t.Errorf("dto.MetricFamily field mismatch, expected %s got %s", scenario.expectFields[i], got)
				}
			}
		})
	}
}
