package comments_test

import (
	"fmt"
	"testing"
	"time"

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

	"github.com/stretchr/testify/require"
)

func TestParse(t *testing.T) {
	type testCaseT struct {
		input  string
		output []comments.Comment
	}

	parseUntil := func(s string) time.Time {
		until, err := time.Parse(time.RFC3339, s)
		require.NoError(t, err)
		return until
	}

	errUntil := func(s string) error {
		_, err := time.Parse("2006-01-02", s)
		require.Error(t, err)
		return err
	}

	testCases := []testCaseT{
		{
			input: "code\n",
		},
		{
			input: "code # bob\n",
		},
		{
			input: "code # bob\ncode # alice\n",
		},
		{
			input: "# pint   bamboozle me this",
		},
		{
			input: "# pint/xxx   bamboozle me this",
		},
		{
			input: "# pint  bambo[]ozle me this",
		},
		{
			input: "# pint ignore/file \t this file",
			output: []comments.Comment{
				{
					Type: comments.InvalidComment,
					Value: comments.Invalid{
						Err: comments.Error{
							Diagnostic: diags.Diagnostic{
								Message: `This comment is not a valid pint control comment: unexpected comment suffix: "this file"`,
								Pos: diags.PositionRanges{
									{
										Line:        1,
										FirstColumn: 1,
										LastColumn:  30,
									},
								},
								FirstColumn: 1,
								LastColumn:  30,
								Kind:        diags.Issue,
							},
						},
					},
				},
			},
		},
		{
			input: "# pint ignore/file",
			output: []comments.Comment{
				{Type: comments.IgnoreFileType},
			},
		},
		{
			input: "# pint ignore/line this line",
			output: []comments.Comment{
				{
					Type: comments.InvalidComment,
					Value: comments.Invalid{
						Err: comments.Error{
							Diagnostic: diags.Diagnostic{
								Message: `This comment is not a valid pint control comment: unexpected comment suffix: "this line"`,
								Pos: diags.PositionRanges{
									{
										Line:        1,
										FirstColumn: 1,
										LastColumn:  28,
									},
								},
								FirstColumn: 1,
								LastColumn:  28,
							},
						},
					},
				},
			},
		},
		{
			input: "# pint ignore/line",
			output: []comments.Comment{
				{Type: comments.IgnoreLineType},
			},
		},
		{
			input: "# pint ignore/begin here",
			output: []comments.Comment{
				{
					Type: comments.InvalidComment,
					Value: comments.Invalid{
						Err: comments.Error{
							Diagnostic: diags.Diagnostic{
								Message: `This comment is not a valid pint control comment: unexpected comment suffix: "here"`,
								Pos: diags.PositionRanges{
									{
										Line:        1,
										FirstColumn: 1,
										LastColumn:  24,
									},
								},
								FirstColumn: 1,
								LastColumn:  24,
								Kind:        diags.Issue,
							},
						},
					},
				},
			},
		},
		{
			input: "# pint ignore/begin",
			output: []comments.Comment{
				{Type: comments.IgnoreBeginType},
			},
		},
		{
			input: "# pint ignore/end here",
			output: []comments.Comment{
				{
					Type: comments.InvalidComment,
					Value: comments.Invalid{
						Err: comments.Error{
							Diagnostic: diags.Diagnostic{
								Message: `This comment is not a valid pint control comment: unexpected comment suffix: "here"`,
								Pos: diags.PositionRanges{
									{
										Line:        1,
										FirstColumn: 1,
										LastColumn:  22,
									},
								},
								FirstColumn: 1,
								LastColumn:  22,
								Kind:        diags.Issue,
							},
						},
					},
				},
			},
		},
		{
			input: "# pint ignore/end",
			output: []comments.Comment{
				{Type: comments.IgnoreEndType},
			},
		},
		{
			input: "#   pint ignore/next-line\there",
			output: []comments.Comment{
				{
					Type: comments.InvalidComment,
					Value: comments.Invalid{
						Err: comments.Error{
							Diagnostic: diags.Diagnostic{
								Message: `This comment is not a valid pint control comment: unexpected comment suffix: "here"`,
								Pos: diags.PositionRanges{
									{
										Line:        1,
										FirstColumn: 1,
										LastColumn:  30,
									},
								},
								FirstColumn: 1,
								LastColumn:  30,
								Kind:        diags.Issue,
							},
						},
					},
				},
			},
		},
		{
			input: "# pint ignore/next-line",
			output: []comments.Comment{
				{Type: comments.IgnoreNextLineType},
			},
		},
		{
			input: "#   pint file/owner",
			output: []comments.Comment{
				{
					Type: comments.InvalidComment,
					Value: comments.Invalid{
						Err: comments.Error{
							Diagnostic: diags.Diagnostic{
								Message: `This comment is not a valid pint control comment: missing file/owner value`,
								Pos: diags.PositionRanges{
									{
										Line:        1,
										FirstColumn: 1,
										LastColumn:  19,
									},
								},
								FirstColumn: 1,
								LastColumn:  19,
								Kind:        diags.Issue,
							},
						},
					},
				},
			},
		},
		{
			input: "# pint file/owner bob and alice",
			output: []comments.Comment{
				{
					Type: comments.FileOwnerType,
					Value: comments.Owner{
						Name: "bob and alice",
						Position: comments.Position{
							Offset: 18,
							Pos: diags.PositionRanges{
								{
									Line:        1,
									FirstColumn: 1,
									LastColumn:  31,
								},
							},
						},
					},
				},
			},
		},
		{
			input: "#   pint rule/owner",
			output: []comments.Comment{
				{
					Type: comments.InvalidComment,
					Value: comments.Invalid{
						Err: comments.Error{
							Diagnostic: diags.Diagnostic{
								Message: `This comment is not a valid pint control comment: missing rule/owner value`,
								Pos: diags.PositionRanges{
									{
										Line:        1,
										FirstColumn: 1,
										LastColumn:  19,
									},
								},
								FirstColumn: 1,
								LastColumn:  19,
								Kind:        diags.Issue,
							},
						},
					},
				},
			},
		},
		{
			input: "# pint rule/owner bob and alice",
			output: []comments.Comment{
				{
					Type: comments.RuleOwnerType,
					Value: comments.Owner{
						Name: "bob and alice",
						Position: comments.Position{
							Offset: 18,
							Pos: diags.PositionRanges{
								{
									Line:        1,
									FirstColumn: 1,
									LastColumn:  31,
								},
							},
						},
					},
				},
			},
		},
		{
			input: "#   pint file/disable",
			output: []comments.Comment{
				{
					Type: comments.InvalidComment,
					Value: comments.Invalid{
						Err: comments.Error{
							Diagnostic: diags.Diagnostic{
								Message: `This comment is not a valid pint control comment: missing file/disable value`,
								Pos: diags.PositionRanges{
									{
										Line:        1,
										FirstColumn: 1,
										LastColumn:  21,
									},
								},
								FirstColumn: 1,
								LastColumn:  21,
								Kind:        diags.Issue,
							},
						},
					},
				},
			},
		},
		{
			input: `# pint file/disable promql/series(http_errors_total{label="this has spaces"})`,
			output: []comments.Comment{
				{
					Type: comments.FileDisableType,
					Value: comments.Disable{
						Match: `promql/series(http_errors_total{label="this has spaces"})`,
						Position: comments.Position{
							Offset: 20,
							Pos: diags.PositionRanges{
								{
									Line:        1,
									FirstColumn: 1,
									LastColumn:  77,
								},
							},
						},
					},
				},
			},
		},
		{
			input: "#   pint disable",
			output: []comments.Comment{
				{
					Type: comments.InvalidComment,
					Value: comments.Invalid{
						Err: comments.Error{
							Diagnostic: diags.Diagnostic{
								Message: `This comment is not a valid pint control comment: missing disable value`,
								Pos: diags.PositionRanges{
									{
										Line:        1,
										FirstColumn: 1,
										LastColumn:  16,
									},
								},
								FirstColumn: 1,
								LastColumn:  16,
								Kind:        diags.Issue,
							},
						},
					},
				},
			},
		},
		{
			input: `# pint disable promql/series(http_errors_total{label="this has spaces"})`,
			output: []comments.Comment{
				{
					Type: comments.DisableType,
					Value: comments.Disable{
						Match: `promql/series(http_errors_total{label="this has spaces"})`,
						Position: comments.Position{
							Offset: 15,
							Pos: diags.PositionRanges{
								{
									Line:        1,
									FirstColumn: 1,
									LastColumn:  72,
								},
							},
						},
					},
				},
			},
		},
		{
			input: `# pint disable promql/series(http_errors_total{label="this has spaces and a # symbol"})`,
			output: []comments.Comment{
				{
					Type: comments.DisableType,
					Value: comments.Disable{
						Match: `promql/series(http_errors_total{label="this has spaces and a # symbol"})`,
						Position: comments.Position{
							Offset: 15,
							Pos: diags.PositionRanges{
								{
									Line:        1,
									FirstColumn: 1,
									LastColumn:  87,
								},
							},
						},
					},
				},
			},
		},
		{
			input: "#   pint file/snooze",
			output: []comments.Comment{
				{
					Type: comments.InvalidComment,
					Value: comments.Invalid{
						Err: comments.Error{
							Diagnostic: diags.Diagnostic{
								Message: `This comment is not a valid pint control comment: missing file/snooze value`,
								Pos: diags.PositionRanges{
									{
										Line:        1,
										FirstColumn: 1,
										LastColumn:  20,
									},
								},
								FirstColumn: 1,
								LastColumn:  20,
								Kind:        diags.Issue,
							},
						},
					},
				},
			},
		},
		{
			input: "#   pint file/snooze 2023-12-31",
			output: []comments.Comment{
				{
					Type: comments.InvalidComment,
					Value: comments.Invalid{
						Err: comments.Error{
							Diagnostic: diags.Diagnostic{
								Message: `This comment is not a valid pint control comment: invalid snooze comment, expected '$TIME $MATCH' got "2023-12-31"`,
								Pos: diags.PositionRanges{
									{
										Line:        1,
										FirstColumn: 1,
										LastColumn:  31,
									},
								},
								FirstColumn: 1,
								LastColumn:  31,
								Kind:        diags.Issue,
							},
						},
					},
				},
			},
		},
		{
			input: "#   pint file/snooze abc",
			output: []comments.Comment{
				{
					Type: comments.InvalidComment,
					Value: comments.Invalid{
						Err: comments.Error{
							Diagnostic: diags.Diagnostic{
								Message: `This comment is not a valid pint control comment: invalid snooze comment, expected '$TIME $MATCH' got "abc"`,
								Pos: diags.PositionRanges{
									{
										Line:        1,
										FirstColumn: 1,
										LastColumn:  24,
									},
								},
								FirstColumn: 1,
								LastColumn:  24,
								Kind:        diags.Issue,
							},
						},
					},
				},
			},
		},
		{
			input: `# pint file/snooze 2023-1231 promql/series(http_errors_total{label="this has spaces"})`,
			output: []comments.Comment{
				{
					Type: comments.InvalidComment,
					Value: comments.Invalid{Err: comments.Error{
						Diagnostic: diags.Diagnostic{
							Message: fmt.Sprintf("This comment is not a valid pint control comment: invalid snooze timestamp: %s", errUntil("2023-1231")),
							Pos: diags.PositionRanges{
								{
									Line:        1,
									FirstColumn: 1,
									LastColumn:  86,
								},
							},
							FirstColumn: 1,
							LastColumn:  86,
							Kind:        diags.Issue,
						},
					}},
				},
			},
		},
		{
			input: `# pint file/snooze 2023-12-31 promql/series(http_errors_total{label="this has spaces"})`,
			output: []comments.Comment{
				{
					Type: comments.FileSnoozeType,
					Value: comments.Snooze{
						Until: parseUntil("2023-12-31T00:00:00Z"),
						Match: `promql/series(http_errors_total{label="this has spaces"})`,
						Position: comments.Position{
							Offset: 30,
							Pos: diags.PositionRanges{
								{
									Line:        1,
									FirstColumn: 1,
									LastColumn:  87,
								},
							},
						},
					},
				},
			},
		},
		{
			input: "#   pint snooze",
			output: []comments.Comment{
				{
					Type: comments.InvalidComment,
					Value: comments.Invalid{
						Err: comments.Error{
							Diagnostic: diags.Diagnostic{
								Message: `This comment is not a valid pint control comment: missing snooze value`,
								Pos: diags.PositionRanges{
									{
										Line:        1,
										FirstColumn: 1,
										LastColumn:  15,
									},
								},
								FirstColumn: 1,
								LastColumn:  15,
								Kind:        diags.Issue,
							},
						},
					},
				},
			},
		},
		{
			input: "#   pint snooze 2023-12-31",
			output: []comments.Comment{
				{
					Type: comments.InvalidComment,
					Value: comments.Invalid{
						Err: comments.Error{
							Diagnostic: diags.Diagnostic{
								Message: `This comment is not a valid pint control comment: invalid snooze comment, expected '$TIME $MATCH' got "2023-12-31"`,
								Pos: diags.PositionRanges{
									{
										Line:        1,
										FirstColumn: 1,
										LastColumn:  26,
									},
								},
								FirstColumn: 1,
								LastColumn:  26,
								Kind:        diags.Issue,
							},
						},
					},
				},
			},
		},
		{
			input: "#   pint snooze abc",
			output: []comments.Comment{
				{
					Type: comments.InvalidComment,
					Value: comments.Invalid{
						Err: comments.Error{
							Diagnostic: diags.Diagnostic{
								Message: `This comment is not a valid pint control comment: invalid snooze comment, expected '$TIME $MATCH' got "abc"`,
								Pos: diags.PositionRanges{
									{
										Line:        1,
										FirstColumn: 1,
										LastColumn:  19,
									},
								},
								FirstColumn: 1,
								LastColumn:  19,
								Kind:        diags.Issue,
							},
						},
					},
				},
			},
		},
		{
			input: `# pint snooze 2023-1231 promql/series(http_errors_total{label="this has spaces"})`,
			output: []comments.Comment{
				{
					Type: comments.InvalidComment,
					Value: comments.Invalid{Err: comments.Error{
						Diagnostic: diags.Diagnostic{
							Message: fmt.Sprintf("This comment is not a valid pint control comment: invalid snooze timestamp: %s", errUntil("2023-1231")),
							Pos: diags.PositionRanges{
								{
									Line:        1,
									FirstColumn: 1,
									LastColumn:  81,
								},
							},
							FirstColumn: 1,
							LastColumn:  81,
							Kind:        diags.Issue,
						},
					}},
				},
			},
		},
		{
			input: `# pint snooze 2023-12-31 promql/series(http_errors_total{label="this has spaces"})`,
			output: []comments.Comment{
				{
					Type: comments.SnoozeType,
					Value: comments.Snooze{
						Until: parseUntil("2023-12-31T00:00:00Z"),
						Match: `promql/series(http_errors_total{label="this has spaces"})`,
						Position: comments.Position{
							Offset: 25,
							Pos: diags.PositionRanges{
								{
									Line:        1,
									FirstColumn: 1,
									LastColumn:  82,
								},
							},
						},
					},
				},
			},
		},
		{
			input: `# pint snooze 2023-12-31 promql/series(http_errors_total{label="this has    spaces"})`,
			output: []comments.Comment{
				{
					Type: comments.SnoozeType,
					Value: comments.Snooze{
						Until: parseUntil("2023-12-31T00:00:00Z"),
						Match: `promql/series(http_errors_total{label="this has    spaces"})`,
						Position: comments.Position{
							Offset: 25,
							Pos: diags.PositionRanges{
								{
									Line:        1,
									FirstColumn: 1,
									LastColumn:  85,
								},
							},
						},
					},
				},
			},
		},
		{
			input: "#   pint rule/set",
			output: []comments.Comment{
				{
					Type: comments.InvalidComment,
					Value: comments.Invalid{
						Err: comments.Error{
							Diagnostic: diags.Diagnostic{
								Message: `This comment is not a valid pint control comment: missing rule/set value`,
								Pos: diags.PositionRanges{
									{
										Line:        1,
										FirstColumn: 1,
										LastColumn:  17,
									},
								},
								FirstColumn: 1,
								LastColumn:  17,
								Kind:        diags.Issue,
							},
						},
					},
				},
			},
		},
		{
			input: "# pint rule/set bob and alice",
			output: []comments.Comment{
				{
					Type: comments.RuleSetType,
					Value: comments.RuleSet{
						Value: "bob and alice",
						Position: comments.Position{
							Offset: 16,
							Pos: diags.PositionRanges{
								{
									Line:        1,
									FirstColumn: 1,
									LastColumn:  29,
								},
							},
						},
					},
				},
			},
		},
		{
			input: "code # pint disable xxx  \ncode # alice\n",
			output: []comments.Comment{
				{
					Type: comments.DisableType,
					Value: comments.Disable{
						Match: "xxx",
						Position: comments.Position{
							Offset: 20,
							Pos: diags.PositionRanges{
								{
									Line:        1,
									FirstColumn: 6,
									LastColumn:  25,
								},
							},
						},
					},
					Offset: len("code "),
				},
			},
		},
		{
			input: "code # pint disable xxx yyy \n # pint\tfile/owner bob",
			output: []comments.Comment{
				{
					Type: comments.DisableType,
					Value: comments.Disable{
						Match: "xxx yyy",
						Position: comments.Position{
							Offset: 20,
							Pos: diags.PositionRanges{
								{
									Line:        1,
									FirstColumn: 6,
									LastColumn:  28,
								},
							},
						},
					},
					Offset: len("code "),
				},
				{
					Type: comments.FileOwnerType,
					Value: comments.Owner{
						Name: "bob",
						Position: comments.Position{
							Offset: 19,
							Pos: diags.PositionRanges{
								{
									Line:        2,
									FirstColumn: 2,
									LastColumn:  22,
								},
							},
						},
					},
					Offset: 1,
				},
			},
		},
		{
			input: "# pint rule/set promql/series(found) min-age foo",
			output: []comments.Comment{
				{
					Type: comments.RuleSetType,
					Value: comments.RuleSet{
						Value: "promql/series(found) min-age foo",
						Position: comments.Position{
							Offset: 16,
							Pos: diags.PositionRanges{
								{
									Line:        1,
									FirstColumn: 1,
									LastColumn:  48,
								},
							},
						},
					},
				},
			},
		},
		{
			input: "{#- comment #} # pint ignore/line",
			output: []comments.Comment{
				{
					Type:   comments.IgnoreLineType,
					Offset: len("{#- comment #} "),
				},
			},
		},
		{
			input: "{# comment #} # pint ignore/line",
			output: []comments.Comment{
				{
					Type:   comments.IgnoreLineType,
					Offset: len("{# comment #} "),
				},
			},
		},
		{
			input: "#pint # pint # pint boo # pint ignore/line",
			output: []comments.Comment{
				{
					Type:   comments.IgnoreLineType,
					Offset: len("#pint # pint # pint boo "),
				},
			},
		},
		{
			input: "{# comment #} # pint ignore/line # pint ignore/file",
			output: []comments.Comment{
				{
					Type: comments.InvalidComment,
					Value: comments.Invalid{
						Err: comments.Error{
							Diagnostic: diags.Diagnostic{
								Message: `This comment is not a valid pint control comment: unexpected comment suffix: "# pint ignore/file"`,
								Pos: diags.PositionRanges{
									{
										Line:        1,
										FirstColumn: 15,
										LastColumn:  51,
									},
								},
								FirstColumn: 1,
								LastColumn:  51,
								Kind:        diags.Issue,
							},
						},
					},
					Offset: 14,
				},
			},
		},
		{
			input: "{#- JIRA-12345: foo<->bar example comment ' -#} # pint ignore/line",
			output: []comments.Comment{
				{
					Type:   comments.IgnoreLineType,
					Offset: len("{#- JIRA-12345: foo<->bar example comment ' -#} "),
				},
			},
		},
		// CRLF line endings must be handled the same as LF.
		{
			input: "# pint ignore/file\r\n",
			output: []comments.Comment{
				{Type: comments.IgnoreFileType},
			},
		},
		{
			input: "# pint file/owner bob\r\n",
			output: []comments.Comment{
				{
					Type: comments.FileOwnerType,
					Value: comments.Owner{
						Name: "bob",
						Position: comments.Position{
							Offset: 18,
							Pos: diags.PositionRanges{
								{
									Line:        1,
									FirstColumn: 1,
									LastColumn:  22,
								},
							},
						},
					},
				},
			},
		},
		{
			input: "code # pint disable xxx\r\n # pint\tfile/owner bob\r\n",
			output: []comments.Comment{
				{
					Type: comments.DisableType,
					Value: comments.Disable{
						Match: "xxx",
						Position: comments.Position{
							Offset: 20,
							Pos: diags.PositionRanges{
								{
									Line:        1,
									FirstColumn: 6,
									LastColumn:  24,
								},
							},
						},
					},
					Offset: len("code "),
				},
				{
					Type: comments.FileOwnerType,
					Value: comments.Owner{
						Name: "bob",
						Position: comments.Position{
							Offset: 19,
							Pos: diags.PositionRanges{
								{
									Line:        2,
									FirstColumn: 2,
									LastColumn:  23,
								},
							},
						},
					},
					Offset: 1,
				},
			},
		},
		{
			input: "# pint rule/set promql/series(found)\r\n",
			output: []comments.Comment{
				{
					Type: comments.RuleSetType,
					Value: comments.RuleSet{
						Value: "promql/series(found)",
						Position: comments.Position{
							Offset: 16,
							Pos: diags.PositionRanges{
								{
									Line:        1,
									FirstColumn: 1,
									LastColumn:  37,
								},
							},
						},
					},
				},
			},
		},
		{
			input: "# pint ignore/line\r",
			output: []comments.Comment{
				{Type: comments.IgnoreLineType},
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.input, func(t *testing.T) {
			output := comments.Parse(1, tc.input, 0)
			require.Equal(t, tc.output, output)
		})
	}
}

func TestCommentValueString(t *testing.T) {
	type testCaseT struct {
		comment  comments.Value
		expected string
	}

	parseUntil := func(s string) time.Time {
		until, err := time.Parse(time.RFC3339, s)
		require.NoError(t, err)
		return until
	}

	testCases := []testCaseT{
		{
			comment: comments.Invalid{Err: comments.Error{
				Diagnostic: diags.Diagnostic{
					Message: "foo bar",
					Pos: diags.PositionRanges{
						{
							Line:        1,
							FirstColumn: 1,
							LastColumn:  100,
						},
					},
					FirstColumn: 1,
					LastColumn:  100,
					Kind:        diags.Issue,
				},
			}},
			expected: "foo bar",
		},
		{
			comment: comments.Invalid{Err: comments.Error{
				Diagnostic: diags.Diagnostic{
					Message: "foo bar",
					Pos: diags.PositionRanges{
						{
							Line:        1,
							FirstColumn: 1,
							LastColumn:  100,
						},
					},
					FirstColumn: 1,
					LastColumn:  100,
					Kind:        diags.Issue,
				},
			}},
			expected: "foo bar",
		},
		{
			comment:  comments.Owner{Name: "bob & alice"},
			expected: "bob & alice",
		},
		{
			comment:  comments.Disable{Match: `promql/series({code="500"})`},
			expected: `promql/series({code="500"})`,
		},
		{
			comment:  comments.RuleSet{Value: "bob & alice"},
			expected: "bob & alice",
		},
		{
			comment:  comments.Snooze{Match: `promql/series({code="500"})`, Until: parseUntil("2023-11-28T00:00:00Z")},
			expected: `2023-11-28T00:00:00Z promql/series({code="500"})`,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.expected, func(t *testing.T) {
			require.Equal(t, tc.expected, tc.comment.String())
		})
	}
}

func TestIsRuleComment(t *testing.T) {
	type testCaseT struct {
		ctype    comments.Type
		expected bool
	}

	testCases := []testCaseT{
		{comments.RuleOwnerType, true},
		{comments.DisableType, true},
		{comments.SnoozeType, true},
		{comments.RuleSetType, true},
		{comments.IgnoreLineType, false},
		{comments.FileOwnerType, false},
	}

	for _, tc := range testCases {
		t.Run(fmt.Sprintf("%v", tc.ctype), func(t *testing.T) {
			require.Equal(t, tc.expected, comments.IsRuleComment(tc.ctype))
		})
	}
}

func TestOwnerError(t *testing.T) {
	oe := comments.OwnerError{
		Diagnostic: diags.Diagnostic{Message: "test error"},
	}
	require.Equal(t, "test error", oe.Error())
}
