package provisioning

import (
	"context"
	"errors"
	"fmt"
	"testing"

	"github.com/grafana/alerting/definition"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"

	"github.com/grafana/grafana/pkg/infra/log"
	"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/legacy_storage"
	"github.com/grafana/grafana/pkg/services/ngalert/provisioning/validation"
	"github.com/grafana/grafana/pkg/services/ngalert/remote/client"
	"github.com/grafana/grafana/pkg/util"
)

func TestGetTemplates(t *testing.T) {
	orgID := int64(1)
	revision := &legacy_storage.ConfigRevision{
		Config: &definitions.PostableUserConfig{
			TemplateFiles: map[string]string{
				"template1": "test1",
				"template2": "test2",
				"template3": "test3",
			},
			ExtraConfigs: []definitions.ExtraConfiguration{
				{
					Identifier: "1234",
					TemplateFiles: map[string]string{
						"template1": "imported-test1",
						"template4": "imported-test4",
					},
				},
			},
		},
	}

	t.Run("returns templates from config file", func(t *testing.T) {
		sut, store, prov := createTemplateServiceSut()
		store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
			assert.Equal(t, orgID, org)
			return revision, nil
		}
		prov.EXPECT().GetProvenances(mock.Anything, mock.Anything, mock.Anything).Return(map[string]models.Provenance{
			"template1": models.ProvenanceAPI,
			"template2": models.ProvenanceFile,
		}, nil)

		result, err := sut.GetTemplates(context.Background(), orgID)
		require.NoError(t, err)

		expected := []definitions.NotificationTemplate{
			newNotificationTemplate(
				"template1",
				"test1",
				models.ProvenanceAPI,
				definition.GrafanaTemplateKind,
			),
			newNotificationTemplate(
				"template2",
				"test2",
				models.ProvenanceFile,
				definition.GrafanaTemplateKind,
			),
			newNotificationTemplate(
				"template3",
				"test3",
				models.ProvenanceNone,
				definition.GrafanaTemplateKind,
			),
		}

		require.EqualValues(t, expected, result)

		prov.AssertCalled(t, "GetProvenances", mock.Anything, orgID, (&definitions.NotificationTemplate{}).ResourceType())
		prov.AssertExpectations(t)
	})

	t.Run("returns empty list when config file contains no templates", func(t *testing.T) {
		sut, store, prov := createTemplateServiceSut()
		store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
			return &legacy_storage.ConfigRevision{
				Config: &definitions.PostableUserConfig{},
			}, nil
		}

		result, err := sut.GetTemplates(context.Background(), 1)

		require.NoError(t, err)
		require.Empty(t, result)
		prov.AssertExpectations(t)
	})

	t.Run("returns imported templates if enabled", func(t *testing.T) {
		sut, store, prov := createTemplateServiceSut()
		sut = sut.WithIncludeImported()
		store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
			assert.Equal(t, orgID, org)
			return revision, nil
		}
		prov.EXPECT().GetProvenances(mock.Anything, mock.Anything, mock.Anything).Return(map[string]models.Provenance{
			"template1": models.ProvenanceAPI,
			"template2": models.ProvenanceFile,
		}, nil)

		result, err := sut.GetTemplates(context.Background(), orgID)
		require.NoError(t, err)

		expected := []definitions.NotificationTemplate{
			newNotificationTemplate(
				"template1",
				"test1",
				models.ProvenanceAPI,
				definition.GrafanaTemplateKind,
			),
			newNotificationTemplate(
				"template2",
				"test2",
				models.ProvenanceFile,
				definition.GrafanaTemplateKind,
			),
			newNotificationTemplate(
				"template3",
				"test3",
				models.ProvenanceNone,
				definition.GrafanaTemplateKind,
			),
			newNotificationTemplate(
				"template1",
				"imported-test1",
				models.ProvenanceConvertedPrometheus,
				definition.MimirTemplateKind,
			),
			newNotificationTemplate(
				"template4",
				"imported-test4",
				models.ProvenanceConvertedPrometheus,
				definition.MimirTemplateKind,
			),
		}

		require.EqualValues(t, expected, result)

		prov.AssertCalled(t, "GetProvenances", mock.Anything, orgID, (&definitions.NotificationTemplate{}).ResourceType())
		prov.AssertExpectations(t)
	})

	t.Run("propagates errors", func(t *testing.T) {
		t.Run("when unable to read config", func(t *testing.T) {
			sut, store, prov := createTemplateServiceSut()
			expectedErr := errors.New("test")
			store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
				return nil, expectedErr
			}

			_, err := sut.GetTemplates(context.Background(), 1)

			require.ErrorIs(t, err, expectedErr)

			prov.AssertExpectations(t)
		})

		t.Run("when provenance status fails", func(t *testing.T) {
			sut, store, prov := createTemplateServiceSut()

			store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
				return revision, nil
			}

			expectedErr := errors.New("test")
			prov.EXPECT().GetProvenances(mock.Anything, mock.Anything, mock.Anything).Return(nil, expectedErr)

			_, err := sut.GetTemplates(context.Background(), 1)

			require.ErrorIs(t, err, expectedErr)

			prov.AssertExpectations(t)
		})
	})
}

