package provisioning

import (
	"context"
	"encoding/json"
	"fmt"
	"slices"
	"strings"
	"testing"

	"github.com/grafana/alerting/notify"
	"github.com/grafana/alerting/notify/notifytest"
	"github.com/grafana/alerting/receivers/schema"
	"github.com/grafana/alerting/receivers/slack"
	"github.com/prometheus/alertmanager/config"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/exp/maps"

	"github.com/grafana/grafana/pkg/components/simplejson"
	"github.com/grafana/grafana/pkg/infra/db"
	"github.com/grafana/grafana/pkg/infra/log"
	"github.com/grafana/grafana/pkg/infra/tracing"
	"github.com/grafana/grafana/pkg/services/accesscontrol"
	"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
	"github.com/grafana/grafana/pkg/services/featuremgmt"
	ac "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
	"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
	"github.com/grafana/grafana/pkg/services/ngalert/models"
	"github.com/grafana/grafana/pkg/services/ngalert/notifier"
	"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
	"github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
	"github.com/grafana/grafana/pkg/services/secrets"
	"github.com/grafana/grafana/pkg/services/secrets/database"
	"github.com/grafana/grafana/pkg/services/secrets/manager"
	"github.com/grafana/grafana/pkg/services/user"
	"github.com/grafana/grafana/pkg/util"
	"github.com/grafana/grafana/pkg/util/testutil"
)

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

	sqlStore := db.InitTestDB(t)
	secretsService := manager.SetupTestService(t, database.ProvideSecretsStore(sqlStore))

	redactedUser := &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{
		1: {
			accesscontrol.ActionAlertingProvisioningRead: nil,
		},
	}}
	decryptedUser := &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{
		1: {
			accesscontrol.ActionAlertingProvisioningReadSecrets: nil,
		},
	}}

	t.Run("service gets contact points from AM config", func(t *testing.T) {
		sut := createContactPointServiceSut(t, secretsService)

		cps, err := sut.GetContactPoints(context.Background(), cpsQuery(1), redactedUser)
		require.NoError(t, err)

		require.Len(t, cps, 2)
		require.Equal(t, "grafana-default-email", cps[0].Name)
		require.Equal(t, "slack receiver", cps[1].Name)
	})

	t.Run("service filters contact points by name", func(t *testing.T) {
		sut := createContactPointServiceSut(t, secretsService)

		cps, err := sut.GetContactPoints(context.Background(), cpsQueryWithName(1, "slack receiver"), redactedUser)
		require.NoError(t, err)

		require.Len(t, cps, 1)
		require.Equal(t, "slack receiver", cps[0].Name)
	})

	t.Run("service filters contact points by name, returns empty when no match", func(t *testing.T) {
		sut := createContactPointServiceSut(t, secretsService)

		cps, err := sut.GetContactPoints(context.Background(), cpsQueryWithName(1, "unknown"), redactedUser)
		require.NoError(t, err)

		require.Len(t, cps, 0)
	})

	t.Run("service stitches contact point into org's AM config", func(t *testing.T) {
		sut := createContactPointServiceSut(t, secretsService)
		newCp := createTestContactPoint()

		_, err := sut.CreateContactPoint(context.Background(), 1, redactedUser, newCp, models.ProvenanceAPI)
		require.NoError(t, err)

		cps, err := sut.GetContactPoints(context.Background(), cpsQuery(1), redactedUser)
		require.NoError(t, err)
		require.Len(t, cps, 3)
		require.Equal(t, "test-contact-point", cps[2].Name)
		require.Equal(t, "slack", cps[2].Type)
	})

	t.Run("it's possible to use a custom uid", func(t *testing.T) {
		customUID := "1337"
		sut := createContactPointServiceSut(t, secretsService)
		newCp := createTestContactPoint()
		newCp.UID = customUID

		_, err := sut.CreateContactPoint(context.Background(), 1, redactedUser, newCp, models.ProvenanceAPI)
		require.NoError(t, err)

		cps, err := sut.GetContactPoints(context.Background(), cpsQueryWithName(1, newCp.Name), redactedUser)
		require.NoError(t, err)
		require.Len(t, cps, 1)
		require.Equal(t, customUID, cps[0].UID)
	})

	t.Run("it's not possible to use the same uid twice", func(t *testing.T) {
		customUID := "1337"
		sut := createContactPointServiceSut(t, secretsService)
		newCp := createTestContactPoint()
		newCp.UID = customUID

		_, err := sut.CreateContactPoint(context.Background(), 1, redactedUser, newCp, models.ProvenanceAPI)
		require.NoError(t, err)

		_, err = sut.CreateContactPoint(context.Background(), 1, redactedUser, newCp, models.ProvenanceAPI)
		require.Error(t, err)
	})

	t.Run("create rejects invalid contact points", func(t *testing.T) {
		sut := createContactPointServiceSut(t, secretsService)
		testCases := []struct {
			name string
			cp   func(*definitions.EmbeddedContactPoint)
		}{
			{
				name: "empty type",
				cp: func(cp *definitions.EmbeddedContactPoint) {
					cp.Type = ""
				},
			},
			{
				name: "empty name",
				cp: func(cp *definitions.EmbeddedContactPoint) {
					cp.Name = ""
				},
			},
			{
				name: "invalid UID",
				cp: func(cp *definitions.EmbeddedContactPoint) {
					cp.UID = strings.Repeat("1", util.MaxUIDLength+1)
				},
			},
		}

		for _, tc := range testCases {
			t.Run(tc.name, func(t *testing.T) {
				newCp := createTestContactPoint()
				tc.cp(&newCp)
				_, err := sut.CreateContactPoint(context.Background(), 1, redactedUser, newCp, models.ProvenanceAPI)
				require.ErrorIs(t, err, ErrValidation)
			})
		}
	})

	t.Run("create accepts contact point with type in different cases", func(t *testing.T) {
		sut := createContactPointServiceSut(t, secretsService)
		newCp := createTestContactPoint()
		newCp.Type = "Slack"

		created, err := sut.CreateContactPoint(context.Background(), 1, redactedUser, newCp, models.ProvenanceAPI)
		require.NoError(t, err)
		assert.EqualValues(t, slack.Type, created.Type)

		got, err := sut.GetContactPoints(context.Background(), cpsQueryWithName(1, newCp.Name), redactedUser)
		require.NoError(t, err)
		require.Len(t, got, 1)
		assert.EqualValues(t, slack.Type, got[0].Type)
	})

	t.Run("update rejects invalid contact points", func(t *testing.T) {
		testCases := []struct {
			name string
			cp   func(*definitions.EmbeddedContactPoint)
		}{
			{
				name: "empty type",
				cp: func(cp *definitions.EmbeddedContactPoint) {
					cp.Type = ""
				},
			},
			{
				name: "empty name",
				cp: func(cp *definitions.EmbeddedContactPoint) {
					cp.Name = ""
				},
			},
			{
				name: "nil settings",
				cp: func(cp *definitions.EmbeddedContactPoint) {
					cp.Settings = nil
				},
			},
			{
				name: "invalid settings after merge",
				cp: func(cp *definitions.EmbeddedContactPoint) {
					cp.Settings, _ = simplejson.NewJson([]byte(`{}`))
				},
			},
		}

		sut := createContactPointServiceSut(t, secretsService)
		newCp := createTestContactPoint()
		newCp, err := sut.CreateContactPoint(context.Background(), 1, redactedUser, newCp, models.ProvenanceAPI)
		require.NoError(t, err)

		for _, tc := range testCases {
			t.Run(tc.name, func(t *testing.T) {
				cp := definitions.EmbeddedContactPoint{
					UID:                   newCp.UID,
					Name:                  newCp.Name,
					Type:                  newCp.Type,
					Settings:              newCp.Settings.DeepCopy(),
					DisableResolveMessage: newCp.DisableResolveMessage,
					Provenance:            newCp.Provenance,
				}
				tc.cp(&cp)
				err = sut.UpdateContactPoint(context.Background(), 1, cp, models.ProvenanceAPI)
				require.ErrorIs(t, err, ErrValidation)
			})
		}
	})

	t.Run("update accepts contact points with type in another case", func(t *testing.T) {
		sut := createContactPointServiceSut(t, secretsService)
		newCp := createTestContactPoint()
		newCp, err := sut.CreateContactPoint(context.Background(), 1, redactedUser, newCp, models.ProvenanceAPI)
		require.NoError(t, err)
		newCp.Type = "Slack"

		err = sut.UpdateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI)
		require.NoError(t, err)

		got, err := sut.GetContactPoints(context.Background(), cpsQueryWithName(1, newCp.Name), redactedUser)
		require.NoError(t, err)
		require.Len(t, got, 1)
		assert.EqualValues(t, slack.Type, got[0].Type)
	})

	t.Run("update renames references when group is renamed", func(t *testing.T) {
		cfg := createEncryptedConfig(t, secretsService)
		store := fakes.NewFakeAlertmanagerConfigStore(cfg)
		sut := createContactPointServiceSutWithConfigStore(t, secretsService, store)

		svc := &fakeReceiverService{}
		sut.receiverService = svc

		newCp := createTestContactPoint()
		oldName := newCp.Name
		newName := "new-name"

		newCp, err := sut.CreateContactPoint(context.Background(), 1, redactedUser, newCp, models.ProvenanceAPI)
		require.NoError(t, err)

		newCp.Name = newName

		svc.RenameReceiverInDependentResourcesFunc = func(ctx context.Context, orgID int64, revision *legacy_storage.ConfigRevision, oldName, newName string, receiverProvenance models.Provenance) error {
			revision.RenameReceiverInRoutes(oldName, newName)
			return nil
		}

		err = sut.UpdateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI)
		require.NoError(t, err)

		parsed, err := legacy_storage.DeserializeAlertmanagerConfig([]byte(store.LastSaveCommand.AlertmanagerConfiguration))
		require.NoError(t, err)

		require.Lenf(t, svc.Calls, 1, "service was supposed to be called once")
		assert.Equal(t, "RenameReceiverInDependentResources", svc.Calls[0].Method)
		assertInTransaction(t, svc.Calls[0].Args[0].(context.Context))
		assert.Equal(t, int64(1), svc.Calls[0].Args[1])
		revision := svc.Calls[0].Args[2].(*legacy_storage.ConfigRevision)
		assert.EqualValues(t, parsed.AlertmanagerConfig.Route, revision.Config.AlertmanagerConfig.Route)
		assert.Equal(t, oldName, svc.Calls[0].Args[3])
		assert.Equal(t, newName, svc.Calls[0].Args[4])
		assert.Equal(t, models.ProvenanceAPI, svc.Calls[0].Args[5])
	})

	t.Run("default provenance of contact points is none", func(t *testing.T) {
		sut := createContactPointServiceSut(t, secretsService)

		cps, err := sut.GetContactPoints(context.Background(), cpsQuery(1), redactedUser)
		require.NoError(t, err)

		require.Equal(t, models.ProvenanceNone, models.Provenance(cps[0].Provenance))
	})

	t.Run("contact point provenance should be correctly checked", func(t *testing.T) {
		tests := []struct {
			name   string
			from   models.Provenance
			to     models.Provenance
			errNil bool
		}{
			{
				name:   "should be able to update from provenance none to api",
				from:   models.ProvenanceNone,
				to:     models.ProvenanceAPI,
				errNil: true,
			},
			{
				name:   "should be able to update from provenance none to file",
				from:   models.ProvenanceNone,
				to:     models.ProvenanceFile,
				errNil: true,
			},
			{
				name:   "should not be able to update from provenance api to file",
				from:   models.ProvenanceAPI,
				to:     models.ProvenanceFile,
				errNil: false,
			},
			{
				name:   "should not be able to update from provenance api to none",
				from:   models.ProvenanceAPI,
				to:     models.ProvenanceNone,
				errNil: false,
			},
			{
				name:   "should not be able to update from provenance file to api",
				from:   models.ProvenanceFile,
				to:     models.ProvenanceAPI,
				errNil: false,
			},
			{
				name:   "should not be able to update from provenance file to none",
				from:   models.ProvenanceFile,
				to:     models.ProvenanceNone,
				errNil: false,
			},
		}
		for _, test := range tests {
			t.Run(test.name, func(t *testing.T) {
				sut := createContactPointServiceSut(t, secretsService)
				newCp := createTestContactPoint()

				newCp, err := sut.CreateContactPoint(context.Background(), 1, redactedUser, newCp, test.from)
				require.NoError(t, err)

				cps, err := sut.GetContactPoints(context.Background(), cpsQueryWithName(1, newCp.Name), redactedUser)
				require.NoError(t, err)
				require.Equal(t, newCp.UID, cps[0].UID)
				require.Equal(t, test.from, models.Provenance(cps[0].Provenance))

				err = sut.UpdateContactPoint(context.Background(), 1, newCp, test.to)
				if test.errNil {
					require.NoError(t, err)

					cps, err = sut.GetContactPoints(context.Background(), cpsQueryWithName(1, newCp.Name), redactedUser)
					require.NoError(t, err)
					require.Equal(t, newCp.UID, cps[0].UID)
					require.Equal(t, test.to, models.Provenance(cps[0].Provenance))
				} else {
					require.Error(t, err, fmt.Sprintf("cannot change provenance from '%s' to '%s'", test.from, test.to))
				}
			})
		}
	})

	t.Run("service respects concurrency token when updating", func(t *testing.T) {
		cfg := createEncryptedConfig(t, secretsService)
		fakeConfigStore := fakes.NewFakeAlertmanagerConfigStore(cfg)
		sut := createContactPointServiceSutWithConfigStore(t, secretsService, fakeConfigStore)
		newCp := createTestContactPoint()
		config, err := sut.configStore.Get(context.Background(), 1)
		require.NoError(t, err)
		expectedConcurrencyToken := config.ConcurrencyToken

		_, err = sut.CreateContactPoint(context.Background(), 1, redactedUser, newCp, models.ProvenanceAPI)
		require.NoError(t, err)

		intercepted := fakeConfigStore.LastSaveCommand
		require.Equal(t, expectedConcurrencyToken, intercepted.FetchedConfigurationHash)
	})

	t.Run("secrets are parsed in a case-insensitive way", func(t *testing.T) {
		// JSON unmarshalling is case-insensitive. This means we can have
		// a setting named "TOKEN" instead of "token". This test ensures that
		// we handle such cases correctly and the token value is properly parsed,
		// even if the setting key does not match the JSON key exactly.
		tests := []struct {
			settingsJSON  string
			expectedValue string
			name          string
		}{
			{
				settingsJSON:  `{"recipient":"value_recipient","TOKEN":"some-other-token"}`,
				expectedValue: "some-other-token",
				name:          "token key is uppercased",
			},

			// This test checks that if multiple token keys are present in the settings,
			// the key with the exact matching name is used.
			{
				settingsJSON:  `{"recipient":"value_recipient","TOKEN":"some-other-token", "token": "second-token"}`,
				expectedValue: "second-token",
				name:          "multiple token keys",
			},
		}

		for _, tc := range tests {
			t.Run(tc.name, func(t *testing.T) {
				sut := createContactPointServiceSut(t, secretsService)

				newCp := createTestContactPoint()
				settings, _ := simplejson.NewJson([]byte(tc.settingsJSON))
				newCp.Settings = settings

				_, err := sut.CreateContactPoint(context.Background(), 1, redactedUser, newCp, models.ProvenanceAPI)
				require.NoError(t, err)

				q := cpsQueryWithName(1, newCp.Name)
				q.Decrypt = true
				cps, err := sut.GetContactPoints(context.Background(), q, decryptedUser)
				require.NoError(t, err)
				require.Len(t, cps, 1)
				require.Equal(t, tc.expectedValue, cps[0].Settings.Get("token").MustString())
			})
		}
	})
}

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

	secretsService := manager.SetupTestService(t, database.ProvideSecretsStore(db.InitTestDB(t)))

	redactedUser := &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{
		1: {
			accesscontrol.ActionAlertingProvisioningRead: nil,
		},
	}}

	decryptedUser := &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{
		1: {
			accesscontrol.ActionAlertingProvisioningReadSecrets: nil,
		},
	}}

	t.Run("GetContactPoints gets redacted contact points by default", func(t *testing.T) {
		sut := createContactPointServiceSut(t, secretsService)

		cps, err := sut.GetContactPoints(context.Background(), cpsQuery(1), redactedUser)
		require.NoError(t, err)

		require.Len(t, cps, 2)
		require.Equal(t, "slack receiver", cps[1].Name)
		require.Equal(t, definitions.RedactedValue, cps[1].Settings.Get("url").MustString())
	})

	t.Run("GetContactPoints errors when Decrypt = true and user does not have permissions", func(t *testing.T) {
		sut := createContactPointServiceSut(t, secretsService)

		q := cpsQuery(1)
		q.Decrypt = true
		_, err := sut.GetContactPoints(context.Background(), q, redactedUser)
		require.ErrorIs(t, err, ac.ErrAuthorizationBase)
	})
	t.Run("GetContactPoints errors when Decrypt = true and user is nil", func(t *testing.T) {
		sut := createContactPointServiceSut(t, secretsService)

		q := cpsQuery(1)
		q.Decrypt = true
		_, err := sut.GetContactPoints(context.Background(), q, nil)
		require.ErrorIs(t, err, ac.ErrAuthorizationBase)
	})

	t.Run("GetContactPoints gets decrypted contact points when Decrypt = true and user has permissions", func(t *testing.T) {
		sut := createContactPointServiceSut(t, secretsService)

		expectedName := "slack receiver"
		q := cpsQueryWithName(1, expectedName)
		q.Decrypt = true
		cps, err := sut.GetContactPoints(context.Background(), q, decryptedUser)
		require.NoError(t, err)

		require.Len(t, cps, 1)
		require.Equal(t, expectedName, cps[0].Name)
		require.Equal(t, "secure url", cps[0].Settings.Get("url").MustString())
	})
}

