package alerting

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"testing"

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

	"github.com/grafana/grafana/pkg/configprovider"
	apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
	"github.com/grafana/grafana/pkg/services/org"
	"github.com/grafana/grafana/pkg/services/org/orgimpl"
	"github.com/grafana/grafana/pkg/services/quota/quotaimpl"
	"github.com/grafana/grafana/pkg/services/user"
	"github.com/grafana/grafana/pkg/tests/testinfra"
	"github.com/grafana/grafana/pkg/util/testutil"
)

func TestIntegration_NamespacingForRules(t *testing.T) {
	testutil.SkipIntegrationTestInShortMode(t)

	dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
		DisableLegacyAlerting: true,
		EnableUnifiedAlerting: true,
		DisableAnonymous:      true,
		AppModeProduction:     true,
	})

	grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p)
	store, cfg := env.SQLStore, env.Cfg
	orgID := int64(1)
	createUser(t, store, cfg, user.CreateUserCommand{
		DefaultOrgRole: string(org.RoleEditor),
		OrgID:          orgID,
		Password:       "editor",
		Login:          "editor",
	})
	createUser(t, store, cfg, user.CreateUserCommand{
		DefaultOrgRole: string(org.RoleViewer),
		OrgID:          orgID,
		Password:       "viewer",
		Login:          "viewer",
	})

	adminClient := newAlertingApiClient(grafanaListedAddr, "admin", "admin")
	editorClient := newAlertingApiClient(grafanaListedAddr, "editor", "editor")
	viewerClient := newAlertingApiClient(grafanaListedAddr, "viewer", "viewer")

	// create test folders, with a rule in each folder
	folder1UID := "test-folder-1"
	folder2UID := "test-folder-2"
	folder3UID := "test-folder-3"
	adminClient.CreateFolder(t, folder1UID, "Test Folder 1")
	adminClient.CreateFolder(t, folder2UID, "Test Folder 2")
	adminClient.CreateFolder(t, folder3UID, "Test Folder 3")
	rule1 := createTestAlertRule("Test Rule 1", folder1UID)
	rule2 := createTestAlertRule("Test Rule 2", folder2UID)
	rule3 := createTestAlertRule("Test Rule 3", folder3UID)
	group1 := apimodels.PostableRuleGroupConfig{
		Name:     "test-group-1",
		Interval: 60,
		Rules:    []apimodels.PostableExtendedRuleNode{rule1},
	}
	group2 := apimodels.PostableRuleGroupConfig{
		Name:     "test-group-2",
		Interval: 60,
		Rules:    []apimodels.PostableExtendedRuleNode{rule2},
	}
	group3 := apimodels.PostableRuleGroupConfig{
		Name:     "test-group-3",
		Interval: 60,
		Rules:    []apimodels.PostableExtendedRuleNode{rule3},
	}
	adminClient.PostRulesGroup(t, folder1UID, &group1, false)
	adminClient.PostRulesGroup(t, folder2UID, &group2, false)
	adminClient.PostRulesGroup(t, folder3UID, &group3, false)

	t.Run("admin, editor, and viewer should be able to see all rules in a given folder", func(t *testing.T) {
		// admin
		rules, status, _ := adminClient.GetAllRulesGroupInFolderWithStatus(t, folder1UID)
		require.Equal(t, http.StatusAccepted, status)
		require.Len(t, rules, 1)

		// editor
		rules, status, _ = editorClient.GetAllRulesGroupInFolderWithStatus(t, folder1UID)
		require.Equal(t, http.StatusAccepted, status)
		require.Len(t, rules, 1)

		// viewer
		rules, status, _ = viewerClient.GetAllRulesGroupInFolderWithStatus(t, folder1UID)
		require.Equal(t, http.StatusAccepted, status)
		require.Len(t, rules, 1)
	})

	t.Run("admin should be able to access restricted folder, but no one else can", func(t *testing.T) {
		restrictedFolderUID := "restricted-folder"
		adminClient.CreateFolder(t, restrictedFolderUID, "Restricted Folder")
		restrictedRule := createTestAlertRule("Restricted Rule", restrictedFolderUID)
		restrictedGroup := apimodels.PostableRuleGroupConfig{
			Name:     "restricted-group",
			Interval: 60,
			Rules:    []apimodels.PostableExtendedRuleNode{restrictedRule},
		}
		adminClient.PostRulesGroup(t, restrictedFolderUID, &restrictedGroup, false)
		setFolderPermissions(t, grafanaListedAddr, restrictedFolderUID, []map[string]interface{}{
			{
				"userId":     1,
				"permission": 4,
			},
		})

		// admin ok
		_, status, _ := adminClient.GetAllRulesGroupInFolderWithStatus(t, restrictedFolderUID)
		require.Equal(t, http.StatusAccepted, status)

		// editor and viewer forbidden
		_, status, _ = editorClient.GetAllRulesGroupInFolderWithStatus(t, restrictedFolderUID)
		require.Equal(t, http.StatusForbidden, status)

		_, status, _ = viewerClient.GetAllRulesGroupInFolderWithStatus(t, restrictedFolderUID)
		require.Equal(t, http.StatusForbidden, status)
	})

	t.Run("errors when a folder does not exist", func(t *testing.T) {
		_, status, _ := adminClient.GetAllRulesGroupInFolderWithStatus(t, "non-existent-folder")
		// even if a folder does not exist, it will return a forbidden error (so users cannot enumerate folders)
		require.Equal(t, http.StatusForbidden, status)
	})

	t.Run("permissions are respected for nested folders", func(t *testing.T) {
		parentFolderUID := "parent-folder"
		childFolderUID := "child-folder"
		adminClient.CreateFolder(t, parentFolderUID, "Parent Folder")
		adminClient.CreateFolder(t, childFolderUID, "Child Folder", parentFolderUID)
		parentRule := createTestAlertRule("Parent Rule", parentFolderUID)
		parentGroup := apimodels.PostableRuleGroupConfig{
			Name:     "parent-group",
			Interval: 60,
			Rules:    []apimodels.PostableExtendedRuleNode{parentRule},
		}
		adminClient.PostRulesGroup(t, parentFolderUID, &parentGroup, false)
		childRule := createTestAlertRule("Child Rule", childFolderUID)
		childGroup := apimodels.PostableRuleGroupConfig{
			Name:     "child-group",
			Interval: 60,
			Rules:    []apimodels.PostableExtendedRuleNode{childRule},
		}
		adminClient.PostRulesGroup(t, childFolderUID, &childGroup, false)

		// allow admin to access parent folder
		setFolderPermissions(t, grafanaListedAddr, parentFolderUID, []map[string]interface{}{
			{
				"userId":     1,
				"permission": 4,
			},
		})

		// admin can get both folders
		allRules, status, _ := adminClient.GetAllRulesWithStatus(t)
		require.Equal(t, http.StatusOK, status)
		require.Contains(t, allRules, "Parent Folder")
		require.Contains(t, allRules, "Parent Folder/Child Folder")

		// editor cannot access either
		allRules, status, _ = editorClient.GetAllRulesWithStatus(t)
		require.Equal(t, http.StatusOK, status)
		require.NotContains(t, allRules, "Parent Folder")
		require.NotContains(t, allRules, "Parent Folder/Child Folder")

		// viewer cannot access either folder
		allRules, status, _ = viewerClient.GetAllRulesWithStatus(t)
		require.Equal(t, http.StatusOK, status)
		require.NotContains(t, allRules, "Parent Folder")
		require.NotContains(t, allRules, "Parent Folder/Child Folder")
	})

	t.Run("org separation", func(t *testing.T) {
		cfgProvider, err := configprovider.ProvideService(cfg)
		require.NoError(t, err)
		orgService, err := orgimpl.ProvideService(store, cfg, quotaimpl.ProvideService(context.Background(), store, cfgProvider))
		require.NoError(t, err)
		newOrg, err := orgService.CreateWithMember(context.Background(), &org.CreateOrgCommand{Name: "Test Org 2"})
		require.NoError(t, err)
		createUser(t, store, cfg, user.CreateUserCommand{
			DefaultOrgRole: string(org.RoleAdmin),
			OrgID:          newOrg.ID,
			Password:       "other-admin",
			Login:          "other-admin-folder-perms",
		})
		otherOrgFolderUID := "other-org-folder"
		otherOrgClient := newAlertingApiClient(grafanaListedAddr, "other-admin-folder-perms", "other-admin")
		otherOrgClient.CreateFolder(t, otherOrgFolderUID, "Other Org Folder")
		otherOrgRule := createTestAlertRule("Other Org Rule", otherOrgFolderUID)
		otherOrgGroup := apimodels.PostableRuleGroupConfig{
			Name:     "other-org-group",
			Interval: 60,
			Rules:    []apimodels.PostableExtendedRuleNode{otherOrgRule},
		}
		otherOrgClient.PostRulesGroup(t, otherOrgFolderUID, &otherOrgGroup, false)

		// admin from org 1 cannot access org 2 alert rules
		allRules, status, _ := adminClient.GetAllRulesWithStatus(t)
		require.Equal(t, http.StatusOK, status)
		require.NotContains(t, allRules, otherOrgFolderUID)

		// admin from org 2 cannot access org 1 alert rules
		allRules, status, _ = otherOrgClient.GetAllRulesWithStatus(t)
		require.Equal(t, http.StatusOK, status)
		require.NotContains(t, allRules, folder1UID)
		require.NotContains(t, allRules, folder2UID)
		require.NotContains(t, allRules, folder3UID)
	})
}