func TestGetTemplate(t *testing.T) {
	orgID := int64(1)
	templateName := "template1"
	templateContent := "test1"
	importedTemplateName := "template2"
	importedTemplateContent := "imported"
	revision := &legacy_storage.ConfigRevision{
		Config: &definitions.PostableUserConfig{
			TemplateFiles: map[string]string{
				templateName: templateContent,
			},
			ExtraConfigs: []definitions.ExtraConfiguration{
				{
					Identifier: "1234",
					TemplateFiles: map[string]string{
						importedTemplateName: importedTemplateContent,
					},
				},
			},
		},
	}

	t.Run("return a template from config by name", func(t *testing.T) {
		sut, store, prov := createTemplateServiceSut()
		store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
			assert.Equal(t, orgID, org)
			return revision, nil
		}
		prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil)

		result, err := sut.GetTemplate(context.Background(), orgID, templateName)
		require.NoError(t, err)

		expected := newNotificationTemplate(
			templateName,
			templateContent,
			models.ProvenanceAPI,
			definition.GrafanaTemplateKind,
		)

		require.Equal(t, expected, result)

		prov.AssertCalled(t, "GetProvenance", mock.Anything, mock.MatchedBy(func(t *definitions.NotificationTemplate) bool {
			return t.Name == expected.Name
		}), orgID)
		prov.AssertExpectations(t)
	})

	t.Run("imported templates cannot be retrieved by name", func(t *testing.T) {
		sut, store, _ := createTemplateServiceSut()
		store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
			assert.Equal(t, orgID, org)
			return revision, nil
		}
		_, err := sut.GetTemplate(context.Background(), orgID, importedTemplateName)
		require.ErrorIs(t, err, ErrTemplateNotFound)
	})

	t.Run("return a template from config by UID", func(t *testing.T) {
		sut, store, prov := createTemplateServiceSut()
		store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
			assert.Equal(t, orgID, org)
			return revision, nil
		}
		prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil)

		result, err := sut.GetTemplate(context.Background(), orgID, templateUID(definition.GrafanaTemplateKind, templateName))
		require.NoError(t, err)

		expected := newNotificationTemplate(
			templateName,
			templateContent,
			models.ProvenanceNone,
			definition.GrafanaTemplateKind,
		)
		require.Equal(t, expected, result)
	})

	t.Run("return an imported template from config by UID", func(t *testing.T) {
		sut, store, prov := createTemplateServiceSut()
		store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
			assert.Equal(t, orgID, org)
			return revision, nil
		}

		uid := templateUID(definition.MimirTemplateKind, importedTemplateName)
		t.Run("should be not found without flag enabled", func(t *testing.T) {
			_, err := sut.GetTemplate(context.Background(), orgID, uid)
			require.ErrorIs(t, err, ErrTemplateNotFound)
		})

		result, err := sut.WithIncludeImported().GetTemplate(context.Background(), orgID, uid)
		require.NoError(t, err)

		expected := newNotificationTemplate(
			importedTemplateName,
			importedTemplateContent,
			models.ProvenanceConvertedPrometheus,
			definition.MimirTemplateKind,
		)
		require.Equal(t, expected, result)
		prov.AssertExpectations(t)
	})

	t.Run("returns ErrTemplateNotFound when template does not exist", func(t *testing.T) {
		sut, store, prov := createTemplateServiceSut()
		store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
			assert.Equal(t, orgID, org)
			return revision, nil
		}
		_, err := sut.GetTemplate(context.Background(), orgID, "not-found")
		require.ErrorIs(t, err, ErrTemplateNotFound)
		prov.AssertExpectations(t)
	})

	t.Run("service propagates errors", func(t *testing.T) {
		t.Run("when unable to read config", func(t *testing.T) {
			sut, store, prov := createTemplateServiceSut()
			expectedErr := errors.New("test")
			store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
				return nil, expectedErr
			}

			_, err := sut.GetTemplate(context.Background(), 1, templateName)

			require.ErrorIs(t, err, expectedErr)

			prov.AssertExpectations(t)
		})

		t.Run("when provenance status fails", func(t *testing.T) {
			sut, store, prov := createTemplateServiceSut()
			store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
				return revision, nil
			}
			expectedErr := errors.New("test")
			prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, expectedErr)

			_, err := sut.GetTemplate(context.Background(), orgID, templateName)
			require.ErrorIs(t, err, expectedErr)

			prov.AssertExpectations(t)
		})
	})
}

