package networkpeer

import (
	"context"
	"encoding/json"
	"net/http"
	"testing"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"

	"github.com/aws/aws-sdk-go-v2/service/ec2"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/golang/mock/gomock"
	. "github.com/onsi/gomega"

	awsvpcpeeringv1alpha1 "github.com/crossplane/provider-aws/apis/vpcpeering/v1alpha1"
	gcpvpcpeeringv1beta1 "github.com/crossplane/provider-gcp/apis/vpcpeering/v1beta1"
	"k8s.io/apimachinery/pkg/types"

	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	corev1 "k8s.io/api/core/v1"

	awssdk "github.com/aws/aws-sdk-go/aws"

	"google.golang.org/api/googleapi"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-api/controllers/reconciler"
	"github.com/tidbcloud/infra-api/controllers/test"
	awsfake "github.com/tidbcloud/infra-api/pkg/providers/aws/fake"
	gcpfake "github.com/tidbcloud/infra-api/pkg/providers/gcp/fake"
)

func TestCreateNetworkpeer(t *testing.T) {
	g := NewGomegaWithT(t)
	mockCli := &test.MockClient{
		MockGet: test.NewMockGetFn(nil),
		MockUpdate: test.MockUpdateFn(func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
			mr, _ := obj.(*cloudv1.NetworkPeer)
			status := mr.Status.Phase
			g.Expect(status).Should(Equal(cloudv1.NetworkPeerPhasePending))

			return nil
		}),
		MockCreate: test.MockCreateFn(func(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
			return nil
		}),
		MockSubresourceUpdate: test.NewMockSubresourceUpdateFn(nil),
		MockSubresourcePatch:  test.NewMockSubresourcePatchFn(nil),
	}

	n := &cloudv1.Network{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "network-123",
			Namespace: "infra-test-1",
		},
		Spec: cloudv1.NetworkSpec{
			Region:   "us-west-2",
			Provider: "aws",
			CIDR:     "10.251.0.0/16",
		},
	}
	VpcID := "vpc-123"
	HostedZoneID := "ASCNDJDEKF"
	cnp := &clusterNetworkPeer{
		cli:     mockCli,
		network: n,
		providerSpec: &cloudv1.GardenerProviderSpec{
			AWS: cloudv1.AwsConfig{
				CrossplaneProvider: "aws",
				AccountID:          "123",
			},
		},
		gardenerNetworkStatus: &cloudv1.GardenerNetworkStatus{
			AWS: cloudv1.GardenerNetworkAWSStatus{
				VpcID:        &VpcID,
				HostedZoneID: &HostedZoneID,
			},
		},
	}

	np := &cloudv1.NetworkPeer{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "networkpeer-1",
			Namespace: "infra-test-1",
		},
		Spec: cloudv1.NetworkPeerSpec{
			Source: cnp.network.Name,
			Target: cloudv1.Target{
				AWS: &cloudv1.AWS{
					AccountID:  "456",
					VpcID:      "vpc-456",
					PeerCIDR:   "10.0.0.1/18",
					PeerRegion: "us-east-2",
				},
			},
		},
	}

	f := np.GetFinalizers()
	np.SetFinalizers(append(f, NetworkpeerFinalizer))

	err := cnp.Create(context.TODO(), np)
	g.Expect(err).Should(Succeed())
}

