package eks

import (
	"context"
	"fmt"
	"testing"
	"time"

	ec2v1alpha1 "github.com/crossplane-contrib/provider-aws/apis/ec2/v1alpha1"
	"github.com/crossplane-contrib/provider-aws/apis/ec2/v1beta1"
	ec2v1beta1 "github.com/crossplane-contrib/provider-aws/apis/ec2/v1beta1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/crossplane/crossplane-runtime/pkg/meta"
	"github.com/stretchr/testify/assert"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/pointer"

	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/pkg/util"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
)

func fakeRouteTask(t *testing.T) *Route {
	return NewRoute(fakeTaskContext(t))
}

func TestRoute_createRoute(t *testing.T) {
	t.Run("create success", func(t *testing.T) {
		task := fakeRouteTask(t)
		name := "test-name"
		parameters := ec2v1alpha1.CustomRouteParameters{
			GatewayIDRef:    &xpv1.Reference{Name: "gateway-id"},
			RouteTableIDRef: &xpv1.Reference{Name: "route-table-id"},
		}
		route, err := task.createRoute(context.Background(), name, parameters)
		assert.Nil(t, err)
		assert.Equal(t, name, route.Name)
		assert.Equal(t, ec2v1alpha1.RouteSpec{
			ResourceSpec: xpv1.ResourceSpec{ProviderConfigReference: &xpv1.Reference{Name: task.ProviderConfigName()}},
			ForProvider: ec2v1alpha1.RouteParameters{
				Region:                task.Internet.Spec.Region,
				DestinationCIDRBlock:  pointer.String("0.0.0.0/0"),
				CustomRouteParameters: parameters,
			},
		}, route.Spec)
	})
}

func TestRoute_createRouteTable(t *testing.T) {
	t.Run("create success", func(t *testing.T) {
		task := fakeRouteTask(t)
		name := "test-name"
		tags := map[string]string{"key": "value"}
		subnets := []*v1beta1.Subnet{
			{ObjectMeta: metav1.ObjectMeta{
				Name: "subnet-1",
			}},
			{ObjectMeta: metav1.ObjectMeta{
				Name: "subnet-2",
			}},
		}
		rt, err := task.createRouteTable(context.Background(), name, tags, subnets, &v1beta1.VPC{
			ObjectMeta: metav1.ObjectMeta{
				Name: "test-vpc",
			}})
		assert.Nil(t, err)
		assert.Equal(t, name, rt.Name)
		assert.Equal(t, v1beta1.RouteTableSpec{
			ResourceSpec: xpv1.ResourceSpec{ProviderConfigReference: &xpv1.Reference{Name: task.ProviderConfigName()}},
			ForProvider: v1beta1.RouteTableParameters{
				Region: task.Internet.Spec.Region,
				Associations: []v1beta1.Association{
					{SubnetIDRef: &xpv1.Reference{Name: "subnet-1"}},
					{SubnetIDRef: &xpv1.Reference{Name: "subnet-2"}},
				},
				IgnoreRoutes: pointer.Bool(true),
				Tags:         util.GetEC2V1BetaTags(tags),
				VPCIDRef:     &xpv1.Reference{Name: "test-vpc"},
			},
		}, rt.Spec)
	})
}