func TestUpsertTemplate(t *testing.T) {
	orgID := int64(1)
	templateName := "template1"
	currentTemplateContent := "test1"
	amConfigToken := util.GenerateShortUID()
	revision := func() *legacy_storage.ConfigRevision {
		return &legacy_storage.ConfigRevision{
			Config: &definitions.PostableUserConfig{
				TemplateFiles: map[string]string{
					templateName: currentTemplateContent,
				},
			},
			ConcurrencyToken: amConfigToken,
		}
	}

	t.Run("adds new template to config file", func(t *testing.T) {
		sut, store, prov := createTemplateServiceSut()
		store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
			assert.Equal(t, orgID, org)
			return &legacy_storage.ConfigRevision{
				Config:           &definitions.PostableUserConfig{},
				ConcurrencyToken: amConfigToken,
			}, nil
		}
		store.SaveFn = func(ctx context.Context, revision *legacy_storage.ConfigRevision) error {
			assertInTransaction(t, ctx)
			return nil
		}
		prov.EXPECT().SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(ctx context.Context, o models.Provisionable, org int64, p models.Provenance) {
			assertInTransaction(t, ctx)
		}).Return(nil)

		tmpl := definitions.NotificationTemplate{
			Name:            "new-template",
			Template:        "{{ define \"test\"}} test {{ end }}",
			Provenance:      definitions.Provenance(models.ProvenanceAPI),
			ResourceVersion: "",
			Kind:            definition.GrafanaTemplateKind,
		}

		result, err := sut.UpsertTemplate(context.Background(), orgID, tmpl)

		require.NoError(t, err)
		require.Equal(t, newNotificationTemplate(
			tmpl.Name,
			tmpl.Template,
			models.Provenance(tmpl.Provenance),
			tmpl.Kind,
		), result)

		require.Len(t, store.Calls, 2)

		require.Equal(t, "Save", store.Calls[1].Method)
		saved := store.Calls[1].Args[1].(*legacy_storage.ConfigRevision)
		assert.Equal(t, amConfigToken, saved.ConcurrencyToken)
		assert.Contains(t, saved.Config.TemplateFiles, tmpl.Name)
		assert.Equal(t, tmpl.Template, saved.Config.TemplateFiles[tmpl.Name])

		prov.AssertCalled(t, "SetProvenance", mock.Anything, mock.MatchedBy(func(t *definitions.NotificationTemplate) bool {
			return t.Name == tmpl.Name
		}), orgID, models.ProvenanceAPI)
	})

	t.Run("updates current template", func(t *testing.T) {
		t.Run("when version matches", func(t *testing.T) {
			sut, store, prov := createTemplateServiceSut()
			store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
				return revision(), nil
			}
			prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil)
			prov.EXPECT().SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(ctx context.Context, o models.Provisionable, org int64, p models.Provenance) {
				assertInTransaction(t, ctx)
			}).Return(nil)

			tmpl := definitions.NotificationTemplate{
				Name:            templateName,
				Template:        "{{ define \"test\"}} test {{ end }}",
				Provenance:      definitions.Provenance(models.ProvenanceAPI),
				ResourceVersion: calculateTemplateFingerprint("test1"),
				Kind:            definition.GrafanaTemplateKind,
			}

			result, err := sut.UpsertTemplate(context.Background(), orgID, tmpl)

			require.NoError(t, err)
			assert.Equal(t, newNotificationTemplate(
				tmpl.Name,
				tmpl.Template,
				models.Provenance(tmpl.Provenance),
				tmpl.Kind,
			), result)

			require.Len(t, store.Calls, 2)
			require.Equal(t, "Save", store.Calls[1].Method)
			saved := store.Calls[1].Args[1].(*legacy_storage.ConfigRevision)
			assert.Equal(t, amConfigToken, saved.ConcurrencyToken)
			assert.Contains(t, saved.Config.TemplateFiles, tmpl.Name)
			assert.Equal(t, tmpl.Template, saved.Config.TemplateFiles[tmpl.Name])

			prov.AssertExpectations(t)
		})
		t.Run("bypasses optimistic concurrency validation when version is empty", func(t *testing.T) {
			sut, store, prov := createTemplateServiceSut()
			store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
				return revision(), nil
			}
			prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil)
			prov.EXPECT().SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(ctx context.Context, o models.Provisionable, org int64, p models.Provenance) {
				assertInTransaction(t, ctx)
			}).Return(nil)

			tmpl := definitions.NotificationTemplate{
				Name:            templateName,
				Template:        "{{ define \"test\"}} test {{ end }}",
				Provenance:      definitions.Provenance(models.ProvenanceAPI),
				ResourceVersion: "",
			}

			result, err := sut.UpsertTemplate(context.Background(), orgID, tmpl)

			require.NoError(t, err)
			assert.Equal(t, newNotificationTemplate(
				tmpl.Name,
				tmpl.Template,
				models.Provenance(tmpl.Provenance),
				definition.GrafanaTemplateKind,
			), result)

			require.Equal(t, "Save", store.Calls[1].Method)
			saved := store.Calls[1].Args[1].(*legacy_storage.ConfigRevision)
			assert.Equal(t, amConfigToken, saved.ConcurrencyToken)
			assert.Contains(t, saved.Config.TemplateFiles, tmpl.Name)
			assert.Equal(t, tmpl.Template, saved.Config.TemplateFiles[tmpl.Name])
		})
	})

	t.Run("normalizes template content with no define", func(t *testing.T) {
		sut, store, prov := createTemplateServiceSut()
		store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
			assert.Equal(t, orgID, org)
			return revision(), nil
		}
		prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil)
		prov.EXPECT().SaveSucceeds()

		tmpl := definitions.NotificationTemplate{
			Name:            templateName,
			Template:        "content",
			Provenance:      definitions.Provenance(models.ProvenanceNone),
			ResourceVersion: calculateTemplateFingerprint(currentTemplateContent),
			Kind:            definition.GrafanaTemplateKind,
		}

		result, _ := sut.UpsertTemplate(context.Background(), orgID, tmpl)

		expectedContent := fmt.Sprintf("{{ define \"%s\" }}\n  content\n{{ end }}", templateName)
		require.Equal(t, newNotificationTemplate(
			tmpl.Name,
			expectedContent,
			models.Provenance(tmpl.Provenance),
			tmpl.Kind,
		), result)
	})

	t.Run("does not reject template with unknown field", func(t *testing.T) {
		sut, store, prov := createTemplateServiceSut()
		store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
			assert.Equal(t, orgID, org)
			return revision(), nil
		}
		prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil)
		prov.EXPECT().SaveSucceeds()

		tmpl := definitions.NotificationTemplate{
			Name:     "name",
			Template: "{{ .NotAField }}",
		}
		_, err := sut.UpsertTemplate(context.Background(), 1, tmpl)

		require.NoError(t, err)
	})

	t.Run("rejects templates that fail validation", func(t *testing.T) {
		sut, store, prov := createTemplateServiceSut()

		t.Run("empty content", func(t *testing.T) {
			tmpl := definitions.NotificationTemplate{
				Name:     "",
				Template: "",
			}
			_, err := sut.UpsertTemplate(context.Background(), orgID, tmpl)
			require.ErrorIs(t, err, ErrTemplateInvalid)
		})

		t.Run("invalid content", func(t *testing.T) {
			tmpl := definitions.NotificationTemplate{
				Name:     "",
				Template: "{{ .MyField }",
			}
			_, err := sut.UpsertTemplate(context.Background(), orgID, tmpl)
			require.ErrorIs(t, err, ErrTemplateInvalid)
		})

		require.Empty(t, store.Calls)
		prov.AssertExpectations(t)
	})

	t.Run("rejects existing templates if provenance is not right", func(t *testing.T) {
		sut, store, prov := createTemplateServiceSut()
		store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
			return revision(), nil
		}

		prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil)

		expectedErr := errors.New("test")
		sut.validator = func(_ context.Context, from, to models.Provenance) error {
			assert.Equal(t, models.ProvenanceAPI, from)
			assert.Equal(t, models.ProvenanceNone, to)
			return expectedErr
		}

		template := definitions.NotificationTemplate{
			Name:     "template1",
			Template: "asdf-new",
		}
		template.Provenance = definitions.Provenance(models.ProvenanceNone)

		_, err := sut.UpsertTemplate(context.Background(), orgID, template)

		require.ErrorIs(t, err, expectedErr)
	})

	t.Run("rejects existing templates if version is not right", func(t *testing.T) {
		sut, store, prov := createTemplateServiceSut()
		store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
			return revision(), nil
		}
		prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil)

		template := definitions.NotificationTemplate{
			Name:            "template1",
			Template:        "asdf-new",
			ResourceVersion: "bad-version",
			Provenance:      definitions.Provenance(models.ProvenanceNone),
		}

		_, err := sut.UpsertTemplate(context.Background(), orgID, template)

		require.ErrorIs(t, err, ErrVersionConflict)
		prov.AssertExpectations(t)
	})

	t.Run("rejects new template if version is set", func(t *testing.T) {
		sut, store, _ := createTemplateServiceSut()
		store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
			return revision(), nil
		}
		template := definitions.NotificationTemplate{
			Name:            "template2",
			Template:        "asdf-new",
			ResourceVersion: "version",
			Provenance:      definitions.Provenance(models.ProvenanceNone),
		}
		_, err := sut.UpsertTemplate(context.Background(), orgID, template)
		require.ErrorIs(t, err, ErrTemplateNotFound)
	})

	t.Run("rejects new template has UID ", func(t *testing.T) {
		sut, store, _ := createTemplateServiceSut()
		store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
			return revision(), nil
		}
		template := definitions.NotificationTemplate{
			UID:        "new-template",
			Name:       "template2",
			Template:   "asdf-new",
			Provenance: definitions.Provenance(models.ProvenanceNone),
		}
		_, err := sut.UpsertTemplate(context.Background(), orgID, template)
		require.ErrorIs(t, err, ErrTemplateNotFound)
	})

	t.Run("rejects new templates of mimir kind", func(t *testing.T) {
		sut, store, _ := createTemplateServiceSut()
		store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
			return revision(), nil
		}
		template := definitions.NotificationTemplate{
			Name:       "template2",
			Template:   "asdf-new",
			Provenance: definitions.Provenance(models.ProvenanceNone),
			Kind:       definition.MimirTemplateKind,
		}
		_, err := sut.UpsertTemplate(context.Background(), orgID, template)
		require.ErrorIs(t, err, ErrTemplateInvalid)
	})

	t.Run("propagates errors", func(t *testing.T) {
		tmpl := definitions.NotificationTemplate{
			Name:       templateName,
			Template:   "content",
			Provenance: definitions.Provenance(models.ProvenanceNone),
		}
		t.Run("when unable to read config", func(t *testing.T) {
			sut, store, _ := createTemplateServiceSut()
			expectedErr := errors.New("test")
			store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
				return nil, expectedErr
			}

			_, err := sut.UpsertTemplate(context.Background(), orgID, tmpl)
			require.ErrorIs(t, err, expectedErr)
		})

		t.Run("when reading provenance status fails", func(t *testing.T) {
			sut, store, prov := createTemplateServiceSut()
			store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
				return revision(), nil
			}
			expectedErr := errors.New("test")
			prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, expectedErr)

			_, err := sut.UpsertTemplate(context.Background(), orgID, tmpl)

			require.ErrorIs(t, err, expectedErr)

			prov.AssertExpectations(t)
		})

		t.Run("when provenance fails to save", func(t *testing.T) {
			sut, store, prov := createTemplateServiceSut()
			expectedErr := errors.New("test")
			store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
				return revision(), nil
			}
			prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil)
			prov.EXPECT().SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(expectedErr)

			_, err := sut.UpsertTemplate(context.Background(), orgID, tmpl)
			require.ErrorIs(t, err, expectedErr)

			prov.AssertExpectations(t)
		})

		t.Run("when AM config fails to save", func(t *testing.T) {
			sut, store, prov := createTemplateServiceSut()
			store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
				return revision(), nil
			}
			expectedErr := errors.New("test")
			store.SaveFn = func(ctx context.Context, revision *legacy_storage.ConfigRevision) error {
				return expectedErr
			}
			prov.EXPECT().SaveSucceeds()
			prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil)

			_, err := sut.UpsertTemplate(context.Background(), 1, tmpl)
			require.ErrorIs(t, err, expectedErr)
		})
	})
}