func TestCreateAWSInternalNetworkpeer(t *testing.T) {
	g := NewGomegaWithT(t)
	scheme := runtime.NewScheme()
	g.Expect(cloudv1.AddToScheme(scheme)).To(Succeed())
	g.Expect(awsvpcpeeringv1alpha1.SchemeBuilder.AddToScheme(scheme)).To(Succeed())

	requestNetwork := &cloudv1.Network{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "request-network",
			Namespace: "infra-test-1",
		},
		Spec: cloudv1.NetworkSpec{
			Region:   "us-west-2",
			Provider: "aws",
			CIDR:     "10.251.0.0/16",
		},
	}

	spec := cloudv1.GardenerNetworkStatus{
		AWS: cloudv1.GardenerNetworkAWSStatus{
			VpcID: pointer.String("vpc-456"),
		},
	}

	raw, err := json.Marshal(&spec)
	g.Expect(err).ShouldNot(HaveOccurred())

	acceptNetwork := &cloudv1.Network{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "accept-network",
			Namespace: "infra-test-1",
		},
		Spec: cloudv1.NetworkSpec{
			Region:   "us-west-2",
			Provider: "aws",
			CIDR:     "10.251.0.0/16",
		},
		Status: cloudv1.NetworkStatus{
			FromProvider: runtime.RawExtension{Raw: raw},
		},
	}
	np := &cloudv1.NetworkPeer{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "networkpeer-1",
			Namespace: "infra-test-1",
		},
		Spec: cloudv1.NetworkPeerSpec{
			Source: requestNetwork.Name,
			Target: cloudv1.Target{
				TargetNetwork: &cloudv1.TargetNetwork{
					NetworkName: acceptNetwork.Name,
				},
			},
		},
	}

	client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(acceptNetwork, np).WithStatusSubresource(acceptNetwork, np).Build()

	cnp := &clusterNetworkPeer{
		cli:     client,
		network: requestNetwork,
		providerSpec: &cloudv1.GardenerProviderSpec{
			AWS: cloudv1.AwsConfig{
				CrossplaneProvider: "aws",
				AccountID:          "123",
			},
		},
		gardenerNetworkStatus: &cloudv1.GardenerNetworkStatus{
			AWS: cloudv1.GardenerNetworkAWSStatus{
				VpcID:        pointer.String("VpcID"),
				HostedZoneID: pointer.String("ASCNDJDEKF"),
			},
		},
	}

	err = cnp.Create(context.TODO(), np)
	g.Expect(err).Should(Succeed())

	peering := new(awsvpcpeeringv1alpha1.VPCPeeringConnection)
	err = client.Get(context.TODO(), types.NamespacedName{Name: np.ClusterScopeName()}, peering)
	g.Expect(err).ShouldNot(HaveOccurred())
	g.Expect(*peering.Spec.ForProvider.PeerVPCID).Should(Equal("vpc-456"))
}

func TestCreateGCPInternalNetworkpeer(t *testing.T) {
	g := NewGomegaWithT(t)
	scheme := runtime.NewScheme()
	g.Expect(cloudv1.AddToScheme(scheme)).To(Succeed())
	g.Expect(gcpvpcpeeringv1beta1.SchemeBuilder.AddToScheme(scheme)).To(Succeed())

	requestNetwork := &cloudv1.Network{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "request-network",
			Namespace: "infra-test-1",
		},
		Spec: cloudv1.NetworkSpec{
			Region:   "us-west-2",
			Provider: "gcp",
			CIDR:     "10.251.0.0/16",
		},
	}

	spec := cloudv1.GardenerNetworkStatus{
		GCP: cloudv1.GardenerNetworkGCPStatus{
			ProjectID:   pointer.String("accept"),
			NetworkName: pointer.String("accept"),
		},
	}

	raw, err := json.Marshal(&spec)
	g.Expect(err).ShouldNot(HaveOccurred())

	acceptNetwork := &cloudv1.Network{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "accept-network",
			Namespace: "infra-test-1",
		},
		Spec: cloudv1.NetworkSpec{
			Region:   "us-west-2",
			Provider: "gcp",
			CIDR:     "10.251.0.0/16",
		},
		Status: cloudv1.NetworkStatus{
			FromProvider: runtime.RawExtension{Raw: raw},
		},
	}
	np := &cloudv1.NetworkPeer{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "networkpeer-1",
			Namespace: "infra-test-1",
		},
		Spec: cloudv1.NetworkPeerSpec{
			Source: requestNetwork.Name,
			Target: cloudv1.Target{
				TargetNetwork: &cloudv1.TargetNetwork{
					NetworkName: acceptNetwork.Name,
				},
			},
		},
	}

	client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(acceptNetwork, np).WithStatusSubresource(acceptNetwork, np).Build()

	cnp := &clusterNetworkPeer{
		cli:     client,
		network: requestNetwork,
		providerSpec: &cloudv1.GardenerProviderSpec{
			GCP: cloudv1.GcpConfig{
				CrossplaneProvider: "gcp",
				ProjectID:          "test-project",
			},
		},
		gardenerNetworkStatus: &cloudv1.GardenerNetworkStatus{
			GCP: cloudv1.GardenerNetworkGCPStatus{
				ProjectID:   pointer.String("request"),
				NetworkName: pointer.String("request"),
			},
		},
	}

	err = cnp.Create(context.TODO(), np)
	g.Expect(err).Should(Succeed())

	peering := new(gcpvpcpeeringv1beta1.Peering)
	err = client.Get(context.TODO(), types.NamespacedName{Name: np.ClusterScopeName()}, peering)
	g.Expect(err).ShouldNot(HaveOccurred())
	g.Expect(peering.Spec.ForProvider.PeerNetwork).Should(Equal("https://www.googleapis.com/compute/v1/projects/accept/global/networks/accept"))
}

