package k8s

import (
	"context"
	"encoding/json"
	"testing"

	"github.com/crossplane-contrib/provider-aws/apis/ec2/v1beta1"
	"github.com/crossplane-contrib/provider-aws/apis/eks/manualv1alpha1"
	v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	. "github.com/onsi/gomega"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"

	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/scheme"
	"github.com/tidbcloud/infra-provider/pkg/util"
)

func withLastAppliedAnnotation(obj client.Object) {
	bs, err := GetModifiedConfiguration(obj, v1beta1.SchemeGroupVersion, false)
	if err != nil {
		panic(err)
	}
	util.SetAnnotation(obj, consts.LastAppliedConfigAnnotation, string(bs))
}

func withLabel(key, val string) func(obj client.Object) {
	return func(obj client.Object) {
		labels := obj.GetLabels()
		if labels == nil {
			labels = map[string]string{}
		}
		labels[key] = val

		obj.SetLabels(labels)
	}
}

func withTag(key, val string) func(obj client.Object) {
	return func(obj client.Object) {
		vpc := obj.(*v1beta1.VPC)
		vpc.Spec.ForProvider.Tags = append(vpc.Spec.ForProvider.Tags, v1beta1.Tag{
			Key:   key,
			Value: val,
		})
	}

}

func newTestVPC(fns ...func(obj client.Object)) *v1beta1.VPC {
	vpc := &v1beta1.VPC{
		ObjectMeta: metav1.ObjectMeta{
			Name: "test-vpc",
			Annotations: map[string]string{
				"crossplane": "vpc-id-xxxxx",
			},
		},
		Spec: v1beta1.VPCSpec{
			ForProvider: v1beta1.VPCParameters{
				Tags: []v1beta1.Tag{
					{
						Key:   "Name",
						Value: "test-vpc",
					},
					{
						Key:   "crossplane-kind",
						Value: "vpc.ec2.aws.crossplane.io",
					},
					{
						Key:   "crossplane-name",
						Value: "test",
					},
				},
			},
		},
	}

	for _, fn := range fns {
		fn(vpc)
	}
	return vpc
}

