// Copyright 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.
// Provenance-includes-location: https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/debbf30360b8d3a0ded8db09c4419d2a9c99b94a/pkg/translator/prometheusremotewrite/helper_test.go
// Provenance-includes-license: Apache-2.0
// Provenance-includes-copyright: Copyright The OpenTelemetry Authors.

package prometheusremotewrite

import (
	"context"
	"slices"
	"strings"
	"testing"
	"time"

	"github.com/prometheus/common/model"
	"github.com/prometheus/otlptranslator"
	"github.com/stretchr/testify/require"
	"go.opentelemetry.io/collector/pdata/pcommon"
	"go.opentelemetry.io/collector/pdata/pmetric"

	"github.com/prometheus/prometheus/config"
	"github.com/prometheus/prometheus/model/exemplar"
	"github.com/prometheus/prometheus/model/labels"
	"github.com/prometheus/prometheus/model/metadata"
	"github.com/prometheus/prometheus/prompb"
	"github.com/prometheus/prometheus/storage"
	"github.com/prometheus/prometheus/util/teststorage"
	"github.com/prometheus/prometheus/util/testutil"
)

type sample = teststorage.Sample

func TestPrometheusConverter_createAttributes(t *testing.T) {
	resourceAttrs := map[string]string{
		"service.name":        "service name",
		"service.instance.id": "service ID",
		"existent-attr":       "resource value",
		// This one is for testing conflict with metric attribute.
		"metric-attr": "resource value",
		// This one is for testing conflict with auto-generated job attribute.
		"job": "resource value",
		// This one is for testing conflict with auto-generated instance attribute.
		"instance": "resource value",
	}
	scopeAttrs := pcommon.NewMap()
	scopeAttrs.FromRaw(map[string]any{
		"attr1": "value1",
		"attr2": "value2",
	})
	defaultScope := scope{
		name:       "test-scope",
		version:    "1.0.0",
		schemaURL:  "https://schema.com",
		attributes: scopeAttrs,
	}

	resource := pcommon.NewResource()
	for k, v := range resourceAttrs {
		resource.Attributes().PutStr(k, v)
	}
	attrs := pcommon.NewMap()
	attrs.PutStr("metric-attr", "metric value")
	attrs.PutStr("metric-attr-other", "metric value other")

	// Setup resources with underscores for sanitization tests
	resourceAttrsWithUnderscores := map[string]string{
		"service.name":        "service name",
		"service.instance.id": "service ID",
		"_private":            "private value",
		"__reserved__":        "reserved value",
		"label___multi":       "multi value",
	}
	resourceWithUnderscores := pcommon.NewResource()
	for k, v := range resourceAttrsWithUnderscores {
		resourceWithUnderscores.Attributes().PutStr(k, v)
	}
	attrsWithUnderscores := pcommon.NewMap()
	attrsWithUnderscores.PutStr("_metric_private", "private metric")
	attrsWithUnderscores.PutStr("metric___multi", "multi metric")

	testCases := []struct {
		name                                 string
		resource                             pcommon.Resource
		attrs                                pcommon.Map
		scope                                scope
		promoteAllResourceAttributes         bool
		promoteResourceAttributes            []string
		promoteScope                         bool
		ignoreResourceAttributes             []string
		ignoreAttrs                          []string
		labelNameUnderscoreSanitization      bool
		labelNamePreserveMultipleUnderscores bool
		expectedLabels                       labels.Labels
	}{
		{
			name:                      "Successful conversion without resource attribute promotion and without scope promotion",
			scope:                     defaultScope,
			promoteScope:              false,
			promoteResourceAttributes: nil,
			expectedLabels: labels.FromStrings(
				"__name__", "test_metric",
				"instance", "service ID",
				"job", "service name",
				"metric_attr", "metric value",
				"metric_attr_other", "metric value other",
			),
		},
		{
			name:                      "Successful conversion without resource attribute promotion and with scope promotion",
			scope:                     defaultScope,
			promoteScope:              true,
			promoteResourceAttributes: nil,
			expectedLabels: labels.FromStrings(
				"__name__", "test_metric",
				"instance", "service ID",
				"job", "service name",
				"metric_attr", "metric value",
				"metric_attr_other", "metric value other",
				"otel_scope_name", defaultScope.name,
				"otel_scope_schema_url", defaultScope.schemaURL,
				"otel_scope_version", defaultScope.version,
				"otel_scope_attr1", "value1",
				"otel_scope_attr2", "value2",
			),
		},
		{
			name:                      "Successful conversion without resource attribute promotion and with scope promotion, but without scope",
			scope:                     scope{},
			promoteResourceAttributes: nil,
			promoteScope:              true,
			expectedLabels: labels.FromStrings(
				"__name__", "test_metric",
				"instance", "service ID",
				"job", "service name",
				"metric_attr", "metric value",
				"metric_attr_other", "metric value other",
			),
		},
		{
			name:                      "Successful conversion with some attributes ignored",
			promoteResourceAttributes: nil,
			ignoreAttrs:               []string{"metric-attr-other"},
			expectedLabels: labels.FromStrings(
				"__name__", "test_metric",
				"instance", "service ID",
				"job", "service name",
				"metric_attr", "metric value",
			),
		},
		{
			name:                      "Successful conversion with some attributes ignored and with scope promotion",
			scope:                     defaultScope,
			promoteScope:              true,
			promoteResourceAttributes: nil,
			ignoreAttrs:               []string{"metric-attr-other"},
			expectedLabels: labels.FromStrings(
				"__name__", "test_metric",
				"instance", "service ID",
				"job", "service name",
				"metric_attr", "metric value",
				"otel_scope_name", defaultScope.name,
				"otel_scope_schema_url", defaultScope.schemaURL,
				"otel_scope_version", defaultScope.version,
				"otel_scope_attr1", "value1",
				"otel_scope_attr2", "value2",
			),
		},
		{
			name:                      "Successful conversion with resource attribute promotion and with scope promotion",
			scope:                     defaultScope,
			promoteResourceAttributes: []string{"non-existent-attr", "existent-attr"},
			promoteScope:              true,
			expectedLabels: labels.FromStrings(
				"__name__", "test_metric",
				"instance", "service ID",
				"job", "service name",
				"metric_attr", "metric value",
				"metric_attr_other", "metric value other",
				"existent_attr", "resource value",
				"otel_scope_name", defaultScope.name,
				"otel_scope_schema_url", defaultScope.schemaURL,
				"otel_scope_version", defaultScope.version,
				"otel_scope_attr1", "value1",
				"otel_scope_attr2", "value2",
			),
		},
		{
			name:                      "Successful conversion with resource attribute promotion and with scope promotion, conflicting resource attributes are ignored",
			scope:                     defaultScope,
			promoteScope:              true,
			promoteResourceAttributes: []string{"non-existent-attr", "existent-attr", "metric-attr", "job", "instance"},
			expectedLabels: labels.FromStrings(
				"__name__", "test_metric",
				"instance", "service ID",
				"job", "service name",
				"existent_attr", "resource value",
				"metric_attr", "metric value",
				"metric_attr_other", "metric value other",
				"otel_scope_name", defaultScope.name,
				"otel_scope_schema_url", defaultScope.schemaURL,
				"otel_scope_version", defaultScope.version,
				"otel_scope_attr1", "value1",
				"otel_scope_attr2", "value2",
			),
		},
		{
			name:                      "Successful conversion with resource attribute promotion and with scope promotion, attributes are only promoted once",
			scope:                     defaultScope,
			promoteScope:              true,
			promoteResourceAttributes: []string{"existent-attr", "existent-attr"},
			expectedLabels: labels.FromStrings(
				"__name__", "test_metric",
				"instance", "service ID",
				"job", "service name",
				"existent_attr", "resource value",
				"metric_attr", "metric value",
				"metric_attr_other", "metric value other",
				"otel_scope_name", defaultScope.name,
				"otel_scope_schema_url", defaultScope.schemaURL,
				"otel_scope_version", defaultScope.version,
				"otel_scope_attr1", "value1",
				"otel_scope_attr2", "value2",
			),
		},
		{
			name:                         "Successful conversion promoting all resource attributes and with scope promotion",
			scope:                        defaultScope,
			promoteAllResourceAttributes: true,
			promoteScope:                 true,
			expectedLabels: labels.FromStrings(
				"__name__", "test_metric",
				"instance", "service ID",
				"job", "service name",
				"existent_attr", "resource value",
				"metric_attr", "metric value",
				"metric_attr_other", "metric value other",
				"service_name", "service name",
				"service_instance_id", "service ID",
				"otel_scope_name", defaultScope.name,
				"otel_scope_schema_url", defaultScope.schemaURL,
				"otel_scope_version", defaultScope.version,
				"otel_scope_attr1", "value1",
				"otel_scope_attr2", "value2",
			),
		},
		{
			name:                         "Successful conversion promoting all resource attributes and with scope promotion, ignoring 'service.instance.id'",
			scope:                        defaultScope,
			promoteScope:                 true,
			promoteAllResourceAttributes: true,
			ignoreResourceAttributes: []string{
				"service.instance.id",
			},
			expectedLabels: labels.FromStrings(
				"__name__", "test_metric",
				"instance", "service ID",
				"job", "service name",
				"existent_attr", "resource value",
				"metric_attr", "metric value",
				"metric_attr_other", "metric value other",
				"service_name", "service name",
				"otel_scope_name", defaultScope.name,
				"otel_scope_schema_url", defaultScope.schemaURL,
				"otel_scope_version", defaultScope.version,
				"otel_scope_attr1", "value1",
				"otel_scope_attr2", "value2",
			),
		},
		// Label sanitization test cases
		{
			name:                                 "Underscore sanitization enabled - prepends key_ to labels starting with single _",
			resource:                             resourceWithUnderscores,
			attrs:                                attrsWithUnderscores,
			promoteResourceAttributes:            []string{"_private"},
			labelNameUnderscoreSanitization:      true,
			labelNamePreserveMultipleUnderscores: true,
			expectedLabels: labels.FromStrings(
				"__name__", "test_metric",
				"instance", "service ID",
				"job", "service name",
				"key_private", "private value",
				"key_metric_private", "private metric",
				"metric___multi", "multi metric",
			),
		},
		{
			name:                                 "Underscore sanitization disabled - keeps labels with _ as-is",
			resource:                             resourceWithUnderscores,
			attrs:                                attrsWithUnderscores,
			promoteResourceAttributes:            []string{"_private"},
			labelNameUnderscoreSanitization:      false,
			labelNamePreserveMultipleUnderscores: true,
			expectedLabels: labels.FromStrings(
				"__name__", "test_metric",
				"instance", "service ID",
				"job", "service name",
				"_private", "private value",
				"_metric_private", "private metric",
				"metric___multi", "multi metric",
			),
		},
		{
			name:                                 "Multiple underscores preserved - keeps consecutive underscores",
			resource:                             resourceWithUnderscores,
			attrs:                                attrsWithUnderscores,
			promoteResourceAttributes:            []string{"label___multi"},
			labelNameUnderscoreSanitization:      false,
			labelNamePreserveMultipleUnderscores: true,
			expectedLabels: labels.FromStrings(
				"__name__", "test_metric",
				"instance", "service ID",
				"job", "service name",
				"label___multi", "multi value",
				"_metric_private", "private metric",
				"metric___multi", "multi metric",
			),
		},
		{
			name:                                 "Multiple underscores collapsed - collapses to single underscore",
			resource:                             resourceWithUnderscores,
			attrs:                                attrsWithUnderscores,
			promoteResourceAttributes:            []string{"label___multi"},
			labelNameUnderscoreSanitization:      false,
			labelNamePreserveMultipleUnderscores: false,
			expectedLabels: labels.FromStrings(
				"__name__", "test_metric",
				"instance", "service ID",
				"job", "service name",
				"label_multi", "multi value",
				"_metric_private", "private metric",
				"metric_multi", "multi metric",
			),
		},
		{
			name:                                 "Both sanitization options enabled",
			resource:                             resourceWithUnderscores,
			attrs:                                attrsWithUnderscores,
			promoteResourceAttributes:            []string{"_private", "label___multi"},
			labelNameUnderscoreSanitization:      true,
			labelNamePreserveMultipleUnderscores: true,
			expectedLabels: labels.FromStrings(
				"__name__", "test_metric",
				"instance", "service ID",
				"job", "service name",
				"key_private", "private value",
				"label___multi", "multi value",
				"key_metric_private", "private metric",
				"metric___multi", "multi metric",
			),
		},
		{
			name:                                 "Both sanitization options disabled",
			resource:                             resourceWithUnderscores,
			attrs:                                attrsWithUnderscores,
			promoteResourceAttributes:            []string{"_private", "label___multi"},
			labelNameUnderscoreSanitization:      false,
			labelNamePreserveMultipleUnderscores: false,
			expectedLabels: labels.FromStrings(
				"__name__", "test_metric",
				"instance", "service ID",
				"job", "service name",
				"_private", "private value",
				"label_multi", "multi value",
				"_metric_private", "private metric",
				"metric_multi", "multi metric",
			),
		},
		{
			name:                                 "Reserved labels (starting with __) are never modified",
			resource:                             resourceWithUnderscores,
			attrs:                                attrsWithUnderscores,
			promoteResourceAttributes:            []string{"__reserved__"},
			labelNameUnderscoreSanitization:      true,
			labelNamePreserveMultipleUnderscores: false,
			expectedLabels: labels.FromStrings(
				"__name__", "test_metric",
				"instance", "service ID",
				"job", "service name",
				"__reserved__", "reserved value",
				"key_metric_private", "private metric",
				"metric_multi", "multi metric",
			),
		},
		{
			name:                      "__name__ attribute is filtered when passed in ignoreAttrs",
			promoteResourceAttributes: nil,
			ignoreAttrs:               []string{model.MetricNameLabel},
			expectedLabels: labels.FromStrings(
				"__name__", "test_metric",
				"instance", "service ID",
				"job", "service name",
				"metric_attr", "metric value",
				"metric_attr_other", "metric value other",
			),
		},
	}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			c := NewPrometheusConverter(teststorage.NewAppendable().AppenderV2(t.Context()))
			settings := Settings{
				PromoteResourceAttributes: NewPromoteResourceAttributes(config.OTLPConfig{
					PromoteAllResourceAttributes: tc.promoteAllResourceAttributes,
					PromoteResourceAttributes:    tc.promoteResourceAttributes,
					IgnoreResourceAttributes:     tc.ignoreResourceAttributes,
				}),
				PromoteScopeMetadata:                 tc.promoteScope,
				LabelNameUnderscoreSanitization:      tc.labelNameUnderscoreSanitization,
				LabelNamePreserveMultipleUnderscores: tc.labelNamePreserveMultipleUnderscores,
			}
			// Use test case specific resource/attrs if provided, otherwise use defaults
			// Check if tc.resource is initialized (non-zero) by trying to get its attributes
			testResource := resource
			testAttrs := attrs
			// For pcommon types, we can check if they're non-zero by seeing if they have attributes
			// Since zero-initialized Resource is not valid, we use a simple heuristic:
			// if the struct has been explicitly set in the test case, use it
			if tc.resource != (pcommon.Resource{}) {
				testResource = tc.resource
			}
			if tc.attrs != (pcommon.Map{}) {
				testAttrs = tc.attrs
			}
			// Initialize resource and scope context as FromMetrics would.
			require.NoError(t, c.setResourceContext(testResource, settings))
			require.NoError(t, c.setScopeContext(tc.scope, settings))

			lbls, err := c.createAttributes(testAttrs, settings, tc.ignoreAttrs, false, metadata.Metadata{}, model.MetricNameLabel, "test_metric")
			require.NoError(t, err)

			testutil.RequireEqual(t, tc.expectedLabels, lbls)
		})
	}

	// Test that __name__ attributes in OTLP data are filtered out to prevent
	// duplicate labels.
	t.Run("__name__ attribute in OTLP data is filtered", func(t *testing.T) {
		resource := pcommon.NewResource()
		resource.Attributes().PutStr("service.name", "test-service")
		resource.Attributes().PutStr("service.instance.id", "test-instance")

		// Create attributes with __name__ to simulate problematic OTLP data.
		attrsWithNameLabel := pcommon.NewMap()
		attrsWithNameLabel.PutStr("__name__", "wrong_metric_name")
		attrsWithNameLabel.PutStr("other_attr", "value")

		c := NewPrometheusConverter(teststorage.NewAppendable().AppenderV2(t.Context()))
		settings := Settings{}

		require.NoError(t, c.setResourceContext(resource, settings))
		require.NoError(t, c.setScopeContext(scope{}, settings))

		// Call createAttributes with reservedLabelNames to filter __name__.
		lbls, err := c.createAttributes(
			attrsWithNameLabel,
			settings,
			reservedLabelNames,
			true,
			metadata.Metadata{},
			model.MetricNameLabel, "correct_metric_name",
		)
		require.NoError(t, err)

		// Verify there's exactly one __name__ label with the correct value.
		nameCount := 0
		var nameValue string
		lbls.Range(func(l labels.Label) {
			if l.Name == model.MetricNameLabel {
				nameCount++
				nameValue = l.Value
			}
		})

		require.Equal(t, 1, nameCount)
		require.Equal(t, "correct_metric_name", nameValue)
		require.Equal(t, "value", lbls.Get("other_attr"))
	})

	// Test that __type__ and __unit__ attributes in OTLP data are overwritten
	// by auto-generated labels from metadata when EnableTypeAndUnitLabels is true.
	t.Run("__type__ and __unit__ attributes are overwritten by metadata", func(t *testing.T) {
		resource := pcommon.NewResource()
		resource.Attributes().PutStr("service.name", "test-service")
		resource.Attributes().PutStr("service.instance.id", "test-instance")

		// Create attributes with __type__ and __unit__ to simulate problematic OTLP data.
		attrsWithTypeAndUnit := pcommon.NewMap()
		attrsWithTypeAndUnit.PutStr(model.MetricTypeLabel, "wrong_type")
		attrsWithTypeAndUnit.PutStr(model.MetricUnitLabel, "wrong_unit")
		attrsWithTypeAndUnit.PutStr("other_attr", "value")

		c := NewPrometheusConverter(teststorage.NewAppendable().AppenderV2(t.Context()))
		settings := Settings{EnableTypeAndUnitLabels: true}

		require.NoError(t, c.setResourceContext(resource, settings))
		require.NoError(t, c.setScopeContext(scope{}, settings))

		// Call createAttributes with Metadata containing correct Type and Unit.
		lbls, err := c.createAttributes(
			attrsWithTypeAndUnit,
			settings,
			reservedLabelNames,
			true,
			metadata.Metadata{Type: model.MetricTypeGauge, Unit: "seconds"},
			model.MetricNameLabel, "test_metric",
		)
		require.NoError(t, err)

		// Verify there's exactly one __type__ label with the correct value (from metadata).
		typeCount := 0
		var typeValue string
		lbls.Range(func(l labels.Label) {
			if l.Name == model.MetricTypeLabel {
				typeCount++
				typeValue = l.Value
			}
		})
		require.Equal(t, 1, typeCount)
		require.Equal(t, "gauge", typeValue)

		// Verify there's exactly one __unit__ label with the correct value (from metadata).
		unitCount := 0
		var unitValue string
		lbls.Range(func(l labels.Label) {
			if l.Name == model.MetricUnitLabel {
				unitCount++
				unitValue = l.Value
			}
		})
		require.Equal(t, 1, unitCount)
		require.Equal(t, "seconds", unitValue)
		require.Equal(t, "value", lbls.Get("other_attr"))
	})
}

