package prom

import (
	"encoding/json"
	"fmt"
	"maps"
	"testing"
	"time"

	"github.com/google/uuid"
	prommodel "github.com/prometheus/common/model"
	"github.com/stretchr/testify/require"
	"go.yaml.in/yaml/v3"

	"github.com/grafana/grafana/pkg/expr"
	"github.com/grafana/grafana/pkg/expr/mathexp"
	"github.com/grafana/grafana/pkg/services/datasources"
	"github.com/grafana/grafana/pkg/services/ngalert/models"
	"github.com/grafana/grafana/pkg/util"
)

func TestPrometheusRulesToGrafana(t *testing.T) {
	defaultInterval := 2 * time.Minute

	testCases := []struct {
		name        string
		orgID       int64
		namespace   string
		promGroup   PrometheusRuleGroup
		config      Config
		expectError bool
		errorMsg    string
	}{
		{
			name:      "valid rule group",
			orgID:     1,
			namespace: "some-namespace-uid",
			promGroup: PrometheusRuleGroup{
				Name:        "test-group-1",
				Interval:    prommodel.Duration(10 * time.Second),
				QueryOffset: util.Pointer(prommodel.Duration(1 * time.Minute)),
				Rules: []PrometheusRule{
					{
						Alert:         "alert-1",
						Expr:          "cpu_usage > 80",
						For:           util.Pointer(prommodel.Duration(5 * time.Minute)),
						KeepFiringFor: util.Pointer(prommodel.Duration(60 * time.Second)),
						Labels: map[string]string{
							"severity": "critical",
						},
						Annotations: map[string]string{
							"summary": "CPU usage is critical",
						},
					},
				},
			},
			expectError: false,
		},
		{
			// If the rule group has no recording rules, the target datasource
			// can be anything and should not be validated.
			name:      "alert rules with non-prometheus target datasource",
			orgID:     1,
			namespace: "namespaceUID",
			promGroup: PrometheusRuleGroup{
				Name:     "test-group-1",
				Interval: prommodel.Duration(10 * time.Second),
				Rules: []PrometheusRule{
					{
						Alert: "alert-1",
						Expr:  "up == 0",
					},
				},
			},
			config: Config{
				TargetDatasourceUID:  "target-datasource-uid",
				TargetDatasourceType: "non-prometheus-datasource",
			},
			expectError: false,
		},
		{
			// If the rule group has recording rules and a non-prometheus target datasource,
			// we should return an error
			name:      "recording rules with non-prometheus target datasource",
			orgID:     1,
			namespace: "namespaceUID",
			promGroup: PrometheusRuleGroup{
				Name:     "test-group-1",
				Interval: prommodel.Duration(10 * time.Second),
				Rules: []PrometheusRule{
					{
						Record: "some_metric",
						Expr:   "sum(rate(http_requests_total[5m]))",
					},
				},
			},
			config: Config{
				TargetDatasourceUID:  "target-datasource-uid",
				TargetDatasourceType: "non-prometheus-datasource",
			},
			expectError: true,
			errorMsg:    "invalid target datasource type: non-prometheus-datasource, must be prometheus",
		},
		{
			// If the rule group has recording rules and a non-prometheus target datasource,
			// we should return an error
			name:      "mixed group with both alert and recording rules requires prometheus target datasource",
			orgID:     1,
			namespace: "namespaceUID",
			promGroup: PrometheusRuleGroup{
				Name:     "mixed-rules-group",
				Interval: prommodel.Duration(10 * time.Second),
				Rules: []PrometheusRule{
					{
						Alert: "alert-1",
						Expr:  "up == 0",
					},
					{
						Record: "some_metric",
						Expr:   "sum(rate(http_requests_total[5m]))",
					},
				},
			},
			config: Config{
				TargetDatasourceUID:  "target-datasource-uid",
				TargetDatasourceType: "non-prometheus-datasource",
			},
			expectError: true,
			errorMsg:    "invalid target datasource type: non-prometheus-datasource, must be prometheus",
		},
		{
			name:      "rule group with empty interval",
			orgID:     1,
			namespace: "namespaceUID",
			promGroup: PrometheusRuleGroup{
				Name: "test-group-1",
				Rules: []PrometheusRule{
					{
						Alert: "alert-1",
						Expr:  "up == 0",
					},
				},
			},
			expectError: false,
		},
		{
			name:      "rule group with empty name",
			orgID:     1,
			namespace: "namespaceUID",
			promGroup: PrometheusRuleGroup{
				Name: "",
				Rules: []PrometheusRule{
					{
						Alert: "alert-1",
						Expr:  "up == 0",
					},
				},
			},
			expectError: true,
			errorMsg:    "rule group name must not be empty",
		},
		{
			name:      "recording rule",
			orgID:     1,
			namespace: "namespaceUID",
			promGroup: PrometheusRuleGroup{
				Name:     "test-group-1",
				Interval: prommodel.Duration(10 * time.Second),
				Rules: []PrometheusRule{
					{
						Record: "some_metric",
						Expr:   "sum(rate(http_requests_total[5m]))",
					},
				},
			},
			expectError: false,
		},
		{
			name:      "recording rule with target datasource",
			orgID:     1,
			namespace: "namespaceUID",
			promGroup: PrometheusRuleGroup{
				Name:     "test-group-1",
				Interval: prommodel.Duration(10 * time.Second),
				Rules: []PrometheusRule{
					{
						Record: "some_metric",
						Expr:   "sum(rate(http_requests_total[5m]))",
					},
				},
			},
			config: Config{
				TargetDatasourceUID:  "target-datasource-uid",
				TargetDatasourceType: datasources.DS_PROMETHEUS,
			},
			expectError: false,
		},
		{
			name:      "query_offset must be >= 0",
			orgID:     1,
			namespace: "namespaceUID",
			promGroup: PrometheusRuleGroup{
				Name:        "test-group-1",
				Interval:    prommodel.Duration(10 * time.Second),
				QueryOffset: util.Pointer(prommodel.Duration(-1)),
				Rules: []PrometheusRule{
					{
						Alert: "alert-1",
						Expr:  "up == 0",
					},
				},
			},
			expectError: true,
			errorMsg:    "query_offset must be >= 0",
		},
		{
			name:      "rule group with limit is not supported",
			orgID:     1,
			namespace: "namespaceUID",
			promGroup: PrometheusRuleGroup{
				Name:     "test-group-1",
				Interval: prommodel.Duration(10 * time.Second),
				Limit:    5,
				Rules: []PrometheusRule{
					{
						Alert: "alert-1",
						Expr:  "up == 0",
					},
				},
			},
			expectError: true,
			errorMsg:    "limit is not supported",
		},
		{
			name:      "rule group with labels",
			orgID:     1,
			namespace: "namespaceUID",
			promGroup: PrometheusRuleGroup{
				Name:     "test-group-1",
				Interval: prommodel.Duration(10 * time.Second),
				Labels:   map[string]string{"team": "devops"},
				Rules: []PrometheusRule{
					{
						Alert: "alert-1",
						Expr:  "up == 0",
					},
				},
			},
			expectError: false,
		},
		{
			name:      "when global query offset is set, it should be used",
			orgID:     1,
			namespace: "some-namespace-uid",
			promGroup: PrometheusRuleGroup{
				Name:     "test-group-1",
				Interval: prommodel.Duration(10 * time.Second),
				Rules: []PrometheusRule{
					{
						Alert: "alert-1",
						Expr:  "cpu_usage > 80",
						For:   util.Pointer(prommodel.Duration(5 * time.Minute)),
						Labels: map[string]string{
							"severity": "critical",
						},
						Annotations: map[string]string{
							"summary": "CPU usage is critical",
						},
					},
				},
			},
			config: Config{
				EvaluationOffset: util.Pointer(5 * time.Minute),
			},
			expectError: false,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			tc.config.DatasourceUID = "datasource-uid"
			tc.config.DatasourceType = datasources.DS_PROMETHEUS
			tc.config.DefaultInterval = defaultInterval
			converter, err := NewConverter(tc.config)
			require.NoError(t, err)

			grafanaGroup, err := converter.PrometheusRulesToGrafana(tc.orgID, tc.namespace, tc.promGroup)

			if tc.expectError {
				require.Error(t, err, tc.name)
				if tc.errorMsg != "" {
					require.Contains(t, err.Error(), tc.errorMsg, tc.name)
				}
				return
			}
			require.NoError(t, err, tc.name)

			require.Equal(t, tc.promGroup.Name, grafanaGroup.Title, tc.name)

			expectedInterval := int64(time.Duration(tc.promGroup.Interval).Seconds())
			if expectedInterval == 0 {
				expectedInterval = int64(defaultInterval.Seconds())
			}
			require.Equal(t, expectedInterval, grafanaGroup.Interval, tc.name)

			require.Equal(t, len(tc.promGroup.Rules), len(grafanaGroup.Rules), tc.name)

			for j, promRule := range tc.promGroup.Rules {
				grafanaRule := grafanaGroup.Rules[j]

				if promRule.Record != "" {
					require.Equal(t, promRule.Record, grafanaRule.Title)
					require.NotNil(t, grafanaRule.Record)
					require.Equal(t, grafanaRule.Record.From, queryRefID)
					require.Equal(t, promRule.Record, grafanaRule.Record.Metric)

					targetDatasourceUID := tc.config.TargetDatasourceUID
					if targetDatasourceUID == "" {
						targetDatasourceUID = tc.config.DatasourceUID
					}
					require.Equal(t, targetDatasourceUID, grafanaRule.Record.TargetDatasourceUID)
				} else {
					require.Equal(t, promRule.Alert, grafanaRule.Title)
				}

				var expectedFor time.Duration
				if promRule.For != nil {
					expectedFor = time.Duration(*promRule.For)
				}
				require.Equal(t, expectedFor, grafanaRule.For, tc.name)

				var expectedKeepFiringFor time.Duration
				if promRule.KeepFiringFor != nil {
					expectedKeepFiringFor = time.Duration(*promRule.KeepFiringFor)
				}
				require.Equal(t, expectedKeepFiringFor, grafanaRule.KeepFiringFor, tc.name)

				expectedLabels := make(map[string]string, len(promRule.Labels)+len(tc.promGroup.Labels))
				maps.Copy(expectedLabels, tc.promGroup.Labels)
				maps.Copy(expectedLabels, promRule.Labels)
				expectedLabels = withInternalLabel(expectedLabels)

				uidData := fmt.Sprintf("%d|%s|%s|%d", tc.orgID, tc.namespace, tc.promGroup.Name, j)
				u := uuid.NewSHA1(uuid.NameSpaceOID, []byte(uidData))
				require.Equal(t, u.String(), grafanaRule.UID, tc.name)

				require.Equal(t, expectedLabels, grafanaRule.Labels, tc.name)
				require.Equal(t, promRule.Annotations, grafanaRule.Annotations, tc.name)

				evalOffset := time.Duration(0)
				if tc.config.EvaluationOffset != nil {
					evalOffset = *tc.config.EvaluationOffset
				}
				if tc.promGroup.QueryOffset != nil {
					// group-level offset takes precedence
					evalOffset = time.Duration(*tc.promGroup.QueryOffset)
				}

				require.Equal(t, models.Duration(evalOffset), grafanaRule.Data[0].RelativeTimeRange.To)
				require.Equal(t, models.Duration(10*time.Minute+evalOffset), grafanaRule.Data[0].RelativeTimeRange.From)

				if promRule.Record != "" {
					require.Nil(t, grafanaRule.MissingSeriesEvalsToResolve)
				} else {
					require.Equal(t, util.Pointer(int64(1)), grafanaRule.MissingSeriesEvalsToResolve)
				}

				require.Equal(t, models.OkErrState, grafanaRule.ExecErrState)
				require.Equal(t, models.OK, grafanaRule.NoDataState)

				// Update the rule with the group-level labels,
				// to test that they are saved to the rule definition.
				mergedLabels := make(map[string]string)
				maps.Copy(mergedLabels, tc.promGroup.Labels)
				maps.Copy(mergedLabels, promRule.Labels)
				promRule.Labels = mergedLabels
				originalRuleDefinition, err := yaml.Marshal(promRule)
				require.NoError(t, err)
				require.Equal(t, string(originalRuleDefinition), grafanaRule.Metadata.PrometheusStyleRule.OriginalRuleDefinition)
			}
		})
	}
}