func TestCreateTemplate(t *testing.T) {
	orgID := int64(1)
	amConfigToken := util.GenerateShortUID()

	tmpl := definitions.NotificationTemplate{
		Name:       "new-template",
		Template:   "{{ define \"test\"}} test {{ end }}",
		Provenance: definitions.Provenance(models.ProvenanceAPI),
		Kind:       definition.GrafanaTemplateKind,
	}

	revision := func() *legacy_storage.ConfigRevision {
		return &legacy_storage.ConfigRevision{
			Config:           &definitions.PostableUserConfig{},
			ConcurrencyToken: amConfigToken,
		}
	}

	t.Run("adds new template to config file", func(t *testing.T) {
		sut, store, prov := createTemplateServiceSut()
		store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
			assert.Equal(t, orgID, org)
			return revision(), nil
		}
		store.SaveFn = func(ctx context.Context, revision *legacy_storage.ConfigRevision) error {
			assertInTransaction(t, ctx)
			return nil
		}
		prov.EXPECT().SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(ctx context.Context, o models.Provisionable, org int64, p models.Provenance) {
			assertInTransaction(t, ctx)
		}).Return(nil)

		result, err := sut.CreateTemplate(context.Background(), orgID, tmpl)

		require.NoError(t, err)
		require.Equal(t, newNotificationTemplate(
			tmpl.Name,
			tmpl.Template,
			models.Provenance(tmpl.Provenance),
			tmpl.Kind,
		), result)

		require.Len(t, store.Calls, 2)

		require.Equal(t, "Save", store.Calls[1].Method)
		saved := store.Calls[1].Args[1].(*legacy_storage.ConfigRevision)
		assert.Equal(t, amConfigToken, saved.ConcurrencyToken)
		assert.Contains(t, saved.Config.TemplateFiles, tmpl.Name)
		assert.Equal(t, tmpl.Template, saved.Config.TemplateFiles[tmpl.Name])

		prov.AssertCalled(t, "SetProvenance", mock.Anything, mock.MatchedBy(func(t *definitions.NotificationTemplate) bool {
			return t.Name == tmpl.Name
		}), orgID, models.ProvenanceAPI)
	})

	t.Run("returns ErrTemplateExists if template exists", func(t *testing.T) {
		sut, store, _ := createTemplateServiceSut()
		store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
			assert.Equal(t, orgID, org)
			return &legacy_storage.ConfigRevision{
				Config: &definitions.PostableUserConfig{
					TemplateFiles: map[string]string{
						tmpl.Name: "test",
					},
				},
				ConcurrencyToken: amConfigToken,
			}, nil
		}

		_, err := sut.CreateTemplate(context.Background(), orgID, tmpl)

		require.ErrorIs(t, err, ErrTemplateExists)
	})

	t.Run("rejects templates that fail validation", func(t *testing.T) {
		sut, store, prov := createTemplateServiceSut()

		t.Run("empty content", func(t *testing.T) {
			tmpl := definitions.NotificationTemplate{
				Name:     "",
				Template: "",
			}
			_, err := sut.CreateTemplate(context.Background(), orgID, tmpl)
			require.ErrorIs(t, err, ErrTemplateInvalid)
		})

		t.Run("invalid content", func(t *testing.T) {
			tmpl := definitions.NotificationTemplate{
				Name:     "",
				Template: "{{ .MyField }",
			}
			_, err := sut.CreateTemplate(context.Background(), orgID, tmpl)
			require.ErrorIs(t, err, ErrTemplateInvalid)
		})

		t.Run("invalid kind", func(t *testing.T) {
			tmpl := definitions.NotificationTemplate{
				Name:     "new-template",
				Template: "{{ define \"test\"}} test {{ end }}",
				Kind:     "unknown",
			}
			_, err := sut.CreateTemplate(context.Background(), orgID, tmpl)
			require.ErrorIs(t, err, ErrTemplateInvalid)
		})

		require.Empty(t, store.Calls)
		prov.AssertExpectations(t)
	})

	t.Run("rejects templates with mimir kind", func(t *testing.T) {
		sut, _, _ := createTemplateServiceSut()

		tmpl := definitions.NotificationTemplate{
			Name:     "new-template",
			Template: "{{ define \"test\"}} test {{ end }}",
			Kind:     definition.MimirTemplateKind,
		}

		_, err := sut.CreateTemplate(context.Background(), orgID, tmpl)
		require.ErrorIs(t, err, ErrTemplateInvalid)
	})

	t.Run("propagates errors", func(t *testing.T) {
		t.Run("when unable to read config", func(t *testing.T) {
			sut, store, _ := createTemplateServiceSut()
			expectedErr := errors.New("test")
			store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
				return nil, expectedErr
			}

			_, err := sut.CreateTemplate(context.Background(), orgID, tmpl)
			require.ErrorIs(t, err, expectedErr)
		})

		t.Run("when provenance fails to save", func(t *testing.T) {
			sut, store, prov := createTemplateServiceSut()
			expectedErr := errors.New("test")
			store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
				return revision(), nil
			}
			prov.EXPECT().SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(expectedErr)

			_, err := sut.CreateTemplate(context.Background(), orgID, tmpl)
			require.ErrorIs(t, err, expectedErr)

			prov.AssertExpectations(t)
		})

		t.Run("when AM config fails to save", func(t *testing.T) {
			sut, store, prov := createTemplateServiceSut()
			store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
				return revision(), nil
			}
			expectedErr := errors.New("test")
			store.SaveFn = func(ctx context.Context, revision *legacy_storage.ConfigRevision) error {
				return expectedErr
			}
			prov.EXPECT().SaveSucceeds()
			prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil)

			_, err := sut.CreateTemplate(context.Background(), 1, tmpl)
			require.ErrorIs(t, err, expectedErr)
		})
	})
}

