// SPDX-License-Identifier: AGPL-3.0-only
// Provenance-includes-location: https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apiserver/pkg/storage/cacher/cacher_test.go
// Provenance-includes-license: Apache-2.0
// Provenance-includes-copyright: The Kubernetes Authors.

package apistore_test

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/fields"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/runtime"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	"k8s.io/apiserver/pkg/apis/example"
	examplev1 "k8s.io/apiserver/pkg/apis/example/v1"
	"k8s.io/apiserver/pkg/storage"
	"k8s.io/apiserver/pkg/storage/storagebackend"

	claims "github.com/grafana/authlib/types"
	"github.com/grafana/grafana/pkg/apimachinery/identity"
	storagetesting "github.com/grafana/grafana/pkg/apiserver/storage/testing"
	"github.com/grafana/grafana/pkg/storage/unified/apistore"
	"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
)

func init() {
	metav1.AddToGroupVersion(scheme, metav1.SchemeGroupVersion)
	utilruntime.Must(example.AddToScheme(scheme))
	utilruntime.Must(examplev1.AddToScheme(scheme))

	// Make sure there is a user in every context
	storagetesting.NewContext = func() context.Context {
		testUserA := &identity.StaticRequester{
			Type:           claims.TypeUser,
			Login:          "testuser",
			UserID:         123,
			UserUID:        "u123",
			OrgRole:        identity.RoleAdmin,
			IsGrafanaAdmin: true, // can do anything
		}
		return identity.WithRequester(context.Background(), testUserA)
	}
}

// GetPodAttrs returns labels and fields of a given object for filtering purposes.
func GetPodAttrs(obj runtime.Object) (labels.Set, fields.Set, error) {
	pod, ok := obj.(*example.Pod)
	if !ok {
		return nil, nil, fmt.Errorf("not a pod")
	}
	return labels.Set(pod.Labels), PodToSelectableFields(pod), nil
}

// PodToSelectableFields returns a field set that represents the object
// TODO: fields are not labels, and the validation rules for them do not apply.
func PodToSelectableFields(pod *example.Pod) fields.Set {
	// The purpose of allocation with a given number of elements is to reduce
	// amount of allocations needed to create the fields.Set. If you add any
	// field here or the number of object-meta related fields changes, this should
	// be adjusted.
	podSpecificFieldsSet := make(fields.Set, 5)
	podSpecificFieldsSet["spec.nodeName"] = pod.Spec.NodeName
	podSpecificFieldsSet["spec.restartPolicy"] = string(pod.Spec.RestartPolicy)
	podSpecificFieldsSet["status.phase"] = string(pod.Status.Phase)
	return AddObjectMetaFieldsSet(podSpecificFieldsSet, &pod.ObjectMeta, true)
}

func AddObjectMetaFieldsSet(source fields.Set, objectMeta *metav1.ObjectMeta, hasNamespaceField bool) fields.Set {
	source["metadata.name"] = objectMeta.Name
	if hasNamespaceField {
		source["metadata.namespace"] = objectMeta.Namespace
	}
	return source
}

func checkStorageInvariants(s storage.Interface) storagetesting.KeyValidation {
	return func(ctx context.Context, t *testing.T, key string) {
		obj := &example.Pod{}
		err := s.Get(ctx, key, storage.GetOptions{}, obj)
		if err != nil {
			t.Fatalf("Get failed: %v", err)
		}
	}
}

func TestCreate(t *testing.T) {
	ctx, store, destroyFunc, err := testSetup(t)
	defer destroyFunc()
	assert.NoError(t, err)
	storagetesting.RunTestCreate(ctx, t, store, checkStorageInvariants(store))
}

// No TTL support in unifed storage
// func TestCreateWithTTL(t *testing.T) {
// 	ctx, store, destroyFunc, err := testSetup(t)
// 	defer destroyFunc()
// 	assert.NoError(t, err)
// 	storagetesting.RunTestCreateWithTTL(ctx, t, store)
// }

func TestCreateWithKeyExist(t *testing.T) {
	ctx, store, destroyFunc, err := testSetup(t)
	defer destroyFunc()
	assert.NoError(t, err)
	storagetesting.RunTestCreateWithKeyExist(ctx, t, store)
}

func TestValidUpdate(t *testing.T) {
	ctx, store, destroyFunc, err := testSetup(t)
	defer destroyFunc()
	assert.NoError(t, err)
	storagetesting.RunTestValidUpdate(ctx, t, store)
}

func TestGet(t *testing.T) {
	ctx, store, destroyFunc, err := testSetup(t)
	defer destroyFunc()
	assert.NoError(t, err)
	storagetesting.RunTestGet(ctx, t, store)
}