func TestPrometheusRulesToGrafanaWithDuplicateRuleNames(t *testing.T) {
	cfg := Config{
		DatasourceUID:   "datasource-uid",
		DatasourceType:  datasources.DS_PROMETHEUS,
		DefaultInterval: 2 * time.Minute,
	}
	converter, err := NewConverter(cfg)
	require.NoError(t, err)

	promGroup := PrometheusRuleGroup{
		Name:     "test-group-1",
		Interval: prommodel.Duration(10 * time.Second),
		Rules: []PrometheusRule{
			{
				Alert: "alert",
				Expr:  "up",
			},
			{
				Alert: "alert",
				Expr:  "up",
			},
			{
				Alert: "another alert",
				Expr:  "up",
			},
			{
				Alert: "alert",
				Expr:  "up",
			},
		},
	}

	group, err := converter.PrometheusRulesToGrafana(1, "namespaceUID", promGroup)
	require.NoError(t, err)

	require.Equal(t, "test-group-1", group.Title)
	require.Len(t, group.Rules, 4)
	require.Equal(t, "alert", group.Rules[0].Title)
	require.Equal(t, "alert", group.Rules[1].Title)
	require.Equal(t, "another alert", group.Rules[2].Title)
	require.Equal(t, "alert", group.Rules[3].Title)
}

