package aks

import (
	"context"
	"fmt"
	"testing"

	v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/stretchr/testify/assert"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/pkg/util"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers"
	"github.com/tidbcloud/infra-provider/regional-server/test/kubemock"
	"github.com/upbound/provider-azure/apis/network/v1beta1"
	"go.uber.org/mock/gomock"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	labelsutil "k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

func fakeNATGateway(t *testing.T) *NATGateway {
	return NewNATGateway(fakeTaskContext(t))
}

func Test_getNATGateway(t *testing.T) {
	natTask := fakeNATGateway(t)
	assert.Equal(t, getNATGatewayName(natTask.Internet), namer.UniqueIdentifier(natTask.Internet.ObjectMeta, namer.WithPrefix("nat")))
}

func TestNATGateway_newNATGateway(t *testing.T) {
	natTask := fakeNATGateway(t)
	tags := cloudv1.TagsFromLabels(natTask.Internet.Labels)
	labels := map[string]string{
		v1alpha1.InternetNameKey: natTask.Internet.Name,
	}
	natRes := natTask.newNATGateway(labels, tags)
	assert.Equal(t, getNATGatewayName(natTask.Internet), natRes.Name)
	assert.Equal(t, labels, natRes.Labels)
	assert.Equal(t, v1beta1.NATGatewaySpec{
		ResourceSpec: xpv1.ResourceSpec{
			ProviderConfigReference: &xpv1.Reference{
				Name: natTask.ProviderConfigName(),
			},
		},
		ForProvider: v1beta1.NATGatewayParameters{
			ResourceGroupName: ptr.To(controllers.GetResourceGroupName(natTask.VPC)),
			Location:          ptr.To(natTask.Internet.Spec.Region),
			SkuName:           ptr.To("Standard"),
			Tags:              util.ToMapPointer(tags),
		},
	}, natRes.Spec)
}

func TestNATGateway_newPublicIPAssociation(t *testing.T) {
	natTask := fakeNATGateway(t)
	name := "name"
	ipAddressName := "ipAddressName"
	labels := map[string]string{"key": "value"}
	nat := &v1beta1.NATGateway{
		ObjectMeta: metav1.ObjectMeta{
			Name: "nat",
		},
	}
	association := natTask.newPublicIPAssociation(name, ipAddressName, labels, nat)
	assert.Equal(t, name, association.Name)
	assert.Equal(t, labels, association.Labels)
	assert.Equal(t, v1beta1.NATGatewayPublicIPAssociationSpec{
		ResourceSpec: xpv1.ResourceSpec{
			ProviderConfigReference: &xpv1.Reference{
				Name: natTask.ProviderConfigName(),
			},
		},
		ForProvider: v1beta1.NATGatewayPublicIPAssociationParameters{
			PublicIPAddressIDRef: &xpv1.Reference{
				Name: ipAddressName,
			},
			NATGatewayIDRef: &xpv1.Reference{
				Name: nat.Name,
			},
		},
	}, association.Spec)
}

func TestNATGateway_newSubnetAssociation(t *testing.T) {
	natTask := fakeNATGateway(t)
	name := "name"
	subnetName := "subnetName"
	labels := map[string]string{"key": "value"}
	nat := &v1beta1.NATGateway{
		ObjectMeta: metav1.ObjectMeta{
			Name: "nat",
		},
	}
	association := natTask.newSubnetAssociation(name, subnetName, labels, nat)
	assert.Equal(t, name, association.Name)
	assert.Equal(t, labels, association.Labels)
	assert.Equal(t, v1beta1.SubnetNATGatewayAssociationSpec{
		ResourceSpec: xpv1.ResourceSpec{
			ProviderConfigReference: &xpv1.Reference{
				Name: natTask.ProviderConfigName(),
			},
		},
		ForProvider: v1beta1.SubnetNATGatewayAssociationParameters{
			SubnetIDRef: &xpv1.Reference{
				Name: subnetName,
			},
			NATGatewayIDRef: &xpv1.Reference{
				Name: nat.Name,
			},
		},
	}, association.Spec)
}

func TestNATGateway_createPublicIPAssociations(t *testing.T) {
	natTask := fakeNATGateway(t)
	labels := map[string]string{
		v1alpha1.InternetNameKey: natTask.Internet.Name,
	}
	t.Run("publicip not found", func(t *testing.T) {
		err := natTask.createPublicIPAssociations(context.Background(), labels, &v1beta1.NATGateway{})
		assert.ErrorContains(t, err, "not found")
	})
	t.Run("create success", func(t *testing.T) {
		for idx := 0; idx < defaultAddressCount; idx++ {
			ip := &v1beta1.PublicIP{
				ObjectMeta: metav1.ObjectMeta{
					Name: fmt.Sprintf("ip-%d", idx),
					Labels: map[string]string{
						v1alpha1.InternetNameKey: natTask.Internet.Name,
					},
				},
			}
			assert.Nil(t, natTask.Local.Create(context.Background(), ip))
		}
		natRes := &v1beta1.NATGateway{
			ObjectMeta: metav1.ObjectMeta{
				Name: "nat",
			},
		}
		assert.Nil(t, natTask.createPublicIPAssociations(context.Background(), labels, natRes))
		for idx := 0; idx < defaultAddressCount; idx++ {
			association := &v1beta1.NATGatewayPublicIPAssociation{}
			assert.Nil(t, natTask.Local.Get(context.Background(), types.NamespacedName{
				Name: fmt.Sprintf("ip-%d", idx),
			}, association))
			assert.Equal(t, fmt.Sprintf("ip-%d", idx), association.Spec.ForProvider.PublicIPAddressIDRef.Name)
			assert.Equal(t, "nat", association.Spec.ForProvider.NATGatewayIDRef.Name)
			assert.Equal(t, labels, association.Labels)
		}
	})
	t.Run("list ip failed", func(t *testing.T) {
		ctl := gomock.NewController(t)
		mockcli := kubemock.NewMockClient(ctl)
		natTask.Local = mockcli
		mockcli.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).Return(assert.AnError).Times(1)
		err := natTask.createPublicIPAssociations(context.Background(), labels, &v1beta1.NATGateway{})
		assert.NotNil(t, err)
	})
	t.Run("create association failed", func(t *testing.T) {
		ctl := gomock.NewController(t)
		mockcli := kubemock.NewMockClient(ctl)
		natTask.Local = mockcli
		mockcli.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, list *v1beta1.PublicIPList, opts ...*client.ListOptions) error {
			list.Items = []v1beta1.PublicIP{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name:   "ip",
						Labels: labels,
					},
				},
			}
			return nil
		}).Times(1)
		mockcli.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(apierrors.NewNotFound(schema.GroupResource{}, "not found")).Times(1)
		mockcli.EXPECT().Create(gomock.Any(), gomock.Any()).Return(assert.AnError).Times(1)
		err := natTask.createPublicIPAssociations(context.Background(), labels, &v1beta1.NATGateway{})
		assert.NotNil(t, err)
	})
}

