package sql

import (
	"context"
	"database/sql/driver"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/grafana/dskit/services"
	"github.com/stretchr/testify/require"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

	"github.com/grafana/grafana/pkg/apimachinery/utils"
	"github.com/grafana/grafana/pkg/storage/unified/resource"
	"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
	"github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl"
	"github.com/grafana/grafana/pkg/storage/unified/sql/test"
	"github.com/grafana/grafana/pkg/util/sqlite"
	"github.com/grafana/grafana/pkg/util/testutil"
)

var (
	errTest = errors.New("things happened")
	resKey  = &resourcepb.ResourceKey{
		Namespace: "ns",
		Group:     "gr",
		Resource:  "rs",
		Name:      "nm",
	}
)

type (
	Cols = []string         // column names
	Rows = [][]driver.Value // row values returned
)

type testBackend struct {
	*backend
	test.TestDBProvider
}

func expectSuccessfulResourceVersionLock(t *testing.T, dbp test.TestDBProvider, rv int64, timestamp int64) {
	dbp.SQLMock.ExpectQuery("select resource_version, unix_timestamp for update").
		WillReturnRows(sqlmock.NewRows([]string{"resource_version", "unix_timestamp"}).
			AddRow(rv, timestamp))
}

func expectSuccessfulResourceVersionSaveRV(t *testing.T, dbp test.TestDBProvider) {
	dbp.SQLMock.ExpectExec("update resource set resource_version").WillReturnResult(sqlmock.NewResult(1, 1))
	dbp.SQLMock.ExpectExec("update resource_history set resource_version").WillReturnResult(sqlmock.NewResult(1, 1))
	dbp.SQLMock.ExpectExec("update resource_version set resource_version").WillReturnResult(sqlmock.NewResult(1, 1))
}

func expectSuccessfulResourceVersionExec(t *testing.T, dbp test.TestDBProvider, cbs ...func()) {
	for _, cb := range cbs {
		cb()
	}
	expectSuccessfulResourceVersionLock(t, dbp, 100, 200)
	expectSuccessfulResourceVersionSaveRV(t, dbp)
}

func (b testBackend) ExecWithResult(expectedSQL string, lastInsertID int64, rowsAffected int64) {
	b.SQLMock.ExpectExec(expectedSQL).WillReturnResult(sqlmock.NewResult(lastInsertID, rowsAffected))
}

func (b testBackend) ExecWithErr(expectedSQL string, err error) {
	b.SQLMock.ExpectExec(expectedSQL).WillReturnError(err)
}

func (b testBackend) QueryWithResult(expectedSQL string, numCols int, rs Rows) {
	rows := b.SQLMock.NewRows(make([]string, numCols))
	if len(rs) > 0 {
		rows = rows.AddRows(rs...)
	}
	b.SQLMock.ExpectQuery(expectedSQL).WillReturnRows(rows)
}

func (b testBackend) QueryWithErr(expectedSQL string, err error) {
	b.SQLMock.ExpectQuery(expectedSQL).WillReturnError(err)
}

func setupBackendTest(t *testing.T) (testBackend, context.Context) {
	t.Helper()

	ctx := testutil.NewDefaultTestContext(t)
	dbp := test.NewDBProviderMatchWords(t)
	b, err := NewBackend(BackendOptions{DBProvider: dbp})
	require.NoError(t, err)
	require.NotNil(t, b)
	svc, ok := b.(services.Service)
	require.True(t, ok)
	require.NoError(t, services.StartAndAwaitRunning(ctx, svc))

	bb, ok := b.(*backend)
	require.True(t, ok)
	require.NotNil(t, bb)

	return testBackend{
		backend:        bb,
		TestDBProvider: dbp,
	}, ctx
}

func TestNewBackend(t *testing.T) {
	t.Parallel()

	t.Run("happy path", func(t *testing.T) {
		t.Parallel()

		dbp := test.NewDBProviderNopSQL(t)
		b, err := NewBackend(BackendOptions{DBProvider: dbp})
		require.NoError(t, err)
		require.NotNil(t, b)
	})

	t.Run("happy path without storage services enabled", func(t *testing.T) {
		t.Parallel()

		dbp := test.NewDBProviderNopSQL(t)
		b, err := NewBackend(BackendOptions{DBProvider: dbp, DisableStorageServices: true})
		require.NoError(t, err)
		require.NotNil(t, b)
	})

	t.Run("with TmpDir does not create directory eagerly", func(t *testing.T) {
		t.Parallel()

		tmpDir := t.TempDir() + "/not-yet-created"
		dbp := test.NewDBProviderNopSQL(t)
		b, err := NewBackend(BackendOptions{DBProvider: dbp, TmpDir: tmpDir})
		require.NoError(t, err)
		require.NotNil(t, b)

		// The directory should NOT have been created at construction time.
		_, statErr := os.Stat(tmpDir)
		require.True(t, os.IsNotExist(statErr), "TmpDir should not be created eagerly by NewBackend")
	})

	t.Run("with empty TmpDir", func(t *testing.T) {
		t.Parallel()

		dbp := test.NewDBProviderNopSQL(t)
		b, err := NewBackend(BackendOptions{DBProvider: dbp, TmpDir: ""})
		require.NoError(t, err)
		require.NotNil(t, b)
	})

	t.Run("no db provider", func(t *testing.T) {
		t.Parallel()

		b, err := NewBackend(BackendOptions{})
		require.Nil(t, b)
		require.Error(t, err)
		require.ErrorContains(t, err, "no db provider")
	})
}