func TestApplyX(t *testing.T) {
	cases := []struct {
		desc    string
		current []client.Object
		changes []struct {
			applyObj client.Object
			updateFn func(obj client.Object)
		}
		expected client.Object
	}{
		{
			desc: "create obj by apply",
			changes: []struct {
				applyObj client.Object
				updateFn func(obj client.Object)
			}{
				{
					applyObj: newTestVPC(),
				},
			},
			expected: newTestVPC(withLastAppliedAnnotation),
		},
		{
			desc: "apply obj multiple times: same obj",
			changes: []struct {
				applyObj client.Object
				updateFn func(obj client.Object)
			}{
				{
					applyObj: newTestVPC(),
				},
				{
					applyObj: newTestVPC(),
				},
			},
			expected: newTestVPC(withLastAppliedAnnotation),
		},
		{
			desc: "apply obj multiple times: change map",
			changes: []struct {
				applyObj client.Object
				updateFn func(obj client.Object)
			}{
				{
					applyObj: newTestVPC(withLabel("aaa", "bbb")),
				},
				{
					applyObj: newTestVPC(withLabel("ccc", "ddd")),
				},
			},
			expected: newTestVPC(withLabel("ccc", "ddd"), withLastAppliedAnnotation),
		},
		{
			desc: "apply obj multiple times: remove item from map",
			changes: []struct {
				applyObj client.Object
				updateFn func(obj client.Object)
			}{
				{
					applyObj: newTestVPC(withLabel("aaa", "bbb"), withLabel("ccc", "ddd")),
				},
				{
					applyObj: newTestVPC(withLabel("ccc", "ddd")),
				},
				{
					applyObj: newTestVPC(),
				},
			},
			expected: newTestVPC(withLastAppliedAnnotation),
		},
		{
			desc: "apply obj multiple times: change list",
			changes: []struct {
				applyObj client.Object
				updateFn func(obj client.Object)
			}{
				{
					applyObj: newTestVPC(withTag("aaa", "aaa")),
				},
				{
					applyObj: newTestVPC(withTag("ccc", "ddd")),
				},
			},
			expected: newTestVPC(withTag("ccc", "ddd"), withLastAppliedAnnotation),
		},
		{
			desc: "apply obj multiple times: remove item from list",
			changes: []struct {
				applyObj client.Object
				updateFn func(obj client.Object)
			}{
				{
					applyObj: newTestVPC(withTag("aaa", "aaa"), withTag("bbb", "bbb")),
				},
				{
					applyObj: newTestVPC(withTag("aaa", "aaa")),
				},
			},
			expected: newTestVPC(withTag("aaa", "aaa"), withLastAppliedAnnotation),
		},
		{
			desc: "apply obj multiple times: change item of list",
			changes: []struct {
				applyObj client.Object
				updateFn func(obj client.Object)
			}{
				{
					applyObj: newTestVPC(withTag("aaa", "aaa"), withTag("bbb", "bbb")),
				},
				{
					applyObj: newTestVPC(withTag("aaa", "bbb"), withTag("ccc", "ddd")),
				},
			},
			expected: newTestVPC(withTag("aaa", "bbb"), withTag("ccc", "ddd"), withLastAppliedAnnotation),
		},
		{
			desc: "apply obj which is changed by third party: change map",
			changes: []struct {
				applyObj client.Object
				updateFn func(obj client.Object)
			}{
				{
					applyObj: newTestVPC(),
				},
				{
					updateFn: withLabel("third-party.com/change", "test"),
				},
				{
					applyObj: newTestVPC(),
				},
			},
			expected: newTestVPC(withLastAppliedAnnotation, withLabel("third-party.com/change", "test")),
		},
		{
			desc: "apply obj which is changed by third party: append item into list",
			changes: []struct {
				applyObj client.Object
				updateFn func(obj client.Object)
			}{
				{
					applyObj: newTestVPC(withTag("aaa", "bbb")),
				},
				{
					updateFn: withTag("ccc", "ddd"),
				},
				{
					applyObj: newTestVPC(withTag("aaa", "bbb")),
				},
			},
			expected: newTestVPC(withTag("aaa", "bbb"), withLastAppliedAnnotation, withTag("ccc", "ddd")),
		},
		{
			desc: "apply obj which is changed by third party: change item of list",
			changes: []struct {
				applyObj client.Object
				updateFn func(obj client.Object)
			}{
				{
					applyObj: newTestVPC(withTag("aaa", "bbb")),
				},
				{
					updateFn: withTag("ccc", "ddd"),
				},
				{
					applyObj: newTestVPC(withTag("aaa", "bbb"), withTag("eee", "fff")),
				},
			},
			expected: newTestVPC(withTag("aaa", "bbb"), withTag("eee", "fff"), withLastAppliedAnnotation),
		},
	}

	for _, c := range cases {
		t.Run(c.desc, func(t *testing.T) {
			g := NewWithT(t)
			cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(c.current...).Build()
			var obj client.Object
			for _, change := range c.changes {
				if change.applyObj != nil {
					obj = change.applyObj.DeepCopyObject().(client.Object)
					err := Apply(context.Background(), cli, obj)
					if err != nil {
						panic(err)
					}
				} else {
					change.updateFn(obj)
					err := cli.Update(context.Background(), obj)
					if err != nil {
						panic(err)
					}
				}
			}
			// reset resource version and gvk to avoid diff
			obj.SetResourceVersion("")
			obj.GetObjectKind().SetGroupVersionKind(schema.GroupVersionKind{})
			g.Expect(obj).To(Equal(c.expected))
		})
	}
}