func TestNATGateway_createSubnetAssociations(t *testing.T) {
	natTask := fakeNATGateway(t)
	labels := map[string]string{
		v1alpha1.InternetNameKey: natTask.Internet.Name,
	}
	t.Run("subnet not found", func(t *testing.T) {
		err := natTask.createSubnetAssociations(context.Background(), labels, &v1beta1.NATGateway{})
		assert.ErrorContains(t, err, "not found")
	})
	t.Run("create success", func(t *testing.T) {
		subnet := &v1beta1.Subnet{
			ObjectMeta: metav1.ObjectMeta{
				Name: "subnet-node",
				Labels: map[string]string{
					v1alpha1.VPCNameKey:    natTask.VPC.Name,
					v1alpha1.SubnetTypeKey: v1alpha1.NodeSubnetType,
				},
			},
		}
		assert.Nil(t, natTask.Local.Create(context.Background(), subnet))
		natRes := &v1beta1.NATGateway{
			ObjectMeta: metav1.ObjectMeta{
				Name: "nat",
			},
		}
		podSubnet := &v1beta1.Subnet{
			ObjectMeta: metav1.ObjectMeta{
				Name: "subnet-pod",
				Labels: map[string]string{
					v1alpha1.VPCNameKey:    natTask.VPC.Name,
					v1alpha1.SubnetTypeKey: v1alpha1.PodSubnetType,
				},
			},
		}
		assert.Nil(t, natTask.Local.Create(context.Background(), podSubnet))
		assert.Nil(t, natTask.createSubnetAssociations(context.Background(), labels, natRes))
		association := &v1beta1.SubnetNATGatewayAssociation{}
		assert.Nil(t, natTask.Local.Get(context.Background(), types.NamespacedName{
			Name: subnet.Name,
		}, association))
		assert.Equal(t, subnet.Name, association.Spec.ForProvider.SubnetIDRef.Name)
		assert.Equal(t, "nat", association.Spec.ForProvider.NATGatewayIDRef.Name)
		assert.Equal(t, labels, association.Labels)
	})
	t.Run("list subnet failed", func(t *testing.T) {
		ctl := gomock.NewController(t)
		mockcli := kubemock.NewMockClient(ctl)
		natTask.Local = mockcli
		mockcli.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).Return(assert.AnError).Times(1)
		err := natTask.createSubnetAssociations(context.Background(), labels, &v1beta1.NATGateway{})
		assert.NotNil(t, err)
	})
	t.Run("subnet not found", func(t *testing.T) {
		ctl := gomock.NewController(t)
		mockcli := kubemock.NewMockClient(ctl)
		natTask.Local = mockcli
		mockcli.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, list *v1beta1.SubnetList, opts ...*client.ListOptions) error {
			list.Items = []v1beta1.Subnet{}
			return nil
		}).Times(1)
		err := natTask.createSubnetAssociations(context.Background(), labels, &v1beta1.NATGateway{})
		assert.ErrorContains(t, err, "not found")
	})
}

