package service

import (
	"context"
	"fmt"
	"reflect"
	"slices"
	"testing"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	"gopkg.in/ini.v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apiserver/pkg/endpoints/request"

	dashboardv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
	"github.com/grafana/grafana/pkg/apimachinery/identity"
	"github.com/grafana/grafana/pkg/apimachinery/utils"
	"github.com/grafana/grafana/pkg/components/simplejson"
	"github.com/grafana/grafana/pkg/infra/kvstore"
	"github.com/grafana/grafana/pkg/infra/log"
	"github.com/grafana/grafana/pkg/infra/serverlock"
	"github.com/grafana/grafana/pkg/infra/tracing"
	"github.com/grafana/grafana/pkg/services/accesscontrol"
	"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
	acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
	"github.com/grafana/grafana/pkg/services/apiserver/client"
	"github.com/grafana/grafana/pkg/services/dashboards"
	"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
	"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/org"
	"github.com/grafana/grafana/pkg/services/org/orgtest"
	"github.com/grafana/grafana/pkg/services/publicdashboards"
	"github.com/grafana/grafana/pkg/services/quota"
	"github.com/grafana/grafana/pkg/services/search/model"
	"github.com/grafana/grafana/pkg/services/search/sort"
	"github.com/grafana/grafana/pkg/services/sqlstore"
	"github.com/grafana/grafana/pkg/services/user"
	"github.com/grafana/grafana/pkg/setting"
	"github.com/grafana/grafana/pkg/storage/unified/resource"
	"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
	"github.com/grafana/grafana/pkg/storage/unified/search/builders"
	"github.com/grafana/grafana/pkg/tests/testsuite"
	"github.com/grafana/grafana/pkg/util/testutil"
)

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

func TestDashboardServiceValidation(t *testing.T) {
	fakePublicDashboardService := publicdashboards.NewFakePublicDashboardServiceWrapper(t)

	service := &DashboardServiceImpl{
		cfg:                    setting.NewCfg(),
		log:                    log.New("test.logger"),
		folderService:          foldertest.NewFakeService(),
		ac:                     actest.FakeAccessControl{ExpectedEvaluate: true},
		features:               featuremgmt.WithFeatures(),
		publicDashboardService: fakePublicDashboardService,
	}

	ctx, k8sCliMock := setupK8sDashboardTests(service)
	k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
	k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil)

	t.Run("Save dashboard validation", func(t *testing.T) {
		dto := &dashboards.SaveDashboardDTO{}

		t.Run("When saving a dashboard with empty title it should return error", func(t *testing.T) {
			titles := []string{"", " ", "   \t   "}

			for _, title := range titles {
				dto.Dashboard = dashboards.NewDashboard(title)
				_, err := service.SaveDashboard(ctx, dto, false)
				require.Equal(t, err, dashboards.ErrDashboardTitleEmpty)
			}
		})

		t.Run("Should return validation error if message is too long", func(t *testing.T) {
			dto.Dashboard = dashboards.NewDashboard("Dash")
			dto.Message = `Here we go, 500+ characters for testing. I'm sorry that you're
			having to read this. I spent too long trying to come up with something clever
			to say or a funny joke. Unforuntately, nothing came to mind. So instead, I'm
			will share this with you, as a form of payment for having to read this:
			https://youtu.be/dQw4w9WgXcQ?si=KeoTIpn9tUtQnOBk! Enjoy :) Now lets see if
			this test passes or if the result is more exciting than these 500 characters
			I wrote. Best of luck to the both of us!!`
			_, err := service.SaveDashboard(ctx, dto, false)
			require.Equal(t, err, dashboards.ErrDashboardMessageTooLong)

			// set to a shorter message for the rest of the tests
			dto.Message = `message`
		})

		t.Run("When saving a dashboard should validate uid", func(t *testing.T) {
			testCases := []struct {
				Uid   string
				Error error
			}{
				{Uid: "", Error: nil},
				{Uid: "   ", Error: nil},
				{Uid: "  \t  ", Error: nil},
				{Uid: "asdf90_-", Error: nil},
				{Uid: "asdf/90", Error: dashboards.ErrDashboardInvalidUid},
				{Uid: "   asdfghjklqwertyuiopzxcvbnmasdfghjklqwer   ", Error: nil},
				{Uid: "asdfghjklqwertyuiopzxcvbnmasdfghjklqwertyuiopzxcvbnmasdfghjklqwertyuiopzxcvbnm", Error: dashboards.ErrDashboardUidTooLong},
			}

			for _, tc := range testCases {
				dto.Dashboard = dashboards.NewDashboard("title")
				dto.Dashboard.SetUID(tc.Uid)
				dto.User = &user.SignedInUser{}

				if tc.Error == nil {
					k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil).Once()
				}
				_, err := service.BuildSaveDashboardCommand(ctx, dto, false)
				require.Equal(t, err, tc.Error)
			}
		})

		t.Run("Should return validation error if a folder that is specified can't be found", func(t *testing.T) {
			dto.Dashboard = dashboards.NewDashboard("Dash")
			dto.Dashboard.FolderUID = "non-existing-folder"
			folderSvc := foldertest.FakeService{ExpectedError: dashboards.ErrFolderNotFound}
			service.folderService = &folderSvc
			_, err := service.SaveDashboard(ctx, dto, false)
			require.Equal(t, err, dashboards.ErrFolderNotFound)
		})
	})
}

func setupK8sDashboardTests(service *DashboardServiceImpl) (context.Context, *client.MockK8sHandler) {
	mockCli := new(client.MockK8sHandler)
	service.k8sclient = mockCli

	ctx := context.Background()
	userCtx := &user.SignedInUser{UserID: 1, OrgID: 1}
	ctx = identity.WithRequester(ctx, userCtx)

	return ctx, mockCli
}

func TestGetDashboard(t *testing.T) {
	service := &DashboardServiceImpl{
		cfg: setting.NewCfg(),
	}
	query := &dashboards.GetDashboardQuery{
		UID:   "test-uid",
		OrgID: 1,
	}

	t.Run("Should get dashboard", func(t *testing.T) {
		ctx, k8sCliMock := setupK8sDashboardTests(service)
		dashboardUnstructured := unstructured.Unstructured{Object: map[string]any{
			"metadata": map[string]any{
				"name":       "uid",
				"generation": int64(1),
			},
			"spec": map[string]any{
				"test":  "test",
				"title": "testing slugify",
			},
		}}

		dashboardExpected := dashboards.Dashboard{
			UID:     "uid", // uid is the name of the k8s object
			Title:   "testing slugify",
			Slug:    "testing-slugify", // slug is taken from title
			OrgID:   1,                 // orgID is populated from the query
			Version: 1,
			Data:    simplejson.NewFromAny(map[string]any{"test": "test", "title": "testing slugify", "uid": "uid", "version": int64(1)}),
		}
		k8sCliMock.On("Get", mock.Anything, query.UID, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil).Once()
		k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil)

		dashboard, err := service.GetDashboard(ctx, query)
		require.NoError(t, err)
		require.NotNil(t, dashboard)
		k8sCliMock.AssertExpectations(t)
		// make sure the conversion is working
		require.True(t, reflect.DeepEqual(dashboard, &dashboardExpected))
	})

	t.Run("Should get uid if not passed in at first", func(t *testing.T) {
		query := &dashboards.GetDashboardQuery{
			ID:    1,
			UID:   "",
			OrgID: 1,
		}
		ctx, k8sCliMock := setupK8sDashboardTests(service)
		k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
		dashboardUnstructured := unstructured.Unstructured{Object: map[string]any{
			"metadata": map[string]any{
				"name":       "uid",
				"generation": int64(2),
			},
			"spec": map[string]any{
				"test":  "test",
				"title": "testing slugify",
			},
		}}

		dashboardExpected := dashboards.Dashboard{
			UID:     "uid", // uid is the name of the k8s object
			Title:   "testing slugify",
			Slug:    "testing-slugify", // slug is taken from title
			OrgID:   1,                 // orgID is populated from the query
			Version: 2,
			Data:    simplejson.NewFromAny(map[string]any{"test": "test", "title": "testing slugify", "uid": "uid", "version": int64(2)}),
		}
		k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil).Once()
		k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil)
		k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resourcepb.ResourceSearchResponse{
			Results: &resourcepb.ResourceTable{
				Columns: []*resourcepb.ResourceTableColumnDefinition{
					{
						Name: "title",
						Type: resourcepb.ResourceTableColumnDefinition_STRING,
					},
					{
						Name: "folder",
						Type: resourcepb.ResourceTableColumnDefinition_STRING,
					},
				},
				Rows: []*resourcepb.ResourceTableRow{
					{
						Key: &resourcepb.ResourceKey{
							Name:     "uid",
							Resource: "dashboard",
						},
						Cells: [][]byte{
							[]byte("Dashboard 1"),
							[]byte("folder1"),
						},
					},
				},
			},
			TotalHits: 1,
		}, nil)

		dashboard, err := service.GetDashboard(ctx, query)
		require.NoError(t, err)
		require.NotNil(t, dashboard)
		k8sCliMock.AssertExpectations(t)
		k8sCliMock.AssertExpectations(t)
		// make sure the conversion is working
		require.True(t, reflect.DeepEqual(dashboard, &dashboardExpected))
	})

	t.Run("Should return error when Kubernetes client fails", func(t *testing.T) {
		ctx, k8sCliMock := setupK8sDashboardTests(service)
		k8sCliMock.On("Get", mock.Anything, query.UID, mock.Anything, mock.Anything, mock.Anything).Return(nil, assert.AnError).Once()

		dashboard, err := service.GetDashboard(ctx, query)
		require.Error(t, err)
		require.Nil(t, dashboard)
		k8sCliMock.AssertExpectations(t)
	})

	t.Run("Should return dashboard not found if Kubernetes client returns nil", func(t *testing.T) {
		ctx, k8sCliMock := setupK8sDashboardTests(service)
		k8sCliMock.On("Get", mock.Anything, query.UID, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil).Once()
		dashboard, err := service.GetDashboard(ctx, query)
		require.Error(t, err)
		require.Equal(t, dashboards.ErrDashboardNotFound, err)
		require.Nil(t, dashboard)
		k8sCliMock.AssertExpectations(t)
	})
}