func TestTmpDir(t *testing.T) {
	t.Parallel()

	t.Run("returns empty string for empty dataPath", func(t *testing.T) {
		require.Equal(t, "", tmpDir(""))
	})

	t.Run("returns dataPath/tmp for non-empty dataPath", func(t *testing.T) {
		require.Equal(t, filepath.Join("/var/lib/grafana", "tmp"), tmpDir("/var/lib/grafana"))
	})
}

func TestBackend_Init(t *testing.T) {
	t.Parallel()

	t.Run("happy path", func(t *testing.T) {
		t.Parallel()

		ctx := testutil.NewDefaultTestContext(t)
		dbp := test.NewDBProviderWithPing(t)
		dbp.SQLMock.ExpectPing().WillReturnError(nil)
		b, err := NewBackend(BackendOptions{DBProvider: dbp})
		require.NoError(t, err)
		require.NotNil(t, b)

		svc, ok := b.(services.Service)
		require.True(t, ok)
		require.NoError(t, services.StartAndAwaitRunning(context.Background(), svc))

		// cannot start service twice
		require.ErrorContains(t, services.StartAndAwaitRunning(ctx, svc), "invalid service state")
		svc.StopAsync()
		require.NoError(t, svc.AwaitTerminated(ctx))
	})

	t.Run("happy path without storage services enabled", func(t *testing.T) {
		t.Parallel()

		ctx := testutil.NewDefaultTestContext(t)
		dbp := test.NewDBProviderWithPing(t)
		dbp.SQLMock.ExpectPing().WillReturnError(nil)
		b, err := NewBackend(BackendOptions{DBProvider: dbp, DisableStorageServices: true})
		require.NoError(t, err)
		require.NotNil(t, b)

		svc, ok := b.(services.Service)
		require.True(t, ok)
		require.NoError(t, services.StartAndAwaitRunning(ctx, svc))
		svc.StopAsync()
		require.NoError(t, svc.AwaitTerminated(ctx))
	})

	t.Run("no db provider", func(t *testing.T) {
		t.Parallel()

		dbp := test.TestDBProvider{
			Err: errTest,
		}
		_, err := NewBackend(BackendOptions{DBProvider: dbp})
		require.Error(t, err)
		require.ErrorContains(t, err, "initialize resource DB")
	})

	t.Run("no dialect for driver", func(t *testing.T) {
		t.Parallel()

		mockDB, _, err := sqlmock.New()
		require.NoError(t, err)
		dbp := test.TestDBProvider{
			DB: dbimpl.NewDB(mockDB, "juancarlo"),
		}

		_, err = NewBackend(BackendOptions{DBProvider: dbp})
		require.Error(t, err)
		require.ErrorContains(t, err, "no dialect for driver")
	})

	t.Run("database unreachable", func(t *testing.T) {
		t.Parallel()

		dbp := test.NewDBProviderWithPing(t)
		dbp.SQLMock.ExpectPing().WillReturnError(errTest)
		_, err := NewBackend(BackendOptions{DBProvider: dbp})
		require.Error(t, err)
		require.ErrorIs(t, err, errTest)
	})
}

func TestBackend_IsHealthy(t *testing.T) {
	t.Parallel()

	ctx := testutil.NewDefaultTestContext(t)
	dbp := test.NewDBProviderWithPing(t)
	dbp.SQLMock.ExpectPing().WillReturnError(nil) // for Init
	b, err := NewBackend(BackendOptions{DBProvider: dbp})
	require.NoError(t, err)
	require.NotNil(t, dbp.DB)
	svc, ok := b.(services.Service)
	require.True(t, ok)

	require.NoError(t, services.StartAndAwaitRunning(ctx, svc))

	dbp.SQLMock.ExpectPing().WillReturnError(nil)
	res, err := b.IsHealthy(ctx, nil) //nolint:staticcheck
	require.NoError(t, err)
	require.NotNil(t, res)

	dbp.SQLMock.ExpectPing().WillReturnError(errTest)
	res, err = b.IsHealthy(ctx, nil) //nolint:staticcheck
	require.Nil(t, res)
	require.Error(t, err)
	require.ErrorIs(t, err, errTest)
}