func TestIntegration_NamespacingForPrometheusRules(t *testing.T) {
	testutil.SkipIntegrationTestInShortMode(t)

	dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
		DisableLegacyAlerting: true,
		EnableUnifiedAlerting: true,
		DisableAnonymous:      true,
		AppModeProduction:     true,
	})
	grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p)
	store, cfg := env.SQLStore, env.Cfg
	orgID := int64(1)
	createUser(t, store, cfg, user.CreateUserCommand{
		DefaultOrgRole: string(org.RoleEditor),
		OrgID:          orgID,
		Password:       "editor",
		Login:          "editor",
	})
	createUser(t, store, cfg, user.CreateUserCommand{
		DefaultOrgRole: string(org.RoleViewer),
		OrgID:          orgID,
		Password:       "viewer",
		Login:          "viewer",
	})
	adminClient := newAlertingApiClient(grafanaListedAddr, "admin", "admin")
	editorClient := newAlertingApiClient(grafanaListedAddr, "editor", "editor")
	viewerClient := newAlertingApiClient(grafanaListedAddr, "viewer", "viewer")

	// create prometheus rules in 3 separate folders
	ds := adminClient.CreateDatasource(t, "prometheus")
	dsUID := ds.Body.Datasource.UID
	folder1UID := "prometheus-folder-1"
	folder2UID := "prometheus-folder-2"
	folder3UID := "prometheus-folder-3"
	adminClient.CreateFolder(t, folder1UID, "Prometheus Folder 1")
	adminClient.CreateFolder(t, folder2UID, "Prometheus Folder 2")
	adminClient.CreateFolder(t, folder3UID, "Prometheus Folder 3")
	duration1m := model.Duration(1 * 60 * 1000)
	prometheusRules1 := map[string][]apimodels.PrometheusRuleGroup{
		"Prometheus Folder 1": {
			{
				Name: "test-group-1",
				Rules: []apimodels.PrometheusRule{
					{
						Alert: "HighCPUUsage",
						Expr:  "cpu_usage > 80",
						For:   &duration1m,
						Labels: map[string]string{
							"severity": "warning",
						},
						Annotations: map[string]string{
							"summary": "High CPU usage detected",
						},
					},
				},
			},
		},
	}
	prometheusRules2 := map[string][]apimodels.PrometheusRuleGroup{
		"Prometheus Folder 2": {
			{
				Name: "test-group-2",
				Rules: []apimodels.PrometheusRule{
					{
						Alert: "HighMemoryUsage",
						Expr:  "memory_usage > 90",
						For:   &duration1m,
						Labels: map[string]string{
							"severity": "critical",
						},
						Annotations: map[string]string{
							"summary": "High memory usage detected",
						},
					},
				},
			},
		},
	}
	prometheusRules3 := map[string][]apimodels.PrometheusRuleGroup{
		"Prometheus Folder 3": {
			{
				Name: "test-group-3",
				Rules: []apimodels.PrometheusRule{
					{
						Alert: "DiskSpaceLow",
						Expr:  "disk_usage > 95",
						For:   &duration1m,
						Labels: map[string]string{
							"severity": "warning",
						},
						Annotations: map[string]string{
							"summary": "Disk space is running low",
						},
					},
				},
			},
		},
	}
	// then import them
	headers := map[string]string{
		"Content-Type":     "application/json",
		"X-Datasource-UID": dsUID,
	}
	adminClient.ConvertPrometheusPostRuleGroups(t, dsUID, prometheusRules1, headers)
	adminClient.ConvertPrometheusPostRuleGroups(t, dsUID, prometheusRules2, headers)
	adminClient.ConvertPrometheusPostRuleGroups(t, dsUID, prometheusRules3, headers)

	t.Run("admin, editor, and viewer should be able to get all Prometheus rules", func(t *testing.T) {
		// admin
		rules := adminClient.ConvertPrometheusGetAllRules(t, headers)
		require.Len(t, rules, 3)
		require.Contains(t, rules, "Prometheus Folder 1")
		require.Contains(t, rules, "Prometheus Folder 2")
		require.Contains(t, rules, "Prometheus Folder 3")

		// editor
		rules = editorClient.ConvertPrometheusGetAllRules(t, headers)
		require.Len(t, rules, 3)
		require.Contains(t, rules, "Prometheus Folder 1")
		require.Contains(t, rules, "Prometheus Folder 2")
		require.Contains(t, rules, "Prometheus Folder 3")

		// viewer
		rules = viewerClient.ConvertPrometheusGetAllRules(t, headers)
		require.Len(t, rules, 3)
		require.Contains(t, rules, "Prometheus Folder 1")
		require.Contains(t, rules, "Prometheus Folder 2")
		require.Contains(t, rules, "Prometheus Folder 3")
	})

	t.Run("only admin can view restricted folder", func(t *testing.T) {
		restrictedFolderUID := "restricted-prometheus-folder"
		adminClient.CreateFolder(t, restrictedFolderUID, "Restricted Prometheus Folder")
		restrictedPrometheusRules := map[string][]apimodels.PrometheusRuleGroup{
			"Restricted Prometheus Folder": {
				{
					Name: "restricted-group",
					Rules: []apimodels.PrometheusRule{
						{
							Alert: "RestrictedAlert",
							Expr:  "restricted_metric > 100",
							For:   &duration1m,
							Labels: map[string]string{
								"severity": "critical",
							},
						},
					},
				},
			},
		}
		adminClient.ConvertPrometheusPostRuleGroups(t, dsUID, restrictedPrometheusRules, headers)
		setFolderPermissions(t, grafanaListedAddr, restrictedFolderUID, []map[string]interface{}{
			{
				"userId":     1,
				"permission": 4,
			},
		})

		// admin can see the restricted folder
		rules := adminClient.ConvertPrometheusGetAllRules(t, headers)
		require.Contains(t, rules, "Restricted Prometheus Folder")

		// editor and viewer cannot
		rules = editorClient.ConvertPrometheusGetAllRules(t, headers)
		require.NotContains(t, rules, "Restricted Prometheus Folder")
		rules = viewerClient.ConvertPrometheusGetAllRules(t, headers)
		require.NotContains(t, rules, "Restricted Prometheus Folder")
	})

	t.Run("should maintain org separation for Prometheus rules", func(t *testing.T) {
		cfgProvider, err := configprovider.ProvideService(cfg)
		require.NoError(t, err)
		orgService, err := orgimpl.ProvideService(store, cfg, quotaimpl.ProvideService(context.Background(), store, cfgProvider))
		require.NoError(t, err)
		newOrg, err := orgService.CreateWithMember(context.Background(), &org.CreateOrgCommand{Name: "Prometheus Test Org 2"})
		require.NoError(t, err)
		createUser(t, store, cfg, user.CreateUserCommand{
			DefaultOrgRole: string(org.RoleAdmin),
			OrgID:          newOrg.ID,
			Password:       "other-prometheus-admin",
			Login:          "other-prometheus-admin",
		})
		otherOrgClient := newAlertingApiClient(grafanaListedAddr, "other-prometheus-admin", "other-prometheus-admin")
		otherOrgDs := otherOrgClient.CreateDatasource(t, "prometheus")
		otherOrgDsUID := otherOrgDs.Body.Datasource.UID
		otherOrgFolderUID := "other-org-prometheus-folder"
		otherOrgClient.CreateFolder(t, otherOrgFolderUID, "Other Org Prometheus Folder")
		otherOrgPrometheusRules := map[string][]apimodels.PrometheusRuleGroup{
			"Other Org Prometheus Folder": {
				{
					Name: "other-org-group",
					Rules: []apimodels.PrometheusRule{
						{
							Alert: "OtherOrgAlert",
							Expr:  "other_org_metric > 75",
							For:   &duration1m,
						},
					},
				},
			},
		}
		otherOrgHeaders := map[string]string{
			"Content-Type":     "application/json",
			"X-Datasource-UID": otherOrgDsUID,
		}
		otherOrgClient.ConvertPrometheusPostRuleGroups(t, otherOrgDsUID, otherOrgPrometheusRules, otherOrgHeaders)

		// admin from org 1 cannot see org 2 rules
		rules := adminClient.ConvertPrometheusGetAllRules(t, headers)
		require.NotContains(t, rules, "Other Org Prometheus Folder")

		// admin from org 2 cannot see org 1 rules
		rules = otherOrgClient.ConvertPrometheusGetAllRules(t, otherOrgHeaders)
		require.NotContains(t, rules, "Prometheus Folder 1")
		require.NotContains(t, rules, "Prometheus Folder 2")
		require.NotContains(t, rules, "Prometheus Folder 3")
		require.Contains(t, rules, "Other Org Prometheus Folder")
	})
}

