package approute

import (
	"context"
	"testing"

	"cloud.google.com/go/container/apiv1/containerpb"
	"github.com/go-logr/logr/testr"
	"github.com/stretchr/testify/require"
	"go.uber.org/mock/gomock"
	grpccodes "google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/tools/record"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"
	"sigs.k8s.io/yaml"

	eksv1beta1 "github.com/crossplane-contrib/provider-aws/apis/eks/v1beta1"
	gkev1beta2 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta2"
	xpmeta "github.com/crossplane/crossplane-runtime/pkg/meta"
	clusterctlpb "github.com/tidbcloud/infra-provider/api/tidbcloud/cluster-server/v1"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	rsconfig "github.com/tidbcloud/infra-provider/regional-server/pkg/config"
	"github.com/tidbcloud/infra-provider/regional-server/test/service/mockclustercli"
	"github.com/tidbcloud/infra-provider/regional-server/test/service/mockgcpcontainer"
	"github.com/tidbcloud/infra-provider/regional-server/test/testdata"
	agentcontrolapi "github.com/tidbcloud/observability/agent-control-server/pkg/agentcontrol/api"
)

func TestAPPRouteReconcilerPausedAnno(t *testing.T) {
	s := runtime.NewScheme()
	require.NoError(t, v1alpha1.AddToScheme(s))
	require.NoError(t, eksv1beta1.SchemeBuilder.AddToScheme(s))
	logger := testr.New(t)
	ctrl := gomock.NewController(t)
	clusterCli := mockclustercli.NewMockClusterClient(ctrl)
	config := rsconfig.RootConfig{}
	config.ControllerConfig.Provider = "aws"
	config.ClusterControlServiceEndpoint = "http://localhost"
	defaultAPPBox := new(v1alpha1.APPBox)
	defaultAppRoute := new(v1alpha1.APPRoute)
	defaultVPC := new(v1alpha1.VPC)
	{
		bs, err := testdata.LoadDefaultAPPBoxYAML()
		require.NoError(t, err)
		require.NoError(t, yaml.Unmarshal(bs, defaultAPPBox))
		if defaultAPPBox.Annotations == nil {
			defaultAPPBox.Annotations = map[string]string{}
		}
	}
	{
		bs, err := testdata.LoadDefaultAPPRouteYAML()
		require.NoError(t, err)
		require.NoError(t, yaml.Unmarshal(bs, defaultAppRoute))
		if defaultAppRoute.Annotations == nil {
			defaultAppRoute.Annotations = map[string]string{}
		}
	}
	{
		bs, err := testdata.LoadDefaultVPCYAML()
		require.NoError(t, err)
		require.NoError(t, yaml.Unmarshal(bs, defaultVPC))
		if defaultVPC.Annotations == nil {
			defaultVPC.Annotations = map[string]string{}
		}

	}
	{
		// if approute cr has skip anno, reconcile should do nothing.
		approute := defaultAppRoute.DeepCopy()
		approute.Status.Phase = v1alpha1.APPRoutePhasePending
		approute.Annotations[v1alpha1.AnnotationKeyReconciliationPaused] = "true"
		appbox := defaultAPPBox.DeepCopy()
		appbox.Status.Phase = v1alpha1.APPBoxPhaseCreating
		eks := new(eksv1beta1.Cluster)
		eks.Name = "foo"
		eks.Status.AtProvider.CertificateAuthorityData = "Y2EtZGF0YQ=="
		eks.Status.AtProvider.Endpoint = "http://localhost"
		eks.Status.AtProvider.ResourcesVpcConfig.VpcID = "vpc-foo"
		vpc := defaultVPC.DeepCopy()
		vpc.Status.SecurityGroup = v1alpha1.SecurityGroup{
			Name:            "foo",
			SecurityGroupID: "sg-foo",
		}
		vpc.Status.NodeSubnets = []v1alpha1.Subnet{{Name: "node-subnet-1"}}
		vpc.Status.PublicSubnets = []v1alpha1.Subnet{{Name: "public-subnet-1"}}
		vpc.Status.PrivateSubnets = []v1alpha1.Subnet{{Name: "private-subnet-1"}}
		kubeClient := fake.NewClientBuilder().WithScheme(s).WithObjects(
			approute,
			appbox,
			eks,
			vpc).WithStatusSubresource(approute, appbox, eks, vpc).Build()
		rr := NewAPPRouteReconciler(
			kubeClient,
			logger,
			s,
			record.NewFakeRecorder(1024),
			config,
			&agentcontrolapi.MockAgentControlAPI{}).
			WithClusterClient(clusterCli)
		// call cluster control service at least once
		clusterCli.EXPECT().
			CreateCluster(gomock.Any(), gomock.Any()).
			Return(new(clusterctlpb.Cluster), nil).
			MaxTimes(0)
		restult, err := rr.Reconcile(context.TODO(), reconcile.Request{
			NamespacedName: types.NamespacedName{
				Namespace: "foo",
				Name:      "foo",
			},
		})
		require.NoError(t, err)
		require.True(t, restult.IsZero())
		newapproute := approute.DeepCopy()
		require.NoError(t, kubeClient.Get(context.TODO(), client.ObjectKeyFromObject(newapproute), newapproute))
		require.Equal(t, approute, newapproute)
		ctrl.Satisfied()
	}
}