func TestGetAllDashboards(t *testing.T) {
	service := &DashboardServiceImpl{
		cfg: setting.NewCfg(),
	}
	ctx, k8sCliMock := setupK8sDashboardTests(service)

	dashboardUnstructured := unstructured.Unstructured{Object: map[string]any{
		"metadata": map[string]any{
			"name":       "uid",
			"generation": int64(1),
		},
		"spec": map[string]any{
			"test":  "test",
			"title": "testing slugify",
		},
	}}

	dashboardExpected := dashboards.Dashboard{
		UID:     "uid", // uid is the name of the k8s object
		Title:   "testing slugify",
		Slug:    "testing-slugify", // slug is taken from title
		OrgID:   1,                 // orgID is populated from the query
		Version: 1,                 // default to version 1
		Data:    simplejson.NewFromAny(map[string]any{"test": "test", "title": "testing slugify", "uid": "uid", "version": int64(1)}),
	}

	k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil)
	k8sCliMock.On("List", mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.UnstructuredList{Items: []unstructured.Unstructured{dashboardUnstructured}}, nil).Once()

	dashes, err := service.GetAllDashboardsByOrgId(ctx, 1)
	require.NoError(t, err)
	require.NotNil(t, dashes)
	k8sCliMock.AssertExpectations(t)
	// make sure the conversion is working
	require.True(t, reflect.DeepEqual(dashes, []*dashboards.Dashboard{&dashboardExpected}))
}

func TestGetAllDashboardsByOrgId(t *testing.T) {
	service := &DashboardServiceImpl{
		cfg: setting.NewCfg(),
	}
	ctx, k8sCliMock := setupK8sDashboardTests(service)

	dashboardUnstructured := unstructured.Unstructured{Object: map[string]any{
		"metadata": map[string]any{
			"name":       "uid",
			"generation": int64(1),
		},
		"spec": map[string]any{
			"test":  "test",
			"title": "testing slugify",
		},
	}}

	dashboardExpected := dashboards.Dashboard{
		UID:     "uid", // uid is the name of the k8s object
		Title:   "testing slugify",
		Slug:    "testing-slugify", // slug is taken from title
		OrgID:   1,                 // orgID is populated from the query
		Version: 1,                 // default to version 1
		Data:    simplejson.NewFromAny(map[string]any{"test": "test", "title": "testing slugify", "uid": "uid", "version": int64(1)}),
	}

	k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil)
	k8sCliMock.On("List", mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.UnstructuredList{Items: []unstructured.Unstructured{dashboardUnstructured}}, nil).Once()

	dashes, err := service.GetAllDashboardsByOrgId(ctx, 1)
	require.NoError(t, err)
	require.NotNil(t, dashes)
	k8sCliMock.AssertExpectations(t)
	// make sure the conversion is working
	require.True(t, reflect.DeepEqual(dashes, []*dashboards.Dashboard{&dashboardExpected}))
}

