// Copyright 2023 Prometheus Team
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package jira

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"
	"time"

	commoncfg "github.com/prometheus/common/config"
	"github.com/prometheus/common/model"
	"github.com/prometheus/common/promslog"
	"github.com/stretchr/testify/require"

	"github.com/prometheus/alertmanager/config"
	"github.com/prometheus/alertmanager/notify"
	"github.com/prometheus/alertmanager/notify/test"
	"github.com/prometheus/alertmanager/template"
	"github.com/prometheus/alertmanager/types"
)

func TestJiraRetry(t *testing.T) {
	notifier, err := New(
		&config.JiraConfig{
			APIURL: &config.URL{
				URL: &url.URL{
					Scheme: "https",
					Host:   "example.atlassian.net",
					Path:   "/rest/api/2",
				},
			},
			HTTPConfig: &commoncfg.HTTPClientConfig{},
		},
		test.CreateTmpl(t),
		promslog.NewNopLogger(),
	)
	require.NoError(t, err)

	retryCodes := append(test.DefaultRetryCodes(), http.StatusTooManyRequests)

	for statusCode, expected := range test.RetryTests(retryCodes) {
		actual, _ := notifier.retrier.Check(statusCode, nil)
		require.Equal(t, expected, actual, fmt.Sprintf("retry - error on status %d", statusCode))
	}
}

func TestJiraTemplating(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/search":
			w.Write([]byte(`{"total": 0, "issues": []}`))
			return
		default:
			dec := json.NewDecoder(r.Body)
			out := make(map[string]any)
			err := dec.Decode(&out)
			if err != nil {
				panic(err)
			}
		}
	}))
	defer srv.Close()
	u, _ := url.Parse(srv.URL)

	for _, tc := range []struct {
		title string
		cfg   *config.JiraConfig

		retry  bool
		errMsg string
	}{
		{
			title: "full-blown message",
			cfg: &config.JiraConfig{
				Summary:     `{{ template "jira.default.summary" . }}`,
				Description: `{{ template "jira.default.description" . }}`,
			},
			retry: false,
		},
		{
			title: "summary with templating errors",
			cfg: &config.JiraConfig{
				Summary: "{{ ",
			},
			errMsg: "template: :1: unclosed action",
		},
		{
			title: "description with templating errors",
			cfg: &config.JiraConfig{
				Summary:     `{{ template "jira.default.summary" . }}`,
				Description: "{{ ",
			},
			errMsg: "template: :1: unclosed action",
		},
		{
			title: "priority with templating errors",
			cfg: &config.JiraConfig{
				Summary:     `{{ template "jira.default.summary" . }}`,
				Description: `{{ template "jira.default.description" . }}`,
				Priority:    "{{ ",
			},
			errMsg: "template: :1: unclosed action",
		},
	} {
		tc := tc

		t.Run(tc.title, func(t *testing.T) {
			tc.cfg.APIURL = &config.URL{URL: u}
			tc.cfg.HTTPConfig = &commoncfg.HTTPClientConfig{}
			pd, err := New(tc.cfg, test.CreateTmpl(t), promslog.NewNopLogger())
			require.NoError(t, err)

			ctx := context.Background()
			ctx = notify.WithGroupKey(ctx, "1")

			ok, err := pd.Notify(ctx, []*types.Alert{
				{
					Alert: model.Alert{
						Labels: model.LabelSet{
							"lbl1": "val1",
						},
						StartsAt: time.Now(),
						EndsAt:   time.Now().Add(time.Hour),
					},
				},
			}...)
			if tc.errMsg == "" {
				require.NoError(t, err)
			} else {
				require.Error(t, err)
				require.Contains(t, err.Error(), tc.errMsg)
			}
			require.Equal(t, tc.retry, ok)
		})
	}
}

