package controller

import (
	"fmt"
	"reflect"
	"testing"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/tools/cache"

	. "kubevirt.io/containerized-data-importer/pkg/common"
)

type CloneFixture struct {
	ControllerFixture
}

func newCloneFixture(t *testing.T) *CloneFixture {
	f := &CloneFixture{
		ControllerFixture: *newControllerFixture(t),
	}
	return f
}

func (f *CloneFixture) newCloneController() *CloneController {
	return &CloneController{
		Controller: *f.newController("test/mycloneimage", "Always", "5"),
	}
}

func (f *CloneFixture) run(pvcName string) {
	f.runController(pvcName, true, false, false)
}

func (f *CloneFixture) runWithExpectation(pvcName string) {
	f.runController(pvcName, true, false, true)
}

func (f *CloneFixture) runExpectError(pvcName string) {
	f.runController(pvcName, true, true, false)
}

func (f *CloneFixture) runController(pvcName string,
	startInformers bool,
	expectError bool,
	withCreateExpectation bool) {
	c := f.newCloneController()
	if startInformers {
		stopCh := make(chan struct{})
		defer close(stopCh)
		go c.pvcInformer.Run(stopCh)
		go c.podInformer.Run(stopCh)
		cache.WaitForCacheSync(stopCh, c.pvcInformer.HasSynced)
		cache.WaitForCacheSync(stopCh, c.pvcInformer.HasSynced)
	}

	if withCreateExpectation {
		c.raisePodCreate(pvcName)
	}
	err := c.syncPvc(pvcName)
	if !expectError && err != nil {
		f.t.Errorf("error syncing pvc: %s: %v", pvcName, err)
	} else if expectError && err == nil {
		f.t.Error("expected error syncing pvc, got nil")
	}

	k8sActions := filterActions(f.kubeclient.Actions())
	for i, action := range k8sActions {
		if len(f.kubeactions) < i+1 {
			f.t.Errorf("%d unexpected actions: %+v", len(k8sActions)-len(f.kubeactions), k8sActions[i:])
			break
		}

		expectedAction := f.kubeactions[i]
		checkAction(expectedAction, action, f.t)
	}

	if len(f.kubeactions) > len(k8sActions) {
		f.t.Errorf("%d additional expected actions:%+v", len(f.kubeactions)-len(k8sActions), f.kubeactions[len(k8sActions):])
	}
}

// Verifies basic pods creation when new PVC is discovered
func TestCreatesClonePods(t *testing.T) {
	f := newCloneFixture(t)
	pvc := createClonePvc("target-pvc", "target-ns", map[string]string{AnnCloneRequest: "source-ns/golden-pvc"}, nil)

	f.pvcLister = append(f.pvcLister, pvc)
	f.kubeobjects = append(f.kubeobjects, pvc)
	id := string(pvc.GetUID())
	expSourcePod := createSourcePod(pvc, DataVolName, id)
	f.expectCreatePodAction(expSourcePod)
	expTargetPod := createTargetPod(pvc, DataVolName, id, "source-ns")
	f.expectCreatePodAction(expTargetPod)

	f.run(getPvcKey(pvc, t))
}

// Verifies pods creation is observed and pvc labels are set.
func TestCloneObservePod(t *testing.T) {
	f := newCloneFixture(t)
	pvc := createPvc("trget-pvc", "target-ns", map[string]string{AnnCloneRequest: "source-ns/golden-pvc"}, nil)
	id := string(pvc.GetUID())

	sourcePod := createSourcePod(pvc, DataVolName, id)
	sourcePod.Name = "madeup-source-name"
	sourcePod.Status.Phase = corev1.PodPending
	sourcePod.Namespace = "source-ns"

	targetPod := createTargetPod(pvc, DataVolName, id, sourcePod.Namespace)
	targetPod.Name = "madeup-target-name"
	targetPod.Status.Phase = corev1.PodPending
	targetPod.Namespace = "target-ns"

	f.pvcLister = append(f.pvcLister, pvc)
	f.podLister = append(f.podLister, sourcePod)
	f.podLister = append(f.podLister, targetPod)
	f.kubeobjects = append(f.kubeobjects, pvc)
	f.kubeobjects = append(f.kubeobjects, sourcePod)
	f.kubeobjects = append(f.kubeobjects, targetPod)

	expPvc := pvc.DeepCopy()
	expPvc.ObjectMeta.Labels = map[string]string{CDILabelKey: CDILabelValue}
	expPvc.ObjectMeta.Annotations = map[string]string{AnnPodPhase: string(corev1.PodPending), AnnCloneRequest: "source-ns/golden-pvc"}

	f.expectUpdatePvcAction(expPvc)

	f.run(getPvcKey(pvc, t))
}