func TestGetProvisionedDashboardData(t *testing.T) {
	service := &DashboardServiceImpl{
		cfg: setting.NewCfg(),
		orgService: &orgtest.FakeOrgService{
			ExpectedOrgs: []*org.OrgDTO{{ID: 1}, {ID: 2}},
		},
	}

	ctx, k8sCliMock := setupK8sDashboardTests(service)
	provisioningTimestamp := int64(1234567)
	k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
	repo := "test"
	k8sCliMock.On("Search", mock.Anything, int64(1),
		mock.MatchedBy(func(req *resourcepb.ResourceSearchRequest) bool {
			// make sure the kind is added to the query
			return req.Options.Fields[0].Values[0] == string(utils.ManagerKindClassicFP) && // nolint:staticcheck
				req.Options.Fields[1].Values[0] == repo
		})).Return(&resourcepb.ResourceSearchResponse{
		Results: &resourcepb.ResourceTable{
			Columns: []*resourcepb.ResourceTableColumnDefinition{},
			Rows:    []*resourcepb.ResourceTableRow{},
		},
		TotalHits: 0,
	}, nil).Once()
	k8sCliMock.On("Search", mock.Anything, int64(2), mock.MatchedBy(func(req *resourcepb.ResourceSearchRequest) bool {
		// make sure the kind is added to the query
		return req.Options.Fields[0].Values[0] == string(utils.ManagerKindClassicFP) && // nolint:staticcheck
			req.Options.Fields[1].Values[0] == repo
	})).Return(&resourcepb.ResourceSearchResponse{
		Results: &resourcepb.ResourceTable{
			Columns: []*resourcepb.ResourceTableColumnDefinition{
				{
					Name: "title",
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
				{
					Name: "folder",
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
				{
					Name: resource.SEARCH_FIELD_LEGACY_ID,
					Type: resourcepb.ResourceTableColumnDefinition_INT64,
				},
				{
					Name: resource.SEARCH_FIELD_MANAGER_KIND, // nolint:staticcheck
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
				{
					Name: resource.SEARCH_FIELD_MANAGER_ID,
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
				{
					Name: resource.SEARCH_FIELD_SOURCE_PATH,
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
				{
					Name: resource.SEARCH_FIELD_SOURCE_CHECKSUM,
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
				{
					Name: resource.SEARCH_FIELD_SOURCE_TIME,
					Type: resourcepb.ResourceTableColumnDefinition_INT64,
				},
			},
			Rows: []*resourcepb.ResourceTableRow{
				{
					Key: &resourcepb.ResourceKey{
						Name:     "uid",
						Resource: "dashboard",
					},
					Cells: [][]byte{
						[]byte("Dashboard 1"),
						[]byte("folder 1"),
						[]byte("1"),
						[]byte(string(utils.ManagerKindClassicFP)), // nolint:staticcheck
						[]byte(repo),
						[]byte("path/to/file"),
						[]byte("hash"),
						[]byte("1234567"),
					},
				},
			},
		},
		TotalHits: 1,
	}, nil).Once()
	dashes, err := service.GetProvisionedDashboardData(ctx, repo)
	require.NoError(t, err)
	require.Len(t, dashes, 1)
	require.Equal(t, dashes[0], &dashboards.DashboardProvisioning{
		ID:          0,
		DashboardID: 1,
		Name:        "test",
		ExternalID:  "path/to/file",
		CheckSum:    "hash",
		Updated:     provisioningTimestamp,
	})
	k8sCliMock.AssertExpectations(t)
}

func TestGetProvisionedDashboardDataByDashboardID(t *testing.T) {
	service := &DashboardServiceImpl{
		cfg: setting.NewCfg(),
		orgService: &orgtest.FakeOrgService{
			ExpectedOrgs: []*org.OrgDTO{{ID: 1}, {ID: 2}},
		},
	}

	ctx, k8sCliMock := setupK8sDashboardTests(service)
	provisioningTimestamp := int64(1234567)
	k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
	k8sCliMock.On("Search", mock.Anything, int64(1), mock.Anything).Return(&resourcepb.ResourceSearchResponse{
		Results: &resourcepb.ResourceTable{
			Columns: []*resourcepb.ResourceTableColumnDefinition{},
			Rows:    []*resourcepb.ResourceTableRow{},
		},
		TotalHits: 0,
	}, nil)
	k8sCliMock.On("Search", mock.Anything, int64(2), mock.Anything).Return(&resourcepb.ResourceSearchResponse{
		Results: &resourcepb.ResourceTable{
			Columns: []*resourcepb.ResourceTableColumnDefinition{
				{
					Name: "title",
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
				{
					Name: "folder",
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
				{
					Name: resource.SEARCH_FIELD_LEGACY_ID,
					Type: resourcepb.ResourceTableColumnDefinition_INT64,
				},
				{
					Name: resource.SEARCH_FIELD_MANAGER_KIND, // nolint:staticcheck
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
				{
					Name: resource.SEARCH_FIELD_MANAGER_ID,
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
				{
					Name: resource.SEARCH_FIELD_SOURCE_PATH,
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
				{
					Name: resource.SEARCH_FIELD_SOURCE_CHECKSUM,
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
				{
					Name: resource.SEARCH_FIELD_SOURCE_TIME,
					Type: resourcepb.ResourceTableColumnDefinition_INT64,
				},
			},
			Rows: []*resourcepb.ResourceTableRow{
				{
					Key: &resourcepb.ResourceKey{
						Name:     "uid",
						Resource: "dashboard",
					},
					Cells: [][]byte{
						[]byte("Dashboard 1"),
						[]byte("folder 1"),
						[]byte("1"),
						[]byte(string(utils.ManagerKindClassicFP)), // nolint:staticcheck
						[]byte("test"),
						[]byte("path/to/file"),
						[]byte("hash"),
						[]byte("1234567"),
					},
				},
			},
		},
		TotalHits: 1,
	}, nil)
	dash, err := service.GetProvisionedDashboardDataByDashboardID(ctx, 1)
	require.NoError(t, err)
	require.Equal(t, dash, &dashboards.DashboardProvisioning{
		ID:          0,
		DashboardID: 1,
		Name:        "test",
		ExternalID:  "path/to/file",
		CheckSum:    "hash",
		Updated:     provisioningTimestamp,
	})
	k8sCliMock.AssertExpectations(t)
}

func TestGetProvisionedDashboardDataByDashboardUID(t *testing.T) {
	service := &DashboardServiceImpl{
		cfg: setting.NewCfg(),
		orgService: &orgtest.FakeOrgService{
			ExpectedOrgs: []*org.OrgDTO{{ID: 1}, {ID: 2}},
		},
	}
	ctx, k8sCliMock := setupK8sDashboardTests(service)
	provisioningTimestamp := int64(1234567)
	k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
	k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resourcepb.ResourceSearchResponse{
		Results: &resourcepb.ResourceTable{
			Columns: []*resourcepb.ResourceTableColumnDefinition{
				{
					Name: "title",
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
				{
					Name: "folder",
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
				{
					Name: resource.SEARCH_FIELD_LEGACY_ID,
					Type: resourcepb.ResourceTableColumnDefinition_INT64,
				},
				{
					Name: resource.SEARCH_FIELD_MANAGER_KIND, // nolint:staticcheck
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
				{
					Name: resource.SEARCH_FIELD_MANAGER_ID,
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
				{
					Name: resource.SEARCH_FIELD_SOURCE_PATH,
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
				{
					Name: resource.SEARCH_FIELD_SOURCE_CHECKSUM,
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
				{
					Name: resource.SEARCH_FIELD_SOURCE_TIME,
					Type: resourcepb.ResourceTableColumnDefinition_INT64,
				},
			},
			Rows: []*resourcepb.ResourceTableRow{
				{
					Key: &resourcepb.ResourceKey{
						Name:     "uid",
						Resource: "dashboard",
					},
					Cells: [][]byte{
						[]byte("Dashboard 1"),
						[]byte("folder 1"),
						[]byte("1"),
						[]byte(string(utils.ManagerKindClassicFP)), // nolint:staticcheck
						[]byte("test"),
						[]byte("path/to/file"),
						[]byte("hash"),
						[]byte("1234567"),
					},
				},
			},
		},
		TotalHits: 1,
	}, nil).Once()
	dash, err := service.GetProvisionedDashboardDataByDashboardUID(ctx, 1, "uid")
	require.NoError(t, err)
	require.Equal(t, dash, &dashboards.DashboardProvisioning{
		ID:          0,
		DashboardID: 1,
		Name:        "test",
		ExternalID:  "path/to/file",
		CheckSum:    "hash",
		Updated:     provisioningTimestamp,
	})
	k8sCliMock.AssertExpectations(t)
}

func TestUnprovisionDashboard(t *testing.T) {
	service := &DashboardServiceImpl{
		cfg: setting.NewCfg(),
		orgService: &orgtest.FakeOrgService{
			ExpectedOrgs: []*org.OrgDTO{{ID: 1}, {ID: 2}},
		},
	}

	ctx, k8sCliMock := setupK8sDashboardTests(service)
	dash := &unstructured.Unstructured{Object: map[string]any{
		"metadata": map[string]any{
			"name": "uid",
			"annotations": map[string]any{
				utils.AnnoKeyManagerKind:     utils.ManagerKindClassicFP, // nolint:staticcheck
				utils.AnnoKeyManagerIdentity: "test",
				utils.AnnoKeySourceChecksum:  "hash",
				utils.AnnoKeySourcePath:      "path/to/file",
				utils.AnnoKeySourceTimestamp: "2025-01-01T00:00:00Z",
			},
		},
		"spec": map[string]any{},
	}}
	k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(dash, nil)
	dashWithoutAnnotations := &unstructured.Unstructured{Object: map[string]any{
		"apiVersion": dashboardv0.APIVERSION,
		"kind":       "Dashboard",
		"metadata": map[string]any{
			"name":        "uid",
			"namespace":   "default",
			"annotations": map[string]any{},
		},
		"spec": map[string]any{
			"uid":     "uid",
			"version": 1,
		},
	}}
	// should update it to be without annotations
	k8sCliMock.On("Update", mock.Anything, dashWithoutAnnotations, mock.Anything, metav1.UpdateOptions{
		FieldValidation: metav1.FieldValidationIgnore,
	}).Return(dashWithoutAnnotations, nil)
	k8sCliMock.On("GetNamespace", mock.Anything).Return("default")
	k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil)
	k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resourcepb.ResourceSearchResponse{
		Results: &resourcepb.ResourceTable{
			Columns: []*resourcepb.ResourceTableColumnDefinition{
				{
					Name: "title",
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
				{
					Name: "folder",
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
			},
			Rows: []*resourcepb.ResourceTableRow{
				{
					Key: &resourcepb.ResourceKey{
						Name:     "uid",
						Resource: "dashboard",
					},
					Cells: [][]byte{
						[]byte("Dashboard 1"),
						[]byte("folder 1"),
					},
				},
			},
		},
		TotalHits: 1,
	}, nil)
	err := service.UnprovisionDashboard(ctx, 1)
	require.NoError(t, err)
	k8sCliMock.AssertExpectations(t)
}

func TestGetDashboardsByPluginID(t *testing.T) {
	service := &DashboardServiceImpl{
		cfg: setting.NewCfg(),
	}
	query := &dashboards.GetDashboardsByPluginIDQuery{
		PluginID: "testing",
		OrgID:    1,
	}
	uidUnstructured := &unstructured.Unstructured{Object: map[string]any{
		"metadata": map[string]any{
			"name": "uid1",
		},
		"spec": map[string]any{
			"title": "Dashboard 1",
		},
	}}

	ctx, k8sCliMock := setupK8sDashboardTests(service)
	k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
	k8sCliMock.On("Get", mock.Anything, "uid", mock.Anything, mock.Anything, mock.Anything).Return(uidUnstructured, nil)
	k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil)
	k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.MatchedBy(func(req *resourcepb.ResourceSearchRequest) bool {
		return ( // gofmt comment helper
		req.Options.Fields[0].Key == "manager.kind" && req.Options.Fields[0].Values[0] == string(utils.ManagerKindPlugin) &&
			req.Options.Fields[1].Key == "manager.id" && req.Options.Fields[1].Values[0] == "testing")
	})).Return(&resourcepb.ResourceSearchResponse{
		Results: &resourcepb.ResourceTable{
			Columns: []*resourcepb.ResourceTableColumnDefinition{
				{
					Name: "title",
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
				{
					Name: "folder",
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
			},
			Rows: []*resourcepb.ResourceTableRow{
				{
					Key: &resourcepb.ResourceKey{
						Name:     "uid",
						Resource: "dashboard",
					},
					Cells: [][]byte{
						[]byte("Dashboard 1"),
						[]byte("folder 1"),
					},
				},
			},
		},
		TotalHits: 1,
	}, nil)
	dashes, err := service.GetDashboardsByPluginID(ctx, query)
	require.NoError(t, err)
	require.Len(t, dashes, 1)
	k8sCliMock.AssertExpectations(t)
}

func TestSetDefaultPermissionsWhenSavingFolderForProvisionedDashboards(t *testing.T) {
	folderPermService := acmock.NewMockedPermissionsService()
	folderPermService.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil)

	cfg := setting.NewCfg()
	f := ini.Empty()
	f.Section("rbac").Key("resources_with_managed_permissions_on_creation").SetValue("folder")
	tempCfg, err := setting.NewCfgFromINIFile(f)
	require.NoError(t, err)
	cfg.RBAC = tempCfg.RBAC

	service := &DashboardServiceImpl{
		cfg:               cfg,
		folderPermissions: folderPermService,
		folderService: &foldertest.FakeService{
			ExpectedFolder: &folder.Folder{
				ID:  0,
				UID: "general",
			},
		},
		ac:        actest.FakeAccessControl{ExpectedEvaluate: true},
		acService: &actest.FakeService{},
		log:       log.NewNopLogger(),
	}

	cmd := &folder.CreateFolderCommand{
		Title: "foo",
		OrgID: 1,
	}

	service.features = featuremgmt.WithFeatures()
	folder, err := service.SaveFolderForProvisionedDashboards(context.Background(), cmd, "")
	require.NoError(t, err)
	require.NotNil(t, folder)

	folderPermService.AssertNumberOfCalls(t, "SetPermissions", 0)
}

func TestSaveProvisionedDashboard(t *testing.T) {
	service := &DashboardServiceImpl{
		cfg: setting.NewCfg(),
		folderService: &foldertest.FakeService{
			ExpectedFolder: &folder.Folder{
				ID:  0,
				UID: "general",
			},
		},
		ac:  actest.FakeAccessControl{ExpectedEvaluate: true},
		log: log.NewNopLogger(),
	}

	query := &dashboards.SaveDashboardDTO{
		OrgID: 1,
		User:  &user.SignedInUser{UserID: 1},
		Dashboard: &dashboards.Dashboard{
			UID:   "uid",
			Title: "testing slugify",
			Slug:  "testing-slugify",
			OrgID: 1,
			Data:  simplejson.NewFromAny(map[string]any{"test": "test", "title": "testing slugify", "uid": "uid"}),
		},
	}
	dashboardUnstructured := unstructured.Unstructured{Object: map[string]any{
		"metadata": map[string]any{
			"name": "uid",
		},
		"spec": map[string]any{
			"test":    "test",
			"version": int64(1),
			"title":   "testing slugify",
		},
	}}

	ctx, k8sCliMock := setupK8sDashboardTests(service)
	k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil)
	k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil)
	k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, metav1.UpdateOptions{
		FieldValidation: metav1.FieldValidationIgnore,
	}).Return(&dashboardUnstructured, nil)
	k8sCliMock.On("GetNamespace", mock.Anything).Return("default")

	dashboard, err := service.SaveProvisionedDashboard(ctx, query, &dashboards.DashboardProvisioning{})
	require.NoError(t, err)
	require.NotNil(t, dashboard)
	k8sCliMock.AssertExpectations(t)
}

func TestSaveDashboard(t *testing.T) {
	service := &DashboardServiceImpl{
		cfg: setting.NewCfg(),
		folderService: &foldertest.FakeService{
			ExpectedFolder: &folder.Folder{},
		},
		ac: actest.FakeAccessControl{ExpectedEvaluate: true},
	}

	query := &dashboards.SaveDashboardDTO{
		OrgID: 1,
		User:  &user.SignedInUser{UserID: 1},
		Dashboard: &dashboards.Dashboard{
			UID:   "uid", // uid is the name of the k8s object
			Title: "testing slugify",
			Slug:  "testing-slugify", // slug is taken from title
			OrgID: 1,                 // orgID is populated from the query
			Data:  simplejson.NewFromAny(map[string]any{"test": "test", "title": "testing slugify", "uid": "uid"}),
		},
	}

	dashboardUnstructured := unstructured.Unstructured{Object: map[string]any{
		"metadata": map[string]any{
			"name": "uid",
		},
		"spec": map[string]any{
			"test":    "test",
			"version": int64(1),
			"title":   "testing slugify",
		},
	}}

	t.Run("Should use Kubernetes create if feature flags are enabled and dashboard doesn't exist", func(t *testing.T) {
		ctx, k8sCliMock := setupK8sDashboardTests(service)
		k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
		k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil)
		k8sCliMock.On("GetNamespace", mock.Anything).Return("default")
		k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, metav1.UpdateOptions{
			FieldValidation: metav1.FieldValidationIgnore,
		}).Return(&dashboardUnstructured, nil)

		dashboard, err := service.SaveDashboard(ctx, query, false)
		require.NoError(t, err)
		require.NotNil(t, dashboard)
	})

	t.Run("Should use Kubernetes update if feature flags are enabled and dashboard exists", func(t *testing.T) {
		ctx, k8sCliMock := setupK8sDashboardTests(service)
		k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil)
		k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil)
		k8sCliMock.On("GetNamespace", mock.Anything).Return("default")
		k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, metav1.UpdateOptions{
			FieldValidation: metav1.FieldValidationIgnore,
		}).Return(&dashboardUnstructured, nil)

		dashboard, err := service.SaveDashboard(ctx, query, false)
		require.NoError(t, err)
		require.NotNil(t, dashboard)
	})

	t.Run("Should return an error if uid is invalid", func(t *testing.T) {
		ctx, k8sCliMock := setupK8sDashboardTests(service)
		k8sCliMock.On("GetNamespace", mock.Anything).Return("default")
		k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, metav1.UpdateOptions{
			FieldValidation: metav1.FieldValidationIgnore,
		}).Return(&dashboardUnstructured, nil)

		query.Dashboard.UID = "invalid/uid"
		_, err := service.SaveDashboard(ctx, query, false)
		require.Error(t, err)
	})
}

func TestDeleteDashboard(t *testing.T) {
	fakePublicDashboardService := publicdashboards.NewFakePublicDashboardServiceWrapper(t)
	service := &DashboardServiceImpl{
		cfg:                    setting.NewCfg(),
		publicDashboardService: fakePublicDashboardService,
	}

	t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) {
		ctx, k8sCliMock := setupK8sDashboardTests(service)
		k8sCliMock.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()

		err := service.DeleteDashboard(ctx, 1, "uid", 1)
		require.NoError(t, err)
		k8sCliMock.AssertExpectations(t)
	})

	t.Run("If UID is not passed in, it should retrieve that first", func(t *testing.T) {
		ctx, k8sCliMock := setupK8sDashboardTests(service)
		k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
		k8sCliMock.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
		k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resourcepb.ResourceSearchResponse{
			Results: &resourcepb.ResourceTable{
				Columns: []*resourcepb.ResourceTableColumnDefinition{
					{
						Name: "title",
						Type: resourcepb.ResourceTableColumnDefinition_STRING,
					},
					{
						Name: "folder",
						Type: resourcepb.ResourceTableColumnDefinition_STRING,
					},
				},
				Rows: []*resourcepb.ResourceTableRow{
					{
						Key: &resourcepb.ResourceKey{
							Name:     "uid",
							Resource: "dashboard",
						},
						Cells: [][]byte{
							[]byte("Dashboard 1"),
							[]byte("folder1"),
						},
					},
				},
			},
			TotalHits: 1,
		}, nil)
		err := service.DeleteDashboard(ctx, 1, "", 1)
		require.NoError(t, err)
		k8sCliMock.AssertExpectations(t)
		k8sCliMock.AssertExpectations(t)
	})
}

func TestDeleteAllDashboards(t *testing.T) {
	service := &DashboardServiceImpl{
		cfg: setting.NewCfg(),
	}

	ctx, k8sCliMock := setupK8sDashboardTests(service)
	k8sCliMock.On("DeleteCollection", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()

	err := service.DeleteAllDashboards(ctx, 1)
	require.NoError(t, err)
	k8sCliMock.AssertExpectations(t)
}

func TestSearchDashboards(t *testing.T) {
	fakeFolders := foldertest.NewFakeService()
	fakeFolders.ExpectedFolder = &folder.Folder{
		Title: "testing-folder-1",
		UID:   "f1",
	}
	fakeFolders.ExpectedFolders = []*folder.Folder{fakeFolders.ExpectedFolder}
	service := &DashboardServiceImpl{
		cfg:           setting.NewCfg(),
		features:      featuremgmt.WithFeatures(),
		folderService: fakeFolders,
		metrics:       newDashboardsMetrics(prometheus.NewRegistry()),
	}

	expectedResult := model.HitList{
		{
			UID:   "uid1",
			OrgID: 1,
			Title: "Dashboard 1",
			Type:  "dash-db",
			URI:   "db/dashboard-1",
			URL:   "/d/uid1/dashboard-1",
			Tags: []string{
				"tag1",
				"tag2",
			},
			FolderTitle: "testing-folder-1",
			FolderUID:   "f1",
			FolderID:    1,
			FolderURL:   "/dashboards/f/f1/testing-folder-1",
		},
		{
			UID:         "uid2",
			OrgID:       1,
			Title:       "Dashboard 2",
			Type:        "dash-db",
			URI:         "db/dashboard-2",
			URL:         "/d/uid2/dashboard-2",
			Tags:        []string{},
			FolderTitle: "testing-folder-1",
			FolderUID:   "f1",
			FolderID:    1,
			FolderURL:   "/dashboards/f/f1/testing-folder-1",
		},
	}
	query := dashboards.FindPersistedDashboardsQuery{
		DashboardUIDs: []string{"uid1", "uid2"},
	}

	t.Run("Should search correctly", func(t *testing.T) {
		ctx, k8sCliMock := setupK8sDashboardTests(service)
		expectedFolders := model.HitList{
			{
				UID:   "f1",
				Title: "testing-folder-1",
				ID:    1,
			},
		}
		fakeFolders.ExpectedHitList = expectedFolders
		k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
		k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resourcepb.ResourceSearchResponse{
			Results: &resourcepb.ResourceTable{
				Columns: []*resourcepb.ResourceTableColumnDefinition{
					{
						Name: "title",
						Type: resourcepb.ResourceTableColumnDefinition_STRING,
					},
					{
						Name: "folder",
						Type: resourcepb.ResourceTableColumnDefinition_STRING,
					},
					{
						Name: "tags",
						Type: resourcepb.ResourceTableColumnDefinition_STRING,
					},
				},
				Rows: []*resourcepb.ResourceTableRow{
					{
						Key: &resourcepb.ResourceKey{
							Name:     "uid1",
							Resource: "dashboard",
						},
						Cells: [][]byte{
							[]byte("Dashboard 1"),
							[]byte("f1"),
							[]byte("[\"tag1\", \"tag2\"]"),
						},
					},
					{
						Key: &resourcepb.ResourceKey{
							Name:     "uid2",
							Resource: "dashboard",
						},
						Cells: [][]byte{
							[]byte("Dashboard 2"),
							[]byte("f1"),
							[]byte(""),
						},
					},
				},
			},
			TotalHits: 1,
		}, nil)

		result, err := service.SearchDashboards(ctx, &query)
		require.NoError(t, err)
		require.Equal(t, expectedResult, result)
		k8sCliMock.AssertExpectations(t)
	})

	t.Run("Should handle Shared with me folder correctly", func(t *testing.T) {
		ctx, k8sCliMock := setupK8sDashboardTests(service)
		k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
		k8sCliMock.On("Search", mock.Anything, int64(1), mock.MatchedBy(func(req *resourcepb.ResourceSearchRequest) bool {
			if len(req.Options.Fields) == 0 {
				return false
			}
			// make sure the search request includes the shared folders
			for _, field := range req.Options.Fields {
				if field.Key == resource.SEARCH_FIELD_NAME {
					return slices.Equal(field.Values, []string{"shared-uid1", "shared-uid2"})
				}
			}
			return false
		})).Return(&resourcepb.ResourceSearchResponse{
			Results: &resourcepb.ResourceTable{
				Columns: []*resourcepb.ResourceTableColumnDefinition{
					{
						Name: "title",
						Type: resourcepb.ResourceTableColumnDefinition_STRING,
					},
					{
						Name: "folder",
						Type: resourcepb.ResourceTableColumnDefinition_STRING,
					},
					{
						Name: "tags",
						Type: resourcepb.ResourceTableColumnDefinition_STRING,
					},
				},
				Rows: []*resourcepb.ResourceTableRow{
					{
						Key: &resourcepb.ResourceKey{
							Name:     "shared-uid1",
							Resource: "dashboard",
						},
						Cells: [][]byte{
							[]byte("Shared Dashboard 1"),
							[]byte("f1"),
							[]byte("[\"shared\"]"),
						},
					},
				},
			},
			TotalHits: 1,
		}, nil).Once()
		k8sCliMock.On("Search", mock.Anything, int64(1), mock.MatchedBy(func(req *resourcepb.ResourceSearchRequest) bool {
			if len(req.Options.Fields) == 0 {
				return false
			}
			for _, field := range req.Options.Fields {
				if field.Key == resource.SEARCH_FIELD_NAME {
					return slices.Equal(field.Values, []string{"shared-uid1"})
				}
			}
			return false
		})).Return(&resourcepb.ResourceSearchResponse{
			Results: &resourcepb.ResourceTable{
				Columns: []*resourcepb.ResourceTableColumnDefinition{
					{
						Name: "title",
						Type: resourcepb.ResourceTableColumnDefinition_STRING,
					},
					{
						Name: "folder",
						Type: resourcepb.ResourceTableColumnDefinition_STRING,
					},
					{
						Name: "tags",
						Type: resourcepb.ResourceTableColumnDefinition_STRING,
					},
				},
				Rows: []*resourcepb.ResourceTableRow{
					{
						Key: &resourcepb.ResourceKey{
							Name:     "shared-uid1",
							Resource: "dashboard",
						},
						Cells: [][]byte{
							[]byte("Shared Dashboard 1"),
							[]byte("f1"),
							[]byte("[\"shared\"]"),
						},
					},
				},
			},
			TotalHits: 1,
		}, nil)
		fakeFolders.ExpectedFolders = []*folder.Folder{}
		query := dashboards.FindPersistedDashboardsQuery{
			FolderUIDs:   []string{folder.SharedWithMeFolderUID},
			SignedInUser: &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{1: {dashboards.ActionDashboardsRead: {dashboards.ScopeDashboardsPrefix + "shared-uid1", dashboards.ScopeDashboardsPrefix + "shared-uid2"}}}},
		}

		result, err := service.SearchDashboards(ctx, &query)
		require.NoError(t, err)
		require.Len(t, result, 1)
		require.Equal(t, "shared-uid1", result[0].UID)
		require.Equal(t, "Shared Dashboard 1", result[0].Title)
		k8sCliMock.AssertExpectations(t)
	})
}

func TestGetDashboards(t *testing.T) {
	service := &DashboardServiceImpl{
		cfg: setting.NewCfg(),
	}

	expectedResult := []*dashboards.Dashboard{
		{
			UID:     "uid1",
			Slug:    "dashboard-1",
			OrgID:   1,
			Title:   "Dashboard 1",
			Version: 1,
			Data:    simplejson.NewFromAny(map[string]any{"title": "Dashboard 1", "uid": "uid1", "version": int64(1)}),
		},
		{
			UID:     "uid2",
			Slug:    "dashboard-2",
			OrgID:   1,
			Title:   "Dashboard 2",
			Version: 1,
			Data:    simplejson.NewFromAny(map[string]any{"title": "Dashboard 2", "uid": "uid2", "version": int64(1)}),
		},
	}
	uid1Unstructured := &unstructured.Unstructured{Object: map[string]any{
		"metadata": map[string]any{
			"name":       "uid1",
			"generation": int64(1),
		},
		"spec": map[string]any{
			"title": "Dashboard 1",
		},
	}}
	uid2Unstructured := &unstructured.Unstructured{Object: map[string]any{
		"metadata": map[string]any{
			"name":       "uid2",
			"generation": int64(1),
		},
		"spec": map[string]any{
			"title": "Dashboard 2",
		},
	}}
	queryByIDs := &dashboards.GetDashboardsQuery{
		DashboardIDs: []int64{1, 2},
		OrgID:        1,
	}
	queryByUIDs := &dashboards.GetDashboardsQuery{
		DashboardUIDs: []string{"uid1", "uid2"},
		OrgID:         1,
	}

	ctx, k8sCliMock := setupK8sDashboardTests(service)
	k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
	k8sCliMock.On("Get", mock.Anything, "uid1", mock.Anything, mock.Anything, mock.Anything).Return(uid1Unstructured, nil)
	k8sCliMock.On("Get", mock.Anything, "uid2", mock.Anything, mock.Anything, mock.Anything).Return(uid2Unstructured, nil)
	k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil)
	k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resourcepb.ResourceSearchResponse{
		Results: &resourcepb.ResourceTable{
			Columns: []*resourcepb.ResourceTableColumnDefinition{
				{
					Name: "title",
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
				{
					Name: "folder",
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
			},
			Rows: []*resourcepb.ResourceTableRow{
				{
					Key: &resourcepb.ResourceKey{
						Name:     "uid1",
						Resource: "dashboard",
					},
					Cells: [][]byte{
						[]byte("Dashboard 1"),
						[]byte(""),
					},
				},
				{
					Key: &resourcepb.ResourceKey{
						Name:     "uid2",
						Resource: "dashboard",
					},
					Cells: [][]byte{
						[]byte("Dashboard 2"),
						[]byte(""),
					},
				},
			},
		},
		TotalHits: 1,
	}, nil)

	// by ids
	result, err := service.GetDashboards(ctx, queryByIDs)
	require.NoError(t, err)
	require.Equal(t, expectedResult, result)
	k8sCliMock.AssertExpectations(t)

	// by uids
	result, err = service.GetDashboards(ctx, queryByUIDs)
	require.NoError(t, err)
	require.Equal(t, expectedResult, result)
	k8sCliMock.AssertExpectations(t)
}

func TestGetDashboardUIDByID(t *testing.T) {
	service := &DashboardServiceImpl{
		cfg: setting.NewCfg(),
	}

	expectedResult := &dashboards.DashboardRef{
		UID:       "uid1",
		Slug:      "dashboard-1",
		FolderUID: "folder1",
	}
	query := &dashboards.GetDashboardRefByIDQuery{
		ID: 1,
	}
	ctx, k8sCliMock := setupK8sDashboardTests(service)
	k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
	k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resourcepb.ResourceSearchResponse{
		Results: &resourcepb.ResourceTable{
			Columns: []*resourcepb.ResourceTableColumnDefinition{
				{
					Name: "title",
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
				{
					Name: "folder",
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
			},
			Rows: []*resourcepb.ResourceTableRow{
				{
					Key: &resourcepb.ResourceKey{
						Name:     "uid1",
						Resource: "dashboard",
					},
					Cells: [][]byte{
						[]byte("Dashboard 1"),
						[]byte("folder1"),
					},
				},
			},
		},
		TotalHits: 1,
	}, nil)
	result, err := service.GetDashboardUIDByID(ctx, query)
	require.NoError(t, err)
	require.Equal(t, expectedResult, result)
	k8sCliMock.AssertExpectations(t)

	// 0 should return error
	_, err = service.GetDashboardUIDByID(ctx, &dashboards.GetDashboardRefByIDQuery{
		ID: 0,
	})
	require.Error(t, err)
	require.Equal(t, dashboards.ErrDashboardNotFound, err)
}

func TestUnstructuredToLegacyDashboard(t *testing.T) {
	k8sCliMock := new(client.MockK8sHandler)
	k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{"user:useruid": {ID: 10, UID: "useruid"}}, nil)
	dr := &DashboardServiceImpl{
		k8sclient: k8sCliMock,
	}
	t.Run("successfully converts unstructured to legacy dashboard", func(t *testing.T) {
		uid := "36b7c825-79cc-435e-acf6-c78bd96a4510"
		orgID := int64(123)
		title := "Test Dashboard"
		now := metav1.Now()
		item := &unstructured.Unstructured{
			Object: map[string]interface{}{
				"spec": map[string]interface{}{
					"title":   title,
					"version": int64(1),
				},
			},
		}

		obj, err := utils.MetaAccessor(item)
		require.NoError(t, err)
		obj.SetCreationTimestamp(now)
		obj.SetName(uid)
		obj.SetCreatedBy("user:useruid")
		obj.SetUpdatedBy("user:useruid")
		obj.SetDeprecatedInternalID(1) // nolint:staticcheck
		result, err := dr.UnstructuredToLegacyDashboard(context.Background(), item, orgID)
		assert.NoError(t, err)
		assert.NotNil(t, result)
		assert.Equal(t, uid, result.UID)
		assert.Equal(t, title, result.Title)
		assert.Equal(t, orgID, result.OrgID)
		assert.Equal(t, "test-dashboard", result.Slug) // should slugify the title
		assert.Equal(t, false, result.HasACL)
		assert.Equal(t, false, result.IsFolder)
		assert.Equal(t, int64(1), result.ID)
		assert.Equal(t, now.Format(time.RFC3339), result.Created.Format(time.RFC3339))
		assert.Equal(t, int64(10), result.CreatedBy)
		assert.Equal(t, now.Format(time.RFC3339), result.Updated.Format(time.RFC3339)) // updated should default to created
		assert.Equal(t, int64(10), result.UpdatedBy)
	})

	t.Run("returns error if spec is missing", func(t *testing.T) {
		item := &unstructured.Unstructured{
			Object: map[string]interface{}{},
		}
		_, err := dr.UnstructuredToLegacyDashboard(context.Background(), item, int64(123))
		assert.Error(t, err)
		assert.Equal(t, "error parsing dashboard from k8s response", err.Error())
	})
}

func TestGetDashboardTags(t *testing.T) {
	service := &DashboardServiceImpl{
		cfg: setting.NewCfg(),
	}

	expectedResult := []*dashboards.DashboardTagCloudItem{
		{
			Term:  "tag1",
			Count: 1,
		},
		{
			Term:  "tag2",
			Count: 3,
		},
	}
	query := &dashboards.GetDashboardTagsQuery{
		OrgID: 1,
	}
	ctx, k8sCliMock := setupK8sDashboardTests(service)
	k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resourcepb.ResourceSearchResponse{
		Facet: map[string]*resourcepb.ResourceSearchResponse_Facet{
			"tags": {
				Terms: []*resourcepb.ResourceSearchResponse_TermFacet{
					{
						Term:  "tag1",
						Count: 1,
					},
					{
						Term:  "tag2",
						Count: 3,
					},
				},
			},
		},
	}, nil)

	result, err := service.GetDashboardTags(ctx, query)
	require.NoError(t, err)
	require.Equal(t, expectedResult, result)
}

func TestQuotaCount(t *testing.T) {
	service := &DashboardServiceImpl{
		cfg: setting.NewCfg(),
	}

	orgs := []*org.OrgDTO{
		{
			ID: 1,
		},
		{
			ID: 2,
		},
	}

	countOrg1 := resourcepb.ResourceStatsResponse{
		Stats: []*resourcepb.ResourceStatsResponse_Stats{
			{
				Count: 1,
			},
		},
	}
	countOrg2 := resourcepb.ResourceStatsResponse{
		Stats: []*resourcepb.ResourceStatsResponse_Stats{
			{
				Count: 2,
			},
		},
	}

	query := &quota.ScopeParameters{
		OrgID: 1,
	}
	ctx, k8sCliMock := setupK8sDashboardTests(service)
	orgSvc := orgtest.FakeOrgService{ExpectedOrgs: orgs}
	service.orgService = &orgSvc
	k8sCliMock.On("GetStats", mock.Anything, int64(2)).Return(&countOrg2, nil).Once()
	k8sCliMock.On("GetStats", mock.Anything, int64(1)).Return(&countOrg1, nil).Once()

	result, err := service.Count(ctx, query)
	require.NoError(t, err)

	orgTag, err := quota.NewTag(dashboards.QuotaTargetSrv, dashboards.QuotaTarget, quota.OrgScope)
	require.NoError(t, err)
	c, _ := result.Get(orgTag)
	require.Equal(t, c, int64(1))

	globalTag, err := quota.NewTag(dashboards.QuotaTargetSrv, dashboards.QuotaTarget, quota.GlobalScope)
	require.NoError(t, err)
	c, _ = result.Get(globalTag)
	require.Equal(t, c, int64(3))
}

func TestCountDashboardsInOrg(t *testing.T) {
	service := &DashboardServiceImpl{
		cfg: setting.NewCfg(),
	}
	count := resourcepb.ResourceStatsResponse{
		Stats: []*resourcepb.ResourceStatsResponse_Stats{
			{
				Count: 3,
			},
		},
	}

	ctx, k8sCliMock := setupK8sDashboardTests(service)
	k8sCliMock.On("GetStats", mock.Anything, mock.Anything).Return(&count, nil).Once()
	result, err := service.CountDashboardsInOrg(ctx, 1)
	require.NoError(t, err)
	require.Equal(t, result, int64(3))
}

func TestCountInFolders(t *testing.T) {
	service := &DashboardServiceImpl{
		cfg: setting.NewCfg(),
	}
	ctx, k8sCliMock := setupK8sDashboardTests(service)
	dashs := &resourcepb.ResourceSearchResponse{
		Results: &resourcepb.ResourceTable{
			Columns: []*resourcepb.ResourceTableColumnDefinition{
				{
					Name: "title",
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
				{
					Name: "folder",
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
			},
			Rows: []*resourcepb.ResourceTableRow{
				{
					Key: &resourcepb.ResourceKey{
						Name:     "uid",
						Resource: "dashboard",
					},
					Cells: [][]byte{
						[]byte("Dashboard 1"),
						[]byte("folder 1"),
					},
				},
				{
					Key: &resourcepb.ResourceKey{
						Name:     "uid2",
						Resource: "dashboard",
					},
					Cells: [][]byte{
						[]byte("Dashboard 2"),
						[]byte("folder 1"),
					},
				},
			},
		},
		TotalHits: 2,
	}

	k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
	k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(dashs, nil).Once()
	result, err := service.CountInFolders(ctx, 1, []string{"folder1"}, &user.SignedInUser{})
	require.NoError(t, err)
	require.Equal(t, result, int64(2))
}

func TestSearchDashboardsThroughK8sRaw(t *testing.T) {
	t.Run("can search dashboards", func(t *testing.T) {
		ctx := context.Background()
		k8sCliMock := new(client.MockK8sHandler)
		service := &DashboardServiceImpl{k8sclient: k8sCliMock}
		query := &dashboards.FindPersistedDashboardsQuery{
			OrgId: 1,
			Sort:  sort.SortAlphaAsc,
		}
		k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
		k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.MatchedBy(func(req *resourcepb.ResourceSearchRequest) bool {
			// should include sort field only once
			titleFieldCount := 0
			for _, field := range req.Fields {
				if field == "title" {
					titleFieldCount++
				}
			}

			return len(req.SortBy) == 1 &&
				// should be converted to "title" due to ParseSortName
				req.SortBy[0].Field == "title" &&
				!req.SortBy[0].Desc &&
				titleFieldCount == 1
		})).Return(&resourcepb.ResourceSearchResponse{
			Results: &resourcepb.ResourceTable{
				Columns: []*resourcepb.ResourceTableColumnDefinition{
					{
						Name: "title",
						Type: resourcepb.ResourceTableColumnDefinition_STRING,
					},
					{
						Name: "folder",
						Type: resourcepb.ResourceTableColumnDefinition_STRING,
					},
				},
				Rows: []*resourcepb.ResourceTableRow{
					{
						Key: &resourcepb.ResourceKey{
							Name:     "uid",
							Resource: "dashboard",
						},
						Cells: [][]byte{
							[]byte("Dashboard 1"),
							[]byte("folder1"),
						},
					},
				},
			},
			TotalHits: 1,
		}, nil)
		res, err := service.searchDashboardsThroughK8s(ctx, query)
		require.NoError(t, err)
		assert.Equal(t, []*dashboards.Dashboard{
			{
				UID:       "uid",
				OrgID:     1,
				FolderUID: "folder1",
				Title:     "Dashboard 1",
				Slug:      "dashboard-1", // should be slugified
			},
		}, res)
		assert.Equal(t, "dash-db", query.Type) // query type should be added
	})

	t.Run("search will try and match all included tags", func(t *testing.T) {
		ctx := context.Background()
		k8sCliMock := new(client.MockK8sHandler)
		service := &DashboardServiceImpl{k8sclient: k8sCliMock}
		query := &dashboards.FindPersistedDashboardsQuery{
			OrgId: 1,
			Sort:  model.SortOption{Name: "viewed-recently-desc"},
			Tags:  []string{"tag1", "tag2"},
		}
		k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
		k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.MatchedBy(func(req *resourcepb.ResourceSearchRequest) bool {
			// make sure we use AND logic with multiple tags
			for _, field := range req.Options.Fields {
				if field.Key == "tags" {
					return field.Operator == "="
				}
			}
			return false
		})).Return(&resourcepb.ResourceSearchResponse{
			Results: &resourcepb.ResourceTable{
				Columns: []*resourcepb.ResourceTableColumnDefinition{},
				Rows:    []*resourcepb.ResourceTableRow{},
			}}, nil)
		_, err := service.searchDashboardsThroughK8s(ctx, query)
		require.NoError(t, err)
	})

	t.Run("search will include sort field in hit fields", func(t *testing.T) {
		ctx := context.Background()
		k8sCliMock := new(client.MockK8sHandler)
		service := &DashboardServiceImpl{k8sclient: k8sCliMock}
		query := &dashboards.FindPersistedDashboardsQuery{
			OrgId: 1,
			Sort:  model.SortOption{Name: "viewed-recently-desc"},
		}
		k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
		k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.MatchedBy(func(req *resourcepb.ResourceSearchRequest) bool {
			return len(req.SortBy) == 1 &&
				req.SortBy[0].Field == "views_last_30_days" &&
				req.SortBy[0].Desc &&
				slices.Contains(req.Fields, "views_last_30_days")
		})).Return(&resourcepb.ResourceSearchResponse{
			Results: &resourcepb.ResourceTable{
				Columns: []*resourcepb.ResourceTableColumnDefinition{
					{
						Name: "title",
						Type: resourcepb.ResourceTableColumnDefinition_STRING,
					},
					{
						Name: "folder",
						Type: resourcepb.ResourceTableColumnDefinition_STRING,
					},
				},
				Rows: []*resourcepb.ResourceTableRow{
					{
						Key: &resourcepb.ResourceKey{
							Name:     "uid",
							Resource: "dashboard",
						},
						Cells: [][]byte{
							[]byte("Dashboard 1"),
							[]byte("folder1"),
						},
					},
				},
			},
			TotalHits: 1,
		}, nil)
		_, err := service.searchDashboardsThroughK8s(ctx, query)
		require.NoError(t, err)
	})

	t.Run("search will request legacy dashboard ID", func(t *testing.T) {
		ctx := context.Background()
		k8sCliMock := new(client.MockK8sHandler)
		service := &DashboardServiceImpl{k8sclient: k8sCliMock}
		query := &dashboards.FindPersistedDashboardsQuery{
			ManagedBy: utils.ManagerKindClassicFP, //nolint:staticcheck
			OrgId:     1,
		}
		k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
		k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.MatchedBy(func(req *resourcepb.ResourceSearchRequest) bool {
			return slices.Contains(req.Fields, "grafana.app/deprecatedInternalID") &&
				slices.Contains(req.Fields, "labels.grafana.app/deprecatedInternalID")
		})).Return(&resourcepb.ResourceSearchResponse{
			Results:   &resourcepb.ResourceTable{},
			TotalHits: 0,
		}, nil)
		_, err := service.searchDashboardsThroughK8s(ctx, query)
		require.NoError(t, err)
	})
}

