package parser_test

import (
	"bufio"
	"bytes"
	"errors"
	"os"
	"strconv"
	"testing"
	"time"

	"github.com/prometheus/common/model"
	"github.com/stretchr/testify/require"

	"github.com/cloudflare/pint/internal/comments"
	"github.com/cloudflare/pint/internal/diags"
	"github.com/cloudflare/pint/internal/parser"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
)

func TestParse(t *testing.T) {
	type testCaseT struct {
		input  []byte
		output parser.File
		strict bool
		schema parser.Schema
		names  model.ValidationScheme
	}

	testCases := []testCaseT{
		{
			input: nil,
			output: parser.File{
				IsRelaxed: true,
			},
		},
		{
			input: []byte{},
			output: parser.File{
				IsRelaxed: true,
			},
		},
		{
			input: []byte(""),
			output: parser.File{
				IsRelaxed: true,
			},
		},
		{
			input: []byte("\n"),
			output: parser.File{
				IsRelaxed: true,
			},
		},
		{
			input: []byte("\n\n\n"),
			output: parser.File{
				IsRelaxed: true,
			},
		},
		{
			input: []byte("---"),
			output: parser.File{
				IsRelaxed: true,
			},
		},
		{
			input: []byte("---\n"),
			output: parser.File{
				IsRelaxed: true,
			},
		},
		{
			input: []byte("\n---\n\n---\n"),
			output: parser.File{
				IsRelaxed: true,
			},
		},
		{
			input: []byte("\n---\n\n---\n---"),
			output: parser.File{
				IsRelaxed: true,
			},
		},
		{
			input: []byte(string("! !00 \xf6")),
			output: parser.File{
				IsRelaxed: true,
				Error: parser.ParseError{
					Err:  errors.New("yaml: incomplete UTF-8 octet sequence"),
					Line: 1,
				},
			},
		},
		{
			input: []byte("- 0: 0\n  00000000: 000000\n  000000:00000000000: 00000000\n  00000000000:000000: 0000000000000000000000000000000000\n  000000: 0000000\n  expr: |"),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 1, Last: 6},
								Error: parser.ParseError{Err: errors.New("incomplete rule, no alert or record key"), Line: 6},
							},
						},
					},
				},
			},
		},
		{
			input: []byte("- record: |\n    multiline\n"),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 1, Last: 2},
								Error: parser.ParseError{Err: errors.New("missing expr key"), Line: 2},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`---
- expr: foo
  record: foo
---
- expr: bar
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								RecordingRule: &parser.RecordingRule{
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "foo",
											Pos: diags.PositionRanges{
												{Line: 2, FirstColumn: 9, LastColumn: 11},
											},
										},
									},
									Record: parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 3, FirstColumn: 11, LastColumn: 13},
										},
									},
								},
								Lines: diags.LineRange{First: 2, Last: 3},
							},
						},
					},
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 5, Last: 5},
								Error: parser.ParseError{Err: errors.New("incomplete rule, no alert or record key"), Line: 5},
							},
						},
					},
				},
			},
		},
		{
			input: []byte("- expr: foo\n"),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 1, Last: 1},
								Error: parser.ParseError{Err: errors.New("incomplete rule, no alert or record key"), Line: 1},
							},
						},
					},
				},
			},
		},
		{
			input: []byte("- alert: foo\n"),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 1, Last: 1},
								Error: parser.ParseError{Err: errors.New("missing expr key"), Line: 1},
							},
						},
					},
				},
			},
		},
		{
			input: []byte("- alert: foo\n  record: foo\n"),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 1, Last: 2},
								Error: parser.ParseError{Err: errors.New("got both record and alert keys in a single rule"), Line: 1},
							},
						},
					},
				},
			},
		},
		{
			input: []byte("- record: foo\n  labels:\n    foo: bar\n"),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 1, Last: 3},
								Error: parser.ParseError{Err: errors.New("missing expr key"), Line: 1},
							},
						},
					},
				},
			},
		},
		{
			input: []byte("- record: - foo\n"),
			output: parser.File{
				IsRelaxed: true,
				Error: parser.ParseError{
					Err:  errors.New("yaml: block sequence entries are not allowed in this context"),
					Line: 1,
				},
			},
		},
		{
			input: []byte("- record: foo  expr: sum(\n"),
			output: parser.File{
				IsRelaxed: true,
				Error: parser.ParseError{
					Err:  errors.New("yaml: mapping values are not allowed in this context"),
					Line: 1,
				},
			},
		},
		{
			input: []byte("- record\n\texpr: foo\n"),
			output: parser.File{
				IsRelaxed: true,
				Error: parser.ParseError{
					Err:  errors.New("found a tab character that violates indentation"),
					Line: 2,
				},
			},
		},
		{
			input: []byte(`
- record: foo
  expr: bar
  expr: bar
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 4},
								Error: parser.ParseError{Err: errors.New("duplicated expr key"), Line: 4},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- record: foo
  expr: bar
  record: bar
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 4},
								Error: parser.ParseError{Err: errors.New("duplicated record key"), Line: 4},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- alert: foo
  alert: bar
  expr: bar
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 3},
								Error: parser.ParseError{Err: errors.New("duplicated alert key"), Line: 3},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- alert: foo
  for: 5m
  expr: bar
  for: 1m
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 5},
								Error: parser.ParseError{Err: errors.New("duplicated for key"), Line: 5},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- alert: foo
  keep_firing_for: 5m
  expr: bar
  keep_firing_for: 1m
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 5},
								Error: parser.ParseError{Err: errors.New("duplicated keep_firing_for key"), Line: 5},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- alert: foo
  labels: {}
  expr: bar
  labels: {}
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 5},
								Error: parser.ParseError{Err: errors.New("duplicated labels key"), Line: 5},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- record: foo
  labels: {}
  expr: bar
  labels: {}
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 5},
								Error: parser.ParseError{Err: errors.New("duplicated labels key"), Line: 5},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- alert: foo
  annotations: {}
  expr: bar
  annotations: {}
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 5},
								Error: parser.ParseError{Err: errors.New("duplicated annotations key"), Line: 5},
							},
						},
					},
				},
			},
		},
		{
			input: []byte("- record: foo\n  expr: foo\n  extra: true\n"),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 1, Last: 3},
								Error: parser.ParseError{Err: errors.New("invalid key(s) found: extra"), Line: 3},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`- record: foo
  expr: foo offset 10m
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 1, Last: 2},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 1, FirstColumn: 11, LastColumn: 13},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Pos: diags.PositionRanges{
												{Line: 2, FirstColumn: 9, LastColumn: 22},
											},
											Value: "foo offset 10m",
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte("- record: foo\n  expr: foo offset -10m\n"),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 1, Last: 2},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 1, FirstColumn: 11, LastColumn: 13},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "foo offset -10m",
											Pos: diags.PositionRanges{
												{Line: 2, FirstColumn: 9, LastColumn: 23},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
# pint disable head comment
- record: foo # pint disable record comment
  expr: foo offset 10m # pint disable expr comment
  #  pint disable pre-labels comment
  labels:
    # pint disable pre-foo comment
    foo: bar
    # pint disable post-foo comment
    bob: alice
  # pint disable foot comment
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 3, Last: 10},
								Comments: []comments.Comment{
									{
										Type: comments.DisableType,
										Value: comments.Disable{
											Match: "head comment",
											Position: comments.Position{
												Offset: 15,
												Pos: diags.PositionRanges{
													{Line: 2, FirstColumn: 1, LastColumn: 27},
												},
											},
										},
									},
									{
										Type: comments.DisableType,
										Value: comments.Disable{
											Match: "record comment",
											Position: comments.Position{
												Offset: 15,
												Pos: diags.PositionRanges{
													{Line: 3, FirstColumn: 1, LastColumn: 29},
												},
											},
										},
									},
									{
										Type: comments.DisableType,
										Value: comments.Disable{
											Match: "expr comment",
											Position: comments.Position{
												Offset: 15,
												Pos: diags.PositionRanges{
													{Line: 4, FirstColumn: 1, LastColumn: 27},
												},
											},
										},
									},
									{
										Type: comments.DisableType,
										Value: comments.Disable{
											Match: "pre-labels comment",
											Position: comments.Position{
												Offset: 16,
												Pos: diags.PositionRanges{
													{Line: 5, FirstColumn: 3, LastColumn: 36},
												},
											},
										},
									},
									{
										Type: comments.DisableType,
										Value: comments.Disable{
											Match: "foot comment",
											Position: comments.Position{
												Offset: 15,
												Pos: diags.PositionRanges{
													{Line: 7, FirstColumn: 5, LastColumn: 31},
												},
											},
										},
									},
									{
										Type: comments.DisableType,
										Value: comments.Disable{
											Match: "pre-foo comment",
											Position: comments.Position{
												Offset: 15,
												Pos: diags.PositionRanges{
													{Line: 7, FirstColumn: 5, LastColumn: 34},
												},
											},
										},
									},
									{
										Type: comments.DisableType,
										Value: comments.Disable{
											Match: "post-foo comment",
											Position: comments.Position{
												Offset: 15,
												Pos: diags.PositionRanges{
													{Line: 9, FirstColumn: 5, LastColumn: 35},
												},
											},
										},
									},
								},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 3, FirstColumn: 11, LastColumn: 13},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "foo offset 10m",
											Pos: diags.PositionRanges{
												{Line: 4, FirstColumn: 9, LastColumn: 22},
											},
										},
									},
									Labels: &parser.YamlMap{
										Key: &parser.YamlNode{
											Value: "labels",
											Pos: diags.PositionRanges{
												{Line: 6, FirstColumn: 3, LastColumn: 8},
											},
										},
										Items: []*parser.YamlKeyValue{
											{
												Key: &parser.YamlNode{
													Value: "foo",
													Pos: diags.PositionRanges{
														{Line: 8, FirstColumn: 5, LastColumn: 7},
													},
												},
												Value: &parser.YamlNode{
													Value: "bar",
													Pos: diags.PositionRanges{
														{Line: 8, FirstColumn: 10, LastColumn: 12},
													},
												},
											},
											{
												Key: &parser.YamlNode{
													Value: "bob",
													Pos: diags.PositionRanges{
														{Line: 10, FirstColumn: 5, LastColumn: 7},
													},
												},
												Value: &parser.YamlNode{
													Value: "alice",
													Pos: diags.PositionRanges{
														{Line: 10, FirstColumn: 10, LastColumn: 14},
													},
												},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte("- record: foo\n  expr: foo[5m] offset 10m\n"),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 1, Last: 2},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 1, FirstColumn: 11, LastColumn: 13},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "foo[5m] offset 10m",
											Pos: diags.PositionRanges{
												{Line: 2, FirstColumn: 9, LastColumn: 26},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- record: name
  expr: sum(foo)
  labels:
    foo: bar
    bob: alice
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 6},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "name",
										Pos: diags.PositionRanges{
											{Line: 2, FirstColumn: 11, LastColumn: 14},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "sum(foo)",
											Pos: diags.PositionRanges{
												{Line: 3, FirstColumn: 9, LastColumn: 16},
											},
										},
									},
									Labels: &parser.YamlMap{
										Key: &parser.YamlNode{
											Value: "labels",
											Pos: diags.PositionRanges{
												{Line: 4, FirstColumn: 3, LastColumn: 8},
											},
										},
										Items: []*parser.YamlKeyValue{
											{
												Key: &parser.YamlNode{
													Value: "foo",
													Pos: diags.PositionRanges{
														{Line: 5, FirstColumn: 5, LastColumn: 7},
													},
												},
												Value: &parser.YamlNode{
													Value: "bar",
													Pos: diags.PositionRanges{
														{Line: 5, FirstColumn: 10, LastColumn: 12},
													},
												},
											},
											{
												Key: &parser.YamlNode{
													Value: "bob",
													Pos: diags.PositionRanges{
														{Line: 6, FirstColumn: 5, LastColumn: 7},
													},
												},
												Value: &parser.YamlNode{
													Value: "alice",
													Pos: diags.PositionRanges{
														{Line: 6, FirstColumn: 10, LastColumn: 14},
													},
												},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: custom_rules
  rules:
    - record: name
      expr: sum(foo)
      labels:
        foo: bar
        bob: alice
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "custom_rules",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 20},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 5, Last: 9},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "name",
										Pos: diags.PositionRanges{
											{Line: 5, FirstColumn: 15, LastColumn: 18},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "sum(foo)",
											Pos: diags.PositionRanges{
												{Line: 6, FirstColumn: 13, LastColumn: 20},
											},
										},
									},
									Labels: &parser.YamlMap{
										Key: &parser.YamlNode{
											Value: "labels",
											Pos: diags.PositionRanges{
												{Line: 7, FirstColumn: 7, LastColumn: 12},
											},
										},
										Items: []*parser.YamlKeyValue{
											{
												Key: &parser.YamlNode{
													Value: "foo",
													Pos: diags.PositionRanges{
														{Line: 8, FirstColumn: 9, LastColumn: 11},
													},
												},
												Value: &parser.YamlNode{
													Value: "bar",
													Pos: diags.PositionRanges{
														{Line: 8, FirstColumn: 14, LastColumn: 16},
													},
												},
											},
											{
												Key: &parser.YamlNode{
													Value: "bob",
													Pos: diags.PositionRanges{
														{Line: 9, FirstColumn: 9, LastColumn: 11},
													},
												},
												Value: &parser.YamlNode{
													Value: "alice",
													Pos: diags.PositionRanges{
														{Line: 9, FirstColumn: 14, LastColumn: 18},
													},
												},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`- alert: Down
  expr: |
    up == 0
  for: |+
    11m
  labels:
    severity: critical
  annotations:
    uri: https://docs.example.com/down.html

