package api

import (
	"bytes"
	"context"
	"embed"
	"encoding/json"
	"net/http"
	"net/url"
	"path"
	"sort"
	"strings"
	"testing"

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

	"github.com/grafana/grafana/pkg/services/accesscontrol"
	contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
	"github.com/grafana/grafana/pkg/services/dashboards"
	"github.com/grafana/grafana/pkg/services/datasources"
	folder2 "github.com/grafana/grafana/pkg/services/folder"
	. "github.com/grafana/grafana/pkg/services/ngalert/api/compat"
	apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
	ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
	"github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
)

//go:embed test-data/*.*
var testData embed.FS

func TestExportFromPayload(t *testing.T) {
	orgID := int64(1)
	folder := &folder2.Folder{
		UID:      "e4584834-1a87-4dff-8913-8a4748dfca79",
		Title:    "foo bar",
		Fullpath: "foo bar",
	}

	ruleStore := fakes.NewRuleStore(t)
	ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder)

	srv := createService(ruleStore, nil)

	requestFile := "post-rulegroup-101.json"
	rawBody, err := testData.ReadFile(path.Join("test-data", requestFile))
	require.NoError(t, err)
	// compact the json to remove any extra whitespace
	var buf bytes.Buffer
	require.NoError(t, json.Compact(&buf, rawBody))
	// unmarshal the compacted json
	var body apimodels.PostableRuleGroupConfig
	require.NoError(t, json.Unmarshal(buf.Bytes(), &body))

	createRequest := func() *contextmodel.ReqContext {
		return createRequestContextWithPerms(orgID, map[int64]map[string][]string{}, nil)
	}

	t.Run("accept header contains yaml, GET returns text yaml", func(t *testing.T) {
		rc := createRequest()
		rc.Req.Header.Add("Accept", "application/yaml")

		response := srv.ExportFromPayload(rc, body, folder.UID)

		response.WriteTo(rc)

		require.Equal(t, 200, response.Status())
		require.Equal(t, "text/yaml", rc.Context.Resp.Header().Get("Content-Type"))
	})

	t.Run("query format contains yaml, GET returns text yaml", func(t *testing.T) {
		rc := createRequest()
		rc.Req.Form.Set("format", "yaml")

		response := srv.ExportFromPayload(rc, body, folder.UID)
		response.WriteTo(rc)

		require.Equal(t, 200, response.Status())
		require.Equal(t, "text/yaml", rc.Resp.Header().Get("Content-Type"))
	})

	t.Run("query format contains unknown value, GET returns text yaml", func(t *testing.T) {
		rc := createRequest()
		rc.Req.Form.Set("format", "foo")

		response := srv.ExportFromPayload(rc, body, folder.UID)
		response.WriteTo(rc)

		require.Equal(t, 200, response.Status())
		require.Equal(t, "text/yaml", rc.Context.Resp.Header().Get("Content-Type"))
	})

	t.Run("accept header contains json, GET returns json", func(t *testing.T) {
		rc := createRequest()
		rc.Req.Header.Add("Accept", "application/json")

		response := srv.ExportFromPayload(rc, body, folder.UID)
		response.WriteTo(rc)

		require.Equal(t, 200, response.Status())
		require.Equal(t, "application/json", rc.Context.Resp.Header().Get("Content-Type"))
	})

	t.Run("accept header contains json and yaml, GET returns json", func(t *testing.T) {
		rc := createRequest()
		rc.Req.Header.Add("Accept", "application/json, application/yaml")

		response := srv.ExportFromPayload(rc, body, folder.UID)
		response.WriteTo(rc)

		require.Equal(t, 200, response.Status())
		require.Equal(t, "application/json", rc.Context.Resp.Header().Get("Content-Type"))
	})

	t.Run("query param download=true, GET returns content disposition attachment", func(t *testing.T) {
		rc := createRequest()
		rc.Req.Form.Set("download", "true")

		response := srv.ExportFromPayload(rc, body, folder.UID)
		response.WriteTo(rc)

		require.Equal(t, 200, response.Status())
		require.Contains(t, rc.Context.Resp.Header().Get("Content-Disposition"), "attachment")
	})

	t.Run("query param download=false, GET returns empty content disposition", func(t *testing.T) {
		rc := createRequest()
		rc.Req.Form.Set("download", "false")

		response := srv.ExportFromPayload(rc, body, folder.UID)
		response.WriteTo(rc)

		require.Equal(t, 200, response.Status())
		require.Equal(t, "", rc.Context.Resp.Header().Get("Content-Disposition"))
	})

	t.Run("query param download not set, GET returns empty content disposition", func(t *testing.T) {
		rc := createRequest()

		response := srv.ExportFromPayload(rc, body, folder.UID)
		response.WriteTo(rc)

		require.Equal(t, 200, response.Status())
		require.Equal(t, "", rc.Context.Resp.Header().Get("Content-Disposition"))
	})

	t.Run("json body content is as expected", func(t *testing.T) {
		expectedResponse, err := testData.ReadFile(path.Join("test-data", strings.Replace(requestFile, ".json", "-export.json", 1)))
		require.NoError(t, err)

		rc := createRequest()
		rc.Req.Header.Add("Accept", "application/json")

		response := srv.ExportFromPayload(rc, body, folder.UID)
		response.WriteTo(rc)
		t.Log(string(response.Body()))

		require.Equal(t, 200, response.Status())
		require.JSONEq(t, string(expectedResponse), string(response.Body()))
	})

	t.Run("yaml body content is as expected", func(t *testing.T) {
		expectedResponse, err := testData.ReadFile(path.Join("test-data", strings.Replace(requestFile, ".json", "-export.yaml", 1)))
		require.NoError(t, err)

		rc := createRequest()
		rc.Req.Header.Add("Accept", "application/yaml")

		response := srv.ExportFromPayload(rc, body, folder.UID)
		response.WriteTo(rc)
		require.Equal(t, 200, response.Status())
		require.Equal(t, string(expectedResponse), string(response.Body()))
	})

	t.Run("hcl body content is as expected", func(t *testing.T) {
		expectedResponse, err := testData.ReadFile(path.Join("test-data", strings.Replace(requestFile, ".json", "-export.hcl", 1)))
		require.NoError(t, err)

		rc := createRequest()
		rc.Req.Form.Set("format", "hcl")
		rc.Req.Form.Set("download", "false")

		response := srv.ExportFromPayload(rc, body, folder.UID)
		response.WriteTo(rc)

		require.Equal(t, 200, response.Status())
		require.Equal(t, string(expectedResponse), string(response.Body()))
		require.Equal(t, "text/hcl", rc.Resp.Header().Get("Content-Type"))

		t.Run("and add specific headers if download=true", func(t *testing.T) {
			rc := createRequest()
			rc.Req.Form.Set("format", "hcl")
			rc.Req.Form.Set("download", "true")

			response := srv.ExportFromPayload(rc, body, folder.UID)
			response.WriteTo(rc)

			require.Equal(t, 200, response.Status())
			require.Equal(t, string(expectedResponse), string(response.Body()))
			require.Equal(t, "application/terraform+hcl", rc.Resp.Header().Get("Content-Type"))
			require.Equal(t, `attachment;filename=export.tf`, rc.Resp.Header().Get("Content-Disposition"))
		})
	})

	t.Run("hcl body with simplified routing is as expected", func(t *testing.T) {
		requestFile := "post-rulegroup-simplified-routing.json"

		rawBody, err := testData.ReadFile(path.Join("test-data", requestFile))
		require.NoError(t, err)

		var buf bytes.Buffer
		require.NoError(t, json.Compact(&buf, rawBody))

		var body apimodels.PostableRuleGroupConfig
		require.NoError(t, json.Unmarshal(buf.Bytes(), &body))

		expectedResponse, err := testData.ReadFile(path.Join("test-data", strings.Replace(requestFile, ".json", "-export.hcl", 1)))
		require.NoError(t, err)

		rc := createRequest()
		rc.Req.Form.Set("format", "hcl")
		rc.Req.Form.Set("download", "false")

		response := srv.ExportFromPayload(rc, body, folder.UID)
		response.WriteTo(rc)

		require.Equal(t, 200, response.Status())
		require.Equal(t, string(expectedResponse), string(response.Body()))
		require.Equal(t, "text/hcl", rc.Resp.Header().Get("Content-Type"))
	})
}

