package luavm

import (
	"encoding/json"
	"reflect"
	"testing"

	lua "github.com/yuin/gopher-lua"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/utils/pointer"

	configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1"
	workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2"
	"github.com/karmada-io/karmada/pkg/util/helper"
)

func TestGetReplicas(t *testing.T) {
	vm := New(false, 1)
	tests := []struct {
		name         string
		deploy       *appsv1.Deployment
		luaScript    string
		expected     bool
		wantReplica  int32
		wantRequires *workv1alpha2.ReplicaRequirements
	}{
		{
			name: "Test GetReplica with kube.accuratePodRequirements",
			deploy: &appsv1.Deployment{
				TypeMeta: metav1.TypeMeta{
					Kind:       "Deployment",
					APIVersion: "v1",
				},
				ObjectMeta: metav1.ObjectMeta{
					Namespace: "foo",
					Name:      "bar",
				},
				Spec: appsv1.DeploymentSpec{
					Replicas: pointer.Int32(1),
					Template: corev1.PodTemplateSpec{
						Spec: corev1.PodSpec{
							NodeSelector: map[string]string{"foo": "foo1"},
							Tolerations:  []corev1.Toleration{{Key: "bar", Operator: corev1.TolerationOpExists}},
							Containers: []corev1.Container{
								{Resources: corev1.ResourceRequirements{Limits: map[corev1.ResourceName]resource.Quantity{
									corev1.ResourceCPU:    resource.MustParse("100m"),
									corev1.ResourceMemory: resource.MustParse("100M"),
								}}},
								{Resources: corev1.ResourceRequirements{Limits: map[corev1.ResourceName]resource.Quantity{
									corev1.ResourceCPU:    resource.MustParse("1.2"),
									corev1.ResourceMemory: resource.MustParse("1.2G"),
								}}},
							},
						},
					},
				},
			},
			expected: true,
			luaScript: `
local kube = require("kube")
function GetReplicas(desiredObj)
	replica = desiredObj.spec.replicas
	requires = kube.accuratePodRequirements(desiredObj.spec.template)
	return replica, requires 
end`,
			wantReplica: 1,
			wantRequires: &workv1alpha2.ReplicaRequirements{
				NodeClaim: &workv1alpha2.NodeClaim{
					NodeSelector: map[string]string{"foo": "foo1"},
					Tolerations:  []corev1.Toleration{{Key: "bar", Operator: corev1.TolerationOpExists}},
				},
				ResourceRequest: map[corev1.ResourceName]resource.Quantity{
					corev1.ResourceCPU:    resource.MustParse("1.3"),
					corev1.ResourceMemory: resource.MustParse("1.3G"),
				},
			},
		},
		{
			name: "Test GetReplica with kube.resourceAdd",
			deploy: &appsv1.Deployment{
				TypeMeta: metav1.TypeMeta{
					Kind:       "Deployment",
					APIVersion: "v1",
				},
				ObjectMeta: metav1.ObjectMeta{
					Namespace: "foo",
					Name:      "bar",
				},
				Spec: appsv1.DeploymentSpec{
					Replicas: pointer.Int32(1),
					Template: corev1.PodTemplateSpec{
						Spec: corev1.PodSpec{
							NodeSelector: map[string]string{"foo": "foo1"},
							Tolerations:  []corev1.Toleration{{Key: "bar", Operator: corev1.TolerationOpExists}},
							Containers: []corev1.Container{
								{Resources: corev1.ResourceRequirements{Limits: map[corev1.ResourceName]resource.Quantity{
									corev1.ResourceCPU:    resource.MustParse("100m"),
									corev1.ResourceMemory: resource.MustParse("100M"),
								}}},
								{Resources: corev1.ResourceRequirements{Limits: map[corev1.ResourceName]resource.Quantity{
									corev1.ResourceCPU:    resource.MustParse("1.2"),
									corev1.ResourceMemory: resource.MustParse("1.2G"),
								}}},
							},
						},
					},
				},
			},
			expected: true,
			luaScript: `
local kube = require("kube")
function GetReplicas(desiredObj)
	replica = desiredObj.spec.replicas
	requires = {
		nodeClaim = {
			nodeSelector = desiredObj.spec.template.spec.nodeSelector,
			tolerations = desiredObj.spec.template.spec.tolerations
    	},
		resourceRequest = {},
	}
	for i = 1, #desiredObj.spec.template.spec.containers do
		requires.resourceRequest.cpu = kube.resourceAdd(requires.resourceRequest.cpu, desiredObj.spec.template.spec.containers[i].resources.limits.cpu)
		requires.resourceRequest.memory = kube.resourceAdd(requires.resourceRequest.memory, desiredObj.spec.template.spec.containers[i].resources.limits.memory)
	end
	return replica, requires 
end`,
			wantReplica: 1,
			wantRequires: &workv1alpha2.ReplicaRequirements{
				NodeClaim: &workv1alpha2.NodeClaim{
					NodeSelector: map[string]string{"foo": "foo1"},
					Tolerations:  []corev1.Toleration{{Key: "bar", Operator: corev1.TolerationOpExists}},
				},
				ResourceRequest: map[corev1.ResourceName]resource.Quantity{
					corev1.ResourceCPU:    resource.MustParse("1.3"),
					corev1.ResourceMemory: resource.MustParse("1.3G"),
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			toUnstructured, _ := helper.ToUnstructured(tt.deploy)
			replicas, requires, err := vm.GetReplicas(toUnstructured, tt.luaScript)
			if err != nil {
				t.Fatal(err.Error())
			}
			if !reflect.DeepEqual(replicas, tt.wantReplica) {
				t.Errorf("GetReplicas() got = %v, want %v", replicas, tt.wantReplica)
			}

			if got, want := requires.ResourceRequest.Cpu(), tt.wantRequires.ResourceRequest.Cpu(); !got.Equal(*want) {
				t.Errorf("GetReplicas() got Cpu = %s, want %s", got, want)
			}
			if got, want := requires.ResourceRequest.Memory(), tt.wantRequires.ResourceRequest.Memory(); !got.Equal(*want) {
				t.Errorf("GetReplicas() got Memory = %s, want %s", got, want)
			}

			requires.ResourceRequest, tt.wantRequires.ResourceRequest = nil, nil
			if !reflect.DeepEqual(requires, tt.wantRequires) {
				t.Errorf("GetReplicas() got = %v, want %v", requires, tt.wantRequires)
			}
		})
	}
}

func TestReviseDeploymentReplica(t *testing.T) {
	tests := []struct {
		name        string
		object      *unstructured.Unstructured
		replica     int32
		expected    *unstructured.Unstructured
		expectError bool
		luaScript   string
	}{
		{
			name: "Test ReviseDeploymentReplica",
			object: &unstructured.Unstructured{
				Object: map[string]interface{}{
					"apiVersion": "apps/v1",
					"kind":       "Deployment",
					"metadata": map[string]interface{}{
						"name": "fake-deployment",
					},
					"spec": map[string]interface{}{
						"replicas": 1,
					},
				},
			},
			replica:     3,
			expectError: true,
			luaScript: `function ReviseReplica(desiredObj, desiredReplica)
							desiredObj.spec.replicas = desiredReplica  
							return desiredObj
                           end`,
		},
		{
			name: "revise deployment replica",
			object: &unstructured.Unstructured{
				Object: map[string]interface{}{
					"apiVersion": "apps/v1",
					"kind":       "Deployment",
					"metadata": map[string]interface{}{
						"name": "fake-deployment",
					},
					"spec": map[string]interface{}{
						"replicas": int64(1),
					},
				},
			},
			replica: 3,
			expected: &unstructured.Unstructured{
				Object: map[string]interface{}{
					"apiVersion": "apps/v1",
					"kind":       "Deployment",
					"metadata": map[string]interface{}{
						"name": "fake-deployment",
					},
					"spec": map[string]interface{}{
						"replicas": int64(2),
					},
				},
			},
			expectError: false,
			luaScript: `function ReviseReplica(desiredObj, desiredReplica)  
							desiredObj.spec.replicas = desiredReplica	
							return desiredObj 
						end`,
		},
	}
	vm := New(false, 1)

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			res, err := vm.ReviseReplica(tt.object, int64(tt.replica), tt.luaScript)
			if err != nil {
				t.Errorf(err.Error())
			}
			deploy := &appsv1.Deployment{}
			err = runtime.DefaultUnstructuredConverter.FromUnstructured(res.UnstructuredContent(), deploy)
			if err == nil && *deploy.Spec.Replicas == tt.replica {
				t.Log("Success Test")
			}
			if err != nil {
				t.Errorf(err.Error())
			}
		})
	}
}