- record: foo
  expr: |-
    bar
    /
    baz > 1
  labels: {}
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 1, Last: 9},
								AlertingRule: &parser.AlertingRule{
									Alert: parser.YamlNode{
										Value: "Down",
										Pos: diags.PositionRanges{
											{Line: 1, FirstColumn: 10, LastColumn: 13},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "up == 0\n",
											Pos: diags.PositionRanges{
												{Line: 3, FirstColumn: 5, LastColumn: 11},
											},
										},
									},
									For: &parser.YamlDuration{
										Raw:        "11m\n",
										ParseError: errors.New(`unknown unit "m\n" in duration "11m\n"`),
										Pos: diags.PositionRanges{
											{Line: 5, FirstColumn: 5, LastColumn: 7},
										},
									},
									Labels: &parser.YamlMap{
										Key: &parser.YamlNode{
											Value: "labels",
											Pos: diags.PositionRanges{
												{Line: 6, FirstColumn: 3, LastColumn: 8},
											},
										},
										Items: []*parser.YamlKeyValue{
											{
												Key: &parser.YamlNode{
													Value: "severity",
													Pos: diags.PositionRanges{
														{Line: 7, FirstColumn: 5, LastColumn: 12},
													},
												},
												Value: &parser.YamlNode{
													Value: "critical",
													Pos: diags.PositionRanges{
														{Line: 7, FirstColumn: 15, LastColumn: 22},
													},
												},
											},
										},
									},
									Annotations: &parser.YamlMap{
										Key: &parser.YamlNode{
											Value: "annotations",
											Pos: diags.PositionRanges{
												{Line: 8, FirstColumn: 3, LastColumn: 13},
											},
										},
										Items: []*parser.YamlKeyValue{
											{
												Key: &parser.YamlNode{
													Value: "uri",
													Pos: diags.PositionRanges{
														{Line: 9, FirstColumn: 5, LastColumn: 7},
													},
												},
												Value: &parser.YamlNode{
													Value: "https://docs.example.com/down.html",
													Pos: diags.PositionRanges{
														{Line: 9, FirstColumn: 10, LastColumn: 43},
													},
												},
											},
										},
									},
								},
							},
							{
								Lines: diags.LineRange{First: 11, Last: 16},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 11, FirstColumn: 11, LastColumn: 13},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "bar\n/\nbaz > 1",
											Pos: diags.PositionRanges{
												{Line: 13, FirstColumn: 5, LastColumn: 8},
												{Line: 14, FirstColumn: 5, LastColumn: 6},
												{Line: 15, FirstColumn: 5, LastColumn: 11},
											},
										},
									},
									Labels: &parser.YamlMap{
										Key: &parser.YamlNode{
											Value: "labels",
											Pos: diags.PositionRanges{
												{Line: 16, FirstColumn: 3, LastColumn: 8},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`- alert: Foo
  expr:
    (
      xxx
      -
      yyy
    ) * bar > 0
    and on(instance, device) baz
  for: 30m
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 1, Last: 9},
								AlertingRule: &parser.AlertingRule{
									Alert: parser.YamlNode{
										Value: "Foo",
										Pos: diags.PositionRanges{
											{Line: 1, FirstColumn: 10, LastColumn: 12},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "( xxx - yyy ) * bar > 0 and on(instance, device) baz",
											Pos: diags.PositionRanges{
												{Line: 3, FirstColumn: 5, LastColumn: 6},
												{Line: 4, FirstColumn: 7, LastColumn: 10},
												{Line: 5, FirstColumn: 7, LastColumn: 8},
												{Line: 6, FirstColumn: 7, LastColumn: 10},
												{Line: 7, FirstColumn: 5, LastColumn: 16},
												{Line: 8, FirstColumn: 5, LastColumn: 32},
											},
										},
									},
									For: &parser.YamlDuration{
										Value: time.Minute * 30,
										Raw:   "30m",
										Pos: diags.PositionRanges{
											{Line: 9, FirstColumn: 8, LastColumn: 10},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`---
kind: ConfigMap
apiVersion: v1
metadata:
  name: example-app-alerts
  labels:
    app: example-app
data:
  alerts: |
    groups:
      - name: example-app-alerts
        rules:
          - alert: Example_High_Restart_Rate
            expr: sum(rate(kube_pod_container_status_restarts_total{namespace="example-app"}[5m])) > ( 3/60 )
---
kind: ConfigMap
apiVersion: v1
metadata:
  name: other
  labels:
  app: other
data:
  alerts: |

    groups:
      - name: other alerts
        rules:
          - alert: Example_High_Restart_Rate
            expr: "1"

`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "example-app-alerts",
							Pos: diags.PositionRanges{
								{Line: 2, FirstColumn: 11, LastColumn: 28},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 13, Last: 14},
								AlertingRule: &parser.AlertingRule{
									Alert: parser.YamlNode{
										Value: "Example_High_Restart_Rate",
										Pos: diags.PositionRanges{
											{Line: 13, FirstColumn: 20, LastColumn: 44},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: `sum(rate(kube_pod_container_status_restarts_total{namespace="example-app"}[5m])) > ( 3/60 )`,
											Pos: diags.PositionRanges{
												{Line: 14, FirstColumn: 19, LastColumn: 109},
											},
										},
									},
								},
							},
						},
					},
					{
						Name: parser.YamlNode{
							Value: "other alerts",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 11, LastColumn: 22},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 28, Last: 29},
								AlertingRule: &parser.AlertingRule{
									Alert: parser.YamlNode{
										Value: "Example_High_Restart_Rate",
										Pos: diags.PositionRanges{
											{Line: 28, FirstColumn: 16, LastColumn: 40},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "1",
											Pos: diags.PositionRanges{
												{Line: 29, FirstColumn: 16, LastColumn: 16},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`---
kind: ConfigMap
apiVersion: v1
metadata:
  name: example-app-alerts
  labels:
  app: example-app
data:
  alerts: |
    groups:
      - name: example-app-alerts
        rules:
          - alert: Example_Is_Down
            expr: kube_deployment_status_replicas_available{namespace="example-app"} < 1
            for: 5m
            labels:
              priority: "2"
              environment: production
            annotations:
              summary: "No replicas for Example have been running for 5 minutes"

          - alert: Example_High_Restart_Rate
            expr: sum(rate(kube_pod_container_status_restarts_total{namespace="example-app"}[5m])) > ( 3/60 )
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "example-app-alerts",
							Pos: diags.PositionRanges{
								{Line: 2, FirstColumn: 11, LastColumn: 28},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 13, Last: 20},
								AlertingRule: &parser.AlertingRule{
									Alert: parser.YamlNode{
										Value: "Example_Is_Down",
										Pos: diags.PositionRanges{
											{Line: 13, FirstColumn: 20, LastColumn: 34},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: `kube_deployment_status_replicas_available{namespace="example-app"} < 1`,
											Pos: diags.PositionRanges{
												{Line: 14, FirstColumn: 19, LastColumn: 88},
											},
										},
									},
									For: &parser.YamlDuration{
										Value: time.Minute * 5,
										Raw:   "5m",
										Pos: diags.PositionRanges{
											{Line: 15, FirstColumn: 18, LastColumn: 19},
										},
									},
									Labels: &parser.YamlMap{
										Key: &parser.YamlNode{
											Value: "labels",
											Pos: diags.PositionRanges{
												{Line: 16, FirstColumn: 13, LastColumn: 18},
											},
										},
										Items: []*parser.YamlKeyValue{
											{
												Key: &parser.YamlNode{
													Value: "priority",
													Pos: diags.PositionRanges{
														{Line: 17, FirstColumn: 15, LastColumn: 22},
													},
												},
												Value: &parser.YamlNode{
													Value: "2",
													Pos: diags.PositionRanges{
														{Line: 17, FirstColumn: 26, LastColumn: 26},
													},
												},
											},
											{
												Key: &parser.YamlNode{
													Value: "environment",
													Pos: diags.PositionRanges{
														{Line: 18, FirstColumn: 15, LastColumn: 25},
													},
												},
												Value: &parser.YamlNode{
													Value: "production",
													Pos: diags.PositionRanges{
														{Line: 18, FirstColumn: 28, LastColumn: 37},
													},
												},
											},
										},
									},
									Annotations: &parser.YamlMap{
										Key: &parser.YamlNode{
											Value: "annotations",
											Pos: diags.PositionRanges{
												{Line: 19, FirstColumn: 13, LastColumn: 23},
											},
										},
										Items: []*parser.YamlKeyValue{
											{
												Key: &parser.YamlNode{
													Value: "summary",
													Pos: diags.PositionRanges{
														{Line: 20, FirstColumn: 15, LastColumn: 21},
													},
												},
												Value: &parser.YamlNode{
													Value: "No replicas for Example have been running for 5 minutes",
													Pos: diags.PositionRanges{
														{Line: 20, FirstColumn: 25, LastColumn: 79},
													},
												},
											},
										},
									},
								},
							},
							{
								Lines: diags.LineRange{First: 22, Last: 23},
								AlertingRule: &parser.AlertingRule{
									Alert: parser.YamlNode{
										Value: "Example_High_Restart_Rate",
										Pos: diags.PositionRanges{
											{Line: 22, FirstColumn: 20, LastColumn: 44},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: `sum(rate(kube_pod_container_status_restarts_total{namespace="example-app"}[5m])) > ( 3/60 )`,
											Pos: diags.PositionRanges{
												{Line: 23, FirstColumn: 19, LastColumn: 109},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`groups:
- name: "haproxy.api_server.rules"
  rules:
  - alert: HaproxyServerHealthcheckFailure
    expr: increase(haproxy_server_check_failures_total[15m]) > 100
    for: 5m
    labels:
      severity: 24x7
    annotations:
      summary: "HAProxy server healthcheck failure (instance {{ $labels.instance }})"
      description: "Some server healthcheck are failing on {{ $labels.server }}\n  VALUE = {{ $value }}\n  LABELS: {{ $labels }}"
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "haproxy.api_server.rules",
							Pos: diags.PositionRanges{
								{Line: 2, FirstColumn: 10, LastColumn: 33},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 4, Last: 11},
								AlertingRule: &parser.AlertingRule{
									Alert: parser.YamlNode{
										Value: "HaproxyServerHealthcheckFailure",
										Pos: diags.PositionRanges{
											{Line: 4, FirstColumn: 12, LastColumn: 42},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "increase(haproxy_server_check_failures_total[15m]) > 100",
											Pos: diags.PositionRanges{
												{Line: 5, FirstColumn: 11, LastColumn: 66},
											},
										},
									},
									For: &parser.YamlDuration{
										Value: time.Minute * 5,
										Raw:   "5m",
										Pos: diags.PositionRanges{
											{Line: 6, FirstColumn: 10, LastColumn: 11},
										},
									},
									Labels: &parser.YamlMap{
										Key: &parser.YamlNode{
											Value: "labels",
											Pos: diags.PositionRanges{
												{Line: 7, FirstColumn: 5, LastColumn: 10},
											},
										},
										Items: []*parser.YamlKeyValue{
											{
												Key: &parser.YamlNode{
													Value: "severity",
													Pos: diags.PositionRanges{
														{Line: 8, FirstColumn: 7, LastColumn: 14},
													},
												},
												Value: &parser.YamlNode{
													Value: "24x7",
													Pos: diags.PositionRanges{
														{Line: 8, FirstColumn: 17, LastColumn: 20},
													},
												},
											},
										},
									},
									Annotations: &parser.YamlMap{
										Key: &parser.YamlNode{
											Value: "annotations",
											Pos: diags.PositionRanges{
												{Line: 9, FirstColumn: 5, LastColumn: 15},
											},
										},
										Items: []*parser.YamlKeyValue{
											{
												Key: &parser.YamlNode{
													Value: "summary",
													Pos: diags.PositionRanges{
														{Line: 10, FirstColumn: 7, LastColumn: 13},
													},
												},
												Value: &parser.YamlNode{
													Value: "HAProxy server healthcheck failure (instance {{ $labels.instance }})",
													Pos: diags.PositionRanges{
														{Line: 10, FirstColumn: 17, LastColumn: 84},
													},
												},
											},
											{
												Key: &parser.YamlNode{
													Value: "description",
													Pos: diags.PositionRanges{
														{Line: 11, FirstColumn: 7, LastColumn: 17},
													},
												},
												Value: &parser.YamlNode{
													Value: "Some server healthcheck are failing on {{ $labels.server }}\n  VALUE = {{ $value }}\n  LABELS: {{ $labels }}",
													Pos: diags.PositionRanges{
														{Line: 11, FirstColumn: 21, LastColumn: 79},
													},
												},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`groups:
- name: certmanager
  rules:
  # pint disable before recordAnchor
  - &recordAnchor # pint disable recordAnchor
    record: name1 # pint disable name1
    expr: expr1 # pint disable expr1
    # pint disable after expr1
  - <<: *recordAnchor
    expr: expr2
  - <<: *recordAnchor
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "certmanager",
							Pos: diags.PositionRanges{
								{Line: 2, FirstColumn: 9, LastColumn: 19},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 6, Last: 7},
								Comments: []comments.Comment{
									{
										Type: comments.DisableType,
										Value: comments.Disable{
											Match: "before recordAnchor",
											Position: comments.Position{
												Offset: 15,
												Pos: diags.PositionRanges{
													{Line: 5, FirstColumn: 3, LastColumn: 36},
												},
											},
										},
									},
									{
										Type: comments.DisableType,
										Value: comments.Disable{
											Match: "recordAnchor",
											Position: comments.Position{
												Offset: 15,
												Pos: diags.PositionRanges{
													{Line: 6, FirstColumn: 1, LastColumn: 27},
												},
											},
										},
									},
									{
										Type: comments.DisableType,
										Value: comments.Disable{
											Match: "name1",
											Position: comments.Position{
												Offset: 15,
												Pos: diags.PositionRanges{
													{Line: 6, FirstColumn: 1, LastColumn: 20},
												},
											},
										},
									},
									{
										Type: comments.DisableType,
										Value: comments.Disable{
											Match: "after expr1",
											Position: comments.Position{
												Offset: 15,
												Pos: diags.PositionRanges{
													{Line: 8, FirstColumn: 5, LastColumn: 30},
												},
											},
										},
									},
									{
										Type: comments.DisableType,
										Value: comments.Disable{
											Match: "expr1",
											Position: comments.Position{
												Offset: 15,
												Pos: diags.PositionRanges{
													{Line: 7, FirstColumn: 1, LastColumn: 20},
												},
											},
										},
									},
								},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "name1",
										Pos: diags.PositionRanges{
											{Line: 6, FirstColumn: 13, LastColumn: 17},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "expr1",
											Pos: diags.PositionRanges{
												{Line: 7, FirstColumn: 11, LastColumn: 15},
											},
										},
									},
								},
							},
							{
								Comments: []comments.Comment{
									{
										Type: comments.DisableType,
										Value: comments.Disable{
											Match: "before recordAnchor",
											Position: comments.Position{
												Offset: 15,
												Pos: diags.PositionRanges{
													{Line: 5, FirstColumn: 3, LastColumn: 36},
												},
											},
										},
									},
									{
										Type: comments.DisableType,
										Value: comments.Disable{
											Match: "recordAnchor",
											Position: comments.Position{
												Offset: 15,
												Pos: diags.PositionRanges{
													{Line: 6, FirstColumn: 1, LastColumn: 27},
												},
											},
										},
									},
									{
										Type: comments.DisableType,
										Value: comments.Disable{
											Match: "name1",
											Position: comments.Position{
												Offset: 15,
												Pos: diags.PositionRanges{
													{Line: 6, FirstColumn: 1, LastColumn: 20},
												},
											},
										},
									},
								},
								Lines: diags.LineRange{First: 6, Last: 10},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "name1",
										Pos: diags.PositionRanges{
											{Line: 6, FirstColumn: 13, LastColumn: 17},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "expr2",
											Pos: diags.PositionRanges{
												{Line: 10, FirstColumn: 11, LastColumn: 15},
											},
										},
									},
								},
							},
							{
								Comments: []comments.Comment{
									{
										Type: comments.DisableType,
										Value: comments.Disable{
											Match: "before recordAnchor",
											Position: comments.Position{
												Offset: 15,
												Pos: diags.PositionRanges{
													{Line: 5, FirstColumn: 3, LastColumn: 36},
												},
											},
										},
									},
									{
										Type: comments.DisableType,
										Value: comments.Disable{
											Match: "recordAnchor",
											Position: comments.Position{
												Offset: 15,
												Pos: diags.PositionRanges{
													{Line: 6, FirstColumn: 1, LastColumn: 27},
												},
											},
										},
									},
									{
										Type: comments.DisableType,
										Value: comments.Disable{
											Match: "name1",
											Position: comments.Position{
												Offset: 15,
												Pos: diags.PositionRanges{
													{Line: 6, FirstColumn: 1, LastColumn: 20},
												},
											},
										},
									},
									{
										Type: comments.DisableType,
										Value: comments.Disable{
											Match: "after expr1",
											Position: comments.Position{
												Offset: 15,
												Pos: diags.PositionRanges{
													{Line: 8, FirstColumn: 5, LastColumn: 30},
												},
											},
										},
									},
									{
										Type: comments.DisableType,
										Value: comments.Disable{
											Match: "expr1",
											Position: comments.Position{
												Offset: 15,
												Pos: diags.PositionRanges{
													{Line: 7, FirstColumn: 1, LastColumn: 20},
												},
											},
										},
									},
								},
								Lines: diags.LineRange{First: 6, Last: 7},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "name1",
										Pos: diags.PositionRanges{
											{Line: 6, FirstColumn: 13, LastColumn: 17},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "expr1",
											Pos: diags.PositionRanges{
												{Line: 7, FirstColumn: 11, LastColumn: 15},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`groups:
- name: certmanager
  rules:
  - record: name1
    expr: expr1
    labels: &labelsAnchor
      label1: val1
      label2: val2
  - record: name2
    expr: expr2
    labels: *labelsAnchor
    # pint disable foot comment
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "certmanager",
							Pos: diags.PositionRanges{
								{Line: 2, FirstColumn: 9, LastColumn: 19},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 4, Last: 8},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "name1",
										Pos: diags.PositionRanges{
											{Line: 4, FirstColumn: 13, LastColumn: 17},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "expr1",
											Pos: diags.PositionRanges{
												{Line: 5, FirstColumn: 11, LastColumn: 15},
											},
										},
									},
									Labels: &parser.YamlMap{
										Key: &parser.YamlNode{
											Value: "labels",
											Pos: diags.PositionRanges{
												{Line: 6, FirstColumn: 5, LastColumn: 10},
											},
										},
										Items: []*parser.YamlKeyValue{
											{
												Key: &parser.YamlNode{
													Value: "label1",
													Pos: diags.PositionRanges{
														{Line: 7, FirstColumn: 7, LastColumn: 12},
													},
												},
												Value: &parser.YamlNode{
													Value: "val1",
													Pos: diags.PositionRanges{
														{Line: 7, FirstColumn: 15, LastColumn: 18},
													},
												},
											},
											{
												Key: &parser.YamlNode{
													Value: "label2",
													Pos: diags.PositionRanges{
														{Line: 8, FirstColumn: 7, LastColumn: 12},
													},
												},
												Value: &parser.YamlNode{
													Value: "val2",
													Pos: diags.PositionRanges{
														{Line: 8, FirstColumn: 15, LastColumn: 18},
													},
												},
											},
										},
									},
								},
							},
							{
								Comments: []comments.Comment{
									{
										Type: comments.DisableType,
										Value: comments.Disable{
											Match: "foot comment",
											Position: comments.Position{
												Offset: 15,
												Pos: diags.PositionRanges{
													{Line: 12, FirstColumn: 5, LastColumn: 31},
												},
											},
										},
									},
								},
								Lines: diags.LineRange{First: 9, Last: 11},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "name2",
										Pos: diags.PositionRanges{
											{Line: 9, FirstColumn: 13, LastColumn: 17},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "expr2",
											Pos: diags.PositionRanges{
												{Line: 10, FirstColumn: 11, LastColumn: 15},
											},
										},
									},
									Labels: &parser.YamlMap{
										Key: &parser.YamlNode{
											Value: "labels",
											Pos: diags.PositionRanges{
												{Line: 11, FirstColumn: 5, LastColumn: 10},
											},
										},
										Items: []*parser.YamlKeyValue{
											{
												Key: &parser.YamlNode{
													Value: "label1",
													Pos: diags.PositionRanges{
														{Line: 7, FirstColumn: 7, LastColumn: 12},
													},
												},
												Value: &parser.YamlNode{
													Value: "val1",
													Pos: diags.PositionRanges{
														{Line: 7, FirstColumn: 15, LastColumn: 18},
													},
												},
											},
											{
												Key: &parser.YamlNode{
													Value: "label2",
													Pos: diags.PositionRanges{
														{Line: 8, FirstColumn: 7, LastColumn: 12},
													},
												},
												Value: &parser.YamlNode{
													Value: "val2",
													Pos: diags.PositionRanges{
														{Line: 8, FirstColumn: 15, LastColumn: 18},
													},
												},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte("- alert:\n  expr: vector(1)\n"),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 1, Last: 2},
								Error: parser.ParseError{Err: errors.New("alert value cannot be empty"), Line: 1},
							},
						},
					},
				},
			},
		},
		{
			input: []byte("- alert: foo\n  expr:\n"),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 1, Last: 2},
								Error: parser.ParseError{Err: errors.New("expr value cannot be empty"), Line: 2},
							},
						},
					},
				},
			},
		},
		{
			input: []byte("- alert: foo\n"),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 1, Last: 1},
								Error: parser.ParseError{Err: errors.New("missing expr key"), Line: 1},
							},
						},
					},
				},
			},
		},
		{
			input: []byte("- record:\n  expr:\n"),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 1, Last: 2},
								Error: parser.ParseError{Err: errors.New("record value cannot be empty"), Line: 1},
							},
						},
					},
				},
			},
		},
		{
			input: []byte("- record: foo\n  expr:\n"),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 1, Last: 2},
								Error: parser.ParseError{Err: errors.New("expr value cannot be empty"), Line: 2},
							},
						},
					},
				},
			},
		},
		{
			input: []byte("- record: foo\n"),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 1, Last: 1},
								Error: parser.ParseError{Err: errors.New("missing expr key"), Line: 1},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(string(`
# pint file/owner bob
# pint ignore/begin
# pint ignore/end
# pint disable up

- record: foo
  expr: up

# pint file/owner alice

- record: foo
  expr: up

# pint ignore/next-line
`)),
			output: parser.File{
				IsRelaxed: true,
				Comments: []comments.Comment{
					{
						Type: comments.FileOwnerType,
						Value: comments.Owner{
							Name: "bob",
							Position: comments.Position{
								Offset: 18,
								Pos: diags.PositionRanges{
									{
										Line:        2,
										FirstColumn: 1,
										LastColumn:  21,
									},
								},
							},
						},
					},
					{
						Type: comments.FileOwnerType,
						Value: comments.Owner{
							Name: "alice",
							Position: comments.Position{
								Offset: 18,
								Pos: diags.PositionRanges{
									{
										Line:        10,
										FirstColumn: 1,
										LastColumn:  23,
									},
								},
							},
						},
					},
				},
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 7, Last: 8},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 7, FirstColumn: 11, LastColumn: 13},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "up",
											Pos: diags.PositionRanges{
												{Line: 8, FirstColumn: 9, LastColumn: 10},
											},
										},
									},
								},
							},
							{
								Lines: diags.LineRange{First: 12, Last: 13},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 12, FirstColumn: 11, LastColumn: 13},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "up",
											Pos: diags.PositionRanges{
												{Line: 13, FirstColumn: 9, LastColumn: 10},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(string(`
- alert: Template
  expr: &expr up == 0
  labels:
    notify: &maybe_escalate_notify chat-alerts
- alert: Service Down
  expr: *expr
  labels:
    notify: *maybe_escalate_notify
    summary: foo
`)),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 5},
								AlertingRule: &parser.AlertingRule{
									Alert: parser.YamlNode{
										Value: "Template",
										Pos: diags.PositionRanges{
											{Line: 2, FirstColumn: 10, LastColumn: 17},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "up == 0",
											Pos: diags.PositionRanges{
												{Line: 3, FirstColumn: 15, LastColumn: 21},
											},
										},
									},
									Labels: &parser.YamlMap{
										Key: &parser.YamlNode{
											Value: "labels",
											Pos: diags.PositionRanges{
												{Line: 4, FirstColumn: 3, LastColumn: 8},
											},
										},
										Items: []*parser.YamlKeyValue{
											{
												Key: &parser.YamlNode{
													Value: "notify",
													Pos: diags.PositionRanges{
														{Line: 5, FirstColumn: 5, LastColumn: 10},
													},
												},
												Value: &parser.YamlNode{
													Value: "chat-alerts",
													Pos: diags.PositionRanges{
														{Line: 5, FirstColumn: 22, LastColumn: 22},
														{Line: 5, FirstColumn: 37, LastColumn: 46},
													},
												},
											},
										},
									},
								},
							},
							{
								Lines: diags.LineRange{First: 6, Last: 10},
								AlertingRule: &parser.AlertingRule{
									Alert: parser.YamlNode{
										Value: "Service Down",
										Pos: diags.PositionRanges{
											{Line: 6, FirstColumn: 10, LastColumn: 21},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "up == 0",
											Pos: diags.PositionRanges{
												{Line: 7, FirstColumn: 10, LastColumn: 13}, // points at anchor
											},
										},
									},
									Labels: &parser.YamlMap{
										Key: &parser.YamlNode{
											Value: "labels",
											Pos: diags.PositionRanges{
												{Line: 8, FirstColumn: 3, LastColumn: 8},
											},
										},
										Items: []*parser.YamlKeyValue{
											{
												Key: &parser.YamlNode{
													Value: "notify",
													Pos: diags.PositionRanges{
														{Line: 9, FirstColumn: 5, LastColumn: 10},
													},
												},
												Value: &parser.YamlNode{
													Value: "chat-alerts",
													Pos: diags.PositionRanges{
														{Line: 9, FirstColumn: 14, LastColumn: 34},
													},
												},
											},
											{
												Key: &parser.YamlNode{
													Value: "summary",
													Pos: diags.PositionRanges{
														{Line: 10, FirstColumn: 5, LastColumn: 11},
													},
												},
												Value: &parser.YamlNode{
													Value: "foo",
													Pos: diags.PositionRanges{
														{Line: 10, FirstColumn: 14, LastColumn: 16},
													},
												},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			// YAML alias on record value is resolved correctly
			input: []byte(`
- record: &name foo
  expr: expr1
- record: *name
  expr: expr2
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 3},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 2, FirstColumn: 17, LastColumn: 19},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "expr1",
											Pos: diags.PositionRanges{
												{Line: 3, FirstColumn: 9, LastColumn: 13},
											},
										},
									},
								},
							},
							{
								Lines: diags.LineRange{First: 4, Last: 5},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 4, FirstColumn: 12, LastColumn: 15},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "expr2",
											Pos: diags.PositionRanges{
												{Line: 5, FirstColumn: 9, LastColumn: 13},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			// YAML alias on alert value is resolved correctly
			input: []byte(`
- alert: &aname MyAlert
  expr: up == 0
- alert: *aname
  expr: up == 1
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 3},
								AlertingRule: &parser.AlertingRule{
									Alert: parser.YamlNode{
										Value: "MyAlert",
										Pos: diags.PositionRanges{
											{Line: 2, FirstColumn: 17, LastColumn: 23},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "up == 0",
											Pos: diags.PositionRanges{
												{Line: 3, FirstColumn: 9, LastColumn: 15},
											},
										},
									},
								},
							},
							{
								Lines: diags.LineRange{First: 4, Last: 5},
								AlertingRule: &parser.AlertingRule{
									Alert: parser.YamlNode{
										Value: "MyAlert",
										Pos: diags.PositionRanges{
											{Line: 4, FirstColumn: 11, LastColumn: 15},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "up == 1",
											Pos: diags.PositionRanges{
												{Line: 5, FirstColumn: 9, LastColumn: 15},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			// YAML alias on for duration is resolved correctly
			input: []byte(`
- alert: alert1
  expr: up == 0
  for: &dur 5m
- alert: alert2
  expr: up == 1
  for: *dur
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 4},
								AlertingRule: &parser.AlertingRule{
									Alert: parser.YamlNode{
										Value: "alert1",
										Pos: diags.PositionRanges{
											{Line: 2, FirstColumn: 10, LastColumn: 15},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "up == 0",
											Pos: diags.PositionRanges{
												{Line: 3, FirstColumn: 9, LastColumn: 15},
											},
										},
									},
									For: &parser.YamlDuration{
										Raw:   "5m",
										Value: 5 * time.Minute,
										Pos: diags.PositionRanges{
											{Line: 4, FirstColumn: 13, LastColumn: 14},
										},
									},
								},
							},
							{
								Lines: diags.LineRange{First: 5, Last: 7},
								AlertingRule: &parser.AlertingRule{
									Alert: parser.YamlNode{
										Value: "alert2",
										Pos: diags.PositionRanges{
											{Line: 5, FirstColumn: 10, LastColumn: 15},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "up == 1",
											Pos: diags.PositionRanges{
												{Line: 6, FirstColumn: 9, LastColumn: 15},
											},
										},
									},
									For: &parser.YamlDuration{
										Raw:   "5m",
										Value: 5 * time.Minute,
										Pos: diags.PositionRanges{
											{Line: 7, FirstColumn: 9, LastColumn: 11},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			// YAML alias on keep_firing_for duration is resolved correctly
			input: []byte(`
- alert: alert1
  expr: up == 0
  keep_firing_for: &kff 10m
- alert: alert2
  expr: up == 1
  keep_firing_for: *kff
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 4},
								AlertingRule: &parser.AlertingRule{
									Alert: parser.YamlNode{
										Value: "alert1",
										Pos: diags.PositionRanges{
											{Line: 2, FirstColumn: 10, LastColumn: 15},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "up == 0",
											Pos: diags.PositionRanges{
												{Line: 3, FirstColumn: 9, LastColumn: 15},
											},
										},
									},
									KeepFiringFor: &parser.YamlDuration{
										Raw:   "10m",
										Value: 10 * time.Minute,
										Pos: diags.PositionRanges{
											{Line: 4, FirstColumn: 25, LastColumn: 27},
										},
									},
								},
							},
							{
								Lines: diags.LineRange{First: 5, Last: 7},
								AlertingRule: &parser.AlertingRule{
									Alert: parser.YamlNode{
										Value: "alert2",
										Pos: diags.PositionRanges{
											{Line: 5, FirstColumn: 10, LastColumn: 15},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "up == 1",
											Pos: diags.PositionRanges{
												{Line: 6, FirstColumn: 9, LastColumn: 15},
											},
										},
									},
									KeepFiringFor: &parser.YamlDuration{
										Raw:   "10m",
										Value: 10 * time.Minute,
										Pos: diags.PositionRanges{
											{Line: 7, FirstColumn: 21, LastColumn: 23},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			// YAML alias on annotations map is resolved correctly
			input: []byte(`
- alert: alert1
  expr: up == 0
  annotations: &ann
    summary: foo is down
- alert: alert2
  expr: up == 1
  annotations: *ann
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 5},
								AlertingRule: &parser.AlertingRule{
									Alert: parser.YamlNode{
										Value: "alert1",
										Pos: diags.PositionRanges{
											{Line: 2, FirstColumn: 10, LastColumn: 15},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "up == 0",
											Pos: diags.PositionRanges{
												{Line: 3, FirstColumn: 9, LastColumn: 15},
											},
										},
									},
									Annotations: &parser.YamlMap{
										Key: &parser.YamlNode{
											Value: "annotations",
											Pos: diags.PositionRanges{
												{Line: 4, FirstColumn: 3, LastColumn: 13},
											},
										},
										Items: []*parser.YamlKeyValue{
											{
												Key: &parser.YamlNode{
													Value: "summary",
													Pos: diags.PositionRanges{
														{Line: 5, FirstColumn: 5, LastColumn: 11},
													},
												},
												Value: &parser.YamlNode{
													Value: "foo is down",
													Pos: diags.PositionRanges{
														{Line: 5, FirstColumn: 14, LastColumn: 24},
													},
												},
											},
										},
									},
								},
							},
							{
								Lines: diags.LineRange{First: 6, Last: 8},
								AlertingRule: &parser.AlertingRule{
									Alert: parser.YamlNode{
										Value: "alert2",
										Pos: diags.PositionRanges{
											{Line: 6, FirstColumn: 10, LastColumn: 15},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "up == 1",
											Pos: diags.PositionRanges{
												{Line: 7, FirstColumn: 9, LastColumn: 15},
											},
										},
									},
									Annotations: &parser.YamlMap{
										Key: &parser.YamlNode{
											Value: "annotations",
											Pos: diags.PositionRanges{
												{Line: 8, FirstColumn: 3, LastColumn: 13},
											},
										},
										Items: []*parser.YamlKeyValue{
											{
												Key: &parser.YamlNode{
													Value: "summary",
													Pos: diags.PositionRanges{
														{Line: 5, FirstColumn: 5, LastColumn: 11},
													},
												},
												Value: &parser.YamlNode{
													Value: "foo is down",
													Pos: diags.PositionRanges{
														{Line: 5, FirstColumn: 14, LastColumn: 24},
													},
												},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			// Merge key merges default alert options into alert rules
			input: []byte(`
- &defaults
  for: 5m
  labels:
    severity: warning
- alert: foo
  expr: up == 0
  <<: *defaults
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 3, Last: 7},
								AlertingRule: &parser.AlertingRule{
									Alert: parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 6, FirstColumn: 10, LastColumn: 12},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "up == 0",
											Pos: diags.PositionRanges{
												{Line: 7, FirstColumn: 9, LastColumn: 15},
											},
										},
									},
									For: &parser.YamlDuration{
										Raw:   "5m",
										Value: 5 * time.Minute,
										Pos: diags.PositionRanges{
											{Line: 3, FirstColumn: 8, LastColumn: 9},
										},
									},
									Labels: &parser.YamlMap{
										Key: &parser.YamlNode{
											Value: "labels",
											Pos: diags.PositionRanges{
												{Line: 4, FirstColumn: 3, LastColumn: 8},
											},
										},
										Items: []*parser.YamlKeyValue{
											{
												Key: &parser.YamlNode{
													Value: "severity",
													Pos: diags.PositionRanges{
														{Line: 5, FirstColumn: 5, LastColumn: 12},
													},
												},
												Value: &parser.YamlNode{
													Value: "warning",
													Pos: diags.PositionRanges{
														{Line: 5, FirstColumn: 15, LastColumn: 21},
													},
												},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			// Merge key with local override - local for value takes precedence over merged for
			input: []byte(`
- &defaults
  for: 5m
  labels:
    severity: warning
- alert: foo
  expr: up == 0
  for: 10m
  <<: *defaults
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 4, Last: 8},
								AlertingRule: &parser.AlertingRule{
									Alert: parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 6, FirstColumn: 10, LastColumn: 12},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "up == 0",
											Pos: diags.PositionRanges{
												{Line: 7, FirstColumn: 9, LastColumn: 15},
											},
										},
									},
									For: &parser.YamlDuration{
										Raw:   "10m",
										Value: 10 * time.Minute,
										Pos: diags.PositionRanges{
											{Line: 8, FirstColumn: 8, LastColumn: 10},
										},
									},
									Labels: &parser.YamlMap{
										Key: &parser.YamlNode{
											Value: "labels",
											Pos: diags.PositionRanges{
												{Line: 4, FirstColumn: 3, LastColumn: 8},
											},
										},
										Items: []*parser.YamlKeyValue{
											{
												Key: &parser.YamlNode{
													Value: "severity",
													Pos: diags.PositionRanges{
														{Line: 5, FirstColumn: 5, LastColumn: 12},
													},
												},
												Value: &parser.YamlNode{
													Value: "warning",
													Pos: diags.PositionRanges{
														{Line: 5, FirstColumn: 15, LastColumn: 21},
													},
												},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			// Sequence merge key <<: [*a, *b] merges defaults from both aliases
			input: []byte(`
- &defaults_a
  for: 5m
- &defaults_b
  labels:
    severity: warning
- alert: foo
  expr: up == 0
  <<: [*defaults_a, *defaults_b]
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 3, Last: 8},
								AlertingRule: &parser.AlertingRule{
									Alert: parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 7, FirstColumn: 10, LastColumn: 12},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "up == 0",
											Pos: diags.PositionRanges{
												{Line: 8, FirstColumn: 9, LastColumn: 15},
											},
										},
									},
									For: &parser.YamlDuration{
										Raw:   "5m",
										Value: 5 * time.Minute,
										Pos: diags.PositionRanges{
											{Line: 3, FirstColumn: 8, LastColumn: 9},
										},
									},
									Labels: &parser.YamlMap{
										Key: &parser.YamlNode{
											Value: "labels",
											Pos: diags.PositionRanges{
												{Line: 5, FirstColumn: 3, LastColumn: 8},
											},
										},
										Items: []*parser.YamlKeyValue{
											{
												Key: &parser.YamlNode{
													Value: "severity",
													Pos: diags.PositionRanges{
														{Line: 6, FirstColumn: 5, LastColumn: 12},
													},
												},
												Value: &parser.YamlNode{
													Value: "warning",
													Pos: diags.PositionRanges{
														{Line: 6, FirstColumn: 15, LastColumn: 21},
													},
												},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- record: invalid metric name
  expr: bar
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 3},
								Error: parser.ParseError{Err: errors.New("invalid recording rule name: invalid metric name"), Line: 2},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- record: utf-8 enabled name
  expr: bar
  labels:
    "a b c": bar
`),
			names: model.UTF8Validation,
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 5},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "utf-8 enabled name",
										Pos: diags.PositionRanges{
											{Line: 2, FirstColumn: 11, LastColumn: 28},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "bar",
											Pos: diags.PositionRanges{
												{Line: 3, FirstColumn: 9, LastColumn: 11},
											},
										},
									},
									Labels: &parser.YamlMap{
										Key: &parser.YamlNode{
											Value: "labels",
											Pos: diags.PositionRanges{
												{Line: 4, FirstColumn: 3, LastColumn: 8},
											},
										},
										Items: []*parser.YamlKeyValue{
											{
												Key: &parser.YamlNode{
													Value: "a b c",
													Pos: diags.PositionRanges{
														{Line: 5, FirstColumn: 6, LastColumn: 10},
													},
												},
												Value: &parser.YamlNode{
													Value: "bar",
													Pos: diags.PositionRanges{
														{Line: 5, FirstColumn: 14, LastColumn: 16},
													},
												},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- record: foo
  expr: bar
  labels:
    "foo bar": yes
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 5},
								Error: parser.ParseError{Err: errors.New("invalid label name: foo bar"), Line: 5},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- alert: foo
  expr: bar
  labels:
    "foo bar": yes
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 5},
								Error: parser.ParseError{Err: errors.New("invalid label name: foo bar"), Line: 5},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- alert: foo
  expr: bar
  labels:
    "{{ $value }}": yes
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 5},
								Error: parser.ParseError{Err: errors.New("invalid label name: {{ $value }}"), Line: 5},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- alert: foo
  expr: bar
  annotations:
    "foo bar": yes
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 5},
								Error: parser.ParseError{Err: errors.New("invalid annotation name: foo bar"), Line: 5},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- alert: foo
  expr: bar
  labels:
    foo: ` + string("\xed\xbf\xbf")),
			// Label values are invalid only if they aren't valid UTF-8 strings
			// which also makes them unparsable by YAML.
			output: parser.File{
				IsRelaxed: true,
				Error: parser.ParseError{
					Err:  errors.New("yaml: invalid Unicode character"),
					Line: 1,
				},
			},
		},
		{
			input: []byte(`
- alert: foo
  expr: bar
  annotations:
    "{{ $value }}": yes
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 5},
								Error: parser.ParseError{Err: errors.New("invalid annotation name: {{ $value }}"), Line: 5},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- record: foo
  expr: bar
  keep_firing_for: 5m
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 4},
								Error: parser.ParseError{Err: errors.New("invalid field 'keep_firing_for' in recording rule"), Line: 4},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- record: foo
  expr: bar
  for: 5m
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 4},
								Error: parser.ParseError{Err: errors.New("invalid field 'for' in recording rule"), Line: 4},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- record: foo
  expr: bar
  annotations:
    foo: bar
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 5},
								Error: parser.ParseError{Err: errors.New("invalid field 'annotations' in recording rule"), Line: 4},
							},
						},
					},
				},
			},
		},
		// Tag tests
		{
			input: []byte(`
- record: 5
  expr: bar
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 3},
								Error: parser.ParseError{Err: errors.New("record value must be a string, got integer instead"), Line: 2},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- alert: 5
  expr: bar
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 3},
								Error: parser.ParseError{Err: errors.New("alert value must be a string, got integer instead"), Line: 2},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- record: foo
  expr: 5
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 3},
								Error: parser.ParseError{Err: errors.New("expr value must be a string, got integer instead"), Line: 3},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- alert: foo
  expr: bar
  for: 5
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 4},
								Error: parser.ParseError{Err: errors.New("for value must be a string, got integer instead"), Line: 4},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- alert: foo
  expr: bar
  keep_firing_for: 5
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 4},
								Error: parser.ParseError{Err: errors.New("keep_firing_for value must be a string, got integer instead"), Line: 4},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- record: foo
  expr: bar
  labels: []
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 4},
								Error: parser.ParseError{Err: errors.New("labels value must be a mapping, got list instead"), Line: 4},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- alert: foo
  expr: bar
  labels: {}
  annotations: []
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 5},
								Error: parser.ParseError{Err: errors.New("annotations value must be a mapping, got list instead"), Line: 5},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- alert: foo
  expr: bar
  labels:
    foo: 3
  annotations:
    bar: "5"
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 7},
								Error: parser.ParseError{Err: errors.New("labels foo value must be a string, got integer instead"), Line: 5},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- alert: foo
  expr: bar
  labels: {}
  annotations:
    foo: "3"
    bar: 5
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 7},
								Error: parser.ParseError{Err: errors.New("annotations bar value must be a string, got integer instead"), Line: 7},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- record: foo
  expr: bar
  labels: 4
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 4},
								Error: parser.ParseError{Err: errors.New("labels value must be a mapping, got integer instead"), Line: 4},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- record: foo
  expr: bar
  labels: true
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 4},
								Error: parser.ParseError{Err: errors.New("labels value must be a mapping, got bool instead"), Line: 4},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- record: foo
  expr: bar
  labels: null
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 4},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 2, FirstColumn: 11, LastColumn: 13},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "bar",
											Pos: diags.PositionRanges{
												{Line: 3, FirstColumn: 9, LastColumn: 11},
											},
										},
									},
									Labels: &parser.YamlMap{
										Key: &parser.YamlNode{
											Value: "labels",
											Pos: diags.PositionRanges{
												{Line: 4, FirstColumn: 3, LastColumn: 8},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- record: true
  expr: bar
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 3},
								Error: parser.ParseError{Err: errors.New("record value must be a string, got bool instead"), Line: 2},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- record:
    query: foo
  expr: bar
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 4},
								Error: parser.ParseError{Err: errors.New("record value must be a string, got mapping instead"), Line: 3},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- record: foo
  expr: bar
  labels: some
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 4},
								Error: parser.ParseError{Err: errors.New("labels value must be a mapping, got string instead"), Line: 4},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- record: foo
  expr: bar
  labels: !!binary "SGVsbG8sIFdvcmxkIQ=="
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 4},
								Error: parser.ParseError{
									Err:  errors.New("labels value must be a mapping, got binary data instead"),
									Line: 4,
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- alert: foo
  expr: bar
  for: 1.23
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 4},
								Error: parser.ParseError{
									Err:  errors.New("for value must be a string, got float instead"),
									Line: 4,
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- record: foo
  expr: bar
  labels: !!garbage "SGVsbG8sIFdvcmxkIQ=="
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 4},
								Error: parser.ParseError{Err: errors.New("labels value must be a mapping, got garbage instead"), Line: 4},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- record: foo
  expr: bar
  labels: !! "SGVsbG8sIFdvcmxkIQ=="
`),
			output: parser.File{
				IsRelaxed: true,
				Error: parser.ParseError{
					Err:  errors.New("did not find expected tag URI"),
					Line: 4,
				},
			},
		},
		{
			input: []byte(`
- record: &foo foo
  expr: bar
  labels: *foo
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 4},
								Error: parser.ParseError{
									Err:  errors.New("labels value must be a mapping, got string instead"),
									Line: 4,
								},
							},
						},
					},
				},
			},
		},
		// Multi-document tests
		{
			input: []byte(`---
- expr: foo
  record: foo
---
- expr: bar
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 3},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 3, FirstColumn: 11, LastColumn: 13},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "foo",
											Pos: diags.PositionRanges{
												{Line: 2, FirstColumn: 9, LastColumn: 11},
											},
										},
									},
								},
							},
						},
					},
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 5, Last: 5},
								Error: parser.ParseError{
									Err:  errors.New("incomplete rule, no alert or record key"),
									Line: 5,
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`---
- expr: foo
  record: foo
---
- expr: bar
  record: bar
  expr: bar
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 3},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 3, FirstColumn: 11, LastColumn: 13},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "foo",
											Pos: diags.PositionRanges{
												{Line: 2, FirstColumn: 9, LastColumn: 11},
											},
										},
									},
								},
							},
						},
					},
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 5, Last: 7},
								Error: parser.ParseError{Err: errors.New("duplicated expr key"), Line: 7},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`---
- expr: foo
  record: foo
---
- expr: bar
  alert: foo
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 3},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 3, FirstColumn: 11, LastColumn: 13},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "foo",
											Pos: diags.PositionRanges{
												{Line: 2, FirstColumn: 9, LastColumn: 11},
											},
										},
									},
								},
							},
						},
					},
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 5, Last: 6},
								AlertingRule: &parser.AlertingRule{
									Alert: parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 6, FirstColumn: 10, LastColumn: 12},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "bar",
											Pos: diags.PositionRanges{
												{Line: 5, FirstColumn: 9, LastColumn: 11},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`---
groups:
- name: v1
  rules:
  - record: up:count
    expr: count(up)
    labels:
      foo:
        bar: foo
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "v1",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 10},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 5, Last: 9},
								Error: parser.ParseError{
									Err:  errors.New("labels foo value must be a string, got mapping instead"),
									Line: 9,
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: v1
  rules:
  - record: up:count
    expr: count(up)
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "v1",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 10},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 5, Last: 6},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "up:count",
										Pos: diags.PositionRanges{
											{Line: 5, FirstColumn: 13, LastColumn: 20},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "count(up)",
											Pos: diags.PositionRanges{
												{Line: 6, FirstColumn: 11, LastColumn: 19},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: v1
  rules:
  - record: up:count
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "v1",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 10},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 5, Last: 5},
								Error: parser.ParseError{
									Err:  errors.New("missing expr key"),
									Line: 5,
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- record: up:count
  expr: count(up)
`),
			strict: true,
			output: parser.File{
				Error: parser.ParseError{
					Err:  errors.New("top level field must be a groups key, got list"),
					Line: 2,
				},
			},
		},
		{
			input: []byte(`
rules:
  - record: up:count
    expr: count(up)
`),
			strict: true,
			output: parser.File{
				Error: parser.ParseError{
					Err:  errors.New("unexpected key rules"),
					Line: 2,
				},
			},
		},
		{
			input: []byte(`
groups:
  - record: up:count
    expr: count(up)
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Error: parser.ParseError{
							Err:  errors.New("invalid group key record"),
							Line: 3,
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- rules:
  - record: up:count
    expr: count(up)
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Error: parser.ParseError{
							Err:  errors.New("incomplete group definition, name is required and must be set"),
							Line: 3,
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 4, Last: 5},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "up:count",
										Pos: diags.PositionRanges{
											{Line: 4, FirstColumn: 13, LastColumn: 20},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "count(up)",
											Pos: diags.PositionRanges{
												{Line: 5, FirstColumn: 11, LastColumn: 19},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: foo
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "foo",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups: {}
`),
			strict: true,
			output: parser.File{
				Error: parser.ParseError{
					Err:  errors.New("groups value must be a list, got mapping"),
					Line: 2,
				},
			},
		},
		{
			input: []byte(`
groups:
- name: []
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Error: parser.ParseError{
							Err:  errors.New("group name must be a string, got list"),
							Line: 3,
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: foo
  name: bar
  name: bob
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "bar",
							Pos: diags.PositionRanges{
								{Line: 4, FirstColumn: 9, LastColumn: 11},
							},
						},
						Error: parser.ParseError{
							Err:  errors.New("duplicated key name"),
							Line: 4,
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: v1
  rules:
    rules:
      - record: up:count
        expr: count(up)
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "v1",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 10},
							},
						},
						Error: parser.ParseError{
							Err:  errors.New("rules must be a list, got mapping"),
							Line: 4,
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: v1
  rules:
    - rules:
      - record: up:count
        expr: count(up)
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "v1",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 10},
							},
						},
						Rules: []parser.Rule{
							{
								Error: parser.ParseError{
									Err:  errors.New("invalid rule key rules"),
									Line: 5,
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: v1
  rules:
    - rules:
      - record: up:count
		expr: count(up)
`),
			strict: true,
			output: parser.File{
				Error: parser.ParseError{
					Err:  errors.New("found a tab character that violates indentation"),
					Line: 6,
				},
			},
		},
		{
			input: []byte(`
---
groups:
- name: v1
  rules:
    - record: up:count
      expr: count(up)
---
groups:
- name: v1
  rules:
    - rules:
      - record: up:count
        expr: count(up)
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "v1",
							Pos: diags.PositionRanges{
								{Line: 4, FirstColumn: 9, LastColumn: 10},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 6, Last: 7},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "up:count",
										Pos: diags.PositionRanges{
											{Line: 6, FirstColumn: 15, LastColumn: 22},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "count(up)",
											Pos: diags.PositionRanges{
												{Line: 7, FirstColumn: 13, LastColumn: 21},
											},
										},
									},
								},
							},
						},
					},
					{
						Name: parser.YamlNode{
							Value: "v1",
							Pos: diags.PositionRanges{
								{Line: 10, FirstColumn: 9, LastColumn: 10},
							},
						},
						Rules: []parser.Rule{
							{
								Error: parser.ParseError{
									Err:  errors.New("invalid rule key rules"),
									Line: 12,
								},
							},
						},
					},
				},
				Error: parser.ParseError{
					Line: 8,
					Err:  errors.New("multi-document YAML files are not allowed"),
				},
			},
		},
		{
			input: []byte(`
---
groups: []
---
groups:
- name: foo
  rules:
    - labels: !!binary "SGVsbG8sIFdvcmxkIQ=="
      record: foo
      expr: foo
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "foo",
							Pos: diags.PositionRanges{
								{Line: 6, FirstColumn: 9, LastColumn: 11},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 8, Last: 10},
								Error: parser.ParseError{
									Line: 8,
									Err:  errors.New("labels value must be a mapping, got binary data instead"),
								},
							},
						},
					},
				},
				Error: parser.ParseError{
					Line: 4,
					Err:  errors.New("multi-document YAML files are not allowed"),
				},
			},
		},
		{
			input:  []byte("[]"),
			strict: true,
			output: parser.File{
				Error: parser.ParseError{
					Err:  errors.New("top level field must be a groups key, got list"),
					Line: 1,
				},
			},
		},
		{
			input:  []byte("\n\n[]"),
			strict: true,
			output: parser.File{
				Error: parser.ParseError{
					Err:  errors.New("top level field must be a groups key, got list"),
					Line: 3,
				},
			},
		},
		{
			input:  []byte("groups: {}"),
			strict: true,
			output: parser.File{
				Error: parser.ParseError{
					Err:  errors.New("groups value must be a list, got mapping"),
					Line: 1,
				},
			},
		},
		{
			input:  []byte("groups: []"),
			strict: true,
		},
		{
			input:  []byte("xgroups: {}"),
			strict: true,
			output: parser.File{
				Error: parser.ParseError{
					Err:  errors.New("unexpected key xgroups"),
					Line: 1,
				},
			},
		},
		{
			input:  []byte("\nbob\n"),
			strict: true,
			output: parser.File{
				Error: parser.ParseError{
					Err:  errors.New("top level field must be a groups key, got string"),
					Line: 2,
				},
			},
		},
		{
			input: []byte(`groups: []

rules: []
`),
			strict: true,
			output: parser.File{
				Error: parser.ParseError{
					Err:  errors.New("unexpected key rules"),
					Line: 3,
				},
			},
		},
		{
			input: []byte(`
groups:
- name: foo
  rules: []
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "foo",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: 
  rules: []
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Error: parser.ParseError{
							Err:  errors.New("group name must be a string, got null"),
							Line: 3,
						},
					},
				},
			},
		},
		{
			// YAML alias on group name in strict mode is resolved correctly
			input: []byte(`
groups:
- name: &gname mygroup
  rules:
  - record: foo
    expr: sum(up)
- name: *gname
  rules:
  - record: bar
    expr: sum(down)
`),
			strict: true,
			output: parser.File{
				Error: parser.ParseError{
					Err:  errors.New("duplicated group name"),
					Line: 7,
				},
			},
		},
		{
			// YAML alias on group interval in strict mode is resolved correctly
			input: []byte(`
groups:
- name: mygroup
  interval: &ivl 1m
  rules:
  - record: foo
    expr: sum(up)
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "mygroup",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 15},
							},
						},
						Interval: &parser.YamlDuration{
							Raw:   "1m",
							Value: 1 * time.Minute,
							Pos: diags.PositionRanges{
								{Line: 4, FirstColumn: 18, LastColumn: 19},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 6, Last: 7},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 6, FirstColumn: 13, LastColumn: 15},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "sum(up)",
											Pos: diags.PositionRanges{
												{Line: 7, FirstColumn: 11, LastColumn: 17},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: foo
  rules:
  - record: foo
    expr: sum(up)
    labels:
      job: foo
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "foo",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 5, Last: 8},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 5, FirstColumn: 13, LastColumn: 15},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "sum(up)",
											Pos: diags.PositionRanges{
												{Line: 6, FirstColumn: 11, LastColumn: 17},
											},
										},
									},
									Labels: &parser.YamlMap{
										Key: &parser.YamlNode{
											Value: "labels",
											Pos: diags.PositionRanges{
												{Line: 7, FirstColumn: 5, LastColumn: 10},
											},
										},
										Items: []*parser.YamlKeyValue{
											{
												Key: &parser.YamlNode{
													Value: "job",
													Pos: diags.PositionRanges{
														{Line: 8, FirstColumn: 7, LastColumn: 9},
													},
												},
												Value: &parser.YamlNode{
													Value: "foo",
													Pos: diags.PositionRanges{
														{Line: 8, FirstColumn: 12, LastColumn: 14},
													},
												},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: foo
  rules:
  - record: foo
    expr: sum(up)
    xxx: 1
    labels:
      job: foo
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "foo",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Rules: []parser.Rule{
							{
								Error: parser.ParseError{
									Err:  errors.New("invalid rule key xxx"),
									Line: 7,
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: foo
  rules:
    record: foo
    expr: sum(up)
    xxx: 1
    labels:
      job: foo
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "foo",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Error: parser.ParseError{
							Err:  errors.New("rules must be a list, got mapping"),
							Line: 4,
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: foo
  rules:
  - record: foo
    expr: sum(up)
    labels:
      job:
        foo: bar
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "foo",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 5, Last: 9},
								Error: parser.ParseError{
									Line: 9,
									Err:  errors.New("labels job value must be a string, got mapping instead"),
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: foo
  rules:
  - record: foo
    expr:
      sum: sum(up)
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "foo",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 5, Last: 7},
								Error: parser.ParseError{
									Line: 7,
									Err:  errors.New("expr value must be a string, got mapping instead"),
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: foo
  rules: []
- name: foo
  rules: []
`),
			strict: true,
			output: parser.File{
				Error: parser.ParseError{
					Err:  errors.New("duplicated group name"),
					Line: 5,
				},
			},
		},
		{
			input: []byte(`
groups:
- name: foo
  rules:
  - record: foo
    expr: sum(up)
    labels:
      foo: bob
      foo: bar
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "foo",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 8, Last: 9},
								Error: parser.ParseError{
									Line: 9,
									Err:  errors.New("duplicated labels key foo"),
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: v2
  rules:
  - record: up:count
    expr: count(up)
    expr: sum(up)`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "v2",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 10},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 5, Last: 7},
								Error: parser.ParseError{
									Line: 7,
									Err:  errors.New("duplicated expr key"),
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: v2
  rules:
  - record: up:count
    expr: count(up)
bogus: 1
`),
			strict: true,
			output: parser.File{
				Error: parser.ParseError{
					Err:  errors.New("unexpected key bogus"),
					Line: 7,
				},
			},
		},
		{
			input: []byte(`
groups:
- name: v2
  rules:
  - record: up:count
    expr: count(up)
    bogus: 1
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "v2",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 10},
							},
						},
						Rules: []parser.Rule{
							{
								Error: parser.ParseError{
									Err:  errors.New("invalid rule key bogus"),
									Line: 7,
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: v2
  rules:
  - alert: up:count
    for: 5m
    keep_firing_for: 5m
    expr: count(up)
    labels: {}
    annotations: {}
    bogus: 1
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "v2",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 10},
							},
						},
						Rules: []parser.Rule{
							{
								Error: parser.ParseError{
									Err:  errors.New("invalid rule key bogus"),
									Line: 11,
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:

- name: CloudflareKafkaZookeeperExporter

  rules:
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "CloudflareKafkaZookeeperExporter",
							Pos: diags.PositionRanges{
								{Line: 4, FirstColumn: 9, LastColumn: 40},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: foo
  rules:
    expr: 1
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "foo",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Error: parser.ParseError{
							Err:  errors.New("rules must be a list, got mapping"),
							Line: 4,
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: foo
  rules:
    - expr: 1
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "foo",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 5, Last: 5},
								Error: parser.ParseError{
									Line: 5,
									Err:  errors.New("incomplete rule, no alert or record key"),
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: foo
  rules:
    - expr: null
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "foo",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 5, Last: 5},
								Error: parser.ParseError{
									Line: 5,
									Err:  errors.New("incomplete rule, no alert or record key"),
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: foo
  rules:
    - 1: null
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "foo",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Rules: []parser.Rule{
							{
								Error: parser.ParseError{
									Err:  errors.New("invalid rule key 1"),
									Line: 5,
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: foo
  rules:
    - true: !!binary "SGVsbG8sIFdvcmxkIQ=="
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "foo",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Rules: []parser.Rule{
							{
								Error: parser.ParseError{
									Err:  errors.New("invalid rule key true"),
									Line: 5,
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: foo
  rules:
    - expr: !!binary "SGVsbG8sIFdvcmxkIQ=="
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "foo",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 5, Last: 5},
								Error: parser.ParseError{
									Line: 5,
									Err:  errors.New("incomplete rule, no alert or record key"),
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: foo
  rules:
    - expr: !!binary "SGVsbG8sIFdvcmxkIQ=="
      record: foo
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "foo",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 5, Last: 6},
								Error: parser.ParseError{
									Line: 5,
									Err:  errors.New("expr value must be a string, got binary data instead"),
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: foo
  rules:
    - labels: !!binary "SGVsbG8sIFdvcmxkIQ=="
      record: foo
      expr: foo
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "foo",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 5, Last: 7},
								Error: parser.ParseError{
									Line: 5,
									Err:  errors.New("labels value must be a mapping, got binary data instead"),
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
---
groups:
- name: foo
  rules:
    - record: foo
      expr: bar
---
groups:
- name: foo
  rules:
    - record: foo
      expr: bar
`),
			strict: true,
			output: parser.File{
				Error: parser.ParseError{
					Line: 8,
					Err:  errors.New("multi-document YAML files are not allowed"),
				},
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "foo",
							Pos: diags.PositionRanges{
								{Line: 4, FirstColumn: 9, LastColumn: 11},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 6, Last: 7},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 6, FirstColumn: 15, LastColumn: 17},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "bar",
											Pos: diags.PositionRanges{
												{Line: 7, FirstColumn: 13, LastColumn: 15},
											},
										},
									},
								},
							},
						},
					},
					{
						Name: parser.YamlNode{
							Value: "foo",
							Pos: diags.PositionRanges{
								{Line: 10, FirstColumn: 9, LastColumn: 11},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 12, Last: 13},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 12, FirstColumn: 15, LastColumn: 17},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "bar",
											Pos: diags.PositionRanges{
												{Line: 13, FirstColumn: 13, LastColumn: 15},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: foo
  rules:
    - record: foo
      expr: foo
      expr: foo
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "foo",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 5, Last: 7},
								Error: parser.ParseError{
									Line: 7,
									Err:  errors.New("duplicated expr key"),
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: foo
  rules:
    - record: foo
      keep_firing_for: 1m
      keep_firing_for: 2m
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "foo",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 5, Last: 7},
								Error: parser.ParseError{
									Line: 7,
									Err:  errors.New("duplicated keep_firing_for key"),
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: foo
  rules:
    - record: foo
      keep_firing_for: 1m
      record: 2m
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "foo",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 5, Last: 7},
								Error: parser.ParseError{
									Line: 7,
									Err:  errors.New("duplicated record key"),
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: foo
  rules:
    - []
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "foo",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Rules: []parser.Rule{
							{
								Error: parser.ParseError{
									Err:  errors.New("rule definition must be a mapping, got list"),
									Line: 5,
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
1: 0
`),
			strict: true,
			output: parser.File{
				Error: parser.ParseError{
					Err:  errors.New("groups key must be a string, got a integer"),
					Line: 2,
				},
			},
		},
		{
			input: []byte(`
true: 0
`),
			strict: true,
			output: parser.File{
				Error: parser.ParseError{
					Err:  errors.New("groups key must be a string, got a bool"),
					Line: 2,
				},
			},
		},
		{
			input: []byte(`
groups: !!binary "SGVsbG8sIFdvcmxkIQ=="
`),
			strict: true,
			output: parser.File{
				Error: parser.ParseError{
					Err:  errors.New("groups value must be a list, got binary data"),
					Line: 2,
				},
			},
		},
		{
			input: []byte(`
groups:
  - true: null"
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Error: parser.ParseError{
							Err:  errors.New("invalid group key true"),
							Line: 3,
						},
					},
				},
			},
		},
		{
			input: []byte(`
!!!binary "groups": true"
`),
			strict: true,
			output: parser.File{
				Error: parser.ParseError{
					Err:  errors.New("groups key must be a string, got a binary"),
					Line: 2,
				},
			},
		},
		{
			input:  []byte("[]"),
			strict: true,
			output: parser.File{
				Error: parser.ParseError{
					Err:  errors.New("top level field must be a groups key, got list"),
					Line: 1,
				},
			},
		},
		{
			input: []byte(`
groups:
  - true
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Error: parser.ParseError{
							Err:  errors.New("group must be a mapping, got bool"),
							Line: 3,
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
  - name:
    rules: []
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Error: parser.ParseError{
							Err:  errors.New("group name must be a string, got null"),
							Line: 3,
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
  - name: ""
    rules: []
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Error: parser.ParseError{
							Err:  errors.New("group name cannot be empty"),
							Line: 3,
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
  - name: 1
    rules: []
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Error: parser.ParseError{
							Err:  errors.New("group name must be a string, got integer"),
							Line: 3,
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
  - name: foo
    interval: 1
    rules: []
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "foo",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 11, LastColumn: 13},
							},
						},
						Error: parser.ParseError{
							Err:  errors.New("group interval must be a string, got integer"),
							Line: 4,
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
  - name: foo
    interval: xxx
    rules: []
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "foo",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 11, LastColumn: 13},
							},
						},
						Interval: &parser.YamlDuration{
							ParseError: errors.New(`not a valid duration string: "xxx"`),
							Raw:        "xxx",
							Pos: diags.PositionRanges{
								{Line: 4, FirstColumn: 15, LastColumn: 17},
							},
						},
						Error: parser.ParseError{
							Err:  errors.New("invalid interval value: not a valid duration string: \"xxx\""),
							Line: 4,
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
  - name: foo
    query_offset: 1
    rules: []
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "foo",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 11, LastColumn: 13},
							},
						},
						Error: parser.ParseError{
							Err:  errors.New("group query_offset must be a string, got integer"),
							Line: 4,
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
  - name: foo
    query_offset: xxx
    rules: []
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "foo",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 11, LastColumn: 13},
							},
						},
						QueryOffset: &parser.YamlDuration{
							ParseError: errors.New(`not a valid duration string: "xxx"`),
							Raw:        "xxx",
							Pos: diags.PositionRanges{
								{Line: 4, FirstColumn: 19, LastColumn: 21},
							},
						},
						Error: parser.ParseError{
							Err:  errors.New("invalid query_offset value: not a valid duration string: \"xxx\""),
							Line: 4,
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
  - name: foo
    query_offset: 1m
    limit: abc
    rules: []
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "foo",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 11, LastColumn: 13},
							},
						},
						QueryOffset: &parser.YamlDuration{
							Value: time.Minute,
							Raw:   "1m",
							Pos: diags.PositionRanges{
								{Line: 4, FirstColumn: 19, LastColumn: 20},
							},
						},
						Error: parser.ParseError{
							Err:  errors.New("group limit must be a integer, got string"),
							Line: 5,
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: v2
  rules:
  - alert: up:count
    for: 5m &timeout
    keep_firing_for: **timeout
    expr: count(up)
    labels: {}
    annotations: {}
    bogus: 1
`),
			strict: true,
			output: parser.File{
				Error: parser.ParseError{
					Err:  errors.New("did not find expected alphabetic or numeric character"),
					Line: 7,
				},
			},
		},
		{
			// Non-strict parser accepts any scalar value for limit and records
			// the strconv error on the YamlInt itself.
			input: []byte(`
groups:
- name: foo
  limit: abc
  rules: []
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "foo",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Limit: &parser.YamlInt{
							ParseError: errors.New(`strconv.Atoi: parsing "abc": invalid syntax`),
							Raw:        "abc",
							Pos: diags.PositionRanges{
								{Line: 4, FirstColumn: 10, LastColumn: 12},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: v2
  rules:
  - alert: up:count
    for: &for 1
    keep_firing_for: *for
    expr: count(up)
    labels: {}
    annotations: {}
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "v2",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 10},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 5, Last: 10},
								Error: parser.ParseError{
									Line: 6,
									Err:  errors.New("for value must be a string, got integer instead"),
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: v2
  limit: &for 1
  rules:
  - alert: up:count
    keep_firing_for: *for
    expr: count(up)
    labels: {}
    annotations: {}
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "v2",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 10},
							},
						},
						Limit: &parser.YamlInt{
							Value: 1,
							Raw:   "1",
							Pos: diags.PositionRanges{
								{Line: 4, FirstColumn: 15, LastColumn: 15},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 6, Last: 10},
								Error: parser.ParseError{
									Line: 7,
									Err:  errors.New("keep_firing_for value must be a string, got integer instead"),
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: "{{ source }}"
  rules:
# pint ignore/begin
    
# pint ignore/end
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "{{ source }}",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 10, LastColumn: 21},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: foo
  rules:
  - record: foo
    expr: |
      {"up"}
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "foo",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 5, Last: 7},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 5, FirstColumn: 13, LastColumn: 15},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "{\"up\"}\n",
											Pos: diags.PositionRanges{
												{Line: 7, FirstColumn: 7, LastColumn: 12},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: foo
  rules:
  - record: foo
    expr: |
      {'up'}
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "foo",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 5, Last: 7},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 5, FirstColumn: 13, LastColumn: 15},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "{'up'}\n",
											Pos: diags.PositionRanges{
												{Line: 7, FirstColumn: 7, LastColumn: 12},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: mygroup
  partial_response_strategy: bob
  rules:
  - record: up:count
    expr: count(up)
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "mygroup",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 15},
							},
						},
						Error: parser.ParseError{
							Err:  errors.New("partial_response_strategy is only valid when parser is configured to use the Thanos rule schema"),
							Line: 4,
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: mygroup
  partial_response_strategy: warn
  rules:
  - record: up:count
    expr: count(up)
`),
			strict: true,
			schema: parser.ThanosSchema,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "mygroup",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 15},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 6, Last: 7},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "up:count",
										Pos: diags.PositionRanges{
											{Line: 6, FirstColumn: 13, LastColumn: 20},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "count(up)",
											Pos: diags.PositionRanges{
												{Line: 7, FirstColumn: 11, LastColumn: 19},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: mygroup
  partial_response_strategy: abort
  rules:
  - record: up:count
    expr: count(up)
`),
			strict: true,
			schema: parser.ThanosSchema,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "mygroup",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 15},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 6, Last: 7},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "up:count",
										Pos: diags.PositionRanges{
											{Line: 6, FirstColumn: 13, LastColumn: 20},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "count(up)",
											Pos: diags.PositionRanges{
												{Line: 7, FirstColumn: 11, LastColumn: 19},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: mygroup
  partial_response_strategy: abort
  rules:
  - record: up:count
    expr: count(up)
`),
			strict: false,
			schema: parser.PrometheusSchema,
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "mygroup",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 15},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 6, Last: 7},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "up:count",
										Pos: diags.PositionRanges{
											{Line: 6, FirstColumn: 13, LastColumn: 20},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "count(up)",
											Pos: diags.PositionRanges{
												{Line: 7, FirstColumn: 11, LastColumn: 19},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: mygroup
  partial_response_strategy: bob
  rules:
  - record: up:count
    expr: count(up)
`),
			strict: true,
			schema: parser.ThanosSchema,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "mygroup",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 15},
							},
						},
						Error: parser.ParseError{
							Err:  errors.New("invalid partial_response_strategy value: bob"),
							Line: 4,
						},
					},
				},
			},
		},
		{
			// YAML alias on partial_response_strategy in Thanos strict mode is resolved correctly
			input: []byte(`
groups:
- name: group1
  partial_response_strategy: &prs warn
  rules:
  - record: up:count
    expr: count(up)
- name: group2
  partial_response_strategy: *prs
  rules:
  - record: down:count
    expr: count(down)
`),
			strict: true,
			schema: parser.ThanosSchema,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "group1",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 14},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 6, Last: 7},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "up:count",
										Pos: diags.PositionRanges{
											{Line: 6, FirstColumn: 13, LastColumn: 20},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "count(up)",
											Pos: diags.PositionRanges{
												{Line: 7, FirstColumn: 11, LastColumn: 19},
											},
										},
									},
								},
							},
						},
					},
					{
						Name: parser.YamlNode{
							Value: "group2",
							Pos: diags.PositionRanges{
								{Line: 8, FirstColumn: 9, LastColumn: 14},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 11, Last: 12},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "down:count",
										Pos: diags.PositionRanges{
											{Line: 11, FirstColumn: 13, LastColumn: 22},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "count(down)",
											Pos: diags.PositionRanges{
												{Line: 12, FirstColumn: 11, LastColumn: 21},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: mygroup
  partial_response_strategy: 1
  rules:
  - record: up:count
    expr: count(up)
`),
			strict: true,
			schema: parser.ThanosSchema,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "mygroup",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 15},
							},
						},
						Error: parser.ParseError{
							Err:  errors.New("partial_response_strategy must be a string, got integer"),
							Line: 4,
						},
					},
				},
			},
		},
		{
			input: []byte(`
- alert: Multi Line
  expr: foo
          AND ON (instance)
          bar
`),
			strict: false,
			schema: parser.PrometheusSchema,
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 5},
								AlertingRule: &parser.AlertingRule{
									Alert: parser.YamlNode{
										Value: "Multi Line",
										Pos: diags.PositionRanges{
											{Line: 2, FirstColumn: 10, LastColumn: 19},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "foo AND ON (instance) bar",
											Pos: diags.PositionRanges{
												{Line: 3, FirstColumn: 9, LastColumn: 12},
												{Line: 4, FirstColumn: 11, LastColumn: 28},
												{Line: 5, FirstColumn: 11, LastColumn: 13},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
  - alert: FooBar
    expr: >-
      count(
        foo
        or
        bar
      ) > 0
`),
			strict: false,
			schema: parser.PrometheusSchema,
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 8},
								AlertingRule: &parser.AlertingRule{
									Alert: parser.YamlNode{
										Value: "FooBar",
										Pos: diags.PositionRanges{
											{Line: 2, FirstColumn: 12, LastColumn: 17},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "count(\n  foo\n  or\n  bar\n) > 0",
											Pos: diags.PositionRanges{
												{Line: 4, FirstColumn: 7, LastColumn: 13},
												{Line: 5, FirstColumn: 7, LastColumn: 12},
												{Line: 6, FirstColumn: 7, LastColumn: 11},
												{Line: 7, FirstColumn: 7, LastColumn: 12},
												{Line: 8, FirstColumn: 7, LastColumn: 11},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
  - alert: FooBar
    expr: >-
      aaaaaaaaaaaaaaaaaaaaaaaa
      AND ON (colo_id) bbbbbbbbbbb
      > 2
    for: 1m
`),
			strict: false,
			schema: parser.PrometheusSchema,
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 7},
								AlertingRule: &parser.AlertingRule{
									Alert: parser.YamlNode{
										Value: "FooBar",
										Pos: diags.PositionRanges{
											{Line: 2, FirstColumn: 12, LastColumn: 17},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "aaaaaaaaaaaaaaaaaaaaaaaa AND ON (colo_id) bbbbbbbbbbb > 2",
											Pos: diags.PositionRanges{
												{Line: 4, FirstColumn: 7, LastColumn: 31},
												{Line: 5, FirstColumn: 7, LastColumn: 35},
												{Line: 6, FirstColumn: 7, LastColumn: 9},
											},
										},
									},
									For: &parser.YamlDuration{
										Value: time.Minute,
										Raw:   "1m",
										Pos: diags.PositionRanges{
											{Line: 7, FirstColumn: 10, LastColumn: 11},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
  - alert: FooBar
    expr: 'aaaaaaaaaaaaaaaaaaaaaaaa
          AND ON (colo_id) bbbbbbbbbbb
          > 2'
    for: 1m
`),
			strict: false,
			schema: parser.PrometheusSchema,
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 2, Last: 6},
								AlertingRule: &parser.AlertingRule{
									Alert: parser.YamlNode{
										Value: "FooBar",
										Pos: diags.PositionRanges{
											{Line: 2, FirstColumn: 12, LastColumn: 17},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "aaaaaaaaaaaaaaaaaaaaaaaa AND ON (colo_id) bbbbbbbbbbb > 2",
											Pos: diags.PositionRanges{
												{Line: 3, FirstColumn: 12, LastColumn: 36},
												{Line: 4, FirstColumn: 11, LastColumn: 39},
												{Line: 5, FirstColumn: 11, LastColumn: 13},
											},
										},
									},
									For: &parser.YamlDuration{
										Value: time.Minute,
										Raw:   "1m",
										Pos: diags.PositionRanges{
											{Line: 6, FirstColumn: 10, LastColumn: 11},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: foo
  rules: 
  - record: colo:foo:sum
    expr: sum without (instance) ( rate(my_metric[2m]) * on (instance)
      group_left (hardware_generation, hms_scope, sliver) (instance:metadata{})
      )
`),
			strict: false,
			schema: parser.PrometheusSchema,
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "foo",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 5, Last: 8},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "colo:foo:sum",
										Pos: diags.PositionRanges{
											{Line: 5, FirstColumn: 13, LastColumn: 24},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "sum without (instance) ( rate(my_metric[2m]) * on (instance) group_left (hardware_generation, hms_scope, sliver) (instance:metadata{}) )",
											Pos: diags.PositionRanges{
												{Line: 6, FirstColumn: 11, LastColumn: 71},
												{Line: 7, FirstColumn: 7, LastColumn: 80},
												{Line: 8, FirstColumn: 7, LastColumn: 7},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: "{{ source }}"
  rules:
  # AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
  # BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
  - record: some_long_name
    labels:
      metricssource: receiver
    expr:
      clamp_max(
        sum(rate(my_metric_long_name_total{output="control:output:kafka:/:requests:http-b:http_requests_control_sample"}[5m])) /
        sum(rate(my_metric_long_name_total{output="control:output:kafka:/:requests:http-b:http_requests_control_sample"}[5m] offset 30m)),
        2
      )
`),
			strict: true,
			schema: parser.PrometheusSchema,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "{{ source }}",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 10, LastColumn: 21},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 7, Last: 15},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "some_long_name",
										Pos: diags.PositionRanges{
											{Line: 7, FirstColumn: 13, LastColumn: 26},
										},
									},
									Labels: &parser.YamlMap{
										Key: &parser.YamlNode{
											Value: "labels",
											Pos: diags.PositionRanges{
												{Line: 8, FirstColumn: 5, LastColumn: 10},
											},
										},
										Items: []*parser.YamlKeyValue{
											{
												Key: &parser.YamlNode{
													Value: "metricssource",
													Pos: diags.PositionRanges{
														{Line: 9, FirstColumn: 7, LastColumn: 19},
													},
												},
												Value: &parser.YamlNode{
													Value: "receiver",
													Pos: diags.PositionRanges{
														{Line: 9, FirstColumn: 22, LastColumn: 29},
													},
												},
											},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: `clamp_max( sum(rate(my_metric_long_name_total{output="control:output:kafka:/:requests:http-b:http_requests_control_sample"}[5m])) / sum(rate(my_metric_long_name_total{output="control:output:kafka:/:requests:http-b:http_requests_control_sample"}[5m] offset 30m)), 2 )`,
											Pos: diags.PositionRanges{
												{Line: 11, FirstColumn: 7, LastColumn: 17},
												{Line: 12, FirstColumn: 9, LastColumn: 129},
												{Line: 13, FirstColumn: 9, LastColumn: 139},
												{Line: 14, FirstColumn: 9, LastColumn: 10},
												{Line: 15, FirstColumn: 7, LastColumn: 7},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: "{{ source }}"
  rules: 

  - alert: Director_Is_Not_Advertising_Any_Routes
    expr: |
        sum without (name) (
            bird_protocol_prefix_export_count{ip_version="4",name=~".*external.*",proto!="Kernel"}
          * on (instance) group_left (profile,cluster)
            cf_node_role{kubernetes_role="director",role="kubernetes"}
        ) <= 0
    for: 1m
`),
			strict: true,
			schema: parser.PrometheusSchema,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "{{ source }}",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 10, LastColumn: 21},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 6, Last: 13},
								AlertingRule: &parser.AlertingRule{
									Alert: parser.YamlNode{
										Value: "Director_Is_Not_Advertising_Any_Routes",
										Pos: diags.PositionRanges{
											{Line: 6, FirstColumn: 12, LastColumn: 49},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: `sum without (name) (
    bird_protocol_prefix_export_count{ip_version="4",name=~".*external.*",proto!="Kernel"}
  * on (instance) group_left (profile,cluster)
    cf_node_role{kubernetes_role="director",role="kubernetes"}
) <= 0
`,
											Pos: diags.PositionRanges{
												{Line: 8, FirstColumn: 9, LastColumn: 29},
												{Line: 9, FirstColumn: 9, LastColumn: 99},
												{Line: 10, FirstColumn: 9, LastColumn: 55},
												{Line: 11, FirstColumn: 9, LastColumn: 71},
												{Line: 12, FirstColumn: 9, LastColumn: 14},
											},
										},
									},
									For: &parser.YamlDuration{
										Value: time.Minute,
										Raw:   "1m",
										Pos: diags.PositionRanges{
											{Line: 13, FirstColumn: 10, LastColumn: 11},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: xxx
  interval: 3m
  rules: []
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "xxx",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Interval: &parser.YamlDuration{
							Value: time.Minute * 3,
							Raw:   "3m",
							Pos: diags.PositionRanges{
								{Line: 4, FirstColumn: 13, LastColumn: 14},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: xxx
  interval: 3m
  rules: []
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "xxx",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Interval: &parser.YamlDuration{
							Value: time.Minute * 3,
							Raw:   "3m",
							Pos: diags.PositionRanges{
								{Line: 4, FirstColumn: 13, LastColumn: 14},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: xxx
  interval: 3m
  rules: []
---
groups:
- name: yyy
  interval: 2m
  rules: []
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "xxx",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Interval: &parser.YamlDuration{
							Value: time.Minute * 3,
							Raw:   "3m",
							Pos: diags.PositionRanges{
								{Line: 4, FirstColumn: 13, LastColumn: 14},
							},
						},
					},
					{
						Name: parser.YamlNode{
							Value: "yyy",
							Pos: diags.PositionRanges{
								{Line: 8, FirstColumn: 9, LastColumn: 11},
							},
						},
						Interval: &parser.YamlDuration{
							Value: time.Minute * 2,
							Raw:   "2m",
							Pos: diags.PositionRanges{
								{Line: 9, FirstColumn: 13, LastColumn: 14},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: xxx
  interval: 3m
  labels:
    foo: bar
  rules: []
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "xxx",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Interval: &parser.YamlDuration{
							Value: time.Minute * 3,
							Raw:   "3m",
							Pos: diags.PositionRanges{
								{Line: 4, FirstColumn: 13, LastColumn: 14},
							},
						},
						Labels: &parser.YamlMap{
							Key: &parser.YamlNode{
								Value: "labels",
								Pos: diags.PositionRanges{
									{Line: 5, FirstColumn: 3, LastColumn: 8},
								},
							},
							Items: []*parser.YamlKeyValue{
								{
									Key: &parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 6, FirstColumn: 5, LastColumn: 7},
										},
									},
									Value: &parser.YamlNode{
										Value: "bar",
										Pos: diags.PositionRanges{
											{Line: 6, FirstColumn: 10, LastColumn: 12},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: xxx
  labels:
    - foo: bar
  rules: []
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "xxx",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Error: parser.ParseError{
							Err:  errors.New("group labels must be a mapping, got list"),
							Line: 4,
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: xxx
  labels:
    foo: 1
  rules: []
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "xxx",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Error: parser.ParseError{
							Err:  errors.New("labels foo value must be a string, got integer instead"),
							Line: 5,
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: xxx
  labels:
    foo: bar
    bob: foo
    foo: bob
  rules: []
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "xxx",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Error: parser.ParseError{
							Err:  errors.New("duplicated labels key foo"),
							Line: 7,
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: xxx
  interval: 3m
  query_offset: 1s
  limit: 5
  labels:
    foo: bar
  rules: []
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "xxx",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Interval: &parser.YamlDuration{
							Value: time.Minute * 3,
							Raw:   "3m",
							Pos: diags.PositionRanges{
								{Line: 4, FirstColumn: 13, LastColumn: 14},
							},
						},
						QueryOffset: &parser.YamlDuration{
							Value: time.Second,
							Raw:   "1s",
							Pos: diags.PositionRanges{
								{Line: 5, FirstColumn: 17, LastColumn: 18},
							},
						},
						Limit: &parser.YamlInt{
							Value: 5,
							Raw:   "5",
							Pos: diags.PositionRanges{
								{Line: 6, FirstColumn: 10, LastColumn: 10},
							},
						},
						Labels: &parser.YamlMap{
							Key: &parser.YamlNode{
								Value: "labels",
								Pos: diags.PositionRanges{
									{Line: 7, FirstColumn: 3, LastColumn: 8},
								},
							},
							Items: []*parser.YamlKeyValue{
								{
									Key: &parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 8, FirstColumn: 5, LastColumn: 7},
										},
									},
									Value: &parser.YamlNode{
										Value: "bar",
										Pos: diags.PositionRanges{
											{Line: 8, FirstColumn: 10, LastColumn: 12},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: xxx
  labels:
    - foo: bar
  rules: []
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "xxx",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Labels: &parser.YamlMap{
							Key: &parser.YamlNode{
								Value: "labels",
								Pos: diags.PositionRanges{
									{Line: 4, FirstColumn: 3, LastColumn: 8},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: xxx
  labels:
    foo: 1
  rules: []
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "xxx",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Labels: &parser.YamlMap{
							Key: &parser.YamlNode{
								Value: "labels",
								Pos: diags.PositionRanges{
									{Line: 4, FirstColumn: 3, LastColumn: 8},
								},
							},
							Items: []*parser.YamlKeyValue{
								{
									Key: &parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 5, FirstColumn: 5, LastColumn: 7},
										},
									},
									Value: &parser.YamlNode{
										Value: "1",
										Pos: diags.PositionRanges{
											{Line: 5, FirstColumn: 10, LastColumn: 10},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: xxx
  labels:
    foo: bar
    bob: foo
    foo: bob
  rules: []
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "xxx",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 11},
							},
						},
						Labels: &parser.YamlMap{
							Key: &parser.YamlNode{
								Value: "labels",
								Pos: diags.PositionRanges{
									{Line: 4, FirstColumn: 3, LastColumn: 8},
								},
							},
							Items: []*parser.YamlKeyValue{
								{
									Key: &parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 5, FirstColumn: 5, LastColumn: 7},
										},
									},
									Value: &parser.YamlNode{
										Value: "bar",
										Pos: diags.PositionRanges{
											{Line: 5, FirstColumn: 10, LastColumn: 12},
										},
									},
								},
								{
									Key: &parser.YamlNode{
										Value: "bob",
										Pos: diags.PositionRanges{
											{Line: 6, FirstColumn: 5, LastColumn: 7},
										},
									},
									Value: &parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 6, FirstColumn: 10, LastColumn: 12},
										},
									},
								},
								{
									Key: &parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 7, FirstColumn: 5, LastColumn: 7},
										},
									},
									Value: &parser.YamlNode{
										Value: "bob",
										Pos: diags.PositionRanges{
											{Line: 7, FirstColumn: 10, LastColumn: 12},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
- name: xxx
  interval: 3m
  query_offset: 1s
  limit: 5
  labels:
    foo: bar
  rules:
  - record: up:count
    expr: count(up)
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "xxx",
							Pos: diags.PositionRanges{
								{Line: 2, FirstColumn: 9, LastColumn: 11},
							},
						},
						Interval: &parser.YamlDuration{
							Value: time.Minute * 3,
							Raw:   "3m",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 13, LastColumn: 14},
							},
						},
						QueryOffset: &parser.YamlDuration{
							Value: time.Second,
							Raw:   "1s",
							Pos: diags.PositionRanges{
								{Line: 4, FirstColumn: 17, LastColumn: 18},
							},
						},
						Limit: &parser.YamlInt{
							Value: 5,
							Raw:   "5",
							Pos: diags.PositionRanges{
								{Line: 5, FirstColumn: 10, LastColumn: 10},
							},
						},
						Labels: &parser.YamlMap{
							Key: &parser.YamlNode{
								Value: "labels",
								Pos: diags.PositionRanges{
									{Line: 6, FirstColumn: 3, LastColumn: 8},
								},
							},
							Items: []*parser.YamlKeyValue{
								{
									Key: &parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 7, FirstColumn: 5, LastColumn: 7},
										},
									},
									Value: &parser.YamlNode{
										Value: "bar",
										Pos: diags.PositionRanges{
											{Line: 7, FirstColumn: 10, LastColumn: 12},
										},
									},
								},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 9, Last: 10},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "up:count",
										Pos: diags.PositionRanges{
											{Line: 9, FirstColumn: 13, LastColumn: 20},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "count(up)",
											Pos: diags.PositionRanges{
												{Line: 10, FirstColumn: 11, LastColumn: 19},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`

- interval: 3m
  query_offset: 1s
  limit: 5
  labels:
    foo: bar
  rules:
  - record: up:count
    expr: count(up)
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{Value: ""},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 9, Last: 10},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "up:count",
										Pos: diags.PositionRanges{
											{Line: 9, FirstColumn: 13, LastColumn: 20},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "count(up)",
											Pos: diags.PositionRanges{
												{Line: 10, FirstColumn: 11, LastColumn: 19},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
apiVersion: v1
kind: ConfigMap
metadata:
  labels:
    shard: "0"
    total-shards: "1"
  name: shard-jobs
  namespace: foo
data:
  jobs.yaml: |
    jobs:
      - name: foo
        interval: 1m
        queries:
          - name: xxx
            help: ''
            labels:
              - account_id
              - bucket
              - cache_status
        mtls_identity:
          cert_path: /etc/identity/tls.crt
          key_path: /etc/identity/tls.key
`),
			output: parser.File{
				Groups:    nil,
				IsRelaxed: true,
			},
		},
		{
			// Test rule with line comment on the mapping node itself
			input: []byte(`- record: foo # pint disable line comment
  expr: bar
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 1, Last: 2},
								Comments: []comments.Comment{
									{
										Type: comments.DisableType,
										Value: comments.Disable{
											Match: "line comment",
											Position: comments.Position{
												Offset: 15,
												Pos: diags.PositionRanges{
													{Line: 1, FirstColumn: 1, LastColumn: 27},
												},
											},
										},
									},
								},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 1, FirstColumn: 11, LastColumn: 13},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "bar",
											Pos: diags.PositionRanges{
												{Line: 2, FirstColumn: 9, LastColumn: 11},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			// Test flow mapping with line comment (comment attached to parent node)
			input: []byte(`- {record: foo, expr: bar} # pint disable flow comment
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 1, Last: 1},
								Comments: []comments.Comment{
									{
										Type: comments.DisableType,
										Value: comments.Disable{
											Match: "flow comment",
											Position: comments.Position{
												Offset: 15,
												Pos: diags.PositionRanges{
													{Line: 1, FirstColumn: 1, LastColumn: 27},
												},
											},
										},
									},
								},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 1, FirstColumn: 12, LastColumn: 14},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "bar",
											Pos: diags.PositionRanges{
												{Line: 1, FirstColumn: 23, LastColumn: 25},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			// Test flow mapping with foot comment (comment attached to parent node's FootComment)
			input: []byte(`groups:
- name: test
  rules:
  - {record: foo, expr: bar}
  # pint disable foot comment
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "test",
							Pos: diags.PositionRanges{
								{Line: 2, FirstColumn: 9, LastColumn: 12},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 4, Last: 4},
								Comments: []comments.Comment{
									{
										Type: comments.DisableType,
										Value: comments.Disable{
											Match: "foot comment",
											Position: comments.Position{
												Offset: 15,
												Pos: diags.PositionRanges{
													{Line: 5, FirstColumn: 3, LastColumn: 29},
												},
											},
										},
									},
								},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "foo",
										Pos: diags.PositionRanges{
											{Line: 4, FirstColumn: 14, LastColumn: 16},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "bar",
											Pos: diags.PositionRanges{
												{Line: 4, FirstColumn: 25, LastColumn: 27},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: v2
  rules:
  - record: comment:inside:expr
    expr: |
      count(
        foo
        # pint disable promql/series
        + bar
      )
`),
			strict: true,
			output: parser.File{
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "v2",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 10},
							},
						},
						Rules: []parser.Rule{
							// FIXME should we parse comments inside PromQL?
							{
								Lines: diags.LineRange{First: 5, Last: 11},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "comment:inside:expr",
										Pos: diags.PositionRanges{
											{Line: 5, FirstColumn: 13, LastColumn: 31},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "count(\n  foo\n  # pint disable promql/series\n  + bar\n)\n",
											Pos: diags.PositionRanges{
												{Line: 7, FirstColumn: 7, LastColumn: 13},
												{Line: 8, FirstColumn: 7, LastColumn: 12},
												{Line: 9, FirstColumn: 7, LastColumn: 37},
												{Line: 10, FirstColumn: 7, LastColumn: 14},
												{Line: 11, FirstColumn: 7, LastColumn: 7},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`
groups:
- name: v2
  rules:
  - record: comment:inside:expr
    expr: |
      count(
        foo
        # pint disable promql/series
        + bar
      )
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "v2",
							Pos: diags.PositionRanges{
								{Line: 3, FirstColumn: 9, LastColumn: 10},
							},
						},
						Rules: []parser.Rule{
							// FIXME should we parse comments inside PromQL?
							{
								Lines: diags.LineRange{First: 5, Last: 11},
								RecordingRule: &parser.RecordingRule{
									Record: parser.YamlNode{
										Value: "comment:inside:expr",
										Pos: diags.PositionRanges{
											{Line: 5, FirstColumn: 13, LastColumn: 31},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "count(\n  foo\n  # pint disable promql/series\n  + bar\n)\n",
											Pos: diags.PositionRanges{
												{Line: 7, FirstColumn: 7, LastColumn: 13},
												{Line: 8, FirstColumn: 7, LastColumn: 12},
												{Line: 9, FirstColumn: 7, LastColumn: 37},
												{Line: 10, FirstColumn: 7, LastColumn: 14},
												{Line: 11, FirstColumn: 7, LastColumn: 7},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
		{
			input: []byte(`---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  namespace: thanos
spec:
  replicas: 2
  selector:
    matchLabels:
      app: ruler-core
      component: ruler
  serviceName: thanos-ruler
  template:
    metadata:
      labels:
        app: ruler-core
        component: ruler
    spec:
      containers:
        - args:
            - --log.format=json
            - --http-address=0.0.0.0:10902
            - --grpc-address=0.0.0.0:10901
            - --eval-interval=1m
            - |
              --tracing.config=
              type: JAEGER
              config:
                sampler_param: 0.01
                sampler_type: probabilistic
          command:
            - /bin/thanos
            - rule
          imagePullPolicy: IfNotPresent
          livenessProbe:
            httpGet:
              path: /-/healthy
              port: 10902
            initialDelaySeconds: 30
            terminationGracePeriodSeconds: 300
            timeoutSeconds: 5
          name: ruler
          ports:
            - containerPort: 10902
              name: http-metrics
          readinessProbe:
            httpGet:
              path: /-/ready
              port: 10902
            initialDelaySeconds: 30
            timeoutSeconds: 1
          resources:
            limits:
              cpu: "4"
              memory: 4G
            requests:
              cpu: "4"
              memory: 4G
        - args:
            - --config=/rules/current/.pint.hcl
            - watch
            - --listen=:10904
            - --max-problems=50
            - glob
            - /rules/current/rules/*
          command:
            - /usr/local/bin/pint
          imagePullPolicy: IfNotPresent
          livenessProbe:
            httpGet:
              path: /health
              port: 10904
            initialDelaySeconds: 30
            timeoutSeconds: 5
          name: pint
          ports:
            - containerPort: 10904
              name: pint
          resources:
            limits:
              cpu: "1"
              memory: 256Mi
            requests:
              cpu: "1"
              memory: 256Mi
          volumeMounts:
            - mountPath: /rules
              name: rules
---
apiVersion: v1
kind: ConfigMap
metadata:
  labels:
    prometheus.cfplat.com/rules: "true"
  name: ruler-core-ruler
  namespace: thanos
data:
  rules: |
    groups:
      - name: ruler-health-alerts
        rules:
          - alert: Thanos_Rule_Queue_Is_Dropping_Alerts
            expr: rate(thanos_alert_queue_alerts_dropped_total{kubernetes_namespace="thanos", pod_app="ruler-core", pod_component="ruler"}[2m]) > 0
            for: 5m
            labels:
              priority: "3"
              notify: chat-obs-metrics
            annotations:
              summary: Thanos Ruler is failing to queue alerts.
          - alert: Thanos_Rule_Sender_Is_Dropping_Alerts
            expr: rate(thanos_alert_sender_alerts_dropped_total{kubernetes_namespace="thanos", pod_app="ruler-core", pod_component="ruler"}[2m]) > 0
            for: 5m
            labels:
              priority: "3"
              notify: chat-obs-metrics
            annotations:
              summary: Thanos Ruler is failing to send alerts.
          - alert: Thanos_Rule_High_Rule_Evaluation_Failures
            expr: |2-
              
                                  (
                                    sum(rate(prometheus_rule_evaluation_failures_total{kubernetes_namespace="thanos", pod_app="ruler-core", pod_component="ruler"}[2m]) / rate(prometheus_rule_group_last_duration_seconds{kubernetes_namespace="thanos", pod_app="ruler-core", pod_component="ruler"}[2m]))
                                    by (job,kubernetes_name,pod, pod_app, pod_component)
                                  /
                                    sum(rate(prometheus_rule_evaluations_total{kubernetes_namespace="thanos", pod_app="ruler-core", pod_component="ruler"}[2m]) / rate(prometheus_rule_group_last_duration_seconds{kubernetes_namespace="thanos", pod_app="ruler-core", pod_component="ruler"}[2m]))
                                    by (job,kubernetes_name,pod, pod_app, pod_component)
                                  * 100 > 5
                                  )
            for: 5m
            labels:
              priority: "3"
              notify: chat-obs-metrics
            annotations:
              summary: Thanos Ruler is failing to evaluate {{ $value | humanize }}% of rules.
`),
			output: parser.File{
				IsRelaxed: true,
				Groups: []parser.Group{
					{
						Name: parser.YamlNode{
							Value: "ruler-health-alerts",
							Pos: diags.PositionRanges{
								{Line: 2, FirstColumn: 11, LastColumn: 29},
							},
						},
						Rules: []parser.Rule{
							{
								Lines: diags.LineRange{First: 102, Last: 109},
								AlertingRule: &parser.AlertingRule{
									Alert: parser.YamlNode{
										Value: "Thanos_Rule_Queue_Is_Dropping_Alerts",
										Pos: diags.PositionRanges{
											{Line: 102, FirstColumn: 20, LastColumn: 55},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: `rate(thanos_alert_queue_alerts_dropped_total{kubernetes_namespace="thanos", pod_app="ruler-core", pod_component="ruler"}[2m]) > 0`,
											Pos: diags.PositionRanges{
												{Line: 103, FirstColumn: 19, LastColumn: 147},
											},
										},
									},
									For: &parser.YamlDuration{
										Value: 5 * time.Minute,
										Raw:   "5m",
										Pos: diags.PositionRanges{
											{Line: 104, FirstColumn: 18, LastColumn: 19},
										},
									},
									Labels: &parser.YamlMap{
										Key: &parser.YamlNode{
											Value: "labels",
											Pos: diags.PositionRanges{
												{Line: 105, FirstColumn: 13, LastColumn: 18},
											},
										},
										Items: []*parser.YamlKeyValue{
											{
												Key: &parser.YamlNode{
													Value: "priority",
													Pos: diags.PositionRanges{
														{Line: 106, FirstColumn: 15, LastColumn: 22},
													},
												},
												Value: &parser.YamlNode{
													Value: "3",
													Pos: diags.PositionRanges{
														{Line: 106, FirstColumn: 26, LastColumn: 26},
													},
												},
											},
											{
												Key: &parser.YamlNode{
													Value: "notify",
													Pos: diags.PositionRanges{
														{Line: 107, FirstColumn: 15, LastColumn: 20},
													},
												},
												Value: &parser.YamlNode{
													Value: "chat-obs-metrics",
													Pos: diags.PositionRanges{
														{Line: 107, FirstColumn: 23, LastColumn: 38},
													},
												},
											},
										},
									},
									Annotations: &parser.YamlMap{
										Key: &parser.YamlNode{
											Value: "annotations",
											Pos: diags.PositionRanges{
												{Line: 108, FirstColumn: 13, LastColumn: 23},
											},
										},
										Items: []*parser.YamlKeyValue{
											{
												Key: &parser.YamlNode{
													Value: "summary",
													Pos: diags.PositionRanges{
														{Line: 109, FirstColumn: 15, LastColumn: 21},
													},
												},
												Value: &parser.YamlNode{
													Value: "Thanos Ruler is failing to queue alerts.",
													Pos: diags.PositionRanges{
														{Line: 109, FirstColumn: 24, LastColumn: 63},
													},
												},
											},
										},
									},
								},
							},
							{
								Lines: diags.LineRange{First: 110, Last: 117},
								AlertingRule: &parser.AlertingRule{
									Alert: parser.YamlNode{
										Value: "Thanos_Rule_Sender_Is_Dropping_Alerts",
										Pos: diags.PositionRanges{
											{Line: 110, FirstColumn: 20, LastColumn: 56},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: `rate(thanos_alert_sender_alerts_dropped_total{kubernetes_namespace="thanos", pod_app="ruler-core", pod_component="ruler"}[2m]) > 0`,
											Pos: diags.PositionRanges{
												{Line: 111, FirstColumn: 19, LastColumn: 148},
											},
										},
									},
									For: &parser.YamlDuration{
										Value: 5 * time.Minute,
										Raw:   "5m",
										Pos: diags.PositionRanges{
											{Line: 112, FirstColumn: 18, LastColumn: 19},
										},
									},
									Labels: &parser.YamlMap{
										Key: &parser.YamlNode{
											Value: "labels",
											Pos: diags.PositionRanges{
												{Line: 113, FirstColumn: 13, LastColumn: 18},
											},
										},
										Items: []*parser.YamlKeyValue{
											{
												Key: &parser.YamlNode{
													Value: "priority",
													Pos: diags.PositionRanges{
														{Line: 114, FirstColumn: 15, LastColumn: 22},
													},
												},
												Value: &parser.YamlNode{
													Value: "3",
													Pos: diags.PositionRanges{
														{Line: 114, FirstColumn: 26, LastColumn: 26},
													},
												},
											},
											{
												Key: &parser.YamlNode{
													Value: "notify",
													Pos: diags.PositionRanges{
														{Line: 115, FirstColumn: 15, LastColumn: 20},
													},
												},
												Value: &parser.YamlNode{
													Value: "chat-obs-metrics",
													Pos: diags.PositionRanges{
														{Line: 115, FirstColumn: 23, LastColumn: 38},
													},
												},
											},
										},
									},
									Annotations: &parser.YamlMap{
										Key: &parser.YamlNode{
											Value: "annotations",
											Pos: diags.PositionRanges{
												{Line: 116, FirstColumn: 13, LastColumn: 23},
											},
										},
										Items: []*parser.YamlKeyValue{
											{
												Key: &parser.YamlNode{
													Value: "summary",
													Pos: diags.PositionRanges{
														{Line: 117, FirstColumn: 15, LastColumn: 21},
													},
												},
												Value: &parser.YamlNode{
													Value: "Thanos Ruler is failing to send alerts.",
													Pos: diags.PositionRanges{
														{Line: 117, FirstColumn: 24, LastColumn: 62},
													},
												},
											},
										},
									},
								},
							},
							{
								Lines: diags.LineRange{First: 118, Last: 134},
								AlertingRule: &parser.AlertingRule{
									Alert: parser.YamlNode{
										Value: "Thanos_Rule_High_Rule_Evaluation_Failures",
										Pos: diags.PositionRanges{
											{Line: 118, FirstColumn: 20, LastColumn: 60},
										},
									},
									Expr: parser.PromQLExpr{
										Value: &parser.YamlNode{
											Value: "\n                    (\n                      sum(rate(prometheus_rule_evaluation_failures_total{kubernetes_namespace=\"thanos\", pod_app=\"ruler-core\", pod_component=\"ruler\"}[2m]) / rate(prometheus_rule_group_last_duration_seconds{kubernetes_namespace=\"thanos\", pod_app=\"ruler-core\", pod_component=\"ruler\"}[2m]))\n                      by (job,kubernetes_name,pod, pod_app, pod_component)\n                    /\n                      sum(rate(prometheus_rule_evaluations_total{kubernetes_namespace=\"thanos\", pod_app=\"ruler-core\", pod_component=\"ruler\"}[2m]) / rate(prometheus_rule_group_last_duration_seconds{kubernetes_namespace=\"thanos\", pod_app=\"ruler-core\", pod_component=\"ruler\"}[2m]))\n                      by (job,kubernetes_name,pod, pod_app, pod_component)\n                    * 100 > 5\n                    )",
											Pos: diags.PositionRanges{
												{Line: 119, FirstColumn: 22, LastColumn: 22},
												{Line: 120, FirstColumn: 14, LastColumn: 15},
												{Line: 121, FirstColumn: 17, LastColumn: 36},
												{Line: 122, FirstColumn: 15, LastColumn: 301},
												{Line: 123, FirstColumn: 15, LastColumn: 89},
												{Line: 124, FirstColumn: 15, LastColumn: 36},
												{Line: 125, FirstColumn: 15, LastColumn: 293},
												{Line: 126, FirstColumn: 15, LastColumn: 89},
												{Line: 127, FirstColumn: 15, LastColumn: 44},
												{Line: 128, FirstColumn: 15, LastColumn: 35},
											},
										},
									},
									For: &parser.YamlDuration{
										Value: 5 * time.Minute,
										Raw:   "5m",
										Pos: diags.PositionRanges{
											{Line: 129, FirstColumn: 18, LastColumn: 19},
										},
									},
									Labels: &parser.YamlMap{
										Key: &parser.YamlNode{
											Value: "labels",
											Pos: diags.PositionRanges{
												{Line: 130, FirstColumn: 13, LastColumn: 18},
											},
										},
										Items: []*parser.YamlKeyValue{
											{
												Key: &parser.YamlNode{
													Value: "priority",
													Pos: diags.PositionRanges{
														{Line: 131, FirstColumn: 15, LastColumn: 22},
													},
												},
												Value: &parser.YamlNode{
													Value: "3",
													Pos: diags.PositionRanges{
														{Line: 131, FirstColumn: 26, LastColumn: 26},
													},
												},
											},
											{
												Key: &parser.YamlNode{
													Value: "notify",
													Pos: diags.PositionRanges{
														{Line: 132, FirstColumn: 15, LastColumn: 20},
													},
												},
												Value: &parser.YamlNode{
													Value: "chat-obs-metrics",
													Pos: diags.PositionRanges{
														{Line: 132, FirstColumn: 23, LastColumn: 38},
													},
												},
											},
										},
									},
									Annotations: &parser.YamlMap{
										Key: &parser.YamlNode{
											Value: "annotations",
											Pos: diags.PositionRanges{
												{Line: 133, FirstColumn: 13, LastColumn: 23},
											},
										},
										Items: []*parser.YamlKeyValue{
											{
												Key: &parser.YamlNode{
													Value: "summary",
													Pos: diags.PositionRanges{
														{Line: 134, FirstColumn: 15, LastColumn: 21},
													},
												},
												Value: &parser.YamlNode{
													Value: "Thanos Ruler is failing to evaluate {{ $value | humanize }}% of rules.",
													Pos: diags.PositionRanges{
														{Line: 134, FirstColumn: 24, LastColumn: 93},
													},
												},
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
	}

	alwaysEqual := cmp.Comparer(func(_, _ any) bool { return true })
	ignorePrometheusExpr := cmp.FilterValues(func(x, y any) bool {
		_, xe := x.(*parser.PromQLNode)
		_, ye := y.(*parser.PromQLNode)
		return xe || ye
	}, alwaysEqual)

	cmpErrorText := cmp.Comparer(func(x, y any) bool {
		xe := x.(error)
		ye := y.(error)
		return xe.Error() == ye.Error()
	})
	sameErrorText := cmp.FilterValues(func(x, y any) bool {
		_, xe := x.(error)
		_, ye := y.(error)
		return xe && ye
	}, cmpErrorText)

	for i, tc := range testCases {
		t.Run(strconv.Itoa(i+1), func(t *testing.T) {
			t.Logf("\n--- Content ---%s--- END ---", tc.input)

			s := bufio.NewScanner(bytes.NewReader(tc.input))
			for s.Scan() {
				tc.output.TotalLines++
			}

			p := parser.NewParser(parser.Options{IsStrict: tc.strict, Schema: tc.schema, Names: tc.names})
			file := p.Parse(bytes.NewReader(tc.input))

			if diff := cmp.Diff(
				tc.output, file,
				ignorePrometheusExpr,
				sameErrorText,
				cmpopts.IgnoreUnexported(parser.PromQLExpr{}),
			); diff != "" {
				t.Errorf("Parse() returned wrong output (-want +got):\n%s", diff)
				return
			}
		})
	}
}

func TestPromQLExprSyntaxError(t *testing.T) {
	type testCaseT struct {
		name          string
		expectedError string
		input         []byte
		schema        parser.Schema
		names         model.ValidationScheme
	}

	testCases := []testCaseT{
		{
			name: "invalid label matching operator",
			input: []byte(`
groups:
- name: foo
  rules:
  - record: foo
    expr: |
      {'up' == 1}
`),
			expectedError: `1:8: parse error: unexpected "=" in label matching, expected string`,
			schema:        parser.PrometheusSchema,
			names:         model.LegacyValidation,
		},
		{
			name: "unclosed parenthesis",
			input: []byte(`
groups:
- name: foo
  rules:
  - record: foo
    expr: sum(up
`),
			expectedError: `1:7: parse error: unclosed left parenthesis`,
			schema:        parser.PrometheusSchema,
			names:         model.LegacyValidation,
		},
		{
			name: "unclosed brace",
			input: []byte(`
groups:
- name: foo
  rules:
  - record: foo
    expr: up{job="test"
`),
			expectedError: `1:14: parse error: unexpected end of input inside braces`,
			schema:        parser.PrometheusSchema,
			names:         model.LegacyValidation,
		},
		{
			name: "invalid aggregation without grouping",
			input: []byte(`
groups:
- name: foo
  rules:
  - record: foo
    expr: sum without (up)
`),
			expectedError: `1:17: parse error: unexpected end of input in aggregation`,
			schema:        parser.PrometheusSchema,
			names:         model.LegacyValidation,
		},
		{
			name: "bad regex syntax",
			input: []byte(`
groups:
- name: foo
  rules:
  - record: foo
    expr: up{job=~"["}
`),
			expectedError: `1:4: parse error: error parsing regexp: missing closing ]: ` + "`[`",
			schema:        parser.PrometheusSchema,
			names:         model.LegacyValidation,
		},
		{
			name: "invalid binary operator position",
			input: []byte(`
groups:
- name: foo
  rules:
  - record: foo
    expr: up +
`),
			expectedError: `1:5: parse error: unexpected end of input`,
			schema:        parser.PrometheusSchema,
			names:         model.LegacyValidation,
		},
		{
			name: "multiple binary operators",
			input: []byte(`
groups:
- name: foo
  rules:
  - record: foo
    expr: up + * down
`),
			expectedError: `1:6: parse error: unexpected <op:*>`,
			schema:        parser.PrometheusSchema,
			names:         model.LegacyValidation,
		},
		{
			name: "invalid duration syntax",
			input: []byte(`
groups:
- name: foo
  rules:
  - record: foo
    expr: rate(up[5x])
`),
			expectedError: `1:9: parse error: bad number or duration syntax: "5"`,
			schema:        parser.PrometheusSchema,
			names:         model.LegacyValidation,
		},
		{
			name: "missing range selector",
			input: []byte(`
groups:
- name: foo
  rules:
  - record: foo
    expr: rate(up)
`),
			expectedError: `1:6: parse error: expected type range vector in call to function "rate", got instant vector`,
			schema:        parser.PrometheusSchema,
			names:         model.LegacyValidation,
		},
		{
			name: "invalid label name in matcher",
			input: []byte(`
groups:
- name: foo
  rules:
  - record: foo
    expr: up{123="test"}
`),
			expectedError: `1:4: parse error: unexpected character inside braces: '1'`,
			schema:        parser.PrometheusSchema,
			names:         model.LegacyValidation,
		},
		{
			name: "unclosed parenthesis with Thanos schema",
			input: []byte(`
groups:
- name: foo
  rules:
  - record: foo
    expr: sum(up
`),
			expectedError: `1:7: parse error: unclosed left parenthesis`,
			schema:        parser.ThanosSchema,
			names:         model.LegacyValidation,
		},
		{
			name: "invalid binary operator with UTF8 validation",
			input: []byte(`
groups:
- name: foo
  rules:
  - record: foo
    expr: up +
`),
			expectedError: `1:5: parse error: unexpected end of input`,
			schema:        parser.PrometheusSchema,
			names:         model.UTF8Validation,
		},
		{
			name: "bad regex with Thanos schema and UTF8 validation",
			input: []byte(`
groups:
- name: foo
  rules:
  - record: foo
    expr: up{job=~"["}
`),
			expectedError: `1:4: parse error: error parsing regexp: missing closing ]: ` + "`[`",
			schema:        parser.ThanosSchema,
			names:         model.UTF8Validation,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			p := parser.NewParser(parser.Options{Schema: tc.schema, Names: tc.names})
			r := bytes.NewReader(tc.input)
			file := p.Parse(r)

			require.NoError(t, file.Error.Err)
			require.NotEmpty(t, file.Groups)
			for _, group := range file.Groups {
				for _, rule := range group.Rules {
					require.NoError(t, rule.Error.Err)
					expr := rule.Expr()
					err := expr.SyntaxError()
					require.Error(t, err)
					require.Equal(t, tc.expectedError, err.Error())
				}
			}
		})
	}
}

func BenchmarkParse(b *testing.B) {
	data, err := os.ReadFile("testrules.yml")
	require.NoError(b, err)

	p := parser.NewParser(parser.Options{IsStrict: true, Names: model.LegacyValidation})
	for b.Loop() {
		b.StopTimer()
		r := bytes.NewReader(data)
		b.StartTimer()

		f := p.Parse(r)

		b.StopTimer()
		require.Len(b, f.Groups, 90)
		require.NoError(b, f.Error.Err)
		require.Equal(b, 5501, f.TotalLines)
		b.StartTimer()
	}
}