func TestApply(t *testing.T) {
	ctx := context.TODO()
	nodeGroup := manualv1alpha1.NodeGroup{
		ObjectMeta: metav1.ObjectMeta{
			Name: "test-ng",
			Labels: map[string]string{
				"foo": "bar",
			},
		},
		Spec: manualv1alpha1.NodeGroupSpec{
			ForProvider: manualv1alpha1.NodeGroupParameters{
				Region: "us-west-2",
				Tags: map[string]string{
					"hello": "world",
				},
			},
		},
	}
	vpc := v1beta1.VPC{
		ObjectMeta: metav1.ObjectMeta{
			Name: "test-vpc",
			Annotations: map[string]string{
				"crossplane": "vpc-id-xxxxx",
			},
		},
		Spec: v1beta1.VPCSpec{
			ForProvider: v1beta1.VPCParameters{
				Tags: []v1beta1.Tag{
					{
						Key:   "Name",
						Value: "test-vpc",
					},
					{
						Key:   "crossplane-kind",
						Value: "vpc.ec2.aws.crossplane.io",
					},
					{
						Key:   "crossplane-name",
						Value: "test",
					},
				},
			},
		},
		Status: v1beta1.VPCStatus{
			AtProvider: v1beta1.VPCObservation{
				IsDefault: false,
				VPCState:  "available",
			},
		},
	}

	cases := map[string]struct {
		existObjs []client.Object
		// createdByApply is true if the exists objects are created by Apply function.
		createdByApply bool
		// checkExpectedSpecAndLabels is true if the expectedObj should be checked.
		checkExpectedObj bool
		// modifiedObj is the object to be applied.
		modifiedObj client.Object
		// expectedObj is the expected object after the expect function.
		expectedObj client.Object
		expect      func(g *WithT, cli client.Client, modifiedObj client.Object)
	}{
		"try to create an invalid CR with Apply func": {
			existObjs:      []client.Object{},
			createdByApply: false,
			modifiedObj:    nil,
			expectedObj:    nil,
			expect: func(g *WithT, cli client.Client, _ client.Object) {
				g.Expect(Apply(ctx, cli, &manualv1alpha1.NodeGroup{})).ShouldNot(Succeed())
			},
		},
		"CR is NOT created by Apply func, apply with the same modification": {
			existObjs: []client.Object{
				nodeGroup.DeepCopy(),
			},
			createdByApply:   false,
			checkExpectedObj: true,
			modifiedObj:      nodeGroup.DeepCopy(),
			expectedObj:      nodeGroup.DeepCopy(),
			expect: func(g *WithT, cli client.Client, modifiedObj client.Object) {
				var ng manualv1alpha1.NodeGroup
				g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(modifiedObj), &ng)).To(Succeed())
				beforeRV := ng.GetResourceVersion()

				// copyModified is a copy of the modified object,
				// since the modified object will be modified by Apply function.
				copyModified := modifiedObj.DeepCopyObject().(client.Object)
				g.Expect(Apply(ctx, cli, copyModified)).Should(Succeed())
				g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(modifiedObj), &ng)).To(Succeed())
				// The resource version should change, since the exists object does not have the last applied annotation.
				g.Expect(ng.GetResourceVersion()).NotTo(Equal(beforeRV))

				beforeRV = ng.GetResourceVersion()
				g.Expect(Apply(ctx, cli, modifiedObj)).Should(Succeed())
				g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(modifiedObj), &ng)).To(Succeed())
				// The resource version should not change, since the object has the last applied annotation.
				g.Expect(ng.GetResourceVersion()).To(Equal(beforeRV))
			},
		},
		"CR is NOT created by Apply func, update the different field": {
			existObjs: []client.Object{
				nodeGroup.DeepCopy(),
			},
			createdByApply:   false,
			checkExpectedObj: false,
			modifiedObj:      nil,
			expectedObj:      nil,
			expect: func(g *WithT, cli client.Client, _ client.Object) {
				ng := nodeGroup.DeepCopy()
				ng.Spec.ForProvider.ClusterName = "test-cluster"
				ng.Spec.ForProvider.Labels = map[string]string{
					"foo": "bar",
				}
				expectedObj := ng.DeepCopy()
				g.Expect(Apply(ctx, cli, ng)).Should(Succeed())
				checkExpectedSpecAndLabels(g, ctx, cli, client.ObjectKeyFromObject(ng), expectedObj)
			},
		},
		"CR is created by Apply func, updated by different clients": {
			existObjs: []client.Object{
				nodeGroup.DeepCopy(),
			},
			createdByApply:   true,
			checkExpectedObj: false,
			modifiedObj:      nil,
			expectedObj:      nil,
			expect: func(g *WithT, cli client.Client, _ client.Object) {
				// Check if the nodegroup is created.
				currentNG := manualv1alpha1.NodeGroup{}
				g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(&nodeGroup), &currentNG)).To(Succeed())
				checkExpectedSpecAndLabels(g, ctx, cli, client.ObjectKeyFromObject(&nodeGroup), &nodeGroup)

				// Mimic a "third-party" (like Crossplane) set default values and add annotations to the same object.
				version := "1.18"
				currentNG.Spec.ForProvider.Version = &version
				currentNG.Spec.DeletionPolicy = v1.DeletionDelete
				util.SetAnnotation(&currentNG, "crossplane.io/external-create-pending", "test")
				expected := currentNG.DeepCopy()
				g.Expect(cli.Update(ctx, &currentNG)).Should(Succeed())
				g.Expect(currentNG.GetResourceVersion()).Should(Equal("2"))

				// Should not update the nodegroup when apply the same modification.
				modified := nodeGroup.DeepCopy()
				g.Expect(Apply(ctx, cli, modified)).Should(Succeed())
				// The resource version should not change.
				g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(&nodeGroup), &currentNG)).To(Succeed())
				g.Expect(currentNG.GetResourceVersion()).Should(Equal("2"))
				// The nodegroup should both have values from "third-party" and the original object.
				checkExpectedSpecAndLabels(g, ctx, cli, client.ObjectKeyFromObject(&nodeGroup), expected)
			},
		},
		"CR is NOT created by Apply func, updated by different clients": {
			existObjs: []client.Object{
				nodeGroup.DeepCopy(),
			},
			createdByApply:   false,
			checkExpectedObj: false,
			modifiedObj:      nil,
			expectedObj:      nil,
			expect: func(g *WithT, cli client.Client, _ client.Object) {
				currentNG := manualv1alpha1.NodeGroup{}
				g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(&nodeGroup), &currentNG)).To(Succeed())

				// setDefaultValues mimics a "third-party" (like Crossplane) to set default values.
				setDefaultValues := func(current *manualv1alpha1.NodeGroup) *manualv1alpha1.NodeGroup {
					version := "1.18"
					current.Spec.ForProvider.Version = &version
					current.Spec.DeletionPolicy = v1.DeletionDelete
					util.SetAnnotation(current, "crossplane.io/external-create-pending", "test")
					return current.DeepCopy()
				}

				_ = setDefaultValues(&currentNG)
				g.Expect(cli.Update(ctx, &currentNG)).Should(Succeed())
				// The resource version is 999 because the nodegroup is not created by Apply func.
				// After the update, the resource version is 1000.
				g.Expect(currentNG.GetResourceVersion()).Should(Equal("1000"))

				// Applied modification will overwrite the "third-party" modification,
				// because the nodegroup does not have the last applied annotation yet.
				modified := nodeGroup.DeepCopy()
				g.Expect(Apply(ctx, cli, modified)).Should(Succeed())
				g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(&nodeGroup), &currentNG)).To(Succeed())
				// The resource version should change, because the nodegroup does not have the last applied annotation.
				g.Expect(currentNG.GetResourceVersion()).Should(Equal("1001"))

				// The third-party try to set default values again.
				expected := setDefaultValues(&currentNG)
				g.Expect(cli.Update(ctx, &currentNG)).Should(Succeed())
				g.Expect(currentNG.GetResourceVersion()).Should(Equal("1002"))

				// The controller uses Apply func to update the nodegroup with the same modification as the last time.
				modified = nodeGroup.DeepCopy()
				g.Expect(Apply(ctx, cli, modified)).Should(Succeed())
				g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(&nodeGroup), &currentNG)).To(Succeed())
				// The resource version should not change, because the nodegroup has the last applied annotation.
				g.Expect(currentNG.GetResourceVersion()).Should(Equal("1002"))

				// The nodegroup should both have values from "third-party" and the original object.
				checkExpectedSpecAndLabels(g, ctx, cli, client.ObjectKeyFromObject(&nodeGroup), expected)
			},
		},
		"CR is created by Apply func, update list with different order": {
			existObjs: []client.Object{
				vpc.DeepCopy(),
			},
			createdByApply:   true,
			checkExpectedObj: false,
			modifiedObj:      vpc.DeepCopy(),
			expectedObj:      nil,
			expect: func(g *WithT, cli client.Client, modified client.Object) {
				// Change the order of tags.
				tags := []v1beta1.Tag{
					{
						Key:   "crossplane-kind",
						Value: "vpc.ec2.aws.crossplane.io",
					},
					{
						Key:   "Name",
						Value: "test-vpc",
					},
					{
						Key:   "crossplane-name",
						Value: "test",
					},
				}
				modified.(*v1beta1.VPC).Spec.ForProvider.Tags = tags

				current := v1beta1.VPC{}
				g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(modified), &current)).To(Succeed())
				currentRV := current.GetResourceVersion()

				g.Expect(Apply(ctx, cli, modified)).Should(Succeed())
				g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(modified), &current)).To(Succeed())
				g.Expect(current.GetResourceVersion()).ShouldNot(Equal(currentRV))
				g.Expect(current.Spec.ForProvider.Tags).Should(Equal(tags))
				g.Expect(current.Status).Should(Equal(vpc.Status))
				g.Expect(current.Annotations).Should(HaveKeyWithValue("crossplane", "vpc-id-xxxxx"))
				g.Expect(current.Annotations).Should(HaveLen(2))
			},
		},
		"CR is created by Apply func, update list with same order": {
			existObjs: []client.Object{
				vpc.DeepCopy(),
			},
			createdByApply:   true,
			checkExpectedObj: false,
			modifiedObj:      vpc.DeepCopy(),
			expectedObj:      nil,
			expect: func(g *WithT, cli client.Client, modified client.Object) {
				current := v1beta1.VPC{}
				g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(modified), &current)).To(Succeed())
				currentRV := current.GetResourceVersion()

				g.Expect(Apply(ctx, cli, modified)).Should(Succeed())
				g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(modified), &current)).To(Succeed())
				g.Expect(current.GetResourceVersion()).Should(Equal(currentRV))
				g.Expect(current.Status).Should(Equal(vpc.Status))
				g.Expect(current.Annotations).Should(HaveKeyWithValue("crossplane", "vpc-id-xxxxx"))
				g.Expect(current.Annotations).Should(HaveLen(2))
			},
		},
		"CR is NOT created by Apply func, update list with same order": {
			existObjs: []client.Object{
				vpc.DeepCopy(),
			},
			createdByApply:   false,
			checkExpectedObj: false,
			modifiedObj:      vpc.DeepCopy(),
			expectedObj:      nil,
			expect: func(g *WithT, cli client.Client, modified client.Object) {
				current := v1beta1.VPC{}
				g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(modified), &current)).To(Succeed())
				currentRV := current.GetResourceVersion()

				g.Expect(Apply(ctx, cli, modified)).Should(Succeed())
				g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(modified), &current)).To(Succeed())
				g.Expect(current.GetResourceVersion()).ShouldNot(Equal(currentRV))
				g.Expect(current.Status).Should(Equal(vpc.Status))
				g.Expect(current.Annotations).Should(HaveKeyWithValue("crossplane", "vpc-id-xxxxx"))
				g.Expect(current.Annotations).Should(HaveLen(2))
			},
		},
		"CR is NOT created by Apply func, update list with different order": {
			existObjs: []client.Object{
				vpc.DeepCopy(),
			},
			createdByApply:   false,
			checkExpectedObj: false,
			modifiedObj:      vpc.DeepCopy(),
			expectedObj:      nil,
			expect: func(g *WithT, cli client.Client, modified client.Object) {
				// Change the order of tags.
				tags := []v1beta1.Tag{
					{
						Key:   "crossplane-kind",
						Value: "vpc.ec2.aws.crossplane.io",
					},
					{
						Key:   "Name",
						Value: "test-vpc",
					},
					{
						Key:   "crossplane-name",
						Value: "test",
					},
				}
				modified.(*v1beta1.VPC).Spec.ForProvider.Tags = tags

				current := v1beta1.VPC{}
				g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(modified), &current)).To(Succeed())
				currentRV := current.GetResourceVersion()
				g.Expect(current.Annotations).Should(HaveKeyWithValue("crossplane", "vpc-id-xxxxx"))
				g.Expect(current.Annotations).Should(HaveLen(1))

				g.Expect(Apply(ctx, cli, modified)).Should(Succeed())
				g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(modified), &current)).To(Succeed())
				g.Expect(current.Status).Should(Equal(vpc.Status))
				g.Expect(current.GetResourceVersion()).ShouldNot(Equal(currentRV))
				g.Expect(current.Spec.ForProvider.Tags).Should(Equal(tags))
				g.Expect(current.Annotations).Should(HaveKeyWithValue("crossplane", "vpc-id-xxxxx"))
				g.Expect(current.Annotations).Should(HaveLen(2))
			},
		},
	}

	for name, c := range cases {
		t.Run(name, func(t *testing.T) {
			g := NewWithT(t)
			s := runtime.NewScheme()
			g.Expect(manualv1alpha1.SchemeBuilder.AddToScheme(s)).Should(Succeed())
			g.Expect(v1beta1.SchemeBuilder.AddToScheme(s)).Should(Succeed())

			var cli client.Client
			if c.createdByApply {
				cli = fake.NewClientBuilder().WithScheme(s).Build()
				for _, obj := range c.existObjs {
					g.Expect(Apply(ctx, cli, obj)).Should(Succeed())
				}
			} else {
				cli = fake.NewClientBuilder().WithScheme(s).WithObjects(c.existObjs...).Build()
			}

			c.expect(g, cli, c.modifiedObj)

			if c.checkExpectedObj {
				checkExpectedSpecAndLabels(g, ctx, cli, client.ObjectKeyFromObject(&nodeGroup), c.expectedObj)
			}
		})
	}
}

