// 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 (
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	dto "github.com/prometheus/client_model/go"
	"github.com/stretchr/testify/require"
	"go.yaml.in/yaml/v2"
	"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 TestValidationScheme(t *testing.T) {
	var scheme ValidationScheme
	require.Equal(t, UnsetValidation, scheme)
}

func TestValidationScheme_String(t *testing.T) {
	for _, tc := range []struct {
		name   string
		scheme ValidationScheme
		want   string
	}{
		{
			name:   "Unset",
			scheme: UnsetValidation,
			want:   "unset",
		},
		{
			name:   "Legacy",
			scheme: LegacyValidation,
			want:   "legacy",
		},
		{
			name:   "UTF8",
			scheme: UTF8Validation,
			want:   "utf8",
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			require.Equal(t, tc.want, tc.scheme.String())
		})
	}
}

func TestValidationScheme_MarshalYAML(t *testing.T) {
	for _, tc := range []struct {
		name   string
		scheme ValidationScheme
		want   string
	}{
		{
			name:   "Unset",
			scheme: UnsetValidation,
			want:   `""`,
		},
		{
			name:   "Legacy",
			scheme: LegacyValidation,
			want:   "legacy",
		},
		{
			name:   "UTF8",
			scheme: UTF8Validation,
			want:   "utf8",
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			marshaled, err := yaml.Marshal(tc.scheme)
			require.NoError(t, err)
			require.Equal(t, tc.want, strings.TrimSpace(string(marshaled)))
		})
	}
}

func TestValidationScheme_UnmarshalYAML(t *testing.T) {
	for _, tc := range []struct {
		name      string
		input     string
		want      ValidationScheme
		wantError error
	}{
		{
			name:  "Unset empty input",
			input: "",
			want:  UnsetValidation,
		},
		{
			name:  "Unset quoted input",
			input: `""`,
			want:  UnsetValidation,
		},
		{
			name:  "Legacy",
			input: "legacy",
			want:  LegacyValidation,
		},
		{
			name:  "UTF8",
			input: "utf8",
			want:  UTF8Validation,
		},
		{
			name:      "Invalid",
			input:     "invalid",
			wantError: errors.New(`unrecognized ValidationScheme: "invalid"`),
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			scheme := UnsetValidation
			err := yaml.Unmarshal([]byte(tc.input), &scheme)
			if tc.wantError == nil {
				require.NoError(t, err)
				require.Equal(t, tc.want, scheme)
			} else {
				require.EqualError(t, err, tc.wantError.Error())
			}
		})
	}
}

func TestValidationScheme_UnmarshalJSON(t *testing.T) {
	testCases := []struct {
		name    string
		input   string
		want    ValidationScheme
		wantErr bool
	}{
		{
			name:    "invalid",
			input:   `invalid`,
			wantErr: true,
		},
		{
			name:  "empty",
			input: `""`,
			want:  UnsetValidation,
		},
		{
			name:  "legacy validation",
			input: `"legacy"`,
			want:  LegacyValidation,
		},
		{
			name:  "utf8 validation",
			input: `"utf8"`,
			want:  UTF8Validation,
		},
	}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			var got ValidationScheme
			err := json.Unmarshal([]byte(tc.input), &got)
			if tc.wantErr {
				require.Error(t, err)
				return
			}
			require.NoError(t, err)
			require.Equal(t, tc.want, got)

			output, err := json.Marshal(got)
			require.NoError(t, err)
			require.Equal(t, tc.input, string(output))
		})
	}
}

func TestValidationScheme_Set(t *testing.T) {
	testCases := []struct {
		name    string
		input   string
		want    ValidationScheme
		wantErr bool
	}{
		{
			name:    "invalid",
			input:   `invalid`,
			wantErr: true,
		},
		{
			name:  "empty",
			input: ``,
			want:  UnsetValidation,
		},
		{
			name:  "legacy validation",
			input: `legacy`,
			want:  LegacyValidation,
		},
		{
			name:  "utf8 validation",
			input: `utf8`,
			want:  UTF8Validation,
		},
	}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			var got ValidationScheme
			err := got.Set(tc.input)
			if tc.wantErr {
				require.Error(t, err)
				return
			}
			require.NoError(t, err)
			require.Equal(t, tc.want, got)
		})
	}
}

func TestValidationScheme_IsMetricNameValid(t *testing.T) {
	scenarios := []struct {
		mn          string
		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,
		},
		{
			mn:          "dot.in.name",
			legacyValid: false,
			utf8Valid:   true,
		},
	}
	for _, s := range scenarios {
		t.Run(fmt.Sprintf("%s,%t,%t", s.mn, s.legacyValid, s.utf8Valid), func(t *testing.T) {
			if LegacyValidation.IsValidMetricName(s.mn) != s.legacyValid {
				t.Errorf("Expected %v for %q using LegacyValidation.IsValidMetricName", s.legacyValid, s.mn)
			}
			if MetricNameRE.MatchString(s.mn) != s.legacyValid {
				t.Errorf("Expected %v for %q using regexp matching", s.legacyValid, s.mn)
			}
			if UTF8Validation.IsValidMetricName(s.mn) != s.utf8Valid {
				t.Errorf("Expected %v for %q using UTF8Validation.IsValidMetricName", s.utf8Valid, s.mn)
			}

			// Test deprecated functions.
			if IsValidLegacyMetricName(s.mn) != s.legacyValid {
				t.Errorf("Expected %v for %q using IsValidLegacyMetricNames", s.legacyValid, s.mn)
			}
			origScheme := NameValidationScheme
			t.Cleanup(func() {
				NameValidationScheme = origScheme
			})
			NameValidationScheme = LegacyValidation
			if IsValidMetricName(LabelValue(s.mn)) != s.legacyValid {
				t.Errorf("Expected %v for %q using legacy IsValidMetricName", s.legacyValid, s.mn)
			}
			NameValidationScheme = UTF8Validation
			if IsValidMetricName(LabelValue(s.mn)) != s.utf8Valid {
				t.Errorf("Expected %v for %q using utf-8 IsValidMetricName method", s.utf8Valid, 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)
				}
			}
		})
	}
}
