package procedure

import (
	"context"
	"testing"

	cpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	ec2v1alpha1 "github.com/crossplane/provider-aws/apis/ec2/v1alpha1"
	. "github.com/onsi/gomega"
	eksv1alpha1 "github.com/tidbcloud/eks-provider/api/v1alpha1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/kubernetes/scheme"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

const (
	fakeFrpsEpSvcSvcName = "fake-ep-svc-svc-name"
	fakeVpcID            = "fake-vpc-id"
)

var (
	fakeFrpsEndpointInfo ProcedureInfo
	fakeFrpsEndpointKey  types.NamespacedName
)

func init() {
	fakeFrpsEndpointInfo.Region = "us-west-2"
	fakeFrpsEndpointInfo.Cluster = "eks1"
	fakeFrpsEndpointInfo.ProviderConfigRef = "123456789012"
	fakeFrpsEndpointInfo.eksConnection = &eksv1alpha1.EKSConnection{}
	fakeFrpsEndpointInfo.VpcID = "fake-vpc-id"
	fakeFrpsEndpointInfo.namespace = ProcedureNamespace
	fakeFrpsEndpointKey = ResourceKey(fakeFrpsEndpointInfo, frpsEpSvcName)
}

type fakePlMngr struct {
	svcName       string
	errGetSvcName error
	svcDNS        string
	errGetSvcDNS  error
	errAcceptPl   error
}

func (f *fakePlMngr) getEndpointServiceName(ctx context.Context, cli client.Reader, info ProcedureInfo, objName string) (string, error) {
	return f.svcName, f.errGetSvcName
}

func (f *fakePlMngr) getEndpointDNS(ctx context.Context, cli client.Reader, objName string) (string, error) {
	return f.svcDNS, f.errGetSvcDNS
}

func (f *fakePlMngr) acceptPrivateLink(ctx context.Context, endpointSvcID, endpointID string) error {
	return f.errAcceptPl
}

var _ privateLinkMngr = &fakePlMngr{}

func newFakePlMngr(svcName, svcDNS string, errGetSvcName, errGetSvcDNS, errAcceptPl error) *fakePlMngr {
	return &fakePlMngr{
		svcName:       svcName,
		errGetSvcName: errGetSvcName,
		errAcceptPl:   errAcceptPl,
	}
}

func TestFrpsEndpointDeploy(t *testing.T) {
	addEpSvcEpSvcArn := func(obj *ec2v1alpha1.VPCEndpointServiceConfiguration, epSvcArn string) *ec2v1alpha1.VPCEndpointServiceConfiguration {
		obj.Annotations = make(map[string]string)
		obj.Annotations[crossplaneExternalName] = epSvcArn
		return obj
	}
	fakeNlbARN := "fake-nlb-arn"
	cases := map[string]struct {
		info              ProcedureInfo
		objs              []client.Object
		expectErr         bool
		fakeSvcName       string
		fakeGetSvcNameErr error
		fakeAcceptPlErr   error
	}{
		"create-success": {
			info:      fakeFrpsEndpointInfo,
			expectErr: false,
			objs: []client.Object{
				addEpSvcEpSvcArn(
					getFrpsEndpointSvc(fakeFrpsEndpointInfo, fakeNlbARN),
					fakeEpSvcArn,
				),
			},
			fakeSvcName: fakeEpSvcArn,
		},
		"ep-already-exist": {
			info: fakeFrpsEndpointInfo,
			objs: []client.Object{
				&ec2v1alpha1.VPCEndpoint{
					ObjectMeta: metav1.ObjectMeta{
						Name: fakeFrpsEndpointKey.Name,
					},
				},
				addEpSvcEpSvcArn(
					getFrpsEndpointSvc(fakeFrpsEndpointInfo, fakeNlbARN),
					fakeEpSvcArn,
				),
			},
			expectErr:   false,
			fakeSvcName: fakeEpSvcArn,
		},
	}
	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			g := NewGomegaWithT(t)
			s := runtime.NewScheme()
			g.Expect(scheme.AddToScheme(s)).To(Succeed())
			g.Expect(ec2v1alpha1.SchemeBuilder.AddToScheme(s)).To(Succeed())
			cli := fake.NewClientBuilder().WithScheme(s).WithObjects(tc.objs...).Build()
			p := NewFrpsEp(newFakePlMngr(tc.fakeSvcName, "", tc.fakeGetSvcNameErr, nil, tc.fakeAcceptPlErr))
			err := p.Deploy(context.TODO(), cli, cli, tc.info)
			if !tc.expectErr {
				g.Expect(err).To(Succeed())
			} else {
				g.Expect(err).NotTo(Succeed())
			}
		})
	}
}

func TestFrpsEndpointStatus(t *testing.T) {
	addCondition := func(obj *ec2v1alpha1.VPCEndpoint, cond cpv1.Condition) *ec2v1alpha1.VPCEndpoint {
		obj.Annotations = map[string]string{
			crossplaneExternalName: "fake-ep-external-name",
		}
		obj.Status.ConditionedStatus.SetConditions(cond)
		return obj
	}
	fakeVpcEpSvc := getFrpsEndpointSvc(fakeFrpsEndpointInfo, fakeEpSvcArn)
	fakeVpcEpSvc.Annotations = map[string]string{
		crossplaneExternalName: "fake-ep-svc-external-name",
	}
	fakeVpcID := "fake-vpc-id"
	svcName := fakeFrpsEpSvcSvcName
	cases := map[string]struct {
		info            ProcedureInfo
		objs            []client.Object
		expectReady     bool
		expectErr       bool
		fakeSvcName     string
		fakeSvcNameErr  error
		fakeacceptPlErr error
	}{
		"not-exist": {
			info:        fakeFrpsEndpointInfo,
			expectErr:   true,
			expectReady: false,
		},
		"endpoint-ready": {
			info: fakeFrpsEndpointInfo,
			objs: []client.Object{
				addCondition(
					getFrpsEndpoint(fakeFrpsEndpointInfo, &fakeVpcID, &svcName),
					cpv1.Condition{
						Type:   cpv1.TypeReady,
						Status: corev1.ConditionTrue,
					},
				),
				fakeVpcEpSvc,
			},
			expectErr:   false,
			expectReady: true,
		},
		"endpoint-not-ready": {
			info: fakeFrpsEndpointInfo,
			objs: []client.Object{
				addCondition(
					getFrpsEndpoint(fakeFrpsEndpointInfo, &fakeVpcID, &svcName),
					cpv1.Condition{
						Type:   cpv1.TypeReady,
						Status: corev1.ConditionFalse,
					},
				),
				fakeVpcEpSvc,
			},
			expectErr:   false,
			expectReady: false,
		},
	}
	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			g := NewGomegaWithT(t)
			s := runtime.NewScheme()
			g.Expect(scheme.AddToScheme(s)).To(Succeed())
			g.Expect(ec2v1alpha1.SchemeBuilder.AddToScheme(s)).To(Succeed())
			cli := fake.NewClientBuilder().WithScheme(s).WithObjects(tc.objs...).Build()
			p := NewFrpsEp(newFakePlMngr(tc.fakeSvcName, "", tc.fakeSvcNameErr, nil, tc.fakeacceptPlErr))
			ready, err := p.Status(context.TODO(), cli, cli, tc.info)
			if !tc.expectErr {
				g.Expect(err).To(Succeed())
			} else {
				g.Expect(err).NotTo(Succeed())
				return
			}
			g.Expect(ready).To(Equal(tc.expectReady))
		})
	}
}