func TestObserve(t *testing.T) {
	g := NewGomegaWithT(t)
	scheme := runtime.NewScheme()
	g.Expect(cloudv1.AddToScheme(scheme)).To(Succeed())
	g.Expect(awsvpcpeeringv1alpha1.SchemeBuilder.AddToScheme(scheme)).To(Succeed())

	cases := map[string]struct {
		network              *cloudv1.Network
		networkPeer          *cloudv1.NetworkPeer
		vpcPeeringConnection *awsvpcpeeringv1alpha1.VPCPeeringConnection
		wantObservation      reconciler.ExternalObservation
	}{
		"ResourceExist": {
			network: &cloudv1.Network{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "request-network",
					Namespace: "infra-test-1",
				},
				Spec: cloudv1.NetworkSpec{
					Region:   "us-west-2",
					Provider: "aws",
					CIDR:     "10.251.0.0/16",
				},
			},
			networkPeer: &cloudv1.NetworkPeer{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "networkpeer-1",
					Namespace: "infra-test-1",
				},
			},
			vpcPeeringConnection: &awsvpcpeeringv1alpha1.VPCPeeringConnection{
				ObjectMeta: metav1.ObjectMeta{},
				Status: awsvpcpeeringv1alpha1.VPCPeeringConnectionStatus{
					AtProvider: awsvpcpeeringv1alpha1.VPCPeeringConnectionObservation{
						VPCPeeringConnectionID: aws.String("pcx-123456"),
					},
				},
			},
			wantObservation: reconciler.ExternalObservation{
				ResourceExists:   true,
				ResourceUpToDate: false,
				PollInterval:     0,
			},
		},
		"ResourceUpToDataAndReconcile": {
			network: &cloudv1.Network{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "request-network",
					Namespace: "infra-test-1",
				},
				Spec: cloudv1.NetworkSpec{
					Region:   "us-west-2",
					Provider: "aws",
					CIDR:     "10.251.0.0/16",
				},
			},
			networkPeer: &cloudv1.NetworkPeer{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "networkpeer-1",
					Namespace: "infra-test-1",
				},
			},
			vpcPeeringConnection: &awsvpcpeeringv1alpha1.VPCPeeringConnection{
				ObjectMeta: metav1.ObjectMeta{},
				Status: awsvpcpeeringv1alpha1.VPCPeeringConnectionStatus{
					ResourceStatus: xpv1.ResourceStatus{
						ConditionedStatus: xpv1.ConditionedStatus{
							Conditions: []xpv1.Condition{
								{
									Type:   xpv1.TypeReady,
									Status: corev1.ConditionTrue,
								},
							},
						},
					},
					AtProvider: awsvpcpeeringv1alpha1.VPCPeeringConnectionObservation{
						VPCPeeringConnectionID: aws.String("pcx-123456"),
					},
				},
			},
			wantObservation: reconciler.ExternalObservation{
				ResourceExists:   true,
				ResourceUpToDate: true,
				PollInterval:     AWSPeeringPollInterval,
			},
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			tc.vpcPeeringConnection.Name = tc.networkPeer.ClusterScopeName()
			client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(tc.networkPeer, tc.vpcPeeringConnection).WithStatusSubresource(tc.networkPeer, tc.vpcPeeringConnection).Build()
			cnp := &clusterNetworkPeer{
				cli:     client,
				network: tc.network,
			}
			res, err := cnp.Observe(context.TODO(), tc.networkPeer)
			g.Expect(err).Should(Succeed())
			g.Expect(res.ResourceExists).Should(Equal(tc.wantObservation.ResourceExists))
			g.Expect(res.ResourceUpToDate).Should(Equal(tc.wantObservation.ResourceUpToDate))
			g.Expect(res.PollInterval).Should(Equal(tc.wantObservation.PollInterval))
			err = client.Get(context.TODO(), types.NamespacedName{Name: "networkpeer-1", Namespace: "infra-test-1"}, tc.networkPeer)
			g.Expect(err).ShouldNot(HaveOccurred())
			g.Expect(tc.networkPeer.Status.PeeringID).Should(Equal("pcx-123456"))
		})
	}
}