func TestUnconditionalDelete(t *testing.T) {
	ctx, store, destroyFunc, err := testSetup(t)
	defer destroyFunc()
	assert.NoError(t, err)
	storagetesting.RunTestUnconditionalDelete(ctx, t, store)
}

func TestConditionalDelete(t *testing.T) {
	ctx, store, destroyFunc, err := testSetup(t)
	defer destroyFunc()
	assert.NoError(t, err)
	storagetesting.RunTestConditionalDelete(ctx, t, store)
}

func TestDeleteWithSuggestion(t *testing.T) {
	ctx, store, destroyFunc, err := testSetup(t)
	defer destroyFunc()
	assert.NoError(t, err)
	storagetesting.RunTestDeleteWithSuggestion(ctx, t, store)
}

func TestDeleteWithSuggestionAndConflict(t *testing.T) {
	ctx, store, destroyFunc, err := testSetup(t)
	defer destroyFunc()
	assert.NoError(t, err)
	storagetesting.RunTestDeleteWithSuggestionAndConflict(ctx, t, store)
}

type resourceClientMock struct {
	resourcepb.ResourceStoreClient
	resourcepb.ResourceIndexClient
	resourcepb.ManagedObjectIndexClient
	resourcepb.BulkStoreClient
	resourcepb.BlobStoreClient
	resourcepb.DiagnosticsClient
	resourcepb.QuotasClient
}

// always return GRPC Unauthenticated code
func (r resourceClientMock) List(ctx context.Context, in *resourcepb.ListRequest, opts ...grpc.CallOption) (*resourcepb.ListResponse, error) {
	return &resourcepb.ListResponse{}, status.Error(codes.Unauthenticated, "missing token")
}

func TestGRPCtoHTTPStatusMapping(t *testing.T) {
	t.Run("ensure that GRPC Unauthenticated code gets translated to HTTP StatusUnauthorized", func(t *testing.T) {
		s, _, err := apistore.NewStorage(
			&storagebackend.ConfigForResource{},
			resourceClientMock{},
			nil,
			nil,
			nil,
			nil,
			nil,
			nil,
			nil,
			nil,
			apistore.StorageOptions{})
		require.NoError(t, err)

		err = s.GetList(context.Background(), "/group/resource.grafana.app/resource/resources/namespace/default", storage.ListOptions{}, nil)
		require.Error(t, err)

		var statusError *apierrors.StatusError
		ok := errors.As(err, &statusError)
		require.Equal(t, true, ok)
		require.Equal(t, int(statusError.Status().Code), http.StatusUnauthorized)
	})
}

// TODO: this test relies on update
//func TestDeleteWithSuggestionOfDeletedObject(t *testing.T) {
//	ctx, store, destroyFunc, err := testSetup(t)
//	defer destroyFunc()
//	assert.NoError(t, err)
//	storagetesting.RunTestDeleteWithSuggestionOfDeletedObject(ctx, t, store)
//}

//func TestValidateDeletionWithSuggestion(t *testing.T) {
//	ctx, store, destroyFunc, err := testSetup(t)
//	defer destroyFunc()
//	assert.NoError(t, err)
//	storagetesting.RunTestValidateDeletionWithSuggestion(ctx, t, store)
//}
//
//func TestPreconditionalDeleteWithSuggestion(t *testing.T) {
//	ctx, store, destroyFunc, err := testSetup(t)
//	defer destroyFunc()
//	assert.NoError(t, err)
//	storagetesting.RunTestPreconditionalDeleteWithSuggestion(ctx, t, store)
//}

//func TestList(t *testing.T) {
//	ctx, store, destroyFunc, err := testSetup(t)
//	defer destroyFunc()
//	assert.NoError(t, err)
//	storagetesting.RunTestList(ctx, t, store, func(ctx context.Context, t *testing.T, rv string) {
//
//	}, true)
//}

//func compact(store *Storage) storagetesting.Compaction {
//	return func(ctx context.Context, t *testing.T, resourceVersion string) {
//		// tests expect that the resource version is incremented after compaction:
//		// https://github.com/kubernetes/apiserver/blob/4f7f407e71725f4056328bbeb6d6139843716ca6/pkg/storage/etcd3/compact.go#L137
//		_ = store.getNewResourceVersion()
//	}
//}

//func TestGetListNonRecursive(t *testing.T) {
//	ctx, store, destroyFunc, err := testSetup(t)
//	defer destroyFunc()
//	assert.NoError(t, err)
//	storagetesting.RunTestGetListNonRecursive(ctx, t, compact(store.(*Storage)), store)
//}