func Test_convertTimeStamp(t *testing.T) {
	tests := []struct {
		name string
		arg  pcommon.Timestamp
		want int64
	}{
		{"zero", 0, 0},
		{"1ms", 1_000_000, 1},
		{"1s", pcommon.Timestamp(time.Unix(1, 0).UnixNano()), 1000},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := convertTimeStamp(tt.arg)
			require.Equal(t, tt.want, got)
		})
	}
}

func TestPrometheusConverter_AddSummaryDataPoints(t *testing.T) {
	scopeAttrs := pcommon.NewMap()
	scopeAttrs.FromRaw(map[string]any{
		"attr1": "value1",
		"attr2": "value2",
	})
	defaultScope := scope{
		name:       "test-scope",
		version:    "1.0.0",
		schemaURL:  "https://schema.com",
		attributes: scopeAttrs,
	}
	ts := pcommon.Timestamp(time.Now().UnixNano())
	tests := []struct {
		name         string
		metric       func() pmetric.Metric
		scope        scope
		promoteScope bool
		want         func() []sample
	}{
		{
			name: "summary with start time and without scope promotion",
			metric: func() pmetric.Metric {
				metric := pmetric.NewMetric()
				metric.SetName("test_summary")
				metric.SetEmptySummary()

				dp := metric.Summary().DataPoints().AppendEmpty()
				dp.SetTimestamp(ts)
				dp.SetStartTimestamp(ts)

				return metric
			},
			scope:        defaultScope,
			promoteScope: false,
			want: func() []sample {
				return []sample{
					{
						MF: "test_summary",
						L: labels.FromStrings(
							model.MetricNameLabel, "test_summary"+sumStr,
						),
						T:  convertTimeStamp(ts),
						ST: convertTimeStamp(ts),
						V:  0,
					},
					{
						MF: "test_summary",
						L: labels.FromStrings(
							model.MetricNameLabel, "test_summary"+countStr,
						),
						T:  convertTimeStamp(ts),
						ST: convertTimeStamp(ts),
						V:  0,
					},
				}
			},
		},
		{
			name: "summary with start time and with scope promotion",
			metric: func() pmetric.Metric {
				metric := pmetric.NewMetric()
				metric.SetName("test_summary")
				metric.SetEmptySummary()

				dp := metric.Summary().DataPoints().AppendEmpty()
				dp.SetTimestamp(ts)
				dp.SetStartTimestamp(ts)

				return metric
			},
			scope:        defaultScope,
			promoteScope: true,
			want: func() []sample {
				scopeLabels := []string{
					"otel_scope_attr1", "value1",
					"otel_scope_attr2", "value2",
					"otel_scope_name", defaultScope.name,
					"otel_scope_schema_url", defaultScope.schemaURL,
					"otel_scope_version", defaultScope.version,
				}
				return []sample{
					{
						MF: "test_summary",
						L: labels.FromStrings(append(scopeLabels,
							model.MetricNameLabel, "test_summary"+sumStr)...),
						T:  convertTimeStamp(ts),
						ST: convertTimeStamp(ts),
						V:  0,
					},
					{
						MF: "test_summary",
						L: labels.FromStrings(append(scopeLabels,
							model.MetricNameLabel, "test_summary"+countStr)...),
						T:  convertTimeStamp(ts),
						ST: convertTimeStamp(ts),
						V:  0,
					},
				}
			},
		},
		{
			name: "summary without start time and without scope promotion",
			metric: func() pmetric.Metric {
				metric := pmetric.NewMetric()
				metric.SetName("test_summary")
				metric.SetEmptySummary()

				dp := metric.Summary().DataPoints().AppendEmpty()
				dp.SetTimestamp(ts)

				return metric
			},
			scope:        defaultScope,
			promoteScope: false,
			want: func() []sample {
				return []sample{
					{
						MF: "test_summary",
						L: labels.FromStrings(
							model.MetricNameLabel, "test_summary"+sumStr,
						),
						T: convertTimeStamp(ts),
						V: 0,
					},
					{
						MF: "test_summary",
						L: labels.FromStrings(
							model.MetricNameLabel, "test_summary"+countStr,
						),
						T: convertTimeStamp(ts),
						V: 0,
					},
				}
			},
		},
		{
			name: "summary without start time and without scope promotion and some quantiles",
			metric: func() pmetric.Metric {
				metric := pmetric.NewMetric()
				metric.SetName("test_summary")
				metric.SetEmptySummary()

				dp := metric.Summary().DataPoints().AppendEmpty()
				dp.SetTimestamp(ts)
				dp.SetCount(50)
				dp.SetSum(100)
				dp.QuantileValues().EnsureCapacity(2)
				h := dp.QuantileValues().AppendEmpty()
				h.SetQuantile(0.5)
				h.SetValue(30)
				n := dp.QuantileValues().AppendEmpty()
				n.SetQuantile(0.9)
				n.SetValue(40)

				return metric
			},
			scope:        defaultScope,
			promoteScope: false,
			want: func() []sample {
				return []sample{
					{
						MF: "test_summary",
						L: labels.FromStrings(
							model.MetricNameLabel, "test_summary"+sumStr,
						),
						T: convertTimeStamp(ts),
						V: 100,
					},
					{
						MF: "test_summary",
						L: labels.FromStrings(
							model.MetricNameLabel, "test_summary"+countStr,
						),
						T: convertTimeStamp(ts),
						V: 50,
					},
					{
						MF: "test_summary",
						L: labels.FromStrings(
							model.MetricNameLabel, "test_summary",
							quantileStr, "0.5",
						),
						T: convertTimeStamp(ts),
						V: 30,
					},
					{
						MF: "test_summary",
						L: labels.FromStrings(
							model.MetricNameLabel, "test_summary",
							quantileStr, "0.9",
						),
						T: convertTimeStamp(ts),
						V: 40,
					},
				}
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			metric := tt.metric()
			appTest := teststorage.NewAppendable()
			app := appTest.AppenderV2(t.Context())
			converter := NewPrometheusConverter(app)
			settings := Settings{
				PromoteScopeMetadata: tt.promoteScope,
			}
			resource := pcommon.NewResource()

			// Initialize resource and scope context as FromMetrics would.
			require.NoError(t, converter.setResourceContext(resource, settings))
			require.NoError(t, converter.setScopeContext(tt.scope, settings))

			require.NoError(t, converter.addSummaryDataPoints(
				context.Background(),
				metric.Summary().DataPoints(),
				settings,
				storage.AOptions{
					MetricFamilyName: metric.Name(),
				},
			))
			require.NoError(t, app.Commit())
			teststorage.RequireEqual(t, tt.want(), appTest.ResultSamples())
		})
	}
}