func TestNATGatewy_Create(t *testing.T) {
	natTask := fakeNATGateway(t)
	t.Run("create success", func(t *testing.T) {
		for idx := 0; idx < defaultAddressCount; idx++ {
			ip := &v1beta1.PublicIP{
				ObjectMeta: metav1.ObjectMeta{
					Name: fmt.Sprintf("ip-%d", idx),
					Labels: map[string]string{
						v1alpha1.InternetNameKey: natTask.Internet.Name,
					},
				},
			}
			assert.Nil(t, natTask.Local.Create(context.Background(), ip))
		}
		subnet := &v1beta1.Subnet{
			ObjectMeta: metav1.ObjectMeta{
				Name: "subnet-node",
				Labels: map[string]string{
					v1alpha1.VPCNameKey:    natTask.VPC.Name,
					v1alpha1.SubnetTypeKey: v1alpha1.NodeSubnetType,
				},
			},
		}
		assert.Nil(t, natTask.Local.Create(context.Background(), subnet))
		podSubnet := &v1beta1.Subnet{
			ObjectMeta: metav1.ObjectMeta{
				Name: "subnet-pod",
				Labels: map[string]string{
					v1alpha1.VPCNameKey:    natTask.VPC.Name,
					v1alpha1.SubnetTypeKey: v1alpha1.PodSubnetType,
				},
			},
		}
		assert.Nil(t, natTask.Local.Create(context.Background(), podSubnet))
		result := natTask.Create(context.Background())
		assert.True(t, result.IsDoneState())
		nat := &v1beta1.NATGateway{}
		assert.Nil(t, natTask.Local.Get(context.Background(), types.NamespacedName{Name: getNATGatewayName(natTask.Internet)}, nat))

		ipAssociations := &v1beta1.NATGatewayPublicIPAssociationList{}
		assert.Nil(t, natTask.Local.List(context.Background(), ipAssociations, &client.ListOptions{
			LabelSelector: labelsutil.SelectorFromSet(map[string]string{
				v1alpha1.InternetNameKey: natTask.Internet.Name,
			}),
		}))
		assert.Equal(t, defaultAddressCount, len(ipAssociations.Items))
		subnetAssociation := &v1beta1.SubnetNATGatewayAssociation{}
		assert.Nil(t, natTask.Local.Get(context.Background(), types.NamespacedName{Name: subnet.Name}, subnetAssociation))
	})
}