func TestAggregateDeploymentStatus(t *testing.T) {
	statusMap := map[string]interface{}{
		"replicas":            0,
		"readyReplicas":       1,
		"updatedReplicas":     0,
		"availableReplicas":   0,
		"unavailableReplicas": 0,
	}
	raw, _ := helper.BuildStatusRawExtension(statusMap)
	aggregatedStatusItems := []workv1alpha2.AggregatedStatusItem{
		{ClusterName: "member1", Status: raw, Applied: true},
		{ClusterName: "member2", Status: raw, Applied: true},
	}

	oldDeploy := &appsv1.Deployment{
		TypeMeta: metav1.TypeMeta{
			Kind:       "Deployment",
			APIVersion: "apps/v1",
		},
	}
	oldDeploy.Status = appsv1.DeploymentStatus{
		Replicas: 0, ReadyReplicas: 1, UpdatedReplicas: 0, AvailableReplicas: 0, UnavailableReplicas: 0}

	newDeploy := &appsv1.Deployment{Status: appsv1.DeploymentStatus{Replicas: 0, ReadyReplicas: 3, UpdatedReplicas: 0, AvailableReplicas: 0, UnavailableReplicas: 0}}
	oldObj, _ := helper.ToUnstructured(oldDeploy)
	newObj, _ := helper.ToUnstructured(newDeploy)

	tests := []struct {
		name                  string
		curObj                *unstructured.Unstructured
		aggregatedStatusItems []workv1alpha2.AggregatedStatusItem
		expectedObj           *unstructured.Unstructured
		luaScript             string
	}{
		{
			name:                  "Test AggregateDeploymentStatus",
			curObj:                oldObj,
			aggregatedStatusItems: aggregatedStatusItems,
			expectedObj:           newObj,
			luaScript: `function AggregateStatus(desiredObj, statusItems) 
										for i = 1, #statusItems do    
											desiredObj.status.readyReplicas = desiredObj.status.readyReplicas + statusItems[i].status.readyReplicas 
										end    
										return desiredObj
									end`,
		},
	}
	vm := New(false, 1)

	for _, tt := range tests {
		actualObj, _ := vm.AggregateStatus(tt.curObj, tt.aggregatedStatusItems, tt.luaScript)
		actualDeploy := appsv1.DeploymentStatus{}
		err := helper.ConvertToTypedObject(actualObj.Object["status"], &actualDeploy)
		if err != nil {
			t.Error(err.Error())
		}
		expectDeploy := appsv1.DeploymentStatus{}
		err = helper.ConvertToTypedObject(tt.expectedObj.Object["status"], &expectDeploy)
		if err != nil {
			t.Error(err.Error())
		}
		if !reflect.DeepEqual(expectDeploy, actualDeploy) {
			t.Errorf("AggregateStatus() got = %v, want %v", actualDeploy, expectDeploy)
		}
	}
}