func TestPrometheusConverter_AddHistogramDataPoints(t *testing.T) {
	scopeAttrs := pcommon.NewMap()
	scopeAttrs.FromRaw(map[string]any{
		"attr1": "value1",
		"attr2": "value2",
	})
	defaultScope := scope{
		name:       "test-scope",
		version:    "1.0.0",
		schemaURL:  "https://schema.com",
		attributes: scopeAttrs,
	}
	ts := pcommon.Timestamp(time.Now().UnixNano())
	tests := []struct {
		name         string
		metric       func() pmetric.Metric
		scope        scope
		promoteScope bool
		want         func() []sample
	}{
		{
			name: "histogram with start time and without scope promotion",
			metric: func() pmetric.Metric {
				metric := pmetric.NewMetric()
				metric.SetName("test_hist")
				metric.SetEmptyHistogram().SetAggregationTemporality(pmetric.AggregationTemporalityCumulative)

				pt := metric.Histogram().DataPoints().AppendEmpty()
				pt.SetTimestamp(ts)
				pt.SetStartTimestamp(ts)

				return metric
			},
			scope:        defaultScope,
			promoteScope: false,
			want: func() []sample {
				return []sample{
					{
						MF: "test_hist",
						L: labels.FromStrings(
							model.MetricNameLabel, "test_hist"+countStr,
						),
						T:  convertTimeStamp(ts),
						ST: convertTimeStamp(ts),
						V:  0,
					},
					{
						MF: "test_hist",
						L: labels.FromStrings(
							model.MetricNameLabel, "test_hist_bucket",
							model.BucketLabel, "+Inf",
						),
						T:  convertTimeStamp(ts),
						ST: convertTimeStamp(ts),
						V:  0,
					},
				}
			},
		},
		{
			name: "histogram with start time and with scope promotion",
			metric: func() pmetric.Metric {
				metric := pmetric.NewMetric()
				metric.SetName("test_hist")
				metric.SetEmptyHistogram().SetAggregationTemporality(pmetric.AggregationTemporalityCumulative)

				pt := metric.Histogram().DataPoints().AppendEmpty()
				pt.SetTimestamp(ts)
				pt.SetStartTimestamp(ts)

				return metric
			},
			scope:        defaultScope,
			promoteScope: true,
			want: func() []sample {
				scopeLabels := []string{
					"otel_scope_attr1", "value1",
					"otel_scope_attr2", "value2",
					"otel_scope_name", defaultScope.name,
					"otel_scope_schema_url", defaultScope.schemaURL,
					"otel_scope_version", defaultScope.version,
				}
				return []sample{
					{
						MF: "test_hist",
						L: labels.FromStrings(append(scopeLabels,
							model.MetricNameLabel, "test_hist"+countStr)...),
						T:  convertTimeStamp(ts),
						ST: convertTimeStamp(ts),
						V:  0,
					},
					{
						MF: "test_hist",
						L: labels.FromStrings(append(scopeLabels,
							model.MetricNameLabel, "test_hist_bucket",
							model.BucketLabel, "+Inf")...),
						T:  convertTimeStamp(ts),
						ST: convertTimeStamp(ts),
						V:  0,
					},
				}
			},
		},
		{
			name: "histogram without start time",
			metric: func() pmetric.Metric {
				metric := pmetric.NewMetric()
				metric.SetName("test_hist")
				metric.SetEmptyHistogram().SetAggregationTemporality(pmetric.AggregationTemporalityCumulative)

				pt := metric.Histogram().DataPoints().AppendEmpty()
				pt.SetTimestamp(ts)

				return metric
			},
			want: func() []sample {
				return []sample{
					{
						MF: "test_hist",
						L: labels.FromStrings(
							model.MetricNameLabel, "test_hist"+countStr,
						),
						T: convertTimeStamp(ts),
						V: 0,
					},
					{
						MF: "test_hist",
						L: labels.FromStrings(
							model.MetricNameLabel, "test_hist_bucket",
							model.BucketLabel, "+Inf",
						),
						T: convertTimeStamp(ts),
						V: 0,
					},
				}
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			metric := tt.metric()
			appTest := teststorage.NewAppendable()
			app := appTest.AppenderV2(t.Context())
			converter := NewPrometheusConverter(app)
			settings := Settings{
				PromoteScopeMetadata: tt.promoteScope,
			}
			resource := pcommon.NewResource()

			// Initialize resource and scope context as FromMetrics would.
			require.NoError(t, converter.setResourceContext(resource, settings))
			require.NoError(t, converter.setScopeContext(tt.scope, settings))

			require.NoError(t, converter.addHistogramDataPoints(
				context.Background(),
				metric.Histogram().DataPoints(),
				settings,
				storage.AOptions{
					MetricFamilyName: metric.Name(),
				},
			))
			require.NoError(t, app.Commit())
			teststorage.RequireEqual(t, tt.want(), appTest.ResultSamples())
		})
	}
}