func TestRoute_Create(t *testing.T) {
	t.Run("vpc not found", func(t *testing.T) {
		task := fakeRouteTask(t)
		task.VPC.Name = "not-found"
		result := task.Create(context.Background())
		res, err, retriableErr := deploy.MinRequeue([]deploy.Result{result})
		assert.Nil(t, err)
		assert.ErrorContains(t, retriableErr, "not found")
		assert.Equal(t, time.Second*30, res.RequeueAfter)
	})
	t.Run("aws vpc not found", func(t *testing.T) {
		task := fakeRouteTask(t)
		result := task.Create(context.Background())
		res, err, retriableErr := deploy.MinRequeue([]deploy.Result{result})
		assert.Nil(t, err)
		assert.ErrorContains(t, retriableErr, "not found")
		assert.Equal(t, time.Second*30, res.RequeueAfter)
	})

	task := fakeRouteTask(t)
	task.VPC.Status.VPC.Name = namer.UniqueIdentifier(task.VPC.ObjectMeta)
	vpcRes := &v1beta1.VPC{ObjectMeta: metav1.ObjectMeta{
		Name: namer.UniqueIdentifier(task.VPC.ObjectMeta),
	}}
	assert.Nil(t, task.Local.Create(context.Background(), vpcRes))
	zones := task.GetZones()
	name := namer.UniqueIdentifier(task.Internet.ObjectMeta)
	t.Run("node subnet not found", func(t *testing.T) {
		result := task.Create(context.Background())
		res, err, retriableErr := deploy.MinRequeue([]deploy.Result{result})
		assert.Nil(t, err)
		assert.ErrorContains(t, retriableErr, "subnet not found in zone")
		assert.Equal(t, time.Second*30, res.RequeueAfter)
	})
	t.Run("public subnet not found", func(t *testing.T) {
		for _, zone := range zones {
			task.VPC.Status.NodeSubnets = append(task.VPC.Status.NodeSubnets, v1alpha1.Subnet{
				Name:               namer.NodesSubnetName(name, zone),
				AvailabilityZone:   zone,
				AvailabilityZoneID: fmt.Sprintf("%s-id", zone),
			})
			assert.Nil(t, task.Local.Create(context.Background(), &ec2v1beta1.Subnet{
				ObjectMeta: metav1.ObjectMeta{
					Name: namer.NodesSubnetName(name, zone),
					Annotations: map[string]string{
						meta.AnnotationKeyExternalName: fmt.Sprintf("id-node-%s", zone),
					},
				},
			}))
		}
		assert.Nil(t, task.Local.Status().Update(context.Background(), task.VPC))
		result := task.Create(context.Background())
		res, err, retriableErr := deploy.MinRequeue([]deploy.Result{result})
		assert.Nil(t, err)
		assert.ErrorContains(t, retriableErr, "subnet not found in zone")
		assert.Equal(t, time.Second*30, res.RequeueAfter)
	})
	t.Run("public subnet not found", func(t *testing.T) {
		for _, zone := range zones {
			task.VPC.Status.PrivateSubnets = append(task.VPC.Status.PrivateSubnets, v1alpha1.Subnet{
				Name:               namer.PrivateSubnetName(name, zone),
				AvailabilityZone:   zone,
				AvailabilityZoneID: fmt.Sprintf("%s-id", zone),
			})
			assert.Nil(t, task.Local.Create(context.Background(), &ec2v1beta1.Subnet{
				ObjectMeta: metav1.ObjectMeta{
					Name: namer.PrivateSubnetName(name, zone),
					Annotations: map[string]string{
						meta.AnnotationKeyExternalName: fmt.Sprintf("id-private-%s", zone),
					},
				},
			}))
		}
		assert.Nil(t, task.Local.Status().Update(context.Background(), task.VPC))
		result := task.Create(context.Background())
		res, err, retriableErr := deploy.MinRequeue([]deploy.Result{result})
		assert.Nil(t, err)
		assert.ErrorContains(t, retriableErr, "subnet not found in zone")
		assert.Equal(t, time.Second*30, res.RequeueAfter)
	})
	t.Run("nat not found", func(t *testing.T) {
		for _, zone := range zones {
			task.VPC.Status.PublicSubnets = append(task.VPC.Status.PublicSubnets, v1alpha1.Subnet{
				Name:               namer.PublicSubnetName(name, zone),
				AvailabilityZone:   zone,
				AvailabilityZoneID: fmt.Sprintf("%s-id", zone),
			})
			assert.Nil(t, task.Local.Create(context.Background(), &ec2v1beta1.Subnet{
				ObjectMeta: metav1.ObjectMeta{
					Name: namer.PublicSubnetName(name, zone),
					Annotations: map[string]string{
						meta.AnnotationKeyExternalName: fmt.Sprintf("id-public-%s", zone),
					},
				},
			}))
		}
		assert.Nil(t, task.Local.Status().Update(context.Background(), task.VPC))
		result := task.Create(context.Background())
		res, err, retriableErr := deploy.MinRequeue([]deploy.Result{result})
		assert.Nil(t, err)
		assert.ErrorContains(t, retriableErr, "not found")
		assert.Equal(t, time.Second*30, res.RequeueAfter)
	})
	t.Run("nat gatway not found", func(t *testing.T) {
		for _, zone := range zones {
			task.Internet.Status.NATs = append(task.Internet.Status.NATs, v1alpha1.NAT{
				Name:         namer.PublicSubnetName(name, zone),
				NATGatewayID: fmt.Sprintf("gateway-id-%s", zone),
			})
		}
		assert.Nil(t, task.Local.Status().Update(context.Background(), task.VPC))
		result := task.Create(context.Background())
		res, err, retriableErr := deploy.MinRequeue([]deploy.Result{result})
		assert.Nil(t, err)
		assert.ErrorContains(t, retriableErr, "not found")
		assert.Equal(t, time.Second*30, res.RequeueAfter)
	})
	t.Run("create success", func(t *testing.T) {
		for _, zone := range zones {
			assert.Nil(t, task.Local.Create(context.Background(), &v1beta1.NATGateway{
				ObjectMeta: metav1.ObjectMeta{
					Name: namer.PublicSubnetName(name, zone),
				},
			}))
		}
		task.Internet.Status.InternetGateway = v1alpha1.InternetGateway{
			Name: "internet-gateway",
		}
		assert.Nil(t, task.Local.Status().Update(context.Background(), task.Internet))
		result := task.Create(context.Background())
		assert.True(t, result.IsDoneState())
		assert.Equal(t, len(zones)*2, len(task.routeTables))
		assert.Equal(t, len(zones)*2, len(task.routes))
		for idx, zone := range zones {
			assert.Equal(t, namer.PrivateSubnetName(name, zone), task.routeTables[idx*2].Name)
			assert.Equal(t, []v1beta1.Association{
				{SubnetIDRef: &xpv1.Reference{Name: namer.NodesSubnetName(name, zone)}},
				{SubnetIDRef: &xpv1.Reference{Name: namer.PrivateSubnetName(name, zone)}},
			}, task.routeTables[idx*2].Spec.ForProvider.Associations)
			assert.Equal(t, namer.PrivateSubnetName(name, zone), task.routes[idx*2].Name)
			assert.Equal(t, ec2v1alpha1.CustomRouteParameters{
				NATGatewayIDRef: &xpv1.Reference{Name: namer.PublicSubnetName(name, zone)},
				RouteTableIDRef: &xpv1.Reference{Name: namer.PrivateSubnetName(name, zone)},
			}, task.routes[idx*2].Spec.ForProvider.CustomRouteParameters)
			assert.Equal(t, namer.PublicSubnetName(name, zone), task.routeTables[idx*2+1].Name)
			assert.Equal(t, []v1beta1.Association{
				{SubnetIDRef: &xpv1.Reference{Name: namer.PublicSubnetName(name, zone)}},
			}, task.routeTables[idx*2+1].Spec.ForProvider.Associations)
			assert.Equal(t, namer.PublicSubnetName(name, zone), task.routes[idx*2+1].Name)
			assert.Equal(t, ec2v1alpha1.CustomRouteParameters{
				GatewayIDRef:    &xpv1.Reference{Name: task.Internet.Status.InternetGateway.Name},
				RouteTableIDRef: &xpv1.Reference{Name: namer.PublicSubnetName(name, zone)},
			}, task.routes[idx*2+1].Spec.ForProvider.CustomRouteParameters)
		}
	})
	t.Run("Delete success", func(t *testing.T) {
		result := task.Delete(context.Background())
		assert.True(t, result.IsDoneState())
		for _, zone := range zones {
			rt := &v1beta1.RouteTable{}
			assert.ErrorContains(t, task.Local.Get(context.Background(), types.NamespacedName{Name: namer.PrivateSubnetName(name, zone)}, rt), "not found")
			assert.ErrorContains(t, task.Local.Get(context.Background(), types.NamespacedName{Name: namer.PublicSubnetName(name, zone)}, rt), "not found")
			route := &ec2v1alpha1.Route{}
			assert.ErrorContains(t, task.Local.Get(context.Background(), types.NamespacedName{Name: namer.PrivateSubnetName(name, zone)}, route), "not found")
			assert.ErrorContains(t, task.Local.Get(context.Background(), types.NamespacedName{Name: namer.PublicSubnetName(name, zone)}, route), "not found")
		}
	})
	t.Run("internet is originated from gardener", func(t *testing.T) {
		task.Internet.Status.InternetGateway = v1alpha1.InternetGateway{
			Name: "internet-gateway",
		}
		assert.Nil(t, task.Local.Status().Update(context.Background(), task.Internet))
		task.Internet.SetAnnotations(map[string]string{v1alpha1.AnnotationKeyOriginatedFromGardener: "true"})
		assert.Nil(t, task.Local.Update(context.Background(), task.Internet))
		task.routeTables = nil
		task.routes = nil

		result := task.Create(context.Background())
		assert.True(t, result.IsDoneState())
		assert.Nil(t, result.Error())
		assert.Equal(t, len(zones)+1, len(task.routeTables))
		assert.Equal(t, len(zones)+1, len(task.routes))
		for idx, zone := range zones {
			if idx > 0 {
				idx = idx + 1
			}
			assert.Equal(t, namer.PrivateSubnetName(name, zone), task.routeTables[idx].Name)
			assert.Equal(t, []v1beta1.Association{
				{SubnetIDRef: &xpv1.Reference{Name: namer.NodesSubnetName(name, zone)}},
				{SubnetIDRef: &xpv1.Reference{Name: namer.PrivateSubnetName(name, zone)}},
			}, task.routeTables[idx].Spec.ForProvider.Associations)
			assert.Equal(t, namer.PrivateSubnetName(name, zone), task.routes[idx].Name)
			assert.Equal(t, ec2v1alpha1.CustomRouteParameters{
				NATGatewayIDRef: &xpv1.Reference{Name: namer.PublicSubnetName(name, zone)},
				RouteTableIDRef: &xpv1.Reference{Name: namer.PrivateSubnetName(name, zone)},
			}, task.routes[idx].Spec.ForProvider.CustomRouteParameters)
		}
		publicRouteTable := &v1beta1.RouteTable{}
		assert.Nil(t, task.Local.Get(context.Background(), types.NamespacedName{Name: vpcPublicRouteTableName(task.VPCName())}, publicRouteTable))
		assert.Equal(t, []v1beta1.Association{
			{SubnetIDRef: &xpv1.Reference{Name: namer.PublicSubnetName(name, zones[0])}},
			{SubnetIDRef: &xpv1.Reference{Name: namer.PublicSubnetName(name, zones[1])}},
			{SubnetIDRef: &xpv1.Reference{Name: namer.PublicSubnetName(name, zones[2])}},
		}, publicRouteTable.Spec.ForProvider.Associations)
		publicRoute := &ec2v1alpha1.Route{}
		assert.Nil(t, task.Local.Get(context.Background(), types.NamespacedName{Name: vpcPublicRouteTableName(task.VPCName())}, publicRoute))
		assert.Equal(t, ec2v1alpha1.CustomRouteParameters{
			GatewayIDRef:    &xpv1.Reference{Name: task.Internet.Status.InternetGateway.Name},
			RouteTableIDRef: &xpv1.Reference{Name: vpcPublicRouteTableName(task.VPCName())},
		}, publicRoute.Spec.ForProvider.CustomRouteParameters)
	})
	t.Run("Delete gardener originated internet", func(t *testing.T) {
		result := task.Delete(context.Background())
		assert.True(t, result.IsDoneState())
		assert.Nil(t, result.Error())
		for _, zone := range zones {
			rt := &v1beta1.RouteTable{}
			assert.ErrorContains(t, task.Local.Get(context.Background(), types.NamespacedName{Name: namer.PrivateSubnetName(name, zone)}, rt), "not found")
			route := &ec2v1alpha1.Route{}
			assert.ErrorContains(t, task.Local.Get(context.Background(), types.NamespacedName{Name: namer.PrivateSubnetName(name, zone)}, route), "not found")
		}
		publicRouteTable := &v1beta1.RouteTable{}
		assert.ErrorContains(t, task.Local.Get(context.Background(), types.NamespacedName{Name: vpcPublicRouteTableName(task.VPCName())}, publicRouteTable), "not found")
		publicRoute := &ec2v1alpha1.Route{}
		assert.ErrorContains(t, task.Local.Get(context.Background(), types.NamespacedName{Name: vpcPublicRouteTableName(task.VPCName())}, publicRoute), "not found")
	})
}

