package provisioning

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"math/rand"
	"slices"
	"strconv"
	"strings"
	"testing"
	"time"

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

	"github.com/grafana/grafana/pkg/apimachinery/identity"
	"github.com/grafana/grafana/pkg/bus"
	"github.com/grafana/grafana/pkg/expr"
	"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/featuremgmt"
	"github.com/grafana/grafana/pkg/services/folder"
	"github.com/grafana/grafana/pkg/services/folder/foldertest"
	"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
	"github.com/grafana/grafana/pkg/services/ngalert/models"
	"github.com/grafana/grafana/pkg/services/ngalert/store"
	"github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
	"github.com/grafana/grafana/pkg/services/user"
	"github.com/grafana/grafana/pkg/setting"
	"github.com/grafana/grafana/pkg/util"
	"github.com/grafana/grafana/pkg/util/testutil"
)

// note: additional integration tests are in /pkg/tests/api/alerting/api_provisioning_test.go

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

	ruleService := createAlertRuleService(t, nil)
	var orgID int64 = 1
	u := &user.SignedInUser{
		UserUID: util.GenerateShortUID(),
		UserID:  1,
		OrgID:   orgID,
	}

	t.Run("group creation should set the right provenance", func(t *testing.T) {
		group := createDummyGroup("group-test-1", orgID)
		err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
		require.NoError(t, err)

		readGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "group-test-1")
		require.NoError(t, err)
		require.NotEmpty(t, readGroup.Rules)
		for _, rule := range readGroup.Rules {
			_, provenance, err := ruleService.GetAlertRule(context.Background(), u, rule.UID)
			require.NoError(t, err)
			require.Equal(t, models.ProvenanceAPI, provenance)
		}
	})

	t.Run("alert rule group should be updated correctly", func(t *testing.T) {
		rule := dummyRule("test#3", orgID)
		rule.RuleGroup = "a"
		rule, err := ruleService.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
		require.NoError(t, err)
		require.Equal(t, int64(60), rule.IntervalSeconds)

		var interval int64 = 120
		err = ruleService.UpdateRuleGroup(context.Background(), u, rule.NamespaceUID, rule.RuleGroup, 120)
		require.NoError(t, err)

		rule, _, err = ruleService.GetAlertRule(context.Background(), u, rule.UID)
		require.NoError(t, err)
		require.Equal(t, interval, rule.IntervalSeconds)
	})

	t.Run("if a folder was renamed the interval should be fetched from the renamed folder", func(t *testing.T) {
		var orgID int64 = 2
		rule := dummyRule("test#1", orgID)
		rule.NamespaceUID = "123abc"
		u := &user.SignedInUser{OrgID: orgID}
		rule, err := ruleService.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
		require.NoError(t, err)

		rule.NamespaceUID = "abc123"
		_, err = ruleService.UpdateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
		require.NoError(t, err)
	})

	t.Run("group update should propagate folderUID from group to rules", func(t *testing.T) {
		ruleService := createAlertRuleService(t, nil)
		group := createDummyGroup("namespace-test", orgID)
		group.Rules[0].NamespaceUID = ""

		err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
		require.NoError(t, err)

		readGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "namespace-test")
		require.NoError(t, err)
		require.NotEmpty(t, readGroup.Rules)
		require.Equal(t, "my-namespace", readGroup.Rules[0].NamespaceUID)
	})

	t.Run("group creation should propagate group title correctly", func(t *testing.T) {
		group := createDummyGroup("group-test-3", orgID)
		group.Rules[0].RuleGroup = "something different"

		err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
		require.NoError(t, err)

		readGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "group-test-3")
		require.NoError(t, err)
		require.NotEmpty(t, readGroup.Rules)
		for _, rule := range readGroup.Rules {
			require.Equal(t, "group-test-3", rule.RuleGroup)
		}
	})

	t.Run("alert rule should get interval from existing rule group", func(t *testing.T) {
		rule := dummyRule("test#4", orgID)
		rule.RuleGroup = "b"
		rule, err := ruleService.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
		require.NoError(t, err)

		var interval int64 = 120
		err = ruleService.UpdateRuleGroup(context.Background(), u, rule.NamespaceUID, rule.RuleGroup, 120)
		require.NoError(t, err)

		rule = dummyRule("test#4-1", orgID)
		rule.RuleGroup = "b"
		rule, err = ruleService.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
		require.NoError(t, err)
		require.Equal(t, interval, rule.IntervalSeconds)
	})

	t.Run("updating a rule group's top level fields should bump the version number", func(t *testing.T) {
		const (
			orgID              = 123
			namespaceUID       = "abc"
			ruleUID            = "some_rule_uid"
			ruleGroup          = "abc"
			newInterval  int64 = 120
		)
		u := &user.SignedInUser{OrgID: orgID}
		rule := dummyRule("my_rule", orgID)
		rule.UID = ruleUID
		rule.RuleGroup = ruleGroup
		rule.NamespaceUID = namespaceUID
		_, err := ruleService.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
		require.NoError(t, err)

		rule, _, err = ruleService.GetAlertRule(context.Background(), u, ruleUID)
		require.NoError(t, err)
		require.Equal(t, int64(1), rule.Version)
		require.Equal(t, int64(60), rule.IntervalSeconds)

		err = ruleService.UpdateRuleGroup(context.Background(), u, namespaceUID, ruleGroup, newInterval)
		require.NoError(t, err)

		rule, _, err = ruleService.GetAlertRule(context.Background(), u, ruleUID)
		require.NoError(t, err)
		require.Equal(t, int64(2), rule.Version)
		require.Equal(t, newInterval, rule.IntervalSeconds)
	})

	t.Run("updating a group by updating a rule should bump that rule's data and version number", func(t *testing.T) {
		group := createDummyGroup("group-test-5", orgID)
		err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
		require.NoError(t, err)
		updatedGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "group-test-5")
		require.NoError(t, err)

		updatedGroup.Rules[0].Title = "some-other-title-asdf"
		err = ruleService.ReplaceRuleGroup(context.Background(), u, updatedGroup, models.ProvenanceAPI, "")
		require.NoError(t, err)

		readGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "group-test-5")
		require.NoError(t, err)
		require.NotEmpty(t, readGroup.Rules)
		require.Len(t, readGroup.Rules, 1)
		require.Equal(t, "some-other-title-asdf", readGroup.Rules[0].Title)
		require.Equal(t, int64(2), readGroup.Rules[0].Version)
	})

	t.Run("updating a group should not override its rules editor settings", func(t *testing.T) {
		namespaceUID := "my-namespace"
		groupTitle := "test-group-123"

		// create the rule group via the rule store, to persist the editor settings
		rule := createTestRule(util.GenerateShortUID(), groupTitle, orgID, namespaceUID)
		ruleMetadata := models.AlertRuleMetadata{
			EditorSettings: models.EditorSettings{
				SimplifiedQueryAndExpressionsSection: true,
			},
		}
		rule.Metadata = ruleMetadata
		r, err := ruleService.ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.InsertRule{{AlertRule: rule}})
		require.NoError(t, err)
		require.Len(t, r, 1)

		// Set the UID for the rule to update it
		rule.UID = r[0].UID
		// clear the metadata to check that the existing metadata is not overridden
		rule.Metadata = models.AlertRuleMetadata{}

		// Now update the rule group with the rule to update its metadata
		group := models.AlertRuleGroup{
			Title:     groupTitle,
			Interval:  60,
			FolderUID: namespaceUID,
			Rules:     []models.AlertRule{rule},
		}

		err = ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
		require.NoError(t, err)

		readGroup, err := ruleService.GetRuleGroup(context.Background(), u, namespaceUID, groupTitle)
		require.NoError(t, err)
		require.NotEmpty(t, readGroup.Rules)
		require.Len(t, readGroup.Rules, 1)

		// check that the metadata is still there
		require.Equal(t, ruleMetadata, readGroup.Rules[0].Metadata)
	})

	t.Run("updating a group with editor settings should override its prometheus rule definition", func(t *testing.T) {
		namespaceUID := "my-namespace"
		groupTitle := "test-group-123"

		// create the rule group via the rule store, to persist the editor settings
		rule := createTestRule(util.GenerateShortUID(), groupTitle, orgID, namespaceUID)
		ruleMetadata := models.AlertRuleMetadata{
			EditorSettings: models.EditorSettings{
				SimplifiedQueryAndExpressionsSection: true,
			},
			PrometheusStyleRule: &models.PrometheusStyleRule{
				OriginalRuleDefinition: "old",
			},
		}
		rule.Metadata = ruleMetadata
		r, err := ruleService.ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.InsertRule{{AlertRule: rule}})
		require.NoError(t, err)
		require.Len(t, r, 1)

		// Set the UID for the rule to update it
		rule.UID = r[0].UID
		// clear the editor settings in the metadata to check that the existing setting is not overridden
		rule.Metadata = models.AlertRuleMetadata{
			PrometheusStyleRule: &models.PrometheusStyleRule{
				OriginalRuleDefinition: "new",
			},
		}

		// Now update the rule group with the rule to update its metadata
		group := models.AlertRuleGroup{
			Title:     groupTitle,
			Interval:  60,
			FolderUID: namespaceUID,
			Rules:     []models.AlertRule{rule},
		}

		err = ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
		require.NoError(t, err)

		readGroup, err := ruleService.GetRuleGroup(context.Background(), u, namespaceUID, groupTitle)
		require.NoError(t, err)
		require.NotEmpty(t, readGroup.Rules)
		require.Len(t, readGroup.Rules, 1)

		// check that the editor settings are still there
		require.True(t, readGroup.Rules[0].Metadata.EditorSettings.SimplifiedQueryAndExpressionsSection)
		// check the new prometheus rule definition
		require.Equal(t, "new", readGroup.Rules[0].Metadata.PrometheusStyleRule.OriginalRuleDefinition)
	})

	t.Run("updating a group should override its prometheus rule definition", func(t *testing.T) {
		namespaceUID := "my-namespace"
		groupTitle := "test-group-123"

		// create the rule group via the rule store, to persist the editor settings
		rule := createTestRule(util.GenerateShortUID(), groupTitle, orgID, namespaceUID)
		ruleMetadata := models.AlertRuleMetadata{
			PrometheusStyleRule: &models.PrometheusStyleRule{
				OriginalRuleDefinition: "old",
			},
		}
		rule.Metadata = ruleMetadata
		r, err := ruleService.ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.InsertRule{{AlertRule: rule}})
		require.NoError(t, err)
		require.Len(t, r, 1)

		// Set the UID for the rule to update it
		rule.UID = r[0].UID
		// make the metadata empty
		rule.Metadata = models.AlertRuleMetadata{}

		// Now update the rule group with the rule to update its metadata
		group := models.AlertRuleGroup{
			Title:     groupTitle,
			Interval:  60,
			FolderUID: namespaceUID,
			Rules:     []models.AlertRule{rule},
		}

		err = ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
		require.NoError(t, err)

		readGroup, err := ruleService.GetRuleGroup(context.Background(), u, namespaceUID, groupTitle)
		require.NoError(t, err)
		require.NotEmpty(t, readGroup.Rules)
		require.Len(t, readGroup.Rules, 1)

		// check that the prometheus rule definition is empty
		require.Nil(t, readGroup.Rules[0].Metadata.PrometheusStyleRule)
	})

	t.Run("updating a rule should not override its editor settings", func(t *testing.T) {
		rule := createTestRule(util.GenerateShortUID(), "my-group", orgID, "my-folder")
		ruleMetadata := models.AlertRuleMetadata{
			EditorSettings: models.EditorSettings{
				SimplifiedQueryAndExpressionsSection: true,
			},
		}
		rule.Metadata = ruleMetadata
		r, err := ruleService.ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.InsertRule{{AlertRule: rule}})
		require.NoError(t, err)
		require.Len(t, r, 1)

		// Set the UID for the rule to update it
		rule.UID = r[0].UID
		// clear the metadata to check that the existing metadata is not overridden
		rule.Metadata = models.AlertRuleMetadata{}

		// Update the rule
		_, err = ruleService.UpdateAlertRule(context.Background(), u, rule, models.ProvenanceAPI)
		require.NoError(t, err)

		// Read the rule and check that the editor settings are preserved
		readRule, _, err := ruleService.GetAlertRule(context.Background(), u, rule.UID)
		require.NoError(t, err)
		require.Equal(t, ruleMetadata, readRule.Metadata)
	})

	t.Run("updating a group to temporarily overlap rule names should not throw unique constraint", func(t *testing.T) {
		var orgID int64 = 1
		group := models.AlertRuleGroup{
			Title:     "overlap-test",
			Interval:  60,
			FolderUID: "my-namespace",
			Rules: []models.AlertRule{
				dummyRule("overlap-test-rule-1", orgID),
				dummyRule("overlap-test-rule-2", orgID),
			},
		}
		err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
		require.NoError(t, err)
		updatedGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "overlap-test")
		require.NoError(t, err)

		updatedGroup.Rules[0].Title = "overlap-test-rule-2"
		updatedGroup.Rules[1].Title = "overlap-test-rule-3"
		err = ruleService.ReplaceRuleGroup(context.Background(), u, updatedGroup, models.ProvenanceAPI, "")
		require.NoError(t, err)

		readGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "overlap-test")
		require.NoError(t, err)
		require.NotEmpty(t, readGroup.Rules)
		require.Len(t, readGroup.Rules, 2)
		require.Equal(t, "overlap-test-rule-2", readGroup.Rules[0].Title)
		require.Equal(t, "overlap-test-rule-3", readGroup.Rules[1].Title)
		require.Equal(t, int64(3), readGroup.Rules[0].Version)
		require.Equal(t, int64(3), readGroup.Rules[1].Version)
	})

	t.Run("updating a group to swap the name of two rules should not throw unique constraint", func(t *testing.T) {
		var orgID int64 = 1
		group := models.AlertRuleGroup{
			Title:     "swap-test",
			Interval:  60,
			FolderUID: "my-namespace",
			Rules: []models.AlertRule{
				dummyRule("swap-test-rule-1", orgID),
				dummyRule("swap-test-rule-2", orgID),
			},
		}
		err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
		require.NoError(t, err)
		updatedGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "swap-test")
		require.NoError(t, err)

		updatedGroup.Rules[0].Title = "swap-test-rule-2"
		updatedGroup.Rules[1].Title = "swap-test-rule-1"
		err = ruleService.ReplaceRuleGroup(context.Background(), u, updatedGroup, models.ProvenanceAPI, "")
		require.NoError(t, err)

		readGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "swap-test")
		require.NoError(t, err)
		require.NotEmpty(t, readGroup.Rules)
		require.Len(t, readGroup.Rules, 2)
		require.Equal(t, "swap-test-rule-2", readGroup.Rules[0].Title)
		require.Equal(t, "swap-test-rule-1", readGroup.Rules[1].Title)
		require.Equal(t, int64(3), readGroup.Rules[0].Version) // Needed an extra update to break the update cycle.
		require.Equal(t, int64(3), readGroup.Rules[1].Version)
	})

	t.Run("updating a group that has a rule name cycle should not throw unique constraint", func(t *testing.T) {
		var orgID int64 = 1
		group := models.AlertRuleGroup{
			Title:     "cycle-test",
			Interval:  60,
			FolderUID: "my-namespace",
			Rules: []models.AlertRule{
				dummyRule("cycle-test-rule-1", orgID),
				dummyRule("cycle-test-rule-2", orgID),
				dummyRule("cycle-test-rule-3", orgID),
			},
		}
		err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
		require.NoError(t, err)
		updatedGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "cycle-test")
		require.NoError(t, err)

		updatedGroup.Rules[0].Title = "cycle-test-rule-2"
		updatedGroup.Rules[1].Title = "cycle-test-rule-3"
		updatedGroup.Rules[2].Title = "cycle-test-rule-1"
		err = ruleService.ReplaceRuleGroup(context.Background(), u, updatedGroup, models.ProvenanceAPI, "")
		require.NoError(t, err)

		readGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "cycle-test")
		require.NoError(t, err)
		require.NotEmpty(t, readGroup.Rules)
		require.Len(t, readGroup.Rules, 3)
		require.Equal(t, "cycle-test-rule-2", readGroup.Rules[0].Title)
		require.Equal(t, "cycle-test-rule-3", readGroup.Rules[1].Title)
		require.Equal(t, "cycle-test-rule-1", readGroup.Rules[2].Title)
		require.Equal(t, int64(3), readGroup.Rules[0].Version) // Needed an extra update to break the update cycle.
		require.Equal(t, int64(3), readGroup.Rules[1].Version)
		require.Equal(t, int64(3), readGroup.Rules[2].Version)
	})

	t.Run("updating a group that has multiple rule name cycles should not throw unique constraint", func(t *testing.T) {
		var orgID int64 = 1
		group := models.AlertRuleGroup{
			Title:     "multi-cycle-test",
			Interval:  60,
			FolderUID: "my-namespace",
			Rules: []models.AlertRule{
				dummyRule("multi-cycle-test-rule-1", orgID),
				dummyRule("multi-cycle-test-rule-2", orgID),

				dummyRule("multi-cycle-test-rule-3", orgID),
				dummyRule("multi-cycle-test-rule-4", orgID),
				dummyRule("multi-cycle-test-rule-5", orgID),
			},
		}
		err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
		require.NoError(t, err)
		updatedGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "multi-cycle-test")
		require.NoError(t, err)

		updatedGroup.Rules[0].Title = "multi-cycle-test-rule-2"
		updatedGroup.Rules[1].Title = "multi-cycle-test-rule-1"

		updatedGroup.Rules[2].Title = "multi-cycle-test-rule-4"
		updatedGroup.Rules[3].Title = "multi-cycle-test-rule-5"
		updatedGroup.Rules[4].Title = "multi-cycle-test-rule-3"

		err = ruleService.ReplaceRuleGroup(context.Background(), u, updatedGroup, models.ProvenanceAPI, "")
		require.NoError(t, err)

		readGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "multi-cycle-test")
		require.NoError(t, err)
		require.NotEmpty(t, readGroup.Rules)
		require.Len(t, readGroup.Rules, 5)
		require.Equal(t, "multi-cycle-test-rule-2", readGroup.Rules[0].Title)
		require.Equal(t, "multi-cycle-test-rule-1", readGroup.Rules[1].Title)
		require.Equal(t, "multi-cycle-test-rule-4", readGroup.Rules[2].Title)
		require.Equal(t, "multi-cycle-test-rule-5", readGroup.Rules[3].Title)
		require.Equal(t, "multi-cycle-test-rule-3", readGroup.Rules[4].Title)
		require.Equal(t, int64(3), readGroup.Rules[0].Version) // Needed an extra update to break the update cycle.
		require.Equal(t, int64(3), readGroup.Rules[1].Version)
		require.Equal(t, int64(3), readGroup.Rules[2].Version) // Needed an extra update to break the update cycle.
		require.Equal(t, int64(3), readGroup.Rules[3].Version)
		require.Equal(t, int64(3), readGroup.Rules[4].Version)
	})

	t.Run("updating a group to recreate a rule using the same name should not throw unique constraint", func(t *testing.T) {
		var orgID int64 = 1
		group := models.AlertRuleGroup{
			Title:     "recreate-test",
			Interval:  60,
			FolderUID: "my-namespace",
			Rules: []models.AlertRule{
				dummyRule("recreate-test-rule-1", orgID),
			},
		}
		err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
		require.NoError(t, err)
		updatedGroup := models.AlertRuleGroup{
			Title:     "recreate-test",
			Interval:  60,
			FolderUID: "my-namespace",
			Rules: []models.AlertRule{
				dummyRule("recreate-test-rule-1", orgID),
			},
		}
		err = ruleService.ReplaceRuleGroup(context.Background(), u, updatedGroup, models.ProvenanceAPI, "")
		require.NoError(t, err)

		readGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "recreate-test")
		require.NoError(t, err)
		require.NotEmpty(t, readGroup.Rules)
		require.Len(t, readGroup.Rules, 1)
		require.Equal(t, "recreate-test-rule-1", readGroup.Rules[0].Title)
		require.Equal(t, int64(1), readGroup.Rules[0].Version)
	})

	t.Run("updating a group to create a rule that temporarily overlaps an existing should not throw unique constraint", func(t *testing.T) {
		var orgID int64 = 1
		group := models.AlertRuleGroup{
			Title:     "create-overlap-test",
			Interval:  60,
			FolderUID: "my-namespace",
			Rules: []models.AlertRule{
				dummyRule("create-overlap-test-rule-1", orgID),
			},
		}
		err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
		require.NoError(t, err)
		updatedGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "create-overlap-test")
		require.NoError(t, err)
		updatedGroup.Rules[0].Title = "create-overlap-test-rule-2"
		updatedGroup.Rules = append(updatedGroup.Rules, dummyRule("create-overlap-test-rule-1", orgID))

		err = ruleService.ReplaceRuleGroup(context.Background(), u, updatedGroup, models.ProvenanceAPI, "")
		require.NoError(t, err)

		readGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "create-overlap-test")
		require.NoError(t, err)
		require.NotEmpty(t, readGroup.Rules)
		require.Len(t, readGroup.Rules, 2)
		require.Equal(t, "create-overlap-test-rule-2", readGroup.Rules[0].Title)
		require.Equal(t, "create-overlap-test-rule-1", readGroup.Rules[1].Title)
		require.Equal(t, int64(2), readGroup.Rules[0].Version)
		require.Equal(t, int64(1), readGroup.Rules[1].Version)
	})

	t.Run("updating a group by updating a rule should not remove dashboard and panel ids", func(t *testing.T) {
		dashboardUid := "huYnkl7H"
		panelId := int64(5678)
		group := createDummyGroup("group-test-5", orgID)
		group.Rules[0].Annotations = map[string]string{
			models.DashboardUIDAnnotation: dashboardUid,
			models.PanelIDAnnotation:      strconv.FormatInt(panelId, 10),
		}

		err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
		require.NoError(t, err)
		updatedGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "group-test-5")
		require.NoError(t, err)

		require.NotNil(t, updatedGroup.Rules[0].DashboardUID)
		require.NotNil(t, updatedGroup.Rules[0].PanelID)
		require.Equal(t, dashboardUid, *updatedGroup.Rules[0].DashboardUID)
		require.Equal(t, panelId, *updatedGroup.Rules[0].PanelID)
	})

	t.Run("alert rule provenace 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) {
				rule := dummyRule(t.Name(), orgID)
				rule, err := ruleService.CreateAlertRule(context.Background(), u, rule, test.from)
				require.NoError(t, err)

				_, err = ruleService.UpdateAlertRule(context.Background(), u, rule, test.to)
				if test.errNil {
					require.NoError(t, err)
				} else {
					require.Error(t, err)
				}
			})
		}
	})

	t.Run("alert rule provenace should be correctly checked when writing groups", 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 be able to update from provenance api to none",
				from:   models.ProvenanceAPI,
				to:     models.ProvenanceNone,
				errNil: true,
			},
			{
				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,
			},
			{
				name:   "should be able to update from provenance none to 'converted prometheus'",
				from:   models.ProvenanceNone,
				to:     models.ProvenanceConvertedPrometheus,
				errNil: true,
			},
			{
				name:   "should be able to update from provenance 'converted prometheus' to none",
				from:   models.ProvenanceConvertedPrometheus,
				to:     models.ProvenanceNone,
				errNil: true,
			},
			{
				name:   "should not be able to update from provenance 'converted prometheus' to api",
				from:   models.ProvenanceConvertedPrometheus,
				to:     models.ProvenanceAPI,
				errNil: false,
			},
			{
				name:   "should not be able to update from provenance 'converted prometheus' to file",
				from:   models.ProvenanceConvertedPrometheus,
				to:     models.ProvenanceFile,
				errNil: false,
			},
			{
				name:   "should not be able to update from provenance api to 'converted prometheus'",
				from:   models.ProvenanceAPI,
				to:     models.ProvenanceConvertedPrometheus,
				errNil: false,
			},
			{
				name:   "should not be able to update from provenance file to 'converted prometheus'",
				from:   models.ProvenanceFile,
				to:     models.ProvenanceConvertedPrometheus,
				errNil: false,
			},
		}
		for _, test := range tests {
			t.Run(test.name, func(t *testing.T) {
				var orgID int64 = 1
				group := createDummyGroup(t.Name(), orgID)
				err := ruleService.ReplaceRuleGroup(context.Background(), u, group, test.from, "")
				require.NoError(t, err)

				group.Rules[0].Title = t.Name()
				err = ruleService.ReplaceRuleGroup(context.Background(), u, group, test.to, "")
				if test.errNil {
					require.NoError(t, err)
				} else {
					require.Error(t, err)
				}
			})
		}
	})

	t.Run("quota met causes create to be rejected", func(t *testing.T) {
		ruleService := createAlertRuleService(t, nil)
		checker := &MockQuotaChecker{}
		checker.EXPECT().LimitExceeded()
		ruleService.quotas = checker

		_, err := ruleService.CreateAlertRule(context.Background(), u, dummyRule("test#1", orgID), models.ProvenanceNone)

		require.ErrorIs(t, err, models.ErrQuotaReached)
	})

	t.Run("quota met causes group write to be rejected", func(t *testing.T) {
		ruleService := createAlertRuleService(t, nil)
		checker := &MockQuotaChecker{}
		checker.EXPECT().LimitExceeded()
		ruleService.quotas = checker

		group := createDummyGroup("quota-reached", orgID)
		err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")

		require.ErrorIs(t, err, models.ErrQuotaReached)
	})

	t.Run("alert rules created without a group should be considered NoGroup rules", func(t *testing.T) {
		rule := createNoGroupRule("test-no-group-rule", orgID, "my-namespace")
		// This is the way legacy storage creates rules without a group
		rule.RuleGroup = ""
		rule, err := ruleService.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
		require.NoError(t, err)
		require.Equal(t, int64(60), rule.IntervalSeconds)

		rule, _, err = ruleService.GetAlertRule(context.Background(), u, rule.UID)
		require.NoError(t, err)
		require.True(t, models.IsNoGroupRuleGroup(rule.RuleGroup), "Rule should be considered NoGroup rule")

		ruleGroup, err := ruleService.GetRuleGroup(context.Background(), u, rule.NamespaceUID, rule.RuleGroup)
		require.NoError(t, err)
		require.NotNil(t, ruleGroup)
		require.True(t, models.IsNoGroupRuleGroup(ruleGroup.Title), "Rule group should be NoGroup rule group")
		require.Len(t, ruleGroup.Rules, 1, "Rule group should only contain one NoGroup rule")
	})

	t.Run("multiple alert rules created without a group should be considered NoGroup rules, and be returned in separate groups", func(t *testing.T) {
		rule := createNoGroupRule("test-no-group-rule-1", orgID, "my-namespace")
		// This is the way legacy storage creates rules without a group
		rule.RuleGroup = ""
		rule, err := ruleService.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
		require.NoError(t, err)
		require.Equal(t, int64(60), rule.IntervalSeconds)

		rule2 := createNoGroupRule("test-no-group-rule-2", orgID, "my-namespace")
		// This is the way legacy storage creates rules without a group
		rule2.RuleGroup = ""
		rule2, err = ruleService.CreateAlertRule(context.Background(), u, rule2, models.ProvenanceNone)
		require.NoError(t, err)
		require.Equal(t, int64(60), rule.IntervalSeconds)

		rule, _, err = ruleService.GetAlertRule(context.Background(), u, rule.UID)
		require.NoError(t, err)
		require.True(t, models.IsNoGroupRuleGroup(rule.RuleGroup), "Rule should be considered NoGroup rule")

		rule2, _, err = ruleService.GetAlertRule(context.Background(), u, rule2.UID)
		require.NoError(t, err)
		require.True(t, models.IsNoGroupRuleGroup(rule2.RuleGroup), "Rule should be considered NoGroup rule")

		require.NotEqual(t, rule.RuleGroup, rule2.RuleGroup, "Both rules should have different NoGroup rule groups")

		ruleGroup, err := ruleService.GetRuleGroup(context.Background(), u, rule.NamespaceUID, rule.RuleGroup)
		require.NoError(t, err)
		require.NotNil(t, ruleGroup)
		require.True(t, models.IsNoGroupRuleGroup(ruleGroup.Title), "Rule group should be NoGroup rule group")
		require.Len(t, ruleGroup.Rules, 1, "Rule group should only contain one NoGroup rule")

		ruleGroup2, err := ruleService.GetRuleGroup(context.Background(), u, rule2.NamespaceUID, rule2.RuleGroup)
		require.NoError(t, err)
		require.NotNil(t, ruleGroup2)
		require.True(t, models.IsNoGroupRuleGroup(ruleGroup2.Title), "Rule group should be NoGroup rule group")
		require.Len(t, ruleGroup2.Rules, 1, "Rule group should only contain one NoGroup rule")

		require.NotEqual(t, ruleGroup, ruleGroup2, "Both NoGroup rule groups should be different")
	})

	t.Run("setting the group interval on NoGroup rules should only affect 1 rule", func(t *testing.T) {
		rule := createNoGroupRule("test-no-group-rule-1", orgID, "my-namespace")
		// This is the way legacy storage creates rules without a group
		rule.RuleGroup = ""
		rule, err := ruleService.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
		require.NoError(t, err)
		require.Equal(t, int64(60), rule.IntervalSeconds)

		rule2 := createNoGroupRule("test-no-group-rule-2", orgID, "my-namespace")
		// This is the way legacy storage creates rules without a group
		rule2.RuleGroup = ""
		rule2, err = ruleService.CreateAlertRule(context.Background(), u, rule2, models.ProvenanceNone)
		require.NoError(t, err)
		require.Equal(t, int64(60), rule.IntervalSeconds)

		rule, _, err = ruleService.GetAlertRule(context.Background(), u, rule.UID)
		require.NoError(t, err)
		require.True(t, models.IsNoGroupRuleGroup(rule.RuleGroup), "Rule should be considered NoGroup rule")

		rule2, _, err = ruleService.GetAlertRule(context.Background(), u, rule2.UID)
		require.NoError(t, err)
		require.True(t, models.IsNoGroupRuleGroup(rule2.RuleGroup), "Rule should be considered NoGroup rule")

		require.NotEqual(t, rule.RuleGroup, rule2.RuleGroup, "Both rules should have different NoGroup rule groups")

		var updatedInterval int64 = 120
		err = ruleService.UpdateRuleGroup(context.Background(), u, rule.NamespaceUID, rule.RuleGroup, updatedInterval)
		require.NoError(t, err)

		rule, _, err = ruleService.GetAlertRule(context.Background(), u, rule.UID)
		require.NoError(t, err)
		require.Equal(t, updatedInterval, rule.IntervalSeconds, "Rule should have updated interval")
		rule2, _, err = ruleService.GetAlertRule(context.Background(), u, rule2.UID)
		require.NoError(t, err)
		require.Equal(t, int64(60), rule2.IntervalSeconds, "Rule should not have updated interval")
		require.NotEqual(t, updatedInterval, rule2.IntervalSeconds, "Both rules should not have updated interval")
	})

	t.Run("alert rule in NoGroup should be updated correctly", func(t *testing.T) {
		rule := createNoGroupRule("test-no-group-rule", orgID, "my-namespace")
		// This is the way legacy storage creates rules without a group
		rule.RuleGroup = ""
		rule, err := ruleService.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
		require.NoError(t, err)
		require.Equal(t, int64(60), rule.IntervalSeconds)

		// get the actual calculated group for use with the api
		rule, _, err = ruleService.GetAlertRule(context.Background(), u, rule.UID)
		require.NoError(t, err)
		require.True(t, models.IsNoGroupRuleGroup(rule.RuleGroup), "Rule should be considered NoGroup rule")

		err = ruleService.UpdateRuleGroup(context.Background(), u, rule.NamespaceUID, rule.RuleGroup, 120)
		require.NoError(t, err)

		rule, _, err = ruleService.GetAlertRule(context.Background(), u, rule.UID)
		require.NoError(t, err)
		require.Equal(t, int64(120), rule.IntervalSeconds)
	})
}

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

	orgID := rand.Int63()
	u := &user.SignedInUser{OrgID: orgID, UserUID: util.GenerateShortUID()}
	groupKey := models.GenerateGroupKey(orgID)
	groupIntervalSeconds := int64(30)
	gen := models.RuleGen
	rules := gen.With(gen.WithGroupKey(groupKey), gen.WithIntervalSeconds(groupIntervalSeconds)).GenerateManyRef(3)
	groupProvenance := models.ProvenanceAPI

	initServiceWithData := func(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) {
		service, ruleStore, provenanceStore, ac := initService(t)
		ruleStore.Rules = map[int64][]*models.AlertRule{
			orgID: rules,
		}
		for _, rule := range rules {
			require.NoError(t, provenanceStore.SetProvenance(context.Background(), rule, orgID, groupProvenance))
		}
		return service, ruleStore, provenanceStore, ac
	}

	t.Run("when user can write all rules", func(t *testing.T) {
		t.Run("and a new rule creates a new group", func(t *testing.T) {
			rule := gen.With(gen.WithOrgID(orgID)).Generate()
			service, ruleStore, provenanceStore, ac := initServiceWithData(t)

			ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
				return true, nil
			}

			actualRule, err := service.CreateAlertRule(context.Background(), u, rule, models.ProvenanceFile)
			require.NoError(t, err)

			require.Len(t, ac.Calls, 1)
			assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)

			t.Run("it should assign default interval", func(t *testing.T) {
				require.Equal(t, service.defaultIntervalSeconds, actualRule.IntervalSeconds)
			})

			t.Run("inserts to database", func(t *testing.T) {
				inserts := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
					a, ok := cmd.([]models.InsertRule)
					return a, ok
				})
				require.Len(t, inserts, 1)
				cmd := inserts[0].([]models.InsertRule)
				require.Len(t, cmd, 1)
			})

			t.Run("set correct provenance", func(t *testing.T) {
				p, err := provenanceStore.GetProvenance(context.Background(), &actualRule, orgID)
				require.NoError(t, err)
				require.Equal(t, models.ProvenanceFile, p)
			})
		})
		t.Run("and it adds a rule to a group", func(t *testing.T) {
			rule := gen.With(gen.WithGroupKey(groupKey)).Generate()
			service, ruleStore, provenanceStore, ac := initServiceWithData(t)

			ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
				return true, nil
			}

			actualRule, err := service.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
			require.NoError(t, err)

			require.Len(t, ac.Calls, 1)
			assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)

			t.Run("it should assign group interval", func(t *testing.T) {
				require.Equal(t, groupIntervalSeconds, actualRule.IntervalSeconds)
			})

			t.Run("inserts to database", func(t *testing.T) {
				inserts := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
					a, ok := cmd.([]models.InsertRule)
					return a, ok
				})
				require.Len(t, inserts, 1)
				cmd := inserts[0].([]models.InsertRule)
				require.Len(t, cmd, 1)
			})

			t.Run("set correct provenance", func(t *testing.T) {
				p, err := provenanceStore.GetProvenance(context.Background(), &actualRule, orgID)
				require.NoError(t, err)
				require.Equal(t, models.ProvenanceNone, p)
			})
		})
	})
	t.Run("when user cannot write all rules", func(t *testing.T) {
		t.Run("and it creates a new group", func(t *testing.T) {
			rule := gen.With(gen.WithOrgID(orgID)).Generate()
			t.Run("it should authorize the change", func(t *testing.T) {
				service, ruleStore, provenanceStore, ac := initServiceWithData(t)

				ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
					return false, nil
				}
				ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
					assert.Equal(t, u, user)
					assert.Equal(t, rule.GetGroupKey(), change.GroupKey)
					assert.Len(t, change.New, 1)
					assert.Empty(t, change.Update)
					assert.Empty(t, change.Delete)
					assert.Empty(t, change.AffectedGroups)
					return nil
				}

				actualRule, err := service.CreateAlertRule(context.Background(), u, rule, models.ProvenanceFile)
				require.NoError(t, err)

				require.Len(t, ac.Calls, 2)
				assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
				assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method)

				t.Run("it should assign default interval", func(t *testing.T) {
					require.Equal(t, service.defaultIntervalSeconds, actualRule.IntervalSeconds)
				})

				t.Run("inserts to database", func(t *testing.T) {
					inserts := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
						a, ok := cmd.([]models.InsertRule)
						return a, ok
					})
					require.Len(t, inserts, 1)
					cmd := inserts[0].([]models.InsertRule)
					require.Len(t, cmd, 1)
				})

				t.Run("set correct provenance", func(t *testing.T) {
					p, err := provenanceStore.GetProvenance(context.Background(), &actualRule, orgID)
					require.NoError(t, err)
					require.Equal(t, models.ProvenanceFile, p)
				})
			})
		})
		t.Run("and it adds a rule to a group", func(t *testing.T) {
			rule := gen.With(gen.WithGroupKey(groupKey)).Generate()
			t.Run("it should authorize the change to whole group", func(t *testing.T) {
				service, ruleStore, provenanceStore, ac := initServiceWithData(t)

				ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
					return false, nil
				}
				ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
					assert.Equal(t, u, user)
					assert.Equal(t, rule.GetGroupKey(), change.GroupKey)
					assert.Contains(t, change.AffectedGroups, change.GroupKey)
					assert.EqualValues(t, change.AffectedGroups[change.GroupKey], rules)
					assert.Len(t, change.New, 1)
					assert.Empty(t, change.Update)
					assert.Empty(t, change.Delete)
					return nil
				}

				actualRule, err := service.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
				require.NoError(t, err)

				require.Len(t, ac.Calls, 2)
				assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
				assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method)

				t.Run("it should assign group interval", func(t *testing.T) {
					require.Equal(t, groupIntervalSeconds, actualRule.IntervalSeconds)
				})

				t.Run("inserts to database", func(t *testing.T) {
					inserts := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
						a, ok := cmd.([]models.InsertRule)
						return a, ok
					})
					require.Len(t, inserts, 1)
					cmd := inserts[0].([]models.InsertRule)
					require.Len(t, cmd, 1)
				})

				t.Run("set correct provenance", func(t *testing.T) {
					p, err := provenanceStore.GetProvenance(context.Background(), &actualRule, orgID)
					require.NoError(t, err)
					require.Equal(t, models.ProvenanceNone, p)
				})
			})
		})
		t.Run("it should not insert if not authorized", func(t *testing.T) {
			rule := gen.With(gen.WithGroupKey(groupKey)).Generate()
			service, ruleStore, _, ac := initServiceWithData(t)

			ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
				return false, nil
			}
			expectedErr := errors.New("test error")
			ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
				return expectedErr
			}

			_, err := service.CreateAlertRule(context.Background(), u, rule, models.ProvenanceFile)
			require.ErrorIs(t, expectedErr, err)

			require.Len(t, ac.Calls, 2)
			assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
			assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method)

			inserts := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
				a, ok := cmd.([]models.InsertRule)
				return a, ok
			})
			require.Empty(t, inserts)
		})
	})

	ruleService := createAlertRuleService(t, nil)
	t.Run("should return the created id", func(t *testing.T) {
		rule, err := ruleService.CreateAlertRule(context.Background(), u, dummyRule("test#1", orgID), models.ProvenanceNone)
		require.NoError(t, err)
		require.NotEqual(t, 0, rule.ID, "expected to get the created id and not the zero value")
	})

	t.Run("should set the right provenance", func(t *testing.T) {
		rule, err := ruleService.CreateAlertRule(context.Background(), u, dummyRule("test#2", orgID), models.ProvenanceAPI)
		require.NoError(t, err)

		_, provenance, err := ruleService.GetAlertRule(context.Background(), u, rule.UID)
		require.NoError(t, err)
		require.Equal(t, models.ProvenanceAPI, provenance)
	})

	t.Run("when UID is specified", func(t *testing.T) {
		t.Run("return error if it is not valid UID", func(t *testing.T) {
			rule := dummyRule("test#3", orgID)
			rule.UID = strings.Repeat("1", util.MaxUIDLength+1)
			rule, err := ruleService.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
			require.ErrorIs(t, err, models.ErrAlertRuleFailedValidation)
		})
		t.Run("should create a new rule with this UID", func(t *testing.T) {
			rule := dummyRule("test#3", orgID)
			uid := util.GenerateShortUID()
			rule.UID = uid
			created, err := ruleService.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
			require.NoError(t, err)
			require.Equal(t, uid, created.UID)
			_, _, err = ruleService.GetAlertRule(context.Background(), u, uid)
			require.NoError(t, err)
		})
	})
	t.Run("when dashboard is specified", func(t *testing.T) {
		t.Run("return no error when both specified", func(t *testing.T) {
			rule := dummyRule("test#4", orgID)
			dashboardUid := "oinwerfgiuac"
			panelId := int64(42)
			rule.Annotations = map[string]string{
				models.DashboardUIDAnnotation: dashboardUid,
				models.PanelIDAnnotation:      strconv.FormatInt(panelId, 10),
			}
			rule, err := ruleService.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
			require.NoError(t, err)
		})
		t.Run("return 4xx error when missing dashboard uid", func(t *testing.T) {
			rule := dummyRule("test#3", orgID)
			panelId := int64(42)
			rule.Annotations = map[string]string{
				models.PanelIDAnnotation: strconv.FormatInt(panelId, 10),
			}
			rule, err := ruleService.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
			require.ErrorIs(t, err, models.ErrAlertRuleFailedValidation)
		})
		t.Run("return 4xx error when missing panel id", func(t *testing.T) {
			rule := dummyRule("test#3", orgID)
			dashboardUid := "oinwerfgiuac"
			rule.Annotations = map[string]string{
				models.DashboardUIDAnnotation: dashboardUid,
			}
			rule, err := ruleService.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
			require.ErrorIs(t, err, models.ErrAlertRuleFailedValidation)
		})
	})

	t.Run("should not allow creating with a NoGroup Rule", func(t *testing.T) {
		// NoGroup rules are not allowed to be created directly via provisioning, they must be created via new k8s apis
		ruleWNoGroup := createNoGroupRule("test_No_group_create_disallowed", orgID, "test-no-group-ns")
		_, err := ruleService.CreateAlertRule(context.Background(), u, ruleWNoGroup, models.ProvenanceNone)
		require.ErrorIs(t, err, models.ErrAlertRuleFailedValidation)
		require.ErrorContains(t, err, "rules must have a valid group")
	})

	t.Run("should allow creating Rule without a group", func(t *testing.T) {
		ruleWNoGroup := createNoGroupRule("test_No_group_create_allowed", orgID, "test-no-group-ns")
		ruleWNoGroup.RuleGroup = "" // This is the way legacy storage creates rules without a group
		_, err := ruleService.CreateAlertRule(context.Background(), u, ruleWNoGroup, models.ProvenanceNone)
		require.NoError(t, err)
		// We should be able to retrieve the rule and see that it is a NoGroup rule
		retrievedRule, _, err := ruleService.GetAlertRule(context.Background(), u, ruleWNoGroup.UID)
		require.NoError(t, err)
		require.True(t, models.IsNoGroupRuleGroup(retrievedRule.RuleGroup), "Rule should be considered NoGroup rule")
	})
}