func TestCreateMathNode(t *testing.T) {
	node, err := createMathNode()
	require.NoError(t, err)

	require.Equal(t, expr.DatasourceUID, node.DatasourceUID)
	require.Equal(t, string(expr.QueryTypeMath), node.QueryType)
	require.Equal(t, "prometheus_math", node.RefID)

	var model map[string]interface{}
	err = json.Unmarshal(node.Model, &model)
	require.NoError(t, err)

	require.Equal(t, "prometheus_math", model["refId"])
	require.Equal(t, string(expr.QueryTypeMath), model["type"])
	require.Equal(t, "is_number($query) || is_nan($query) || is_inf($query)", model["expression"])

	ds := model["datasource"].(map[string]interface{})
	require.Equal(t, expr.DatasourceUID, ds["name"])
	require.Equal(t, expr.DatasourceType, ds["type"])
	require.Equal(t, expr.DatasourceUID, ds["uid"])
}

func TestCreateThresholdNode(t *testing.T) {
	node, err := createThresholdNode()
	require.NoError(t, err)

	require.Equal(t, expr.DatasourceUID, node.DatasourceUID)
	require.Equal(t, string(expr.QueryTypeThreshold), node.QueryType)
	require.Equal(t, "threshold", node.RefID)

	var model map[string]interface{}
	err = json.Unmarshal(node.Model, &model)
	require.NoError(t, err)

	require.Equal(t, "threshold", model["refId"])
	require.Equal(t, string(expr.QueryTypeThreshold), model["type"])

	ds := model["datasource"].(map[string]interface{})
	require.Equal(t, expr.DatasourceUID, ds["name"])
	require.Equal(t, expr.DatasourceType, ds["type"])
	require.Equal(t, expr.DatasourceUID, ds["uid"])

	conditions := model["conditions"].([]interface{})
	require.Len(t, conditions, 1)

	condition := conditions[0].(map[string]interface{})
	evaluator := condition["evaluator"].(map[string]interface{})
	require.Equal(t, string(expr.ThresholdIsAbove), evaluator["type"])
	require.Equal(t, []interface{}{float64(0)}, evaluator["params"])
}

