package controllers_test

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/golang/mock/gomock"
	. "github.com/onsi/gomega"
	apiv1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	"github.com/aws/eks-anywhere/controllers"
	anywherev1 "github.com/aws/eks-anywhere/pkg/api/v1alpha1"
	"github.com/aws/eks-anywhere/pkg/constants"
	"github.com/aws/eks-anywhere/pkg/providers/cloudstack"
	"github.com/aws/eks-anywhere/pkg/providers/cloudstack/decoder"
)

func TestCloudStackDatacenterReconcilerSetupWithManager(t *testing.T) {
	client := env.Client()
	r := controllers.NewCloudStackDatacenterReconciler(client, nil)

	g := NewWithT(t)
	g.Expect(r.SetupWithManager(env.Manager())).To(Succeed())
}

func TestCloudStackDatacenterReconcilerSuccess(t *testing.T) {
	g := NewWithT(t)
	ctx := context.Background()

	dcConfig := createCloudstackDatacenterConfig()
	secrets := &apiv1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "testCred",
			Namespace: constants.EksaSystemNamespace,
		},
		Data: map[string][]byte{
			decoder.APIKeyKey:    []byte("test-key1"),
			decoder.APIUrlKey:    []byte("http://1.1.1.1:8080/client/api"),
			decoder.SecretKeyKey: []byte("test-secret1"),
		},
	}
	objs := []runtime.Object{dcConfig, secrets}
	client := fake.NewClientBuilder().WithRuntimeObjects(objs...).
		WithStatusSubresource(dcConfig).
		Build()

	ctrl := gomock.NewController(t)
	validatorRegistry := cloudstack.NewMockValidatorRegistry(ctrl)
	execConfig := &decoder.CloudStackExecConfig{
		Profiles: []decoder.CloudStackProfileConfig{
			{
				Name:          "testCred",
				ApiKey:        "test-key1",
				SecretKey:     "test-secret1",
				ManagementUrl: "http://1.1.1.1:8080/client/api",
			},
		},
	}
	validator := cloudstack.NewMockProviderValidator(ctrl)
	validatorRegistry.EXPECT().Get(execConfig).Return(validator, nil).Times(1)
	validator.EXPECT().ValidateCloudStackDatacenterConfig(ctx, dcConfig).Times(1)

	req := reconcile.Request{
		NamespacedName: types.NamespacedName{
			Name:      name,
			Namespace: namespace,
		},
	}

	r := controllers.NewCloudStackDatacenterReconciler(client, validatorRegistry)

	_, err := r.Reconcile(ctx, req)
	g.Expect(err).NotTo(HaveOccurred())
}

func TestCloudStackDatacenterReconcilerSetDefaultSuccess(t *testing.T) {
	g := NewWithT(t)
	ctx := context.Background()

	dcConfig := createCloudstackDatacenterConfig()
	dcConfig.Spec.AvailabilityZones = nil
	dcConfig.Spec.Zones = []anywherev1.CloudStackZone{
		{
			Id:      "",
			Name:    "",
			Network: anywherev1.CloudStackResourceIdentifier{},
		},
	}
	secrets := &apiv1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "global",
			Namespace: constants.EksaSystemNamespace,
		},
		Data: map[string][]byte{
			decoder.APIKeyKey:    []byte("test-key1"),
			decoder.APIUrlKey:    []byte("http://1.1.1.1:8080/client/api"),
			decoder.SecretKeyKey: []byte("test-secret1"),
		},
	}
	objs := []runtime.Object{dcConfig, secrets}
	client := fake.NewClientBuilder().WithRuntimeObjects(objs...).
		WithStatusSubresource(dcConfig).
		Build()

	ctrl := gomock.NewController(t)
	validatorRegistry := cloudstack.NewMockValidatorRegistry(ctrl)
	execConfig := &decoder.CloudStackExecConfig{
		Profiles: []decoder.CloudStackProfileConfig{
			{
				Name:          "global",
				ApiKey:        "test-key1",
				SecretKey:     "test-secret1",
				ManagementUrl: "http://1.1.1.1:8080/client/api",
			},
		},
	}
	validator := cloudstack.NewMockProviderValidator(ctrl)
	validatorRegistry.EXPECT().Get(execConfig).Return(validator, nil).Times(1)
	az := anywherev1.CloudStackAvailabilityZone{
		Name:           anywherev1.DefaultCloudStackAZPrefix + "-0",
		CredentialsRef: "global",
		Zone:           dcConfig.Spec.Zones[0],
	}
	dcConfig.Spec.AvailabilityZones = append(dcConfig.Spec.AvailabilityZones, az)
	dcConfig.Spec.Zones = nil
	validator.EXPECT().ValidateCloudStackDatacenterConfig(ctx, dcConfig).Times(1)

	req := reconcile.Request{
		NamespacedName: types.NamespacedName{
			Name:      name,
			Namespace: namespace,
		},
	}

	r := controllers.NewCloudStackDatacenterReconciler(client, validatorRegistry)
	_, err := r.Reconcile(ctx, req)
	g.Expect(err).NotTo(HaveOccurred())
	getDcConfig := &anywherev1.CloudStackDatacenterConfig{}
	err = client.Get(ctx, req.NamespacedName, getDcConfig)
	g.Expect(err).NotTo(HaveOccurred())
	g.Expect(len(getDcConfig.Spec.AvailabilityZones)).ToNot(Equal(0))
	g.Expect(getDcConfig.Spec.AvailabilityZones[0].Name).To(Equal(anywherev1.DefaultCloudStackAZPrefix + "-0"))
}