func checkExpectedSpecAndLabels(g *WithT, ctx context.Context, cli client.Client, key client.ObjectKey, expectedObj client.Object) {
	var ng manualv1alpha1.NodeGroup
	g.Expect(cli.Get(ctx, key, &ng)).To(Succeed())
	g.Expect(ng.Spec).To(Equal(expectedObj.(*manualv1alpha1.NodeGroup).Spec))
	g.Expect(ng.Labels).To(Equal(expectedObj.GetLabels()))
	g.Expect(ng.Annotations).Should(HaveKey(consts.LastAppliedConfigAnnotation))
}

func Test_checkObject(t *testing.T) {
	tests := []struct {
		name    string
		obj     client.Object
		wantErr bool
	}{
		{
			name:    "nil object",
			obj:     nil,
			wantErr: true,
		},
		{
			name:    "empty object",
			obj:     &manualv1alpha1.NodeGroup{},
			wantErr: true,
		},
		{
			name: "object with obj meta only",
			obj: &manualv1alpha1.NodeGroup{
				ObjectMeta: metav1.ObjectMeta{
					Name: "test",
				},
			},
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if err := checkObject(tt.obj); (err != nil) != tt.wantErr {
				t.Errorf("checkObject() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

func withNormalizeLastAppliedAnnotation(obj client.Object) {
	util.SetAnnotation(obj, consts.NormalizeLastAppliedConfigAnnotation, "true")
}

func withDynamicFields(obj client.Object) {
	obj.SetUID("test-uid")
	obj.SetGeneration(99)
	obj.SetCreationTimestamp(metav1.Now())
	obj.SetManagedFields([]metav1.ManagedFieldsEntry{
		{
			Manager:    "test-manager",
			Operation:  metav1.ManagedFieldsOperationApply,
			APIVersion: "ec2.aws.crossplane.io/v1beta1",
		},
	})
	if vpc, ok := obj.(*v1beta1.VPC); ok {
		vpc.Status.AtProvider.VPCID = "vpc-observed"
		vpc.Status.AtProvider.VPCState = "available"
	}
}

func lastAppliedObject(g *WithT, obj client.Object) map[string]any {
	annotation := obj.GetAnnotations()[consts.LastAppliedConfigAnnotation]
	g.Expect(annotation).NotTo(BeEmpty())

	var out map[string]any
	g.Expect(json.Unmarshal([]byte(annotation), &out)).To(Succeed())
	return out
}

func expectNoDynamicFields(g *WithT, obj map[string]any) {
	expectNoDynamicFieldsWithNormalizeMarker(g, obj, true)
}

func expectNoDynamicFieldsWithNormalizeMarker(g *WithT, obj map[string]any, wantMarker bool) {
	_, ok := obj["status"]
	g.Expect(ok).To(BeFalse())

	metadata, ok := obj["metadata"].(map[string]any)
	g.Expect(ok).To(BeTrue())
	for _, field := range []string{"creationTimestamp", "generation", "managedFields", "resourceVersion", "selfLink", "uid"} {
		_, ok = metadata[field]
		g.Expect(ok).To(BeFalse(), field)
	}

	annotations, _ := metadata["annotations"].(map[string]any)
	_, ok = annotations[consts.LastAppliedConfigAnnotation]
	g.Expect(ok).To(BeFalse())
	if wantMarker {
		g.Expect(annotations[consts.NormalizeLastAppliedConfigAnnotation]).To(Equal("true"))
	} else {
		_, ok = annotations[consts.NormalizeLastAppliedConfigAnnotation]
		g.Expect(ok).To(BeFalse())
	}
}

func expectDynamicFields(g *WithT, obj map[string]any) {
	_, ok := obj["status"]
	g.Expect(ok).To(BeTrue())

	metadata, ok := obj["metadata"].(map[string]any)
	g.Expect(ok).To(BeTrue())
	for _, field := range []string{"creationTimestamp", "generation", "managedFields", "uid"} {
		_, ok = metadata[field]
		g.Expect(ok).To(BeTrue(), field)
	}
}

func TestNormalizeLastAppliedConfigurationAnnotation(t *testing.T) {
	g := NewWithT(t)

	obj := newTestVPC(withNormalizeLastAppliedAnnotation)
	withDynamicFields(obj)
	legacyApplied, err := GetModifiedConfiguration(obj, v1beta1.SchemeGroupVersion, false)
	g.Expect(err).ToNot(HaveOccurred())
	util.SetAnnotation(obj, consts.LastAppliedConfigAnnotation, string(legacyApplied))

	g.Expect(NormalizeLastAppliedConfigurationAnnotation(obj)).To(BeTrue())
	expectNoDynamicFields(g, lastAppliedObject(g, obj))

	g.Expect(NormalizeLastAppliedConfigurationAnnotation(obj)).To(BeFalse())
}

func TestApplyWithoutNormalizeLastAppliedConfigurationKeepsLegacyAnnotation(t *testing.T) {
	g := NewWithT(t)
	ctx := context.Background()

	obj := newTestVPC()
	withDynamicFields(obj)

	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()
	g.Expect(Apply(ctx, cli, obj)).To(Succeed())

	var current v1beta1.VPC
	g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(obj), &current)).To(Succeed())
	expectDynamicFields(g, lastAppliedObject(g, &current))
}

func TestApplyWithoutNormalizeLastAppliedConfigurationKeepsLegacyBehaviorAfterCleanCreate(t *testing.T) {
	g := NewWithT(t)
	ctx := context.Background()

	obj := newTestVPC()
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()
	g.Expect(Apply(ctx, cli, obj)).To(Succeed())

	modified := newTestVPC(withLabel("force-patch", "true"))
	withDynamicFields(modified)
	g.Expect(Apply(ctx, cli, modified)).To(Succeed())

	var current v1beta1.VPC
	g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(obj), &current)).To(Succeed())
	expectDynamicFields(g, lastAppliedObject(g, &current))
}

func TestApplyWithNormalizeLastAppliedConfigurationOptionCreatesSlimAnnotation(t *testing.T) {
	g := NewWithT(t)
	ctx := context.Background()

	obj := newTestVPC()
	withDynamicFields(obj)

	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()
	g.Expect(Apply(ctx, cli, obj, WithNormalizeLastAppliedConfiguration())).To(Succeed())

	var current v1beta1.VPC
	g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(obj), &current)).To(Succeed())
	expectNoDynamicFieldsWithNormalizeMarker(g, lastAppliedObject(g, &current), false)
}