func TestSearchProvisionedDashboardsThroughK8sRaw(t *testing.T) {
	ctx := context.Background()
	k8sCliMock := new(client.MockK8sHandler)
	service := &DashboardServiceImpl{k8sclient: k8sCliMock}
	query := &dashboards.FindPersistedDashboardsQuery{
		OrgId: 1,
	}
	provisioningTimestamp := int64(1234567)
	k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
	k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resourcepb.ResourceSearchResponse{
		Results: &resourcepb.ResourceTable{
			Columns: []*resourcepb.ResourceTableColumnDefinition{
				{
					Name: "title",
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
				{
					Name: "folder",
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
				{
					Name: resource.SEARCH_FIELD_MANAGER_KIND, // nolint:staticcheck
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
				{
					Name: resource.SEARCH_FIELD_MANAGER_ID,
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
				{
					Name: resource.SEARCH_FIELD_SOURCE_PATH,
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
				{
					Name: resource.SEARCH_FIELD_SOURCE_CHECKSUM,
					Type: resourcepb.ResourceTableColumnDefinition_STRING,
				},
				{
					Name: resource.SEARCH_FIELD_SOURCE_TIME,
					Type: resourcepb.ResourceTableColumnDefinition_INT64,
				},
			},
			Rows: []*resourcepb.ResourceTableRow{
				{
					Key: &resourcepb.ResourceKey{
						Name:     "uid",
						Resource: "dashboard",
					},
					Cells: [][]byte{
						[]byte("Dashboard 1"),
						[]byte("folder 1"),
						[]byte(string(utils.ManagerKindClassicFP)), // nolint:staticcheck
						[]byte("test"),
						[]byte("path/to/file"),
						[]byte("hash"),
						[]byte("1234567"),
					},
				},
			},
		},
		TotalHits: 1,
	}, nil)
	res, err := service.searchProvisionedDashboardsThroughK8s(ctx, query)
	require.NoError(t, err)
	assert.Equal(t, []*dashboardProvisioningWithUID{
		{
			DashboardUID: "uid",
			DashboardProvisioning: dashboards.DashboardProvisioning{
				Name:       "test",
				ExternalID: "path/to/file",
				CheckSum:   "hash",
				Updated:    provisioningTimestamp,
			},
		},
	}, res) // only should return the one provisioned dashboard
	assert.Equal(t, "dash-db", query.Type) // query type should be added as dashboards only
}

func TestLegacySaveCommandToUnstructured(t *testing.T) {
	namespace := "test-namespace"
	t.Run("successfully converts save command to unstructured", func(t *testing.T) {
		cmd := &dashboards.SaveDashboardCommand{
			FolderUID: "folder-uid",
			Message:   "saving this dashboard",
			Dashboard: simplejson.NewFromAny(map[string]any{"test": "test", "title": "testing slugify", "uid": "test-uid"}),
		}

		result, err := LegacySaveCommandToUnstructured(cmd, namespace)
		assert.NoError(t, err)
		assert.NotNil(t, result)
		assert.Equal(t, "test-uid", result.GetName())
		assert.Equal(t, "test-namespace", result.GetNamespace())
		spec := result.Object["spec"].(map[string]any)
		assert.Equal(t, spec["version"], 1)
		assert.Equal(t, result.GetAnnotations(), map[string]string{utils.AnnoKeyFolder: "folder-uid", utils.AnnoKeyMessage: "saving this dashboard"})
	})

	t.Run("should increase version when called", func(t *testing.T) {
		cmd := &dashboards.SaveDashboardCommand{
			Dashboard: simplejson.NewFromAny(map[string]any{"test": "test", "title": "testing slugify", "uid": "test-uid", "version": int64(1)}),
		}
		result, err := LegacySaveCommandToUnstructured(cmd, namespace)
		assert.NoError(t, err)
		assert.NotNil(t, result)
		spec := result.Object["spec"].(map[string]any)
		assert.Equal(t, spec["version"], float64(2))
		// folder annotation should not be set if not inside a folder
		assert.Equal(t, result.GetAnnotations(), map[string]string(nil))
	})
}

func TestSetDefaultPermissionsAfterCreate(t *testing.T) {
	t.Run("Should set correct default permissions", func(t *testing.T) {
		testCases := []struct {
			name               string
			rootFolder         bool
			User               *user.SignedInUser
			expectedPermission []accesscontrol.SetResourcePermissionCommand
		}{
			{
				name:       "in root folder",
				rootFolder: true,
				expectedPermission: []accesscontrol.SetResourcePermissionCommand{
					{UserID: 1, Permission: dashboardaccess.PERMISSION_ADMIN.String()},
					{BuiltinRole: string(org.RoleEditor), Permission: dashboardaccess.PERMISSION_EDIT.String()},
					{BuiltinRole: string(org.RoleViewer), Permission: dashboardaccess.PERMISSION_VIEW.String()},
				},
			},
			{
				name:       "in root folder and user is anonymous",
				rootFolder: true,
				User: &user.SignedInUser{
					IsAnonymous: true,
					UserID:      0,
				},
				expectedPermission: []accesscontrol.SetResourcePermissionCommand{
					{BuiltinRole: string(org.RoleEditor), Permission: dashboardaccess.PERMISSION_EDIT.String()},
					{BuiltinRole: string(org.RoleViewer), Permission: dashboardaccess.PERMISSION_VIEW.String()},
				},
			},
			{
				name:               "in subfolder",
				rootFolder:         false,
				expectedPermission: nil,
			},
		}

		for _, tc := range testCases {
			t.Run(tc.name, func(t *testing.T) {
				user := &user.SignedInUser{
					OrgID:   1,
					OrgRole: "Admin",
					UserID:  1,
				}
				if tc.User != nil {
					user = tc.User
				}
				ctx := request.WithNamespace(context.Background(), "default")
				ctx = identity.WithRequester(ctx, user)

				// Setup mocks and service
				features := featuremgmt.WithFeatures()

				permService := acmock.NewMockedPermissionsService()
				permService.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil)

				service := &DashboardServiceImpl{
					cfg:                       setting.NewCfg(),
					log:                       log.New("test-logger"),
					features:                  features,
					dashboardPermissions:      permService,
					folderPermissions:         permService,
					dashboardPermissionsReady: make(chan struct{}),
					acService:                 &actest.FakeService{},
				}
				service.RegisterDashboardPermissions(permService)

				// Create test object
				key := &resourcepb.ResourceKey{Group: "dashboard.grafana.app", Resource: "dashboards", Name: "test", Namespace: "default"}
				obj := &dashboardv0.Dashboard{
					TypeMeta: metav1.TypeMeta{
						APIVersion: "dashboard.grafana.app/v0alpha1",
					},
				}
				meta, err := utils.MetaAccessor(obj)
				require.NoError(t, err)
				if !tc.rootFolder {
					meta.SetFolder("subfolder")
				}
				// Call the method
				err = service.SetDefaultPermissionsAfterCreate(ctx, key, user, meta)
				require.NoError(t, err)

				// Verify results
				if tc.expectedPermission == nil {
					permService.AssertNotCalled(t, "SetPermissions")
				} else {
					permService.AssertCalled(t, "SetPermissions", mock.Anything, mock.Anything, mock.Anything, tc.expectedPermission)
				}
			})
		}
	})
}

func TestCleanUpDashboard(t *testing.T) {
	t.Run("Should delete public dashboards and clean up after delete", func(t *testing.T) {
		sqlStore, _ := sqlstore.InitTestDB(t)
		fakePublicDashboardService := publicdashboards.NewFakePublicDashboardServiceWrapper(t)
		service := &DashboardServiceImpl{
			cfg:                    setting.NewCfg(),
			sqlStore:               sqlStore,
			publicDashboardService: fakePublicDashboardService,
		}

		fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, int64(1), []string{"dash-uid"}).Return(nil)

		err := service.CleanUpDashboard(context.Background(), "dash-uid", int64(1), int64(1))
		require.NoError(t, err)
		fakePublicDashboardService.AssertExpectations(t)
	})

	t.Run("Should return error if DeleteByDashboardUIDs fails", func(t *testing.T) {
		fakePublicDashboardService := publicdashboards.NewFakePublicDashboardServiceWrapper(t)
		service := &DashboardServiceImpl{
			cfg:                    setting.NewCfg(),
			publicDashboardService: fakePublicDashboardService,
		}

		fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, int64(1), []string{"dash-uid"}).Return(fmt.Errorf("deletion error"))

		err := service.CleanUpDashboard(context.Background(), "dash-uid", int64(1), int64(1))
		require.Error(t, err)
		require.Equal(t, "deletion error", err.Error())
		fakePublicDashboardService.AssertExpectations(t)
	})
}

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

	tests := []struct {
		name       string
		batchSize  int
		setupFunc  func(*DashboardServiceImpl, context.Context, *client.MockK8sHandler)
		verifyFunc func(*testing.T, *DashboardServiceImpl, context.Context, *client.MockK8sHandler, *kvstore.FakeKVStore)
	}{
		{
			name:      "Should process dashboard cleanup for all orgs",
			batchSize: 10,
			setupFunc: func(service *DashboardServiceImpl, ctx context.Context, k8sCliMock *client.MockK8sHandler) {
				// Test organizations
				fakeOrgService := service.orgService.(*orgtest.FakeOrgService)
				fakeOrgService.ExpectedOrgs = []*org.OrgDTO{
					{ID: 1, Name: "org1"},
					{ID: 2, Name: "org2"},
				}

				kv := service.kvstore.(*kvstore.FakeKVStore)
				fakePublicDashboardService := service.publicDashboardService.(*publicdashboards.FakePublicDashboardServiceWrapper)

				// Create dashboard unstructured items for response
				dashboard1 := createTestUnstructuredDashboard("dash1", "org1-dashboard", "101")
				dashboard2 := createTestUnstructuredDashboard("dash2", "org2-dashboard", "201")

				// Setup test data in KV store. Only populate org 1.
				_ = kv.Set(ctx, int64(1), k8sDashboardKvNamespace, k8sDashboardKvLastResourceVersionKey, "100")

				// Mock K8s responses for org 1
				k8sCliMock.On("List", mock.AnythingOfType("*context.valueCtx"), int64(1), mock.MatchedBy(func(opts metav1.ListOptions) bool {
					return opts.LabelSelector == utils.LabelKeyGetTrash+"=true" &&
						opts.Continue == ""
				})).Return(&unstructured.UnstructuredList{
					Object: map[string]interface{}{
						"metadata": map[string]interface{}{
							"resourceVersion": "101",
						},
					},
					Items: []unstructured.Unstructured{dashboard1},
				}, nil).Once()

				// Mock K8s responses for org 2
				k8sCliMock.On("List", mock.AnythingOfType("*context.valueCtx"), int64(2), mock.MatchedBy(func(opts metav1.ListOptions) bool {
					return opts.LabelSelector == utils.LabelKeyGetTrash+"=true" &&
						opts.Continue == ""
				})).Return(&unstructured.UnstructuredList{
					Object: map[string]interface{}{
						"metadata": map[string]interface{}{
							"resourceVersion": "201",
						},
					},
					Items: []unstructured.Unstructured{dashboard2},
				}, nil).Once()

				// should be called twice, one for each list call
				k8sCliMock.On("GetUsersFromMeta", mock.AnythingOfType("*context.valueCtx"), mock.Anything).Return(map[string]*user.User{}, nil).Times(2)

				// Mock cleanup
				fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, int64(1), []string{"dash1"}).Return(nil).Once()
				fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, int64(2), []string{"dash2"}).Return(nil).Once()
			},
			verifyFunc: func(t *testing.T, service *DashboardServiceImpl, ctx context.Context, k8sCliMock *client.MockK8sHandler, kv *kvstore.FakeKVStore) {
				k8sCliMock.AssertExpectations(t)

				// Verify KV store was updated with new resource versions
				val1, found1, _ := kv.Get(ctx, int64(1), k8sDashboardKvNamespace, k8sDashboardKvLastResourceVersionKey)
				require.True(t, found1)
				require.Equal(t, "101", val1)

				val2, found2, _ := kv.Get(ctx, int64(2), k8sDashboardKvNamespace, k8sDashboardKvLastResourceVersionKey)
				require.True(t, found2)
				require.Equal(t, "201", val2)
			},
		},
		{
			name:      "Should handle pagination and batching when processing large sets of dashboards",
			batchSize: 3,
			setupFunc: func(service *DashboardServiceImpl, ctx context.Context, k8sCliMock *client.MockK8sHandler) {
				// Test organization
				fakeOrgService := service.orgService.(*orgtest.FakeOrgService)
				fakeOrgService.ExpectedOrgs = []*org.OrgDTO{
					{ID: 1, Name: "org1"},
				}

				kv := service.kvstore.(*kvstore.FakeKVStore)
				fakePublicDashboardService := service.publicDashboardService.(*publicdashboards.FakePublicDashboardServiceWrapper)

				// Setup initial resource version
				initialVersion := "100"
				_ = kv.Set(ctx, int64(1), k8sDashboardKvNamespace, k8sDashboardKvLastResourceVersionKey, initialVersion)

				// Create dashboard batches (5 dashboards total, to be processed in 2 batches)
				firstBatch := []unstructured.Unstructured{
					createTestUnstructuredDashboard("dash1", "dashboard1", "101"),
					createTestUnstructuredDashboard("dash2", "dashboard2", "102"),
					createTestUnstructuredDashboard("dash3", "dashboard3", "150"),
				}
				secondBatch := []unstructured.Unstructured{
					createTestUnstructuredDashboard("dash4", "dashboard4", "180"),
					createTestUnstructuredDashboard("dash5", "dashboard5", "200"),
				}

				// First batch response with continue token
				k8sCliMock.On("List", mock.AnythingOfType("*context.valueCtx"), int64(1), mock.MatchedBy(func(opts metav1.ListOptions) bool {
					return opts.LabelSelector == utils.LabelKeyGetTrash+"=true" &&
						opts.Continue == ""
				})).Return(&unstructured.UnstructuredList{
					Object: map[string]interface{}{
						"metadata": map[string]interface{}{
							"resourceVersion": "200",
							"continue":        "next-token",
						},
					},
					Items: firstBatch,
				}, nil).Once()

				// Second batch response with updated resource version
				k8sCliMock.On("List", mock.AnythingOfType("*context.valueCtx"), int64(1), mock.MatchedBy(func(opts metav1.ListOptions) bool {
					return opts.LabelSelector == utils.LabelKeyGetTrash+"=true" &&
						opts.Continue == "next-token"
				})).Return(&unstructured.UnstructuredList{
					Object: map[string]interface{}{
						"metadata": map[string]interface{}{
							"resourceVersion": "200",
						},
					},
					Items: secondBatch,
				}, nil).Once()

				// should be called twice, one for each list call
				k8sCliMock.On("GetUsersFromMeta", mock.AnythingOfType("*context.valueCtx"), mock.Anything).Return(map[string]*user.User{}, nil).Times(2)

				// Mock public dashboard deletion for each dashboard
				fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, int64(1), []string{"dash1"}).Return(nil).Once()
				fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, int64(1), []string{"dash2"}).Return(nil).Once()
				fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, int64(1), []string{"dash3"}).Return(nil).Once()
				fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, int64(1), []string{"dash4"}).Return(nil).Once()
				fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, int64(1), []string{"dash5"}).Return(nil).Once()
			},
			verifyFunc: func(t *testing.T, service *DashboardServiceImpl, ctx context.Context, k8sCliMock *client.MockK8sHandler, kv *kvstore.FakeKVStore) {
				k8sCliMock.AssertExpectations(t)

				// Verify KV store was updated with latest resource version
				val, found, _ := kv.Get(ctx, int64(1), k8sDashboardKvNamespace, k8sDashboardKvLastResourceVersionKey)
				require.True(t, found)
				require.Equal(t, "200", val)
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			// Setup test database and utilities
			sqlStore, _ := sqlstore.InitTestDB(t)
			lockService := serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest())
			kv := kvstore.NewFakeKVStore()

			fakePublicDashboardService := publicdashboards.NewFakePublicDashboardServiceWrapper(t)
			fakeOrgService := orgtest.NewOrgServiceFake()
			features := featuremgmt.WithFeatures()

			service := &DashboardServiceImpl{
				cfg:                    setting.NewCfg(),
				log:                    log.New("test.logger"),
				sqlStore:               sqlStore,
				publicDashboardService: fakePublicDashboardService,
				orgService:             fakeOrgService,
				serverLockService:      lockService,
				kvstore:                kv,
				features:               features,
			}

			ctx, k8sCliMock := setupK8sDashboardTests(service)

			if tc.setupFunc != nil {
				tc.setupFunc(service, ctx, k8sCliMock)
			}

			// Execute
			err := service.cleanupK8sDashboardResources(ctx, int64(tc.batchSize), 20*time.Second)
			require.NoError(t, err)

			if tc.verifyFunc != nil {
				tc.verifyFunc(t, service, ctx, k8sCliMock, kv)
			}
		})
	}

	t.Run("Should start and stop background job correctly", func(t *testing.T) {
		// Setup test database and utilities
		sqlStore, _ := sqlstore.InitTestDB(t)
		lockService := serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest())

		cfg := setting.NewCfg()
		cfg.K8sDashboardCleanup = setting.K8sDashboardCleanupSettings{
			Interval:  30 * time.Second,
			Timeout:   25 * time.Second,
			BatchSize: 10,
		}

		service := &DashboardServiceImpl{
			cfg:               cfg,
			log:               log.New("test.logger"),
			features:          featuremgmt.WithFeatures(),
			serverLockService: lockService,
		}

		// Create a test context that can be canceled
		ctx, cancel := context.WithCancel(context.Background())
		defer cancel()

		// Start job with the context
		done := service.startK8sDeletedDashboardsCleanupJob(ctx)

		// Cancel context to verify graceful shutdown
		cancel()

		// Wait for goroutine to exit instead of using sleep
		select {
		case <-done:
			// Job exited successfully
		case <-time.After(time.Second):
			t.Fatal("Cleanup job didn't exit within timeout")
		}
	})
}

