package clusterapi_test

import (
	"context"
	"testing"

	. "github.com/onsi/gomega"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
	kubeadmv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1"
	dockerv1 "sigs.k8s.io/cluster-api/test/infrastructure/docker/api/v1beta1"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/aws/eks-anywhere/internal/test"
	"github.com/aws/eks-anywhere/pkg/clients/kubernetes"
	"github.com/aws/eks-anywhere/pkg/clusterapi"
	"github.com/aws/eks-anywhere/pkg/constants"
)

type (
	dockerGroup   = clusterapi.WorkerGroup[*dockerv1.DockerMachineTemplate]
	dockerWorkers = clusterapi.Workers[*dockerv1.DockerMachineTemplate]
)

func TestWorkersUpdateImmutableObjectNamesError(t *testing.T) {
	g := NewWithT(t)
	ctx := context.Background()
	group1 := dockerGroup{
		MachineDeployment: machineDeployment(),
	}
	group2 := dockerGroup{
		MachineDeployment: machineDeployment(),
	}

	workers := &dockerWorkers{
		Groups: []dockerGroup{group1, group2},
	}
	client := test.NewFakeKubeClientAlwaysError()

	g.Expect(
		workers.UpdateImmutableObjectNames(ctx, client, dummyRetriever, noChangesCompare),
	).NotTo(Succeed())
}

func TestWorkersUpdateImmutableObjectNamesSuccess(t *testing.T) {
	g := NewWithT(t)
	ctx := context.Background()
	group1 := dockerGroup{
		MachineDeployment: machineDeployment(),
	}
	group2 := dockerGroup{
		MachineDeployment: machineDeployment(),
	}

	workers := &dockerWorkers{
		Groups: []dockerGroup{group1, group2},
	}
	client := test.NewFakeKubeClient()

	g.Expect(
		workers.UpdateImmutableObjectNames(ctx, client, dummyRetriever, noChangesCompare),
	).To(Succeed())
}

func TestWorkerObjects(t *testing.T) {
	g := NewWithT(t)
	group1 := dockerGroup{
		MachineDeployment: machineDeployment(),
	}
	group2 := dockerGroup{
		MachineDeployment: machineDeployment(),
	}

	workers := &dockerWorkers{
		Groups: []dockerGroup{group1, group2},
	}

	objects := workers.WorkerObjects()
	wantObjects := []kubernetes.Object{
		group1.KubeadmConfigTemplate,
		group1.MachineDeployment,
		group1.ProviderMachineTemplate,
		group2.KubeadmConfigTemplate,
		group2.MachineDeployment,
		group2.ProviderMachineTemplate,
	}

	g.Expect(objects).To(ConsistOf(wantObjects))
}

func TestWorkerGroupUpdateImmutableObjectNamesNoMachineDeployment(t *testing.T) {
	g := NewWithT(t)
	ctx := context.Background()
	group := &dockerGroup{
		MachineDeployment: machineDeployment(),
	}
	client := test.NewFakeKubeClient()

	g.Expect(group.UpdateImmutableObjectNames(ctx, client, dummyRetriever, noChangesCompare)).To(Succeed())
}

func TestWorkerGroupUpdateImmutableObjectNamesErrorReadingMachineDeployment(t *testing.T) {
	g := NewWithT(t)
	ctx := context.Background()
	group := &dockerGroup{
		MachineDeployment: machineDeployment(),
	}
	client := test.NewFakeKubeClientAlwaysError()

	g.Expect(
		group.UpdateImmutableObjectNames(ctx, client, dummyRetriever, noChangesCompare),
	).To(MatchError(ContainSubstring("reading current machine deployment from API")))
}