func TestRemoveSecretsForContactPoint(t *testing.T) {
	overrides := map[schema.IntegrationType]func(settings map[string]any){
		"webhook": func(settings map[string]any) { // add additional field to the settings because valid config does not allow it to be specified along with password
			settings["authorization_credentials"] = "test-authz-creds"
		},
		"jira": func(settings map[string]any) { // add additional field to the settings because valid config does not allow it to be specified along with password
			settings["api_token"] = "test-token"
		},
		"oncall": func(settings map[string]any) { // add authorization_credentials field since it's expected as a secret field
			settings["authorization_credentials"] = "test-authz-creds"
		},
	}

	configs := notifytest.AllKnownV1ConfigsForTesting
	keys := maps.Keys(configs)
	slices.Sort(keys)
	for _, integrationType := range keys {
		integration := models.IntegrationGen(models.IntegrationMuts.WithValidConfig(integrationType))()
		if f, ok := overrides[integrationType]; ok {
			f(integration.Settings)
		}
		settingsRaw, err := json.Marshal(integration.Settings)
		require.NoError(t, err)
		typeSchema, _ := notify.GetSchemaVersionForIntegration(integrationType, schema.V1)
		expectedFields := typeSchema.GetSecretFieldsPaths()

		t.Run(string(integrationType), func(t *testing.T) {
			cp := definitions.EmbeddedContactPoint{
				Name:     "integration-" + string(integrationType),
				Type:     string(integrationType),
				Settings: simplejson.MustJson(settingsRaw),
			}
			secureFields, err := RemoveSecretsForContactPoint(&cp)
			require.NoError(t, err)

		FIELDS_ASSERT:
			for _, path := range expectedFields {
				field := path.String()
				assert.Contains(t, secureFields, field)
				var expectedValue any = integration.Settings
				for _, segment := range path {
					v, ok := expectedValue.(map[string]any)
					if !ok {
						assert.Fail(t, fmt.Sprintf("cannot get expected value for field '%s'", field))
						continue FIELDS_ASSERT
					}
					expectedValue = v[segment]
				}
				assert.EqualValues(t, secureFields[field], expectedValue)
				v, err := cp.Settings.GetPath(path...).Value()
				assert.NoError(t, err)
				assert.Nilf(t, v, "field %s is expected to be removed from the settings", field)
			}
		})
	}
}