func TestBackend_create(t *testing.T) {
	t.Parallel()
	meta, err := utils.MetaAccessor(&unstructured.Unstructured{
		Object: map[string]any{},
	})
	require.NoError(t, err)
	event := resource.WriteEvent{
		Type:   resourcepb.WatchEvent_ADDED,
		Key:    resKey,
		Object: meta,
	}

	t.Run("happy path", func(t *testing.T) {
		t.Parallel()
		b, ctx := setupBackendTest(t)
		b.SQLMock.ExpectBegin()
		expectSuccessfulResourceVersionExec(t, b.TestDBProvider,
			func() { b.ExecWithResult("insert resource", 0, 1) },
			func() { b.ExecWithResult("insert resource_history", 0, 1) },
		)
		b.SQLMock.ExpectCommit()
		v, err := b.create(ctx, event)
		require.NoError(t, err)
		require.Equal(t, int64(200), v)
	})

	t.Run("resource already exists", func(t *testing.T) {
		t.Parallel()
		b, ctx := setupBackendTest(t)
		b.SQLMock.ExpectBegin()
		expectSuccessfulResourceVersionExec(t, b.TestDBProvider,
			func() { b.ExecWithResult("insert resource", 0, 1) },
			func() { b.ExecWithResult("insert resource_history", 0, 1) },
		)
		b.SQLMock.ExpectCommit()
		b.SQLMock.ExpectBegin()
		b.SQLMock.ExpectExec("insert resource").WillReturnError(sqlite.ErrTestUniqueConstraintViolation)
		b.SQLMock.ExpectRollback()

		// First we insert the resource successfully. This is what the happy path test does as well.
		v, err := b.create(ctx, event)
		require.NoError(t, err)
		require.Equal(t, int64(200), v)

		// Then we try to insert the same resource again. This should fail.
		_, err = b.create(ctx, event)
		require.ErrorIs(t, err, resource.ErrResourceAlreadyExists)
	})

	t.Run("error inserting into resource", func(t *testing.T) {
		t.Parallel()
		b, ctx := setupBackendTest(t)

		b.SQLMock.ExpectBegin()
		b.ExecWithErr("insert resource", errTest)
		b.SQLMock.ExpectRollback()

		v, err := b.create(ctx, event)
		require.Zero(t, v)
		require.Error(t, err)
		require.ErrorContains(t, err, "insert into resource")
	})

	t.Run("error inserting into resource_history", func(t *testing.T) {
		t.Parallel()
		b, ctx := setupBackendTest(t)

		b.SQLMock.ExpectBegin()
		b.ExecWithResult("insert resource", 0, 1)
		b.ExecWithErr("insert resource_history", errTest)
		b.SQLMock.ExpectRollback()

		v, err := b.create(ctx, event)
		require.Zero(t, v)
		require.Error(t, err)
		require.ErrorContains(t, err, "insert into resource history")
	})
}

func TestBackend_update(t *testing.T) {
	t.Parallel()
	meta, err := utils.MetaAccessor(&unstructured.Unstructured{
		Object: map[string]any{},
	})
	require.NoError(t, err)
	meta.SetFolder("folderuid")
	event := resource.WriteEvent{
		Type:   resourcepb.WatchEvent_MODIFIED,
		Key:    resKey,
		Object: meta,
	}

	t.Run("happy path", func(t *testing.T) {
		t.Parallel()
		b, ctx := setupBackendTest(t)

		b.SQLMock.ExpectBegin()
		expectSuccessfulResourceVersionExec(t, b.TestDBProvider,
			func() { b.ExecWithResult("update resource", 0, 1) },
			func() { b.ExecWithResult("insert resource_history", 0, 1) },
		)
		b.SQLMock.ExpectCommit()

		v, err := b.update(ctx, event)
		require.NoError(t, err)
		require.Equal(t, int64(200), v)
	})

	t.Run("error in first update to resource", func(t *testing.T) {
		t.Parallel()
		b, ctx := setupBackendTest(t)

		b.SQLMock.ExpectBegin()
		b.ExecWithErr("update resource", errTest)
		b.SQLMock.ExpectRollback()

		v, err := b.update(ctx, event)
		require.Zero(t, v)
		require.Error(t, err)
		require.ErrorContains(t, err, "resource update")
	})

	t.Run("error inserting into resource history", func(t *testing.T) {
		t.Parallel()
		b, ctx := setupBackendTest(t)

		b.SQLMock.ExpectBegin()
		b.ExecWithResult("update resource", 0, 1)
		b.ExecWithErr("insert resource_history", errTest)
		b.SQLMock.ExpectRollback()

		v, err := b.update(ctx, event)
		require.Zero(t, v)
		require.Error(t, err)
		require.ErrorContains(t, err, "insert into resource history")
	})
}