func TestUpdateAlertRule(t *testing.T) {
	orgID := rand.Int63()
	u := &user.SignedInUser{OrgID: orgID}
	groupKey := models.GenerateGroupKey(orgID)
	groupIntervalSeconds := int64(30)
	gen := models.RuleGen
	rules := gen.With(gen.WithGroupKey(groupKey), gen.WithIntervalSeconds(groupIntervalSeconds)).GenerateManyRef(3)
	groupProvenance := models.ProvenanceAPI

	initServiceWithData := func(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) {
		service, ruleStore, provenanceStore, ac := initService(t)
		ruleStore.Rules = map[int64][]*models.AlertRule{
			orgID: rules,
		}
		for _, rule := range rules {
			require.NoError(t, provenanceStore.SetProvenance(context.Background(), rule, orgID, groupProvenance))
		}
		return service, ruleStore, provenanceStore, ac
	}

	t.Run("when user can write all rules", func(t *testing.T) {
		rule := models.CopyRule(rules[0])
		rule.RuleGroup = rule.RuleGroup + "_new"
		rule.Title = rule.Title + "_new"
		service, ruleStore, _, ac := initServiceWithData(t)

		ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
			return true, nil
		}

		_, err := service.UpdateAlertRule(context.Background(), u, *rule, models.ProvenanceAPI)
		require.NoError(t, err)

		require.Len(t, ac.Calls, 1)
		assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)

		updates := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
			a, ok := cmd.([]models.UpdateRule)
			return a, ok
		})
		require.Len(t, updates, 1)
	})
	t.Run("when user cannot write all rules", func(t *testing.T) {
		rule := models.CopyRule(rules[0])
		rule.Title = rule.Title + "_new"

		t.Run("it should authorize the change to whole group", func(t *testing.T) {
			service, ruleStore, _, ac := initServiceWithData(t)

			ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
				return false, nil
			}
			ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
				assert.Equal(t, u, user)
				assert.Equal(t, groupKey, change.GroupKey)
				assert.Contains(t, change.AffectedGroups, groupKey)
				assert.EqualValues(t, rules, change.AffectedGroups[groupKey])
				assert.Len(t, change.Update, 1)
				assert.Empty(t, change.New)
				assert.Empty(t, change.Delete)
				return nil
			}

			_, err := service.UpdateAlertRule(context.Background(), u, *rule, groupProvenance)
			require.NoError(t, err)

			require.Len(t, ac.Calls, 2)
			assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
			assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method)

			updates := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
				a, ok := cmd.([]models.UpdateRule)
				return a, ok
			})
			require.Len(t, updates, 1)
		})
		t.Run("it should not update if not authorized", func(t *testing.T) {
			service, ruleStore, _, ac := initServiceWithData(t)

			ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
				return false, nil
			}
			expectedErr := errors.New("test error")
			ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
				return expectedErr
			}

			_, err := service.UpdateAlertRule(context.Background(), u, *rule, groupProvenance)
			require.ErrorIs(t, expectedErr, err)

			require.Len(t, ac.Calls, 2)
			assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
			assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method)

			updates := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
				a, ok := cmd.([]models.UpdateRule)
				return a, ok
			})
			require.Empty(t, updates)
		})

		t.Run("when there are no changes it should be successful", func(t *testing.T) {
			// For this test we will not change the rule, and we will not use "admin" (CanWriteAllRules)
			// permissions. The response of the service should still be successful.
			service, ruleStore, _, ac := initServiceWithData(t)

			rule := models.CopyRule(rules[0])

			_, err := service.ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.InsertRule{{AlertRule: *rule}})
			require.NoError(t, err)

			ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
				return false, nil
			}

			_, err = service.UpdateAlertRule(context.Background(), u, *rule, groupProvenance)
			require.NoError(t, err)

			updates := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
				a, ok := cmd.([]models.UpdateRule)
				return a, ok
			})
			require.Empty(t, updates)
		})
	})

	// NoGroup-specific tests for UpdateAlertRule
	t.Run("NoGroup: UpdateAlertRule preserves interval and sentinel group", func(t *testing.T) {
		service, ruleStore, provenanceStore, ac := initService(t)
		ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { return true, nil }

		rule := createNoGroupRule("nogroup-update", orgID, "my-namespace")
		_, err := ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.InsertRule{{AlertRule: rule}})
		require.NoError(t, err)
		require.NoError(t, provenanceStore.SetProvenance(context.Background(), &rule, orgID, models.ProvenanceNone))

		// mutate fields and attempt to change interval via UpdateAlertRule
		rule.Title = "nogroup-update-new"
		originalInterval := int64(60)
		require.Equal(t, originalInterval, rule.IntervalSeconds)
		rule.IntervalSeconds = originalInterval + 60

		updated, err := service.UpdateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
		require.NoError(t, err)
		require.True(t, models.IsNoGroupRuleGroup(updated.RuleGroup))
		require.Equal(t, "nogroup-update-new", updated.Title)
		require.Equal(t, originalInterval, updated.IntervalSeconds)
	})
}