// TestAddHistogramDataPoints_ExemplarLeakAcrossDataPoints verifies that
// exemplars from a previous data point don't leak into _sum/_count of the
// next data point. Regression test for stale exemplar leak.
func TestAddHistogramDataPoints_ExemplarLeakAcrossDataPoints(t *testing.T) {
	ts := pcommon.Timestamp(time.Now().UnixNano())
	exTs := pcommon.Timestamp(time.Now().Add(time.Second).UnixNano())

	metric := pmetric.NewMetric()
	metric.SetName("test_hist")
	metric.SetEmptyHistogram().SetAggregationTemporality(pmetric.AggregationTemporalityCumulative)

	// First data point: has buckets and an exemplar with value 200 (> bound 100, so falls into +Inf).
	pt1 := metric.Histogram().DataPoints().AppendEmpty()
	pt1.SetTimestamp(ts)
	pt1.SetStartTimestamp(ts)
	pt1.SetSum(42)
	pt1.SetCount(10)
	pt1.ExplicitBounds().FromRaw([]float64{100})
	pt1.BucketCounts().FromRaw([]uint64{7, 3})

	ex := pt1.Exemplars().AppendEmpty()
	ex.SetTimestamp(exTs)
	ex.SetDoubleValue(200) // > 100, so falls into the +Inf bucket.

	// Second data point: no exemplars.
	pt2 := metric.Histogram().DataPoints().AppendEmpty()
	pt2.SetTimestamp(ts)
	pt2.SetStartTimestamp(ts)
	pt2.SetSum(84)
	pt2.SetCount(20)
	pt2.ExplicitBounds().FromRaw([]float64{100})
	pt2.BucketCounts().FromRaw([]uint64{14, 6})

	appTest := teststorage.NewAppendable()
	app := appTest.AppenderV2(t.Context())
	converter := NewPrometheusConverter(app)
	settings := Settings{}
	resource := pcommon.NewResource()

	require.NoError(t, converter.setResourceContext(resource, settings))
	require.NoError(t, converter.setScopeContext(scope{}, settings))
	require.NoError(t, converter.addHistogramDataPoints(
		context.Background(),
		metric.Histogram().DataPoints(),
		settings,
		storage.AOptions{
			MetricFamilyName: metric.Name(),
		},
	))
	require.NoError(t, app.Commit())

	exConverted := exemplar.Exemplar{
		Value: 200,
		Ts:    convertTimeStamp(exTs),
		HasTs: true,
	}
	tsMs := convertTimeStamp(ts)

	want := []sample{
		// -- First data point --
		// _sum: no exemplars.
		{
			MF: "test_hist",
			L:  labels.FromStrings(model.MetricNameLabel, "test_hist_sum"),
			T:  tsMs, ST: tsMs, V: 42,
		},
		// _count: no exemplars.
		{
			MF: "test_hist",
			L:  labels.FromStrings(model.MetricNameLabel, "test_hist_count"),
			T:  tsMs, ST: tsMs, V: 10,
		},
		// le=100 bucket: no exemplars (exemplar value 200 > 100).
		{
			MF: "test_hist",
			L:  labels.FromStrings(model.MetricNameLabel, "test_hist_bucket", model.BucketLabel, "100"),
			T:  tsMs, ST: tsMs, V: 7,
		},
		// le=+Inf bucket: gets the exemplar.
		{
			MF: "test_hist",
			L:  labels.FromStrings(model.MetricNameLabel, "test_hist_bucket", model.BucketLabel, "+Inf"),
			T:  tsMs, ST: tsMs, V: 10,
			ES: []exemplar.Exemplar{exConverted},
		},
		// -- Second data point --
		// _sum: NO exemplars (this is the regression check).
		{
			MF: "test_hist",
			L:  labels.FromStrings(model.MetricNameLabel, "test_hist_sum"),
			T:  tsMs, ST: tsMs, V: 84,
		},
		// _count: NO exemplars (this is the regression check).
		{
			MF: "test_hist",
			L:  labels.FromStrings(model.MetricNameLabel, "test_hist_count"),
			T:  tsMs, ST: tsMs, V: 20,
		},
		// le=100 bucket: no exemplars.
		{
			MF: "test_hist",
			L:  labels.FromStrings(model.MetricNameLabel, "test_hist_bucket", model.BucketLabel, "100"),
			T:  tsMs, ST: tsMs, V: 14,
		},
		// le=+Inf bucket: no exemplars.
		{
			MF: "test_hist",
			L:  labels.FromStrings(model.MetricNameLabel, "test_hist_bucket", model.BucketLabel, "+Inf"),
			T:  tsMs, ST: tsMs, V: 20,
		},
	}

	teststorage.RequireEqual(t, want, appTest.ResultSamples())
}

