/*
Copyright 2025 The Kubernetes Authors.
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.
*/

package informers

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/runtime/schema"
	kubeinformers "k8s.io/client-go/informers"
	"k8s.io/client-go/kubernetes/fake"
)

func TestTransformRemoveManagedFields(t *testing.T) {
	t.Run("removes managed fields from Service", func(t *testing.T) {
		svc := fakeService()
		require.NotEmpty(t, svc.ManagedFields)

		transform := TransformerWithOptions[*corev1.Service](TransformRemoveManagedFields())
		got, err := transform(svc)
		require.NoError(t, err)
		result := got.(*corev1.Service)
		assert.Empty(t, result.ManagedFields)
		// unrelated fields must be preserved
		assert.NotEmpty(t, result.Name)
		assert.NotEmpty(t, result.Spec.Selector)
		assert.NotEmpty(t, result.Status.LoadBalancer.Ingress)
	})

	t.Run("removes managed fields from Pod", func(t *testing.T) {
		pod := fakePod()
		require.NotEmpty(t, pod.ManagedFields)

		transform := TransformerWithOptions[*corev1.Pod](TransformRemoveManagedFields())
		got, err := transform(pod)
		require.NoError(t, err)
		result := got.(*corev1.Pod)
		assert.Empty(t, result.ManagedFields)
		assert.NotEmpty(t, result.Name)
		assert.NotEmpty(t, result.Spec.NodeName)
	})

	t.Run("idempotent when managed fields already nil", func(t *testing.T) {
		svc := fakeService()
		svc.ManagedFields = nil

		transform := TransformerWithOptions[*corev1.Service](TransformRemoveManagedFields())
		got, err := transform(svc)
		require.NoError(t, err)
		assert.Empty(t, got.(*corev1.Service).ManagedFields)
	})
}

func TestTransformRemoveLastAppliedConfig(t *testing.T) {
	t.Run("removes last-applied-configuration annotation", func(t *testing.T) {
		svc := fakeService()
		require.Contains(t, svc.Annotations, corev1.LastAppliedConfigAnnotation)

		transform := TransformerWithOptions[*corev1.Service](TransformRemoveLastAppliedConfig())
		got, err := transform(svc)
		require.NoError(t, err)
		result := got.(*corev1.Service)
		assert.NotContains(t, result.Annotations, corev1.LastAppliedConfigAnnotation)
		// other annotations must survive
		assert.Contains(t, result.Annotations, "description")
		assert.Contains(t, result.Annotations, "external-dns.alpha.kubernetes.io/hostname")
	})

	t.Run("idempotent when annotation is absent", func(t *testing.T) {
		svc := fakeService()
		delete(svc.Annotations, corev1.LastAppliedConfigAnnotation)

		transform := TransformerWithOptions[*corev1.Service](TransformRemoveLastAppliedConfig())
		got, err := transform(svc)
		require.NoError(t, err)
		assert.NotContains(t, got.(*corev1.Service).Annotations, corev1.LastAppliedConfigAnnotation)
	})
}