func TestDeleteAlertRule(t *testing.T) {
	orgID := rand.Int63()
	u := &user.SignedInUser{OrgID: orgID}
	groupKey := models.GenerateGroupKey(orgID)
	groupIntervalSeconds := int64(30)
	gen := models.RuleGen
	rules := gen.With(gen.WithGroupKey(groupKey), gen.WithIntervalSeconds(groupIntervalSeconds)).GenerateManyRef(3)
	groupProvenance := models.ProvenanceAPI

	initServiceWithData := func(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) {
		service, ruleStore, provenanceStore, ac := initService(t)
		ruleStore.Rules = map[int64][]*models.AlertRule{
			orgID: rules,
		}
		for _, rule := range rules {
			require.NoError(t, provenanceStore.SetProvenance(context.Background(), rule, orgID, groupProvenance))
		}
		return service, ruleStore, provenanceStore, ac
	}

	t.Run("when user can write all rules", func(t *testing.T) {
		rule := rules[0]
		service, ruleStore, _, ac := initServiceWithData(t)

		ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
			return true, nil
		}

		err := service.DeleteAlertRule(context.Background(), u, rule.UID, groupProvenance)
		require.NoError(t, err)

		require.Len(t, ac.Calls, 1)
		assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)

		deletes := getDeleteQueries(ruleStore)
		require.Len(t, deletes, 1)
	})
	t.Run("when user cannot write all rules", func(t *testing.T) {
		rule := models.CopyRule(rules[0])
		rule.Title = rule.Title + "_new"

		t.Run("it should authorize the change to whole group", func(t *testing.T) {
			rule := rules[0]
			service, ruleStore, _, ac := initServiceWithData(t)

			ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
				return false, nil
			}
			ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
				assert.Equal(t, u, user)
				assert.Equal(t, groupKey, change.GroupKey)
				assert.Contains(t, change.AffectedGroups, groupKey)
				assert.EqualValues(t, rules, change.AffectedGroups[groupKey])
				assert.Empty(t, change.Update)
				assert.Empty(t, change.New)
				assert.Len(t, change.Delete, 1)
				return nil
			}

			err := service.DeleteAlertRule(context.Background(), u, rule.UID, groupProvenance)
			require.NoError(t, err)

			require.Len(t, ac.Calls, 2)
			assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
			assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method)

			deletes := getDeleteQueries(ruleStore)
			require.Len(t, deletes, 1)
		})
		t.Run("it should not delete if not authorized", func(t *testing.T) {
			service, ruleStore, _, ac := initServiceWithData(t)

			ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
				return false, nil
			}
			expectedErr := errors.New("test error")
			ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
				return expectedErr
			}

			_, err := service.UpdateAlertRule(context.Background(), u, *rule, groupProvenance)
			require.ErrorIs(t, expectedErr, err)

			require.Len(t, ac.Calls, 2)
			assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
			assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method)

			deletes := getDeleteQueries(ruleStore)
			require.Empty(t, deletes)
		})
	})

	// NoGroup-specific behaviors
	t.Run("deleting a NoGroup rule removes only that rule", func(t *testing.T) {
		service, ruleStore, provenanceStore, ac := initService(t)
		ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { return true, nil }

		// create two NoGroup rules in the same namespace (distinct sentinel groups)
		r1 := createNoGroupRule("nogroup-del-1", orgID, "my-namespace")
		r2 := createNoGroupRule("nogroup-del-2", orgID, "my-namespace")
		_, err := ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.InsertRule{{AlertRule: r1}, {AlertRule: r2}})
		require.NoError(t, err)
		require.NoError(t, provenanceStore.SetProvenance(context.Background(), &r1, orgID, models.ProvenanceNone))
		require.NoError(t, provenanceStore.SetProvenance(context.Background(), &r2, orgID, models.ProvenanceNone))

		err = service.DeleteAlertRule(context.Background(), u, r1.UID, models.ProvenanceNone)
		require.NoError(t, err)

		deletes := getDeleteQueries(ruleStore)
		require.Len(t, deletes, 1)
		uids := deletes[0].Params[3].([]string)
		require.Contains(t, uids, r1.UID)

		// verify r2 remains in store
		remaining := ruleStore.Rules[orgID]
		require.Len(t, remaining, 1)
		require.Equal(t, r2.UID, remaining[0].UID)
		// and its sentinel group remains intact
		require.True(t, models.IsNoGroupRuleGroup(remaining[0].RuleGroup))
	})

	t.Run("when user cannot write all rules, deleting a NoGroup rule authorizes and succeeds", func(t *testing.T) {
		service, ruleStore, provenanceStore, ac := initService(t)
		ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { return false, nil }

		r := createNoGroupRule("nogroup-del-auth", orgID, "my-namespace")
		_, err := ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.InsertRule{{AlertRule: r}})
		require.NoError(t, err)
		require.NoError(t, provenanceStore.SetProvenance(context.Background(), &r, orgID, models.ProvenanceNone))

		ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
			// expect single delete and affected group contains exactly the rule
			require.Len(t, change.Delete, 1)
			require.Contains(t, change.AffectedGroups, change.GroupKey)
			require.Len(t, change.AffectedGroups[change.GroupKey], 1)
			require.Equal(t, r.UID, change.AffectedGroups[change.GroupKey][0].UID)
			return nil
		}

		err = service.DeleteAlertRule(context.Background(), u, r.UID, models.ProvenanceNone)
		require.NoError(t, err)

		deletes := getDeleteQueries(ruleStore)
		require.Len(t, deletes, 1)
		uids := deletes[0].Params[3].([]string)
		require.Contains(t, uids, r.UID)
	})
}