func TestRoute_PostCreate(t *testing.T) {
	t.Run("route table not found", func(t *testing.T) {
		task := fakeRouteTask(t)
		task.routeTables = append(task.routeTables, &v1beta1.RouteTable{
			ObjectMeta: metav1.ObjectMeta{
				Name: "not-found",
			},
		})
		result := task.PostCreate(context.Background())
		res, err, retriableErr := deploy.MinRequeue([]deploy.Result{result})
		assert.Nil(t, err)
		assert.ErrorContains(t, retriableErr, "not found")
		assert.Equal(t, time.Second*30, res.RequeueAfter)
	})
	t.Run("route table not ready", func(t *testing.T) {
		task := fakeRouteTask(t)
		rt := &v1beta1.RouteTable{
			ObjectMeta: metav1.ObjectMeta{
				Name: "not-ready",
			},
			Status: v1beta1.RouteTableStatus{
				AtProvider: v1beta1.RouteTableObservation{
					RouteTableID: "id",
				},
			},
		}
		task.routeTables = append(task.routeTables, rt)
		assert.Nil(t, task.Local.Create(context.Background(), rt))
		result := task.PostCreate(context.Background())
		res, err, retriableErr := deploy.MinRequeue([]deploy.Result{result})
		assert.Nil(t, err)
		assert.ErrorContains(t, retriableErr, "not ready")
		assert.Equal(t, time.Second*30, res.RequeueAfter)
	})
	task := fakeRouteTask(t)
	t.Run("route table is ready", func(t *testing.T) {
		rt := &v1beta1.RouteTable{
			ObjectMeta: metav1.ObjectMeta{
				Name: "test",
			},
			Status: v1beta1.RouteTableStatus{
				ResourceStatus: xpv1.ResourceStatus{
					ConditionedStatus: xpv1.ConditionedStatus{
						Conditions: []xpv1.Condition{
							{Type: xpv1.TypeReady, Status: corev1.ConditionTrue},
						},
					},
				},
				AtProvider: v1beta1.RouteTableObservation{
					RouteTableID: "id",
				},
			},
		}
		task.routeTables = append(task.routeTables, rt)
		assert.Nil(t, task.Local.Create(context.Background(), rt))
		result := task.PostCreate(context.Background())
		assert.True(t, result.IsDoneState())
		assert.Equal(t, []v1alpha1.RouteTable{
			{
				RouteTableID: "id",
				Name:         "test",
			},
		}, task.Internet.Status.RouteTables)
	})
	t.Run("create success", func(t *testing.T) {
		route := &ec2v1alpha1.Route{
			ObjectMeta: metav1.ObjectMeta{
				Name: "test-r",
			},
			Spec: ec2v1alpha1.RouteSpec{
				ForProvider: ec2v1alpha1.RouteParameters{
					CustomRouteParameters: ec2v1alpha1.CustomRouteParameters{
						RouteTableIDRef: &xpv1.Reference{Name: "test"},
					},
				},
			},
			Status: ec2v1alpha1.RouteStatus{
				ResourceStatus: xpv1.ResourceStatus{
					ConditionedStatus: xpv1.ConditionedStatus{
						Conditions: []xpv1.Condition{
							{Type: xpv1.TypeReady, Status: corev1.ConditionTrue},
						},
					},
				},
				AtProvider: ec2v1alpha1.RouteObservation{
					Return: pointer.Bool(true),
				},
			},
		}
		task.routes = append(task.routes, route)
		assert.Nil(t, task.Local.Create(context.Background(), route))
		result := task.PostCreate(context.Background())
		assert.True(t, result.IsDoneState())
		assert.Equal(t, []v1alpha1.Route{{Name: "test-r", RouteTableRef: "test"}}, task.Internet.Status.Routes)
	})
}

func TestRoute_deleteRoute(t *testing.T) {
	t.Run("delete success", func(t *testing.T) {
		task := fakeRouteTask(t)
		name := "test-name"
		assert.Nil(t, task.Local.Create(context.Background(), &ec2v1alpha1.Route{
			ObjectMeta: metav1.ObjectMeta{
				Name: name,
			},
		}))
		assert.Nil(t, task.deleteRoute(context.Background(), name))
		assert.Nil(t, task.deleteRoute(context.Background(), name))
	})
}

func TestRoute_deleteRouteTable(t *testing.T) {
	t.Run("delete success", func(t *testing.T) {
		task := fakeRouteTask(t)
		name := "test-name"
		assert.Nil(t, task.Local.Create(context.Background(), &v1beta1.RouteTable{
			ObjectMeta: metav1.ObjectMeta{
				Name: name,
			},
		}))
		assert.Nil(t, task.deleteRouteTable(context.Background(), name))
		assert.Nil(t, task.deleteRouteTable(context.Background(), name))
	})
}