func TestUpdateTemplate(t *testing.T) {
	orgID := int64(1)
	currentTemplateContent := "test1"

	tmpl := definitions.NotificationTemplate{
		Name:            "template1",
		Template:        "{{ define \"test\"}} test {{ end }}",
		Provenance:      definitions.Provenance(models.ProvenanceAPI),
		ResourceVersion: "",
		Kind:            definition.GrafanaTemplateKind,
	}

	amConfigToken := util.GenerateShortUID()
	revision := func() *legacy_storage.ConfigRevision {
		return &legacy_storage.ConfigRevision{
			Config: &definitions.PostableUserConfig{
				TemplateFiles: map[string]string{
					tmpl.Name: currentTemplateContent,
				},
			},
			ConcurrencyToken: amConfigToken,
		}
	}

	t.Run("returns ErrTemplateNotFound if template name does not exist", func(t *testing.T) {
		sut, store, prov := createTemplateServiceSut()
		store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
			assert.Equal(t, orgID, org)
			return &legacy_storage.ConfigRevision{
				Config:           &definitions.PostableUserConfig{},
				ConcurrencyToken: amConfigToken,
			}, nil
		}
		_, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)

		require.ErrorIs(t, err, ErrTemplateNotFound)

		require.Len(t, store.Calls, 1)
		prov.AssertExpectations(t)
	})

	t.Run("returns ErrTemplateNotFound if template UID does not exist", func(t *testing.T) {
		sut, store, prov := createTemplateServiceSut()
		store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
			assert.Equal(t, orgID, org)
			return &legacy_storage.ConfigRevision{
				Config: &definitions.PostableUserConfig{
					TemplateFiles: map[string]string{
						"not-found": "test", // create a template with name that matches UID to make sure we do not search by name
						tmpl.Name:   "test",
					},
				},
				ConcurrencyToken: amConfigToken,
			}, nil
		}
		tmpl := tmpl
		tmpl.UID = "not-found"
		_, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)

		require.ErrorIs(t, err, ErrTemplateNotFound)

		require.Len(t, store.Calls, 1)
		prov.AssertExpectations(t)
	})

	testcases := []struct {
		name        string
		templateUid string
	}{
		{
			name:        "by name",
			templateUid: "",
		},
		{
			name:        "by uid",
			templateUid: templateUID(tmpl.Kind, tmpl.Name),
		},
	}

	for _, tt := range testcases {
		t.Run(fmt.Sprintf("updates current template %s", tt.name), func(t *testing.T) {
			t.Run("when version matches", func(t *testing.T) {
				sut, store, prov := createTemplateServiceSut()
				store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
					return revision(), nil
				}
				prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil)
				prov.EXPECT().SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(ctx context.Context, o models.Provisionable, org int64, p models.Provenance) {
					assertInTransaction(t, ctx)
				}).Return(nil)

				tmpl.UID = tt.templateUid
				result, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)

				require.NoError(t, err)
				assert.Equal(t, newNotificationTemplate(
					tmpl.Name,
					tmpl.Template,
					models.Provenance(tmpl.Provenance),
					tmpl.Kind,
				), result)

				require.Len(t, store.Calls, 2)
				require.Equal(t, "Save", store.Calls[1].Method)
				saved := store.Calls[1].Args[1].(*legacy_storage.ConfigRevision)
				assert.Equal(t, amConfigToken, saved.ConcurrencyToken)
				assert.Contains(t, saved.Config.TemplateFiles, tmpl.Name)
				assert.Equal(t, tmpl.Template, saved.Config.TemplateFiles[tmpl.Name])

				prov.AssertExpectations(t)
			})
			t.Run("bypasses optimistic concurrency validation when version is empty", func(t *testing.T) {
				sut, store, prov := createTemplateServiceSut()
				store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
					return revision(), nil
				}
				prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil)
				prov.EXPECT().SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(ctx context.Context, o models.Provisionable, org int64, p models.Provenance) {
					assertInTransaction(t, ctx)
				}).Return(nil)

				result, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)

				require.NoError(t, err)
				assert.Equal(t, newNotificationTemplate(
					tmpl.Name,
					tmpl.Template,
					models.Provenance(tmpl.Provenance),
					tmpl.Kind,
				), result)

				require.Equal(t, "Save", store.Calls[1].Method)
				saved := store.Calls[1].Args[1].(*legacy_storage.ConfigRevision)
				assert.Equal(t, amConfigToken, saved.ConcurrencyToken)
				assert.Contains(t, saved.Config.TemplateFiles, tmpl.Name)
				assert.Equal(t, tmpl.Template, saved.Config.TemplateFiles[tmpl.Name])
			})
		})
	}

	t.Run("creates a new template and delete old one when template is renamed", func(t *testing.T) {
		sut, store, prov := createTemplateServiceSut()
		store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
			return revision(), nil
		}
		prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil)
		prov.EXPECT().DeleteProvenance(mock.Anything, mock.Anything, mock.Anything).Return(nil).Run(func(ctx context.Context, o models.Provisionable, org int64) {
			assertInTransaction(t, ctx)
		}).Return(nil)
		prov.EXPECT().SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(ctx context.Context, o models.Provisionable, org int64, p models.Provenance) {
			assertInTransaction(t, ctx)
		}).Return(nil)

		oldName := tmpl.Name
		tmpl := tmpl
		tmpl.UID = templateUID(tmpl.Kind, tmpl.Name) // UID matches the current template
		tmpl.Name = "new-template-name"              // but name is different
		result, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)

		require.NoError(t, err)
		assert.Equal(t, newNotificationTemplate(
			tmpl.Name,
			tmpl.Template,
			models.Provenance(tmpl.Provenance),
			tmpl.Kind,
		), result)

		require.Len(t, store.Calls, 2)
		require.Equal(t, "Save", store.Calls[1].Method)
		saved := store.Calls[1].Args[1].(*legacy_storage.ConfigRevision)
		assert.Equal(t, amConfigToken, saved.ConcurrencyToken)
		assert.Contains(t, saved.Config.TemplateFiles, tmpl.Name)
		assert.Equal(t, tmpl.Template, saved.Config.TemplateFiles[tmpl.Name])
		assert.NotContains(t, saved.Config.TemplateFiles, oldName)

		prov.AssertCalled(t, "DeleteProvenance", mock.Anything, mock.MatchedBy(func(t *definitions.NotificationTemplate) bool {
			return t.Name == oldName
		}), mock.Anything)
		prov.AssertExpectations(t)
	})

	t.Run("rejects rename operation if template with the new name exists", func(t *testing.T) {
		sut, store, prov := createTemplateServiceSut()
		prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil)
		store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
			return &legacy_storage.ConfigRevision{
				Config: &definitions.PostableUserConfig{
					TemplateFiles: map[string]string{
						tmpl.Name:           currentTemplateContent,
						"new-template-name": "test",
					},
				},
				ConcurrencyToken: amConfigToken,
			}, nil
		}

		tmpl := tmpl
		tmpl.UID = templateUID(tmpl.Kind, tmpl.Name) // UID matches the current template
		tmpl.Name = "new-template-name"              // but name matches another existing template
		_, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)

		require.ErrorIs(t, err, ErrTemplateExists)

		prov.AssertExpectations(t)
	})

	t.Run("rejects templates that fail validation", func(t *testing.T) {
		sut, store, prov := createTemplateServiceSut()

		t.Run("empty content", func(t *testing.T) {
			tmpl := definitions.NotificationTemplate{
				Name:     "",
				Template: "",
			}
			_, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)
			require.ErrorIs(t, err, ErrTemplateInvalid)
		})

		t.Run("invalid content", func(t *testing.T) {
			tmpl := definitions.NotificationTemplate{
				Name:     "",
				Template: "{{ .MyField }",
			}
			_, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)
			require.ErrorIs(t, err, ErrTemplateInvalid)
		})

		t.Run("invalid kind", func(t *testing.T) {
			tmpl := definitions.NotificationTemplate{
				Name:     "",
				Template: "",
				Kind:     "unknown",
			}
			_, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)
			require.ErrorIs(t, err, ErrTemplateInvalid)
		})

		require.Empty(t, store.Calls)
		prov.AssertExpectations(t)
	})

	t.Run("rejects existing templates if provenance is not right", func(t *testing.T) {
		sut, store, prov := createTemplateServiceSut()
		store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
			return revision(), nil
		}

		prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil)

		expectedErr := errors.New("test")
		sut.validator = func(_ context.Context, from, to models.Provenance) error {
			assert.Equal(t, models.ProvenanceAPI, from)
			assert.Equal(t, models.ProvenanceNone, to)
			return expectedErr
		}

		template := definitions.NotificationTemplate{
			Name:     "template1",
			Template: "asdf-new",
		}
		template.Provenance = definitions.Provenance(models.ProvenanceNone)

		_, err := sut.UpdateTemplate(context.Background(), orgID, template)

		require.ErrorIs(t, err, expectedErr)
	})

	t.Run("rejects existing templates if version is not right", func(t *testing.T) {
		sut, store, prov := createTemplateServiceSut()
		store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
			return revision(), nil
		}
		prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil)

		template := definitions.NotificationTemplate{
			Name:            "template1",
			Template:        "asdf-new",
			ResourceVersion: "bad-version",
			Provenance:      definitions.Provenance(models.ProvenanceNone),
		}

		_, err := sut.UpdateTemplate(context.Background(), orgID, template)

		require.ErrorIs(t, err, ErrVersionConflict)
		prov.AssertExpectations(t)
	})

	t.Run("rejects existing templates if kind changes", func(t *testing.T) {
		sut, store, prov := createTemplateServiceSut()
		store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
			return revision(), nil
		}
		prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil)

		template := definitions.NotificationTemplate{
			Name:            "template1",
			Template:        "asdf-new",
			ResourceVersion: "bad-version",
			Provenance:      definitions.Provenance(models.ProvenanceNone),
			Kind:            definition.MimirTemplateKind,
		}

		_, err := sut.UpdateTemplate(context.Background(), orgID, template)

		require.ErrorIs(t, err, ErrTemplateInvalid)
		prov.AssertExpectations(t)
	})

	t.Run("propagates errors", func(t *testing.T) {
		t.Run("when unable to read config", func(t *testing.T) {
			sut, store, _ := createTemplateServiceSut()
			expectedErr := errors.New("test")
			store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
				return nil, expectedErr
			}

			_, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)
			require.ErrorIs(t, err, expectedErr)
		})

		t.Run("when reading provenance status fails", func(t *testing.T) {
			sut, store, prov := createTemplateServiceSut()
			store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
				return revision(), nil
			}
			expectedErr := errors.New("test")
			prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, expectedErr)

			_, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)

			require.ErrorIs(t, err, expectedErr)

			prov.AssertExpectations(t)
		})

		t.Run("when provenance fails to save", func(t *testing.T) {
			sut, store, prov := createTemplateServiceSut()
			expectedErr := errors.New("test")
			store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
				return revision(), nil
			}
			prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil)
			prov.EXPECT().SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(expectedErr)

			_, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)
			require.ErrorIs(t, err, expectedErr)

			prov.AssertExpectations(t)
		})

		t.Run("when AM config fails to save", func(t *testing.T) {
			sut, store, prov := createTemplateServiceSut()
			store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
				return revision(), nil
			}
			expectedErr := errors.New("test")
			store.SaveFn = func(ctx context.Context, revision *legacy_storage.ConfigRevision) error {
				return expectedErr
			}
			prov.EXPECT().SaveSucceeds()
			prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil)

			_, err := sut.UpdateTemplate(context.Background(), 1, tmpl)
			require.ErrorIs(t, err, expectedErr)
		})
	})
}

