package receivers

import (
	"context"
	"embed"
	"encoding/json"
	"fmt"
	"maps"
	"net/http"
	"path"
	"slices"
	"strings"
	"testing"

	"github.com/grafana/alerting/notify/notifytest"
	"github.com/grafana/alerting/receivers/line"
	"github.com/grafana/alerting/receivers/schema"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"k8s.io/apimachinery/pkg/api/errors"
	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/types"

	"github.com/grafana/alerting/notify"

	"github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1"
	common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
	"github.com/grafana/grafana/pkg/bus"
	"github.com/grafana/grafana/pkg/infra/tracing"
	"github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/routingtree"
	"github.com/grafana/grafana/pkg/services/accesscontrol"
	"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
	"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
	"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
	"github.com/grafana/grafana/pkg/services/dashboards"
	"github.com/grafana/grafana/pkg/services/folder/foldertest"
	"github.com/grafana/grafana/pkg/services/ngalert/api"
	"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/store"
	"github.com/grafana/grafana/pkg/services/org"
	"github.com/grafana/grafana/pkg/tests/api/alerting"
	"github.com/grafana/grafana/pkg/tests/apis"
	test_common "github.com/grafana/grafana/pkg/tests/apis/alerting/notifications/common"
	"github.com/grafana/grafana/pkg/tests/testinfra"
	"github.com/grafana/grafana/pkg/tests/testsuite"
	"github.com/grafana/grafana/pkg/util"
	"github.com/grafana/grafana/pkg/util/testutil"
)

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

func TestMain(m *testing.M) {
	testsuite.Run(m)
}

func getTestHelper(t *testing.T) *apis.K8sTestHelper {
	return apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{})
}

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

	ctx := context.Background()
	helper := getTestHelper(t)
	client := test_common.NewReceiverClient(t, helper.Org1.Admin)
	newResource := &v0alpha1.Receiver{
		ObjectMeta: v1.ObjectMeta{
			Namespace: "default",
		},
		Spec: v0alpha1.ReceiverSpec{
			Title:        "Test-Receiver",
			Integrations: []v0alpha1.ReceiverIntegration{},
		},
	}

	t.Run("create should fail if object name is specified", func(t *testing.T) {
		resource := newResource.Copy().(*v0alpha1.Receiver)
		resource.Name = "new-receiver"
		_, err := client.Create(ctx, resource, v1.CreateOptions{})
		require.Truef(t, errors.IsBadRequest(err), "Expected BadRequest but got %s", err)
	})

	var resourceID string
	t.Run("create should succeed and provide resource name", func(t *testing.T) {
		actual, err := client.Create(ctx, newResource, v1.CreateOptions{})
		require.NoError(t, err)
		require.NotEmptyf(t, actual.Name, "Resource name should not be empty")
		require.NotEmptyf(t, actual.UID, "Resource UID should not be empty")
		resourceID = actual.Name
	})

	t.Run("resource should be available by the identifier", func(t *testing.T) {
		actual, err := client.Get(ctx, resourceID, v1.GetOptions{})
		require.NoError(t, err)
		require.NotEmptyf(t, actual.Name, "Resource name should not be empty")
		require.Equal(t, newResource.Spec, actual.Spec)
	})

	t.Run("update should rename receiver if name in the specification changes", func(t *testing.T) {
		existing, err := client.Get(ctx, resourceID, v1.GetOptions{})
		require.NoError(t, err)

		updated := existing.Copy().(*v0alpha1.Receiver)
		updated.Spec.Title = "another-newReceiver"

		actual, err := client.Update(ctx, updated, v1.UpdateOptions{})
		require.NoError(t, err)
		require.Equal(t, updated.Spec, actual.Spec)
		require.NotEqualf(t, updated.Name, actual.Name, "Update should change the resource name but it didn't")
		require.NotEqualf(t, updated.ResourceVersion, actual.ResourceVersion, "Update should change the resource version but it didn't")

		resource, err := client.Get(ctx, actual.Name, v1.GetOptions{})
		require.NoError(t, err)
		require.Equal(t, actual.Spec, resource.Spec)
		require.Equal(t, actual.Name, resource.Name)
		require.Equal(t, actual.ResourceVersion, resource.ResourceVersion)
	})
}