func TestWorkerGroupUpdateImmutableObjectNamesErrorUpdatingMachineTemplateName(t *testing.T) {
	g := NewWithT(t)
	ctx := context.Background()
	group := &dockerGroup{
		MachineDeployment:       machineDeployment(),
		ProviderMachineTemplate: dockerMachineTemplate(),
		KubeadmConfigTemplate:   kubeadmConfigTemplate(),
	}
	group.MachineDeployment.Spec.Template.Spec.InfrastructureRef = *objectReference(group.ProviderMachineTemplate)
	group.MachineDeployment.Spec.Template.Spec.Bootstrap.ConfigRef = objectReference(group.KubeadmConfigTemplate)
	client := test.NewFakeKubeClient(group.MachineDeployment)

	g.Expect(
		group.UpdateImmutableObjectNames(ctx, client, errorRetriever, noChangesCompare),
	).To(MatchError(ContainSubstring("reading DockerMachineTemplate eksa-system/mt-1 from API")))
}

func TestWorkerGroupUpdateImmutableObjectNamesErrorUpdatingKubeadmConfigTemplate(t *testing.T) {
	g := NewWithT(t)
	ctx := context.Background()
	group := &dockerGroup{
		MachineDeployment:       machineDeployment(),
		ProviderMachineTemplate: dockerMachineTemplate(),
		KubeadmConfigTemplate:   kubeadmConfigTemplate(),
	}
	group.MachineDeployment.Spec.Template.Spec.InfrastructureRef = *objectReference(group.ProviderMachineTemplate)
	group.KubeadmConfigTemplate.Name = "invalid-name"
	group.MachineDeployment.Spec.Template.Spec.Bootstrap.ConfigRef = objectReference(group.KubeadmConfigTemplate)
	client := test.NewFakeKubeClient(group.MachineDeployment, group.KubeadmConfigTemplate, group.ProviderMachineTemplate)
	group.KubeadmConfigTemplate.Spec.Template.Spec.PostKubeadmCommands = []string{"ls"}

	g.Expect(
		group.UpdateImmutableObjectNames(ctx, client, dummyRetriever, noChangesCompare),
	).To(MatchError(ContainSubstring("incrementing name for KubeadmConfigTemplate eksa-system/invalid-name")))
}

func TestWorkerGroupUpdateImmutableObjectNamesSuccess(t *testing.T) {
	g := NewWithT(t)
	ctx := context.Background()
	group := &dockerGroup{
		MachineDeployment:       machineDeployment(),
		ProviderMachineTemplate: dockerMachineTemplate(),
		KubeadmConfigTemplate:   kubeadmConfigTemplate(),
	}
	group.MachineDeployment.Spec.Template.Spec.InfrastructureRef = *objectReference(group.ProviderMachineTemplate)
	group.MachineDeployment.Spec.Template.Spec.Bootstrap.ConfigRef = objectReference(group.KubeadmConfigTemplate)
	client := test.NewFakeKubeClient(group.MachineDeployment, group.KubeadmConfigTemplate, group.ProviderMachineTemplate)
	group.KubeadmConfigTemplate.Spec.Template.Spec.PostKubeadmCommands = []string{"ls"}

	g.Expect(
		group.UpdateImmutableObjectNames(ctx, client, dummyRetriever, withChangesCompare),
	).To(Succeed())
	g.Expect(group.KubeadmConfigTemplate.Name).To(Equal("template-2"))
	g.Expect(group.ProviderMachineTemplate.Name).To(Equal("mt-2"))
	g.Expect(group.MachineDeployment.Spec.Template.Spec.Bootstrap.ConfigRef.Name).To(Equal(group.KubeadmConfigTemplate.Name))
	g.Expect(group.MachineDeployment.Spec.Template.Spec.InfrastructureRef.Name).To(Equal(group.ProviderMachineTemplate.Name))
}

func TestGetKubeadmConfigTemplateSuccess(t *testing.T) {
	g := NewWithT(t)
	ctx := context.Background()
	k := kubeadmConfigTemplate()
	client := test.NewFakeKubeClient(k)

	g.Expect(clusterapi.GetKubeadmConfigTemplate(ctx, client, k.Name, k.Namespace)).To(Equal(k))
}