func TestGetAlertRule(t *testing.T) {
	orgID := rand.Int63()
	u := &user.SignedInUser{OrgID: orgID}
	groupKey := models.GenerateGroupKey(orgID)
	gen := models.RuleGen
	rules := gen.With(gen.WithGroupKey(groupKey)).GenerateManyRef(3)
	rule := rules[0]
	expectedProvenance := models.ProvenanceAPI

	initServiceWithData := func(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) {
		service, ruleStore, provenanceStore, ac := initService(t)
		ruleStore.Rules = map[int64][]*models.AlertRule{
			orgID: rules,
		}
		require.NoError(t, provenanceStore.SetProvenance(context.Background(), rule, orgID, expectedProvenance))

		return service, ruleStore, provenanceStore, ac
	}

	t.Run("should authorize access to rule", func(t *testing.T) {
		service, _, _, ac := initServiceWithData(t)

		expected := errors.New("test")
		ac.AuthorizeAccessInFolderFunc = func(ctx context.Context, user identity.Requester, namespaced models.Namespaced) error {
			assert.Equal(t, u, user)
			assert.EqualValues(t, rule, namespaced)
			return expected
		}

		_, _, err := service.GetAlertRule(context.Background(), u, rule.UID)
		require.Error(t, err)
		require.Equal(t, expected, err)

		assert.Len(t, ac.Calls, 1)
		assert.Equal(t, "AuthorizeRuleRead", ac.Calls[0].Method)

		ac.Calls = nil
		ac.AuthorizeAccessInFolderFunc = func(ctx context.Context, user identity.Requester, namespaced models.Namespaced) error {
			return nil
		}

		actual, provenance, err := service.GetAlertRule(context.Background(), u, rule.UID)
		require.NoError(t, err)
		assert.Equal(t, *rule, actual)
		assert.Equal(t, expectedProvenance, provenance)
	})

	t.Run("should return ErrAlertRuleNotFound if rule does not exist", func(t *testing.T) {
		service, ruleStore, _, ac := initServiceWithData(t)

		_, _, err := service.GetAlertRule(context.Background(), u, "no-rule-uid")
		require.ErrorIs(t, err, models.ErrAlertRuleNotFound)

		assert.Len(t, ac.Calls, 0)
		require.IsType(t, ruleStore.RecordedOps[0], models.GetAlertRuleByUIDQuery{})
		query := ruleStore.RecordedOps[0].(models.GetAlertRuleByUIDQuery)
		assert.Equal(t, models.GetAlertRuleByUIDQuery{
			OrgID: orgID,
			UID:   "no-rule-uid",
		}, query)
	})
}