// TestIntegrationResourcePermissions focuses on testing resource permissions for the alerting receiver resource. It
// verifies that access is correctly set when creating resources and assigning permissions to users, teams, and roles.
func TestIntegrationResourcePermissions(t *testing.T) {
	testutil.SkipIntegrationTestInShortMode(t)

	ctx := context.Background()
	helper := getTestHelper(t)

	org1 := helper.Org1
	noneUser := org1.None

	creator := helper.CreateUser("creator", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
		createWildcardPermission(
			accesscontrol.ActionAlertingReceiversCreate,
		),
	})

	admin := org1.Admin
	viewer := org1.Viewer
	editor := org1.Editor
	adminClient := test_common.NewReceiverClient(t, admin)

	writeACMetadata := []string{"canWrite", "canDelete"}
	allACMetadata := []string{"canWrite", "canDelete", "canReadSecrets", "canAdmin"}

	mustID := func(user apis.User) int64 {
		id, err := user.Identity.GetInternalID()
		require.NoError(t, err)
		return id
	}

	for _, tc := range []struct {
		name          string
		creatingUser  apis.User
		testUser      apis.User
		assignments   []accesscontrol.SetResourcePermissionCommand
		expACMetadata []string
		expRead       bool
	}{
		// Basic access.
		{
			name:          "Admin creates and has all metadata and access",
			creatingUser:  admin,
			testUser:      admin,
			assignments:   nil,
			expACMetadata: allACMetadata,
			expRead:       true,
		},
		{
			name:          "Creator creates and has all metadata and access",
			creatingUser:  creator,
			testUser:      creator,
			assignments:   nil,
			expACMetadata: allACMetadata,
			expRead:       true,
		},
		{
			name:          "Admin creates, noneUser has no metadata and no access",
			creatingUser:  admin,
			testUser:      noneUser,
			assignments:   nil,
			expACMetadata: nil,
			expRead:       false,
		},
		{
			name:          "Admin creates, viewer has no metadata but has access",
			creatingUser:  admin,
			testUser:      viewer,
			expACMetadata: nil,
			expRead:       true,
		},
		{
			name:          "Admin creates, editor has write metadata and access",
			creatingUser:  admin,
			testUser:      editor,
			expACMetadata: writeACMetadata,
			expRead:       true,
		},
		// User-based assignments.
		{
			name:          "Admin creates, assigns read, noneUser has no metadata but has access",
			creatingUser:  admin,
			testUser:      noneUser,
			assignments:   []accesscontrol.SetResourcePermissionCommand{{UserID: mustID(noneUser), Permission: string(ngmodels.PermissionView)}},
			expACMetadata: nil,
			expRead:       true,
		},
		{
			name:          "Admin creates, assigns write, noneUser has write metadata and access",
			creatingUser:  admin,
			testUser:      noneUser,
			assignments:   []accesscontrol.SetResourcePermissionCommand{{UserID: mustID(noneUser), Permission: string(ngmodels.PermissionEdit)}},
			expACMetadata: writeACMetadata,
			expRead:       true,
		},
		{
			name:          "Admin creates, assigns admin, noneUser has all metadata and access",
			creatingUser:  admin,
			testUser:      noneUser,
			assignments:   []accesscontrol.SetResourcePermissionCommand{{UserID: mustID(noneUser), Permission: string(ngmodels.PermissionAdmin)}},
			expACMetadata: allACMetadata,
			expRead:       true,
		},
		// Other users don't get assignments.
		{
			name:          "Admin creates, assigns read to noneUser, creator has no metadata and no access",
			creatingUser:  admin,
			testUser:      creator,
			assignments:   []accesscontrol.SetResourcePermissionCommand{{UserID: mustID(noneUser), Permission: string(ngmodels.PermissionView)}},
			expACMetadata: nil,
			expRead:       false,
		},
		{
			name:          "Admin creates, assigns write to noneUser, creator has no metadata and no access",
			creatingUser:  admin,
			testUser:      creator,
			assignments:   []accesscontrol.SetResourcePermissionCommand{{UserID: mustID(noneUser), Permission: string(ngmodels.PermissionEdit)}},
			expACMetadata: nil,
			expRead:       false,
		},
		{
			name:          "Admin creates, assigns admin to noneUser, creator has no metadata and no access",
			creatingUser:  admin,
			testUser:      creator,
			assignments:   []accesscontrol.SetResourcePermissionCommand{{UserID: mustID(noneUser), Permission: string(ngmodels.PermissionAdmin)}},
			expACMetadata: nil,
			expRead:       false,
		},
		// Role-based access.
		{
			name:          "Admin creates, assigns editor, viewer has write metadata and access",
			creatingUser:  admin,
			testUser:      viewer,
			assignments:   []accesscontrol.SetResourcePermissionCommand{{UserID: mustID(viewer), Permission: string(ngmodels.PermissionEdit)}},
			expACMetadata: writeACMetadata,
			expRead:       true,
		},
		{
			name:          "Admin creates, assigns admin, viewer has all metadata and access",
			creatingUser:  admin,
			testUser:      viewer,
			assignments:   []accesscontrol.SetResourcePermissionCommand{{UserID: mustID(viewer), Permission: string(ngmodels.PermissionAdmin)}},
			expACMetadata: allACMetadata,
			expRead:       true,
		},
		{
			name:          "Admin creates, assigns admin, editor has all metadata and access",
			creatingUser:  admin,
			testUser:      editor,
			assignments:   []accesscontrol.SetResourcePermissionCommand{{UserID: mustID(editor), Permission: string(ngmodels.PermissionAdmin)}},
			expACMetadata: allACMetadata,
			expRead:       true,
		},
		// Team-based access. Staff team has editor+admin but not viewer in it.
		{
			name:          "Admin creates, assigns admin to staff, viewer has no metadata and access",
			creatingUser:  admin,
			testUser:      viewer,
			assignments:   []accesscontrol.SetResourcePermissionCommand{{TeamID: org1.Staff.ID, Permission: string(ngmodels.PermissionAdmin)}},
			expACMetadata: nil,
			expRead:       true,
		},
		{
			name:          "Admin creates, assigns admin to staff, editor has all metadata and access",
			creatingUser:  admin,
			testUser:      editor,
			assignments:   []accesscontrol.SetResourcePermissionCommand{{TeamID: org1.Staff.ID, Permission: string(ngmodels.PermissionAdmin)}},
			expACMetadata: allACMetadata,
			expRead:       true,
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			createClient := test_common.NewReceiverClient(t, tc.creatingUser)
			client := test_common.NewReceiverClient(t, tc.testUser)

			var created = &v0alpha1.Receiver{
				ObjectMeta: v1.ObjectMeta{
					Namespace: "default",
				},
				Spec: v0alpha1.ReceiverSpec{
					Title:        "receiver-1",
					Integrations: []v0alpha1.ReceiverIntegration{},
				},
			}
			d, err := json.Marshal(created)
			require.NoError(t, err)

			// Create receiver with creatingUser
			created, err = createClient.Create(ctx, created, v1.CreateOptions{})
			require.NoErrorf(t, err, "Payload %s", string(d))
			require.NotNil(t, created)

			defer func() {
				_ = adminClient.Delete(ctx, created.Name, v1.DeleteOptions{})
			}()

			// Assign resource permissions
			cliCfg := helper.Org1.Admin.NewRestConfig()
			alertingApi := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)
			for _, permission := range tc.assignments {
				status, body := alertingApi.AssignReceiverPermission(t, created.Name, permission)
				require.Equalf(t, http.StatusOK, status, "Expected status 200 but got %d: %s", status, body)
			}

			// Test read
			if tc.expRead {
				// Helper methods.
				extractReceiverFromList := func(list *v0alpha1.ReceiverList, name string) *v0alpha1.Receiver {
					for i := range list.Items {
						if list.Items[i].Name == name {
							return list.Items[i].Copy().(*v0alpha1.Receiver)
						}
					}
					return nil
				}

				// Obtain expected responses using admin client as source of truth.
				expectedGetWithMetadata, expectedListWithMetadata := func() (*v0alpha1.Receiver, *v0alpha1.Receiver) {
					expectedGet, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
					require.NoError(t, err)
					require.NotNil(t, expectedGet)

					// Set expected metadata.
					expectedGetWithMetadata := expectedGet.Copy().(*v0alpha1.Receiver)
					// Clear any existing access control metadata.
					for _, k := range allACMetadata {
						delete(expectedGetWithMetadata.Annotations, v0alpha1.AccessControlAnnotation(k))
					}
					for _, ac := range tc.expACMetadata {
						expectedGetWithMetadata.SetAccessControl(ac)
					}

					expectedList, err := adminClient.List(ctx, v1.ListOptions{})
					require.NoError(t, err)
					expectedListWithMetadata := extractReceiverFromList(expectedList, created.Name)
					require.NotNil(t, expectedListWithMetadata)
					expectedListWithMetadata = expectedListWithMetadata.Copy().(*v0alpha1.Receiver)
					// Clear any existing access control metadata.
					for _, k := range allACMetadata {
						delete(expectedListWithMetadata.Annotations, v0alpha1.AccessControlAnnotation(k))
					}
					for _, ac := range tc.expACMetadata {
						expectedListWithMetadata.SetAccessControl(ac)
					}
					return expectedGetWithMetadata, expectedListWithMetadata
				}()

				t.Run("should be able to list receivers", func(t *testing.T) {
					list, err := client.List(ctx, v1.ListOptions{})
					require.NoError(t, err)
					listedReceiver := extractReceiverFromList(list, created.Name)
					assert.Equalf(t, expectedListWithMetadata, listedReceiver, "Expected %v but got %v", expectedListWithMetadata, listedReceiver)
				})

				t.Run("should be able to read receiver by resource identifier", func(t *testing.T) {
					got, err := client.Get(ctx, expectedGetWithMetadata.Name, v1.GetOptions{})
					require.NoError(t, err)
					assert.Equalf(t, expectedGetWithMetadata, got, "Expected %v but got %v", expectedGetWithMetadata, got)
				})
			} else {
				t.Run("list receivers should be empty", func(t *testing.T) {
					list, err := client.List(ctx, v1.ListOptions{})
					require.NoError(t, err)
					require.Emptyf(t, list.Items, "Expected no receivers but got %v", list.Items)
				})

				t.Run("should be forbidden to read receiver by name", func(t *testing.T) {
					_, err := client.Get(ctx, created.Name, v1.GetOptions{})
					require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
				})
			}
		})
	}
}

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

	ctx := context.Background()
	helper := getTestHelper(t)

	org1 := helper.Org1

	type testCase struct {
		user           apis.User
		canRead        bool
		canUpdate      bool
		canCreate      bool
		canDelete      bool
		canReadSecrets bool
		canAdmin       bool
	}
	// region users
	unauthorized := helper.CreateUser("unauthorized", "Org1", org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{})

	reader := helper.CreateUser("reader", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
		createWildcardPermission(accesscontrol.ActionAlertingReceiversRead),
	})
	secretsReader := helper.CreateUser("secretsReader", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
		createWildcardPermission(accesscontrol.ActionAlertingReceiversReadSecrets),
	})
	creator := helper.CreateUser("creator", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
		createWildcardPermission(
			accesscontrol.ActionAlertingReceiversRead,
			accesscontrol.ActionAlertingReceiversCreate,
		),
	})
	updater := helper.CreateUser("updater", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
		createWildcardPermission(
			accesscontrol.ActionAlertingReceiversRead,
			accesscontrol.ActionAlertingReceiversUpdate,
		),
	})
	deleter := helper.CreateUser("deleter", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
		createWildcardPermission(
			accesscontrol.ActionAlertingReceiversRead,
			accesscontrol.ActionAlertingReceiversDelete,
		),
	})
	legacyReader := helper.CreateUser("legacyReader", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
		{
			Actions: []string{
				accesscontrol.ActionAlertingNotificationsRead,
			},
		},
	})
	legacyWriter := helper.CreateUser("legacyWriter", "Org1", org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
		{
			Actions: []string{
				accesscontrol.ActionAlertingNotificationsRead,
				accesscontrol.ActionAlertingNotificationsWrite,
			},
		},
	})
	adminLikeUser := helper.CreateUser("adminLikeUser", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
		createWildcardPermission(append(
			[]string{accesscontrol.ActionAlertingReceiversCreate},
			ossaccesscontrol.ReceiversAdminActions...,
		)...),
	})

	// Test receivers with uids longer than 40 characters. User name is used in receiver name.
	adminLikeUserLongName := helper.CreateUser("adminLikeUserCreatingAReallyLongReceiverName", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
		createWildcardPermission(append(
			[]string{accesscontrol.ActionAlertingReceiversCreate},
			ossaccesscontrol.ReceiversAdminActions...,
		)...),
	})

	// endregion

	testCases := []testCase{
		{
			user:      unauthorized,
			canRead:   false,
			canUpdate: false,
			canCreate: false,
			canDelete: false,
		},
		{
			user:           org1.Admin,
			canRead:        true,
			canCreate:      true,
			canUpdate:      true,
			canDelete:      true,
			canAdmin:       true,
			canReadSecrets: true,
		},
		{
			user:      org1.Editor,
			canRead:   true,
			canUpdate: true,
			canCreate: true,
			canDelete: true,
		},
		{
			user:    org1.Viewer,
			canRead: true,
		},
		{
			user:    reader,
			canRead: true,
		},
		{
			user:           secretsReader,
			canRead:        true,
			canReadSecrets: true,
		},
		{
			user:      creator,
			canRead:   true,
			canCreate: true,
		},
		{
			user:      updater,
			canRead:   true,
			canUpdate: true,
		},
		{
			user:      deleter,
			canRead:   true,
			canDelete: true,
		},
		{
			user:    legacyReader,
			canRead: true,
		},
		{
			user:      legacyWriter,
			canRead:   true,
			canCreate: true,
			canUpdate: true,
			canDelete: true,
		},
		{
			user:           adminLikeUser,
			canRead:        true,
			canCreate:      true,
			canUpdate:      true,
			canDelete:      true,
			canAdmin:       true,
			canReadSecrets: true,
		},
		{
			user:           adminLikeUserLongName,
			canRead:        true,
			canCreate:      true,
			canUpdate:      true,
			canDelete:      true,
			canAdmin:       true,
			canReadSecrets: true,
		},
	}

	adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
	for _, tc := range testCases {
		t.Run(fmt.Sprintf("user '%s'", tc.user.Identity.GetLogin()), func(t *testing.T) {
			client := test_common.NewReceiverClient(t, tc.user)

			var expected = &v0alpha1.Receiver{
				ObjectMeta: v1.ObjectMeta{
					Namespace: "default",
				},
				Spec: v0alpha1.ReceiverSpec{
					Title:        fmt.Sprintf("receiver-1-%s", tc.user.Identity.GetLogin()),
					Integrations: []v0alpha1.ReceiverIntegration{},
				},
			}
			d, err := json.Marshal(expected)
			require.NoError(t, err)

			newReceiver := expected.Copy().(*v0alpha1.Receiver)
			newReceiver.Spec.Title = fmt.Sprintf("receiver-2-%s", tc.user.Identity.GetLogin())
			if tc.canCreate {
				t.Run("should be able to create receiver", func(t *testing.T) {
					actual, err := client.Create(ctx, newReceiver, v1.CreateOptions{})
					require.NoErrorf(t, err, "Payload %s", string(d))

					require.Equal(t, newReceiver.Spec, actual.Spec)

					t.Run("should fail if already exists", func(t *testing.T) {
						_, err := client.Create(ctx, newReceiver, v1.CreateOptions{})
						require.Truef(t, errors.IsConflict(err), "expected  bad request but got %s", err)
					})

					// Cleanup.
					require.NoError(t, adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{}))
				})
			} else {
				t.Run("should be forbidden to create", func(t *testing.T) {
					_, err := client.Create(ctx, newReceiver, v1.CreateOptions{})
					require.Truef(t, errors.IsForbidden(err), "Payload %s", string(d))
				})
			}

			// create resource to proceed with other tests. We don't use the one created above because the user will always
			// have admin permissions on it.
			expected, err = adminClient.Create(ctx, expected, v1.CreateOptions{})
			require.NoErrorf(t, err, "Payload %s", string(d))
			require.NotNil(t, expected)

			if tc.canRead {
				// Set expected metadata.
				expectedWithMetadata := expected.Copy().(*v0alpha1.Receiver)
				expectedWithMetadata.SetInUse(0, nil)
				expectedWithMetadata.SetCanUse(true)
				if tc.canUpdate {
					expectedWithMetadata.SetAccessControl("canWrite")
				}
				if tc.canDelete {
					expectedWithMetadata.SetAccessControl("canDelete")
				}
				if tc.canReadSecrets {
					expectedWithMetadata.SetAccessControl("canReadSecrets")
				}
				if tc.canAdmin {
					expectedWithMetadata.SetAccessControl("canAdmin")
				}
				t.Run("should be able to list receivers", func(t *testing.T) {
					list, err := client.List(ctx, v1.ListOptions{})
					require.NoError(t, err)
					require.Len(t, list.Items, 2) // default + created
				})

				t.Run("should be able to read receiver by resource identifier", func(t *testing.T) {
					got, err := client.Get(ctx, expected.Name, v1.GetOptions{})
					require.NoError(t, err)
					require.Equal(t, expectedWithMetadata, got)

					t.Run("should get NotFound if resource does not exist", func(t *testing.T) {
						_, err := client.Get(ctx, "Notfound", v1.GetOptions{})
						require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err)
					})
				})
			} else {
				t.Run("list receivers should be empty", func(t *testing.T) {
					list, err := client.List(ctx, v1.ListOptions{})
					require.NoError(t, err)
					require.Emptyf(t, list.Items, "Expected no receivers but got %v", list.Items)
				})

				t.Run("should be forbidden to read receiver by name", func(t *testing.T) {
					_, err := client.Get(ctx, expected.Name, v1.GetOptions{})
					require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)

					t.Run("should get forbidden even if name does not exist", func(t *testing.T) {
						_, err := client.Get(ctx, "Notfound", v1.GetOptions{})
						require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
					})
				})
			}

			updatedExpected := expected.Copy().(*v0alpha1.Receiver)
			updatedExpected.Spec.Integrations = append(updatedExpected.Spec.Integrations, createIntegration(t, "email"))

			d, err = json.Marshal(updatedExpected)
			require.NoError(t, err)

			if tc.canUpdate {
				t.Run("should be able to update receiver", func(t *testing.T) {
					updated, err := client.Update(ctx, updatedExpected, v1.UpdateOptions{})
					require.NoErrorf(t, err, "Payload %s", string(d))

					expected = updated

					t.Run("should get NotFound if name does not exist", func(t *testing.T) {
						up := updatedExpected.Copy().(*v0alpha1.Receiver)
						up.Name = "notFound"
						_, err := client.Update(ctx, up, v1.UpdateOptions{})
						require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err)
					})
				})
			} else {
				t.Run("should be forbidden to update receiver", func(t *testing.T) {
					_, err := client.Update(ctx, updatedExpected, v1.UpdateOptions{})
					require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)

					t.Run("should get forbidden even if resource does not exist", func(t *testing.T) {
						up := updatedExpected.Copy().(*v0alpha1.Receiver)
						up.Name = "notFound"
						_, err := client.Update(ctx, up, v1.UpdateOptions{})
						require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
					})
				})
			}

			deleteOptions := v1.DeleteOptions{Preconditions: &v1.Preconditions{ResourceVersion: util.Pointer(expected.ResourceVersion)}}

			if tc.canDelete {
				t.Run("should be able to delete receiver", func(t *testing.T) {
					err := client.Delete(ctx, expected.Name, deleteOptions)
					require.NoError(t, err)

					t.Run("should get NotFound if name does not exist", func(t *testing.T) {
						err := client.Delete(ctx, "notfound", v1.DeleteOptions{})
						require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err)
					})
				})
			} else {
				t.Run("should be forbidden to delete receiver", func(t *testing.T) {
					err := client.Delete(ctx, expected.Name, deleteOptions)
					require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)

					t.Run("should be forbidden even if resource does not exist", func(t *testing.T) {
						err := client.Delete(ctx, "notfound", v1.DeleteOptions{})
						require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
					})
				})
				require.NoError(t, adminClient.Delete(ctx, expected.Name, v1.DeleteOptions{}))
			}

			if tc.canRead {
				t.Run("should get empty list if no receivers", func(t *testing.T) {
					list, err := client.List(ctx, v1.ListOptions{})
					require.NoError(t, err)
					require.Len(t, list.Items, 1)
				})
			}
		})
	}
}

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

	ctx := context.Background()
	helper := getTestHelper(t)

	cliCfg := helper.Org1.Admin.NewRestConfig()
	legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)

	adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
	// Prepare environment and create notification policy and rule that use receiver
	alertmanagerRaw, err := testData.ReadFile(path.Join("test-data", "notification-settings.json"))
	require.NoError(t, err)
	var amConfig definitions.PostableUserConfig
	require.NoError(t, json.Unmarshal(alertmanagerRaw, &amConfig))

	// Add more references to the receiver in other routes.
	route1 := *amConfig.AlertmanagerConfig.Route.Routes[0]
	route1.Routes = nil
	route2 := route1
	parentRoute := *amConfig.AlertmanagerConfig.Route.Routes[0]
	parentRoute.Routes = []*definitions.Route{&route1, &route2}
	amConfig.AlertmanagerConfig.Route.Routes = append(amConfig.AlertmanagerConfig.Route.Routes, &parentRoute)

	persistInitialConfig(t, amConfig)

	postGroupRaw, err := testData.ReadFile(path.Join("test-data", "rulegroup-1.json"))
	require.NoError(t, err)
	var ruleGroup definitions.PostableRuleGroupConfig
	require.NoError(t, json.Unmarshal(postGroupRaw, &ruleGroup))

	// Add more references to the receiver by creating adding same rule with a different title.
	ruleGen := func() definitions.PostableGrafanaRule { return *ruleGroup.Rules[0].GrafanaManagedAlert }
	rule2 := ruleGen()
	rule2.Title = "Rule2"
	rule2.NotificationSettings = &definitions.AlertRuleNotificationSettings{Receiver: "grafana-default-email"}
	rule3 := ruleGen()
	rule3.Title = "Rule3"
	ruleGroup.Rules = append(ruleGroup.Rules,
		definitions.PostableExtendedRuleNode{
			ApiRuleNode:         ruleGroup.Rules[0].ApiRuleNode,
			GrafanaManagedAlert: &rule2,
		},
		definitions.PostableExtendedRuleNode{
			ApiRuleNode:         ruleGroup.Rules[0].ApiRuleNode,
			GrafanaManagedAlert: &rule3,
		},
	)

	folderUID := "test-folder"
	legacyCli.CreateFolder(t, folderUID, "TEST")
	_, status, data := legacyCli.PostRulesGroupWithStatus(t, folderUID, &ruleGroup, false)
	require.Equalf(t, http.StatusAccepted, status, "Failed to post Rule: %s", data)

	requestReceivers := func(t *testing.T, title string) (v0alpha1.Receiver, v0alpha1.Receiver) {
		t.Helper()
		receivers, err := adminClient.List(ctx, v1.ListOptions{})
		require.NoError(t, err)
		require.Len(t, receivers.Items, 2)
		idx := slices.IndexFunc(receivers.Items, func(interval v0alpha1.Receiver) bool {
			return interval.Spec.Title == title
		})
		receiverListed := receivers.Items[idx]

		receiverGet, err := adminClient.Get(ctx, receiverListed.Name, v1.GetOptions{})
		require.NoError(t, err)

		return receiverListed, *receiverGet
	}

	checkInUse := func(t *testing.T, receiverList, receiverGet v0alpha1.Receiver, routes, rules int) {
		t.Helper()
		assert.Equalf(t, fmt.Sprintf("%d", routes), receiverList.Annotations[v0alpha1.InUseAnnotation("routes")], "LIST: Expected %s used by %d routes", receiverList.Spec.Title, routes)
		assert.Equalf(t, fmt.Sprintf("%d", rules), receiverList.Annotations[v0alpha1.InUseAnnotation("rules")], "LIST: Expected %s used by %d rules", receiverList.Spec.Title, rules)
		assert.Equalf(t, fmt.Sprintf("%d", routes), receiverGet.Annotations[v0alpha1.InUseAnnotation("routes")], "GET: Expected %s used by %d routes", receiverGet.Spec.Title, routes)
		assert.Equalf(t, fmt.Sprintf("%d", rules), receiverGet.Annotations[v0alpha1.InUseAnnotation("rules")], "GET: Expected %s used by %d rules", receiverGet.Spec.Title, rules)
	}

	receiverListed, receiverGet := requestReceivers(t, "user-defined")
	checkInUse(t, receiverListed, receiverGet, 4, 2)

	// Verify the default.
	receiverListed, receiverGet = requestReceivers(t, "grafana-default-email")
	checkInUse(t, receiverListed, receiverGet, 1, 1)

	// Removing the new extra route should leave only 1.
	amConfig.AlertmanagerConfig.Route.Routes = amConfig.AlertmanagerConfig.Route.Routes[:1]
	v1Route, err := routingtree.ConvertToK8sResource(helper.Org1.AdminServiceAccount.OrgId, *amConfig.AlertmanagerConfig.Route, "", func(int64) string { return "default" })
	require.NoError(t, err)
	routeAdminClient := test_common.NewRoutingTreeClient(t, helper.Org1.Admin)
	_, err = routeAdminClient.Update(ctx, v1Route, v1.UpdateOptions{})
	require.NoError(t, err)

	receiverListed, receiverGet = requestReceivers(t, "user-defined")
	checkInUse(t, receiverListed, receiverGet, 1, 2)

	// Remove the extra rules.
	ruleGroup.Rules = ruleGroup.Rules[:1]
	_, status, data = legacyCli.PostRulesGroupWithStatus(t, folderUID, &ruleGroup, false)
	require.Equalf(t, http.StatusAccepted, status, "Failed to post Rule: %s", data)

	receiverListed, receiverGet = requestReceivers(t, "user-defined")
	checkInUse(t, receiverListed, receiverGet, 1, 1)

	receiverListed, receiverGet = requestReceivers(t, "grafana-default-email")
	checkInUse(t, receiverListed, receiverGet, 1, 0)

	// Remove the remaining routes.
	amConfig.AlertmanagerConfig.Route.Routes = nil
	v1route, err := routingtree.ConvertToK8sResource(1, *amConfig.AlertmanagerConfig.Route, "", func(int64) string { return "default" })
	require.NoError(t, err)
	_, err = routeAdminClient.Update(ctx, v1route, v1.UpdateOptions{})
	require.NoError(t, err)

	// Remove the remaining rules.
	ruleGroup.Rules = nil
	_, status, data = legacyCli.PostRulesGroupWithStatus(t, folderUID, &ruleGroup, false)
	require.Equalf(t, http.StatusAccepted, status, "Failed to post Rule: %s", data)

	receiverListed, receiverGet = requestReceivers(t, "user-defined")
	checkInUse(t, receiverListed, receiverGet, 0, 0)

	receiverListed, receiverGet = requestReceivers(t, "grafana-default-email")
	checkInUse(t, receiverListed, receiverGet, 1, 0)
}

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

	ctx := context.Background()
	helper := getTestHelper(t)

	org := helper.Org1

	admin := org.Admin
	adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
	env := helper.GetEnv()
	ac := acimpl.ProvideAccessControl(env.FeatureToggles)
	db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, bus.ProvideBus(tracing.InitializeTracerForTest()))
	require.NoError(t, err)

	created, err := adminClient.Create(ctx, &v0alpha1.Receiver{
		ObjectMeta: v1.ObjectMeta{
			Namespace: "default",
		},
		Spec: v0alpha1.ReceiverSpec{
			Title: "test-receiver-1",
			Integrations: []v0alpha1.ReceiverIntegration{
				createIntegration(t, "email"),
			},
		},
	}, v1.CreateOptions{})
	require.NoError(t, err)
	require.Equal(t, "none", created.GetProvenanceStatus())

	t.Run("should provide provenance status", func(t *testing.T) {
		require.NoError(t, db.SetProvenance(ctx, &definitions.EmbeddedContactPoint{
			UID: *created.Spec.Integrations[0].Uid,
		}, admin.Identity.GetOrgID(), "API"))

		got, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
		require.NoError(t, err)
		require.Equal(t, "API", got.GetProvenanceStatus())
	})

	t.Run("should not let update if provisioned", func(t *testing.T) {
		got, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
		require.NoError(t, err)
		updated := got.Copy().(*v0alpha1.Receiver)
		updated.Spec.Integrations = append(updated.Spec.Integrations, createIntegration(t, "email"))

		_, err = adminClient.Update(ctx, updated, v1.UpdateOptions{})
		require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
	})

	t.Run("should not let delete if provisioned", func(t *testing.T) {
		err := adminClient.Delete(ctx, created.Name, v1.DeleteOptions{})
		require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
	})
}

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

	ctx := context.Background()
	helper := getTestHelper(t)

	adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
	receiver := v0alpha1.Receiver{
		ObjectMeta: v1.ObjectMeta{
			Namespace: "default",
		},
		Spec: v0alpha1.ReceiverSpec{
			Title:        "receiver-1",
			Integrations: []v0alpha1.ReceiverIntegration{},
		},
	}

	created, err := adminClient.Create(ctx, &receiver, v1.CreateOptions{})
	require.NoError(t, err)
	require.NotNil(t, created)
	require.NotEmpty(t, created.ResourceVersion)

	t.Run("should forbid if version does not match", func(t *testing.T) {
		updated := created.Copy().(*v0alpha1.Receiver)
		updated.ResourceVersion = "test"
		_, err := adminClient.Update(ctx, updated, v1.UpdateOptions{})
		require.Truef(t, errors.IsConflict(err), "should get Forbidden error but got %s", err)
	})
	t.Run("should update if version matches", func(t *testing.T) {
		updated := created.Copy().(*v0alpha1.Receiver)
		updated.Spec.Integrations = append(updated.Spec.Integrations, createIntegration(t, "email"))
		actualUpdated, err := adminClient.Update(ctx, updated, v1.UpdateOptions{})
		require.NoError(t, err)
		for i, integration := range actualUpdated.Spec.Integrations {
			updated.Spec.Integrations[i].Uid = integration.Uid
		}
		require.EqualValues(t, updated.Spec, actualUpdated.Spec)
		require.NotEqual(t, updated.ResourceVersion, actualUpdated.ResourceVersion)
	})
	t.Run("should fail to update if version is empty", func(t *testing.T) {
		updated := created.Copy().(*v0alpha1.Receiver)
		updated.ResourceVersion = ""
		updated.Spec.Integrations = append(updated.Spec.Integrations, createIntegration(t, "webhook"))
		_, err := adminClient.Update(ctx, updated, v1.UpdateOptions{})
		require.Truef(t, errors.IsConflict(err), "should get Forbidden error but got %s", err) // TODO Change that? K8s returns 400 instead.
	})
	t.Run("should fail to delete if version does not match", func(t *testing.T) {
		actual, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
		require.NoError(t, err)

		err = adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{
			Preconditions: &v1.Preconditions{
				ResourceVersion: util.Pointer("something"),
			},
		})
		require.Truef(t, errors.IsConflict(err), "should get Forbidden error but got %s", err)
	})
	t.Run("should succeed if version matches", func(t *testing.T) {
		actual, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
		require.NoError(t, err)

		err = adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{
			Preconditions: &v1.Preconditions{
				ResourceVersion: util.Pointer(actual.ResourceVersion),
			},
		})
		require.NoError(t, err)
	})
	t.Run("should succeed if version is empty", func(t *testing.T) {
		actual, err := adminClient.Create(ctx, &receiver, v1.CreateOptions{})
		require.NoError(t, err)

		err = adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{
			Preconditions: &v1.Preconditions{
				ResourceVersion: util.Pointer(actual.ResourceVersion),
			},
		})
		require.NoError(t, err)
	})
}

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

	ctx := context.Background()
	helper := getTestHelper(t)

	adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
	receiver := v0alpha1.Receiver{
		ObjectMeta: v1.ObjectMeta{
			Namespace: "default",
		},
		Spec: v0alpha1.ReceiverSpec{
			Title: "receiver",
			Integrations: []v0alpha1.ReceiverIntegration{
				createIntegration(t, "email"),
				createIntegration(t, "webhook"),
				createIntegration(t, "sns"),
			},
		},
	}

	current, err := adminClient.Create(ctx, &receiver, v1.CreateOptions{})
	require.NoError(t, err)
	require.NotNil(t, current)

	t.Run("should patch with json patch", func(t *testing.T) {
		current, err := adminClient.Get(ctx, current.Name, v1.GetOptions{})
		require.NoError(t, err)

		index := slices.IndexFunc(current.Spec.Integrations, func(t v0alpha1.ReceiverIntegration) bool {
			return t.Type == "webhook"
		})

		patch := []map[string]any{
			{
				"op":   "remove",
				"path": fmt.Sprintf("/spec/integrations/%d/settings/username", index),
			},
			{
				"op":   "remove",
				"path": fmt.Sprintf("/spec/integrations/%d/secureFields/password", index),
			},
			{
				"op":    "replace",
				"path":  fmt.Sprintf("/spec/integrations/%d/settings/authorization_scheme", index),
				"value": "bearer",
			},
			{
				"op":    "add",
				"path":  fmt.Sprintf("/spec/integrations/%d/settings/authorization_credentials", index),
				"value": "authz-token",
			},
			{
				"op":   "remove",
				"path": fmt.Sprintf("/spec/integrations/%d/secureFields/authorization_credentials", index),
			},
		}

		expected := current.Spec.Integrations[index]
		delete(expected.Settings, "username")
		delete(expected.Settings, "password")
		expected.Settings["authorization_scheme"] = "bearer"
		delete(expected.SecureFields, "password")
		expected.SecureFields["authorization_credentials"] = true

		patchData, err := json.Marshal(patch)
		require.NoError(t, err)

		result, err := adminClient.Patch(ctx, current.Name, types.JSONPatchType, patchData, v1.PatchOptions{})
		require.NoError(t, err)

		require.EqualValues(t, expected, result.Spec.Integrations[index])

		// Use export endpoint because it's the only way to get decrypted secrets fast.
		cliCfg := helper.Org1.Admin.NewRestConfig()
		legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)

		export := legacyCli.ExportReceiverTyped(t, current.Spec.Title, true)
		// we need special unmarshaler to parse settings
		cp, err := api.ContactPointFromContactPointExport(export)
		require.NoError(t, err)

		assert.Len(t, cp.Sns, 1)
		assert.Len(t, cp.Email, 1)
		require.Len(t, cp.Webhook, 1)

		settings := cp.Webhook[0]
		assert.EqualValues(t, "authz-token", *settings.AuthorizationCredentials)
		assert.EqualValues(t, "bearer", *settings.AuthorizationScheme)
		assert.Nil(t, settings.Password)
		assert.Nil(t, settings.User)
	})
}

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

	ctx := context.Background()
	helper := getTestHelper(t)
	env := helper.GetEnv()
	ac := acimpl.ProvideAccessControl(env.FeatureToggles)
	db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, bus.ProvideBus(tracing.InitializeTracerForTest()))
	require.NoError(t, err)
	orgID := helper.Org1.Admin.Identity.GetOrgID()

	cliCfg := helper.Org1.Admin.NewRestConfig()
	legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)

	adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
	// Prepare environment and create notification policy and rule that use time receiver
	alertmanagerRaw, err := testData.ReadFile(path.Join("test-data", "notification-settings.json"))
	require.NoError(t, err)
	var amConfig definitions.PostableUserConfig
	require.NoError(t, json.Unmarshal(alertmanagerRaw, &amConfig))

	persistInitialConfig(t, amConfig)

	postGroupRaw, err := testData.ReadFile(path.Join("test-data", "rulegroup-1.json"))
	require.NoError(t, err)
	var ruleGroup definitions.PostableRuleGroupConfig
	require.NoError(t, json.Unmarshal(postGroupRaw, &ruleGroup))

	folderUID := "test-folder"
	legacyCli.CreateFolder(t, folderUID, "TEST")
	_, status, data := legacyCli.PostRulesGroupWithStatus(t, folderUID, &ruleGroup, false)
	require.Equalf(t, http.StatusAccepted, status, "Failed to post Rule: %s", data)

	receivers, err := adminClient.List(ctx, v1.ListOptions{})
	require.NoError(t, err)
	require.Len(t, receivers.Items, 2)
	idx := slices.IndexFunc(receivers.Items, func(interval v0alpha1.Receiver) bool {
		return interval.Spec.Title == "user-defined"
	})
	receiver := receivers.Items[idx]

	currentRoute := legacyCli.GetRoute(t)
	currentRuleGroup, status := legacyCli.GetRulesGroup(t, folderUID, ruleGroup.Name)
	require.Equal(t, http.StatusAccepted, status)

	t.Run("Update", func(t *testing.T) {
		t.Run("should rename all references if name changes", func(t *testing.T) {
			renamed := receiver.Copy().(*v0alpha1.Receiver)
			expectedTitle := renamed.Spec.Title + "-new"
			renamed.Spec.Title = expectedTitle

			actual, err := adminClient.Update(ctx, renamed, v1.UpdateOptions{})
			require.NoError(t, err)

			updatedRuleGroup, status := legacyCli.GetRulesGroup(t, folderUID, ruleGroup.Name)
			require.Equal(t, http.StatusAccepted, status)
			for idx, rule := range updatedRuleGroup.Rules {
				assert.Equalf(t, expectedTitle, rule.GrafanaManagedAlert.NotificationSettings.Receiver, "receiver in rule %d should have been renamed but it did not", idx)
			}

			updatedRoute := legacyCli.GetRoute(t)
			for _, route := range updatedRoute.Routes {
				assert.Equalf(t, expectedTitle, route.Receiver, "time receiver in routes should have been renamed but it did not")
			}

			actual, err = adminClient.Get(ctx, actual.Name, v1.GetOptions{})
			require.NoError(t, err)

			receiver = *actual
		})

		t.Run("should fail if at least one resource is provisioned", func(t *testing.T) {
			require.NoError(t, err)
			renamed := receiver.Copy().(*v0alpha1.Receiver)
			renamed.Spec.Title += util.GenerateShortUID()

			t.Run("provisioned route", func(t *testing.T) {
				require.NoError(t, db.SetProvenance(ctx, &currentRoute, orgID, "API"))
				t.Cleanup(func() {
					require.NoError(t, db.DeleteProvenance(ctx, &currentRoute, orgID))
				})
				actual, err := adminClient.Update(ctx, renamed, v1.UpdateOptions{})
				require.Errorf(t, err, "Expected error but got successful result: %v", actual)
				require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
			})

			t.Run("provisioned rules", func(t *testing.T) {
				ruleUid := currentRuleGroup.Rules[0].GrafanaManagedAlert.UID
				resource := &ngmodels.AlertRule{UID: ruleUid}
				require.NoError(t, db.SetProvenance(ctx, resource, orgID, "API"))
				t.Cleanup(func() {
					require.NoError(t, db.DeleteProvenance(ctx, resource, orgID))
				})

				actual, err := adminClient.Update(ctx, renamed, v1.UpdateOptions{})
				require.Errorf(t, err, "Expected error but got successful result: %v", actual)
				require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
			})
		})
	})

	t.Run("Delete", func(t *testing.T) {
		t.Run("should fail to delete if receiver is used in rule and routes", func(t *testing.T) {
			err := adminClient.Delete(ctx, receiver.Name, v1.DeleteOptions{})
			require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
		})

		t.Run("should fail to delete if receiver is used in only rule", func(t *testing.T) {
			route := legacyCli.GetRoute(t)
			route.Routes[0].Receiver = ""
			legacyCli.UpdateRoute(t, route, true)

			err = adminClient.Delete(ctx, receiver.Name, v1.DeleteOptions{})
			require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
		})
	})
}

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

	ctx := context.Background()
	helper := getTestHelper(t)

	adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
	var defaultReceiver *v0alpha1.Receiver
	t.Run("should list the default receiver", func(t *testing.T) {
		items, err := adminClient.List(ctx, v1.ListOptions{})
		require.NoError(t, err)
		assert.Len(t, items.Items, 1)
		defaultReceiver = &items.Items[0]
		assert.Equal(t, "grafana-default-email", defaultReceiver.Spec.Title)
		assert.NotEmpty(t, defaultReceiver.UID)
		assert.NotEmpty(t, defaultReceiver.Name)
		assert.NotEmpty(t, defaultReceiver.ResourceVersion)

		defaultReceiver, err = adminClient.Get(ctx, defaultReceiver.Name, v1.GetOptions{})
		require.NoError(t, err)
		assert.NotEmpty(t, defaultReceiver.UID)
		assert.NotEmpty(t, defaultReceiver.Name)
		assert.NotEmpty(t, defaultReceiver.ResourceVersion)
		assert.Len(t, defaultReceiver.Spec.Integrations, 1)
	})

	t.Run("should be able to update default receiver", func(t *testing.T) {
		require.NotNil(t, defaultReceiver)
		newDefault := defaultReceiver.Copy().(*v0alpha1.Receiver)
		newDefault.Spec.Integrations = append(newDefault.Spec.Integrations, createIntegration(t, line.Type))

		updatedReceiver, err := adminClient.Update(ctx, newDefault, v1.UpdateOptions{})
		require.NoError(t, err)

		expected := newDefault.Copy().(*v0alpha1.Receiver)
		expected.Spec.Integrations[0].Uid = updatedReceiver.Spec.Integrations[0].Uid // default integration does not have UID before first update
		lineIntegration := expected.Spec.Integrations[1]
		lineIntegration.SecureFields = map[string]bool{
			"token": true,
		}
		delete(lineIntegration.Settings, "token")
		assert.Equal(t, "LINE", updatedReceiver.Spec.Integrations[1].Type) // this type is in the schema but not in backend
		lineIntegration.Type = "LINE"
		lineIntegration.Uid = updatedReceiver.Spec.Integrations[1].Uid
		expected.Spec.Integrations[1] = lineIntegration

		assert.Equal(t, expected.Spec, updatedReceiver.Spec)
	})

	t.Run("should fail to create receiver with the existing name", func(t *testing.T) {
		newReceiver := &v0alpha1.Receiver{
			ObjectMeta: v1.ObjectMeta{
				Namespace: "default",
			},
			Spec: v0alpha1.ReceiverSpec{
				Title:        defaultReceiver.Spec.Title,
				Integrations: []v0alpha1.ReceiverIntegration{},
			},
		}
		_, err := adminClient.Create(ctx, newReceiver, v1.CreateOptions{})
		require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
	})

	t.Run("should not let delete default receiver", func(t *testing.T) {
		err := adminClient.Delete(ctx, defaultReceiver.Name, v1.DeleteOptions{})
		require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
	})

	var receiver *v0alpha1.Receiver
	t.Run("should correctly persist all known integrations", func(t *testing.T) {
		integrations := make([]v0alpha1.ReceiverIntegration, 0, len(notifytest.AllKnownV1ConfigsForTesting))
		keysIter := maps.Keys(notifytest.AllKnownV1ConfigsForTesting)
		keys := slices.Collect(keysIter)
		slices.Sort(keys)
		for _, key := range keys {
			integrations = append(integrations, createIntegration(t, key))
		}
		var err error
		receiver, err = adminClient.Create(ctx, &v0alpha1.Receiver{
			ObjectMeta: v1.ObjectMeta{
				Namespace: "default",
			},
			Spec: v0alpha1.ReceiverSpec{
				Title:        "all-receivers",
				Integrations: integrations,
			},
		}, v1.CreateOptions{})
		require.NoError(t, err)
		require.Len(t, receiver.Spec.Integrations, len(integrations))

		// Set expected metadata
		receiver.SetAccessControl("canWrite")
		receiver.SetAccessControl("canDelete")
		receiver.SetAccessControl("canReadSecrets")
		receiver.SetAccessControl("canAdmin")
		receiver.SetInUse(0, nil)
		receiver.SetCanUse(true)

		// Use export endpoint because it's the only way to get decrypted secrets fast.
		cliCfg := helper.Org1.Admin.NewRestConfig()
		legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)

		export := legacyCli.ExportReceiverTyped(t, receiver.Spec.Title, true)
		for _, integration := range export.Receivers {
			expected := notifytest.AllKnownV1ConfigsForTesting[schema.IntegrationType(integration.Type)]
			assert.JSONEqf(t, expected.Config, string(integration.Settings), "integration %s", integration.Type)
		}
	})

	t.Run("should be able read what it is created", func(t *testing.T) {
		get, err := adminClient.Get(ctx, receiver.Name, v1.GetOptions{})
		require.NoError(t, err)
		require.Equal(t, receiver, get)
		t.Run("should return secrets in secureFields but not settings", func(t *testing.T) {
			for _, integration := range get.Spec.Integrations {
				integrationType := schema.IntegrationType(integration.Type)
				t.Run(integration.Type, func(t *testing.T) {
					expected := notifytest.AllKnownV1ConfigsForTesting[schema.IntegrationType(integration.Type)]
					var fields map[string]any
					require.NoError(t, json.Unmarshal([]byte(expected.Config), &fields))
					typeSchema, ok := notify.GetSchemaVersionForIntegration(integrationType, schema.V1)
					require.True(t, ok)
					secretFields := typeSchema.GetSecretFieldsPaths()
					for _, fieldPath := range secretFields {
						field := fieldPath.String()
						if _, ok := fields[field]; !ok { // skip field that is not in the original setting
							continue
						}
						assert.Contains(t, integration.SecureFields, field)
						assert.Truef(t, integration.SecureFields[field], "secure field should be always true")

						value, ok, err := unstructured.NestedString(integration.Settings, strings.Split(field, ".")...)
						assert.NoErrorf(t, err, "failed to read field %s from settings", field)
						assert.Falsef(t, ok, "secret field %s should not be in settings, value [%s]", field, value)
					}
				})
			}
		})
	})

	t.Run("should fail to persist receiver with invalid config", func(t *testing.T) {
		keysIter := maps.Keys(notifytest.AllKnownV1ConfigsForTesting)
		keys := slices.Collect(keysIter)
		slices.Sort(keys)
		for _, key := range keys {
			t.Run(string(key), func(t *testing.T) {
				integration := createIntegration(t, key)
				// Make the integration invalid, so it fails to create. This is usually done by sending empty settings.
				clear(integration.Settings)
				if key == "webex" {
					// Webex integration is special case and passes validation without any settings so we instead set an invalid URL.
					integration.Settings["api_url"] = "(*^$*^%!@#$*()"
				}

				receiver, err := adminClient.Create(ctx, &v0alpha1.Receiver{
					ObjectMeta: v1.ObjectMeta{
						Namespace: "default",
					},
					Spec: v0alpha1.ReceiverSpec{
						Title:        fmt.Sprintf("invalid-%s", key),
						Integrations: []v0alpha1.ReceiverIntegration{integration},
					},
				}, v1.CreateOptions{})
				require.Errorf(t, err, "Expected error but got successful result: %v", receiver)
				require.Truef(t, errors.IsBadRequest(err), "Expected BadRequest, got: %s", err)
			})
		}
	})
}

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

	ctx := context.Background()
	helper := getTestHelper(t)

	adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
	recv1 := &v0alpha1.Receiver{
		ObjectMeta: v1.ObjectMeta{
			Namespace: "default",
		},
		Spec: v0alpha1.ReceiverSpec{
			Title: "test-receiver-1",
			Integrations: []v0alpha1.ReceiverIntegration{
				createIntegration(t, "email"),
			},
		},
	}
	recv1, err := adminClient.Create(ctx, recv1, v1.CreateOptions{})
	require.NoError(t, err)

	recv2 := &v0alpha1.Receiver{
		ObjectMeta: v1.ObjectMeta{
			Namespace: "default",
		},
		Spec: v0alpha1.ReceiverSpec{
			Title: "test-receiver-2",
			Integrations: []v0alpha1.ReceiverIntegration{
				createIntegration(t, "email"),
			},
		},
	}
	recv2, err = adminClient.Create(ctx, recv2, v1.CreateOptions{})
	require.NoError(t, err)

	env := helper.GetEnv()
	ac := acimpl.ProvideAccessControl(env.FeatureToggles)
	db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, bus.ProvideBus(tracing.InitializeTracerForTest()))
	require.NoError(t, err)
	require.NoError(t, db.SetProvenance(ctx, &definitions.EmbeddedContactPoint{
		UID: *recv2.Spec.Integrations[0].Uid,
	}, helper.Org1.Admin.Identity.GetOrgID(), "API"))
	recv2, err = adminClient.Get(ctx, recv2.Name, v1.GetOptions{})

	require.NoError(t, err)

	receivers, err := adminClient.List(ctx, v1.ListOptions{})
	require.NoError(t, err)
	require.Len(t, receivers.Items, 3) // Includes default.

	t.Run("should filter by receiver name", func(t *testing.T) {
		t.Skip("disabled until app installer supports it") // TODO revisit when custom field selectors are supported
		list, err := adminClient.List(ctx, v1.ListOptions{
			FieldSelector: "spec.title=" + recv1.Spec.Title,
		})
		require.NoError(t, err)
		require.Len(t, list.Items, 1)
		require.Equal(t, recv1.Name, list.Items[0].Name)
	})

	t.Run("should filter by metadata name", func(t *testing.T) {
		list, err := adminClient.List(ctx, v1.ListOptions{
			FieldSelector: "metadata.name=" + recv2.Name,
		})
		require.NoError(t, err)
		require.Len(t, list.Items, 1)
		require.Equal(t, recv2.Name, list.Items[0].Name)
	})

	t.Run("should filter by multiple filters", func(t *testing.T) {
		t.Skip("disabled until app installer supports it") // TODO revisit when custom field selectors are supported
		list, err := adminClient.List(ctx, v1.ListOptions{
			FieldSelector: fmt.Sprintf("metadata.name=%s,spec.title=%s", recv2.Name, recv2.Spec.Title),
		})
		require.NoError(t, err)
		require.Len(t, list.Items, 1)
		require.Equal(t, recv2.Name, list.Items[0].Name)
	})

	t.Run("should be empty when filter does not match", func(t *testing.T) {
		list, err := adminClient.List(ctx, v1.ListOptions{
			FieldSelector: fmt.Sprintf("metadata.name=%s", "unknown"),
		})
		require.NoError(t, err)
		require.Empty(t, list.Items)
	})
}

