package core

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

	mock_condition "github.com/aws/amazon-vpc-resource-controller-k8s/mocks/amazon-vcp-resource-controller-k8s/pkg/condition"
	"github.com/aws/amazon-vpc-resource-controller-k8s/pkg/config"
	"github.com/golang/mock/gomock"
	"github.com/stretchr/testify/assert"
	admissionv1 "k8s.io/api/admission/v1"
	v1 "k8s.io/api/authentication/v1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

type MockNodeUpdateWebhook struct {
	MockCondition *mock_condition.MockConditions
}

func TestNodeUpdateWebhook_Handle(t *testing.T) {
	schema := runtime.NewScheme()
	err := clientgoscheme.AddToScheme(schema)
	assert.NoError(t, err)

	decoder := admission.NewDecoder(schema)

	baseNode := &corev1.Node{
		TypeMeta: metav1.TypeMeta{
			Kind:       "Node",
			APIVersion: "v1",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name: "default",
			Annotations: map[string]string{
				"existing-key": "existing-val",
			},
			Labels: map[string]string{
				"existing-label": "existing-val",
			},
		},
		Spec: corev1.NodeSpec{
			Unschedulable: false,
		},
		Status: corev1.NodeStatus{
			Phase: "Ready",
		},
	}

	oldNode := baseNode.DeepCopy()
	oldNodeRaw, _ := json.Marshal(oldNode)

	nodeWithHasTrunkAttachedLabel := baseNode.DeepCopy()
	nodeWithHasTrunkAttachedLabel.Labels[config.HasTrunkAttachedLabel] = "true"
	nodeWithHasTrunkAttachedLabelRaw, _ := json.Marshal(nodeWithHasTrunkAttachedLabel)

	nodeWithENIConfigLabel := baseNode.DeepCopy()
	nodeWithENIConfigLabel.Labels[config.CustomNetworkingLabel] = "us-west-2a"
	nodeWithENIConfigLabelRaw, _ := json.Marshal(nodeWithENIConfigLabel)

	nodeWithUnknownLabel := baseNode.DeepCopy()
	nodeWithUnknownLabel.Labels["some-label"] = "some-val"
	nodeWithUnknownLabelRaw, _ := json.Marshal(nodeWithUnknownLabel)

	nodeWithUpdatedStatus := baseNode.DeepCopy()
	nodeWithUpdatedStatus.Status.Phase = "NotReady"
	nodeWithUpdatedStatusRaw, _ := json.Marshal(nodeWithUpdatedStatus)

	nodeWithUpdatedSpec := baseNode.DeepCopy()
	nodeWithUpdatedSpec.Spec.ProviderID = "some-val"
	nodeWithUpdatedSpecRaw, _ := json.Marshal(nodeWithUpdatedSpec)

	nodeWithTaints := baseNode.DeepCopy()
	nodeWithTaints.Spec.Taints = []corev1.Taint{
		{
			Key:   "effect",
			Value: "NoSchedule",
		},
	}
	//json.Marshal(fargatePodWithDifferentAnnotation)

	test := []struct {
		name           string
		req            []admission.Request // Club tests with similar response & diff request in 1 test
		want           admission.Response
		mockInvocation func(mock MockNodeUpdateWebhook)
	}{
		{
			name: "[sgp] allow request when username is not aws-node",
			req: []admission.Request{
				{
					AdmissionRequest: admissionv1.AdmissionRequest{
						Operation: admissionv1.Update,
						UserInfo: v1.UserInfo{
							Username: "system:serviceaccount:sa:sa-name",
						},
						Object: runtime.RawExtension{
							Raw:    oldNodeRaw,
							Object: oldNode,
						},
					},
				},
			},
			want: admission.Response{
				AdmissionResponse: admissionv1.AdmissionResponse{
					Allowed: true,
					Result: &metav1.Status{
						Code: http.StatusOK,
					},
				},
			},
			mockInvocation: func(mock MockNodeUpdateWebhook) {
			},
		},
		{
			name: "[sgp] allow request when expected label/annotations are modified",
			req: []admission.Request{
				{
					AdmissionRequest: admissionv1.AdmissionRequest{
						Operation: admissionv1.Update,
						UserInfo: v1.UserInfo{
							Username: awsNodeUsername,
						},
						Object: runtime.RawExtension{
							Raw:    nodeWithHasTrunkAttachedLabelRaw,
							Object: nodeWithHasTrunkAttachedLabel,
						},
						OldObject: runtime.RawExtension{
							Raw:    oldNodeRaw,
							Object: oldNode,
						},
					},
				},
				{
					AdmissionRequest: admissionv1.AdmissionRequest{
						Operation: admissionv1.Update,
						UserInfo: v1.UserInfo{
							Username: awsNodeUsername,
						},
						Object: runtime.RawExtension{
							Raw:    nodeWithENIConfigLabelRaw,
							Object: nodeWithENIConfigLabel,
						},
						OldObject: runtime.RawExtension{
							Raw:    oldNodeRaw,
							Object: oldNode,
						},
					},
				},
			},
			want: admission.Response{
				AdmissionResponse: admissionv1.AdmissionResponse{
					Allowed: true,
					Result: &metav1.Status{
						Code: http.StatusOK,
					},
				},
			},
		},
		{
			name: "[sgp] deny request if any non expected field is modified",
			req: []admission.Request{
				{
					AdmissionRequest: admissionv1.AdmissionRequest{
						Operation: admissionv1.Update,
						UserInfo: v1.UserInfo{
							Username: awsNodeUsername,
						},
						Object: runtime.RawExtension{
							Raw:    nodeWithUnknownLabelRaw,
							Object: nodeWithUnknownLabel,
						},
						OldObject: runtime.RawExtension{
							Raw:    oldNodeRaw,
							Object: oldNode,
						},
					},
				},
				{
					AdmissionRequest: admissionv1.AdmissionRequest{
						Operation: admissionv1.Update,
						UserInfo: v1.UserInfo{
							Username: awsNodeUsername,
						},
						Object: runtime.RawExtension{
							Raw:    nodeWithUpdatedStatusRaw,
							Object: nodeWithUpdatedStatus,
						},
						OldObject: runtime.RawExtension{
							Raw:    oldNodeRaw,
							Object: oldNode,
						},
					},
				},
				{
					AdmissionRequest: admissionv1.AdmissionRequest{
						Operation: admissionv1.Update,
						UserInfo: v1.UserInfo{
							Username: awsNodeUsername,
						},
						Object: runtime.RawExtension{
							Raw:    nodeWithUpdatedSpecRaw,
							Object: nodeWithUpdatedSpec,
						},
						OldObject: runtime.RawExtension{
							Raw:    oldNodeRaw,
							Object: oldNode,
						},
					},
				},
			},
			want: admission.Response{
				AdmissionResponse: admissionv1.AdmissionResponse{
					Allowed: false,
					Result: &metav1.Status{
						Code: http.StatusOK,
					},
				},
			},
		},
	}

	for _, tt := range test {
		t.Run(tt.name, func(t *testing.T) {
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()

			for _, req := range tt.req {
				ctx := context.Background()
				mock := MockNodeUpdateWebhook{
					MockCondition: mock_condition.NewMockConditions(ctrl),
				}

				h := &NodeUpdateWebhook{
					decoder:   decoder,
					Log:       zap.New(),
					Condition: mock.MockCondition,
				}

				if tt.mockInvocation != nil {
					tt.mockInvocation(mock)
				}

				got := h.Handle(ctx, req)
				assert.Equal(t, tt.want.Allowed, got.Allowed)
				assert.Equal(t, tt.want.Result.Status, got.Result.Status)
			}
		})
	}
}
