// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may
// not use this file except in compliance with the License. A copy of the
// License is located at
//
//     http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file 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 k8s

import (
	"context"
	"testing"

	"github.com/aws/amazon-vpc-resource-controller-k8s/apis/vpcresources/v1alpha1"
	mock_custom "github.com/aws/amazon-vpc-resource-controller-k8s/mocks/amazon-vcp-resource-controller-k8s/controllers/custom"
	"github.com/aws/amazon-vpc-resource-controller-k8s/pkg/config"

	"github.com/golang/mock/gomock"
	"github.com/stretchr/testify/assert"
	appV1 "k8s.io/api/apps/v1"
	v1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	fakeClientSet "k8s.io/client-go/kubernetes/fake"
	"sigs.k8s.io/controller-runtime/pkg/client"
	fakeClient "sigs.k8s.io/controller-runtime/pkg/client/fake"
)

var (
	nodeName         = "node-name"
	mockClusterName  = "cluster-name"
	mockNodeID       = "i-123456789"
	mockResourceName = config.ResourceNamePodENI

	existingResource         = "extended-resource"
	existingResourceQuantity = int64(5)
	mockNode                 = &v1.Node{
		TypeMeta: metav1.TypeMeta{},
		ObjectMeta: metav1.ObjectMeta{
			Name: nodeName,
			Labels: map[string]string{
				config.NodeLabelOS: config.OSLinux,
			},
		},
		Spec: v1.NodeSpec{},
		Status: v1.NodeStatus{
			Capacity: v1.ResourceList{
				v1.ResourceName(existingResource): *resource.NewQuantity(existingResourceQuantity, resource.DecimalExponent),
			},
		},
	}
	mockDeployment = &appV1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:      config.OldVPCControllerDeploymentName,
			Namespace: config.KubeSystemNamespace,
		},
	}

	mockDS = &appV1.DaemonSet{
		ObjectMeta: metav1.ObjectMeta{
			Name:      config.VpcCNIDaemonSetName,
			Namespace: config.KubeSystemNamespace,
		},
	}

	mockCNINode = &v1alpha1.CNINode{
		ObjectMeta: metav1.ObjectMeta{
			Name: nodeName,
		},
	}
)

// getMockK8sWrapper returns the mock wrapper interface
func getMockK8sWrapperWithClient(ctrl *gomock.Controller, objs []runtime.Object) (K8sWrapper, client.Client,
	*mock_custom.MockController) {
	scheme := runtime.NewScheme()
	_ = v1.AddToScheme(scheme)
	_ = appV1.AddToScheme(scheme)
	_ = v1alpha1.AddToScheme(scheme)

	client := fakeClient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objs...).Build()
	clientSet := fakeClientSet.NewSimpleClientset(mockNode)
	mockController := mock_custom.NewMockController(ctrl)

	return NewK8sWrapper(client, clientSet.CoreV1(), context.Background()), client, mockController
}

// TestK8sWrapper_AdvertiseCapacity tests that the capacity is advertised to the k8s node
func TestK8sWrapper_AdvertiseCapacity(t *testing.T) {
	ctrl := gomock.NewController(t)
	wrapper, k8sClient, _ := getMockK8sWrapperWithClient(ctrl, []runtime.Object{mockNode, mockDeployment, mockDS})

	// Make node copy and ensure that the advertised capacity is 0
	testNode := mockNode.DeepCopy()
	quantity := testNode.Status.Capacity[v1.ResourceName(mockResourceName)]
	assert.True(t, quantity.IsZero())

	// Advertise capacity
	capacityToAdvertise := 10
	err := wrapper.AdvertiseCapacityIfNotSet(nodeName, mockResourceName, capacityToAdvertise)
	assert.NoError(t, err)

	// Get the node from the client and verify the capacity is set
	node := &v1.Node{}
	err = k8sClient.Get(context.Background(), types.NamespacedName{Name: nodeName}, node)

	// Verify no error and the capacity is set to the desired capacity
	assert.NoError(t, err)
	newQuantity := node.Status.Capacity[v1.ResourceName(mockResourceName)]
	assert.Equal(t, int64(capacityToAdvertise), newQuantity.Value())
}

// TestK8sWrapper_AdvertiseCapacity_Err tests that error is thrown when an error is encountered on the advertise resource
func TestK8sWrapper_AdvertiseCapacity_Err(t *testing.T) {
	ctrl := gomock.NewController(t)
	wrapper, _, _ := getMockK8sWrapperWithClient(ctrl, []runtime.Object{mockNode, mockDeployment, mockDS})

	deletedNodeName := "deleted-node"
	err := wrapper.AdvertiseCapacityIfNotSet(deletedNodeName, mockResourceName, 10)
	assert.NotNil(t, err)
}