func TestCloudstackDatacenterConfigReconcilerDelete(t *testing.T) {
	g := NewWithT(t)
	ctx := context.Background()

	dcConfig := createCloudstackDatacenterConfig()
	dcConfig.DeletionTimestamp = &metav1.Time{Time: time.Now()}
	dcConfig.Finalizers = []string{"my-finalizer"}
	objs := []runtime.Object{dcConfig}
	client := fake.NewClientBuilder().WithRuntimeObjects(objs...).Build()
	ctrl := gomock.NewController(t)
	validatorRegistry := cloudstack.NewMockValidatorRegistry(ctrl)

	req := reconcile.Request{
		NamespacedName: types.NamespacedName{
			Name:      name,
			Namespace: namespace,
		},
	}

	r := controllers.NewCloudStackDatacenterReconciler(client, validatorRegistry)

	_, err := r.Reconcile(ctx, req)
	g.Expect(err).NotTo(HaveOccurred())
}

func TestCloudstackDatacenterConfigGetValidatorFailure(t *testing.T) {
	g := NewWithT(t)
	ctx := context.Background()

	dcConfig := createCloudstackDatacenterConfig()
	dcConfig.Spec.AvailabilityZones = nil
	objs := []runtime.Object{dcConfig}
	client := fake.NewClientBuilder().WithRuntimeObjects(objs...).Build()

	ctrl := gomock.NewController(t)
	validatorRegistry := cloudstack.NewMockValidatorRegistry(ctrl)
	execConfig := &decoder.CloudStackExecConfig{}
	errMsg := "building cmk executable: nil exec config for CloudMonkey, unable to proceed"
	validatorRegistry.EXPECT().Get(execConfig).Return(nil, errors.New(errMsg)).Times(1)

	req := reconcile.Request{
		NamespacedName: types.NamespacedName{
			Name:      name,
			Namespace: namespace,
		},
	}

	r := controllers.NewCloudStackDatacenterReconciler(client, validatorRegistry)

	_, err := r.Reconcile(ctx, req)
	g.Expect(err).To(MatchError(ContainSubstring(errMsg)))
}

func TestCloudstackDatacenterConfigGetDatacenterFailure(t *testing.T) {
	g := NewWithT(t)
	ctx := context.Background()
	client := fake.NewClientBuilder().WithScheme(runtime.NewScheme()).Build()
	ctrl := gomock.NewController(t)
	validatorRegistry := cloudstack.NewMockValidatorRegistry(ctrl)

	req := reconcile.Request{
		NamespacedName: types.NamespacedName{
			Name:      name,
			Namespace: namespace,
		},
	}

	r := controllers.NewCloudStackDatacenterReconciler(client, validatorRegistry)
	_, err := r.Reconcile(ctx, req)
	g.Expect(err).To(MatchError(ContainSubstring("failed getting cloudstack datacenter config")))
}