func TestGetRuleGroup(t *testing.T) {
	orgID := rand.Int63()
	u := &user.SignedInUser{OrgID: orgID}
	groupKey := models.GenerateGroupKey(orgID)
	intervalSeconds := int64(30)
	gen := models.RuleGen
	rules := gen.With(gen.WithGroupKey(groupKey), gen.WithIntervalSeconds(intervalSeconds)).GenerateManyRef(3)
	derefRules := make([]models.AlertRule, 0, len(rules))
	for _, rule := range rules {
		derefRules = append(derefRules, *rule)
	}
	expectedProvenance := models.ProvenanceAPI

	initServiceWithData := func(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) {
		service, ruleStore, provenanceStore, ac := initService(t)
		ruleStore.Rules = map[int64][]*models.AlertRule{
			orgID: rules,
		}
		for _, rule := range rules {
			require.NoError(t, provenanceStore.SetProvenance(context.Background(), rule, orgID, expectedProvenance))
		}

		return service, ruleStore, provenanceStore, ac
	}

	t.Run("return ErrAlertRuleGroupNotFound when rule group does not exist", func(t *testing.T) {
		service, _, _, ac := initServiceWithData(t)

		_, err := service.GetRuleGroup(context.Background(), u, groupKey.NamespaceUID, "no-rule-group")
		require.ErrorIs(t, err, models.ErrAlertRuleGroupNotFound)
		require.Empty(t, ac.Calls)
	})

	t.Run("when user cannot read all rules", func(t *testing.T) {
		t.Run("it should authorize access to entire group", func(t *testing.T) {
			service, _, _, ac := initServiceWithData(t)

			ac.CanReadAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
				assert.Equal(t, u, user)
				return false, nil
			}
			expectedErr := errors.New("error")
			ac.AuthorizeAccessToRuleGroupFunc = func(ctx context.Context, user identity.Requester, r models.RulesGroup) error {
				assert.Equal(t, u, user)
				assert.EqualValues(t, rules, r)
				return expectedErr
			}

			_, err := service.GetRuleGroup(context.Background(), u, groupKey.NamespaceUID, groupKey.RuleGroup)
			require.Error(t, err)
			require.Equal(t, expectedErr, err)

			assert.Len(t, ac.Calls, 2)
			assert.Equal(t, "CanReadAllRules", ac.Calls[0].Method)
			assert.Equal(t, "AuthorizeRuleGroupRead", ac.Calls[1].Method)

			ac.AuthorizeAccessToRuleGroupFunc = func(ctx context.Context, user identity.Requester, rules models.RulesGroup) error {
				return nil
			}

			group, err := service.GetRuleGroup(context.Background(), u, groupKey.NamespaceUID, groupKey.RuleGroup)
			require.NoError(t, err)

			assert.Equal(t, groupKey.RuleGroup, group.Title)
			assert.Equal(t, groupKey.NamespaceUID, group.FolderUID)
			assert.Equal(t, intervalSeconds, group.Interval)
			assert.Equal(t, derefRules, group.Rules)
		})
	})

	t.Run("when user can read all rules", func(t *testing.T) {
		t.Run("it should skip AuthorizeRuleGroupRead", func(t *testing.T) {
			service, _, _, ac := initServiceWithData(t)

			ac.CanReadAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
				assert.Equal(t, u, user)
				return true, nil
			}

			group, err := service.GetRuleGroup(context.Background(), u, groupKey.NamespaceUID, groupKey.RuleGroup)
			require.NoError(t, err)

			assert.Len(t, ac.Calls, 1)
			assert.Equal(t, "CanReadAllRules", ac.Calls[0].Method)

			assert.Equal(t, groupKey.RuleGroup, group.Title)
			assert.Equal(t, groupKey.NamespaceUID, group.FolderUID)
			assert.Equal(t, intervalSeconds, group.Interval)
			assert.Equal(t, derefRules, group.Rules)
		})
	})

	t.Run("return error immediately when CanReadAllRules returns error", func(t *testing.T) {
		service, _, _, ac := initServiceWithData(t)

		expectedErr := errors.New("test")
		ac.CanReadAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
			return false, expectedErr
		}

		_, err := service.GetRuleGroup(context.Background(), u, groupKey.NamespaceUID, groupKey.RuleGroup)
		require.Error(t, err)
		require.Equal(t, expectedErr, err)

		assert.Len(t, ac.Calls, 1)
		assert.Equal(t, "CanReadAllRules", ac.Calls[0].Method)
	})

	// NoGroup-specific behaviors
	// A NoGroup rule should be returned as a one-rule group addressed by its sentinel group title
	t.Run("should return NoGroup rule group with exactly one rule", func(t *testing.T) {
		service, ruleStore, _, ac := initService(t)
		ac.CanReadAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { return true, nil }

		rule := createNoGroupRule("nogroup-rule-1", orgID, "my-namespace")
		ruleStore.Rules = map[int64][]*models.AlertRule{
			orgID: {&rule},
		}

		group, err := service.GetRuleGroup(context.Background(), u, rule.NamespaceUID, rule.RuleGroup)
		require.NoError(t, err)
		require.True(t, models.IsNoGroupRuleGroup(group.Title))
		require.Equal(t, rule.NamespaceUID, group.FolderUID)
		require.Equal(t, rule.IntervalSeconds, group.Interval)
		require.Len(t, group.Rules, 1)
		require.Equal(t, rule.UID, group.Rules[0].UID)
	})

	// Multiple NoGroup rules in the same namespace must produce separate sentinel groups
	t.Run("should return distinct NoGroup groups for multiple rules", func(t *testing.T) {
		service, ruleStore, _, ac := initService(t)
		ac.CanReadAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { return true, nil }

		rule1 := createNoGroupRule("nogroup-rule-a", orgID, "my-namespace")
		rule2 := createNoGroupRule("nogroup-rule-b", orgID, "my-namespace")
		require.NotEqual(t, rule1.RuleGroup, rule2.RuleGroup)

		ruleStore.Rules = map[int64][]*models.AlertRule{
			orgID: {&rule1, &rule2},
		}

		group1, err := service.GetRuleGroup(context.Background(), u, rule1.NamespaceUID, rule1.RuleGroup)
		require.NoError(t, err)
		require.True(t, models.IsNoGroupRuleGroup(group1.Title))
		require.Len(t, group1.Rules, 1)
		require.Equal(t, rule1.UID, group1.Rules[0].UID)

		group2, err := service.GetRuleGroup(context.Background(), u, rule2.NamespaceUID, rule2.RuleGroup)
		require.NoError(t, err)
		require.True(t, models.IsNoGroupRuleGroup(group2.Title))
		require.Len(t, group2.Rules, 1)
		require.Equal(t, rule2.UID, group2.Rules[0].UID)
	})
}