func TestHealthDeploymentStatus(t *testing.T) {
	var cnt int32 = 2
	newDeploy := &appsv1.Deployment{
		Spec: appsv1.DeploymentSpec{
			Replicas: &cnt,
		},

		ObjectMeta: metav1.ObjectMeta{
			Generation: 1,
		},
		Status: appsv1.DeploymentStatus{ObservedGeneration: 1, Replicas: 2, ReadyReplicas: 2, UpdatedReplicas: 2, AvailableReplicas: 2}}
	newObj, _ := helper.ToUnstructured(newDeploy)

	tests := []struct {
		name        string
		curObj      *unstructured.Unstructured
		expectedObj bool
		luaScript   string
	}{
		{
			name:        "Test HealthDeploymentStatus",
			curObj:      newObj,
			expectedObj: true,
			luaScript: `function InterpretHealth(observedObj)
							return (observedObj.status.updatedReplicas == observedObj.spec.replicas) and (observedObj.metadata.generation == observedObj.status.observedGeneration)
                        end `,
		},
	}
	vm := New(false, 1)

	for _, tt := range tests {
		flag, err := vm.InterpretHealth(tt.curObj, tt.luaScript)

		if err != nil {
			t.Error(err.Error())
		}
		if !reflect.DeepEqual(flag, tt.expectedObj) {
			t.Errorf("AggregateStatus() got = %v, want %v", flag, tt.expectedObj)
		}
	}
}