func TestUpdateAWSInternalNetworkpeer(t *testing.T) {
	g := NewGomegaWithT(t)
	scheme := runtime.NewScheme()
	g.Expect(cloudv1.AddToScheme(scheme)).To(Succeed())
	g.Expect(awsvpcpeeringv1alpha1.SchemeBuilder.AddToScheme(scheme)).To(Succeed())

	requestNetwork := &cloudv1.Network{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "request-network",
			Namespace: "infra-test-1",
		},
		Spec: cloudv1.NetworkSpec{
			Region:   "us-west-2",
			Provider: "aws",
			CIDR:     "10.0.0.8/8",
		},
	}

	targetNetworkStatus := cloudv1.GardenerNetworkStatus{
		AWS: cloudv1.GardenerNetworkAWSStatus{
			VpcID: pointer.String("vpc-456"),
			SecurityGroups: []cloudv1.SecurityGroup{
				{
					Purpose: cloudv1.NodePurposeSecurityGroup,
					ID:      "target-node-sg",
				},
			},
		},
	}

	raw, err := json.Marshal(&targetNetworkStatus)
	g.Expect(err).ShouldNot(HaveOccurred())

	acceptNetwork := &cloudv1.Network{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "accept-network",
			Namespace: "infra-test-1",
		},
		Spec: cloudv1.NetworkSpec{
			Region:   "us-east-1",
			Provider: "aws",
			CIDR:     "10.251.0.0/16",
		},
		Status: cloudv1.NetworkStatus{
			FromProvider: runtime.RawExtension{Raw: raw},
		},
	}
	np := &cloudv1.NetworkPeer{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "networkpeer-1",
			Namespace: "infra-test-1",
		},
		Spec: cloudv1.NetworkPeerSpec{
			Source: requestNetwork.Name,
			Target: cloudv1.Target{
				TargetNetwork: &cloudv1.TargetNetwork{
					NetworkName: acceptNetwork.Name,
				},
			},
		},
	}

	client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(acceptNetwork, np).WithStatusSubresource(acceptNetwork, np).Build()
	awsCli := awsfake.NewMockClient(t)
	input1 := &ec2.AuthorizeSecurityGroupIngressInput{
		CidrIp:  awssdk.String("10.251.0.0/16"),
		GroupId: awssdk.String("source-node-sg"),
		// all protocol
		IpProtocol: awssdk.String("-1"),
	}

	awsCli.MockEc2Client.EXPECT().AuthorizeSecurityGroupIngress(gomock.Any(), input1, gomock.Any()).Times(1).Return(nil, nil)

	input2 := &ec2.AuthorizeSecurityGroupIngressInput{
		CidrIp:  awssdk.String("10.0.0.8/8"),
		GroupId: awssdk.String("target-node-sg"),
		// all protocol
		IpProtocol: awssdk.String("-1"),
	}
	awsCli.MockEc2Client.EXPECT().AuthorizeSecurityGroupIngress(gomock.Any(), input2, gomock.Any()).Times(1).Return(nil, nil)
	cnp := &clusterNetworkPeer{
		cli:       client,
		AWSClient: awsCli,
		network:   requestNetwork,
		providerSpec: &cloudv1.GardenerProviderSpec{
			AWS: cloudv1.AwsConfig{
				CrossplaneProvider: "aws",
				AccountID:          "123",
			},
		},
		gardenerNetworkStatus: &cloudv1.GardenerNetworkStatus{
			AWS: cloudv1.GardenerNetworkAWSStatus{
				VpcID:        pointer.String("VpcID"),
				HostedZoneID: pointer.String("ASCNDJDEKF"),
				SecurityGroups: []cloudv1.SecurityGroup{
					{
						Purpose: cloudv1.NodePurposeSecurityGroup,
						ID:      "source-node-sg",
					},
				},
			},
		},
	}

	err = cnp.Update(context.TODO(), np)
	g.Expect(err).Should(Succeed())
}