func TestListAlertRules(t *testing.T) {
	orgID := rand.Int63()
	u := &user.SignedInUser{OrgID: orgID}
	groupKey1 := models.GenerateGroupKey(orgID)
	groupKey2 := models.GenerateGroupKey(orgID)
	gen := models.RuleGen
	rules1 := gen.With(gen.WithGroupKey(groupKey1), gen.WithUniqueGroupIndex()).GenerateManyRef(3)
	models.RulesGroup(rules1).SortByGroupIndex()
	rules2 := gen.With(gen.WithGroupKey(groupKey2), gen.WithUniqueGroupIndex()).GenerateManyRef(4)
	models.RulesGroup(rules2).SortByGroupIndex()
	allRules := append(rules1, rules2...)

	fs := foldertest.NewFakeService()
	fs.AddFolder(&folder.Folder{
		OrgID: orgID,
		UID:   groupKey1.NamespaceUID,
		Title: "folder1",
	})
	fs.AddFolder(&folder.Folder{
		OrgID: orgID,
		UID:   groupKey2.NamespaceUID,
		Title: "folder2",
	})

	initServiceWithData := func(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) {
		service, ruleStore, provenanceStore, ac := initService(t)
		service.folderService = fs
		ruleStore.Rules = map[int64][]*models.AlertRule{
			orgID: allRules,
		}
		ac.HasAccessInFolderFunc = func(ctx context.Context, user identity.Requester, folder models.Namespaced) (bool, error) {
			return true, nil
		}

		return service, ruleStore, provenanceStore, ac
	}

	t.Run("when user can read all rules", func(t *testing.T) {
		t.Run("should skip AuthorizeRuleGroupRead and return all rules", func(t *testing.T) {
			service, _, _, ac := initServiceWithData(t)
			ac.CanReadAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
				return true, nil
			}

			rules, _, token, err := service.ListAlertRules(context.Background(), u, ListAlertRulesOptions{})
			require.NoError(t, err)
			// check that rules contain all uids from allRules
			ruleUIDs := make(map[string]bool)
			for _, r := range rules {
				ruleUIDs[r.UID] = true
			}
			for _, r := range allRules {
				assert.True(t, ruleUIDs[r.UID])
			}
			require.Len(t, ruleUIDs, len(allRules))
			require.Empty(t, token)

			assert.Len(t, ac.Calls, 1)
			assert.Equal(t, "CanReadAllRules", ac.Calls[0].Method)
		})
	})

	t.Run("when user cannot read all rules", func(t *testing.T) {
		t.Run("should return only rules in accessible folders", func(t *testing.T) {
			service, _, _, ac := initServiceWithData(t)
			ac.CanReadAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
				return false, nil
			}
			ac.HasAccessInFolderFunc = func(ctx context.Context, user identity.Requester, folder models.Namespaced) (bool, error) {
				return folder.GetNamespaceUID() == groupKey2.NamespaceUID, nil
			}

			rules, _, token, err := service.ListAlertRules(context.Background(), u, ListAlertRulesOptions{})
			require.NoError(t, err)
			// check that rules contain all uids from rules1
			ruleUIDs := make(map[string]bool)
			for _, r := range rules {
				ruleUIDs[r.UID] = true
			}
			for _, r := range rules2 {
				assert.True(t, ruleUIDs[r.UID])
			}
			require.Len(t, ruleUIDs, len(rules2))
			require.Empty(t, token)

			assert.Len(t, ac.Calls, 3)
			assert.Equal(t, "CanReadAllRules", ac.Calls[0].Method)
			assert.Equal(t, "HasAccessInFolder", ac.Calls[1].Method)
			assert.Equal(t, "HasAccessInFolder", ac.Calls[2].Method)
		})
	})
}

func TestGetAlertRules(t *testing.T) {
	orgID := rand.Int63()
	u := &user.SignedInUser{OrgID: orgID}
	groupKey1 := models.GenerateGroupKey(orgID)
	groupKey2 := models.GenerateGroupKey(orgID)
	gen := models.RuleGen
	rules1 := gen.With(gen.WithGroupKey(groupKey1), gen.WithUniqueGroupIndex()).GenerateManyRef(3)
	models.RulesGroup(rules1).SortByGroupIndex()
	rules2 := gen.With(gen.WithGroupKey(groupKey2), gen.WithUniqueGroupIndex()).GenerateManyRef(4)
	models.RulesGroup(rules2).SortByGroupIndex()
	allRules := append(rules1, rules2...)
	expectedProvenance := models.ProvenanceAPI

	initServiceWithData := func(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) {
		service, ruleStore, provenanceStore, ac := initService(t)
		ruleStore.Rules = map[int64][]*models.AlertRule{
			orgID: allRules,
		}
		for _, rule := range rules1 {
			require.NoError(t, provenanceStore.SetProvenance(context.Background(), rule, orgID, expectedProvenance))
		}

		return service, ruleStore, provenanceStore, ac
	}

	t.Run("return error when CanReadAllRules return error", func(t *testing.T) {
		service, _, _, ac := initServiceWithData(t)
		expectedErr := errors.New("test")
		ac.CanReadAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
			return false, expectedErr
		}

		_, _, err := service.GetAlertRules(context.Background(), u)
		require.ErrorIs(t, err, expectedErr)
	})

	t.Run("when user can read all rules", func(t *testing.T) {
		t.Run("should skip AuthorizeRuleGroupRead and return all rules", func(t *testing.T) {
			service, _, _, ac := initServiceWithData(t)
			ac.CanReadAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
				return true, nil
			}

			rules, provenance, err := service.GetAlertRules(context.Background(), u)
			require.NoError(t, err)
			require.Equal(t, allRules, rules)
			require.Len(t, provenance, len(rules1))

			assert.Len(t, ac.Calls, 1)
			assert.Equal(t, "CanReadAllRules", ac.Calls[0].Method)
		})
	})

	t.Run("when user cannot read all rules", func(t *testing.T) {
		t.Run("should group rules and check AuthorizeRuleGroupRead and return only available rules", func(t *testing.T) {
			t.Run("should remove group from output if AuthorizeRuleGroupRead returns authorization error", func(t *testing.T) {
				service, _, _, ac := initServiceWithData(t)
				ac.CanReadAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
					return false, nil
				}
				ac.AuthorizeAccessToRuleGroupFunc = func(ctx context.Context, user identity.Requester, rules models.RulesGroup) error {
					if rules[0].GetGroupKey() == groupKey1 {
						return accesscontrol.NewAuthorizationErrorGeneric("test")
					}
					return nil
				}

				rules, provenance, err := service.GetAlertRules(context.Background(), u)
				require.NoError(t, err)

				assert.Equal(t, rules2, rules)
				assert.Empty(t, provenance)

				assert.Len(t, ac.Calls, 3)
				assert.Equal(t, "CanReadAllRules", ac.Calls[0].Method)
				assert.Equal(t, "AuthorizeRuleGroupRead", ac.Calls[1].Method)
				assert.Equal(t, "AuthorizeRuleGroupRead", ac.Calls[2].Method)

				group1 := ac.Calls[1].Args[2].(models.RulesGroup)
				group2 := ac.Calls[2].Args[2].(models.RulesGroup)
				require.Len(t, append(group1, group2...), len(allRules))
			})

			t.Run("should immediately exist if AuthorizeRuleGroupRead returns another error", func(t *testing.T) {
				service, _, _, ac := initServiceWithData(t)
				ac.CanReadAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
					return false, nil
				}
				expectedErr := errors.New("test")
				ac.AuthorizeAccessToRuleGroupFunc = func(ctx context.Context, user identity.Requester, rules models.RulesGroup) error {
					return expectedErr
				}

				_, _, err := service.GetAlertRules(context.Background(), u)
				require.ErrorIs(t, err, expectedErr)
			})
		})
	})
}