func createContactPointServiceSut(t *testing.T, secretService secrets.Service) *ContactPointService {
	// Encrypt secure settings.
	cfg := createEncryptedConfig(t, secretService)
	store := fakes.NewFakeAlertmanagerConfigStore(cfg)
	return createContactPointServiceSutWithConfigStore(t, secretService, store)
}

func createContactPointServiceSutWithConfigStore(t *testing.T, secretService secrets.Service, configStore legacy_storage.AMConfigStore) *ContactPointService {
	t.Helper()
	// Encrypt secure settings.
	xact := newNopTransactionManager()
	provisioningStore := fakes.NewFakeProvisioningStore()

	receiverService := notifier.NewReceiverService(
		ac.NewReceiverAccess[*models.Receiver](acimpl.ProvideAccessControl(featuremgmt.WithFeatures()), true),
		legacy_storage.NewAlertmanagerConfigStore(configStore, notifier.NewExtraConfigsCrypto(secretService)),
		provisioningStore,
		&fakeAlertRuleNotificationStore{},
		secretService,
		xact,
		log.NewNopLogger(),
		fakes.NewFakeReceiverPermissionsService(),
		tracing.InitializeTracerForTest(),
		false,
	)

	return NewContactPointService(
		legacy_storage.NewAlertmanagerConfigStore(configStore, notifier.NewExtraConfigsCrypto(secretService)),
		secretService,
		provisioningStore,
		xact,
		receiverService,
		log.NewNopLogger(),
		nil,
		fakes.NewFakeReceiverPermissionsService(),
	)
}