func TestRetainDeployment(t *testing.T) {
	tests := []struct {
		name        string
		desiredObj  *unstructured.Unstructured
		observedObj *unstructured.Unstructured
		expectError bool
		luaScript   string
	}{
		{
			name: "Test RetainDeployment1",
			desiredObj: &unstructured.Unstructured{
				Object: map[string]interface{}{
					"apiVersion": "apps/v1",
					"kind":       "Deployment",
					"metadata": map[string]interface{}{
						"name": "fake-deployment",
					},
					"spec": map[string]interface{}{
						"replicas": 2,
					},
				},
			},
			observedObj: &unstructured.Unstructured{
				Object: map[string]interface{}{
					"apiVersion": "apps/v1",
					"kind":       "Deployment",
					"metadata": map[string]interface{}{
						"name": "fake-deployment",
					},
					"spec": map[string]interface{}{
						"replicas": int64(2),
					},
				},
			},
			expectError: true,
			luaScript:   "function Retain(desiredObj, observedObj)\n  desiredObj = observedObj\n  return desiredObj\n end",
		},
		{
			name: "Test RetainDeployment2",
			desiredObj: &unstructured.Unstructured{
				Object: map[string]interface{}{
					"apiVersion": "apps/v1",
					"kind":       "Deployment",
					"metadata": map[string]interface{}{
						"name": "fake-deployment",
					},
					"spec": map[string]interface{}{
						"replicas": int64(1),
					},
				},
			},
			observedObj: &unstructured.Unstructured{
				Object: map[string]interface{}{
					"apiVersion": "apps/v1",
					"kind":       "Deployment",
					"metadata": map[string]interface{}{
						"name": "fake-deployment",
					},
					"spec": map[string]interface{}{
						"replicas": int64(1),
					},
				},
			},
			expectError: false,
			luaScript: `function Retain(desiredObj, observedObj) 
							desiredObj = observedObj 
							return desiredObj
						end`,
		},
	}
	vm := New(false, 1)

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			res, err := vm.Retain(tt.desiredObj, tt.observedObj, tt.luaScript)
			if err != nil {
				t.Errorf(err.Error())
			}
			if !reflect.DeepEqual(res.UnstructuredContent(), tt.observedObj.Object) {
				t.Errorf("Retain() got = %v, want %v", res.UnstructuredContent(), tt.observedObj.Object)
			}
		})
	}
}