// TestK8sWrapper_AdvertiseCapacity_Node_Nil_Capacity tests when a node is returned with nil capacity map. Referring to issue #144
// https://github.com/aws/amazon-vpc-resource-controller-k8s/issues/144
func TestK8sWrapper_AdvertiseCapacity_Node_Nil_Capacity(t *testing.T) {
	ctrl := gomock.NewController(t)
	mockErrNode := &v1.Node{
		TypeMeta: metav1.TypeMeta{},
		ObjectMeta: metav1.ObjectMeta{
			Name: nodeName,
		},
		Spec: v1.NodeSpec{},
		Status: v1.NodeStatus{
			Capacity: nil,
		},
	}
	wrapper, _, _ := getMockK8sWrapperWithClient(ctrl, []runtime.Object{mockErrNode})
	err := wrapper.AdvertiseCapacityIfNotSet(nodeName, mockResourceName, 10)
	assert.NotNil(t, err)
	assert.True(t, errors.IsConflict(err))
}

// TestK8sWrapper_AdvertiseCapacity_AlreadySet tests that if capacity of node is already set no error is thrown.
func TestK8sWrapper_AdvertiseCapacity_AlreadySet(t *testing.T) {
	ctrl := gomock.NewController(t)
	wrapper, _, _ := getMockK8sWrapperWithClient(ctrl, []runtime.Object{mockNode, mockDeployment, mockDS})
	err := wrapper.AdvertiseCapacityIfNotSet(nodeName, existingResource, 5)

	capacity := mockNode.Status.Capacity[v1.ResourceName(existingResource)]
	assert.NoError(t, err)
	assert.Equal(t, existingResourceQuantity, capacity.Value())
}

func TestNewK8sWrapper_GetDaemonSet(t *testing.T) {
	ctrl := gomock.NewController(t)

	wrapper, _, _ := getMockK8sWrapperWithClient(ctrl, []runtime.Object{mockNode, mockDeployment, mockDS})
	ds, err := wrapper.GetDaemonSet(config.VpcCNIDaemonSetName, config.KubeSystemNamespace)

	assert.NoError(t, err)
	assert.Equal(t, config.VpcCNIDaemonSetName, ds.Name)
}

func TestNewK8sWrapper_GetDaemonSet_Err(t *testing.T) {
	ctrl := gomock.NewController(t)

	wrapper, _, _ := getMockK8sWrapperWithClient(ctrl, []runtime.Object{mockNode, mockDeployment, mockDS})
	_, err := wrapper.GetDaemonSet(config.VpcCNIDaemonSetName, "")
	assert.NotNil(t, err)
}

func TestK8sWrapper_GetDeployment(t *testing.T) {
	ctrl := gomock.NewController(t)
	wrapper, _, _ := getMockK8sWrapperWithClient(ctrl, []runtime.Object{mockNode, mockDeployment, mockDS})

	deployment, err := wrapper.GetDeployment(config.KubeSystemNamespace,
		config.OldVPCControllerDeploymentName)
	assert.NoError(t, err)
	assert.Equal(t, deployment.ObjectMeta, mockDeployment.ObjectMeta)
}

func TestK8sWrapper_GetDeployment_Err(t *testing.T) {
	ctrl := gomock.NewController(t)
	wrapper, _, _ := getMockK8sWrapperWithClient(ctrl, []runtime.Object{mockNode, mockDeployment, mockDS})

	_, err := wrapper.GetDeployment("default",
		config.OldVPCControllerDeploymentName)
	assert.Error(t, err)
}

func TestK8sWrapper_CreateCNINodeWithExistedObject_NoError(t *testing.T) {
	ctrl := gomock.NewController(t)
	wrapper, _, _ := getMockK8sWrapperWithClient(ctrl, []runtime.Object{mockCNINode})

	err := wrapper.CreateCNINode(mockNode, mockClusterName, mockNodeID)
	assert.NoError(t, err)
	cniNode, err := wrapper.GetCNINode(types.NamespacedName{Name: mockNode.Name})
	assert.NoError(t, err)
	assert.Equal(t, mockNode.Name, cniNode.Name)
	err = wrapper.CreateCNINode(mockNode, mockClusterName, mockNodeID)
	assert.NoError(t, err)
}

func TestK8sWrapper_CreateCNINode_NoError(t *testing.T) {
	ctrl := gomock.NewController(t)
	wrapper, _, _ := getMockK8sWrapperWithClient(ctrl, []runtime.Object{})

	err := wrapper.CreateCNINode(mockNode, mockClusterName, mockNodeID)
	assert.NoError(t, err)
	cniNode, err := wrapper.GetCNINode(types.NamespacedName{Name: mockNode.Name})
	assert.NoError(t, err)
	assert.Equal(t, mockNode.Name, cniNode.Name)
	assert.Equal(t, mockNodeID, cniNode.Spec.Tags[config.NetworkInterfaceNodeIDKey])
	assert.Equal(t, mockClusterName, cniNode.Spec.Tags[config.VPCCNIClusterNameKey])
	assert.Contains(t, cniNode.Finalizers, config.NodeTerminationFinalizer)
}