func TestExportRules(t *testing.T) {
	orgID := int64(1)
	f1 := randFolder()
	f2 := randFolder()

	ruleStore := fakes.NewRuleStore(t)

	hasAccessKey1 := ngmodels.AlertRuleGroupKey{
		OrgID:        orgID,
		NamespaceUID: f1.UID,
		RuleGroup:    "HAS-ACCESS-1",
	}

	gen := ngmodels.RuleGen
	accessQuery := gen.GenerateQuery()
	noAccessQuery := gen.GenerateQuery()
	mdl := map[string]any{
		"foo": "bar",
		"baz": "a <=> b", // explicitly check greater/less than characters
	}
	model, err := json.Marshal(mdl)
	require.NoError(t, err)
	accessQuery.Model = model

	hasAccess1 := gen.With(gen.WithGroupKey(hasAccessKey1), gen.WithQuery(accessQuery), gen.WithUniqueGroupIndex()).GenerateManyRef(5)
	ruleStore.PutRule(context.Background(), hasAccess1...)
	noAccessKey1 := ngmodels.AlertRuleGroupKey{
		OrgID:        orgID,
		NamespaceUID: f1.UID,
		RuleGroup:    "NO-ACCESS",
	}
	noAccess1 := gen.With(gen.WithGroupKey(noAccessKey1), gen.WithQuery(noAccessQuery)).GenerateManyRef(5)
	noAccessRule := gen.With(gen.WithGroupKey(noAccessKey1), gen.WithQuery(accessQuery)).GenerateRef()
	noAccess1 = append(noAccess1, noAccessRule)
	ruleStore.PutRule(context.Background(), noAccess1...)

	hasAccessKey2 := ngmodels.AlertRuleGroupKey{
		OrgID:        orgID,
		NamespaceUID: f2.UID,
		RuleGroup:    "HAS-ACCESS-2",
	}
	hasAccess2 := gen.With(gen.WithGroupKey(hasAccessKey2), gen.WithQuery(accessQuery), gen.WithUniqueGroupIndex()).GenerateManyRef(5)
	ruleStore.PutRule(context.Background(), hasAccess2...)

	noAccessByFolder := gen.With(gen.WithQuery(accessQuery), gen.WithNamespaceUIDNotIn(f1.UID, f2.UID)).GenerateManyRef(10)

	ruleStore.PutRule(context.Background(), noAccessByFolder...)
	// overwrite the folders visible to user because PutRule automatically creates folders in the fake store.
	ruleStore.Folders[orgID] = []*folder2.Folder{f1, f2}

	srv := createService(ruleStore, nil)

	allRules := make([]*ngmodels.AlertRule, 0, len(hasAccess1)+len(hasAccess2)+len(noAccess1))
	allRules = append(allRules, hasAccess1...)
	allRules = append(allRules, hasAccess2...)
	allRules = append(allRules, noAccess1...)

	testCases := []struct {
		title           string
		params          url.Values
		headers         http.Header
		expectedStatus  int
		expectedHeaders http.Header
		expectedRules   []*ngmodels.AlertRule
	}{
		{
			title:          "return all rules user has access to when no parameters",
			expectedStatus: 200,
			expectedHeaders: http.Header{
				"Content-Type": []string{"text/yaml"},
			},
			expectedRules: allRules,
		},
		{
			title: "return all rules in folder",
			params: url.Values{
				"folderUid": []string{hasAccessKey1.NamespaceUID},
			},
			expectedStatus: 200,
			expectedHeaders: http.Header{
				"Content-Type": []string{"text/yaml"},
			},
			expectedRules: append(hasAccess1, noAccess1...),
		},
		{
			title: "return all rules in many folders",
			params: url.Values{
				"folderUid": []string{hasAccessKey1.NamespaceUID, hasAccessKey2.NamespaceUID},
			},
			expectedStatus: 200,
			expectedHeaders: http.Header{
				"Content-Type": []string{"text/yaml"},
			},
			expectedRules: allRules,
		},
		{
			title: "return rules in single group",
			params: url.Values{
				"folderUid": []string{hasAccessKey1.NamespaceUID},
				"group":     []string{hasAccessKey1.RuleGroup},
			},
			expectedStatus: 200,
			expectedHeaders: http.Header{
				"Content-Type": []string{"text/yaml"},
			},
			expectedRules: hasAccess1,
		},
		{
			title: "return single rule",
			params: url.Values{
				"ruleUid": []string{hasAccess1[0].UID},
			},
			expectedStatus: 200,
			expectedHeaders: http.Header{
				"Content-Type": []string{"text/yaml"},
			},
			expectedRules: []*ngmodels.AlertRule{hasAccess1[0]},
		},
		{
			title: "fail if group and many folders",
			params: url.Values{
				"folderUid": []string{hasAccessKey1.NamespaceUID, hasAccessKey2.NamespaceUID},
				"group":     []string{hasAccessKey1.RuleGroup},
			},
			expectedStatus: 400,
		},
		{
			title: "fail if ruleUid and group",
			params: url.Values{
				"folderUid": []string{hasAccessKey1.NamespaceUID},
				"group":     []string{hasAccessKey1.RuleGroup},
				"ruleUid":   []string{hasAccess1[0].UID},
			},
			expectedStatus: 400,
		},
		{
			title: "fail if ruleUid and folderUid",
			params: url.Values{
				"folderUid": []string{hasAccessKey1.NamespaceUID},
				"ruleUid":   []string{hasAccess1[0].UID},
			},
			expectedStatus: 400,
		},
		{
			title: "forbidden if folders are not accessible",
			params: url.Values{
				"folderUid": []string{noAccessByFolder[0].NamespaceUID},
			},
			expectedStatus: http.StatusForbidden,
			expectedRules:  nil,
		},
		{
			title: "return in JSON if header is specified",
			headers: http.Header{
				"Accept": []string{"application/json"},
			},
			expectedStatus: 200,
			expectedRules:  allRules,
			expectedHeaders: http.Header{
				"Content-Type": []string{"application/json"},
			},
		},
		{
			title: "return in JSON if format is specified",
			params: url.Values{
				"format": []string{"json"},
			},
			expectedStatus: 200,
			expectedRules:  allRules,
			expectedHeaders: http.Header{
				"Content-Type": []string{"application/json"},
			},
		},
		{
			title: "return in HCL if format is specified",
			params: url.Values{
				"format": []string{"hcl"},
			},
			expectedStatus: 200,
			expectedRules:  allRules,
			expectedHeaders: http.Header{
				"Content-Type": []string{"text/hcl"},
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.title, func(t *testing.T) {
			rc := createRequestContextWithPerms(orgID, map[int64]map[string][]string{
				orgID: {
					dashboards.ActionFoldersRead:         []string{dashboards.ScopeFoldersProvider.GetResourceScopeUID(f1.UID), dashboards.ScopeFoldersProvider.GetResourceScopeUID(f2.UID)},
					accesscontrol.ActionAlertingRuleRead: []string{dashboards.ScopeFoldersProvider.GetResourceScopeUID(f1.UID), dashboards.ScopeFoldersProvider.GetResourceScopeUID(f2.UID)},
					datasources.ActionQuery:              []string{datasources.ScopeProvider.GetResourceScopeUID(accessQuery.DatasourceUID)},
				},
			}, nil)
			rc.Req.Form = tc.params
			rc.Req.Header = tc.headers

			resp := srv.ExportRules(rc)

			require.Equal(t, tc.expectedStatus, resp.Status())
			if tc.expectedStatus != 200 {
				return
			}
			var exp []ngmodels.AlertRuleGroupWithFolderFullpath
			gr := ngmodels.GroupByAlertRuleGroupKey(tc.expectedRules)
			for key, rules := range gr {
				folder, err := ruleStore.GetNamespaceByUID(context.Background(), key.NamespaceUID, orgID, nil)
				require.NoError(t, err)
				exp = append(exp, ngmodels.NewAlertRuleGroupWithFolderFullpathFromRulesGroup(key, rules, folder.Fullpath))
			}
			sort.SliceStable(exp, func(i, j int) bool {
				gi, gj := exp[i], exp[j]
				if gi.OrgID != gj.OrgID {
					return gi.OrgID < gj.OrgID
				}
				if gi.FolderUID != gj.FolderUID {
					return gi.FolderUID < gj.FolderUID
				}
				return gi.Title < gj.Title
			})
			groups, err := AlertingFileExportFromAlertRuleGroupWithFolderFullpath(exp)
			require.NoError(t, err)

			require.Equal(t, string(exportResponse(rc, groups).Body()), string(resp.Body()))

			resp.WriteTo(rc)
			actualHeaders := rc.Resp.Header()
			for h, hv := range tc.expectedHeaders {
				assert.Contains(t, actualHeaders, h)
				actual := actualHeaders[h]
				require.Equal(t, hv, actual)
			}
		})
	}
}