func TestPrometheusRulesToGrafana_NodesInRules(t *testing.T) {
	cfg := Config{
		DatasourceUID:   "datasource-uid",
		DatasourceType:  datasources.DS_PROMETHEUS,
		DefaultInterval: 2 * time.Minute,
	}
	converter, err := NewConverter(cfg)
	require.NoError(t, err)

	t.Run("alert rule should have math and threshold nodes", func(t *testing.T) {
		group := PrometheusRuleGroup{
			Name: "test",
			Rules: []PrometheusRule{
				{
					Alert: "alert1",
					Expr:  "up == 0",
				},
			},
		}

		result, err := converter.PrometheusRulesToGrafana(1, "namespace", group)
		require.NoError(t, err)
		require.Len(t, result.Rules, 1)
		require.Len(t, result.Rules[0].Data, 3)

		// First node should be query
		require.Equal(t, "query", result.Rules[0].Data[0].RefID)

		// Second node should be math
		require.Equal(t, "prometheus_math", result.Rules[0].Data[1].RefID)
		require.Equal(t, string(expr.QueryTypeMath), result.Rules[0].Data[1].QueryType)
		// Check that the math expression is valid
		var model map[string]interface{}
		err = json.Unmarshal(result.Rules[0].Data[1].Model, &model)
		require.NoError(t, err)
		require.Equal(t, "is_number($query) || is_nan($query) || is_inf($query)", model["expression"])
		// The math expression should be parsed successfully
		_, err = mathexp.New(model["expression"].(string))
		require.NoError(t, err)

		// Third node should be threshold
		require.Equal(t, "threshold", result.Rules[0].Data[2].RefID)
		require.Equal(t, string(expr.QueryTypeThreshold), result.Rules[0].Data[2].QueryType)
	})

	t.Run("recording rule should only have query node", func(t *testing.T) {
		group := PrometheusRuleGroup{
			Name: "test",
			Rules: []PrometheusRule{
				{
					Record: "metric",
					Expr:   "sum(rate(http_requests_total[5m]))",
				},
			},
		}

		result, err := converter.PrometheusRulesToGrafana(1, "namespace", group)
		require.NoError(t, err)
		require.Len(t, result.Rules, 1)
		require.Len(t, result.Rules[0].Data, 1)

		// Should only have query node
		require.Equal(t, "query", result.Rules[0].Data[0].RefID)
	})
}