func TestNATGateway_checkNATGatewayStatus(t *testing.T) {
	t.Run("nat is ready", func(t *testing.T) {
		natTask := fakeNATGateway(t)
		nat := &v1beta1.NATGateway{
			ObjectMeta: metav1.ObjectMeta{
				Name: getNATGatewayName(natTask.Internet),
			},
			Status: v1beta1.NATGatewayStatus{
				ResourceStatus: v1.ResourceStatus{
					ConditionedStatus: xpv1.ConditionedStatus{
						Conditions: []xpv1.Condition{
							{
								Type:   xpv1.TypeReady,
								Status: corev1.ConditionTrue,
							},
						},
					},
				},
				AtProvider: v1beta1.NATGatewayObservation{
					ID: ptr.To("nat-id"),
				},
			},
		}
		assert.Nil(t, natTask.Local.Create(context.Background(), nat))
		assert.Nil(t, natTask.checkNATGatewayStatus(context.Background()))
		internet := &v1alpha1.Internet{}
		assert.Nil(t, natTask.Local.Get(context.Background(), client.ObjectKeyFromObject(natTask.Internet), internet))
		assert.Equal(t, 1, len(internet.Status.NATs))
		assert.Equal(t, v1alpha1.NAT{
			Name:         nat.Name,
			NATGatewayID: "nat-id",
		}, internet.Status.NATs[0])
	})
	t.Run("nat is not ready", func(t *testing.T) {
		natTask := fakeNATGateway(t)
		nat := &v1beta1.NATGateway{
			ObjectMeta: metav1.ObjectMeta{
				Name: getNATGatewayName(natTask.Internet),
			},
		}
		assert.Nil(t, natTask.Local.Create(context.Background(), nat))
		err := natTask.checkNATGatewayStatus(context.Background())
		assert.ErrorContains(t, err, fmt.Sprintf("NATGateway %s is not ready", nat.Name))
	})
}

