package controller

import (
	"fmt"
	"reflect"
	"testing"

	"k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	k8sfake "k8s.io/client-go/kubernetes/fake"
	"k8s.io/client-go/tools/cache"
	k8stesting "k8s.io/client-go/tools/cache/testing"
	"k8s.io/client-go/util/workqueue"
	. "kubevirt.io/containerized-data-importer/pkg/common"
)

func TestNewImportController(t *testing.T) {
	type args struct {
		client        kubernetes.Interface
		pvcInformer   cache.SharedIndexInformer
		podInformer   cache.SharedIndexInformer
		importerImage string
		pullPolicy    string
		verbose       string
	}
	// Set up environment
	myclient := k8sfake.NewSimpleClientset()
	pvcSource := k8stesting.NewFakePVCControllerSource()
	podSource := k8stesting.NewFakeControllerSource()

	// create informers
	pvcInformer := cache.NewSharedIndexInformer(pvcSource, nil, DEFAULT_RESYNC_PERIOD, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
	podInformer := cache.NewSharedIndexInformer(podSource, nil, DEFAULT_RESYNC_PERIOD, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})

	tests := []struct {
		name string
		args args
		want *ImportController
	}{
		{
			name: "expect controller to be created with expected informers",
			args: args{myclient, pvcInformer, podInformer, "test/image", "Always", "-v=5"},
			want: &ImportController{myclient, nil, nil, pvcInformer, podInformer, "test/image", "Always", "-v=5"},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := NewImportController(tt.args.client, tt.args.pvcInformer, tt.args.podInformer, tt.args.importerImage, tt.args.pullPolicy, tt.args.verbose)
			if got == nil {
				t.Errorf("NewImportController() not created - %v", got)
			}
			if !reflect.DeepEqual(got.podInformer, tt.want.podInformer) {
				t.Errorf("NewImportController() podInformer = %v, want %v", got.podInformer, tt.want.podInformer)
			}
			if !reflect.DeepEqual(got.pvcInformer, tt.want.pvcInformer) {
				t.Errorf("NewImportController() pvcInformer = %v, want %v", got.pvcInformer, tt.want.pvcInformer)
			}
			// queues are generated from the controller
			if got.pvcQueue == nil {
				t.Errorf("NewImportController() pvcQueue was not generated properly = %v", got.pvcQueue)
			}
			if got.podQueue == nil {
				t.Errorf("NewImportController() podQueue was not generated properly = %v", got.podQueue)
			}
		})
	}
}