func TestTransformRemoveStatusConditions(t *testing.T) {
	t.Run("removes conditions from Service", func(t *testing.T) {
		svc := fakeService()
		require.NotEmpty(t, svc.Status.Conditions)

		transform := TransformerWithOptions[*corev1.Service](TransformRemoveStatusConditions())
		got, err := transform(svc)
		require.NoError(t, err)
		result := got.(*corev1.Service)
		assert.Empty(t, result.Status.Conditions)
		// unrelated status fields must be preserved
		assert.NotEmpty(t, result.Status.LoadBalancer.Ingress)
	})

	t.Run("removes conditions from Pod", func(t *testing.T) {
		pod := fakePod()
		pod.Status.Conditions = []corev1.PodCondition{
			{Type: corev1.PodReady, Status: corev1.ConditionTrue},
		}
		require.NotEmpty(t, pod.Status.Conditions)

		transform := TransformerWithOptions[*corev1.Pod](TransformRemoveStatusConditions())
		got, err := transform(pod)
		require.NoError(t, err)
		assert.Empty(t, got.(*corev1.Pod).Status.Conditions)
	})

	t.Run("removes conditions from Node", func(t *testing.T) {
		node := fakeNode()
		require.NotEmpty(t, node.Status.Conditions)

		transform := TransformerWithOptions[*corev1.Node](TransformRemoveStatusConditions())
		got, err := transform(node)
		require.NoError(t, err)
		result := got.(*corev1.Node)
		assert.Empty(t, result.Status.Conditions)
		// Status.Addresses must be preserved
		assert.NotEmpty(t, result.Status.Addresses)
	})

	t.Run("no-op when conditions are already empty", func(t *testing.T) {
		svc := fakeService()
		svc.Status.Conditions = nil

		transform := TransformerWithOptions[*corev1.Service](TransformRemoveStatusConditions())
		got, err := transform(svc)
		require.NoError(t, err)
		assert.Empty(t, got.(*corev1.Service).Status.Conditions)
	})
}

func TestTransformKeepAnnotationPrefix(t *testing.T) {
	t.Run("keeps only matching prefix", func(t *testing.T) {
		pod := fakePod()
		require.Len(t, pod.Annotations, 3)

		transform := TransformerWithOptions[*corev1.Pod](TransformKeepAnnotationPrefix("external-dns.alpha.kubernetes.io/"))
		got, err := transform(pod)
		require.NoError(t, err)
		result := got.(*corev1.Pod)
		assert.Equal(t, map[string]string{
			"external-dns.alpha.kubernetes.io/hostname": "pod.example.com",
		}, result.Annotations)
	})

	t.Run("multiple prefixes use OR logic", func(t *testing.T) {
		pod := fakePod()

		transform := TransformerWithOptions[*corev1.Pod](
			TransformKeepAnnotationPrefix("external-dns.alpha.kubernetes.io/"),
			TransformKeepAnnotationPrefix("unrelated.io/"),
		)
		got, err := transform(pod)
		require.NoError(t, err)
		result := got.(*corev1.Pod)
		assert.Contains(t, result.Annotations, "external-dns.alpha.kubernetes.io/hostname")
		assert.Contains(t, result.Annotations, "unrelated.io/annotation")
		assert.NotContains(t, result.Annotations, corev1.LastAppliedConfigAnnotation)
	})

	t.Run("nil annotations map is left unchanged", func(t *testing.T) {
		pod := fakePod()
		pod.Annotations = nil

		transform := TransformerWithOptions[*corev1.Pod](TransformKeepAnnotationPrefix("external-dns.alpha.kubernetes.io/"))
		got, err := transform(pod)
		require.NoError(t, err)
		assert.Nil(t, got.(*corev1.Pod).Annotations)
	})
}