func TestAPPRouteReconcilerAWS(t *testing.T) {
	s := runtime.NewScheme()
	require.NoError(t, v1alpha1.AddToScheme(s))
	require.NoError(t, eksv1beta1.SchemeBuilder.AddToScheme(s))
	logger := testr.New(t)
	ctrl := gomock.NewController(t)
	config := rsconfig.RootConfig{}
	config.ControllerConfig.Provider = "aws"
	config.ClusterControlServiceEndpoint = "http://localhost"
	defaultAPPBox := new(v1alpha1.APPBox)
	defaultAppRoute := new(v1alpha1.APPRoute)
	defaultVPC := new(v1alpha1.VPC)
	{
		bs, err := testdata.LoadDefaultAPPBoxYAML()
		require.NoError(t, err)
		require.NoError(t, yaml.Unmarshal(bs, defaultAPPBox))
	}
	{
		bs, err := testdata.LoadDefaultAPPRouteYAML()
		require.NoError(t, err)
		require.NoError(t, yaml.Unmarshal(bs, defaultAppRoute))
	}
	{
		bs, err := testdata.LoadDefaultVPCYAML()
		require.NoError(t, err)
		require.NoError(t, yaml.Unmarshal(bs, defaultVPC))
	}

	t.Run("normal create", func(t *testing.T) {
		// normal create should not return any error.
		clusterCli := mockclustercli.NewMockClusterClient(ctrl)
		approute := defaultAppRoute.DeepCopy()
		approute.Status.Phase = v1alpha1.APPRoutePhasePending
		appbox := defaultAPPBox.DeepCopy()
		appbox.Status.Phase = v1alpha1.APPBoxPhaseActive
		eks := new(eksv1beta1.Cluster)
		eks.Name = "foo"
		eks.Status.AtProvider.CertificateAuthorityData = "Y2EtZGF0YQ=="
		eks.Status.AtProvider.Endpoint = "http://localhost"
		eks.Status.AtProvider.ResourcesVpcConfig.VpcID = "vpc-foo"
		vpc := defaultVPC.DeepCopy()
		vpc.Status.SecurityGroup = v1alpha1.SecurityGroup{
			Name:            "foo",
			SecurityGroupID: "sg-foo",
		}
		vpc.Status.NodeSubnets = []v1alpha1.Subnet{{Name: "node-subnet-1"}}
		vpc.Status.PublicSubnets = []v1alpha1.Subnet{{Name: "public-subnet-1"}}
		vpc.Status.PrivateSubnets = []v1alpha1.Subnet{{Name: "private-subnet-1"}}
		kubeClient := fake.NewClientBuilder().WithScheme(s).WithStatusSubresource(approute, appbox, eks, vpc).WithObjects(approute, appbox, eks, vpc).Build()
		rr := NewAPPRouteReconciler(
			kubeClient,
			logger,
			s,
			record.NewFakeRecorder(1024),
			config,
			&agentcontrolapi.MockAgentControlAPI{}).
			WithClusterClient(clusterCli)
		// call cluster control service at least once
		clusterCli.EXPECT().
			CreateCluster(gomock.Any(), gomock.Any()).
			Return(new(clusterctlpb.Cluster), nil).
			MinTimes(1)
		restult, err := rr.Reconcile(context.TODO(), reconcile.Request{
			NamespacedName: types.NamespacedName{
				Namespace: "foo",
				Name:      "foo",
			},
		})
		require.NoError(t, err)
		require.True(t, restult.IsZero())
		require.NoError(t, kubeClient.Get(context.TODO(), client.ObjectKeyFromObject(approute), approute))
		require.Equal(t, v1alpha1.APPRoutePhaseActive, approute.Status.Phase)
		ctrl.Satisfied()
	})
	t.Run("normal delete", func(t *testing.T) {
		// normal delete should not return any error.
		clusterCli := mockclustercli.NewMockClusterClient(ctrl)
		approute := defaultAppRoute.DeepCopy()
		deleteTime := metav1.Now()
		approute.SetDeletionTimestamp(&deleteTime)
		appbox := defaultAPPBox.DeepCopy()
		appbox.Status.Phase = v1alpha1.APPBoxPhaseActive
		eks := new(eksv1beta1.Cluster)
		eks.Name = "foo"
		eks.Status.AtProvider.CertificateAuthorityData = "Y2EtZGF0YQ=="
		eks.Status.AtProvider.Endpoint = "http://localhost"
		eks.Status.AtProvider.ResourcesVpcConfig.VpcID = "vpc-foo"
		vpc := defaultVPC.DeepCopy()
		vpc.Status.SecurityGroup = v1alpha1.SecurityGroup{
			Name:            "foo",
			SecurityGroupID: "sg-foo",
		}
		vpc.Status.NodeSubnets = []v1alpha1.Subnet{{Name: "node-subnet-1"}}
		vpc.Status.PublicSubnets = []v1alpha1.Subnet{{Name: "public-subnet-1"}}
		vpc.Status.PrivateSubnets = []v1alpha1.Subnet{{Name: "private-subnet-1"}}
		kubeClient := fake.NewClientBuilder().WithScheme(s).WithObjects(
			approute,
			appbox,
			eks,
			vpc).WithStatusSubresource(approute, appbox, eks, vpc).Build()
		rr := NewAPPRouteReconciler(
			kubeClient,
			logger,
			s,
			record.NewFakeRecorder(1024),
			config,
			&agentcontrolapi.MockAgentControlAPI{}).
			WithClusterClient(clusterCli)
		// call cluster control service at least once
		clusterCli.EXPECT().
			DeleteCluster(gomock.Any(), gomock.Any()).
			Return(new(clusterctlpb.Cluster), nil).
			MinTimes(1)
		clusterCli.EXPECT().GetCluster(gomock.Any(), gomock.Any()).Return(nil, status.New(grpccodes.NotFound, "not found").Err()).MinTimes(1)
		restult, err := rr.Reconcile(context.TODO(), reconcile.Request{
			NamespacedName: types.NamespacedName{
				Namespace: "foo",
				Name:      "foo",
			},
		})
		t.Log(restult, err)
		require.NoError(t, err)
		require.True(t, restult.IsZero())
		require.True(t, apierrors.IsNotFound(kubeClient.Get(context.TODO(), client.ObjectKeyFromObject(approute), approute)))
		ctrl.Satisfied()

	})
	t.Run("normal delete with appbox deleted", func(t *testing.T) {
		// delete with appbox deleted should not return any error.
		clusterCli := mockclustercli.NewMockClusterClient(ctrl)
		approute := defaultAppRoute.DeepCopy()
		deleteTime := metav1.Now()
		approute.SetDeletionTimestamp(&deleteTime)
		eks := new(eksv1beta1.Cluster)
		eks.Name = "foo"
		eks.Status.AtProvider.CertificateAuthorityData = "Y2EtZGF0YQ=="
		eks.Status.AtProvider.Endpoint = "http://localhost"
		eks.Status.AtProvider.ResourcesVpcConfig.VpcID = "vpc-foo"
		vpc := defaultVPC.DeepCopy()
		vpc.Status.SecurityGroup = v1alpha1.SecurityGroup{
			Name:            "foo",
			SecurityGroupID: "sg-foo",
		}
		vpc.Status.NodeSubnets = []v1alpha1.Subnet{{Name: "node-subnet-1"}}
		vpc.Status.PublicSubnets = []v1alpha1.Subnet{{Name: "public-subnet-1"}}
		vpc.Status.PrivateSubnets = []v1alpha1.Subnet{{Name: "private-subnet-1"}}
		kubeClient := fake.NewClientBuilder().WithScheme(s).WithObjects(
			approute,
			eks,
			vpc).WithStatusSubresource(approute, eks, vpc).Build()
		rr := NewAPPRouteReconciler(
			kubeClient,
			logger,
			s,
			record.NewFakeRecorder(1024),
			config,
			&agentcontrolapi.MockAgentControlAPI{}).
			WithClusterClient(clusterCli)
		// call cluster control service at least once
		clusterCli.EXPECT().
			DeleteCluster(gomock.Any(), gomock.Any()).
			Return(new(clusterctlpb.Cluster), nil).
			MinTimes(1)
		clusterCli.EXPECT().GetCluster(gomock.Any(), gomock.Any()).Return(nil, status.New(grpccodes.NotFound, "not found").Err()).MinTimes(1)
		restult, err := rr.Reconcile(context.TODO(), reconcile.Request{
			NamespacedName: types.NamespacedName{
				Namespace: "foo",
				Name:      "foo",
			},
		})
		t.Log(restult, err)
		require.NoError(t, err)
		require.True(t, restult.IsZero())
		require.True(t, apierrors.IsNotFound(kubeClient.Get(context.TODO(), client.ObjectKeyFromObject(approute), approute)))
		ctrl.Satisfied()
	})
}