func TestCloudstackDatacenterConfigGetExecConfigFailure(t *testing.T) {
	g := NewWithT(t)
	ctx := context.Background()

	dcConfig := createCloudstackDatacenterConfig()
	objs := []runtime.Object{dcConfig}
	client := fake.NewClientBuilder().WithRuntimeObjects(objs...).Build()

	req := reconcile.Request{
		NamespacedName: types.NamespacedName{
			Name:      name,
			Namespace: namespace,
		},
	}

	r := controllers.NewCloudStackDatacenterReconciler(client, nil)

	_, err := r.Reconcile(ctx, req)
	g.Expect(err).To(MatchError(ContainSubstring("secrets \"testCred\" not found")))

	gotDatacenterConfig := &anywherev1.CloudStackDatacenterConfig{}
	err = client.Get(ctx, req.NamespacedName, gotDatacenterConfig)
	g.Expect(err).NotTo(HaveOccurred())
	g.Expect(gotDatacenterConfig.Status.SpecValid).To(BeFalse())
}

func TestCloudstackDatacenterConfigAccountNotPresentFailure(t *testing.T) {
	g := NewWithT(t)
	ctx := context.Background()
	dcConfig := createCloudstackDatacenterConfig()
	secrets := &apiv1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "testCred",
			Namespace: constants.EksaSystemNamespace,
		},
		Data: map[string][]byte{
			decoder.APIKeyKey:    []byte("test-key1"),
			decoder.APIUrlKey:    []byte("http://1.1.1.1:8080/client/api"),
			decoder.SecretKeyKey: []byte("test-secret1"),
		},
	}
	objs := []runtime.Object{dcConfig, secrets}
	client := fake.NewClientBuilder().WithRuntimeObjects(objs...).Build()

	ctrl := gomock.NewController(t)
	validatorRegistry := cloudstack.NewMockValidatorRegistry(ctrl)
	execConfig := &decoder.CloudStackExecConfig{
		Profiles: []decoder.CloudStackProfileConfig{
			{
				Name:          "testCred",
				ApiKey:        "test-key1",
				SecretKey:     "test-secret1",
				ManagementUrl: "http://1.1.1.1:8080/client/api",
			},
		},
	}
	validator := cloudstack.NewMockProviderValidator(ctrl)
	validatorRegistry.EXPECT().Get(execConfig).Return(validator, nil).Times(1)
	validator.EXPECT().ValidateCloudStackDatacenterConfig(ctx, dcConfig).Return(errors.New("test error")).Times(1)

	req := reconcile.Request{
		NamespacedName: types.NamespacedName{
			Name:      name,
			Namespace: namespace,
		},
	}

	r := controllers.NewCloudStackDatacenterReconciler(client, validatorRegistry)

	_, err := r.Reconcile(ctx, req)
	g.Expect(err).To(MatchError(ContainSubstring("test error")))

	gotDatacenterConfig := &anywherev1.CloudStackDatacenterConfig{}
	err = client.Get(ctx, req.NamespacedName, gotDatacenterConfig)
	g.Expect(err).NotTo(HaveOccurred())
	g.Expect(gotDatacenterConfig.Status.SpecValid).To(BeFalse())
}

func createCloudstackDatacenterConfig() *anywherev1.CloudStackDatacenterConfig {
	return &anywherev1.CloudStackDatacenterConfig{
		TypeMeta: metav1.TypeMeta{
			Kind:       anywherev1.CloudStackDatacenterKind,
			APIVersion: "anywhere.eks.amazonaws.com/v1alpha1",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: namespace,
		},
		Spec: anywherev1.CloudStackDatacenterConfigSpec{
			AvailabilityZones: []anywherev1.CloudStackAvailabilityZone{
				{
					Name:           "testAz",
					CredentialsRef: "testCred",
					Zone: anywherev1.CloudStackZone{
						Name: "zone1",
						Network: anywherev1.CloudStackResourceIdentifier{
							Name: "SharedNet1",
						},
					},
					Domain:                "testDomain",
					Account:               "testAccount",
					ManagementApiEndpoint: "testApiEndpoint",
				},
			},
		},
	}
}