func TestBackend_delete(t *testing.T) {
	t.Parallel()
	meta, err := utils.MetaAccessor(&unstructured.Unstructured{
		Object: map[string]any{},
	})
	require.NoError(t, err)
	event := resource.WriteEvent{
		Type:   resourcepb.WatchEvent_DELETED,
		Key:    resKey,
		Object: meta,
	}

	t.Run("happy path", func(t *testing.T) {
		t.Parallel()
		b, ctx := setupBackendTest(t)

		b.SQLMock.ExpectBegin()
		expectSuccessfulResourceVersionExec(t, b.TestDBProvider,
			func() { b.ExecWithResult("delete resource", 0, 1) },
			func() { b.ExecWithResult("insert resource_history", 0, 1) },
		)
		b.SQLMock.ExpectCommit()

		v, err := b.delete(ctx, event)
		require.NoError(t, err)
		require.Equal(t, int64(200), v)
	})

	t.Run("error deleting resource", func(t *testing.T) {
		t.Parallel()
		b, ctx := setupBackendTest(t)

		b.SQLMock.ExpectBegin()
		b.ExecWithErr("delete resource", errTest)
		b.SQLMock.ExpectCommit()

		v, err := b.delete(ctx, event)
		require.Zero(t, v)
		require.Error(t, err)
		require.ErrorContains(t, err, "delete resource")
	})

	t.Run("error inserting into resource history", func(t *testing.T) {
		t.Parallel()
		b, ctx := setupBackendTest(t)

		b.SQLMock.ExpectBegin()
		b.ExecWithResult("delete resource", 0, 1)
		b.ExecWithErr("insert resource_history", errTest)
		b.SQLMock.ExpectCommit()

		v, err := b.delete(ctx, event)
		require.Zero(t, v)
		require.Error(t, err)
		require.ErrorContains(t, err, "insert into resource history")
	})
}

type readHistoryRow struct {
	guid             string
	namespace        string
	group            string
	resource         string
	name             string
	folder           string
	resource_version string
	value            string
}

func TestBackend_ReadResource(t *testing.T) {
	t.Parallel()

	t.Run("happy path", func(t *testing.T) {
		t.Parallel()
		b, ctx := setupBackendTest(t)

		expectedReadRow := readHistoryRow{
			guid:             "guid",
			namespace:        "ns",
			group:            "gr",
			resource:         "rs",
			name:             "nm",
			folder:           "folder",
			resource_version: "300",
			value:            "rv-300",
		}
		readResource := []string{"guid", "namespace", "group", "resource", "name", "folder", "resource_version", "value"}
		b.SQLMock.ExpectBegin()
		b.SQLMock.ExpectQuery("SELECT .* FROM resource").
			WillReturnRows(sqlmock.NewRows(readResource).
				AddRow(
					expectedReadRow.guid,
					expectedReadRow.namespace,
					expectedReadRow.group,
					expectedReadRow.resource,
					expectedReadRow.name,
					expectedReadRow.folder,
					expectedReadRow.resource_version,
					expectedReadRow.value,
				))
		b.SQLMock.ExpectCommit()

		req := &resourcepb.ReadRequest{
			Key: resKey,
		}
		rps := b.ReadResource(ctx, req)
		require.NotNil(t, rps)
		require.Equal(t, int64(300), rps.ResourceVersion)
		require.Equal(t, "rv-300", string(rps.Value))
		require.Equal(t, "folder", rps.Folder)
	})

	t.Run("no resource found", func(t *testing.T) {
		t.Parallel()
		b, ctx := setupBackendTest(t)

		b.SQLMock.ExpectBegin()
		b.SQLMock.ExpectQuery("SELECT .* FROM resource").
			WillReturnRows(sqlmock.NewRows([]string{}))
		b.SQLMock.ExpectCommit()

		req := &resourcepb.ReadRequest{
			Key: resKey,
		}
		res := b.ReadResource(ctx, req)
		require.NotNil(t, res.Error)
		require.Equal(t, res.Error.Code, int32(404))
	})

	t.Run("with resource version", func(t *testing.T) {
		t.Parallel()
		b, ctx := setupBackendTest(t)

		expectedReadRow := readHistoryRow{
			guid:             "guid",
			namespace:        "ns",
			group:            "gr",
			resource:         "rs",
			name:             "nm",
			folder:           "folder",
			resource_version: "300",
			value:            "rv-300",
		}

		readHistoryColumns := []string{"guid", "namespace", "group", "resource", "name", "folder", "resource_version", "value"}
		b.SQLMock.ExpectBegin()
		b.SQLMock.ExpectQuery("SELECT .* FROM resource_history").
			WillReturnRows(sqlmock.NewRows(readHistoryColumns).
				AddRow(
					expectedReadRow.guid,
					expectedReadRow.namespace,
					expectedReadRow.group,
					expectedReadRow.resource,
					expectedReadRow.name,
					expectedReadRow.folder,
					expectedReadRow.resource_version,
					expectedReadRow.value,
				))
		b.SQLMock.ExpectCommit()

		req := &resourcepb.ReadRequest{
			Key:             resKey,
			ResourceVersion: 300,
		}
		rps := b.ReadResource(ctx, req)
		require.NotNil(t, rps)
		require.Equal(t, int64(300), rps.ResourceVersion)
		require.Equal(t, "rv-300", string(rps.Value))
		require.Equal(t, "folder", rps.Folder)
	})

	t.Run("error reading resource", func(t *testing.T) {
		t.Parallel()
		b, ctx := setupBackendTest(t)

		b.SQLMock.ExpectQuery("SELECT .* FROM resource_history").
			WillReturnError(errTest)

		req := &resourcepb.ReadRequest{
			Key: resKey,
		}
		rps := b.ReadResource(ctx, req)
		require.NotNil(t, rps.Error)
	})
}

