//go:build intergration

package ft

import (
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/ec2"
	ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
	"github.com/aws/smithy-go"
	xpmeta "github.com/crossplane/crossplane-runtime/pkg/meta"
	"golang.org/x/sync/errgroup"

	computepb "cloud.google.com/go/compute/apiv1/computepb"

	ec2v1beta1 "github.com/crossplane-contrib/provider-aws/apis/ec2/v1beta1"
	computev1beta1 "github.com/crossplane-contrib/provider-gcp/apis/compute/v1beta1"
	gcpapierr "github.com/googleapis/gax-go/v2/apierror"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	regionalpb "github.com/tidbcloud/infra-provider/api/tidbcloud/regional-server/v1"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/utils"

	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"sigs.k8s.io/controller-runtime/pkg/client"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

func CreateFooVPCReq() *regionalpb.CreateVpcRequest {
	return &regionalpb.CreateVpcRequest{
		RequestId: "ft-foo",
		Vpc: &regionalpb.Vpc{
			TenantIds:  []string{f.TenantFoo},
			ProjectIds: []string{f.ProjectFoo},
			CidrBlock:  "10.0.0.0/16",
		},
	}
}

func CreateBarVPCReq() *regionalpb.CreateVpcRequest {
	return &regionalpb.CreateVpcRequest{
		RequestId: "bar",
		Vpc: &regionalpb.Vpc{
			TenantIds:  []string{f.TenantFoo},
			ProjectIds: []string{f.ProjectFoo},
			CidrBlock:  "10.1.0.0/16",
		},
	}
}

func CreateTmpVPCReq() *regionalpb.CreateVpcRequest {
	return &regionalpb.CreateVpcRequest{
		RequestId: fmt.Sprintf("ft-tmp-%d", time.Now().Unix()),
		Vpc: &regionalpb.Vpc{
			TenantIds:  []string{f.TenantFoo},
			ProjectIds: []string{f.ProjectFoo},
			CidrBlock:  "10.127.0.0/16",
		},
	}
}

var _ = Describe("vpc create api test", func() {
	It("create vpc", func() {
		interval := 10 * time.Second
		concurrent := 5
		req := CreateFooVPCReq()
		By("create vpc concurrently")
		errG, _ := errgroup.WithContext(ctx)
		for i := 0; i < concurrent; i++ {
			errG.Go(func() error {
				_, err := vpcClient.CreateVpc(ctx, req)
				if err != nil {
					if !strings.Contains(err.Error(), "HTTPSTATUSCODE:409") {
						return nil
					}
				}
				return nil
			})
		}
		Expect(errG.Wait()).To(BeNil())
		vpc := &v1alpha1.VPC{
			ObjectMeta: metav1.ObjectMeta{
				Name:      utils.MD5(req.RequestId),
				Namespace: f.Config.Namespace,
			},
		}
		// create a vpc which is under creating should only return status code 409 if error occuers
		_, err := vpcClient.CreateVpc(ctx, req)
		if err != nil {
			Expect(err.Error()).Should(ContainSubstring("HTTPSTATUSCODE:409"))
		}
		waitVPCReady(ctx, vpc, 5*time.Minute, interval)
		switch f.Config.Provider {
		case "aws":
			By("check crossplane cr, it should be ready")
			xpvpc := new(ec2v1beta1.VPC)
			xpvpc.Name = namer.UniqueIdentifier(vpc.ObjectMeta)
			err := kubeClient.Get(ctx, client.ObjectKeyFromObject(xpvpc), xpvpc)
			Expect(err).To(BeNil())
			// external name should match
			Expect(vpc.Status.VPC.VPCID).To(Equal(xpmeta.GetExternalName(&xpvpc.ObjectMeta)))
			ready, synced := utils.GetConditions(xpvpc.Status.Conditions)
			Expect(ready).To(BeTrue())
			Expect(synced).To(BeTrue())
			By("check aws vpc, it should be ready")
			vpcID := vpc.Status.VPC.VPCID
			in := new(ec2.DescribeVpcsInput)
			in.VpcIds = []string{vpcID}
			in.Filters = append(in.Filters, ec2types.Filter{
				Name:   aws.String("owner-id"),
				Values: []string{f.Config.DefaultAccount},
			})
			out, err := ec2Client.DescribeVpcs(ctx, in)
			Expect(err).To(BeNil())
			Expect(out.Vpcs).To(HaveLen(1))
			Expect(out.Vpcs[0].State).To(Equal(ec2types.VpcStateAvailable))
		case "gcp":
			By("check crossplane cr, it should be ready")
			xpnetwork := new(computev1beta1.Network)
			xpnetwork.Name = namer.UniqueIdentifier(vpc.ObjectMeta, namer.WithLimit(57))
			err := kubeClient.Get(ctx, client.ObjectKeyFromObject(xpnetwork), xpnetwork)
			Expect(err).To(BeNil())
			// external name should match
			Expect(vpc.Status.VPC.VPCID).To(Equal(xpmeta.GetExternalName(&xpnetwork.ObjectMeta)))
			ready, synced := utils.GetConditions(xpnetwork.Status.Conditions)
			Expect(ready).To(BeTrue())
			Expect(synced).To(BeTrue())
			By("check gcp network, it should be ready")
			vpcID := vpc.Status.VPC.VPCID
			in := new(computepb.GetNetworkRequest)
			in.Network = vpcID
			in.Project = vpc.Spec.AccountRef.AccountID
			out, err := gcpNetworksClient.Get(ctx, in)
			Expect(err).To(BeNil())
			Expect(aws.ToString(out.Name)).To(Equal(vpcID))
		}
	})
})

var _ = Describe("vpc delete api test", func() {
	It("delete not existed vpc", func() {
		in := new(regionalpb.DeleteVpcRequest)
		in.VpcId = fmt.Sprintf("notexist-%v", time.Now().Unix())
		err := vpcClient.DeleteVpc(ctx, in)
		Expect(err).To(BeNil())
	})
	It("delete existed vpc", func() {
		interval := 20 * time.Second
		By("prepare resource")
		req := CreateTmpVPCReq()
		vpc := &v1alpha1.VPC{
			ObjectMeta: metav1.ObjectMeta{
				Name:      utils.MD5(req.RequestId),
				Namespace: f.Config.Namespace,
			},
		}
		_, err := vpcClient.CreateVpc(ctx, req)
		Expect(err).To(Or(BeNil(), MatchError(ContainSubstring("HTTPSTATUSCODE:409"))))
		waitVPCReady(ctx, vpc, 5*time.Minute, interval)
		By("delete exited vpc")
		in := new(regionalpb.DeleteVpcRequest)
		in.VpcId = vpc.Status.VPC.VPCID
		err = vpcClient.DeleteVpc(ctx, in)
		Expect(err).To(BeNil())
		By("check vpc cr, crossplane cr and aws/gcp resource should be deleted")
		vpcID := vpc.Status.VPC.VPCID
		Eventually(func() bool {
			key := client.ObjectKeyFromObject(vpc)
			err := kubeClient.Get(ctx, key, vpc)
			if !apierrors.IsNotFound(err) {
				if err != nil {
					logger.Error(err, "get vpc failed", "vpc", key.String())
				} else {
					logger.Info("wait vpc deleted", "vpc", key.String(), "current phase", vpc.Status.Phase)
				}
			}
			return apierrors.IsNotFound(err)
		}, 10*time.Minute, time.Minute).Should(BeTrue(), "vpc %s should be deleted", client.ObjectKeyFromObject(vpc).String())
		switch f.Config.Provider {
		case "aws":
			xpvpc := new(ec2v1beta1.VPC)
			xpvpc.Name = namer.UniqueIdentifier(vpc.ObjectMeta)
			Eventually(func() bool {
				key := client.ObjectKeyFromObject(xpvpc)
				err = kubeClient.Get(ctx, key, xpvpc)
				if !apierrors.IsNotFound(err) {
					if err != nil {
						logger.Error(err, "get xpvpc failed", "xpvpc", key.String())
					} else {
						logger.Info("wait xpvpc deleted", "xpvpc", key.String(), "current phase", xpvpc.Status.ConditionedStatus)
					}
				}
				return apierrors.IsNotFound(err)
			}, 10*time.Minute, time.Minute).Should(BeTrue(), "crossplane vpc %s should be deleted", client.ObjectKeyFromObject(xpvpc).String())
			in := new(ec2.DescribeVpcsInput)
			in.VpcIds = []string{vpcID}
			in.Filters = append(in.Filters, ec2types.Filter{
				Name:   aws.String("owner-id"),
				Values: []string{f.Config.DefaultAccount},
			})
			_, err := ec2Client.DescribeVpcs(ctx, in)
			Expect(err).To(HaveOccurred())
			var apierr smithy.APIError
			Expect(errors.As(err, &apierr)).To(BeTrue())
			Expect(apierr.ErrorCode()).To(Equal("InvalidVpcID.NotFound"))
		case "gcp":
			xpnetwork := new(computev1beta1.Network)
			xpnetwork.Name = namer.UniqueIdentifier(vpc.ObjectMeta, namer.WithLimit(57))
			Eventually(func() bool {
				key := client.ObjectKeyFromObject(xpnetwork)
				err = kubeClient.Get(ctx, key, xpnetwork)
				if !apierrors.IsNotFound(err) {
					if err != nil {
						logger.Error(err, "get xpnetwork failed", "xpnetwork", key.String())
					} else {
						logger.Info("wait xpnetwork deleted", "xpnetwork", key.String(), "current phase", xpnetwork.Status.ConditionedStatus)
					}
				}
				return apierrors.IsNotFound(err)
			}, 10*time.Minute, time.Minute).Should(BeTrue(), "crossplane network %s should be deleted", client.ObjectKeyFromObject(xpnetwork).String())
			vpcID := vpc.Status.VPC.VPCID
			in := new(computepb.GetNetworkRequest)
			in.Network = vpcID
			in.Project = vpc.Spec.AccountRef.AccountID
			_, err := gcpNetworksClient.Get(ctx, in)
			Expect(err).To(HaveOccurred())
			apierr, ok := gcpapierr.FromError(err)
			if ok {
				Expect(apierr.Error()).To(ContainSubstring("googleapi: Error 404"), "apierr is %s", apierr.Error())
			}
		}

	})
})

var _ = Describe("vpc get api test", func() {
	It("get not existed vpc", func() {
		in := new(regionalpb.GetVpcRequest)
		in.VpcId = fmt.Sprintf("notexist-%v", time.Now().Unix())
		_, err := vpcClient.GetVpc(ctx, in)
		Expect(err).To(HaveOccurred())
		Expect(err.Error()).Should(ContainSubstring("HTTPSTATUSCODE:404"))
	})
	It("get existed vpc", func() {
		interval := 10 * time.Second
		By("prepare resource")
		req := CreateFooVPCReq()
		vpc := &v1alpha1.VPC{
			ObjectMeta: metav1.ObjectMeta{
				Name:      utils.MD5(req.RequestId),
				Namespace: f.Config.Namespace,
			},
		}
		_, err := vpcClient.CreateVpc(ctx, req)
		Expect(err).To(Or(BeNil(), MatchError(ContainSubstring("HTTPSTATUSCODE:409"))))
		waitVPCReady(ctx, vpc, 5*time.Minute, interval)
		By("get existed vpc")
		in := new(regionalpb.GetVpcRequest)
		in.VpcId = vpc.Status.VPC.VPCID
		out, err := vpcClient.GetVpc(ctx, in)
		Expect(err).To(BeNil())
		Expect(out.GetVpcId()).To(Equal(in.VpcId))
		switch f.Config.Provider {
		case "aws":
			Expect(out.GetAws()).NotTo(BeNil())
			Expect(out.GetAws().GetVpcId()).To(Equal(in.VpcId))
		case "gcp":
			Expect(out.GetGcp()).NotTo(BeNil())
			Expect(out.GetGcp().GetNetworkName()).To(Equal(in.VpcId))
		}
	})
})

var _ = Describe("vpc list api test", Serial, func() {
	It("prepare resource", func() {
		interval := 10 * time.Second
		req := CreateFooVPCReq()
		vpc := &v1alpha1.VPC{
			ObjectMeta: metav1.ObjectMeta{
				Name:      utils.MD5(req.RequestId),
				Namespace: f.Config.Namespace,
			},
		}
		_, err := vpcClient.CreateVpc(ctx, req)
		Expect(err).To(Or(BeNil(), MatchError(ContainSubstring("HTTPSTATUSCODE:409"))))
		waitVPCReady(ctx, vpc, 5*time.Minute, interval)
	})
	It("list not existed vpc", func() {
		req := CreateFooVPCReq()
		{
			By("list with not existed tenant")
			in := new(regionalpb.ListVpcsRequest)
			in.TenantId = fmt.Sprintf("notexist-%v", time.Now().Unix())
			out, err := vpcClient.ListVpcs(ctx, in)
			Expect(err).To(BeNil())
			Expect(out.GetVpcs()).To(HaveLen(0))
		}
		{
			By("list with not existed project")
			in := new(regionalpb.ListVpcsRequest)
			in.TenantId = req.GetVpc().GetTenantIds()[0]
			in.ProjectId = fmt.Sprintf("notexist-%v", time.Now().Unix())
			out, err := vpcClient.ListVpcs(ctx, in)
			Expect(err).To(BeNil())
			Expect(out.GetVpcs()).To(HaveLen(0))
		}
		{
			By("list with not existed cluster")
			in := new(regionalpb.ListVpcsRequest)
			in.TenantId = req.GetVpc().GetTenantIds()[0]
			in.ProjectId = req.GetVpc().GetProjectIds()[0]
			in.ClusterId = fmt.Sprintf("notexist-%v", time.Now().Unix())
			out, err := vpcClient.ListVpcs(ctx, in)
			Expect(err).To(BeNil())
			Expect(out.GetVpcs()).To(HaveLen(0))
		}
	})
	It("list existed vpc", func() {
		req := CreateFooVPCReq()
		{
			By("list with tenant")
			in := new(regionalpb.ListVpcsRequest)
			in.TenantId = req.GetVpc().GetTenantIds()[0]
			out, err := vpcClient.ListVpcs(ctx, in)
			Expect(err).To(BeNil())
			Expect(len(out.GetVpcs()) > 0).To(BeTrue())
		}
		{
			By("list with tenant and project")
			in := new(regionalpb.ListVpcsRequest)
			in.TenantId = req.GetVpc().GetTenantIds()[0]
			in.ProjectId = req.GetVpc().GetProjectIds()[0]
			out, err := vpcClient.ListVpcs(ctx, in)
			Expect(err).To(BeNil())
			Expect(len(out.GetVpcs()) > 0).To(BeTrue())
		}
	})
})