// Helper functions for testing

func createTestUnstructuredDashboard(uid, title string, resourceVersion string) unstructured.Unstructured {
	return unstructured.Unstructured{
		Object: map[string]interface{}{
			"apiVersion": dashboardv0.DashboardResourceInfo.GroupVersion().String(),
			"kind":       dashboardv0.DashboardResourceInfo.GroupVersionKind().Kind,
			"metadata": map[string]interface{}{
				"name":              uid,
				"deletionTimestamp": "2023-01-01T00:00:00Z",
				"resourceVersion":   resourceVersion,
			},
			"spec": map[string]interface{}{
				"title": title,
			},
		},
	}
}

func TestGetDashboardsByLibraryPanelUID(t *testing.T) {
	fakePublicDashboardService := publicdashboards.NewFakePublicDashboardServiceWrapper(t)

	k8sCliMock := new(client.MockK8sHandler)

	folderSvc := foldertest.NewFakeService()
	service := &DashboardServiceImpl{
		cfg:                    setting.NewCfg(),
		log:                    log.New("test.logger"),
		folderService:          folderSvc,
		ac:                     actest.FakeAccessControl{ExpectedEvaluate: true},
		features:               featuremgmt.WithFeatures(),
		publicDashboardService: fakePublicDashboardService,
		k8sclient:              k8sCliMock,
	}

	searchResponse := &resourcepb.ResourceSearchResponse{
		TotalHits: 3,
		Results: &resourcepb.ResourceTable{
			Columns: []*resourcepb.ResourceTableColumnDefinition{
				{Name: resource.SEARCH_FIELD_TITLE, Type: resourcepb.ResourceTableColumnDefinition_STRING},
				{Name: resource.SEARCH_FIELD_FOLDER, Type: resourcepb.ResourceTableColumnDefinition_STRING},
				{Name: resource.SEARCH_FIELD_TAGS, Type: resourcepb.ResourceTableColumnDefinition_STRING},
				{Name: resource.SEARCH_FIELD_LEGACY_ID, Type: resourcepb.ResourceTableColumnDefinition_INT64},
			},
			Rows: []*resourcepb.ResourceTableRow{
				{
					Key: &resourcepb.ResourceKey{
						Name:     "dashboard1",
						Resource: "dashboard",
					},
					Cells: [][]byte{
						[]byte("Dashboard 1"),
						[]byte("folder1"),
						[]byte("[]"),
						[]byte("1"),
					},
				},
				{
					Key: &resourcepb.ResourceKey{
						Name:     "dashboard2",
						Resource: "dashboard",
					},
					Cells: [][]byte{
						[]byte("Dashboard 2"),
						[]byte("folder2"),
						[]byte("[]"),
						[]byte("2"),
					},
				},
				{
					Key: &resourcepb.ResourceKey{
						Name:     "dashboard3",
						Resource: "dashboard",
					},
					Cells: [][]byte{
						[]byte("Dashboard 3"),
						[]byte(""),
						[]byte("[]"),
						[]byte("3"),
					},
				},
			},
		},
	}

	k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.MatchedBy(func(req *resourcepb.ResourceSearchRequest) bool {
		return len(req.Options.Fields) == 1 &&
			req.Options.Fields[0].Key == builders.DASHBOARD_LIBRARY_PANEL_REFERENCE &&
			req.Options.Fields[0].Values[0] == "test-library-panel"
	})).Return(searchResponse, nil).Once()

	results, err := service.GetDashboardsByLibraryPanelUID(context.Background(), "test-library-panel", 1)

	require.NoError(t, err)
	require.Len(t, results, 3)

	resultMap := make(map[string]*dashboards.DashboardRef)
	for _, result := range results {
		resultMap[result.UID] = result
	}

	expectedDashboards := map[string]struct {
		folderUID string
		id        int64
	}{
		"dashboard1": {folderUID: "folder1", id: 1},
		"dashboard2": {folderUID: "folder2", id: 2},
		"dashboard3": {folderUID: "", id: 3},
	}
	for uid, expected := range expectedDashboards {
		result, exists := resultMap[uid]
		require.True(t, exists, "Expected dashboard %s not found", uid)
		require.Equal(t, expected.folderUID, result.FolderUID, "Folder UID mismatch for %s", uid)
		require.Equal(t, expected.id, result.ID, "ID mismatch for %s", uid) // nolint:staticcheck
	}

	k8sCliMock.AssertExpectations(t)
}