func createTestContactPoint() definitions.EmbeddedContactPoint {
	settings, _ := simplejson.NewJson([]byte(`{"recipient":"value_recipient","token":"value_token"}`))
	return definitions.EmbeddedContactPoint{
		Name:     "test-contact-point",
		Type:     "slack",
		Settings: settings,
	}
}

func cpsQuery(orgID int64) ContactPointQuery {
	return ContactPointQuery{
		OrgID: orgID,
	}
}

func cpsQueryWithName(orgID int64, name string) ContactPointQuery {
	return ContactPointQuery{
		OrgID: orgID,
		Name:  name,
	}
}

func createEncryptedConfig(t *testing.T, secretService secrets.Service) string {
	c := &definitions.PostableUserConfig{}
	err := json.Unmarshal([]byte(defaultAlertmanagerConfigJSON), c)
	require.NoError(t, err)
	err = notifier.EncryptReceiverConfigs(c.AlertmanagerConfig.Receivers, func(ctx context.Context, payload []byte) ([]byte, error) {
		return secretService.Encrypt(ctx, payload, secrets.WithoutScope())
	})
	require.NoError(t, err)
	bytes, err := json.Marshal(c)
	require.NoError(t, err)
	return string(bytes)
}

func TestStitchReceivers(t *testing.T) {
	type testCase struct {
		name               string
		initial            *definitions.PostableUserConfig
		new                *definitions.PostableGrafanaReceiver
		expCfg             definitions.PostableApiAlertingConfig
		expOldReceiver     *string
		expCreatedReceiver bool
		expFullRemoval     bool
	}

	cases := []testCase{
		{
			name: "non matching receiver by UID, no change",
			new: &definitions.PostableGrafanaReceiver{
				UID: "does not exist",
			},
			expOldReceiver: nil,
			expCfg:         createTestConfigWithReceivers().AlertmanagerConfig,
		},
		{
			name: "matching receiver with unchanged name, replaces",
			new: &definitions.PostableGrafanaReceiver{
				UID:  "ghi",
				Name: "receiver-2",
				Type: "teams",
			},
			expOldReceiver: util.Pointer("receiver-2"),
			expCfg: definitions.PostableApiAlertingConfig{
				Config: definitions.Config{
					Route: &definitions.Route{
						Receiver: "receiver-1",
						Routes: []*definitions.Route{
							{
								Receiver: "receiver-1",
							},
						},
					},
				},
				Receivers: []*definitions.PostableApiReceiver{
					{
						Receiver: config.Receiver{
							Name: "receiver-1",
						},
						PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
							GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
								{
									UID:  "abc",
									Name: "receiver-1",
									Type: "slack",
								},
							},
						},
					},
					{
						Receiver: config.Receiver{
							Name: "receiver-2",
						},
						PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
							GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
								{
									UID:  "def",
									Name: "receiver-2",
									Type: "slack",
								},
								{
									UID:  "ghi",
									Name: "receiver-2",
									Type: "teams",
								},
								{
									UID:  "jkl",
									Name: "receiver-2",
									Type: "discord",
								},
							},
						},
					},
				},
			},
		},
		{
			name: "rename with only one receiver in group, renames group and references",
			new: &definitions.PostableGrafanaReceiver{
				UID:  "abc",
				Name: "new-receiver",
				Type: "slack",
			},
			expOldReceiver:     util.Pointer("receiver-1"),
			expCreatedReceiver: true,
			expFullRemoval:     true,
			expCfg: definitions.PostableApiAlertingConfig{
				Config: definitions.Config{
					Route: &definitions.Route{
						Receiver: "receiver-1",
						Routes: []*definitions.Route{
							{
								Receiver: "receiver-1",
							},
						},
					},
				},
				Receivers: []*definitions.PostableApiReceiver{
					{
						Receiver: config.Receiver{
							Name: "new-receiver",
						},
						PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
							GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
								{
									UID:  "abc",
									Name: "new-receiver",
									Type: "slack",
								},
							},
						},
					},
					{
						Receiver: config.Receiver{
							Name: "receiver-2",
						},
						PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
							GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
								{
									UID:  "def",
									Name: "receiver-2",
									Type: "slack",
								},
								{
									UID:  "ghi",
									Name: "receiver-2",
									Type: "email",
								},
								{
									UID:  "jkl",
									Name: "receiver-2",
									Type: "discord",
								},
							},
						},
					},
				},
			},
		},
		{
			name: "rename to another existing group, moves receiver",
			new: &definitions.PostableGrafanaReceiver{
				UID:  "def",
				Name: "receiver-1",
				Type: "slack",
			},
			expOldReceiver:     util.Pointer("receiver-2"),
			expCreatedReceiver: false,
			expCfg: definitions.PostableApiAlertingConfig{
				Config: definitions.Config{
					Route: &definitions.Route{
						Receiver: "receiver-1",
						Routes: []*definitions.Route{
							{
								Receiver: "receiver-1",
							},
						},
					},
				},
				Receivers: []*definitions.PostableApiReceiver{
					{
						Receiver: config.Receiver{
							Name: "receiver-1",
						},
						PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
							GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
								{
									UID:  "abc",
									Name: "receiver-1",
									Type: "slack",
								},
								{
									UID:  "def",
									Name: "receiver-1",
									Type: "slack",
								},
							},
						},
					},
					{
						Receiver: config.Receiver{
							Name: "receiver-2",
						},
						PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
							GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
								{
									UID:  "ghi",
									Name: "receiver-2",
									Type: "email",
								},
								{
									UID:  "jkl",
									Name: "receiver-2",
									Type: "discord",
								},
							},
						},
					},
				},
			},
		},
		{
			name: "rename to another, larger group",
			initial: &definitions.PostableUserConfig{
				AlertmanagerConfig: definitions.PostableApiAlertingConfig{
					Config: definitions.Config{
						Route: &definitions.Route{
							Receiver: "receiver-1",
							Routes: []*definitions.Route{
								{
									Receiver: "receiver-1",
								},
							},
						},
					},
					Receivers: []*definitions.PostableApiReceiver{
						{
							Receiver: config.Receiver{
								Name: "receiver-1",
							},
							PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
								GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
									{
										UID:  "1",
										Name: "receiver-1",
										Type: "slack",
									},
									{
										UID:  "2",
										Name: "receiver-1",
										Type: "slack",
									},
								},
							},
						},
						{
							Receiver: config.Receiver{
								Name: "receiver-2",
							},
							PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
								GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
									{
										UID:  "3",
										Name: "receiver-2",
										Type: "slack",
									},
									{
										UID:  "4",
										Name: "receiver-2",
										Type: "slack",
									},
									{
										UID:  "5",
										Name: "receiver-2",
										Type: "slack",
									},
								},
							},
						},
					},
				},
			},
			new: &definitions.PostableGrafanaReceiver{
				UID:  "2",
				Name: "receiver-2",
				Type: "slack",
			},
			expOldReceiver:     util.Pointer("receiver-1"),
			expCreatedReceiver: false,
			expCfg: definitions.PostableApiAlertingConfig{
				Config: definitions.Config{
					Route: &definitions.Route{
						Receiver: "receiver-1",
						Routes: []*definitions.Route{
							{
								Receiver: "receiver-1",
							},
						},
					},
				},
				Receivers: []*definitions.PostableApiReceiver{
					{
						Receiver: config.Receiver{
							Name: "receiver-1",
						},
						PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
							GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
								{
									UID:  "1",
									Name: "receiver-1",
									Type: "slack",
								},
							},
						},
					},
					{
						Receiver: config.Receiver{
							Name: "receiver-2",
						},
						PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
							GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
								{
									UID:  "3",
									Name: "receiver-2",
									Type: "slack",
								},
								{
									UID:  "4",
									Name: "receiver-2",
									Type: "slack",
								},
								{
									UID:  "5",
									Name: "receiver-2",
									Type: "slack",
								},
								{
									UID:  "2",
									Name: "receiver-2",
									Type: "slack",
								},
							},
						},
					},
				},
			},
		},
		{
			name: "rename when there are many groups",
			initial: &definitions.PostableUserConfig{
				AlertmanagerConfig: definitions.PostableApiAlertingConfig{
					Config: definitions.Config{
						Route: &definitions.Route{
							Receiver: "receiver-1",
							Routes: []*definitions.Route{
								{
									Receiver: "receiver-1",
								},
							},
						},
					},
					Receivers: []*definitions.PostableApiReceiver{
						{
							Receiver: config.Receiver{
								Name: "receiver-1",
							},
							PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
								GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
									{
										UID:  "1",
										Name: "receiver-1",
										Type: "slack",
									},
									{
										UID:  "2",
										Name: "receiver-1",
										Type: "slack",
									},
								},
							},
						},
						{
							Receiver: config.Receiver{
								Name: "receiver-2",
							},
							PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
								GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
									{
										UID:  "3",
										Name: "receiver-2",
										Type: "slack",
									},
								},
							},
						},
						{
							Receiver: config.Receiver{
								Name: "receiver-3",
							},
							PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
								GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
									{
										UID:  "4",
										Name: "receiver-4",
										Type: "slack",
									},
								},
							},
						},
						{
							Receiver: config.Receiver{
								Name: "receiver-4",
							},
							PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
								GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
									{
										UID:  "5",
										Name: "receiver-4",
										Type: "slack",
									},
								},
							},
						},
					},
				},
			},
			new: &definitions.PostableGrafanaReceiver{
				UID:  "2",
				Name: "receiver-4",
				Type: "slack",
			},
			expOldReceiver:     util.Pointer("receiver-1"),
			expCreatedReceiver: false,
			expCfg: definitions.PostableApiAlertingConfig{
				Config: definitions.Config{
					Route: &definitions.Route{
						Receiver: "receiver-1",
						Routes: []*definitions.Route{
							{
								Receiver: "receiver-1",
							},
						},
					},
				},
				Receivers: []*definitions.PostableApiReceiver{
					{
						Receiver: config.Receiver{
							Name: "receiver-1",
						},
						PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
							GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
								{
									UID:  "1",
									Name: "receiver-1",
									Type: "slack",
								},
							},
						},
					},
					{
						Receiver: config.Receiver{
							Name: "receiver-2",
						},
						PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
							GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
								{
									UID:  "3",
									Name: "receiver-2",
									Type: "slack",
								},
							},
						},
					},
					{
						Receiver: config.Receiver{
							Name: "receiver-3",
						},
						PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
							GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
								{
									UID:  "4",
									Name: "receiver-4",
									Type: "slack",
								},
							},
						},
					},
					{
						Receiver: config.Receiver{
							Name: "receiver-4",
						},
						PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
							GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
								{
									UID:  "5",
									Name: "receiver-4",
									Type: "slack",
								},
								{
									UID:  "2",
									Name: "receiver-4",
									Type: "slack",
								},
							},
						},
					},
				},
			},
		},
		{
			name: "rename to a name that doesn't exist, creates new group and moves",
			new: &definitions.PostableGrafanaReceiver{
				UID:  "jkl",
				Name: "brand-new-group",
				Type: "opsgenie",
			},
			expOldReceiver:     util.Pointer("receiver-2"),
			expCreatedReceiver: true,
			expCfg: definitions.PostableApiAlertingConfig{
				Config: definitions.Config{
					Route: &definitions.Route{
						Receiver: "receiver-1",
						Routes: []*definitions.Route{
							{
								Receiver: "receiver-1",
							},
						},
					},
				},
				Receivers: []*definitions.PostableApiReceiver{
					{
						Receiver: config.Receiver{
							Name: "receiver-1",
						},
						PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
							GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
								{
									UID:  "abc",
									Name: "receiver-1",
									Type: "slack",
								},
							},
						},
					},
					{
						Receiver: config.Receiver{
							Name: "receiver-2",
						},
						PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
							GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
								{
									UID:  "def",
									Name: "receiver-2",
									Type: "slack",
								},
								{
									UID:  "ghi",
									Name: "receiver-2",
									Type: "email",
								},
							},
						},
					},
					{
						Receiver: config.Receiver{
							Name: "brand-new-group",
						},
						PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
							GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
								{
									UID:  "jkl",
									Name: "brand-new-group",
									Type: "opsgenie",
								},
							},
						},
					},
				},
			},
		},
		{
			name:    "rename an inconsistent group in the database, algorithm fixes it",
			initial: createInconsistentTestConfigWithReceivers(),
			new: &definitions.PostableGrafanaReceiver{
				UID:  "ghi",
				Name: "brand-new-group",
				Type: "opsgenie",
			},
			expOldReceiver:     util.Pointer("receiver-2"), // Not the inconsistent receiver-3?
			expCreatedReceiver: true,
			expCfg: definitions.PostableApiAlertingConfig{
				Config: definitions.Config{
					Route: &definitions.Route{
						Receiver: "receiver-1",
						Routes: []*definitions.Route{
							{
								Receiver: "receiver-1",
							},
						},
					},
				},
				Receivers: []*definitions.PostableApiReceiver{
					{
						Receiver: config.Receiver{
							Name: "receiver-1",
						},
						PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
							GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
								{
									UID:  "abc",
									Name: "receiver-1",
									Type: "slack",
								},
							},
						},
					},
					{
						Receiver: config.Receiver{
							Name: "receiver-2",
						},
						PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
							GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
								{
									UID:  "def",
									Name: "receiver-2",
									Type: "slack",
								},
								{
									UID:  "jkl",
									Name: "receiver-2",
									Type: "discord",
								},
							},
						},
					},
					{
						Receiver: config.Receiver{
							Name: "brand-new-group",
						},
						PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
							GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
								{
									UID:  "ghi",
									Name: "brand-new-group",
									Type: "opsgenie",
								},
							},
						},
					},
				},
			},
		},
		{
			name: "single item group rename to existing group",
			initial: &definitions.PostableUserConfig{
				AlertmanagerConfig: definitions.PostableApiAlertingConfig{
					Config: definitions.Config{
						Route: &definitions.Route{
							Receiver: "receiver-1",
							Routes: []*definitions.Route{
								{
									Receiver: "receiver-1",
								},
								{
									Receiver: "receiver-2",
								},
							},
						},
					},
					Receivers: []*definitions.PostableApiReceiver{
						{
							Receiver: config.Receiver{
								Name: "receiver-1",
							},
							PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
								GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
									{
										UID:  "1",
										Name: "receiver-1",
										Type: "slack",
									},
									{
										UID:  "2",
										Name: "receiver-1",
										Type: "slack",
									},
								},
							},
						},
						{
							Receiver: config.Receiver{
								Name: "receiver-2",
							},
							PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
								GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
									{
										UID:  "3",
										Name: "receiver-2",
										Type: "slack",
									},
								},
							},
						},
					},
				},
			},
			new: &definitions.PostableGrafanaReceiver{
				UID:  "3",
				Name: "receiver-1",
				Type: "slack",
			},
			expOldReceiver:     util.Pointer("receiver-2"),
			expCreatedReceiver: false,
			expFullRemoval:     true,
			expCfg: definitions.PostableApiAlertingConfig{
				Config: definitions.Config{
					Route: &definitions.Route{
						Receiver: "receiver-1",
						Routes: []*definitions.Route{
							{
								Receiver: "receiver-1",
							},
							{
								Receiver: "receiver-2",
							},
						},
					},
				},
				Receivers: []*definitions.PostableApiReceiver{
					{
						Receiver: config.Receiver{
							Name: "receiver-1",
						},
						PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
							GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
								{
									UID:  "1",
									Name: "receiver-1",
									Type: "slack",
								},
								{
									UID:  "2",
									Name: "receiver-1",
									Type: "slack",
								},
								{
									UID:  "3",
									Name: "receiver-1",
									Type: "slack",
								},
							},
						},
					},
				},
			},
		},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			cfg := createTestConfigWithReceivers()
			if c.initial != nil {
				cfg = c.initial
			}

			renamedReceiver, fullRemoval, createdReceiver := stitchReceiver(cfg, c.new)
			assert.EqualValuesf(t, c.expOldReceiver, renamedReceiver, "expected old receiver to be %v, got %v", c.expOldReceiver, renamedReceiver)
			assert.Equalf(t, c.expFullRemoval, fullRemoval, "expected full removal to be %t, got %t", c.expFullRemoval, fullRemoval)
			assert.Equalf(t, c.expCreatedReceiver, createdReceiver, "expected created receiver to be %t, got %t", c.expCreatedReceiver, createdReceiver)
			require.Equal(t, c.expCfg, cfg.AlertmanagerConfig)
		})
	}
}