func TestJiraNotify(t *testing.T) {
	for _, tc := range []struct {
		title string
		cfg   *config.JiraConfig

		alert *types.Alert

		customFieldAssetFn func(t *testing.T, issue map[string]any)
		searchResponse     issueSearchResult
		issue              issue
		errMsg             string
	}{
		{
			title: "create new issue",
			cfg: &config.JiraConfig{
				Summary:           `{{ template "jira.default.summary" . }}`,
				Description:       `{{ template "jira.default.description" . }}`,
				IssueType:         "Incident",
				Project:           "OPS",
				Priority:          `{{ template "jira.default.priority" . }}`,
				Labels:            []string{"alertmanager", "{{ .GroupLabels.alertname }}"},
				ReopenDuration:    model.Duration(1 * time.Hour),
				ReopenTransition:  "REOPEN",
				ResolveTransition: "CLOSE",
				WontFixResolution: "WONTFIX",
			},
			alert: &types.Alert{
				Alert: model.Alert{
					Labels: model.LabelSet{
						"alertname": "test",
						"instance":  "vm1",
						"severity":  "critical",
					},
					StartsAt: time.Now(),
					EndsAt:   time.Now().Add(time.Hour),
				},
			},
			searchResponse: issueSearchResult{
				Total:  0,
				Issues: []issue{},
			},
			issue: issue{
				Key: "",
				Fields: &issueFields{
					Summary:     "[FIRING:1] test (vm1 critical)",
					Description: "\n\n# Alerts Firing:\n\nLabels:\n  - alertname = test\n  - instance = vm1\n  - severity = critical\n\nAnnotations:\n\nSource: \n\n\n\n\n",
					Issuetype:   &idNameValue{Name: "Incident"},
					Labels:      []string{"ALERT{6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b}", "alertmanager", "test"},
					Project:     &issueProject{Key: "OPS"},
					Priority:    &idNameValue{Name: "High"},
				},
			},
			customFieldAssetFn: func(t *testing.T, issue map[string]any) {},
			errMsg:             "",
		},
		{
			title: "create new issue with custom field and too long summary",
			cfg: &config.JiraConfig{
				Summary:     strings.Repeat("A", maxSummaryLenRunes+10),
				Description: `{{ template "jira.default.description" . }}`,
				IssueType:   "Incident",
				Project:     "OPS",
				Priority:    `{{ template "jira.default.priority" . }}`,
				Labels:      []string{"alertmanager", "{{ .GroupLabels.alertname }}"},
				Fields: map[string]any{
					"components":        map[any]any{"name": "Monitoring"},
					"customfield_10001": "value",
					"customfield_10002": 0,
					"customfield_10003": []any{0},
					"customfield_10004": map[any]any{"value": "red"},
					"customfield_10005": map[any]any{"value": 0},
					"customfield_10006": []map[any]any{{"value": "red"}, {"value": "blue"}, {"value": "green"}},
					"customfield_10007": []map[any]any{{"value": "red"}, {"value": "blue"}, {"value": 0}},
					"customfield_10008": []map[any]any{{"value": 0}, {"value": 1}, {"value": 2}},
				},
				ReopenDuration:    model.Duration(1 * time.Hour),
				ReopenTransition:  "REOPEN",
				ResolveTransition: "CLOSE",
				WontFixResolution: "WONTFIX",
			},
			alert: &types.Alert{
				Alert: model.Alert{
					Labels: model.LabelSet{
						"alertname": "test",
						"instance":  "vm1",
					},
					StartsAt: time.Now(),
					EndsAt:   time.Now().Add(time.Hour),
				},
			},
			searchResponse: issueSearchResult{
				Total:  0,
				Issues: []issue{},
			},
			issue: issue{
				Key: "",
				Fields: &issueFields{
					Summary:     strings.Repeat("A", maxSummaryLenRunes-1) + "…",
					Description: "\n\n# Alerts Firing:\n\nLabels:\n  - alertname = test\n  - instance = vm1\n\nAnnotations:\n\nSource: \n\n\n\n\n",
					Issuetype:   &idNameValue{Name: "Incident"},
					Labels:      []string{"ALERT{6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b}", "alertmanager", "test"},
					Project:     &issueProject{Key: "OPS"},
				},
			},
			customFieldAssetFn: func(t *testing.T, issue map[string]any) {
				require.Equal(t, "value", issue["customfield_10001"])
				require.Equal(t, float64(0), issue["customfield_10002"])
				require.Equal(t, []any{float64(0)}, issue["customfield_10003"])
				require.Equal(t, map[string]any{"value": "red"}, issue["customfield_10004"])
				require.Equal(t, map[string]any{"value": float64(0)}, issue["customfield_10005"])
				require.Equal(t, []any{map[string]any{"value": "red"}, map[string]any{"value": "blue"}, map[string]any{"value": "green"}}, issue["customfield_10006"])
				require.Equal(t, []any{map[string]any{"value": "red"}, map[string]any{"value": "blue"}, map[string]any{"value": float64(0)}}, issue["customfield_10007"])
				require.Equal(t, []any{map[string]any{"value": float64(0)}, map[string]any{"value": float64(1)}, map[string]any{"value": float64(2)}}, issue["customfield_10008"])
			},
			errMsg: "",
		},
		{
			title: "reopen issue",
			cfg: &config.JiraConfig{
				Summary:           `{{ template "jira.default.summary" . }}`,
				Description:       `{{ template "jira.default.description" . }}`,
				IssueType:         "Incident",
				Project:           "OPS",
				Priority:          `{{ template "jira.default.priority" . }}`,
				Labels:            []string{"alertmanager", "{{ .GroupLabels.alertname }}"},
				ReopenDuration:    model.Duration(1 * time.Hour),
				ReopenTransition:  "REOPEN",
				ResolveTransition: "CLOSE",
				WontFixResolution: "WONTFIX",
			},
			alert: &types.Alert{
				Alert: model.Alert{
					Labels: model.LabelSet{
						"alertname": "test",
						"instance":  "vm1",
					},
					StartsAt: time.Now(),
					EndsAt:   time.Now().Add(time.Hour),
				},
			},
			searchResponse: issueSearchResult{
				Total: 1,
				Issues: []issue{
					{
						Key: "OPS-1",
						Fields: &issueFields{
							Status: &issueStatus{
								Name: "Closed",
								StatusCategory: struct {
									Key string `json:"key"`
								}{
									Key: "done",
								},
							},
						},
					},
				},
			},
			issue: issue{
				Key: "",
				Fields: &issueFields{
					Summary:     "[FIRING:1] test (vm1)",
					Description: "\n\n# Alerts Firing:\n\nLabels:\n  - alertname = test\n  - instance = vm1\n\nAnnotations:\n\nSource: \n\n\n\n\n",
					Issuetype:   &idNameValue{Name: "Incident"},
					Labels:      []string{"ALERT{6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b}", "alertmanager", "test"},
					Project:     &issueProject{Key: "OPS"},
					Priority:    &idNameValue{Name: "High"},
				},
			},
			customFieldAssetFn: func(t *testing.T, issue map[string]any) {},
			errMsg:             "",
		},
		{
			title: "error resolve transition not found",
			cfg: &config.JiraConfig{
				Summary:           `{{ template "jira.default.summary" . }}`,
				Description:       `{{ template "jira.default.description" . }}`,
				IssueType:         "Incident",
				Project:           "OPS",
				Priority:          `{{ template "jira.default.priority" . }}`,
				Labels:            []string{"alertmanager", "{{ .GroupLabels.alertname }}"},
				ReopenDuration:    model.Duration(1 * time.Hour),
				ReopenTransition:  "REOPEN",
				ResolveTransition: "CLOSE",
				WontFixResolution: "WONTFIX",
			},
			alert: &types.Alert{
				Alert: model.Alert{
					Labels: model.LabelSet{
						"alertname": "test",
						"instance":  "vm1",
					},
					StartsAt: time.Now().Add(-time.Hour),
					EndsAt:   time.Now().Add(-time.Hour),
				},
			},
			searchResponse: issueSearchResult{
				Total: 1,
				Issues: []issue{
					{
						Key: "OPS-3",
						Fields: &issueFields{
							Status: &issueStatus{
								Name: "Open",
								StatusCategory: struct {
									Key string `json:"key"`
								}{
									Key: "open",
								},
							},
						},
					},
				},
			},
			issue: issue{
				Key: "",
				Fields: &issueFields{
					Summary:     "[RESOLVED] test (vm1)",
					Description: "\n\n\n# Alerts Resolved:\n\nLabels:\n  - alertname = test\n  - instance = vm1\n\nAnnotations:\n\nSource: \n\n\n\n",
					Issuetype:   &idNameValue{Name: "Incident"},
					Labels:      []string{"ALERT{6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b}", "alertmanager", "test"},
					Project:     &issueProject{Key: "OPS"},
				},
			},
			customFieldAssetFn: func(t *testing.T, issue map[string]any) {},
			errMsg:             "can't find transition CLOSE for issue OPS-3",
		},
		{
			title: "error reopen transition not found",
			cfg: &config.JiraConfig{
				Summary:           `{{ template "jira.default.summary" . }}`,
				Description:       `{{ template "jira.default.description" . }}`,
				IssueType:         "Incident",
				Project:           "OPS",
				Priority:          `{{ template "jira.default.priority" . }}`,
				Labels:            []string{"alertmanager", "{{ .GroupLabels.alertname }}"},
				ReopenDuration:    model.Duration(1 * time.Hour),
				ReopenTransition:  "REOPEN",
				ResolveTransition: "CLOSE",
				WontFixResolution: "WONTFIX",
			},
			alert: &types.Alert{
				Alert: model.Alert{
					Labels: model.LabelSet{
						"alertname": "test",
						"instance":  "vm1",
					},
					StartsAt: time.Now(),
					EndsAt:   time.Now().Add(time.Hour),
				},
			},
			searchResponse: issueSearchResult{
				Total: 1,
				Issues: []issue{
					{
						Key: "OPS-3",
						Fields: &issueFields{
							Status: &issueStatus{
								Name: "Closed",
								StatusCategory: struct {
									Key string `json:"key"`
								}{
									Key: "done",
								},
							},
						},
					},
				},
			},
			issue: issue{
				Key: "",
				Fields: &issueFields{
					Summary:     "[FIRING:1] test (vm1)",
					Description: "\n\n# Alerts Firing:\n\nLabels:\n  - alertname = test\n  - instance = vm1\n\nAnnotations:\n\nSource: \n\n\n\n\n",
					Issuetype:   &idNameValue{Name: "Incident"},
					Labels:      []string{"ALERT{6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b}", "alertmanager", "test"},
					Project:     &issueProject{Key: "OPS"},
				},
			},
			customFieldAssetFn: func(t *testing.T, issue map[string]any) {},
			errMsg:             "can't find transition REOPEN for issue OPS-3",
		},
	} {
		tc := tc

		t.Run(tc.title, func(t *testing.T) {
			srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				switch r.URL.Path {
				case "/search":
					enc := json.NewEncoder(w)
					if err := enc.Encode(tc.searchResponse); err != nil {
						panic(err)
					}

					return
				case "/issue/OPS-1/transitions":
					switch r.Method {
					case http.MethodGet:
						w.WriteHeader(http.StatusOK)

						transitions := issueTransitions{
							Transitions: []idNameValue{
								{ID: "12345", Name: "REOPEN"},
							},
						}

						enc := json.NewEncoder(w)
						if err := enc.Encode(transitions); err != nil {
							panic(err)
						}
					case http.MethodPost:
						dec := json.NewDecoder(r.Body)
						var out issue
						err := dec.Decode(&out)
						if err != nil {
							panic(err)
						}

						require.Equal(t, issue{Transition: &idNameValue{ID: "12345"}}, out)
						w.WriteHeader(http.StatusNoContent)
					default:
						t.Fatalf("unexpected method %s", r.Method)
					}

					return
				case "/issue/OPS-2/transitions":
					switch r.Method {
					case http.MethodGet:
						w.WriteHeader(http.StatusOK)

						transitions := issueTransitions{
							Transitions: []idNameValue{
								{ID: "54321", Name: "CLOSE"},
							},
						}

						enc := json.NewEncoder(w)
						if err := enc.Encode(transitions); err != nil {
							panic(err)
						}
					case http.MethodPost:
						dec := json.NewDecoder(r.Body)
						var out issue
						err := dec.Decode(&out)
						if err != nil {
							panic(err)
						}

						require.Equal(t, issue{Transition: &idNameValue{ID: "54321"}}, out)
						w.WriteHeader(http.StatusNoContent)
					default:
						t.Fatalf("unexpected method %s", r.Method)
					}

					return
				case "/issue/OPS-3/transitions":
					switch r.Method {
					case http.MethodGet:
						w.WriteHeader(http.StatusOK)

						transitions := issueTransitions{
							Transitions: []idNameValue{},
						}

						enc := json.NewEncoder(w)
						if err := enc.Encode(transitions); err != nil {
							panic(err)
						}
					default:
						t.Fatalf("unexpected method %s", r.Method)
					}

					return
				case "/issue/OPS-1":
				case "/issue/OPS-2":
				case "/issue/OPS-3":
					fallthrough
				case "/issue":
					body, err := io.ReadAll(r.Body)
					if err != nil {
						panic(err)
					}

					var (
						issue issue
						raw   map[string]any
					)

					if err := json.Unmarshal(body, &issue); err != nil {
						panic(err)
					}

					// We don't care about the key, so copy it over.
					issue.Fields.Fields = tc.issue.Fields.Fields

					require.Equal(t, tc.issue.Key, issue.Key)
					require.Equal(t, tc.issue.Fields, issue.Fields)

					if err := json.Unmarshal(body, &raw); err != nil {
						panic(err)
					}

					if fields, ok := raw["fields"].(map[string]any); ok {
						tc.customFieldAssetFn(t, fields)
					} else {
						t.Errorf("fields should a map of string")
					}

					w.WriteHeader(http.StatusCreated)

					w.WriteHeader(http.StatusCreated)

				default:
					t.Fatalf("unexpected path %s", r.URL.Path)
				}
			}))
			defer srv.Close()
			u, _ := url.Parse(srv.URL)

			tc.cfg.APIURL = &config.URL{URL: u}
			tc.cfg.HTTPConfig = &commoncfg.HTTPClientConfig{}

			notifier, err := New(tc.cfg, test.CreateTmpl(t), promslog.NewNopLogger())
			require.NoError(t, err)

			ctx := context.Background()
			ctx = notify.WithGroupKey(ctx, "1")
			ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": "test"})

			_, err = notifier.Notify(ctx, tc.alert)
			if tc.errMsg == "" {
				require.NoError(t, err)
			} else {
				require.Error(t, err)
				require.EqualError(t, err, tc.errMsg)
			}
		})
	}
}