// Verifies pods status updates are reflected in PVC annotations
func TestClonePodStatusUpdating(t *testing.T) {
	f := newCloneFixture(t)
	pvc := createPvc("target-pvc", "target-ns", map[string]string{AnnCloneRequest: "source-ns/golden-pvc"}, nil)
	id := string(pvc.GetUID())
	sourcePod := createSourcePod(pvc, DataVolName, id)
	sourcePod.Name = "madeup-source-name"
	sourcePod.Status.Phase = corev1.PodRunning
	sourcePod.Namespace = "source-ns"

	targetPod := createTargetPod(pvc, DataVolName, id, sourcePod.Namespace)
	targetPod.Name = "madeup-target-name"
	targetPod.Status.Phase = corev1.PodRunning
	targetPod.Namespace = "target-ns"

	pvc.ObjectMeta.Annotations = map[string]string{AnnPodPhase: string(corev1.PodPending), AnnCloneRequest: "source-ns/golden-pvc"}
	pvc.ObjectMeta.Labels = map[string]string{CDILabelKey: CDILabelValue}

	f.pvcLister = append(f.pvcLister, pvc)
	f.podLister = append(f.podLister, sourcePod)
	f.podLister = append(f.podLister, targetPod)
	f.kubeobjects = append(f.kubeobjects, pvc)
	f.kubeobjects = append(f.kubeobjects, sourcePod)
	f.kubeobjects = append(f.kubeobjects, targetPod)

	// expecting pvc's pod status annotation to be updated from pending => running
	expPvc := pvc.DeepCopy()
	expPvc.ObjectMeta.Annotations = map[string]string{AnnPodPhase: string(targetPod.Status.Phase), AnnCloneRequest: "source-ns/golden-pvc"}

	f.expectUpdatePvcAction(expPvc)

	f.run(getPvcKey(pvc, t))
}

// verifies no work is done on pvcs without our annotations
func TestCloneIgnorePVC(t *testing.T) {
	f := newCloneFixture(t)
	pvc := createPvc("target-pvc", "target-ns", nil, nil)

	f.pvcLister = append(f.pvcLister, pvc)
	f.kubeobjects = append(f.kubeobjects, pvc)

	f.runWithExpectation(getPvcKey(pvc, t))
}

// verify error if ownership doesn't match
func TestCloneOwnership(t *testing.T) {
	f := newCloneFixture(t)
	pvc := createPvc("testPvc1", "default", map[string]string{AnnCloneRequest: "source-ns/golden-pvc"}, nil)
	id := string(pvc.GetUID())
	sourcePod := createSourcePod(pvc, DataVolName, id)
	sourcePod.Name = "madeup-source-name"
	sourcePod.Status.Phase = corev1.PodPending
	sourcePod.Namespace, _ = ParseSourcePvcAnnotation(pvc.GetAnnotations()[AnnCloneRequest], "/")
	sourcePod.ObjectMeta.OwnerReferences = []metav1.OwnerReference{}

	targetPod := createTargetPod(pvc, DataVolName, id, sourcePod.Namespace)
	targetPod.Status.Phase = corev1.PodPending
	targetPod.Namespace = pvc.Namespace
	targetPod.ObjectMeta.OwnerReferences = []metav1.OwnerReference{}

	f.pvcLister = append(f.pvcLister, pvc)
	f.podLister = append(f.podLister, sourcePod)
	f.podLister = append(f.podLister, targetPod)
	f.kubeobjects = append(f.kubeobjects, pvc)
	f.kubeobjects = append(f.kubeobjects, sourcePod)
	f.kubeobjects = append(f.kubeobjects, targetPod)

	f.runExpectError(getPvcKey(pvc, t))
}