func TestBackend_getHistory(t *testing.T) {
	t.Parallel()

	// Common setup
	key := &resourcepb.ResourceKey{
		Namespace: "ns",
		Group:     "gr",
		Resource:  "rs",
		Name:      "nm",
	}
	rv1, rv2, rv3 := int64(100), int64(200), int64(300)
	cols := []string{"guid", "resource_version", "namespace", "group", "resource", "name", "folder", "value"}

	tests := []struct {
		name                          string
		source                        resourcepb.ListRequest_Source
		versionMatch                  resourcepb.ResourceVersionMatchV2
		resourceVersion               int64
		expectedVersions              []int64
		expectedListRv                int64
		expectedRowsCount             int
		expectedErr                   string
		expectedLatestDeletionAsMinRV bool
	}{
		{
			name:              "with ResourceVersionMatch_NotOlderThan",
			source:            resourcepb.ListRequest_HISTORY,
			versionMatch:      resourcepb.ResourceVersionMatchV2_NotOlderThan,
			resourceVersion:   rv2,
			expectedVersions:  []int64{rv2, rv3}, // Should be in ASC order due to NotOlderThan
			expectedListRv:    rv3,
			expectedRowsCount: 2,
		},
		{
			name:                          "with ResourceVersionMatch_NotOlderThan and ResourceVersion=0",
			source:                        resourcepb.ListRequest_HISTORY,
			versionMatch:                  resourcepb.ResourceVersionMatchV2_NotOlderThan,
			resourceVersion:               0,
			expectedVersions:              []int64{rv1, rv2, rv3}, // Should be in ASC order due to NotOlderThan
			expectedListRv:                rv3,
			expectedRowsCount:             3,
			expectedLatestDeletionAsMinRV: true,
		},
		{
			name:              "with ResourceVersionMatch_Exact",
			source:            resourcepb.ListRequest_HISTORY,
			versionMatch:      resourcepb.ResourceVersionMatchV2_Exact,
			resourceVersion:   rv2,
			expectedVersions:  []int64{rv2},
			expectedListRv:    rv3,
			expectedRowsCount: 1,
		},
		{
			name:                          "with ResourceVersionMatch_Unset (default)",
			source:                        resourcepb.ListRequest_HISTORY,
			expectedVersions:              []int64{rv3, rv2, rv1}, // Should be in DESC order by default
			expectedListRv:                rv3,
			expectedRowsCount:             3,
			expectedLatestDeletionAsMinRV: true,
		},
		{
			name:            "error with ResourceVersionMatch_Exact and ResourceVersion <= 0",
			source:          resourcepb.ListRequest_HISTORY,
			versionMatch:    resourcepb.ResourceVersionMatchV2_Exact,
			resourceVersion: 0,
			expectedErr:     "expecting an explicit resource version query when using Exact matching",
		},
		{
			name:              "with ListRequest_TRASH",
			source:            resourcepb.ListRequest_TRASH,
			expectedVersions:  []int64{rv3, rv2, rv1}, // Should be in DESC order by default
			expectedListRv:    rv3,
			expectedRowsCount: 3,
		},
	}

	for _, tc := range tests {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			b, ctx := setupBackendTest(t)

			// Build request with appropriate matcher
			req := &resourcepb.ListRequest{
				Options:         &resourcepb.ListOptions{Key: key},
				ResourceVersion: tc.resourceVersion,
				VersionMatchV2:  tc.versionMatch,
				Source:          tc.source,
			}

			// Set up mock expectations only if we don't expect an error
			if tc.expectedErr == "" {
				// Build expected values map
				expectedValues := make(map[int64]string)
				for _, rv := range tc.expectedVersions {
					expectedValues[rv] = fmt.Sprintf("rv-%d", rv)
				}

				// Callback that tracks returned items
				callback := func(iter resource.ListIterator) error {
					count := 0
					var seenVersions []int64
					for iter.Next() {
						count++
						currentRV := iter.ResourceVersion()
						seenVersions = append(seenVersions, currentRV)
						expectedValue, ok := expectedValues[currentRV]
						require.True(t, ok, "Got unexpected RV: %d", currentRV)
						require.Equal(t, expectedValue, string(iter.Value()))
					}
					require.Equal(t, tc.expectedRowsCount, count)
					// Verify the order matches what we expect
					require.Equal(t, tc.expectedVersions, seenVersions, "Resource versions returned in incorrect order")
					return nil
				}

				b.SQLMock.ExpectBegin()

				// Expect fetch latest RV call
				latestRVRows := sqlmock.NewRows([]string{"resource_version", "unix_timestamp"}).
					AddRow(rv3, 0)
				b.SQLMock.ExpectQuery("SELECT .* FROM resource_version").WillReturnRows(latestRVRows)

				if tc.expectedLatestDeletionAsMinRV {
					latestHistoryRVRows := sqlmock.NewRows([]string{"resource_version"}).
						AddRow(rv1)
					b.SQLMock.ExpectQuery("SELECT .* FROM resource_history").
						WillReturnRows(latestHistoryRVRows)
				}

				// Expect history query
				historyRows := sqlmock.NewRows(cols)
				for _, rv := range tc.expectedVersions {
					historyRows.AddRow(
						"guid",                           // guid
						rv,                               // resource_version
						"ns",                             // namespace
						"gr",                             // group
						"rs",                             // resource
						"nm",                             // name
						"folder",                         // folder
						[]byte(fmt.Sprintf("rv-%d", rv)), // value
					)
				}
				b.SQLMock.ExpectQuery("SELECT .* FROM resource_history").WillReturnRows(historyRows)
				b.SQLMock.ExpectCommit()

				// Execute the test
				listRv, err := b.getHistory(ctx, req, callback)
				require.NoError(t, err)
				require.Equal(t, tc.expectedListRv, listRv)
			} else {
				// For error cases, we use a simple empty callback
				callback := func(iter resource.ListIterator) error { return nil }

				// Execute the test expecting an error
				listRv, err := b.getHistory(ctx, req, callback)
				require.Zero(t, listRv)
				require.Error(t, err)
				require.ErrorContains(t, err, tc.expectedErr)
			}
		})
	}
}

