// 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 handler

import (
	"testing"

	"github.com/aws/amazon-vpc-resource-controller-k8s/mocks/amazon-vcp-resource-controller-k8s/pkg/k8s"
	"github.com/aws/amazon-vpc-resource-controller-k8s/mocks/amazon-vcp-resource-controller-k8s/pkg/k8s/pod"
	"github.com/aws/amazon-vpc-resource-controller-k8s/mocks/amazon-vcp-resource-controller-k8s/pkg/pool"
	"github.com/aws/amazon-vpc-resource-controller-k8s/mocks/amazon-vcp-resource-controller-k8s/pkg/provider"
	"github.com/aws/amazon-vpc-resource-controller-k8s/pkg/api"
	"github.com/aws/amazon-vpc-resource-controller-k8s/pkg/config"
	"github.com/aws/amazon-vpc-resource-controller-k8s/pkg/pool"
	"github.com/aws/amazon-vpc-resource-controller-k8s/pkg/worker"

	"github.com/golang/mock/gomock"
	"github.com/stretchr/testify/assert"
	v1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	k8sctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
)

var (
	resourceName = config.ResourceNameIPAddress

	uid          = "uid"
	nodeName     = "node-1"
	podName      = "pod-1"
	podNamespace = "pod-ns"
	ipAddress    = "192.168.1.1"

	pod = &v1.Pod{
		TypeMeta: metav1.TypeMeta{},
		ObjectMeta: metav1.ObjectMeta{
			UID:         types.UID(uid),
			Name:        podName,
			Namespace:   podNamespace,
			Annotations: map[string]string{config.ResourceNameIPAddress: ipAddress},
		},
		Spec: v1.PodSpec{
			NodeName: nodeName,
		},
		Status: v1.PodStatus{},
	}

	job = worker.NewWarmPoolCreateJob(nodeName, 1)
)

// TestWarmResourceHandler_HandleCreate tests create assigns a resource and annotates the pod and then reconciles a pool
// and submits the job to the resource provider
func TestWarmResourceHandler_HandleCreate(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	handler, mockK8sWrapper, mockPodAPI, mockProvider, mockPool := getHandlerAndMocks(ctrl)
	podCopy := pod.DeepCopy()

	mockProvider.EXPECT().GetPool(nodeName).Return(mockPool, true)
	mockPool.EXPECT().AssignResource(uid).Return(ipAddress, true, nil)
	mockPodAPI.EXPECT().AnnotatePod(pod.Namespace, pod.Name, types.UID(uid), resourceName, ipAddress).Return(nil)
	mockK8sWrapper.EXPECT().BroadcastEvent(podCopy, ReasonResourceAllocated, gomock.Any(), v1.EventTypeNormal)

	mockPool.EXPECT().ReconcilePool().Return(job)
	mockProvider.EXPECT().SubmitAsyncJob(job)

	delete(podCopy.Annotations, config.ResourceNameIPAddress)

	_, err := handler.HandleCreate(1, podCopy)
	assert.NoError(t, err)
}

func TestWarmResourceHandler_PoolEmpty(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	handler, mockK8sWrapper, _, mockProvider, mockPool := getHandlerAndMocks(ctrl)
	podCopy := pod.DeepCopy()

	mockProvider.EXPECT().GetPool(nodeName).Return(mockPool, true)
	mockPool.EXPECT().AssignResource(uid).Return("", true, pool.ErrWarmPoolEmpty)
	mockK8sWrapper.EXPECT().BroadcastEvent(podCopy, ReasonResourceAllocationFailed, gomock.Any(), v1.EventTypeWarning)
	mockPool.EXPECT().ReconcilePool().Return(job)
	mockProvider.EXPECT().SubmitAsyncJob(job)

	delete(podCopy.Annotations, config.ResourceNameIPAddress)

	rslt, err := handler.HandleCreate(1, podCopy)
	assert.NoError(t, err)
	assert.Equal(t, k8sctrl.Result{
		Requeue:      true,
		RequeueAfter: RequeueAfterWhenWPEmpty,
	}, rslt)
}

// TestWarmResourceHandler_HandleCreate_AlreadyAnnotated tests that if the pod is already annotated then the pool
// is not invoked again
func TestWarmResourceHandler_HandleCreate_AlreadyAnnotated(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	handler, _, _, mockProvider, mockPool := getHandlerAndMocks(ctrl)

	mockProvider.EXPECT().GetPool(nodeName).Return(mockPool, true)

	_, err := handler.HandleCreate(1, pod)
	assert.NoError(t, err)
}