func createTestAlertRule(title, folderUID string) apimodels.PostableExtendedRuleNode {
	return apimodels.PostableExtendedRuleNode{
		GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
			Title:     title,
			Condition: "A",
			Data: []apimodels.AlertQuery{
				{
					RefID: "A",
					RelativeTimeRange: apimodels.RelativeTimeRange{
						From: 600,
						To:   0,
					},
					DatasourceUID: "-100",
					Model: json.RawMessage(`{
						"type": "math",
						"expression": "2 + 3 > 1"
					}`),
				},
			},
			NoDataState:  "NoData",
			ExecErrState: "Error",
		},
	}
}

func setFolderPermissions(t *testing.T, grafanaListedAddr string, folderUID string, permissions []map[string]interface{}) {
	t.Helper()

	permissionPayload := map[string]interface{}{
		"items": permissions,
	}

	payloadBytes, err := json.Marshal(permissionPayload)
	require.NoError(t, err)

	u := fmt.Sprintf("http://admin:admin@%s/api/folders/%s/permissions", grafanaListedAddr, folderUID)
	resp, err := http.Post(u, "application/json", bytes.NewBuffer(payloadBytes)) // nolint:gosec
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, resp.StatusCode)
	err = resp.Body.Close()
	require.NoError(t, err)
}