//func checkStorageCalls(t *testing.T, pageSize, estimatedProcessedObjects uint64) {
//	if reads := getReadsAndReset(); reads != estimatedProcessedObjects {
//		t.Errorf("unexpected reads: %d, expected: %d", reads, estimatedProcessedObjects)
//	}
//	estimatedGetCalls := uint64(1)
//	if pageSize != 0 {
//		// We expect that kube-apiserver will be increasing page sizes
//		// if not full pages are received, so we should see significantly less
//		// than 1000 pages (which would be result of talking to etcd with page size
//		// copied from pred.Limit).
//		// The expected number of calls is n+1 where n is the smallest n so that:
//		// pageSize + pageSize * 2 + pageSize * 4 + ... + pageSize * 2^n >= podCount.
//		// For pageSize = 1, podCount = 1000, we get n+1 = 10, 2 ^ 10 = 1024.
//		currLimit := pageSize
//		for sum := uint64(1); sum < estimatedProcessedObjects; {
//			currLimit *= 2
//			if currLimit > 10000 {
//				currLimit = 10000
//			}
//			sum += currLimit
//			estimatedGetCalls++
//		}
//	}
//	if reads := getReadsAndReset(); reads != estimatedGetCalls {
//		t.Errorf("unexpected reads: %d, expected: %d", reads, estimatedProcessedObjects)
//	}
//}

//func TestListContinuation(t *testing.T) {
//	ctx, store, destroyFunc, err := testSetup(t)
//	defer destroyFunc()
//	assert.NoError(t, err)
//	storagetesting.RunTestListContinuation(ctx, t, store, checkStorageCalls)
//}
//
//func TestListPaginationRareObject(t *testing.T) {
//	ctx, store, destroyFunc, err := testSetup(t)
//	defer destroyFunc()
//	assert.NoError(t, err)
//	storagetesting.RunTestListPaginationRareObject(ctx, t, store, checkStorageCalls)
//}
//
//func TestListContinuationWithFilter(t *testing.T) {
//	ctx, store, destroyFunc, err := testSetup(t)
//	defer destroyFunc()
//	assert.NoError(t, err)
//	storagetesting.RunTestListContinuationWithFilter(ctx, t, store, checkStorageCalls)
//}
//
//func TestListInconsistentContinuation(t *testing.T) {
//	ctx, store, destroyFunc, err := testSetup(t)
//	defer destroyFunc()
//	assert.NoError(t, err)
//	storagetesting.RunTestListInconsistentContinuation(ctx, t, store, compact(store.(*Storage)))
//}
//
//func TestConsistentList(t *testing.T) {
//	// TODO(#109831): Enable use of this test and run it.
//}
//
//func TestGuaranteedUpdate(t *testing.T) {
//	// ctx, store, destroyFunc, err := testSetup(t)
//	// defer destroyFunc()
//	// assert.NoError(t, err)
//	// storagetesting.RunTestGuaranteedUpdate(ctx, t, store, nil)
//}
//
//func TestGuaranteedUpdateWithTTL(t *testing.T) {
//	ctx, store, destroyFunc, err := testSetup(t)
//	defer destroyFunc()
//	assert.NoError(t, err)
//	storagetesting.RunTestGuaranteedUpdateWithTTL(ctx, t, store)
//}
//
//func TestGuaranteedUpdateChecksStoredData(t *testing.T) {
//	// ctx, store, destroyFunc, err := testSetup(t)
//	// defer destroyFunc()
//	// assert.NoError(t, err)
//	// storagetesting.RunTestGuaranteedUpdateChecksStoredData(ctx, t, store)
//}
//
//func TestGuaranteedUpdateWithConflict(t *testing.T) {
//	ctx, store, destroyFunc, err := testSetup(t)
//	defer destroyFunc()
//	assert.NoError(t, err)
//	storagetesting.RunTestGuaranteedUpdateWithConflict(ctx, t, store)
//}
//
//func TestGuaranteedUpdateWithSuggestionAndConflict(t *testing.T) {
//	ctx, store, destroyFunc, err := testSetup(t)
//	defer destroyFunc()
//	assert.NoError(t, err)
//	storagetesting.RunTestGuaranteedUpdateWithSuggestionAndConflict(ctx, t, store)
//}

func TestTransformationFailure(t *testing.T) {
	// TODO(#109831): Enable use of this test and run it.
}

//func TestCount(t *testing.T) {
//	ctx, store, destroyFunc, err := testSetup(t)
//	defer destroyFunc()
//	assert.NoError(t, err)
//	storagetesting.RunTestCount(ctx, t, store)
//}