func TestGetModifiedConfigurationNormalizeLastAppliedConfiguration(t *testing.T) {
	g := NewWithT(t)

	obj := newTestVPC(withNormalizeLastAppliedAnnotation, withLastAppliedAnnotation)
	withDynamicFields(obj)

	modified, err := getModifiedConfiguration(obj, v1beta1.SchemeGroupVersion, false, applyOptions{normalizeLastAppliedConfiguration: true})
	g.Expect(err).ToNot(HaveOccurred())

	var out map[string]any
	g.Expect(json.Unmarshal(modified, &out)).To(Succeed())
	expectNoDynamicFields(g, out)
}

func TestNormalizeConfigurationPreservesLargeNumbers(t *testing.T) {
	g := NewWithT(t)

	normalized, err := normalizeConfiguration([]byte(`{"apiVersion":"test/v1","kind":"LargeNumber","metadata":{"name":"test","generation":10},"spec":{"value":9223372036854775807},"status":{"observed":1}}`), false)
	g.Expect(err).ToNot(HaveOccurred())

	var out struct {
		Spec struct {
			Value json.Number `json:"value"`
		} `json:"spec"`
	}
	g.Expect(json.Unmarshal(normalized, &out)).To(Succeed())
	g.Expect(out.Spec.Value.String()).To(Equal("9223372036854775807"))
}

