/*
Copyright © contributors to CloudNativePG, established as
CloudNativePG a Series of LF Projects, LLC.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

SPDX-License-Identifier: Apache-2.0
*/

package client

import (
	"time"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"

	barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

var scheme = buildScheme()

func buildScheme() *runtime.Scheme {
	scheme := runtime.NewScheme()
	_ = corev1.AddToScheme(scheme)
	_ = barmancloudv1.AddToScheme(scheme)

	return scheme
}

func addToCache(c *ExtendedClient, obj client.Object, fetchUnixTime int64) {
	ce := cachedEntry{
		entry:         obj.DeepCopyObject().(client.Object),
		fetchUnixTime: fetchUnixTime,
		ttlSeconds:    DefaultTTLSeconds,
	}
	ce.entry.SetResourceVersion("from cache")
	c.cachedObjects = append(c.cachedObjects, ce)
}

var _ = Describe("ExtendedClient Get", func() {
	var (
		extendedClient *ExtendedClient
		secretInClient *corev1.Secret
		objectStore    *barmancloudv1.ObjectStore
	)

	BeforeEach(func() {
		secretInClient = &corev1.Secret{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: "default",
				Name:      "test-secret",
			},
		}
		objectStore = &barmancloudv1.ObjectStore{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: "default",
				Name:      "test-object-store",
			},
			Spec: barmancloudv1.ObjectStoreSpec{},
		}

		baseClient := fake.NewClientBuilder().
			WithScheme(scheme).
			WithObjects(secretInClient, objectStore).Build()
		extendedClient = NewExtendedClient(baseClient).(*ExtendedClient)
	})

	It("returns secret from cache if not expired", func(ctx SpecContext) {
		secretNotInClient := &corev1.Secret{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: "default",
				Name:      "test-secret-not-in-client",
			},
		}

		// manually add the secret to the cache, this is not present in the fake client,
		// so we are sure it is from the cache
		addToCache(extendedClient, secretNotInClient, time.Now().Unix())

		err := extendedClient.Get(ctx, client.ObjectKeyFromObject(secretNotInClient), secretInClient)
		Expect(err).NotTo(HaveOccurred())
		Expect(secretInClient).To(Equal(extendedClient.cachedObjects[0].entry))
		Expect(secretInClient.GetResourceVersion()).To(Equal("from cache"))
	})

	It("fetches secret from base client if cache is expired", func(ctx SpecContext) {
		addToCache(extendedClient, secretInClient, time.Now().Add(-2*time.Minute).Unix())

		err := extendedClient.Get(ctx, client.ObjectKeyFromObject(secretInClient), secretInClient)
		Expect(err).NotTo(HaveOccurred())
		Expect(secretInClient.GetResourceVersion()).NotTo(Equal("from cache"))

		// the cache is updated with the new value
		Expect(extendedClient.cachedObjects).To(HaveLen(1))
		Expect(extendedClient.cachedObjects[0].entry.GetResourceVersion()).NotTo(Equal("from cache"))
	})

	It("fetches secret from base client if not in cache", func(ctx SpecContext) {
		err := extendedClient.Get(ctx, client.ObjectKeyFromObject(secretInClient), secretInClient)
		Expect(err).NotTo(HaveOccurred())

		// the cache is updated with the new value
		Expect(extendedClient.cachedObjects).To(HaveLen(1))
	})

	It("does not cache non-secret objects", func(ctx SpecContext) {
		configMap := &corev1.ConfigMap{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: "default",
				Name:      "test-configmap",
			},
		}
		err := extendedClient.Create(ctx, configMap)
		Expect(err).ToNot(HaveOccurred())

		err = extendedClient.Get(ctx, client.ObjectKeyFromObject(configMap), configMap)
		Expect(err).NotTo(HaveOccurred())
		Expect(extendedClient.cachedObjects).To(BeEmpty())
	})

	It("returns the correct object from cache when multiple objects with the same object key are cached",
		func(ctx SpecContext) {
			secretNotInClient := &corev1.Secret{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: "default",
					Name:      "common-name",
				},
			}
			objectStoreNotInClient := &barmancloudv1.ObjectStore{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: "default",
					Name:      "common-name",
				},
			}

			// manually add the objects to the cache, these are not present in the fake client,
			// so we are sure they are from the cache
			addToCache(extendedClient, secretNotInClient, time.Now().Unix())
			addToCache(extendedClient, objectStoreNotInClient, time.Now().Unix())

			err := extendedClient.Get(ctx, client.ObjectKeyFromObject(secretNotInClient), secretInClient)
			Expect(err).NotTo(HaveOccurred())
			err = extendedClient.Get(ctx, client.ObjectKeyFromObject(objectStoreNotInClient), objectStore)
			Expect(err).NotTo(HaveOccurred())

			Expect(secretInClient.GetResourceVersion()).To(Equal("from cache"))
			Expect(objectStore.GetResourceVersion()).To(Equal("from cache"))
		})
})