func TestNATGateway_checkPublicIPAssociationStatus(t *testing.T) {
	natTask := fakeNATGateway(t)
	t.Run("publicIP association is not found", func(t *testing.T) {
		err := natTask.checkPublicIPAssociationStatus(context.Background())
		assert.ErrorContains(t, err, "not found")
	})
	t.Run("publicIP association is ready", func(t *testing.T) {
		for idx := 0; idx < defaultAddressCount; idx++ {
			ip := &v1beta1.PublicIP{
				ObjectMeta: metav1.ObjectMeta{
					Name: fmt.Sprintf("ip-%d", idx),
					Labels: map[string]string{
						v1alpha1.InternetNameKey: natTask.Internet.Name,
					},
				},
				Status: v1beta1.PublicIPStatus{
					ResourceStatus: v1.ResourceStatus{
						ConditionedStatus: xpv1.ConditionedStatus{
							Conditions: []xpv1.Condition{
								{
									Type:   xpv1.TypeReady,
									Status: corev1.ConditionTrue,
								},
							},
						},
					},
					AtProvider: v1beta1.PublicIPObservation{
						ID:        ptr.To(fmt.Sprintf("ip-%d-id", idx)),
						IPAddress: ptr.To(fmt.Sprintf("192.168.1.%d", idx)),
					},
				},
			}
			assert.Nil(t, natTask.Local.Create(context.Background(), ip))
			association := &v1beta1.NATGatewayPublicIPAssociation{
				ObjectMeta: metav1.ObjectMeta{
					Name: fmt.Sprintf("association-%d", idx),
					Labels: map[string]string{
						v1alpha1.InternetNameKey: natTask.Internet.Name,
					},
				},
				Spec: v1beta1.NATGatewayPublicIPAssociationSpec{
					ForProvider: v1beta1.NATGatewayPublicIPAssociationParameters{
						PublicIPAddressIDRef: &xpv1.Reference{
							Name: ip.Name,
						},
					},
				},
				Status: v1beta1.NATGatewayPublicIPAssociationStatus{
					ResourceStatus: v1.ResourceStatus{
						ConditionedStatus: xpv1.ConditionedStatus{
							Conditions: []xpv1.Condition{
								{
									Type:   xpv1.TypeReady,
									Status: corev1.ConditionTrue,
								},
							},
						},
					},
					AtProvider: v1beta1.NATGatewayPublicIPAssociationObservation{
						ID: ptr.To(fmt.Sprintf("association-%d-id", idx)),
					},
				},
			}
			assert.Nil(t, natTask.Local.Create(context.Background(), association))
		}
		assert.Nil(t, natTask.checkPublicIPAssociationStatus(context.Background()))
		internet := &v1alpha1.Internet{}
		assert.Nil(t, natTask.Local.Get(context.Background(), client.ObjectKeyFromObject(natTask.Internet), internet))
		assert.Equal(t, defaultAddressCount, len(internet.Status.IPAssociations))
		assert.Equal(t, defaultAddressCount, len(internet.Status.ExternalIPs))
		for idx := 0; idx < defaultAddressCount; idx++ {
			assert.Equal(t, v1alpha1.IPAssociation{
				Name:            fmt.Sprintf("association-%d", idx),
				IPAssociationID: fmt.Sprintf("association-%d-id", idx),
			}, internet.Status.IPAssociations[idx])
			assert.Equal(t, fmt.Sprintf("192.168.1.%d", idx), internet.Status.ExternalIPs[idx])
		}
	})
	t.Run("publicIP association is not ready", func(t *testing.T) {
		association := &v1beta1.NATGatewayPublicIPAssociation{
			ObjectMeta: metav1.ObjectMeta{
				Name: "association",
				Labels: map[string]string{
					v1alpha1.InternetNameKey: natTask.Internet.Name,
				},
			},
		}
		assert.Nil(t, natTask.Local.Create(context.Background(), association))
		err := natTask.checkPublicIPAssociationStatus(context.Background())
		assert.ErrorContains(t, err, fmt.Sprintf("PublicIPAssociation %s is not ready", association.Name))
	})
}

func TestNATGateway_checkSubnetAssociationStatus(t *testing.T) {
	natTask := fakeNATGateway(t)
	t.Run("subnet association is not found", func(t *testing.T) {
		err := natTask.checkSubnetAssociationStatus(context.Background())
		assert.ErrorContains(t, err, "not found")
	})
	t.Run("subnet association is ready", func(t *testing.T) {
		association := &v1beta1.SubnetNATGatewayAssociation{
			ObjectMeta: metav1.ObjectMeta{
				Name: "association",
				Labels: map[string]string{
					v1alpha1.InternetNameKey: natTask.Internet.Name,
				},
			},
			Spec: v1beta1.SubnetNATGatewayAssociationSpec{
				ForProvider: v1beta1.SubnetNATGatewayAssociationParameters{
					SubnetIDRef: &xpv1.Reference{
						Name: "subnet",
					},
				},
			},
			Status: v1beta1.SubnetNATGatewayAssociationStatus{
				ResourceStatus: v1.ResourceStatus{
					ConditionedStatus: xpv1.ConditionedStatus{
						Conditions: []xpv1.Condition{
							{
								Type:   xpv1.TypeReady,
								Status: corev1.ConditionTrue,
							},
						},
					},
				},
				AtProvider: v1beta1.SubnetNATGatewayAssociationObservation{
					ID: ptr.To("association-id"),
				},
			},
		}
		assert.Nil(t, natTask.Local.Create(context.Background(), association))
		assert.Nil(t, natTask.checkSubnetAssociationStatus(context.Background()))
		internet := &v1alpha1.Internet{}
		assert.Nil(t, natTask.Local.Get(context.Background(), client.ObjectKeyFromObject(natTask.Internet), internet))
		assert.Equal(t, 1, len(internet.Status.SubnetAssociations))
		assert.Equal(t, v1alpha1.SubnetAssociation{
			Name: association.Name,
		}, internet.Status.SubnetAssociations[0])
	})
	t.Run("subnet association is not ready", func(t *testing.T) {
		association := &v1beta1.SubnetNATGatewayAssociation{
			ObjectMeta: metav1.ObjectMeta{
				Name: "association-2",
				Labels: map[string]string{
					v1alpha1.InternetNameKey: natTask.Internet.Name,
				},
			},
		}
		assert.Nil(t, natTask.Local.Create(context.Background(), association))
		err := natTask.checkSubnetAssociationStatus(context.Background())
		assert.ErrorContains(t, err, fmt.Sprintf("SubnetAssociation %s is not ready", association.Name))
	})
}