func TestApplyWithNormalizeLastAppliedConfigurationMarkerCreatesSlimAnnotation(t *testing.T) {
	g := NewWithT(t)
	ctx := context.Background()

	obj := newTestVPC(withNormalizeLastAppliedAnnotation)
	withDynamicFields(obj)

	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()
	g.Expect(Apply(ctx, cli, obj)).To(Succeed())

	var current v1beta1.VPC
	g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(obj), &current)).To(Succeed())
	expectNoDynamicFields(g, lastAppliedObject(g, &current))
}

func TestApplyWithNormalizeLastAppliedConfigurationIgnoresDynamicChanges(t *testing.T) {
	g := NewWithT(t)
	ctx := context.Background()

	obj := newTestVPC(withNormalizeLastAppliedAnnotation)
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()
	g.Expect(Apply(ctx, cli, obj)).To(Succeed())

	var current v1beta1.VPC
	g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(obj), &current)).To(Succeed())
	withDynamicFields(&current)
	current.Status.AtProvider.VPCID = "vpc-changed-by-controller"
	g.Expect(cli.Update(ctx, &current)).To(Succeed())

	g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(obj), &current)).To(Succeed())
	beforeRV := current.GetResourceVersion()
	g.Expect(Apply(ctx, cli, newTestVPC(withNormalizeLastAppliedAnnotation))).To(Succeed())

	g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(obj), &current)).To(Succeed())
	g.Expect(current.GetResourceVersion()).To(Equal(beforeRV))
	expectNoDynamicFields(g, lastAppliedObject(g, &current))
}