func TestTransformRequireAnnotation(t *testing.T) {
	t.Run("matching selector keeps object", func(t *testing.T) {
		svc := fakeService() // annotations include external-dns.alpha.kubernetes.io/hostname=example.com
		sel, err := labels.Parse("external-dns.alpha.kubernetes.io/hostname=example.com")
		require.NoError(t, err)

		transform := TransformerWithOptions[*corev1.Service](TransformRequireAnnotation(sel))
		got, err := transform(svc)
		require.NoError(t, err)
		require.NotNil(t, got)
		assert.Equal(t, svc.Name, got.(*corev1.Service).Name)
	})

	t.Run("non-matching selector drops object", func(t *testing.T) {
		svc := fakeService()
		sel, err := labels.Parse("external-dns.alpha.kubernetes.io/hostname=other.com")
		require.NoError(t, err)

		transform := TransformerWithOptions[*corev1.Service](TransformRequireAnnotation(sel))
		got, err := transform(svc)
		require.NoError(t, err)
		assert.Nil(t, got)
	})

	t.Run("nil selector is a no-op", func(t *testing.T) {
		svc := fakeService()
		transform := TransformerWithOptions[*corev1.Service](TransformRequireAnnotation(nil))
		got, err := transform(svc)
		require.NoError(t, err)
		assert.NotNil(t, got)
	})

	t.Run("empty selector is a no-op", func(t *testing.T) {
		svc := fakeService()
		transform := TransformerWithOptions[*corev1.Service](TransformRequireAnnotation(labels.Everything()))
		got, err := transform(svc)
		require.NoError(t, err)
		assert.NotNil(t, got)
	})

	t.Run("drops object after annotation mutation (simulates MODIFIED event)", func(t *testing.T) {
		sel, err := labels.Parse("external-dns.alpha.kubernetes.io/hostname=example.com")
		require.NoError(t, err)
		transform := TransformerWithOptions[*corev1.Service](TransformRequireAnnotation(sel))

		// First call: annotation matches, object is admitted.
		svc := fakeService() // annotations include external-dns.alpha.kubernetes.io/hostname=example.com
		got, err := transform(svc)
		require.NoError(t, err)
		require.NotNil(t, got)

		// Annotation mutates — simulate MODIFIED event with new value.
		svc.Annotations["external-dns.alpha.kubernetes.io/hostname"] = "other.com"
		got, err = transform(svc)
		require.NoError(t, err)
		assert.Nil(t, got, "mutated object must be dropped as a local guard")
	})
}

func TestTransformerWithOptions_Combined(t *testing.T) {
	svc := fakeService()

	transform := TransformerWithOptions[*corev1.Service](
		TransformRemoveManagedFields(),
		TransformRemoveLastAppliedConfig(),
		TransformRemoveStatusConditions(),
		TransformKeepAnnotationPrefix("external-dns.alpha.kubernetes.io/"),
	)
	got, err := transform(svc)
	require.NoError(t, err)
	result := got.(*corev1.Service)

	assert.Empty(t, result.ManagedFields)
	assert.Empty(t, result.Status.Conditions)
	assert.NotContains(t, result.Annotations, corev1.LastAppliedConfigAnnotation)
	assert.NotContains(t, result.Annotations, "description")
	assert.Contains(t, result.Annotations, "external-dns.alpha.kubernetes.io/hostname")
	// Spec and remaining Status fields are fully preserved
	assert.NotEmpty(t, result.Spec.Selector)
	assert.NotEmpty(t, result.Spec.ExternalIPs)
	assert.NotEmpty(t, result.Status.LoadBalancer.Ingress)
}

func TestTransformerWithOptions_TypeMismatch(t *testing.T) {
	t.Run("non-matching type returns nil", func(t *testing.T) {
		transform := TransformerWithOptions[*corev1.Service](TransformRemoveManagedFields())
		got, err := transform(fakePod())
		require.NoError(t, err)
		assert.Nil(t, got)
	})

	t.Run("non-matching primitive returns nil", func(t *testing.T) {
		transform := TransformerWithOptions[*corev1.Service]()
		got, err := transform("not-a-service")
		require.NoError(t, err)
		assert.Nil(t, got)
	})
}

func TestTransformerWithOptions_Idempotent(t *testing.T) {
	svc := fakeService()
	transform := TransformerWithOptions[*corev1.Service](
		TransformRemoveManagedFields(),
		TransformRemoveLastAppliedConfig(),
		TransformRemoveStatusConditions(),
	)
	first, err := transform(svc)
	require.NoError(t, err)
	second, err := transform(first)
	require.NoError(t, err)

	r1 := first.(*corev1.Service)
	r2 := second.(*corev1.Service)
	assert.Empty(t, r1.ManagedFields)
	assert.Empty(t, r2.ManagedFields)
	assert.NotContains(t, r1.Annotations, corev1.LastAppliedConfigAnnotation)
	assert.NotContains(t, r2.Annotations, corev1.LastAppliedConfigAnnotation)
	assert.Empty(t, r1.Status.Conditions)
	assert.Empty(t, r2.Status.Conditions)
}