func TestUpdateGCPInternalNetworkpeer(t *testing.T) {
	var (
		projectID         = "pingcap"
		targetNetwork     = "target-network"
		targetNetworkCidr = "10.251.0.0/16"
		sourceNetwork     = "source-network"
		sourceNetworkCidr = "10.0.0.8/8"
		g                 = NewGomegaWithT(t)
		mockCtrl          = gomock.NewController(t)
		scheme            = runtime.NewScheme()
	)

	defer mockCtrl.Finish()
	g.Expect(cloudv1.AddToScheme(scheme)).To(Succeed())
	g.Expect(awsvpcpeeringv1alpha1.SchemeBuilder.AddToScheme(scheme)).To(Succeed())

	requestNetwork := &cloudv1.Network{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "request-network",
			Namespace: "infra-test-1",
		},
		Spec: cloudv1.NetworkSpec{
			Region:   "us-west2",
			Provider: "gcp",
			CIDR:     sourceNetworkCidr,
		},
	}

	targetNetworkStatus := cloudv1.GardenerNetworkStatus{
		GCP: cloudv1.GardenerNetworkGCPStatus{
			ProjectID:   pointer.String(projectID),
			NetworkName: pointer.String(targetNetwork),
		},
	}

	raw, err := json.Marshal(&targetNetworkStatus)
	g.Expect(err).ShouldNot(HaveOccurred())

	acceptNetwork := &cloudv1.Network{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "accept-network",
			Namespace: "infra-test-1",
		},
		Spec: cloudv1.NetworkSpec{
			Region:   "us-west1",
			Provider: "gcp",
			CIDR:     targetNetworkCidr,
		},
		Status: cloudv1.NetworkStatus{
			FromProvider: runtime.RawExtension{Raw: raw},
		},
	}
	np := &cloudv1.NetworkPeer{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "networkpeer-1",
			Namespace: "infra-test-1",
		},
		Spec: cloudv1.NetworkPeerSpec{
			Source: requestNetwork.Name,
			Target: cloudv1.Target{
				TargetNetwork: &cloudv1.TargetNetwork{
					NetworkName: acceptNetwork.Name,
				},
			},
		},
	}

	client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(acceptNetwork, np).WithStatusSubresource(acceptNetwork, np).Build()
	gcpCli := gcpfake.NewMockGCPAPI(mockCtrl)

	// conflict error will ignore
	statusConflict := googleapi.Error{
		Code: http.StatusConflict,
	}

	gcpCli.EXPECT().AttachIngressPolicy(projectID, gomock.Any(), networkUrl(projectID, targetNetwork), sourceNetworkCidr).Times(1).Return(&statusConflict)
	gcpCli.EXPECT().AttachIngressPolicy(projectID, gomock.Any(), networkUrl(projectID, sourceNetwork), targetNetworkCidr).Times(1).Return(nil)

	cnp := &clusterNetworkPeer{
		cli:     client,
		GCPAPI:  gcpCli,
		network: requestNetwork,
		providerSpec: &cloudv1.GardenerProviderSpec{
			AWS: cloudv1.AwsConfig{
				CrossplaneProvider: "aws",
				AccountID:          "123",
			},
		},
		gardenerNetworkStatus: &cloudv1.GardenerNetworkStatus{
			GCP: cloudv1.GardenerNetworkGCPStatus{
				ProjectID:   pointer.String(projectID),
				NetworkName: pointer.String(sourceNetwork),
			},
		},
	}

	err = cnp.Update(context.TODO(), np)
	g.Expect(err).Should(Succeed())
}