func TestApplyWithNormalizeLastAppliedConfigurationNormalizesLegacyAnnotation(t *testing.T) {
	g := NewWithT(t)
	ctx := context.Background()

	legacy := newTestVPC(withNormalizeLastAppliedAnnotation)
	withDynamicFields(legacy)
	legacyApplied, err := GetModifiedConfiguration(legacy, v1beta1.SchemeGroupVersion, false)
	g.Expect(err).ToNot(HaveOccurred())
	util.SetAnnotation(legacy, consts.LastAppliedConfigAnnotation, string(legacyApplied))

	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(legacy).Build()

	var current v1beta1.VPC
	g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(legacy), &current)).To(Succeed())
	current.Status.AtProvider.VPCID = "vpc-changed-by-controller"
	current.SetManagedFields([]metav1.ManagedFieldsEntry{{Manager: "another-manager", Operation: metav1.ManagedFieldsOperationUpdate}})
	g.Expect(cli.Update(ctx, &current)).To(Succeed())

	g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(legacy), &current)).To(Succeed())
	beforeRV := current.GetResourceVersion()
	g.Expect(Apply(ctx, cli, newTestVPC(withNormalizeLastAppliedAnnotation))).To(Succeed())

	g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(legacy), &current)).To(Succeed())
	g.Expect(current.GetResourceVersion()).To(Equal(beforeRV))
}

