package timeinterval

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

	"github.com/grafana/grafana-app-sdk/resource"
	"github.com/prometheus/alertmanager/config"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"k8s.io/apimachinery/pkg/api/errors"
	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"

	"github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v1beta1"
	"github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v1beta1/fakes"
	"github.com/grafana/grafana/pkg/bus"
	"github.com/grafana/grafana/pkg/infra/tracing"
	"github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/timeinterval"
	"github.com/grafana/grafana/pkg/services/accesscontrol"
	"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
	"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
	"github.com/grafana/grafana/pkg/services/dashboards"
	"github.com/grafana/grafana/pkg/services/folder/foldertest"
	"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
	ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
	"github.com/grafana/grafana/pkg/services/ngalert/store"
	"github.com/grafana/grafana/pkg/services/org"
	"github.com/grafana/grafana/pkg/tests/api/alerting"
	"github.com/grafana/grafana/pkg/tests/apis"
	"github.com/grafana/grafana/pkg/tests/apis/alerting/notifications/common"
	"github.com/grafana/grafana/pkg/tests/testinfra"
	"github.com/grafana/grafana/pkg/tests/testsuite"
	"github.com/grafana/grafana/pkg/util"
	"github.com/grafana/grafana/pkg/util/testutil"
)

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

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

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

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

	ctx := context.Background()
	helper := getTestHelper(t)
	client, err := v1beta1.NewTimeIntervalClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
	require.NoError(t, err)

	newInterval := &v1beta1.TimeInterval{
		ObjectMeta: v1.ObjectMeta{
			Namespace: "default",
		},
		Spec: v1beta1.TimeIntervalSpec{
			Name:          "time-newInterval",
			TimeIntervals: fakes.IntervalGenerator{}.GenerateMany(2),
		},
	}

	t.Run("create should fail if object name is specified", func(t *testing.T) {
		interval := newInterval.Copy().(*v1beta1.TimeInterval)
		interval.Name = "time-newInterval"
		_, err := client.Create(ctx, interval, resource.CreateOptions{})
		require.Truef(t, errors.IsBadRequest(err), "Expected BadRequest but got %s", err)
	})

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

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

	t.Run("update should rename interval if name in the specification changes", func(t *testing.T) {
		if existingInterval == nil {
			t.Skip()
		}
		updated := existingInterval.Copy().(*v1beta1.TimeInterval)
		updated.Spec.Name = "another-newInterval"
		actual, err := client.Update(ctx, updated, resource.UpdateOptions{})
		require.NoError(t, err)
		require.Equal(t, updated.Spec, actual.Spec)
		require.NotEqualf(t, updated.Name, actual.Name, "Update should change the resource name but it didn't")
		require.NotEqualf(t, updated.ResourceVersion, actual.ResourceVersion, "Update should change the resource version but it didn't")

		resource, err := client.Get(ctx, actual.GetStaticMetadata().Identifier())
		require.NoError(t, err)
		require.Equal(t, actual, resource)
	})
}

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

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

	org1 := helper.Org1

	type testCase struct {
		user      apis.User
		canRead   bool
		canUpdate bool
		canCreate bool
		canDelete bool
	}

	reader := helper.CreateUser("IntervalsReader", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
		{
			Actions: []string{
				accesscontrol.ActionAlertingNotificationsTimeIntervalsRead,
			},
		},
	})
	writer := helper.CreateUser("IntervalsWriter", "Org1", org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
		{
			Actions: []string{
				accesscontrol.ActionAlertingNotificationsTimeIntervalsRead,
				accesscontrol.ActionAlertingNotificationsTimeIntervalsWrite,
			},
		},
	})

	deleter := helper.CreateUser("IntervalsDeleter", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
		{
			Actions: []string{
				accesscontrol.ActionAlertingNotificationsTimeIntervalsRead,
				accesscontrol.ActionAlertingNotificationsTimeIntervalsDelete,
			},
		},
	})

	testCases := []testCase{
		{
			user:      org1.Admin,
			canRead:   true,
			canUpdate: true,
			canCreate: true,
			canDelete: true,
		},
		{
			user:      org1.Editor,
			canRead:   true,
			canUpdate: true,
			canCreate: true,
			canDelete: true,
		},
		{
			user:    org1.Viewer,
			canRead: true,
		},
		{
			user:    reader,
			canRead: true,
		},
		{
			user:      writer,
			canRead:   true,
			canCreate: true,
			canUpdate: true,
		},
		{
			user:      deleter,
			canRead:   true,
			canDelete: true,
		},
	}

	adminClient, err := v1beta1.NewTimeIntervalClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
	require.NoError(t, err)

	for _, tc := range testCases {
		t.Run(fmt.Sprintf("user '%s'", tc.user.Identity.GetLogin()), func(t *testing.T) {
			client, err := v1beta1.NewTimeIntervalClientFromGenerator(tc.user.GetClientRegistry())
			require.NoError(t, err)
			expected := &v1beta1.TimeInterval{
				ObjectMeta: v1.ObjectMeta{
					Namespace: "default",
				},
				Spec: v1beta1.TimeIntervalSpec{
					Name:          fmt.Sprintf("time-interval-1-%s", tc.user.Identity.GetLogin()),
					TimeIntervals: fakes.IntervalGenerator{}.GenerateMany(2),
				},
			}
			expected.SetProvenanceStatus("")
			d, err := json.Marshal(expected)
			require.NoError(t, err)

			if tc.canCreate {
				t.Run("should be able to create time interval", func(t *testing.T) {
					actual, err := client.Create(ctx, expected, resource.CreateOptions{})
					require.NoErrorf(t, err, "Payload %s", string(d))
					require.Equal(t, expected.Spec, actual.Spec)

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

					expected = actual
				})
			} else {
				t.Run("should be forbidden to create", func(t *testing.T) {
					_, err := client.Create(ctx, expected, resource.CreateOptions{})
					require.Truef(t, errors.IsForbidden(err), "Payload %s", string(d))
				})

				// create resource to proceed with other tests
				expected, err = adminClient.Create(ctx, expected, resource.CreateOptions{})
				require.NoErrorf(t, err, "Payload %s", string(d))
				require.NotNil(t, expected)
			}

			if tc.canRead {
				t.Run("should be able to list time intervals", func(t *testing.T) {
					list, err := client.List(ctx, apis.DefaultNamespace, resource.ListOptions{})
					require.NoError(t, err)
					require.Len(t, list.Items, 1)
				})

				t.Run("should be able to read time interval by resource identifier", func(t *testing.T) {
					got, err := client.Get(ctx, expected.GetStaticMetadata().Identifier())
					require.NoError(t, err)
					require.Equal(t, expected.Spec, got.Spec)

					t.Run("should get NotFound if resource does not exist", func(t *testing.T) {
						_, err := client.Get(ctx, resource.Identifier{Namespace: apis.DefaultNamespace, Name: "Notfound"})
						require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err)
					})
				})
			} else {
				t.Run("should be forbidden to list time intervals", func(t *testing.T) {
					_, err := client.List(ctx, apis.DefaultNamespace, resource.ListOptions{})
					require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
				})

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

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

			updatedExpected := expected.Copy().(*v1beta1.TimeInterval)
			updatedExpected.Spec.TimeIntervals = fakes.IntervalGenerator{}.GenerateMany(2)

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

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

					expected = updated

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

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

			deleteOptions := v1.DeleteOptions{Preconditions: &v1.Preconditions{ResourceVersion: util.Pointer(expected.ResourceVersion)}}
			oldClient := common.NewTimeIntervalClient(t, tc.user)
			if tc.canDelete {
				t.Run("should be able to delete time interval", func(t *testing.T) {
					err := oldClient.Delete(ctx, expected.GetStaticMetadata().Identifier().Name, deleteOptions)
					require.NoError(t, err)

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

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

			if tc.canRead {
				t.Run("should get empty list if no mute timings", func(t *testing.T) {
					list, err := client.List(ctx, apis.DefaultNamespace, resource.ListOptions{})
					require.NoError(t, err)
					require.Len(t, list.Items, 0)
				})
			}
		})
	}
}

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

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

	// writer has resource write actions but NO provenance set-status permission.
	writer := helper.CreateUser("IntervalsWriter", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
		{
			Actions: []string{
				accesscontrol.ActionAlertingNotificationsTimeIntervalsRead,
				accesscontrol.ActionAlertingNotificationsTimeIntervalsWrite,
				accesscontrol.ActionAlertingNotificationsTimeIntervalsDelete,
			},
		},
	})
	writerClient, err := v1beta1.NewTimeIntervalClientFromGenerator(writer.GetClientRegistry())
	require.NoError(t, err)

	// provisioner has the same write actions PLUS provenance set-status permission.
	provisioner := helper.CreateUser("IntervalsProvisioner", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
		{
			Actions: []string{
				accesscontrol.ActionAlertingNotificationsTimeIntervalsRead,
				accesscontrol.ActionAlertingNotificationsTimeIntervalsWrite,
				accesscontrol.ActionAlertingNotificationsTimeIntervalsDelete,
				accesscontrol.ActionAlertingProvisioningSetStatus,
			},
		},
	})
	provisionerClient, err := v1beta1.NewTimeIntervalClientFromGenerator(provisioner.GetClientRegistry())
	require.NoError(t, err)

	newInterval := func(name string) *v1beta1.TimeInterval {
		return &v1beta1.TimeInterval{
			ObjectMeta: v1.ObjectMeta{
				Namespace: "default",
			},
			Spec: v1beta1.TimeIntervalSpec{
				Name:          name,
				TimeIntervals: fakes.IntervalGenerator{}.GenerateMany(2),
			},
		}
	}

	t.Run("create", func(t *testing.T) {
		t.Run("writer can create without provenance", func(t *testing.T) {
			interval := newInterval("writer-create-no-prov")
			created, err := writerClient.Create(ctx, interval, resource.CreateOptions{})
			require.NoError(t, err)
			require.Empty(t, created.GetProvenanceStatus())
			require.NoError(t, writerClient.Delete(ctx, created.GetStaticMetadata().Identifier(), resource.DeleteOptions{}))
		})

		t.Run("writer cannot create with provenance set", func(t *testing.T) {
			interval := newInterval("writer-create-with-prov")
			interval.SetProvenanceStatus(string(ngmodels.ProvenanceAPI))
			_, err := writerClient.Create(ctx, interval, resource.CreateOptions{})
			require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
		})

		t.Run("provisioner can create with provenance", func(t *testing.T) {
			interval := newInterval("provisioner-create-with-prov")
			interval.SetProvenanceStatus(string(ngmodels.ProvenanceAPI))
			created, err := provisionerClient.Create(ctx, interval, resource.CreateOptions{})
			require.NoError(t, err)
			require.Equal(t, string(ngmodels.ProvenanceAPI), created.GetProvenanceStatus())
			require.NoError(t, provisionerClient.Delete(ctx, created.GetStaticMetadata().Identifier(), resource.DeleteOptions{}))
		})
	})

	t.Run("update", func(t *testing.T) {
		// Setup: one unprovisioned and one provisioned resource.
		unprov, err := writerClient.Create(ctx, newInterval("update-unprov"), resource.CreateOptions{})
		require.NoError(t, err)
		t.Cleanup(func() {
			_ = provisionerClient.Delete(ctx, unprov.GetStaticMetadata().Identifier(), resource.DeleteOptions{})
		})

		provInterval := newInterval("update-prov")
		provInterval.SetProvenanceStatus(string(ngmodels.ProvenanceAPI))
		prov, err := provisionerClient.Create(ctx, provInterval, resource.CreateOptions{})
		require.NoError(t, err)
		t.Cleanup(func() {
			_ = provisionerClient.Delete(ctx, prov.GetStaticMetadata().Identifier(), resource.DeleteOptions{})
		})

		t.Run("writer can update resource without provenance", func(t *testing.T) {
			current, err := writerClient.Get(ctx, unprov.GetStaticMetadata().Identifier())
			require.NoError(t, err)
			updated := current.Copy().(*v1beta1.TimeInterval)
			updated.Spec.TimeIntervals = fakes.IntervalGenerator{}.GenerateMany(2)
			_, err = writerClient.Update(ctx, updated, resource.UpdateOptions{})
			require.NoError(t, err)
		})

		t.Run("writer cannot set provenance on existing resource", func(t *testing.T) {
			current, err := writerClient.Get(ctx, unprov.GetStaticMetadata().Identifier())
			require.NoError(t, err)
			updated := current.Copy().(*v1beta1.TimeInterval)
			updated.SetProvenanceStatus(string(ngmodels.ProvenanceAPI))
			_, err = writerClient.Update(ctx, updated, resource.UpdateOptions{})
			require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
		})

		t.Run("writer cannot update provisioned resource", func(t *testing.T) {
			current, err := provisionerClient.Get(ctx, prov.GetStaticMetadata().Identifier())
			require.NoError(t, err)
			updated := current.Copy().(*v1beta1.TimeInterval)
			updated.Spec.TimeIntervals = fakes.IntervalGenerator{}.GenerateMany(2)
			_, err = writerClient.Update(ctx, updated, resource.UpdateOptions{})
			require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
		})

		t.Run("provisioner can set provenance on existing resource", func(t *testing.T) {
			current, err := provisionerClient.Get(ctx, unprov.GetStaticMetadata().Identifier())
			require.NoError(t, err)
			updated := current.Copy().(*v1beta1.TimeInterval)
			updated.SetProvenanceStatus(string(ngmodels.ProvenanceAPI))
			got, err := provisionerClient.Update(ctx, updated, resource.UpdateOptions{})
			require.NoError(t, err)
			require.Equal(t, string(ngmodels.ProvenanceAPI), got.GetProvenanceStatus())
			// Reset provenance for subsequent subtests.
			got.SetProvenanceStatus("")
			_, err = provisionerClient.Update(ctx, got, resource.UpdateOptions{})
			require.NoError(t, err)
		})

		t.Run("provisioner can update provisioned resource", func(t *testing.T) {
			current, err := provisionerClient.Get(ctx, prov.GetStaticMetadata().Identifier())
			require.NoError(t, err)
			updated := current.Copy().(*v1beta1.TimeInterval)
			updated.Spec.TimeIntervals = fakes.IntervalGenerator{}.GenerateMany(2)
			_, err = provisionerClient.Update(ctx, updated, resource.UpdateOptions{})
			require.NoError(t, err)
		})
	})

	t.Run("delete", func(t *testing.T) {
		t.Run("writer can delete resource without provenance", func(t *testing.T) {
			created, err := writerClient.Create(ctx, newInterval("delete-unprov"), resource.CreateOptions{})
			require.NoError(t, err)
			err = writerClient.Delete(ctx, created.GetStaticMetadata().Identifier(), resource.DeleteOptions{})
			require.NoError(t, err)
		})

		t.Run("writer cannot delete provisioned resource", func(t *testing.T) {
			interval := newInterval("delete-prov-forbidden")
			interval.SetProvenanceStatus(string(ngmodels.ProvenanceAPI))
			created, err := provisionerClient.Create(ctx, interval, resource.CreateOptions{})
			require.NoError(t, err)
			t.Cleanup(func() {
				_ = provisionerClient.Delete(ctx, created.GetStaticMetadata().Identifier(), resource.DeleteOptions{})
			})
			err = writerClient.Delete(ctx, created.GetStaticMetadata().Identifier(), resource.DeleteOptions{})
			require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
		})

		t.Run("provisioner can delete provisioned resource", func(t *testing.T) {
			interval := newInterval("delete-prov-ok")
			interval.SetProvenanceStatus(string(ngmodels.ProvenanceAPI))
			created, err := provisionerClient.Create(ctx, interval, resource.CreateOptions{})
			require.NoError(t, err)
			err = provisionerClient.Delete(ctx, created.GetStaticMetadata().Identifier(), resource.DeleteOptions{})
			require.NoError(t, err)
		})
	})
}

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

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

	adminClient, err := v1beta1.NewTimeIntervalClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
	require.NoError(t, err)
	oldClient := common.NewTimeIntervalClient(t, helper.Org1.Admin)

	interval := v1beta1.TimeInterval{
		ObjectMeta: v1.ObjectMeta{
			Namespace: "default",
		},
		Spec: v1beta1.TimeIntervalSpec{
			Name:          "time-interval",
			TimeIntervals: fakes.IntervalGenerator{}.GenerateMany(2),
		},
	}

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

	t.Run("should forbid if version does not match", func(t *testing.T) {
		updated := created.Copy().(*v1beta1.TimeInterval)
		_, err := adminClient.Update(ctx, updated, resource.UpdateOptions{
			ResourceVersion: "test",
		})
		require.Truef(t, errors.IsConflict(err), "should get Forbidden error but got %s", err)
	})
	t.Run("should update if version matches", func(t *testing.T) {
		updated := created.Copy().(*v1beta1.TimeInterval)
		updated.Spec.TimeIntervals = fakes.IntervalGenerator{}.GenerateMany(2)
		actualUpdated, err := adminClient.Update(ctx, updated, resource.UpdateOptions{})
		require.NoError(t, err)
		require.EqualValues(t, updated.Spec, actualUpdated.Spec)
		require.NotEqual(t, updated.ResourceVersion, actualUpdated.ResourceVersion)
	})
	t.Run("should update if version is empty", func(t *testing.T) {
		updated := created.Copy().(*v1beta1.TimeInterval)
		updated.ResourceVersion = ""
		updated.Spec.TimeIntervals = fakes.IntervalGenerator{}.GenerateMany(2)

		actualUpdated, err := adminClient.Update(ctx, updated, resource.UpdateOptions{})
		require.NoError(t, err)
		require.EqualValues(t, updated.Spec, actualUpdated.Spec)
		require.NotEqual(t, created.ResourceVersion, actualUpdated.ResourceVersion)
	})
	t.Run("should fail to delete if version does not match", func(t *testing.T) {
		actual, err := adminClient.Get(ctx, created.GetStaticMetadata().Identifier())
		require.NoError(t, err)

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

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

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

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

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

	adminClient, err := v1beta1.NewTimeIntervalClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
	require.NoError(t, err)
	oldClient := common.NewTimeIntervalClient(t, helper.Org1.Admin)

	interval := v1beta1.TimeInterval{
		ObjectMeta: v1.ObjectMeta{
			Namespace: "default",
		},
		Spec: v1beta1.TimeIntervalSpec{
			Name:          "time-interval",
			TimeIntervals: fakes.IntervalGenerator{}.GenerateMany(2),
		},
	}

	current, err := adminClient.Create(ctx, &interval, resource.CreateOptions{})
	require.NoError(t, err)
	require.NotNil(t, current)
	require.NotEmpty(t, current.ResourceVersion)

	t.Run("should patch with merge patch", func(t *testing.T) {
		patch := `{
             "spec": {
                 "time_intervals" : []
             }
		 }`

		result, err := oldClient.Patch(ctx, current.GetStaticMetadata().Identifier().Name, types.MergePatchType, []byte(patch), v1.PatchOptions{})
		require.NoError(t, err)
		require.Empty(t, result.Spec.TimeIntervals)
		current = result
	})

	t.Run("should patch with json patch", func(t *testing.T) {
		expected := fakes.IntervalGenerator{}.Generate()

		patch := []resource.PatchOperation{
			{
				Operation: "add",
				Path:      "/spec/time_intervals/-",
				Value:     expected,
			},
		}

		result, err := adminClient.Patch(ctx, current.GetStaticMetadata().Identifier(), resource.PatchRequest{Operations: patch}, resource.PatchOptions{})
		require.NoError(t, err)
		expectedSpec := v1beta1.TimeIntervalSpec{
			Name: current.Spec.Name,
			TimeIntervals: []v1beta1.TimeIntervalInterval{
				expected,
			},
		}
		require.EqualValues(t, expectedSpec, result.Spec)
		current = result
	})
}

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

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

	adminClient, err := v1beta1.NewTimeIntervalClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
	require.NoError(t, err)

	interval1 := &v1beta1.TimeInterval{
		ObjectMeta: v1.ObjectMeta{
			Namespace: "default",
		},
		Spec: v1beta1.TimeIntervalSpec{
			Name:          "test1",
			TimeIntervals: fakes.IntervalGenerator{}.GenerateMany(2),
		},
	}
	interval1, err = adminClient.Create(ctx, interval1, resource.CreateOptions{})
	require.NoError(t, err)

	interval2 := &v1beta1.TimeInterval{
		ObjectMeta: v1.ObjectMeta{
			Namespace: "default",
		},
		Spec: v1beta1.TimeIntervalSpec{
			Name:          "test2",
			TimeIntervals: fakes.IntervalGenerator{}.GenerateMany(2),
		},
	}
	interval2, err = adminClient.Create(ctx, interval2, resource.CreateOptions{})
	require.NoError(t, err)
	env := helper.GetEnv()
	ac := acimpl.ProvideAccessControl(env.FeatureToggles)
	db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, bus.ProvideBus(tracing.InitializeTracerForTest()))
	require.NoError(t, err)
	require.NoError(t, db.SetProvenance(ctx, &definitions.MuteTimeInterval{
		MuteTimeInterval: config.MuteTimeInterval{
			Name: interval2.Spec.Name,
		},
	}, helper.Org1.Admin.Identity.GetOrgID(), "API"))
	interval2, err = adminClient.Get(ctx, interval2.GetStaticMetadata().Identifier())

	require.NoError(t, err)

	intervals, err := adminClient.List(ctx, apis.DefaultNamespace, resource.ListOptions{})
	require.NoError(t, err)
	require.Len(t, intervals.Items, 2)

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

	t.Run("should filter by interval metadata name", func(t *testing.T) {
		list, err := adminClient.List(ctx, apis.DefaultNamespace, resource.ListOptions{
			FieldSelectors: []string{"metadata.name=" + interval2.Name},
		})
		require.NoError(t, err)
		require.Len(t, list.Items, 1)
		require.Equal(t, interval2.Name, list.Items[0].Name)
	})

	t.Run("should filter by multiple filters", func(t *testing.T) {
		t.Skip("disabled until app installer supports it")
		list, err := adminClient.List(ctx, apis.DefaultNamespace, resource.ListOptions{
			FieldSelectors: []string{fmt.Sprintf("metadata.name=%s", interval2.Name), fmt.Sprintf("spec.name=%s", interval2.Spec.Name)},
		})
		require.NoError(t, err)
		require.Len(t, list.Items, 1)
		require.Equal(t, interval2.Name, list.Items[0].Name)
	})

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

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

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

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

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

	mtis := make([]definitions.MuteTimeInterval, 0, len(amConfig.AlertmanagerConfig.MuteTimeIntervals))
	for _, interval := range amConfig.AlertmanagerConfig.MuteTimeIntervals {
		mtis = append(mtis, definitions.MuteTimeInterval{
			MuteTimeInterval: interval,
		})
	}

	adminClient, err := v1beta1.NewTimeIntervalClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
	require.NoError(t, err)
	v1intervals, err := timeinterval.ConvertToK8sResources(orgID, mtis, func(int64) string { return "default" }, nil)
	require.NoError(t, err)
	for _, interval := range v1intervals.Items {
		_, err := adminClient.Create(ctx, &interval, resource.CreateOptions{})
		require.NoError(t, err)
	}

	common.UpdateDefaultRoute(t, helper.Org1.Admin, amConfig.AlertmanagerConfig.Route)

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

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

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

	intervals, err := adminClient.List(ctx, apis.DefaultNamespace, resource.ListOptions{})
	require.NoError(t, err)
	require.Len(t, intervals.Items, 3)
	intervalIdx := slices.IndexFunc(intervals.Items, func(interval v1beta1.TimeInterval) bool {
		return interval.Spec.Name == "test-interval"
	})
	interval := intervals.Items[intervalIdx]

	replace := func(input []string, oldName, newName string) []string {
		result := make([]string, 0, len(input))
		for _, s := range input {
			if s == oldName {
				result = append(result, newName)
				continue
			}
			result = append(result, s)
		}
		return result
	}

	t.Run("Update", func(t *testing.T) {
		t.Run("should rename all references if name changes", func(t *testing.T) {
			renamed := interval.Copy().(*v1beta1.TimeInterval)
			renamed.Spec.Name += "-new"

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

			updatedRuleGroup, status := legacyCli.GetRulesGroup(t, folderUID, ruleGroup.Name)
			require.Equal(t, http.StatusAccepted, status)
			for idx, rule := range updatedRuleGroup.Rules {
				expectedTimeIntervals := currentRuleGroup.Rules[idx].GrafanaManagedAlert.NotificationSettings.MuteTimeIntervals
				expectedTimeIntervals = replace(expectedTimeIntervals, interval.Spec.Name, actual.Spec.Name)
				assert.Equalf(t, expectedTimeIntervals, rule.GrafanaManagedAlert.NotificationSettings.MuteTimeIntervals, "time interval in rules should have been renamed but it did not")
			}

			updatedRoute := legacyCli.GetRoute(t)
			for idx, route := range updatedRoute.Routes {
				expectedTimeIntervals := replace(currentRoute.Routes[idx].MuteTimeIntervals, interval.Spec.Name, actual.Spec.Name)
				assert.Equalf(t, expectedTimeIntervals, route.MuteTimeIntervals, "mute time interval in routes should have been renamed but it did not")
				expectedTimeIntervals = replace(currentRoute.Routes[idx].ActiveTimeIntervals, interval.Spec.Name, actual.Spec.Name)
				assert.Equalf(t, expectedTimeIntervals, route.ActiveTimeIntervals, "active time interval in routes should have been renamed but it did not")
			}

			interval = *actual
		})

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

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

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

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

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

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

			err = adminClient.Delete(ctx, interval.GetStaticMetadata().Identifier(), resource.DeleteOptions{})
			require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
		})

		t.Run("should fail to delete if time interval is used in route as an active time interval", func(t *testing.T) {
			idx := slices.IndexFunc(intervals.Items, func(interval v1beta1.TimeInterval) bool {
				return interval.Spec.Name == "test-interval-for-active-time-interval"
			})
			intervalToDelete := intervals.Items[idx]

			err = adminClient.Delete(ctx, intervalToDelete.GetStaticMetadata().Identifier(), resource.DeleteOptions{})
			require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
		})
	})
}

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

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

	adminClient, err := v1beta1.NewTimeIntervalClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
	require.NoError(t, err)

	testCases := []struct {
		name     string
		interval v1beta1.TimeIntervalSpec
	}{
		{
			name: "missing name",
			interval: v1beta1.TimeIntervalSpec{
				Name:          "",
				TimeIntervals: fakes.IntervalGenerator{}.GenerateMany(1),
			},
		},
		{
			name: "invalid interval",
			interval: v1beta1.TimeIntervalSpec{
				Name: "test",
				TimeIntervals: []v1beta1.TimeIntervalInterval{
					{
						DaysOfMonth: []string{"1-31"},
					},
				},
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			i := &v1beta1.TimeInterval{
				ObjectMeta: v1.ObjectMeta{
					Namespace: "default",
				},
				Spec: tc.interval,
			}
			_, err := adminClient.Create(ctx, i, resource.CreateOptions{})
			require.Error(t, err)
			require.Truef(t, errors.IsBadRequest(err), "Expected BadRequest, got: %s", err)
		})
	}
}