func TestPrometheusRulesToGrafana_GroupLabels(t *testing.T) {
	cfg := Config{
		DatasourceUID:   "datasource-uid",
		DatasourceType:  datasources.DS_PROMETHEUS,
		DefaultInterval: 2 * time.Minute,
	}
	converter, err := NewConverter(cfg)
	require.NoError(t, err)

	t.Run("group labels are merged with alert rule labels", func(t *testing.T) {
		promGroup := PrometheusRuleGroup{
			Name:     "test-group-1",
			Interval: prommodel.Duration(10 * time.Second),
			Labels: map[string]string{
				"group_label":  "group_value",
				"common_label": "group_value",
			},
			Rules: []PrometheusRule{
				{
					Alert: "alert-1",
					Expr:  "cpu_usage > 80",
					Labels: map[string]string{
						"rule_label":   "rule_value",
						"common_label": "rule_value", // rule-level label should take precedence
					},
				},
			},
		}

		grafanaGroup, err := converter.PrometheusRulesToGrafana(1, "namespace", promGroup)
		require.NoError(t, err)
		require.Len(t, grafanaGroup.Rules, 1)

		// Check that the labels are merged and the rule label takes precedence
		require.Equal(
			t,
			withInternalLabel(map[string]string{
				"group_label":  "group_value",
				"rule_label":   "rule_value",
				"common_label": "rule_value",
			}),
			grafanaGroup.Rules[0].Labels,
		)
	})

	t.Run("group labels are merged with recording rule labels", func(t *testing.T) {
		promGroup := PrometheusRuleGroup{
			Name:     "recording-group",
			Interval: prommodel.Duration(10 * time.Second),
			Labels: map[string]string{
				"group_label":  "group_value",
				"common_label": "group_value",
			},
			Rules: []PrometheusRule{
				{
					Record: "recording_metric",
					Expr:   "sum(rate(http_requests_total[5m]))",
					Labels: map[string]string{
						"rule_label":   "rule_value",
						"common_label": "rule_value",
					},
				},
			},
		}

		grafanaGroup, err := converter.PrometheusRulesToGrafana(1, "namespace", promGroup)
		require.NoError(t, err)
		require.Len(t, grafanaGroup.Rules, 1)

		// Check that the labels are merged and the rule label takes precedence
		require.Equal(
			t,
			withInternalLabel(map[string]string{
				"group_label":  "group_value",
				"rule_label":   "rule_value",
				"common_label": "rule_value",
			}),
			grafanaGroup.Rules[0].Labels,
		)
	})

	t.Run("rule with no labels gets group labels", func(t *testing.T) {
		promGroup := PrometheusRuleGroup{
			Name:     "group-with-labels",
			Interval: prommodel.Duration(10 * time.Second),
			Labels: map[string]string{
				"group_label1": "group_value1",
				"group_label2": "group_value2",
			},
			Rules: []PrometheusRule{
				{
					Alert: "alert-no-labels",
					Expr:  "up == 0",
				},
			},
		}

		grafanaGroup, err := converter.PrometheusRulesToGrafana(1, "namespace", promGroup)
		require.NoError(t, err)
		require.Len(t, grafanaGroup.Rules, 1)
		require.Equal(t, withInternalLabel(promGroup.Labels), grafanaGroup.Rules[0].Labels)
	})

	t.Run("rule and group with nil labels", func(t *testing.T) {
		promGroup := PrometheusRuleGroup{
			Name:     "group-no-labels",
			Interval: prommodel.Duration(10 * time.Second),
			Rules: []PrometheusRule{
				{
					Alert: "alert-no-labels",
					Expr:  "up == 0",
				},
			},
		}

		grafanaGroup, err := converter.PrometheusRulesToGrafana(1, "namespace", promGroup)
		require.NoError(t, err)
		require.Len(t, grafanaGroup.Rules, 1)
		require.Equal(t, withInternalLabel(map[string]string{}), grafanaGroup.Rules[0].Labels)
	})
}