func TestGetPromExemplars(t *testing.T) {
	ctx := context.Background()
	c := NewPrometheusConverter(teststorage.NewAppendable().AppenderV2(t.Context()))

	t.Run("Exemplars with int value", func(t *testing.T) {
		es := pmetric.NewExemplarSlice()
		exemplar := es.AppendEmpty()
		exemplar.SetTimestamp(pcommon.Timestamp(time.Now().UnixNano()))
		exemplar.SetIntValue(42)
		exemplars, err := c.getPromExemplars(ctx, es)
		require.NoError(t, err)
		require.Len(t, exemplars, 1)
		require.Equal(t, float64(42), exemplars[0].Value)
	})

	t.Run("Exemplars with double value", func(t *testing.T) {
		es := pmetric.NewExemplarSlice()
		exemplar := es.AppendEmpty()
		exemplar.SetTimestamp(pcommon.Timestamp(time.Now().UnixNano()))
		exemplar.SetDoubleValue(69.420)
		exemplars, err := c.getPromExemplars(ctx, es)
		require.NoError(t, err)
		require.Len(t, exemplars, 1)
		require.Equal(t, 69.420, exemplars[0].Value)
	})

	t.Run("Exemplars with unsupported value type", func(t *testing.T) {
		es := pmetric.NewExemplarSlice()
		exemplar := es.AppendEmpty()
		exemplar.SetTimestamp(pcommon.Timestamp(time.Now().UnixNano()))
		_, err := c.getPromExemplars(ctx, es)
		require.Error(t, err)
	})
}