func TestCloneFindPodsInCacheUpdating(t *testing.T) {

	f := newCloneFixture(t)

	tests := []struct {
		pvc       *corev1.PersistentVolumeClaim
		sourcePod *corev1.Pod
		targetPod *corev1.Pod
	}{
		{
			pvc: createPvc("target-pvc", "target-ns", map[string]string{AnnCloneRequest: "source-ns/golden-pvc"}, nil),
		},
		{
			pvc: createPvc("target-pvc2", "target2-ns", map[string]string{AnnCloneRequest: "source-ns/golden-pvc"}, nil),
		},
	}

	id := string(tests[0].pvc.GetUID())
	tests[0].sourcePod = createSourcePod(tests[0].pvc, DataVolName, id)
	tests[0].sourcePod.Namespace = "source-ns"
	tests[0].sourcePod.Name = fmt.Sprintf("fakesourcename%d", 1)

	tests[0].targetPod = createTargetPod(tests[0].pvc, DataVolName, id, tests[0].sourcePod.Namespace)
	tests[0].targetPod.Namespace = "target-ns"
	tests[0].sourcePod.Name = fmt.Sprintf("faketargetname%d", 1)

	f.pvcLister = append(f.pvcLister, tests[0].pvc)
	f.podLister = append(f.podLister, tests[0].sourcePod)
	f.podLister = append(f.podLister, tests[0].targetPod)

	f.kubeobjects = append(f.kubeobjects, tests[0].pvc)
	f.kubeobjects = append(f.kubeobjects, tests[0].sourcePod)
	f.kubeobjects = append(f.kubeobjects, tests[0].targetPod)

	tests[1].sourcePod = createSourcePod(tests[1].pvc, DataVolName, id)
	tests[1].sourcePod.Namespace = "source-ns"
	tests[1].sourcePod.Name = fmt.Sprintf("fakesourcename%d", 2)

	tests[1].targetPod = createTargetPod(tests[1].pvc, DataVolName, id, tests[1].sourcePod.Namespace)
	tests[1].targetPod.Namespace = "target2-ns"
	tests[1].sourcePod.Name = fmt.Sprintf("faketargetname%d", 2)

	f.pvcLister = append(f.pvcLister, tests[1].pvc)
	f.podLister = append(f.podLister, tests[1].sourcePod)
	f.podLister = append(f.podLister, tests[1].targetPod)

	f.kubeobjects = append(f.kubeobjects, tests[1].pvc)
	f.kubeobjects = append(f.kubeobjects, tests[1].sourcePod)
	f.kubeobjects = append(f.kubeobjects, tests[1].targetPod)

	controller := f.newCloneController()

	stopCh := make(chan struct{})
	defer close(stopCh)
	go controller.pvcInformer.Run(stopCh)
	go controller.podInformer.Run(stopCh)
	cache.WaitForCacheSync(stopCh, controller.pvcInformer.HasSynced)
	cache.WaitForCacheSync(stopCh, controller.pvcInformer.HasSynced)

	for _, test := range tests {
		foundSourcePod, foundTargetPod, err := controller.findClonePodsFromCache(test.pvc)
		if err != nil {
			t.Errorf("unexpected error: %v", err)
		}

		if foundSourcePod == nil {
			t.Errorf("didn't find source pod for pvc %v", test.pvc)
		}
		if !reflect.DeepEqual(foundSourcePod, test.sourcePod) {
			t.Errorf("wrong source pod found.\nfound %v\nwant %v", foundSourcePod, test.sourcePod)
		}
		if foundTargetPod == nil {
			t.Errorf("didn't find target pod for pvc %v", test.pvc)
		}
		if !reflect.DeepEqual(foundSourcePod, test.sourcePod) {
			t.Errorf("wrong source pod found.\nfound %v\nwant %v", foundSourcePod, test.sourcePod)
		}
	}
}