func TestPrometheusRulesToGrafana_ExtraLabels(t *testing.T) {
	cfg := Config{
		DatasourceUID:   "datasource-uid",
		DatasourceType:  datasources.DS_PROMETHEUS,
		DefaultInterval: 2 * time.Minute,
		ExtraLabels: map[string]string{
			"extra_label":  "extra_value",
			"common_label": "extra_value",
			"rule_label":   "value_from_extra_labels",
		},
	}
	converter, err := NewConverter(cfg)
	require.NoError(t, err)

	t.Run("extra labels are merged with group and rule labels", func(t *testing.T) {
		promGroup := PrometheusRuleGroup{
			Name:     "test-group-1",
			Interval: prommodel.Duration(10 * time.Second),
			Labels: map[string]string{
				"group_label":  "group_value",
				"common_label": "group_value",
			},
			Rules: []PrometheusRule{
				{
					Alert: "alert-1",
					Expr:  "cpu_usage > 80",
					Labels: map[string]string{
						"rule_label": "rule_value",
					},
				},
			},
		}

		grafanaGroup, err := converter.PrometheusRulesToGrafana(1, "namespace", promGroup)
		require.NoError(t, err)
		require.Len(t, grafanaGroup.Rules, 1)

		expectedLabels := withInternalLabel(map[string]string{
			"extra_label":  "extra_value",
			"common_label": "group_value",
			"group_label":  "group_value",
			"rule_label":   "rule_value",
		})
		require.Equal(t, expectedLabels, grafanaGroup.Rules[0].Labels)
	})

	t.Run("extra labels are not applied to recording rules", func(t *testing.T) {
		promGroup := PrometheusRuleGroup{
			Name:     "test-group-2",
			Interval: prommodel.Duration(10 * time.Second),
			Rules: []PrometheusRule{
				{
					Record: "http_requests_total:rate5m",
					Expr:   "rate(http_requests_total[5m])",
				},
			},
		}

		grafanaGroup, err := converter.PrometheusRulesToGrafana(1, "namespace", promGroup)
		require.NoError(t, err)
		require.Len(t, grafanaGroup.Rules, 1)

		expectedLabels := withInternalLabel(make(map[string]string))
		require.Equal(t, expectedLabels, grafanaGroup.Rules[0].Labels)

		for key := range cfg.ExtraLabels {
			require.NotContains(t, grafanaGroup.Rules[0].Labels, key)
		}
	})
}