// TestBackend_getHistoryPagination tests the ordering behavior for ResourceVersionMatch_NotOlderThan
// when using pagination, ensuring entries are returned in oldest-to-newest order.
func TestBackend_getHistoryPagination(t *testing.T) {
	t.Parallel()

	// Common setup
	key := &resourcepb.ResourceKey{
		Namespace: "ns",
		Group:     "gr",
		Resource:  "rs",
		Name:      "nm",
	}

	// Create resource versions that will be returned in our test
	versions := make([]int64, 10)
	for i := range versions {
		versions[i] = int64(51 + i)
	}
	rv51, rv52, rv53, rv54, rv55, rv56, rv57, rv58, rv59, rv60 := versions[0], versions[1], versions[2], versions[3], versions[4], versions[5], versions[6], versions[7], versions[8], versions[9]

	t.Run("pagination with NotOlderThan should return entries from oldest to newest", func(t *testing.T) {
		b, ctx := setupBackendTest(t)

		// Define all pages we want to test
		pages := []struct {
			versions []int64
			token    *ContinueToken
		}{
			{
				versions: []int64{rv51, rv52, rv53, rv54},
				token:    nil,
			},
			{
				versions: []int64{rv55, rv56, rv57, rv58},
				token: &ContinueToken{
					ResourceVersion: rv54,
					StartOffset:     4,
					SortAscending:   true,
				},
			},
			{
				versions: []int64{rv59, rv60},
				token: &ContinueToken{
					ResourceVersion: rv58,
					StartOffset:     8,
					SortAscending:   true,
				},
			},
		}

		allItems := make([]int64, 0, len(versions)*len(pages))
		initialRV := rv51

		// Test each page
		for _, page := range pages {
			req := &resourcepb.ListRequest{
				Options:         &resourcepb.ListOptions{Key: key},
				ResourceVersion: initialRV,
				VersionMatchV2:  resourcepb.ResourceVersionMatchV2_NotOlderThan,
				Source:          resourcepb.ListRequest_HISTORY,
				Limit:           4,
			}
			if page.token != nil {
				req.NextPageToken = page.token.String()
			}

			expectedLatestDeletionAsMinRV := false
			items := make([]int64, 0)
			callback := func(iter resource.ListIterator) error {
				for iter.Next() {
					items = append(items, iter.ResourceVersion())
				}
				return nil
			}

			b.SQLMock.ExpectBegin()
			historyRows := setupHistoryTest(b, page.versions, rv60, expectedLatestDeletionAsMinRV)
			b.SQLMock.ExpectQuery("SELECT .* FROM resource_history").WillReturnRows(historyRows)
			b.SQLMock.ExpectCommit()

			listRv, err := b.getHistory(ctx, req, callback)
			require.NoError(t, err)
			require.Equal(t, rv60, listRv, "Head version should be the latest resource version (rv60)")
			require.Equal(t, page.versions, items, "Items should be in ASC order")

			allItems = append(allItems, items...)
		}

		// Verify complete sequence
		expectedAllItems := []int64{rv51, rv52, rv53, rv54, rv55, rv56, rv57, rv58, rv59, rv60}
		require.Equal(t, expectedAllItems, allItems)
	})

	t.Run("pagination with ResourceVersion=0 and NotOlderThan should return entries in ASC order", func(t *testing.T) {
		b, ctx := setupBackendTest(t)

		req := &resourcepb.ListRequest{
			Options:         &resourcepb.ListOptions{Key: key},
			ResourceVersion: 0,
			VersionMatchV2:  resourcepb.ResourceVersionMatchV2_NotOlderThan,
			Source:          resourcepb.ListRequest_HISTORY,
			Limit:           4,
		}

		expectedLatestDeletionAsMinRV := true
		// First batch of items we expect, in ASC order (because of NotOlderThan flag)
		// Even with ResourceVersion=0, the order is ASC because we use SortAscending=true
		expectedVersions := []int64{rv51, rv52, rv53, rv54}
		items := make([]int64, 0)

		callback := func(iter resource.ListIterator) error {
			for iter.Next() {
				items = append(items, iter.ResourceVersion())
			}
			return nil
		}

		b.SQLMock.ExpectBegin()
		historyRows := setupHistoryTest(b, expectedVersions, rv60, expectedLatestDeletionAsMinRV)
		b.SQLMock.ExpectQuery("SELECT .* FROM resource_history").WillReturnRows(historyRows)
		b.SQLMock.ExpectCommit()

		listRv, err := b.getHistory(ctx, req, callback)
		require.NoError(t, err)
		require.Equal(t, rv60, listRv, "Head version should be the latest resource version (rv60)")
		require.Equal(t, expectedVersions, items, "Items should be in ASC order even with ResourceVersion=0")
	})
}