func TestGetKubeadmConfigTemplateError(t *testing.T) {
	g := NewWithT(t)
	ctx := context.Background()
	k := kubeadmConfigTemplate()
	client := test.NewFakeKubeClientAlwaysError()

	_, err := clusterapi.GetKubeadmConfigTemplate(ctx, client, k.Name, k.Namespace)
	g.Expect(err).To(HaveOccurred())
}

func TestKubeadmConfigTemplateEqual(t *testing.T) {
	tests := []struct {
		name     string
		new, old *kubeadmv1.KubeadmConfigTemplate
		want     bool
	}{
		{
			name: "equal",
			new: &kubeadmv1.KubeadmConfigTemplate{
				Spec: kubeadmv1.KubeadmConfigTemplateSpec{
					Template: kubeadmv1.KubeadmConfigTemplateResource{
						Spec: kubeadmv1.KubeadmConfigSpec{
							JoinConfiguration: &kubeadmv1.JoinConfiguration{
								NodeRegistration: kubeadmv1.NodeRegistrationOptions{
									Taints: []corev1.Taint{
										{
											Key: "key",
										},
									},
								},
							},
							Files: []kubeadmv1.File{
								{
									Owner: "me",
								},
							},
						},
					},
				},
			},
			old: &kubeadmv1.KubeadmConfigTemplate{
				Spec: kubeadmv1.KubeadmConfigTemplateSpec{
					Template: kubeadmv1.KubeadmConfigTemplateResource{
						Spec: kubeadmv1.KubeadmConfigSpec{
							JoinConfiguration: &kubeadmv1.JoinConfiguration{
								NodeRegistration: kubeadmv1.NodeRegistrationOptions{
									Taints: []corev1.Taint{
										{
											Key: "key",
										},
									},
								},
							},
							Files: []kubeadmv1.File{
								{
									Owner: "me",
								},
							},
						},
					},
				},
			},
			want: true,
		},
		{
			name: "diff taints",
			new: &kubeadmv1.KubeadmConfigTemplate{
				Spec: kubeadmv1.KubeadmConfigTemplateSpec{
					Template: kubeadmv1.KubeadmConfigTemplateResource{
						Spec: kubeadmv1.KubeadmConfigSpec{
							JoinConfiguration: &kubeadmv1.JoinConfiguration{
								NodeRegistration: kubeadmv1.NodeRegistrationOptions{
									Taints: []corev1.Taint{},
								},
							},
							Files: []kubeadmv1.File{
								{
									Owner: "me",
								},
							},
						},
					},
				},
			},
			old: &kubeadmv1.KubeadmConfigTemplate{
				Spec: kubeadmv1.KubeadmConfigTemplateSpec{
					Template: kubeadmv1.KubeadmConfigTemplateResource{
						Spec: kubeadmv1.KubeadmConfigSpec{
							JoinConfiguration: &kubeadmv1.JoinConfiguration{
								NodeRegistration: kubeadmv1.NodeRegistrationOptions{
									Taints: []corev1.Taint{
										{
											Key: "key",
										},
									},
								},
							},
							Files: []kubeadmv1.File{
								{
									Owner: "me",
								},
							},
						},
					},
				},
			},
			want: false,
		},
		{
			name: "diff labels",
			new: &kubeadmv1.KubeadmConfigTemplate{
				Spec: kubeadmv1.KubeadmConfigTemplateSpec{
					Template: kubeadmv1.KubeadmConfigTemplateResource{
						Spec: kubeadmv1.KubeadmConfigSpec{
							JoinConfiguration: &kubeadmv1.JoinConfiguration{
								NodeRegistration: kubeadmv1.NodeRegistrationOptions{
									KubeletExtraArgs: map[string]string{
										"tls-cipher-suites": "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
										"cgroup-driver":     "cgroupfs",
										"eviction-hard":     "nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0%",
									},
								},
							},
							Files: []kubeadmv1.File{
								{
									Owner: "me",
								},
							},
						},
					},
				},
			},
			old: &kubeadmv1.KubeadmConfigTemplate{
				Spec: kubeadmv1.KubeadmConfigTemplateSpec{
					Template: kubeadmv1.KubeadmConfigTemplateResource{
						Spec: kubeadmv1.KubeadmConfigSpec{
							JoinConfiguration: &kubeadmv1.JoinConfiguration{
								NodeRegistration: kubeadmv1.NodeRegistrationOptions{
									KubeletExtraArgs: map[string]string{
										"tls-cipher-suites": "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
										"cgroup-driver":     "cgroupfs",
										"eviction-hard":     "nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0%",
										"node-labels":       "foo-bar",
									},
								},
							},
							Files: []kubeadmv1.File{
								{
									Owner: "me",
								},
							},
						},
					},
				},
			},
			want: false,
		},
		{
			name: "new JoinConfiguration nil",
			new: &kubeadmv1.KubeadmConfigTemplate{
				Spec: kubeadmv1.KubeadmConfigTemplateSpec{
					Template: kubeadmv1.KubeadmConfigTemplateResource{
						Spec: kubeadmv1.KubeadmConfigSpec{
							Files: []kubeadmv1.File{
								{
									Owner: "me",
								},
							},
						},
					},
				},
			},
			old: &kubeadmv1.KubeadmConfigTemplate{
				Spec: kubeadmv1.KubeadmConfigTemplateSpec{
					Template: kubeadmv1.KubeadmConfigTemplateResource{
						Spec: kubeadmv1.KubeadmConfigSpec{
							JoinConfiguration: &kubeadmv1.JoinConfiguration{
								NodeRegistration: kubeadmv1.NodeRegistrationOptions{
									Taints: []corev1.Taint{
										{
											Key: "key",
										},
									},
								},
							},
							Files: []kubeadmv1.File{
								{
									Owner: "me",
								},
							},
						},
					},
				},
			},
			want: true,
		},
		{
			name: "old JoinConfiguration nil",
			new: &kubeadmv1.KubeadmConfigTemplate{
				Spec: kubeadmv1.KubeadmConfigTemplateSpec{
					Template: kubeadmv1.KubeadmConfigTemplateResource{
						Spec: kubeadmv1.KubeadmConfigSpec{
							JoinConfiguration: &kubeadmv1.JoinConfiguration{
								NodeRegistration: kubeadmv1.NodeRegistrationOptions{
									Taints: []corev1.Taint{
										{
											Key: "key",
										},
									},
								},
							},
							Files: []kubeadmv1.File{
								{
									Owner: "me",
								},
							},
						},
					},
				},
			},
			old: &kubeadmv1.KubeadmConfigTemplate{
				Spec: kubeadmv1.KubeadmConfigTemplateSpec{
					Template: kubeadmv1.KubeadmConfigTemplateResource{
						Spec: kubeadmv1.KubeadmConfigSpec{
							Files: []kubeadmv1.File{
								{
									Owner: "me",
								},
							},
						},
					},
				},
			},
			want: false,
		},
		{
			name: "diff spec",
			new: &kubeadmv1.KubeadmConfigTemplate{
				Spec: kubeadmv1.KubeadmConfigTemplateSpec{
					Template: kubeadmv1.KubeadmConfigTemplateResource{
						Spec: kubeadmv1.KubeadmConfigSpec{
							JoinConfiguration: &kubeadmv1.JoinConfiguration{
								NodeRegistration: kubeadmv1.NodeRegistrationOptions{
									Taints: []corev1.Taint{
										{
											Key: "key",
										},
									},
								},
							},
							Files: []kubeadmv1.File{
								{
									Owner: "me",
								},
							},
						},
					},
				},
			},
			old: &kubeadmv1.KubeadmConfigTemplate{
				Spec: kubeadmv1.KubeadmConfigTemplateSpec{
					Template: kubeadmv1.KubeadmConfigTemplateResource{
						Spec: kubeadmv1.KubeadmConfigSpec{
							JoinConfiguration: &kubeadmv1.JoinConfiguration{
								NodeRegistration: kubeadmv1.NodeRegistrationOptions{
									Taints: []corev1.Taint{
										{
											Key: "key",
										},
									},
								},
							},
							Files: []kubeadmv1.File{
								{
									Owner: "you",
								},
							},
						},
					},
				},
			},
			want: false,
		},
		{
			name: "diff spec files",
			new: &kubeadmv1.KubeadmConfigTemplate{
				Spec: kubeadmv1.KubeadmConfigTemplateSpec{
					Template: kubeadmv1.KubeadmConfigTemplateResource{
						Spec: kubeadmv1.KubeadmConfigSpec{
							JoinConfiguration: &kubeadmv1.JoinConfiguration{
								NodeRegistration: kubeadmv1.NodeRegistrationOptions{
									Taints: []corev1.Taint{
										{
											Key: "key",
										},
									},
								},
							},
							Files: []kubeadmv1.File{
								{
									Owner: "me",
								},
							},
						},
					},
				},
			},
			old: &kubeadmv1.KubeadmConfigTemplate{
				Spec: kubeadmv1.KubeadmConfigTemplateSpec{
					Template: kubeadmv1.KubeadmConfigTemplateResource{
						Spec: kubeadmv1.KubeadmConfigSpec{
							JoinConfiguration: &kubeadmv1.JoinConfiguration{
								NodeRegistration: kubeadmv1.NodeRegistrationOptions{
									Taints: []corev1.Taint{
										{
											Key: "key",
										},
									},
								},
							},
						},
					},
				},
			},
			want: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			g := NewWithT(t)
			g.Expect(clusterapi.KubeadmConfigTemplateEqual(tt.new, tt.old)).To(Equal(tt.want))
		})
	}
}