func TestDeleteTemplate(t *testing.T) {
	orgID := int64(1)
	templateName := "template1"
	templateContent := "test-1"
	templateVersion := calculateTemplateFingerprint(templateContent)
	amConfigToken := util.GenerateShortUID()
	revision := func() *legacy_storage.ConfigRevision {
		return &legacy_storage.ConfigRevision{
			Config: &definitions.PostableUserConfig{
				TemplateFiles: map[string]string{
					templateName: templateContent,
				},
			},
			ConcurrencyToken: amConfigToken,
		}
	}

	testCase := []struct {
		name              string
		templateNameOrUid string
	}{
		{
			name:              "by name",
			templateNameOrUid: templateName,
		},
		{
			name:              "by uid",
			templateNameOrUid: templateUID(definition.GrafanaTemplateKind, templateName),
		},
	}
	for _, tt := range testCase {
		t.Run(fmt.Sprintf("deletes template from config file %s", tt.name), func(t *testing.T) {
			t.Run("when version matches", func(t *testing.T) {
				sut, store, prov := createTemplateServiceSut()
				store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
					return revision(), nil
				}
				prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceFile, nil)
				prov.EXPECT().DeleteProvenance(mock.Anything, mock.Anything, mock.Anything).Run(func(ctx context.Context, o models.Provisionable, org int64) {
					assertInTransaction(t, ctx)
				}).Return(nil)

				err := sut.DeleteTemplate(context.Background(), orgID, tt.templateNameOrUid, definitions.Provenance(models.ProvenanceFile), templateVersion)

				require.NoError(t, err)

				require.Len(t, store.Calls, 2)

				require.Equal(t, "Save", store.Calls[1].Method)
				saved := store.Calls[1].Args[1].(*legacy_storage.ConfigRevision)
				assert.Equal(t, amConfigToken, saved.ConcurrencyToken)
				assert.NotContains(t, saved.Config.TemplateFiles, templateName)

				prov.AssertCalled(t, "DeleteProvenance", mock.Anything, mock.MatchedBy(func(t *definitions.NotificationTemplate) bool {
					return t.Name == templateName
				}), orgID)

				prov.AssertExpectations(t)
			})

			t.Run("bypasses optimistic concurrency when version is empty", func(t *testing.T) {
				sut, store, prov := createTemplateServiceSut()
				store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
					return revision(), nil
				}
				prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceFile, nil)
				prov.EXPECT().DeleteProvenance(mock.Anything, mock.Anything, mock.Anything).Run(func(ctx context.Context, o models.Provisionable, org int64) {
					assertInTransaction(t, ctx)
				}).Return(nil)

				err := sut.DeleteTemplate(context.Background(), orgID, tt.templateNameOrUid, definitions.Provenance(models.ProvenanceFile), "")

				require.NoError(t, err)
				require.Len(t, store.Calls, 2)

				require.Equal(t, "Save", store.Calls[1].Method)
				saved := store.Calls[1].Args[1].(*legacy_storage.ConfigRevision)
				assert.Equal(t, amConfigToken, saved.ConcurrencyToken)
				assert.NotContains(t, saved.Config.TemplateFiles, templateName)

				prov.AssertCalled(t, "DeleteProvenance", mock.Anything, mock.MatchedBy(func(t *definitions.NotificationTemplate) bool {
					return t.Name == templateName
				}), orgID)

				prov.AssertExpectations(t)
			})
		})
	}

	t.Run("should look by name before uid", func(t *testing.T) {
		expectedToDelete := templateUID(definition.GrafanaTemplateKind, templateName)
		sut, store, prov := createTemplateServiceSut()
		store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
			return &legacy_storage.ConfigRevision{
				Config: &definitions.PostableUserConfig{
					TemplateFiles: map[string]string{
						templateName:     templateContent,
						expectedToDelete: templateContent,
					},
				},
				ConcurrencyToken: amConfigToken,
			}, nil
		}
		prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceFile, nil)
		prov.EXPECT().DeleteProvenance(mock.Anything, mock.Anything, mock.Anything).Run(func(ctx context.Context, o models.Provisionable, org int64) {
			assertInTransaction(t, ctx)
		}).Return(nil)

		err := sut.DeleteTemplate(context.Background(), orgID, expectedToDelete, definitions.Provenance(models.ProvenanceFile), templateVersion)

		require.NoError(t, err)

		require.Len(t, store.Calls, 2)

		require.Equal(t, "Save", store.Calls[1].Method)
		saved := store.Calls[1].Args[1].(*legacy_storage.ConfigRevision)
		assert.Equal(t, amConfigToken, saved.ConcurrencyToken)
		assert.NotContains(t, saved.Config.TemplateFiles, expectedToDelete)
		assert.Contains(t, saved.Config.TemplateFiles, templateName)

		prov.AssertCalled(t, "DeleteProvenance", mock.Anything, mock.MatchedBy(func(t *definitions.NotificationTemplate) bool {
			return t.Name == expectedToDelete
		}), orgID)

		prov.AssertExpectations(t)
	})

	t.Run("does not error when deleting templates that do not exist", func(t *testing.T) {
		sut, store, prov := createTemplateServiceSut()
		store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
			return revision(), nil
		}

		err := sut.DeleteTemplate(context.Background(), orgID, "not-found", definitions.Provenance(models.ProvenanceNone), "")

		require.NoError(t, err)

		prov.AssertExpectations(t)
	})

	t.Run("errors if provenance is not right", func(t *testing.T) {
		sut, store, prov := createTemplateServiceSut()
		store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
			return revision(), nil
		}
		prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil)

		expectedErr := errors.New("test")
		sut.validator = func(_ context.Context, from, to models.Provenance) error {
			assert.Equal(t, models.ProvenanceAPI, from)
			assert.Equal(t, models.ProvenanceNone, to)
			return expectedErr
		}

		err := sut.DeleteTemplate(context.Background(), 1, templateName, definitions.Provenance(models.ProvenanceNone), "")

		require.ErrorIs(t, err, expectedErr)

		prov.AssertExpectations(t)
	})

	t.Run("errors if version is not right", func(t *testing.T) {
		sut, store, prov := createTemplateServiceSut()
		store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
			return revision(), nil
		}
		prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil)

		err := sut.DeleteTemplate(context.Background(), 1, templateName, definitions.Provenance(models.ProvenanceNone), "bad-version")

		require.ErrorIs(t, err, ErrVersionConflict)
	})

	t.Run("propagates errors", func(t *testing.T) {
		t.Run("when unable to read config", func(t *testing.T) {
			sut, store, prov := createTemplateServiceSut()
			expectedErr := errors.New("test")
			store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
				return nil, expectedErr
			}

			err := sut.DeleteTemplate(context.Background(), orgID, templateName, definitions.Provenance(models.ProvenanceNone), templateVersion)

			require.ErrorIs(t, err, expectedErr)

			prov.AssertExpectations(t)
		})

		t.Run("when reading provenance status fails", func(t *testing.T) {
			sut, store, prov := createTemplateServiceSut()
			store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
				return revision(), nil
			}
			expectedErr := errors.New("test")
			prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, expectedErr)

			err := sut.DeleteTemplate(context.Background(), orgID, templateName, definitions.Provenance(models.ProvenanceNone), templateVersion)

			require.ErrorIs(t, err, expectedErr)

			prov.AssertExpectations(t)
		})

		t.Run("when provenance fails to save", func(t *testing.T) {
			sut, store, prov := createTemplateServiceSut()
			expectedErr := errors.New("test")
			store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
				return revision(), nil
			}
			prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil)
			prov.EXPECT().DeleteProvenance(mock.Anything, mock.Anything, mock.Anything).Return(expectedErr)

			err := sut.DeleteTemplate(context.Background(), orgID, templateName, definitions.Provenance(models.ProvenanceNone), templateVersion)

			require.ErrorIs(t, err, expectedErr)

			prov.AssertExpectations(t)
		})

		t.Run("when AM config fails to save", func(t *testing.T) {
			sut, store, prov := createTemplateServiceSut()
			store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
				return revision(), nil
			}
			expectedErr := errors.New("test")
			store.SaveFn = func(ctx context.Context, revision *legacy_storage.ConfigRevision) error {
				return expectedErr
			}
			prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil)

			err := sut.DeleteTemplate(context.Background(), orgID, templateName, definitions.Provenance(models.ProvenanceNone), templateVersion)

			require.ErrorIs(t, err, expectedErr)
		})
	})
}