func TestAPPRouteReconcilerGCP(t *testing.T) {
	s := runtime.NewScheme()
	require.NoError(t, v1alpha1.AddToScheme(s))
	require.NoError(t, gkev1beta2.SchemeBuilder.AddToScheme(s))
	logger := testr.New(t)
	ctrl := gomock.NewController(t)
	gcpContainerCli := mockgcpcontainer.NewMockClusterGetter(ctrl)
	config := rsconfig.RootConfig{}
	config.ControllerConfig.Provider = "gcp"
	config.ClusterControlServiceEndpoint = "http://localhost"
	defaultAPPBox := new(v1alpha1.APPBox)
	defaultAppRoute := new(v1alpha1.APPRoute)
	defaultVPC := new(v1alpha1.VPC)
	{
		bs, err := testdata.LoadDefaultAPPBoxYAML()
		require.NoError(t, err)
		require.NoError(t, yaml.Unmarshal(bs, defaultAPPBox))
	}
	{
		bs, err := testdata.LoadDefaultAPPRouteYAML()
		require.NoError(t, err)
		require.NoError(t, yaml.Unmarshal(bs, defaultAppRoute))
	}
	{
		bs, err := testdata.LoadDefaultVPCYAML()
		require.NoError(t, err)
		require.NoError(t, yaml.Unmarshal(bs, defaultVPC))
	}

	t.Run("normal create", func(t *testing.T) {
		// normal create should not return any error.
		clusterCli := mockclustercli.NewMockClusterClient(ctrl)
		approute := defaultAppRoute.DeepCopy()
		approute.Status.Phase = v1alpha1.APPRoutePhasePending
		appbox := defaultAPPBox.DeepCopy()
		appbox.Labels["cloud.pingcap.com/region"] = "us-west1"
		appbox.Spec.Provider = "gke"
		appbox.Spec.Region = "us-west1"
		appbox.Status.Phase = v1alpha1.APPBoxPhaseActive
		gke := new(gkev1beta2.Cluster)
		gke.Name = "foo"
		gke.Spec.ForProvider.Locations = []string{"us-west1-a", "us-west1-b", "us-west1-c"}
		gke.Status.AtProvider.Endpoint = "127.0.0.1"
		gke.Status.AtProvider.Location = "us-west1"
		gke.Status.AtProvider.NetworkConfig = &gkev1beta2.NetworkConfigStatus{
			Network: "vpc-foo",
		}
		xpmeta.SetExternalName(gke, "foo")
		vpc := defaultVPC.DeepCopy()
		kubeClient := fake.NewClientBuilder().WithScheme(s).WithStatusSubresource(approute, appbox, gke, vpc).WithObjects(approute, appbox, gke, vpc).Build()
		{
			gcpContainerCli.EXPECT().GetCluster(
				gomock.Any(),
				gomock.Cond(func(arg any) bool {
					req, ok := arg.(*containerpb.GetClusterRequest)
					if !ok {
						return false
					}
					if req.GetName() != "projects/foo/locations/us-west1/clusters/foo" {
						return false
					}
					return true
				}),
			).Return(&containerpb.Cluster{
				Name: "projects/foo/locations/us-west1/clusters/foo",
				MasterAuth: &containerpb.MasterAuth{
					ClientCertificate: "Y2EtZGF0YQ==",
				},
			}, nil).Times(1)
			clusterCli.EXPECT().CreateCluster(
				gomock.Any(),
				gomock.Any()).
				Return(new(clusterctlpb.Cluster), nil).
				MinTimes(1)
			rr := NewAPPRouteReconciler(
				kubeClient,
				logger,
				s,
				record.NewFakeRecorder(1024),
				config,
				&agentcontrolapi.MockAgentControlAPI{}).
				WithClusterClient(clusterCli).
				WithGCPContainerClient(gcpContainerCli)
			restult, err := rr.Reconcile(context.TODO(), reconcile.Request{
				NamespacedName: types.NamespacedName{
					Namespace: "foo",
					Name:      "foo",
				},
			})
			require.NoError(t, err)
			require.True(t, restult.IsZero())
			require.NoError(t, kubeClient.Get(context.TODO(), client.ObjectKeyFromObject(approute), approute))
			require.Equal(t, v1alpha1.APPRoutePhaseActive, approute.Status.Phase)
			ctrl.Satisfied()
		}
	})
	t.Run("normal delete", func(t *testing.T) {
		// normal delete should not return any error.
		clusterCli := mockclustercli.NewMockClusterClient(ctrl)
		approute := defaultAppRoute.DeepCopy()
		deleteTime := metav1.Now()
		approute.SetDeletionTimestamp(&deleteTime)
		appbox := defaultAPPBox.DeepCopy()
		appbox.Labels["cloud.pingcap.com/region"] = "us-west1"
		appbox.Spec.Provider = "gke"
		appbox.Spec.Region = "us-west1"
		appbox.Status.Phase = v1alpha1.APPBoxPhaseActive
		gke := new(gkev1beta2.Cluster)
		gke.Name = "foo"
		gke.Spec.ForProvider.Locations = []string{"us-west1-a", "us-west1-b", "us-west1-c"}
		gke.Status.AtProvider.Endpoint = "127.0.0.1"
		gke.Status.AtProvider.Location = "us-west1"
		gke.Status.AtProvider.NetworkConfig = &gkev1beta2.NetworkConfigStatus{
			Network: "vpc-foo",
		}
		xpmeta.SetExternalName(gke, "foo")
		vpc := defaultVPC.DeepCopy()
		kubeClient := fake.NewClientBuilder().WithScheme(s).WithObjects(
			approute,
			appbox,
			gke,
			vpc).WithStatusSubresource(approute, appbox, gke, vpc).Build()
		{
			clusterCli.EXPECT().DeleteCluster(
				gomock.Any(),
				gomock.Any()).
				Return(new(clusterctlpb.Cluster), nil).
				MinTimes(1)
			clusterCli.EXPECT().GetCluster(gomock.Any(), gomock.Any()).Return(nil, status.New(grpccodes.NotFound, "not found").Err()).MinTimes(1)
			rr := NewAPPRouteReconciler(
				kubeClient,
				logger,
				s,
				record.NewFakeRecorder(1024),
				config,
				&agentcontrolapi.MockAgentControlAPI{}).
				WithClusterClient(clusterCli).
				WithGCPContainerClient(gcpContainerCli)
			restult, err := rr.Reconcile(context.TODO(), reconcile.Request{
				NamespacedName: types.NamespacedName{
					Namespace: "foo",
					Name:      "foo",
				},
			})
			t.Log(restult, err)
			require.NoError(t, err)
			require.True(t, restult.IsZero())
			require.True(t, apierrors.IsNotFound(kubeClient.Get(context.TODO(), client.ObjectKeyFromObject(approute), approute)))
			ctrl.Satisfied()
		}
	})
	t.Run("normal delete with appbox deleted", func(t *testing.T) {
		// normal delete with appbox deleted should not return any error.
		clusterCli := mockclustercli.NewMockClusterClient(ctrl)
		approute := defaultAppRoute.DeepCopy()
		deleteTime := metav1.Now()
		approute.SetDeletionTimestamp(&deleteTime)
		gke := new(gkev1beta2.Cluster)
		gke.Name = "foo"
		gke.Spec.ForProvider.Locations = []string{"us-west1-a", "us-west1-b", "us-west1-c"}
		gke.Status.AtProvider.Endpoint = "127.0.0.1"
		gke.Status.AtProvider.Location = "us-west1"
		gke.Status.AtProvider.NetworkConfig = &gkev1beta2.NetworkConfigStatus{
			Network: "vpc-foo",
		}
		xpmeta.SetExternalName(gke, "foo")
		vpc := defaultVPC.DeepCopy()
		kubeClient := fake.NewClientBuilder().WithScheme(s).WithObjects(
			approute,
			gke,
			vpc).WithStatusSubresource(approute, gke, vpc).Build()
		{
			clusterCli.EXPECT().DeleteCluster(
				gomock.Any(),
				gomock.Any()).
				Return(new(clusterctlpb.Cluster), nil).
				MinTimes(1)
			clusterCli.EXPECT().GetCluster(gomock.Any(), gomock.Any()).Return(nil, status.New(grpccodes.NotFound, "not found").Err()).MinTimes(1)
			rr := NewAPPRouteReconciler(
				kubeClient,
				logger,
				s,
				record.NewFakeRecorder(1024),
				config,
				&agentcontrolapi.MockAgentControlAPI{}).
				WithClusterClient(clusterCli).
				WithGCPContainerClient(gcpContainerCli)
			restult, err := rr.Reconcile(context.TODO(), reconcile.Request{
				NamespacedName: types.NamespacedName{
					Namespace: "foo",
					Name:      "foo",
				},
			})
			require.NoError(t, err)
			require.True(t, restult.IsZero())
			require.True(t, apierrors.IsNotFound(kubeClient.Get(context.TODO(), client.ObjectKeyFromObject(approute), approute)))
			ctrl.Satisfied()
		}
	})
}