func TestAddTypeAndUnitLabels(t *testing.T) {
	testCases := []struct {
		name           string
		inputLabels    []prompb.Label
		metadata       prompb.MetricMetadata
		expectedLabels []prompb.Label
	}{
		{
			name: "overwrites existing type and unit labels and preserves other labels",
			inputLabels: []prompb.Label{
				{Name: "job", Value: "test-job"},
				{Name: "__type__", Value: "old_type"},
				{Name: "instance", Value: "test-instance"},
				{Name: "__unit__", Value: "old_unit"},
				{Name: "custom_label", Value: "custom_value"},
			},
			metadata: prompb.MetricMetadata{
				Type: prompb.MetricMetadata_COUNTER,
				Unit: "seconds",
			},
			expectedLabels: []prompb.Label{
				{Name: "job", Value: "test-job"},
				{Name: "instance", Value: "test-instance"},
				{Name: "custom_label", Value: "custom_value"},
				{Name: "__type__", Value: "counter"},
				{Name: "__unit__", Value: "seconds"},
			},
		},
		{
			name: "adds type and unit labels when missing",
			inputLabels: []prompb.Label{
				{Name: "job", Value: "test-job"},
				{Name: "instance", Value: "test-instance"},
			},
			metadata: prompb.MetricMetadata{
				Type: prompb.MetricMetadata_GAUGE,
				Unit: "bytes",
			},
			expectedLabels: []prompb.Label{
				{Name: "job", Value: "test-job"},
				{Name: "instance", Value: "test-instance"},
				{Name: "__type__", Value: "gauge"},
				{Name: "__unit__", Value: "bytes"},
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			result := addTypeAndUnitLabels(tc.inputLabels, tc.metadata, Settings{AllowUTF8: false})
			require.ElementsMatch(t, tc.expectedLabels, result)
		})
	}
}

// addTypeAndUnitLabels appends type and unit labels to the given labels slice.
func addTypeAndUnitLabels(labels []prompb.Label, metadata prompb.MetricMetadata, settings Settings) []prompb.Label {
	unitNamer := otlptranslator.UnitNamer{UTF8Allowed: settings.AllowUTF8}

	labels = slices.DeleteFunc(labels, func(l prompb.Label) bool {
		return l.Name == "__type__" || l.Name == "__unit__"
	})

	labels = append(labels, prompb.Label{Name: "__type__", Value: strings.ToLower(metadata.Type.String())})
	labels = append(labels, prompb.Label{Name: "__unit__", Value: unitNamer.Build(metadata.Unit)})

	return labels
}