func TestController_Run(t *testing.T) {
	type fields struct {
		clientset     kubernetes.Interface
		pvcQueue      workqueue.RateLimitingInterface
		podQueue      workqueue.RateLimitingInterface
		pvcInformer   cache.SharedIndexInformer
		podInformer   cache.SharedIndexInformer
		importerImage string
		pullPolicy    string
		verbose       string
	}
	type args struct {
		threadiness int
		stopCh      <-chan struct{}
	}

	tests := []struct {
		name    string
		fields  fields
		args    args
		wantErr bool
	}{
		// TODO: Add test cases.
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			c := &ImportController{
				clientset:     tt.fields.clientset,
				pvcQueue:      tt.fields.pvcQueue,
				podQueue:      tt.fields.podQueue,
				pvcInformer:   tt.fields.pvcInformer,
				podInformer:   tt.fields.podInformer,
				importerImage: tt.fields.importerImage,
				pullPolicy:    tt.fields.pullPolicy,
				verbose:       tt.fields.verbose,
			}
			if err := c.Run(tt.args.threadiness, tt.args.stopCh); (err != nil) != tt.wantErr {
				t.Errorf("Controller.Run() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

func TestController_ProcessNextPodItem(t *testing.T) {
	//create pod and pvc
	pvcWithEndPointAnno := createPvc("testPvcWithEndPointAnno", "default", map[string]string{AnnEndpoint: "http://test"}, nil)
	podWithCdiAnno := createPod(pvcWithEndPointAnno, DataVolName)

	//create and run the informers
	c, _, _, err := createImportController(pvcWithEndPointAnno, podWithCdiAnno, "default")
	if err != nil {
		t.Errorf("Controller.ProcessNextPodItem() failed to initialize fake controller error = %v", err)
		return
	}

	tests := []struct {
		name string
		want bool
	}{
		{
			name: "successfully process pod",
			want: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := c.ProcessNextPodItem(); got != tt.want {
				t.Errorf("Controller.ProcessNextPodItem() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestController_processPodItem(t *testing.T) {
	//create pod and pvc
	const stageCount = 3

	//Generate Staging Specs
	// dictates what NS the spec is actually created in
	stageNs := make([]string, stageCount)
	stageNs[0] = "default"
	stageNs[1] = "default"
	stageNs[2] = "test"

	// create pvc specs
	stagePvcs := make([]*v1.PersistentVolumeClaim, stageCount)
	stagePvcs[0] = createPvc("testPvc1", "default", map[string]string{AnnEndpoint: "http://test"}, nil)
	stagePvcs[1] = createPvc("testPvc2", "default", map[string]string{AnnEndpoint: "http://test"}, nil)
	stagePvcs[2] = createPvc("testPvc3", "default", map[string]string{AnnEndpoint: "http://test"}, nil)

	// create pod specs with pvc reference
	stagePods := make([]*v1.Pod, stageCount)
	stagePods[0] = createPod(stagePvcs[0], DataVolName)
	stagePods[1] = createPod(stagePvcs[1], "myvolume")
	stagePods[2] = createPod(stagePvcs[2], DataVolName)

	//create and run the informers passing back v1 processed pods
	c, _, pods, err := createImportControllerMultiObject(stagePvcs, stagePods, stageNs)
	if err != nil {
		t.Errorf("Controller.ProcessPodItem() failed to initialize fake controller error = %v", err)
		return
	}

	type args struct {
		pod *v1.Pod
	}

	tests := []struct {
		name    string
		args    args
		wantErr bool
	}{
		{
			name:    "successfully process pod",
			args:    args{pods[0]},
			wantErr: false,
		},
		{
			name:    "expect error when processing pod with no cdi-data-volume",
			args:    args{pods[1]},
			wantErr: true,
		},
		{
			name:    "expect error when proessing pvc with different NS than expected pod",
			args:    args{pods[2]},
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if err := c.processPodItem(tt.args.pod); (err != nil) != tt.wantErr {
				t.Errorf("Controller.processPodItem() error = %v, wantErr %v, Pod Name = %v, Pod ClaimRef = %v", err, tt.wantErr, tt.args.pod.Name, tt.args.pod.Spec.Volumes)
			}
		})
	}
}

func TestController_ProcessNextPvcItem(t *testing.T) {
	//create pod and pvc
	const stageCount = 4

	//Generate Staging Specs
	// dictates what NS the spec is actually created in
	stageNs := make([]string, stageCount)
	stageNs[0] = "default"
	stageNs[1] = "default"
	stageNs[2] = "default"
	stageNs[3] = "default"

	// create pvc specs
	stagePvcs := make([]*v1.PersistentVolumeClaim, stageCount)
	stagePvcs[0] = createPvc("testPvc1", "default", map[string]string{AnnEndpoint: "http://test"}, nil)
	stagePvcs[1] = createPvc("testPvc2", "default", map[string]string{AnnEndpoint: "http://test", AnnImportPod: "cdi-importer-pod"}, nil)
	stagePvcs[2] = createPvc("testPvc3", "default", nil, nil)
	stagePvcs[3] = createPvc("testPvc4", "default", map[string]string{AnnEndpoint: "http://test"}, nil)

	// create pod specs with pvc reference
	stagePods := make([]*v1.Pod, stageCount)
	stagePods[0] = createPod(stagePvcs[0], DataVolName)
	stagePods[1] = createPod(stagePvcs[1], DataVolName)
	stagePods[2] = createPod(stagePvcs[2], DataVolName)
	stagePods[3] = createPod(stagePvcs[3], DataVolName)

	//create and run queues and informers
	c, _, _, err := createImportControllerMultiObject(stagePvcs, stagePods, stageNs)
	if err != nil {
		t.Errorf("Controller.ProcessNextPvcItem() failed to initialize fake controller error = %v", err)
		return
	}

	tests := []struct {
		name string
		want bool
	}{
		{
			name: "expect successful processing of queue",
			want: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := c.ProcessNextPvcItem(); got != tt.want {
				t.Errorf("Controller.ProcessNextPvcItem() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestController_processPvcItem(t *testing.T) {
	//create pod and pvc
	const stageCount = 2

	//Generate Staging Specs
	// create pvc specs
	stagePvcs := make([]*v1.PersistentVolumeClaim, stageCount)
	stagePvcs[0] = createPvc("testPvc1", "default", map[string]string{AnnEndpoint: "http://test"}, nil)
	stagePvcs[1] = createPvc("testPvc2", "default", map[string]string{AnnEndpoint: "http://test", AnnImportPod: "cdi-importer-pod"}, nil)

	//create and run queues and informers
	c, _, _, err := createImportControllerMultiObject(stagePvcs, nil, nil)
	if err != nil {
		t.Errorf("Controller.ProcessPvcItem() failed to initialize fake controller error = %v", err)
		return
	}

	type args struct {
		pvc *v1.PersistentVolumeClaim
	}
	tests := []struct {
		name     string
		args     args
		podName  string
		wantErr  bool
		wantFind bool
	}{
		{
			name:     "process pvc and expect importer pod to be created",
			args:     args{stagePvcs[0]},
			podName:  fmt.Sprintf("importer-%s-", stagePvcs[0].Name),
			wantErr:  false,
			wantFind: true,
		},
		{
			name:     "process pvc and do not create importer pod",
			args:     args{stagePvcs[1]},
			podName:  fmt.Sprintf("importer-%s-", stagePvcs[1].Name),
			wantErr:  false,
			wantFind: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if err := c.processPvcItem(tt.args.pvc); (err != nil) != tt.wantErr {
				t.Errorf("Controller.processPvcItem() error = %v, wantErr %v", err, tt.wantErr)
			}
			//find importer pod
			found := false
			got, err := c.clientset.CoreV1().Pods(tt.args.pvc.Namespace).List(metav1.ListOptions{})
			if (len(got.Items) == 0 || err != nil) && !tt.wantErr {
				t.Errorf("Controller.processPvcItem() could not find any pods or got error = %v", err)
			} else {
				for _, p := range got.Items {
					if p.GenerateName == tt.podName {
						found = true
					}
				}
				if found && !tt.wantFind {
					t.Errorf("Controller.processPvcItem() found pod %v but expected not to find pod", tt.podName)
				}
				if !found && tt.wantFind {
					t.Errorf("Controller.processPvcItem() did not find pod %v and expected to find pod", tt.podName)
				}
			}
		})
	}
}

func TestController_forgetKey(t *testing.T) {
	//create staging pvc and pod
	pvcWithEndPointAnno := createPvc("testPvcWithEndPointAnno", "default", map[string]string{AnnEndpoint: "http://test"}, nil)
	podWithCdiAnno := createPod(pvcWithEndPointAnno, DataVolName)

	//run the informers
	c, _, _, err := createImportController(pvcWithEndPointAnno, podWithCdiAnno, "default")
	if err != nil {
		t.Errorf("Controller.forgetKey() failed to initialize fake controller error = %v", err)
		return
	}
	key, shutdown := c.pvcQueue.Get()
	if shutdown {
		t.Errorf("Controller.forgetKey() failed to retrieve key from pvcQueue")
		return
	}
	defer c.pvcQueue.Done(key)

	type args struct {
		key interface{}
		msg string
	}
	tests := []struct {
		name string
		args args
		want bool
	}{
		{
			name: "successfully forget key",
			args: args{key, "test of forgetKey func"},
			want: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := c.forgetKey(tt.args.key, tt.args.msg); got != tt.want {
				t.Errorf("Controller.forgetKey() = %v, want %v", got, tt.want)
			}
		})
	}
}