func TestNATGateway_Delete(t *testing.T) {
	natTask := fakeNATGateway(t)
	for idx := 0; idx < defaultAddressCount; idx++ {
		ip := &v1beta1.PublicIP{
			ObjectMeta: metav1.ObjectMeta{
				Name: fmt.Sprintf("ip-%d", idx),
				Labels: map[string]string{
					v1alpha1.InternetNameKey: natTask.Internet.Name,
				},
			},
		}
		assert.Nil(t, natTask.Local.Create(context.Background(), ip))
	}
	subnet := &v1beta1.Subnet{
		ObjectMeta: metav1.ObjectMeta{
			Name: "subnet-node",
			Labels: map[string]string{
				v1alpha1.VPCNameKey:    natTask.VPC.Name,
				v1alpha1.SubnetTypeKey: v1alpha1.NodeSubnetType,
			},
		},
	}
	assert.Nil(t, natTask.Local.Create(context.Background(), subnet))
	podSubnet := &v1beta1.Subnet{
		ObjectMeta: metav1.ObjectMeta{
			Name: "subnet-pod",
			Labels: map[string]string{
				v1alpha1.VPCNameKey:    natTask.VPC.Name,
				v1alpha1.SubnetTypeKey: v1alpha1.PodSubnetType,
			},
		},
	}
	assert.Nil(t, natTask.Local.Create(context.Background(), podSubnet))
	result := natTask.Create(context.Background())
	assert.True(t, result.IsDoneState())

	result = natTask.Delete(context.Background())
	assert.True(t, result.IsDoneState())
	subnetAssociations := &v1beta1.SubnetNATGatewayAssociationList{}
	assert.Nil(t, natTask.Local.List(context.Background(), subnetAssociations, &client.ListOptions{
		LabelSelector: labelsutil.SelectorFromSet(map[string]string{
			v1alpha1.InternetNameKey: natTask.Internet.Name,
		}),
	}))
	assert.Equal(t, 0, len(subnetAssociations.Items))
	ipAssociations := &v1beta1.NATGatewayPublicIPAssociationList{}
	assert.Nil(t, natTask.Local.List(context.Background(), ipAssociations, &client.ListOptions{
		LabelSelector: labelsutil.SelectorFromSet(map[string]string{
			v1alpha1.InternetNameKey: natTask.Internet.Name,
		}),
	}))
	assert.Equal(t, 0, len(ipAssociations.Items))
	nat := &v1beta1.NATGateway{}
	assert.ErrorContains(t, natTask.Local.Get(context.Background(), types.NamespacedName{Name: getNATGatewayName(natTask.Internet)}, nat), "not found")

	// delete again
	result = natTask.Delete(context.Background())
	assert.True(t, result.IsDoneState())
}