func TestReplaceGroup(t *testing.T) {
	orgID := rand.Int63()
	u := &user.SignedInUser{OrgID: orgID}
	groupKey := models.GenerateGroupKey(orgID)
	groupIntervalSeconds := int64(30)
	gen := models.RuleGen
	rules := gen.With(gen.WithGroupKey(groupKey), gen.WithIntervalSeconds(groupIntervalSeconds)).GenerateManyRef(3)
	groupProvenance := models.ProvenanceAPI

	initServiceWithData := func(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) {
		service, ruleStore, provenanceStore, ac := initService(t)
		ruleStore.Rules = map[int64][]*models.AlertRule{
			orgID: rules,
		}
		for _, rule := range rules {
			require.NoError(t, provenanceStore.SetProvenance(context.Background(), rule, orgID, groupProvenance))
		}
		return service, ruleStore, provenanceStore, ac
	}

	t.Run("when user can write all rules", func(t *testing.T) {
		group := models.AlertRuleGroup{
			Title:      groupKey.RuleGroup,
			FolderUID:  groupKey.NamespaceUID,
			Interval:   groupIntervalSeconds,
			Provenance: groupProvenance,
		}
		for _, rule := range rules {
			r := models.CopyRule(rule)
			r.Title = r.Title + "_new"
			group.Rules = append(group.Rules, *r)
		}

		service, ruleStore, _, ac := initServiceWithData(t)

		ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
			return true, nil
		}

		err := service.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
		require.NoError(t, err)

		require.Len(t, ac.Calls, 1)
		assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)

		updates := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
			a, ok := cmd.([]models.UpdateRule)
			return a, ok
		})
		require.Len(t, updates, 1)
	})
	t.Run("when user cannot write all rules", func(t *testing.T) {
		group := models.AlertRuleGroup{
			Title:      groupKey.RuleGroup,
			FolderUID:  groupKey.NamespaceUID,
			Interval:   groupIntervalSeconds,
			Provenance: groupProvenance,
		}
		for _, rule := range rules {
			r := models.CopyRule(rule)
			r.Title = r.Title + "_new"
			group.Rules = append(group.Rules, *r)
		}

		t.Run("it should not update if not authorized", func(t *testing.T) {
			service, ruleStore, _, ac := initServiceWithData(t)

			ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
				return false, nil
			}
			expectedErr := errors.New("test error")
			ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
				return expectedErr
			}

			err := service.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
			require.ErrorIs(t, err, expectedErr)

			require.Len(t, ac.Calls, 2)
			assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
			assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method)

			updates := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
				a, ok := cmd.([]models.UpdateRule)
				return a, ok
			})
			require.Empty(t, updates)
		})
		t.Run("it should update if authorized", func(t *testing.T) {
			service, ruleStore, _, ac := initServiceWithData(t)

			ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
				return false, nil
			}
			ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
				return nil
			}

			err := service.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
			require.NoError(t, err)

			require.Len(t, ac.Calls, 2)
			assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
			assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method)

			updates := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
				a, ok := cmd.([]models.UpdateRule)
				return a, ok
			})
			require.Len(t, updates, 1)
		})
	})

	t.Run("alert rule metadata should be updated correctly", func(t *testing.T) {
		service, _, _, _ := initServiceWithData(t)

		rule := dummyRule("test#3", orgID)
		// the rule must have a UID to be updated, otherwise it will be created as new
		// and the previous version will be deleted
		rule.UID = util.GenerateShortUID()
		rule.Metadata = models.AlertRuleMetadata{
			EditorSettings: models.EditorSettings{
				SimplifiedQueryAndExpressionsSection: true,
			},
			PrometheusStyleRule: &models.PrometheusStyleRule{
				OriginalRuleDefinition: "old",
			},
		}
		group := models.AlertRuleGroup{
			Title:     rule.RuleGroup,
			Interval:  rule.IntervalSeconds,
			FolderUID: rule.NamespaceUID,
			Rules:     []models.AlertRule{rule},
		}

		err := service.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceNone, "")
		require.NoError(t, err)

		rule.Metadata.PrometheusStyleRule.OriginalRuleDefinition = "new"
		err = service.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceNone, "")
		require.NoError(t, err)

		rule, _, err = service.GetAlertRule(context.Background(), u, rule.UID)
		require.NoError(t, err)
		require.Equal(t, "new", rule.Metadata.PrometheusStyleRule.OriginalRuleDefinition)
	})

	// NoGroup rule group should not allow more than one rule
	t.Run("should reject multiple rules in a NoGroup rule group", func(t *testing.T) {
		service, _, _, _ := initServiceWithData(t)

		// Build a NoGroup group title and attempt to place 2 rules under it
		group := createNoGroupRuleGroup("nogroup-multi", orgID, "my-namespace")
		second := createNoGroupRule("nogroup-second", orgID, "my-namespace")
		// Ensure the second rule is assigned to the same sentinel group
		second.RuleGroup = group.Title
		group.Rules = append(group.Rules, second)

		err := service.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceNone, "")
		require.Error(t, err)
		require.ErrorContains(t, err, "cannot be used for rule groups with multiple rules")
	})

	t.Run("should reject changing the group name in a NoGroup rule group", func(t *testing.T) {
		service, store, _, _ := initServiceWithData(t)

		// Create a NoGroup rule and attempt to change its group name
		groupSeed := createNoGroupRuleGroup("nogroup-multi", orgID, "my-namespace")
		store.Rules[orgID] = []*models.AlertRule{models.CopyRule(&groupSeed.Rules[0])}
		// change the group name away from the sentinel value
		groupSeed.Title = "some-other-group" // not the sentinel group name

		err := service.ReplaceRuleGroup(context.Background(), u, groupSeed, models.ProvenanceNone, "")
		require.Error(t, err)
		require.ErrorContains(t, err, "cannot move rule out of this group")
	})
}

func TestDeleteRuleGroup(t *testing.T) {
	orgID := rand.Int63()
	u := &user.SignedInUser{OrgID: orgID}
	groupKey := models.GenerateGroupKey(orgID)
	groupIntervalSeconds := int64(30)
	gen := models.RuleGen
	rules := gen.With(gen.WithGroupKey(groupKey), gen.WithIntervalSeconds(groupIntervalSeconds)).GenerateManyRef(3)
	groupProvenance := models.ProvenanceAPI

	initServiceWithData := func(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) {
		service, ruleStore, provenanceStore, ac := initService(t)
		ruleStore.Rules = map[int64][]*models.AlertRule{
			orgID: rules,
		}
		for _, rule := range rules {
			require.NoError(t, provenanceStore.SetProvenance(context.Background(), rule, orgID, groupProvenance))
		}
		return service, ruleStore, provenanceStore, ac
	}

	t.Run("when user can write all rules", func(t *testing.T) {
		service, ruleStore, _, ac := initServiceWithData(t)

		ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
			return true, nil
		}

		err := service.DeleteRuleGroup(context.Background(), u, groupKey.NamespaceUID, groupKey.RuleGroup, groupProvenance)
		require.NoError(t, err)

		require.Len(t, ac.Calls, 1)
		assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)

		deletes := getDeleteQueries(ruleStore)
		require.Len(t, deletes, 1)
	})
	t.Run("when user cannot write all rules", func(t *testing.T) {
		t.Run("it should not update if not authorized", func(t *testing.T) {
			service, ruleStore, _, ac := initServiceWithData(t)

			ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
				return false, nil
			}
			expectedErr := errors.New("test error")
			ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
				return expectedErr
			}

			err := service.DeleteRuleGroup(context.Background(), u, groupKey.NamespaceUID, groupKey.RuleGroup, groupProvenance)
			require.ErrorIs(t, err, expectedErr)

			require.Len(t, ac.Calls, 2)
			assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
			assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method)

			deletes := getDeleteQueries(ruleStore)
			require.Empty(t, deletes)
		})
		t.Run("it should update if authorized", func(t *testing.T) {
			service, ruleStore, _, ac := initServiceWithData(t)

			ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
				return false, nil
			}
			ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
				assert.Equal(t, u, user)
				assert.Equal(t, groupKey, change.GroupKey)
				assert.Contains(t, change.AffectedGroups, groupKey)
				assert.ElementsMatch(t, rules, change.AffectedGroups[groupKey])
				assert.Empty(t, change.Update)
				assert.Empty(t, change.New)
				assert.Len(t, change.Delete, len(rules))
				return nil
			}

			err := service.DeleteRuleGroup(context.Background(), u, groupKey.NamespaceUID, groupKey.RuleGroup, groupProvenance)
			require.NoError(t, err)

			require.Len(t, ac.Calls, 2)
			assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
			assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method)

			deletes := getDeleteQueries(ruleStore)
			require.Len(t, deletes, 1)
		})
	})
}

func TestDeleteRuleGroups(t *testing.T) {
	orgID1 := rand.Int63()
	orgID2 := rand.Int63()
	u := &user.SignedInUser{OrgID: orgID1, UserUID: "test-test"}

	// Create groups across different orgs and namespaces
	groupKey1 := models.AlertRuleGroupKey{
		OrgID:        orgID1,
		NamespaceUID: "namespace1",
		RuleGroup:    "group1",
	}
	groupKey2 := models.AlertRuleGroupKey{
		OrgID:        orgID1,
		NamespaceUID: "namespace2",
		RuleGroup:    "group2",
	}
	groupKey3 := models.AlertRuleGroupKey{
		OrgID:        orgID1,
		NamespaceUID: "namespace3",
		RuleGroup:    "group3",
	}
	groupKey4 := models.AlertRuleGroupKey{
		OrgID:        orgID2, // Different org
		NamespaceUID: "namespace1",
		RuleGroup:    "group1",
	}

	gen := models.RuleGen
	// Create rules for each group
	rules1 := gen.With(gen.WithGroupKey(groupKey1)).GenerateManyRef(2)
	rules2 := gen.With(gen.WithGroupKey(groupKey2)).GenerateManyRef(3)
	rules3 := gen.With(gen.WithGroupKey(groupKey3)).GenerateManyRef(2)
	rules4 := gen.With(gen.WithGroupKey(groupKey4)).GenerateManyRef(2)

	org1Rules := slices.Concat(rules1, rules2, rules3)
	org2Rules := rules4

	initServiceWithData := func(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) {
		service, ruleStore, provenanceStore, ac := initService(t)
		ruleStore.Rules = map[int64][]*models.AlertRule{
			orgID1: org1Rules,
			orgID2: org2Rules,
		}
		// Set provenance for all rules
		for _, rules := range []([]*models.AlertRule){org1Rules, org2Rules} {
			for _, rule := range rules {
				err := provenanceStore.SetProvenance(context.Background(), rule, rule.OrgID, models.ProvenanceAPI)
				require.NoError(t, err)
			}
		}
		return service, ruleStore, provenanceStore, ac
	}

	getUIDs := func(rules []*models.AlertRule) []string {
		uids := make([]string, 0, len(rules))
		for _, rule := range rules {
			uids = append(uids, rule.UID)
		}
		return uids
	}

	t.Run("when deleting specific groups", func(t *testing.T) {
		filterOpts := &FilterOptions{
			NamespaceUIDs: []string{"namespace1"},
			RuleGroups:    []string{"group1"},
		}

		t.Run("when user can write all rules", func(t *testing.T) {
			service, ruleStore, _, ac := initServiceWithData(t)
			ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
				return true, nil
			}

			err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
			require.NoError(t, err)

			require.Len(t, ac.Calls, 1)
			assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)

			// Verify only rules from group1 in org1 were deleted
			deletes := getDeletedRules(t, ruleStore)
			require.Len(t, deletes, 1)
			require.Equal(t, "test-test", deletes[0].userID)
			require.ElementsMatch(t, getUIDs(rules1), deletes[0].uids)
		})

		t.Run("when user cannot write all rules", func(t *testing.T) {
			t.Run("should not delete if not authorized", func(t *testing.T) {
				service, ruleStore, _, ac := initServiceWithData(t)
				ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
					return false, nil
				}
				expectedErr := errors.New("test error")
				ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
					return expectedErr
				}

				err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
				require.ErrorIs(t, err, expectedErr)

				require.Len(t, ac.Calls, 2)
				assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
				assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method)

				deletes := getDeletedRules(t, ruleStore)
				require.Empty(t, deletes)
			})

			t.Run("should delete group1 when authorized", func(t *testing.T) {
				service, ruleStore, _, ac := initServiceWithData(t)
				ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
					return false, nil
				}
				ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
					assert.Equal(t, u, user)
					assert.Equal(t, groupKey1, change.GroupKey)
					assert.ElementsMatch(t, rules1, change.AffectedGroups[groupKey1])
					assert.Empty(t, change.Update)
					assert.Empty(t, change.New)
					assert.Len(t, change.Delete, len(rules1))
					return nil
				}

				err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
				require.NoError(t, err)

				require.Len(t, ac.Calls, 2)
				assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
				assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method)

				deletes := getDeletedRules(t, ruleStore)
				require.Len(t, deletes, 1)
				require.ElementsMatch(t, getUIDs(rules1), deletes[0].uids)
			})
		})
	})

	t.Run("when deleting multiple groups from multiple namespaces", func(t *testing.T) {
		filterOpts := &FilterOptions{
			NamespaceUIDs: []string{"namespace1", "namespace2"},
			RuleGroups:    []string{"group1", "group2"},
		}

		t.Run("should delete all matching groups from correct org", func(t *testing.T) {
			service, ruleStore, _, ac := initServiceWithData(t)
			ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
				return true, nil
			}

			err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
			require.NoError(t, err)

			deletes := getDeletedRules(t, ruleStore)
			require.Len(t, deletes, 2)
			require.ElementsMatch(
				t,
				slices.Concat(getUIDs(rules1), getUIDs(rules2)),
				slices.Concat(deletes[0].uids, deletes[1].uids),
			)
		})
	})

	t.Run("when filtering by imported Prometheus rules", func(t *testing.T) {
		filterOpts := &FilterOptions{
			HasPrometheusRuleDefinition: util.Pointer(true),
			NamespaceUIDs:               []string{"namespace1"},
		}

		t.Run("when the group is not imported", func(t *testing.T) {
			filterOpts.RuleGroups = []string{groupKey1.RuleGroup}
			service, _, _, ac := initServiceWithData(t)
			ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
				return true, nil
			}

			err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
			require.ErrorIs(t, err, models.ErrAlertRuleGroupNotFound)
		})

		t.Run("when the group is imported", func(t *testing.T) {
			importedGroup := models.AlertRuleGroupKey{
				OrgID:        orgID1,
				NamespaceUID: "namespace1",
				RuleGroup:    "newgroup",
			}
			importedRules := gen.With(
				gen.WithGroupKey(importedGroup),
				gen.WithPrometheusOriginalRuleDefinition("something"),
			).GenerateManyRef(2)
			filterOpts.RuleGroups = []string{importedGroup.RuleGroup}
			service, ruleStore, _, ac := initServiceWithData(t)
			ruleStore.Rules[orgID1] = append(ruleStore.Rules[orgID1], importedRules...)
			ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
				return true, nil
			}

			err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
			require.NoError(t, err)
			deletes := getDeletedRules(t, ruleStore)
			require.Len(t, deletes, 1)
			require.ElementsMatch(t, getUIDs(importedRules), deletes[0].uids)
		})
	})

	t.Run("with no matching rule groups", func(t *testing.T) {
		filterOpts := &FilterOptions{
			NamespaceUIDs: []string{"non-existent"},
			RuleGroups:    []string{"non-existent"},
		}

		service, ruleStore, _, ac := initServiceWithData(t)
		ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
			return true, nil
		}

		err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
		require.ErrorIs(t, err, models.ErrAlertRuleGroupNotFound)

		deletes := getDeletedRules(t, ruleStore)
		require.Empty(t, deletes)
	})
}