func TestStatusReflection(t *testing.T) {
	testMap := map[string]interface{}{"key": "value"}
	wantRawExtension, _ := helper.BuildStatusRawExtension(testMap)
	type args struct {
		object *unstructured.Unstructured
	}
	tests := []struct {
		name      string
		args      args
		want      *runtime.RawExtension
		wantErr   bool
		luaScript string
	}{
		{
			"Test StatusReflection",
			args{
				&unstructured.Unstructured{
					Object: map[string]interface{}{
						"status": testMap,
					},
				},
			},
			wantRawExtension,
			false,
			`function ReflectStatus (observedObj)
						if observedObj.status == nil then	
							return nil   
						end    
					return observedObj.status
					end`,
		},
	}

	vm := New(false, 1)
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := vm.ReflectStatus(tt.args.object, tt.luaScript)
			if (err != nil) != tt.wantErr {
				t.Errorf("reflectWholeStatus() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("reflectWholeStatus() got = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestGetDeployPodDependencies(t *testing.T) {
	newDeploy1 := appsv1.Deployment{
		TypeMeta: metav1.TypeMeta{
			APIVersion: "v1",
			Kind:       "Deployment",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name:      "test",
			Namespace: "test",
		},
		Spec: appsv1.DeploymentSpec{
			Template: corev1.PodTemplateSpec{
				Spec: corev1.PodSpec{
					ServiceAccountName: "test",
				},
			},
		},
	}
	newDeploy2 := newDeploy1.DeepCopy()
	newDeploy2.Namespace = ""
	newObj1, _ := helper.ToUnstructured(&newDeploy1)
	expect1 := make([]configv1alpha1.DependentObjectReference, 1)
	expect1[0] = configv1alpha1.DependentObjectReference{
		APIVersion: "v1",
		Kind:       "ServiceAccount",
		Namespace:  "test",
		Name:       "test",
	}
	newObj2, _ := helper.ToUnstructured(&newDeploy2)
	expect2 := make([]configv1alpha1.DependentObjectReference, 1)
	expect2[0] = configv1alpha1.DependentObjectReference{
		APIVersion: "v1",
		Kind:       "ServiceAccount",
		Namespace:  "default",
		Name:       "test",
	}
	tests := []struct {
		name      string
		curObj    *unstructured.Unstructured
		luaScript string
		want      []configv1alpha1.DependentObjectReference
	}{
		{
			name:   "Get GetDeployPodDependencies",
			curObj: newObj1,
			luaScript: `function GetDependencies(desiredObj)
							dependentSas = {}
							refs = {}
							if desiredObj.spec.template.spec.serviceAccountName ~= '' and desiredObj.spec.template.spec.serviceAccountName ~= 'default' then
								dependentSas[desiredObj.spec.template.spec.serviceAccountName] = true
							end
						local idx = 1
						for key, value in pairs(dependentSas) do    
							dependObj = {}    
							dependObj.apiVersion = 'v1'   
							dependObj.kind = 'ServiceAccount'    
							dependObj.name = key    
							dependObj.namespace = desiredObj.metadata.namespace    
							refs[idx] = {} 
							refs[idx] = dependObj    
							idx = idx + 1
						end
					return refs
				    end`,
			want: expect1,
		},
		{
			name:   "Test getPodDependencies",
			curObj: newObj1,
			luaScript: `local kube = require("kube")
					function GetDependencies(desiredObj)
						dependencies = kube.getPodDependencies(desiredObj.spec.template, desiredObj.metadata.namespace)
						return dependencies
			            end`,
			want: expect1,
		},
		{
			name:   "Test getPodDependencies when desiredObj's namespace is empty",
			curObj: newObj2,
			luaScript: `local kube = require("kube")
					function GetDependencies(desiredObj)
						dependencies = kube.getPodDependencies(desiredObj.spec.template, desiredObj.metadata.namespace)
						return dependencies
			            end`,
			want: expect2,
		},
	}

	vm := New(false, 1)

	for _, tt := range tests {
		res, err := vm.GetDependencies(tt.curObj, tt.luaScript)
		if err != nil {
			t.Errorf("GetDependencies err %v", err)
		}
		if !reflect.DeepEqual(res, tt.want) {
			t.Errorf("GetDependencies() got = %v, want %v", res, tt.want)
		}
	}
}

func Test_decodeValue(t *testing.T) {
	L := lua.NewState()

	type args struct {
		value interface{}
	}
	tests := []struct {
		name    string
		args    args
		want    lua.LValue
		wantErr bool
	}{
		{
			name: "nil",
			args: args{
				value: nil,
			},
			want: lua.LNil,
		},
		{
			name: "nil pointer",
			args: args{
				value: (*struct{})(nil),
			},
			want: lua.LNil,
		},
		{
			name: "int pointer",
			args: args{
				value: pointer.Int(1),
			},
			want: lua.LNumber(1),
		},
		{
			name: "int",
			args: args{
				value: 1,
			},
			want: lua.LNumber(1),
		},
		{
			name: "uint",
			args: args{
				value: uint(1),
			},
			want: lua.LNumber(1),
		},
		{
			name: "float",
			args: args{
				value: 1.0,
			},
			want: lua.LNumber(1),
		},
		{
			name: "bool",
			args: args{
				value: true,
			},
			want: lua.LBool(true),
		},
		{
			name: "string",
			args: args{
				value: "foo",
			},
			want: lua.LString("foo"),
		},
		{
			name: "json number",
			args: args{
				value: json.Number("1"),
			},
			want: lua.LString("1"),
		},
		{
			name: "slice",
			args: args{
				value: []string{"foo", "bar"},
			},
			want: func() lua.LValue {
				v := L.CreateTable(2, 0)
				v.Append(lua.LString("foo"))
				v.Append(lua.LString("bar"))
				return v
			}(),
		},
		{
			name: "slice pointer",
			args: args{
				value: &[]string{"foo", "bar"},
			},
			want: func() lua.LValue {
				v := L.CreateTable(2, 0)
				v.Append(lua.LString("foo"))
				v.Append(lua.LString("bar"))
				return v
			}(),
		},
		{
			name: "struct",
			args: args{
				value: struct {
					Foo string
				}{
					Foo: "foo",
				},
			},
			want: func() lua.LValue {
				v := L.CreateTable(0, 1)
				v.RawSetString("Foo", lua.LString("foo"))
				return v
			}(),
		},
		{
			name: "struct pointer",
			args: args{
				value: &struct {
					Foo string
				}{
					Foo: "foo",
				},
			},
			want: func() lua.LValue {
				v := L.CreateTable(0, 1)
				v.RawSetString("Foo", lua.LString("foo"))
				return v
			}(),
		},
		{
			name: "[]interface{}",
			args: args{
				value: []interface{}{1, 2},
			},
			want: func() lua.LValue {
				v := L.CreateTable(2, 0)
				v.Append(lua.LNumber(1))
				v.Append(lua.LNumber(2))
				return v
			}(),
		},
		{
			name: "map[string]interface{}",
			args: args{
				value: map[string]interface{}{
					"foo": "foo1",
				},
			},
			want: func() lua.LValue {
				v := L.CreateTable(0, 1)
				v.RawSetString("foo", lua.LString("foo1"))
				return v
			}(),
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := decodeValue(L, tt.args.value)
			if (err != nil) != tt.wantErr {
				t.Errorf("decodeValue() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("decodeValue() got = %v, want %v", got, tt.want)
			}
		})
	}
}