func createTemplateServiceSut() (*TemplateService, *legacy_storage.AlertmanagerConfigStoreFake, *MockProvisioningStore) {
	store := &legacy_storage.AlertmanagerConfigStoreFake{}
	provStore := &MockProvisioningStore{}
	return &TemplateService{
		configStore:     store,
		provenanceStore: provStore,
		xact:            newNopTransactionManager(),
		log:             log.NewNopLogger(),
		validator:       validation.ValidateProvenanceRelaxed,
		limitsProvider:  &NoopLimitsProvider{},
	}, store, provStore
}

func TestTemplateService_LimitsValidation(t *testing.T) {
	orgID := int64(1)
	amConfigToken := util.GenerateShortUID()

	tmpl := definitions.NotificationTemplate{
		Name:       "new-template",
		Template:   "{{ define \"test\"}} test {{ end }}",
		Provenance: definitions.Provenance(models.ProvenanceAPI),
		Kind:       definition.GrafanaTemplateKind,
	}

	revision := func(existingCount int) *legacy_storage.ConfigRevision {
		templates := make(map[string]string, existingCount)
		for i := 0; i < existingCount; i++ {
			templates[fmt.Sprintf("existing-%d", i)] = "content"
		}
		return &legacy_storage.ConfigRevision{
			Config: &definitions.PostableUserConfig{
				TemplateFiles: templates,
			},
			ConcurrencyToken: amConfigToken,
		}
	}

	t.Run("CreateTemplate fails when template count limit exceeded", func(t *testing.T) {
		sut, store, _ := createTemplateServiceSut()
		sut.limitsProvider = &mockLimitsProvider{
			limits: &client.TenantLimits{
				Templates: &client.TemplateLimits{
					MaxTemplatesCount:    5,
					MaxTemplateSizeBytes: 0, // unlimited
				},
			},
		}
		store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
			return revision(5), nil // Already at the limit
		}

		_, err := sut.CreateTemplate(context.Background(), orgID, tmpl)

		require.ErrorIs(t, err, ErrTemplateLimitExceeded)
	})

	t.Run("CreateTemplate fails when template size limit exceeded", func(t *testing.T) {
		sut, store, _ := createTemplateServiceSut()
		sut.limitsProvider = &mockLimitsProvider{
			limits: &client.TenantLimits{
				Templates: &client.TemplateLimits{
					MaxTemplatesCount:    0, // unlimited
					MaxTemplateSizeBytes: 10,
				},
			},
		}
		store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
			return revision(0), nil
		}

		largeTmpl := tmpl
		largeTmpl.Template = "{{ define \"test\"}} this is a very long template content {{ end }}"

		_, err := sut.CreateTemplate(context.Background(), orgID, largeTmpl)

		require.ErrorIs(t, err, ErrTemplateSizeExceeded)
	})

	t.Run("CreateTemplate succeeds when under limits", func(t *testing.T) {
		sut, store, prov := createTemplateServiceSut()
		sut.limitsProvider = &mockLimitsProvider{
			limits: &client.TenantLimits{
				Templates: &client.TemplateLimits{
					MaxTemplatesCount:    10,
					MaxTemplateSizeBytes: 1000,
				},
			},
		}
		store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
			return revision(5), nil
		}
		prov.EXPECT().SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)

		_, err := sut.CreateTemplate(context.Background(), orgID, tmpl)

		require.NoError(t, err)
	})

	t.Run("CreateTemplate succeeds when limits are nil", func(t *testing.T) {
		sut, store, prov := createTemplateServiceSut()
		sut.limitsProvider = &mockLimitsProvider{
			limits: nil,
		}
		store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
			return revision(100), nil
		}
		prov.EXPECT().SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)

		_, err := sut.CreateTemplate(context.Background(), orgID, tmpl)

		require.NoError(t, err)
	})

	t.Run("CreateTemplate succeeds when limits are zero (unlimited)", func(t *testing.T) {
		sut, store, prov := createTemplateServiceSut()
		sut.limitsProvider = &mockLimitsProvider{
			limits: &client.TenantLimits{
				Templates: &client.TemplateLimits{
					MaxTemplatesCount:    0, // unlimited
					MaxTemplateSizeBytes: 0, // unlimited
				},
			},
		}
		store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
			return revision(100), nil
		}
		prov.EXPECT().SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)

		_, err := sut.CreateTemplate(context.Background(), orgID, tmpl)

		require.NoError(t, err)
	})

	t.Run("CreateTemplate succeeds when limits provider returns error (fail open)", func(t *testing.T) {
		sut, store, prov := createTemplateServiceSut()
		sut.limitsProvider = &mockLimitsProvider{
			err: errors.New("failed to fetch limits"),
		}
		store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
			return revision(100), nil
		}
		prov.EXPECT().SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)

		_, err := sut.CreateTemplate(context.Background(), orgID, tmpl)

		require.NoError(t, err)
	})

	t.Run("UpdateTemplate fails when template size limit exceeded", func(t *testing.T) {
		sut, store, prov := createTemplateServiceSut()
		existingTemplateName := "existing-template"
		sut.limitsProvider = &mockLimitsProvider{
			limits: &client.TenantLimits{
				Templates: &client.TemplateLimits{
					MaxTemplatesCount:    100, // Should not matter for updates
					MaxTemplateSizeBytes: 10,
				},
			},
		}
		store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
			return &legacy_storage.ConfigRevision{
				Config: &definitions.PostableUserConfig{
					TemplateFiles: map[string]string{
						existingTemplateName: "short",
					},
				},
				ConcurrencyToken: amConfigToken,
			}, nil
		}
		prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil)

		largeTmpl := definitions.NotificationTemplate{
			Name:     existingTemplateName,
			Template: "{{ define \"test\"}} this is a very long template content that exceeds the limit {{ end }}",
		}

		_, err := sut.UpdateTemplate(context.Background(), orgID, largeTmpl)

		require.ErrorIs(t, err, ErrTemplateSizeExceeded)
	})

	t.Run("UpdateTemplate does not check count limit", func(t *testing.T) {
		sut, store, prov := createTemplateServiceSut()
		existingTemplateName := "existing-template"
		sut.limitsProvider = &mockLimitsProvider{
			limits: &client.TenantLimits{
				Templates: &client.TemplateLimits{
					MaxTemplatesCount:    1, // Way under current count
					MaxTemplateSizeBytes: 10000,
				},
			},
		}
		// Create a revision with many templates (over the count limit)
		templates := make(map[string]string, 100)
		templates[existingTemplateName] = "short"
		for i := 0; i < 99; i++ {
			templates[fmt.Sprintf("template-%d", i)] = "content"
		}
		store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
			return &legacy_storage.ConfigRevision{
				Config: &definitions.PostableUserConfig{
					TemplateFiles: templates,
				},
				ConcurrencyToken: amConfigToken,
			}, nil
		}
		prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil)
		prov.EXPECT().SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)

		updateTmpl := definitions.NotificationTemplate{
			Name:     existingTemplateName,
			Template: "{{ define \"test\"}} updated content {{ end }}",
		}

		// Update should succeed because count limit doesn't apply to updates
		_, err := sut.UpdateTemplate(context.Background(), orgID, updateTmpl)

		require.NoError(t, err)
	})
}

// mockLimitsProvider is a test implementation of LimitsProvider
type mockLimitsProvider struct {
	limits *client.TenantLimits
	err    error
}

func (m *mockLimitsProvider) GetLimits(_ context.Context) (*client.TenantLimits, error) {
	return m.limits, m.err
}