func getDeleteQueries(ruleStore *fakes.RuleStore) []fakes.GenericRecordedQuery {
	generic := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
		a, ok := cmd.(fakes.GenericRecordedQuery)
		if !ok || a.Name != "DeleteAlertRulesByUID" {
			return nil, false
		}
		return a, ok
	})
	result := make([]fakes.GenericRecordedQuery, 0, len(generic))
	for _, g := range generic {
		result = append(result, g.(fakes.GenericRecordedQuery))
	}
	return result
}

type deleteRuleOperation struct {
	orgID  int64
	userID string
	uids   []string
}

func getDeletedRules(t *testing.T, ruleStore *fakes.RuleStore) []deleteRuleOperation {
	t.Helper()

	queries := getDeleteQueries(ruleStore)
	operations := make([]deleteRuleOperation, 0, len(queries))
	for _, q := range queries {
		orgID, ok := q.Params[0].(int64)
		require.True(t, ok, "orgID parameter should be int64")

		uid := ""
		userUID, ok := q.Params[1].(*models.UserUID)
		require.True(t, ok, "parameter should be UserUID")
		if userUID != nil {
			uid = string(*userUID)
		}

		uids, ok := q.Params[3].([]string)
		require.True(t, ok, "uids parameter should be []string")

		operations = append(operations, deleteRuleOperation{
			orgID:  orgID,
			userID: uid,
			uids:   uids,
		})
	}
	return operations
}

func createAlertRuleService(t *testing.T, folderService folder.Service) AlertRuleService {
	t.Helper()
	sqlStore := db.InitTestDB(t)
	store := store.DBstore{
		SQLStore: sqlStore,
		Cfg: setting.UnifiedAlertingSettings{
			BaseInterval: time.Second * 10,
		},
		Logger:         log.NewNopLogger(),
		FolderService:  folderService,
		Bus:            bus.ProvideBus(tracing.InitializeTracerForTest()),
		FeatureToggles: featuremgmt.WithFeatures(),
	}
	// store := fakes.NewRuleStore(t)
	quotas := MockQuotaChecker{}
	quotas.EXPECT().LimitOK()

	if folderService == nil {
		folderService = foldertest.NewFakeService()
	}

	return AlertRuleService{
		ruleStore:              store,
		provenanceStore:        store,
		quotas:                 &quotas,
		xact:                   sqlStore,
		log:                    log.New("testing"),
		baseIntervalSeconds:    10,
		defaultIntervalSeconds: 60,
		folderService:          folderService,
		authz:                  &fakeRuleAccessControlService{},
		nsValidatorProvider:    &NotificationSettingsValidatorProviderFake{},
	}
}

func dummyRule(title string, orgID int64) models.AlertRule {
	return createTestRule(title, "my-cool-group", orgID, "my-namespace")
}

func createNoGroupRuleGroup(title string, orgID int64, namespace string) models.AlertRuleGroup {
	uid := util.GenerateShortUID()
	group, err := models.NewNoGroupRuleGroup(uid)
	if err != nil {
		panic(fmt.Sprintf("failed to create NoGroupRuleGroup: %v", err))
	}

	return models.AlertRuleGroup{
		Title:     group.String(),
		Interval:  60,
		FolderUID: namespace,
		Rules: []models.AlertRule{
			createNoGroupRule(title, orgID, namespace),
		},
	}
}

func createNoGroupRule(title string, orgID int64, namespace string) models.AlertRule {
	uid := util.GenerateShortUID()
	group, err := models.NewNoGroupRuleGroup(uid)
	if err != nil {
		panic(fmt.Sprintf("failed to create NoGroupRuleGroup: %v", err))
	}

	return models.AlertRule{
		UID:             uid,
		OrgID:           orgID,
		Title:           title,
		Condition:       "A",
		Version:         1,
		IntervalSeconds: 60,
		Data: []models.AlertQuery{
			{
				RefID:         "A",
				Model:         json.RawMessage("{}"),
				DatasourceUID: expr.DatasourceUID,
				RelativeTimeRange: models.RelativeTimeRange{
					From: models.Duration(60),
					To:   models.Duration(0),
				},
			},
		},
		NamespaceUID: namespace,
		RuleGroup:    group.String(),
		For:          time.Second * 60,
		NoDataState:  models.OK,
		ExecErrState: models.OkErrState,
	}
}

func createTestRule(title string, groupTitle string, orgID int64, namespace string) models.AlertRule {
	return models.AlertRule{
		OrgID:           orgID,
		Title:           title,
		Condition:       "A",
		Version:         1,
		IntervalSeconds: 60,
		Data: []models.AlertQuery{
			{
				RefID:         "A",
				Model:         json.RawMessage("{}"),
				DatasourceUID: expr.DatasourceUID,
				RelativeTimeRange: models.RelativeTimeRange{
					From: models.Duration(60),
					To:   models.Duration(0),
				},
			},
		},
		NamespaceUID: namespace,
		RuleGroup:    groupTitle,
		For:          time.Second * 60,
		NoDataState:  models.OK,
		ExecErrState: models.OkErrState,
	}
}

func createDummyGroup(title string, orgID int64) models.AlertRuleGroup {
	return models.AlertRuleGroup{
		Title:     title,
		Interval:  60,
		FolderUID: "my-namespace",
		Rules: []models.AlertRule{
			dummyRule(title+"-"+"rule-1", orgID),
		},
	}
}

func initService(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) {
	t.Helper()

	ac := &fakeRuleAccessControlService{}
	ruleStore := fakes.NewRuleStore(t)
	provenanceStore := fakes.NewFakeProvisioningStore()
	folderService := foldertest.NewFakeService()

	quotas := MockQuotaChecker{}
	quotas.EXPECT().LimitOK()

	service := &AlertRuleService{
		folderService:          folderService,
		ruleStore:              ruleStore,
		provenanceStore:        provenanceStore,
		quotas:                 &quotas,
		xact:                   newNopTransactionManager(),
		log:                    log.New("testing"),
		baseIntervalSeconds:    10,
		defaultIntervalSeconds: 60,
		authz:                  ac,
		nsValidatorProvider:    &NotificationSettingsValidatorProviderFake{},
	}

	return service, ruleStore, provenanceStore, ac
}

// func TestNoGroupRuleGroupIntervalHandling(t *testing.T) {
// 	orgID := rand.Int63()
// 	u := &user.SignedInUser{OrgID: orgID, UserUID: util.GenerateShortUID()}

// 	t.Run("UpdateRuleGroup with NoGroupRuleGroup", func(t *testing.T) {
// 		t.Run("should allow interval updates for NoGroupRuleGroup via UpdateRuleGroup", func(t *testing.T) {
// 			service, store, _, ac := initService(t)
// 			ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
// 				return true, nil
// 			}

// 			// pre populate a rule with NoGroupRuleGroup
// 			rule := createNoGroupRule("test-rule", orgID, "my-namespace")
// 			store.Rules[orgID] = []*models.AlertRule{&rule} // Pre-populate the store with the rule

// 			// Update the rule with a new interval via UpdateRuleGroup
// 			newInterval := int64(120)
// 			createdRule, _, err := service.GetAlertRule(context.Background(), u, rule.UID)
// 			require.NoError(t, err)
// 			err = service.UpdateRuleGroup(context.Background(), u, createdRule.NamespaceUID, createdRule.RuleGroup, newInterval)
// 			require.NoError(t, err)
// 			updatedRule, _, err := service.GetAlertRule(context.Background(), u, createdRule.UID)
// 			require.NoError(t, err)
// 			assert.Equal(t, newInterval, updatedRule.IntervalSeconds, "Rule interval should be updated for NoGroupRuleGroup")

// 		})

// 		// t.Run("should preserve group interval for normal groups", func(t *testing.T) {
// 		// 	service, _, _, ac := initService(t)
// 		// 	ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
// 		// 		return true, nil
// 		// 	}

// 		// 	// Create a rule in a normal group
// 		// 	groupInterval := int64(90)
// 		// 	rule := createTestRule("test-rule", "normal-group", orgID, "my-namespace")
// 		// 	rule2 := createTestRule("test-rule-2", "normal-group", orgID, "my-namespace")
// 		// 	rule2.IntervalSeconds = groupInterval // Set the group interval
// 		// 	rule.IntervalSeconds = groupInterval
// 		// 	createdRule, err := service.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
// 		// 	require.NoError(t, err)
// 		// 	createdRule2, err := service.CreateAlertRule(context.Background(), u, rule2, models.ProvenanceNone)
// 		// 	require.NoError(t, err)

// 		// 	// Try to update the rule with a different interval
// 		// 	createdRule.IntervalSeconds = 120
// 		// 	updatedRule, err := service.UpdateAlertRule(context.Background(), u, createdRule, models.ProvenanceNone)
// 		// 	require.NoError(t, err)
// 		// 	assert.Equal(t, int64(120), updatedRule.IntervalSeconds, "Rule interval should be changed for all rules in normal groups")
// 		// 	updatedRule2, _, err := service.GetAlertRule(context.Background(), u, createdRule2.UID)
// 		// 	require.NoError(t, err)
// 		// 	assert.Equal(t, int64(120), updatedRule2.IntervalSeconds, "All rules in the same group should have the same interval after update")
// 		// })
// 	})

// 	t.Run("GetRuleGroup with a NoGroupRuleGroup", func(t *testing.T) {
// 		t.Run("should allow retrieval of sentinel group", func(t *testing.T) {
// 			service, store, _, ac := initService(t)
// 			ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
// 				return true, nil
// 			}

// 			// pre populate a rule with NoGroupRuleGroup
// 			rule := createNoGroupRule("test-rule", orgID, "my-namespace")
// 			store.Rules[orgID] = []*models.AlertRule{&rule} // Pre-populate the store with the rule

// 			noGroupGroup, err := service.GetRuleGroup(context.Background(), u, "my-namespace", rule.RuleGroup)
// 			require.NoError(t, err)
// 			assert.Equal(t, rule.RuleGroup, noGroupGroup.Title, "NoGroupRuleGroup should be retrievable by its group title")
// 			assert.Len(t, noGroupGroup.Rules, 1, "NoGroupRuleGroup should contain only the one rule")
// 		})
// 	})
// }