func Test_isNotReadyAPPRoute(t *testing.T) {
	cases := []struct {
		name     string
		approute *v1alpha1.APPRoute
		expected bool
	}{
		{
			name: "not ready",
			approute: &v1alpha1.APPRoute{
				Status: v1alpha1.APPRouteStatus{
					Phase: v1alpha1.APPRoutePhasePending,
				},
			},
			expected: true,
		},
		{
			name: "ready",
			approute: &v1alpha1.APPRoute{
				Status: v1alpha1.APPRouteStatus{
					Phase: v1alpha1.APPRoutePhaseActive,
				},
			},
			expected: false,
		},
		{
			name: "deleting",
			approute: &v1alpha1.APPRoute{
				Status: v1alpha1.APPRouteStatus{
					Phase: v1alpha1.APPRoutePhaseDeleting,
				},
			},
			expected: false,
		},
		{
			name: "empty",
			approute: &v1alpha1.APPRoute{
				Status: v1alpha1.APPRouteStatus{
					Phase: "",
				},
			},
			expected: true,
		},
		{
			name: "pending",
			approute: &v1alpha1.APPRoute{
				Status: v1alpha1.APPRouteStatus{
					Phase: v1alpha1.APPRoutePhasePending,
				},
			},
			expected: true,
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			require.Equal(t, tc.expected, isNotReadyAPPRoute(tc.approute))
		})
	}
}