func TestPrometheusRulesToGrafana_UID(t *testing.T) {
	orgID := int64(1)
	namespace := "some-namespace"

	promGroup := PrometheusRuleGroup{
		Name:     "test-group-1",
		Interval: prommodel.Duration(10 * time.Second),
		Rules: []PrometheusRule{
			{
				Alert: "alert-1",
				Expr:  "cpu_usage > 80",
				For:   util.Pointer(prommodel.Duration(5 * time.Minute)),
				Labels: map[string]string{
					"severity":   "critical",
					ruleUIDLabel: "rule-uid-1",
				},
				Annotations: map[string]string{
					"summary": "CPU usage is critical",
				},
			},
		},
	}

	converter, err := NewConverter(Config{
		DatasourceUID:   "datasource-uid",
		DatasourceType:  datasources.DS_PROMETHEUS,
		DefaultInterval: 2 * time.Minute,
	})
	require.NoError(t, err)

	t.Run("if not specified, UID is generated based on the rule index", func(t *testing.T) {
		grafanaGroup, err := converter.PrometheusRulesToGrafana(orgID, namespace, promGroup)
		require.NoError(t, err)

		firstUID := grafanaGroup.Rules[0].UID

		// Convert again
		grafanaGroup, err = converter.PrometheusRulesToGrafana(orgID, namespace, promGroup)
		require.NoError(t, err)

		secondUID := grafanaGroup.Rules[0].UID

		// They must be equal
		require.NotEmpty(t, firstUID)
		require.Equal(t, firstUID, secondUID)
	})

	t.Run("if the special label is specified", func(t *testing.T) {
		t.Run("and the label is valid it should be used", func(t *testing.T) {
			orgID := int64(1)
			namespace := "some-namespace"

			converter, err := NewConverter(Config{
				DatasourceUID:   "datasource-uid",
				DatasourceType:  datasources.DS_PROMETHEUS,
				DefaultInterval: 2 * time.Minute,
			})
			require.NoError(t, err)

			promGroup.Rules[0].Labels[ruleUIDLabel] = "rule-uid-1"

			grafanaGroup, err := converter.PrometheusRulesToGrafana(orgID, namespace, promGroup)
			require.NoError(t, err)

			require.Equal(t, "rule-uid-1", grafanaGroup.Rules[0].UID)
		})

		t.Run("and the label is invalid", func(t *testing.T) {
			orgID := int64(1)
			namespace := "some-namespace"

			converter, err := NewConverter(Config{
				DatasourceUID:   "datasource-uid",
				DatasourceType:  datasources.DS_PROMETHEUS,
				DefaultInterval: 2 * time.Minute,
			})
			require.NoError(t, err)

			// create a string of 50 characters
			promGroup.Rules[0].Labels[ruleUIDLabel] = "aaaabbbbccccddddeeeeffffgggghhhhiiiijjjjkkkkllllmm" // too long

			grafanaGroup, err := converter.PrometheusRulesToGrafana(orgID, namespace, promGroup)
			require.Errorf(t, err, "invalid UID label value")
			require.Nil(t, grafanaGroup)
		})

		t.Run("and the label is empty", func(t *testing.T) {
			orgID := int64(1)
			namespace := "some-namespace"

			converter, err := NewConverter(Config{
				DatasourceUID:   "datasource-uid",
				DatasourceType:  datasources.DS_PROMETHEUS,
				DefaultInterval: 2 * time.Minute,
			})
			require.NoError(t, err)

			promGroup.Rules[0].Labels[ruleUIDLabel] = ""

			grafanaGroup, err := converter.PrometheusRulesToGrafana(orgID, namespace, promGroup)
			require.Errorf(t, err, "invalid UID label value")
			require.Nil(t, grafanaGroup)
		})
	})
}