// setupHistoryTest creates the necessary mock expectations for a history test
func setupHistoryTest(b testBackend, resourceVersions []int64, latestRV int64, expectedLatestDeletionAsMinRV4 bool) *sqlmock.Rows {
	// Expect fetch latest RV call - set to the highest resource version
	latestRVRows := sqlmock.NewRows([]string{"resource_version", "unix_timestamp"}).
		AddRow(latestRV, 0)
	b.SQLMock.ExpectQuery("SELECT .* FROM resource_version").WillReturnRows(latestRVRows)

	if expectedLatestDeletionAsMinRV4 {
		latestHistoryRVRows := sqlmock.NewRows([]string{"resource_version"}).
			AddRow(latestRV)
		b.SQLMock.ExpectQuery("SELECT .* FROM resource_history").
			WillReturnRows(latestHistoryRVRows)
	}

	// Create the mock rows for the history items
	cols := []string{"guid", "resource_version", "namespace", "group", "resource", "name", "folder", "value"}
	historyRows := sqlmock.NewRows(cols)
	for _, rv := range resourceVersions {
		historyRows.AddRow(
			"guid",                           // guid
			rv,                               // resource_version
			"ns",                             // namespace
			"gr",                             // group
			"rs",                             // resource
			"nm",                             // name
			"folder",                         // folder
			[]byte(fmt.Sprintf("rv-%d", rv)), // value
		)
	}

	return historyRows
}