func TestJiraPriority(t *testing.T) {
	t.Parallel()
	for _, tc := range []struct {
		title string

		alerts []*types.Alert

		expectedPriority string
	}{
		{
			"empty",
			[]*types.Alert{
				{
					Alert: model.Alert{
						Labels: model.LabelSet{
							"alertname": "test",
							"instance":  "vm1",
						},
						StartsAt: time.Now(),
						EndsAt:   time.Now().Add(time.Hour),
					},
				},
			},
			"",
		},
		{
			"critical",
			[]*types.Alert{
				{
					Alert: model.Alert{
						Labels: model.LabelSet{
							"alertname": "test",
							"instance":  "vm1",
							"severity":  "critical",
						},
						StartsAt: time.Now(),
						EndsAt:   time.Now().Add(time.Hour),
					},
				},
			},
			"High",
		},
		{
			"warning",
			[]*types.Alert{
				{
					Alert: model.Alert{
						Labels: model.LabelSet{
							"alertname": "test",
							"instance":  "vm1",
							"severity":  "warning",
						},
						StartsAt: time.Now(),
						EndsAt:   time.Now().Add(time.Hour),
					},
				},
			},
			"Medium",
		},
		{
			"info",
			[]*types.Alert{
				{
					Alert: model.Alert{
						Labels: model.LabelSet{
							"alertname": "test",
							"instance":  "vm1",
							"severity":  "info",
						},
						StartsAt: time.Now(),
						EndsAt:   time.Now().Add(time.Hour),
					},
				},
			},
			"Low",
		},
		{
			"critical+warning+info",
			[]*types.Alert{
				{
					Alert: model.Alert{
						Labels: model.LabelSet{
							"alertname": "test",
							"instance":  "vm1",
							"severity":  "critical",
						},
						StartsAt: time.Now(),
						EndsAt:   time.Now().Add(time.Hour),
					},
				},
				{
					Alert: model.Alert{
						Labels: model.LabelSet{
							"alertname": "test",
							"instance":  "vm1",
							"severity":  "warning",
						},
						StartsAt: time.Now(),
						EndsAt:   time.Now().Add(time.Hour),
					},
				},
				{
					Alert: model.Alert{
						Labels: model.LabelSet{
							"alertname": "test",
							"instance":  "vm1",
							"severity":  "info",
						},
						StartsAt: time.Now(),
						EndsAt:   time.Now().Add(time.Hour),
					},
				},
			},
			"High",
		},
		{
			"warning+info",
			[]*types.Alert{
				{
					Alert: model.Alert{
						Labels: model.LabelSet{
							"alertname": "test",
							"instance":  "vm1",
							"severity":  "warning",
						},
						StartsAt: time.Now(),
						EndsAt:   time.Now().Add(time.Hour),
					},
				},
				{
					Alert: model.Alert{
						Labels: model.LabelSet{
							"alertname": "test",
							"instance":  "vm1",
							"severity":  "info",
						},
						StartsAt: time.Now(),
						EndsAt:   time.Now().Add(time.Hour),
					},
				},
			},
			"Medium",
		},
		{
			"critical(resolved)+warning+info",
			[]*types.Alert{
				{
					Alert: model.Alert{
						Labels: model.LabelSet{
							"alertname": "test",
							"instance":  "vm1",
							"severity":  "critical",
						},
						StartsAt: time.Now().Add(-time.Hour),
						EndsAt:   time.Now().Add(-time.Hour),
					},
				},
				{
					Alert: model.Alert{
						Labels: model.LabelSet{
							"alertname": "test",
							"instance":  "vm1",
							"severity":  "warning",
						},
						StartsAt: time.Now(),
						EndsAt:   time.Now().Add(time.Hour),
					},
				},
				{
					Alert: model.Alert{
						Labels: model.LabelSet{
							"alertname": "test",
							"instance":  "vm1",
							"severity":  "info",
						},
						StartsAt: time.Now(),
						EndsAt:   time.Now().Add(time.Hour),
					},
				},
			},
			"Medium",
		},
	} {
		tc := tc

		t.Run(tc.title, func(t *testing.T) {
			t.Parallel()
			u, err := url.Parse("http://example.com/")
			require.NoError(t, err)

			tmpl, err := template.FromGlobs([]string{})
			require.NoError(t, err)

			tmpl.ExternalURL = u

			var (
				data = tmpl.Data("jira", model.LabelSet{}, tc.alerts...)

				tmplTextErr  error
				tmplText     = notify.TmplText(tmpl, data, &tmplTextErr)
				tmplTextFunc = func(tmpl string) (string, error) {
					result := tmplText(tmpl)
					return result, tmplTextErr
				}
			)

			priority, err := tmplTextFunc(`{{ template "jira.default.priority" . }}`)
			require.NoError(t, err)
			require.Equal(t, tc.expectedPriority, priority)
		})
	}
}