func TestPrometheusRulesToGrafana_KeepOriginalRuleDefinition(t *testing.T) {
	orgID := int64(1)
	namespace := "namespace"

	promGroup := PrometheusRuleGroup{
		Name: "test-group",
		Rules: []PrometheusRule{
			{
				Alert: "test-alert",
				Expr:  "up == 0",
			},
		},
	}

	testCases := []struct {
		name                       string
		keepOriginalRuleDefinition *bool
		expectDefinition           bool
	}{
		{
			name:                       "keep original rule definition is true",
			keepOriginalRuleDefinition: util.Pointer(true),
			expectDefinition:           true,
		},
		{
			name:                       "keep original rule definition is false",
			keepOriginalRuleDefinition: util.Pointer(false),
			expectDefinition:           false,
		},
		{
			name:                       "keep original rule definition is nil (should use default)",
			keepOriginalRuleDefinition: nil,
			expectDefinition:           true,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			cfg := Config{
				DatasourceUID:              "datasource-uid",
				DatasourceType:             datasources.DS_PROMETHEUS,
				DefaultInterval:            1 * time.Minute,
				KeepOriginalRuleDefinition: tc.keepOriginalRuleDefinition,
			}

			converter, err := NewConverter(cfg)
			require.NoError(t, err)

			// Convert the Prometheus rule to Grafana
			grafanaGroup, err := converter.PrometheusRulesToGrafana(orgID, namespace, promGroup)
			require.NoError(t, err)
			require.Len(t, grafanaGroup.Rules, 1)

			if tc.expectDefinition {
				originalRuleDefinition, err := yaml.Marshal(promGroup.Rules[0])
				require.NoError(t, err)
				require.Equal(
					t,
					string(originalRuleDefinition),
					grafanaGroup.Rules[0].Metadata.PrometheusStyleRule.OriginalRuleDefinition,
				)
			} else {
				require.Nil(t, grafanaGroup.Rules[0].Metadata.PrometheusStyleRule)
			}
		})
	}
}

func TestPrometheusRulesToGrafana_NotificationSettings(t *testing.T) {
	orgID := int64(1)
	namespace := "namespace"

	promGroup := PrometheusRuleGroup{
		Name: "test-group",
		Rules: []PrometheusRule{
			{
				Alert: "test-alert",
				Expr:  "up == 0",
			},
		},
	}

	testCases := []struct {
		name                 string
		notificationSettings *models.NotificationSettings
	}{
		{
			name: "with notification settings specified",
			notificationSettings: &models.NotificationSettings{
				ContactPointRouting: &models.ContactPointRouting{
					Receiver: "test-receiver",
					GroupBy:  []string{"alertname", "instance"},
				},
			},
		},
		{
			name:                 "without notification settings",
			notificationSettings: nil,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			cfg := Config{
				DatasourceUID:        "datasource-uid",
				DatasourceType:       datasources.DS_PROMETHEUS,
				DefaultInterval:      1 * time.Minute,
				NotificationSettings: tc.notificationSettings,
			}

			converter, err := NewConverter(cfg)
			require.NoError(t, err)

			grafanaGroup, err := converter.PrometheusRulesToGrafana(orgID, namespace, promGroup)
			require.NoError(t, err)
			require.Len(t, grafanaGroup.Rules, 1)

			if tc.notificationSettings != nil {
				require.NotNil(t, grafanaGroup.Rules[0].NotificationSettings)
				require.Equal(t, tc.notificationSettings, grafanaGroup.Rules[0].NotificationSettings)
			} else {
				require.Nil(t, grafanaGroup.Rules[0].NotificationSettings)
			}
		})
	}
}

func TestQueryModelContainsRequiredParameters(t *testing.T) {
	cfg := Config{
		DatasourceUID:   "datasource-uid",
		DatasourceType:  datasources.DS_PROMETHEUS,
		DefaultInterval: 1 * time.Minute,
	}
	converter, err := NewConverter(cfg)
	require.NoError(t, err)

	promRule := PrometheusRule{
		Alert: "test-alert",
		Expr:  "up == 0",
	}

	queries, err := converter.createQuery(promRule.Expr, false, PrometheusRuleGroup{})
	require.NoError(t, err)
	require.Len(t, queries, 3)

	for _, query := range queries {
		var model map[string]any
		err = json.Unmarshal(query.Model, &model)
		require.NoError(t, err)

		// Check intervalMs
		intervalMs, exists := model["intervalMs"]
		require.True(t, exists)
		_, isNumber := intervalMs.(float64)
		require.True(t, isNumber, "intervalMs should be a number")

		// Check maxDataPoints
		maxDataPoints, exists := model["maxDataPoints"]
		require.True(t, exists)
		_, isNumber = maxDataPoints.(float64)
		require.True(t, isNumber, "maxDataPoints should be a number")
	}
}

func withInternalLabel(l map[string]string) map[string]string {
	result := map[string]string{
		models.ConvertedPrometheusRuleLabel: "true",
	}
	maps.Copy(result, l)

	return result
}