func TestBackend_StorageDisabled(t *testing.T) {
	t.Parallel()

	t.Run("WriteEvent returns error when storage is disabled", func(t *testing.T) {
		t.Parallel()
		b, ctx := setupBackendTestStorageDisabled(t)

		meta, err := utils.MetaAccessor(&unstructured.Unstructured{
			Object: map[string]any{},
		})
		require.NoError(t, err)
		event := resource.WriteEvent{
			Type:   resourcepb.WatchEvent_ADDED,
			Key:    resKey,
			Object: meta,
		}

		rv, err := b.WriteEvent(ctx, event)
		require.Zero(t, rv)
		require.Error(t, err)
		require.ErrorContains(t, err, "storage backend is not enabled")
	})

	t.Run("ProcessBulk returns error when storage is disabled", func(t *testing.T) {
		t.Parallel()
		b, ctx := setupBackendTestStorageDisabled(t)

		resp := b.ProcessBulk(ctx, resource.BulkSettings{}, nil)
		require.NotNil(t, resp)
		require.NotNil(t, resp.Error)
		require.Contains(t, resp.Error.Message, "storage backend is not enabled")
	})

	t.Run("WatchWriteEvents returns error when storage is disabled", func(t *testing.T) {
		t.Parallel()
		b, ctx := setupBackendTestStorageDisabled(t)

		ch, err := b.WatchWriteEvents(ctx)
		require.Nil(t, ch)
		require.Error(t, err)
		require.ErrorContains(t, err, "watcher is not enabled")
	})
}

// setupBackendTestStorageDisabled creates a test backend with storage disabled (search-only/read-only mode)
func setupBackendTestStorageDisabled(t *testing.T) (testBackend, context.Context) {
	t.Helper()

	ctx := testutil.NewDefaultTestContext(t)
	dbp := test.NewDBProviderMatchWords(t)
	b, err := NewBackend(BackendOptions{DBProvider: dbp, DisableStorageServices: true})
	require.NoError(t, err)
	require.NotNil(t, b)
	svc, ok := b.(services.Service)
	require.True(t, ok)
	require.NoError(t, services.StartAndAwaitRunning(ctx, svc))

	bb, ok := b.(*backend)
	require.True(t, ok)
	require.NotNil(t, bb)

	return testBackend{
		backend:        bb,
		TestDBProvider: dbp,
	}, ctx
}

func TestSqlPruneHistoryRequest_Validate(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name    string
		req     sqlPruneHistoryRequest
		wantErr string
	}{
		{
			name: "valid namespaced key",
			req: sqlPruneHistoryRequest{
				Key: &resourcepb.ResourceKey{
					Namespace: "default",
					Group:     "apps",
					Resource:  "deployments",
					Name:      "my-deploy",
				},
				HistoryLimit: 10,
			},
		},
		{
			name: "valid cluster-scoped key (no namespace)",
			req: sqlPruneHistoryRequest{
				Key: &resourcepb.ResourceKey{
					Namespace: "",
					Group:     "cluster.example.io",
					Resource:  "clusterresources",
					Name:      "my-cluster-resource",
				},
				HistoryLimit: 10,
			},
		},
		{
			name: "missing key",
			req: sqlPruneHistoryRequest{
				HistoryLimit: 10,
			},
			wantErr: "missing key",
		},
		{
			name: "missing group",
			req: sqlPruneHistoryRequest{
				Key: &resourcepb.ResourceKey{
					Namespace: "default",
					Resource:  "deployments",
					Name:      "my-deploy",
				},
				HistoryLimit: 10,
			},
			wantErr: "missing group",
		},
		{
			name: "missing resource",
			req: sqlPruneHistoryRequest{
				Key: &resourcepb.ResourceKey{
					Namespace: "default",
					Group:     "apps",
					Name:      "my-deploy",
				},
				HistoryLimit: 10,
			},
			wantErr: "missing resource",
		},
		{
			name: "invalid history limit",
			req: sqlPruneHistoryRequest{
				Key: &resourcepb.ResourceKey{
					Namespace: "default",
					Group:     "apps",
					Resource:  "deployments",
					Name:      "my-deploy",
				},
				HistoryLimit: 0,
			},
			wantErr: "history limit must be greater than zero",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			err := tc.req.Validate()
			if tc.wantErr != "" {
				require.ErrorContains(t, err, tc.wantErr)
			} else {
				require.NoError(t, err)
			}
		})
	}
}