func TestWorkerGroupDeepCopy(t *testing.T) {
	g := NewWithT(t)
	group := &dockerGroup{
		MachineDeployment:       machineDeployment(),
		KubeadmConfigTemplate:   kubeadmConfigTemplate(),
		ProviderMachineTemplate: dockerMachineTemplate(),
	}

	g.Expect(group.DeepCopy()).To(Equal(group))
}

func kubeadmConfigTemplate() *kubeadmv1.KubeadmConfigTemplate {
	return &kubeadmv1.KubeadmConfigTemplate{
		TypeMeta: metav1.TypeMeta{
			Kind:       "KubeadmConfigTemplate",
			APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name:      "template-1",
			Namespace: constants.EksaSystemNamespace,
		},
		Spec: kubeadmv1.KubeadmConfigTemplateSpec{
			Template: kubeadmv1.KubeadmConfigTemplateResource{
				Spec: kubeadmv1.KubeadmConfigSpec{
					Files: []kubeadmv1.File{
						{
							Owner: "me",
						},
					},
				},
			},
		},
	}
}

func machineDeployment() *clusterv1.MachineDeployment {
	return &clusterv1.MachineDeployment{
		TypeMeta: metav1.TypeMeta{
			Kind:       "MachineDeployment",
			APIVersion: "cluster.x-k8s.io/v1beta1",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name:      "deployment",
			Namespace: constants.EksaSystemNamespace,
		},
	}
}

func objectReference(obj client.Object) *corev1.ObjectReference {
	return &corev1.ObjectReference{
		Kind:       obj.GetObjectKind().GroupVersionKind().Kind,
		APIVersion: obj.GetObjectKind().GroupVersionKind().Version,
		Name:       obj.GetName(),
		Namespace:  obj.GetNamespace(),
	}
}