func createTestConfigWithReceivers() *definitions.PostableUserConfig {
	return &definitions.PostableUserConfig{
		AlertmanagerConfig: definitions.PostableApiAlertingConfig{
			Config: definitions.Config{
				Route: &definitions.Route{
					Receiver: "receiver-1",
					Routes: []*definitions.Route{
						{
							Receiver: "receiver-1",
						},
					},
				},
			},
			Receivers: []*definitions.PostableApiReceiver{
				{
					Receiver: config.Receiver{
						Name: "receiver-1",
					},
					PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
						GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
							{
								UID:  "abc",
								Name: "receiver-1",
								Type: "slack",
							},
						},
					},
				},
				{
					Receiver: config.Receiver{
						Name: "receiver-2",
					},
					PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
						GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
							{
								UID:  "def",
								Name: "receiver-2",
								Type: "slack",
							},
							{
								UID:  "ghi",
								Name: "receiver-2",
								Type: "email",
							},
							{
								UID:  "jkl",
								Name: "receiver-2",
								Type: "discord",
							},
						},
					},
				},
			},
		},
	}
}

// This is an invalid config, with inconsistently named receivers (intentionally).
func createInconsistentTestConfigWithReceivers() *definitions.PostableUserConfig {
	return &definitions.PostableUserConfig{
		AlertmanagerConfig: definitions.PostableApiAlertingConfig{
			Config: definitions.Config{
				Route: &definitions.Route{
					Receiver: "receiver-1",
					Routes: []*definitions.Route{
						{
							Receiver: "receiver-1",
						},
					},
				},
			},
			Receivers: []*definitions.PostableApiReceiver{
				{
					Receiver: config.Receiver{
						Name: "receiver-1",
					},
					PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
						GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
							{
								UID:  "abc",
								Name: "receiver-1",
								Type: "slack",
							},
						},
					},
				},
				{
					Receiver: config.Receiver{
						Name: "receiver-2",
					},
					PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
						GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
							{
								UID:  "def",
								Name: "receiver-2",
								Type: "slack",
							},
							{
								UID:  "ghi",
								Name: "receiver-3",
								Type: "email",
							},
							{
								UID:  "jkl",
								Name: "receiver-2",
								Type: "discord",
							},
						},
					},
				},
			},
		},
	}
}