func TestTransformerWithOptions_WithFakeClient(t *testing.T) {
	ctx := t.Context()
	svc := fakeService()
	fakeClient := fake.NewClientset()

	_, err := fakeClient.CoreV1().Services(svc.Namespace).Create(ctx, svc, metav1.CreateOptions{})
	require.NoError(t, err)

	factory := kubeinformers.NewSharedInformerFactoryWithOptions(fakeClient, 0, kubeinformers.WithNamespace(svc.Namespace))
	serviceInformer := factory.Core().V1().Services()
	err = serviceInformer.Informer().SetTransform(TransformerWithOptions[*corev1.Service](
		TransformRemoveManagedFields(),
		TransformRemoveLastAppliedConfig(),
		TransformRemoveStatusConditions(),
	))
	require.NoError(t, err)

	factory.Start(ctx.Done())
	err = WaitForCacheSync(ctx, factory)
	require.NoError(t, err)

	got, err := serviceInformer.Lister().Services(svc.Namespace).Get(svc.Name)
	require.NoError(t, err)

	assert.Empty(t, got.ManagedFields)
	assert.Empty(t, got.Status.Conditions)
	assert.NotContains(t, got.Annotations, corev1.LastAppliedConfigAnnotation)
	// TypeMeta is populated by the transformer
	assert.Equal(t, "Service", got.Kind)
	assert.NotEmpty(t, got.APIVersion)
	// Spec and remaining Status are preserved
	assert.Equal(t, svc.Spec.Selector, got.Spec.Selector)
	assert.Equal(t, svc.Spec.ExternalIPs, got.Spec.ExternalIPs)
	assert.Equal(t, svc.Status.LoadBalancer.Ingress, got.Status.LoadBalancer.Ingress)
}

func TestPopulateGVK(t *testing.T) {
	t.Run("populates Kind and APIVersion on Service", func(t *testing.T) {
		svc := fakeService()
		require.Empty(t, svc.Kind)

		populateGVK(svc)
		assert.Equal(t, "Service", svc.Kind)
		assert.NotEmpty(t, svc.APIVersion)
	})

	t.Run("populates Kind and APIVersion on Node", func(t *testing.T) {
		node := fakeNode()
		require.Empty(t, node.Kind)

		populateGVK(node)
		assert.Equal(t, "Node", node.Kind)
		assert.NotEmpty(t, node.APIVersion)
	})

	t.Run("idempotent when GVK already set", func(t *testing.T) {
		svc := fakeService()
		svc.Kind = "Service"
		svc.APIVersion = "v1"

		populateGVK(svc)
		assert.Equal(t, "Service", svc.Kind)
		assert.Equal(t, "v1", svc.APIVersion)
	})

	t.Run("unstructured object retains its own GVK unchanged", func(t *testing.T) {
		gvk := schema.GroupVersionKind{Group: "example.com", Version: "v1alpha1", Kind: "MyResource"}
		obj := &unstructured.Unstructured{}
		obj.SetGroupVersionKind(gvk)

		populateGVK(obj)
		assert.Equal(t, gvk, obj.GroupVersionKind())
	})

	t.Run("Istio Gateway not in k8s scheme, Kind populated via reflection fallback", func(t *testing.T) {
		// Istio types are not registered in k8s.io/client-go/kubernetes/scheme, so the
		// scheme lookup fails. populateGVK falls back to reflection and derives Kind
		// from the Go struct name. Group and Version remain empty.
		gw := &networkingv1beta1.Gateway{}
		require.Empty(t, gw.Kind)

		populateGVK(gw)
		assert.Equal(t, "Gateway", gw.Kind)
		assert.Empty(t, gw.APIVersion) // Group/Version unknown without the Istio scheme
	})
}