// persistInitialConfig helps create an initial config with new receivers using legacy json. Config API blocks receiver
// modifications, so we need to use k8s API to create new receivers before posting the config.
func persistInitialConfig(t *testing.T, amConfig definitions.PostableUserConfig) {
	ctx := context.Background()

	helper := getTestHelper(t)

	receiverClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
	for _, receiver := range amConfig.AlertmanagerConfig.Receivers {
		if receiver.Name == "grafana-default-email" {
			continue
		}

		toCreate := v0alpha1.Receiver{
			ObjectMeta: v1.ObjectMeta{
				Namespace: "default",
			},
			Spec: v0alpha1.ReceiverSpec{
				Title:        receiver.Name,
				Integrations: []v0alpha1.ReceiverIntegration{},
			},
		}

		for _, integration := range receiver.GrafanaManagedReceivers {
			settings := common.Unstructured{}
			require.NoError(t, settings.UnmarshalJSON(integration.Settings))
			toCreate.Spec.Integrations = append(toCreate.Spec.Integrations, v0alpha1.ReceiverIntegration{
				Settings:              settings.Object,
				Type:                  integration.Type,
				DisableResolveMessage: util.Pointer(false),
			})
		}

		created, err := receiverClient.Create(ctx, &toCreate, v1.CreateOptions{})
		require.NoError(t, err)

		for i, integration := range created.Spec.Integrations {
			receiver.GrafanaManagedReceivers[i].UID = *integration.Uid
		}
	}

	nsMapper := func(_ int64) string { return "default" }

	routeClient := test_common.NewRoutingTreeClient(t, helper.Org1.Admin)
	v1route, err := routingtree.ConvertToK8sResource(helper.Org1.AdminServiceAccount.OrgId, *amConfig.AlertmanagerConfig.Route, "", nsMapper)
	require.NoError(t, err)
	_, err = routeClient.Update(ctx, v1route, v1.UpdateOptions{})
	require.NoError(t, err)
}

func createIntegration(t *testing.T, integrationType schema.IntegrationType) v0alpha1.ReceiverIntegration {
	cfg, ok := notifytest.AllKnownV1ConfigsForTesting[integrationType]
	require.Truef(t, ok, "no known config for integration type %s", integrationType)
	return createIntegrationWithSettings(t, integrationType, schema.V1, cfg.Config)
}
func createIntegrationWithSettings(t *testing.T, integrationType schema.IntegrationType, integrationVersion schema.Version, settingsJson string) v0alpha1.ReceiverIntegration {
	settings := common.Unstructured{}
	require.NoError(t, settings.UnmarshalJSON([]byte(settingsJson)))
	return v0alpha1.ReceiverIntegration{
		Settings:              settings.Object,
		Type:                  string(integrationType),
		Version:               string(integrationVersion),
		DisableResolveMessage: util.Pointer(false),
	}
}

func createWildcardPermission(actions ...string) resourcepermissions.SetResourcePermissionCommand {
	return resourcepermissions.SetResourcePermissionCommand{
		Actions:           actions,
		Resource:          "receivers",
		ResourceAttribute: "uid",
		ResourceID:        "*",
	}
}