// TestNewWarmResourceHandler_HandleDelete tests resources are deleted by calling the respective resource provider
func TestWarmResourceHandler_HandleDelete(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	handler, _, _, mockProvider, mockPool := getHandlerAndMocks(ctrl)

	mockProvider.EXPECT().GetPool(nodeName).Return(mockPool, true)
	mockPool.EXPECT().FreeResource(uid, ipAddress).Return(false, nil)

	_, err := handler.HandleDelete(pod)
	assert.NoError(t, err)
}

// TestNewWarmResourceHandler_HandleDelete tests resources are deleted by calling the respective resource provider
func TestWarmResourceHandler_HandleDelete_ReconcileAfter(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	handler, _, _, mockProvider, mockPool := getHandlerAndMocks(ctrl)

	mockProvider.EXPECT().GetPool(nodeName).Return(mockPool, true)
	mockPool.EXPECT().FreeResource(uid, ipAddress).Return(true, nil)
	mockPool.EXPECT().ReconcilePool().Return(job)
	mockProvider.EXPECT().SubmitAsyncJob(job)

	_, err := handler.HandleDelete(pod)
	assert.NoError(t, err)
}

// TestNewWarmResourceHandler_HandleDelete tests resources are deleted by calling the respective resource provider
func TestWarmResourceHandler_HandleDelete_ResourceNotExist(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	handler, _, _, mockProvider, mockPool := getHandlerAndMocks(ctrl)

	mockProvider.EXPECT().GetPool(nodeName).Return(mockPool, true)

	podCopy := pod.DeepCopy()
	delete(podCopy.Annotations, config.ResourceNameIPAddress)

	mockPool.EXPECT().GetAssignedResource(uid).Return("", false)

	_, err := handler.HandleDelete(podCopy)
	assert.NoError(t, err)
}

func TestWarmResourceHandler_HandleDelete_ResourceExistsInDataStoreOnly(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	handler, _, _, mockProvider, mockPool := getHandlerAndMocks(ctrl)

	mockProvider.EXPECT().GetPool(nodeName).Return(mockPool, true)

	podCopy := pod.DeepCopy()
	delete(podCopy.Annotations, config.ResourceNameIPAddress)

	mockPool.EXPECT().GetAssignedResource(string(podCopy.UID)).Return(ipAddress, true)
	mockPool.EXPECT().FreeResource(string(podCopy.UID), ipAddress).Return(false, nil)

	_, err := handler.HandleDelete(podCopy)
	assert.NoError(t, err)
}

// TestNewWarmResourceHandler_HandleDelete_Error asserts error is returned if the resource pool is not found
func TestWarmResourceHandler_HandleDelete_Error(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	handler, _, _, mockProvider, _ := getHandlerAndMocks(ctrl)

	mockProvider.EXPECT().GetPool(nodeName).Return(nil, false)

	_, err := handler.HandleDelete(pod)
	assert.Nil(t, err)
}

// TestWarmResourceHandler_getResourcePool returns the resource pool for the given node name
func TestWarmResourceHandler_getResourcePool(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	handler, _, _, mockProvider, mockPool := getHandlerAndMocks(ctrl)

	mockProvider.EXPECT().GetPool(nodeName).Return(mockPool, true)

	resourcePool, err := handler.getResourcePool(nodeName)

	assert.NoError(t, err)
	assert.Equal(t, mockPool, resourcePool)
}

// TestWarmResourceHandler_getResourcePool_PoolNotFound returns error if the resource pool is not present
func TestWarmResourceHandler_getResourcePool_PoolNotFound(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	handler, _, _, mockProvider, _ := getHandlerAndMocks(ctrl)

	mockProvider.EXPECT().GetPool(nodeName).Return(nil, false)

	_, err := handler.getResourcePool(nodeName)

	assert.NotNil(t, err)
}

func getHandlerAndMocks(ctrl *gomock.Controller) (*warmResourceHandler, *mock_k8s.MockK8sWrapper,
	*mock_pod.MockPodClientAPIWrapper, *mock_provider.MockResourceProvider, *mock_pool.MockPool) {

	mockWrapper := mock_k8s.NewMockK8sWrapper(ctrl)
	mockProvider := mock_provider.NewMockResourceProvider(ctrl)
	mockPool := mock_pool.NewMockPool(ctrl)
	mockPodAPI := mock_pod.NewMockPodClientAPIWrapper(ctrl)

	handler := &warmResourceHandler{
		log: zap.New(zap.UseDevMode(true)).WithName("warm handler"),
		APIWrapper: api.Wrapper{
			K8sAPI: mockWrapper,
			PodAPI: mockPodAPI,
		},
		resourceName:     resourceName,
		resourceProvider: mockProvider,
	}

	return handler, mockWrapper, mockPodAPI, mockProvider, mockPool
}