func TestApplyWithNormalizeLastAppliedConfigurationRollbackRemovesMarkerThenRestoresLegacy(t *testing.T) {
	g := NewWithT(t)
	ctx := context.Background()

	obj := newTestVPC(withNormalizeLastAppliedAnnotation, withLabel("third-party.com/change", "keep"))
	cli := fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()
	g.Expect(Apply(ctx, cli, obj)).To(Succeed())

	var current v1beta1.VPC
	g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(obj), &current)).To(Succeed())
	g.Expect(current.Annotations).To(HaveKeyWithValue(consts.NormalizeLastAppliedConfigAnnotation, "true"))

	result, err := ApplyWithResult(ctx, cli, newTestVPC(withLabel("third-party.com/change", "keep")))
	g.Expect(err).ToNot(HaveOccurred())
	g.Expect(result).To(Equal(ApplyResultPatched))

	g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(obj), &current)).To(Succeed())
	g.Expect(current.Annotations).NotTo(HaveKey(consts.NormalizeLastAppliedConfigAnnotation))
	g.Expect(current.Labels).To(HaveKeyWithValue("third-party.com/change", "keep"))
	expectNoDynamicFieldsWithNormalizeMarker(g, lastAppliedObject(g, &current), false)

	result, err = ApplyWithResult(ctx, cli, newTestVPC(withLabel("third-party.com/change", "keep")))
	g.Expect(err).ToNot(HaveOccurred())
	g.Expect(result).To(Equal(ApplyResultPatched))

	g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(obj), &current)).To(Succeed())
	g.Expect(current.Annotations).NotTo(HaveKey(consts.NormalizeLastAppliedConfigAnnotation))
	g.Expect(current.Labels).To(HaveKeyWithValue("third-party.com/change", "keep"))
	legacyApplied := lastAppliedObject(g, &current)
	_, ok := legacyApplied["status"]
	g.Expect(ok).To(BeTrue())
	metadata := legacyApplied["metadata"].(map[string]any)
	g.Expect(metadata).To(HaveKey("creationTimestamp"))

	result, err = ApplyWithResult(ctx, cli, newTestVPC(withLabel("third-party.com/change", "keep")))
	g.Expect(err).ToNot(HaveOccurred())
	g.Expect(result).To(Equal(ApplyResultUnchanged))
}
