package dataplane

import (
	"context"
	"testing"

	ackv1alpha1 "github.com/crossplane-contrib/provider-upjet-alibabacloud/apis/ack/v1alpha1"
	"github.com/stretchr/testify/assert"
	ec2v1beta1 "github.com/upbound/provider-aws/apis/ec2/v1beta1"
	eksv1beta1 "github.com/upbound/provider-aws/apis/eks/v1beta1"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	k8sscheme "k8s.io/client-go/kubernetes/scheme"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"

	dmpbv1 "github.com/tidbcloud/cluster-service-ng/api/tidbcloud/dataplane-manager/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/pkg/consts"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/repository"
)

func init() {
	_ = v1alpha1.AddToScheme(k8sscheme.Scheme)
	_ = eksv1beta1.SchemeBuilder.AddToScheme(k8sscheme.Scheme)
	_ = ec2v1beta1.SchemeBuilder.AddToScheme(k8sscheme.Scheme)
	_ = ackv1alpha1.SchemeBuilder.AddToScheme(k8sscheme.Scheme)
}

func TestDataPlaneService_Protovalidate_InvalidRequestsForK8sUpgrade(t *testing.T) {
	scheme := runtime.NewScheme()
	_ = k8sscheme.AddToScheme(scheme)
	_ = v1alpha1.AddToScheme(scheme)

	srv := newTestDataPlaneGRPCServer(t)
	client := newTestDataPlaneGRPCClient(t, srv)

	t.Run("empty version in ListK8SInformationRequest", func(t *testing.T) {
		req := &dmpbv1.ListK8SInformationRequest{
			Version: "",
		}
		resp, err := client.ListK8SInformation(context.Background(), req)
		assert.Error(t, err)
		assert.Nil(t, resp)
		assert.Contains(t, err.Error(), "version")
		assert.Equal(t, codes.Unknown, status.Code(err))
	})

	t.Run("empty data_plane_id in GetK8SInformationRequest", func(t *testing.T) {
		req := &dmpbv1.GetK8SInformationRequest{
			DataPlaneId: "",
		}
		resp, err := client.GetK8SInformation(context.Background(), req)
		assert.Error(t, err)
		assert.Nil(t, resp)
		assert.Contains(t, err.Error(), "data_plane_id")
		assert.Equal(t, codes.Unknown, status.Code(err))
	})

	t.Run("empty data_plane_id in UpgradeDataPlaneRequest", func(t *testing.T) {
		req := &dmpbv1.UpgradeDataPlaneRequest{
			DataPlaneId: "",
			K8SVersion:  "1.34",
		}
		resp, err := client.UpgradeDataPlane(context.Background(), req)
		assert.Error(t, err)
		assert.Nil(t, resp)
		assert.Contains(t, err.Error(), "data_plane_id")
		assert.Equal(t, codes.InvalidArgument, status.Code(err))
	})

	t.Run("empty k8s_version in UpgradeDataPlaneRequest", func(t *testing.T) {
		req := &dmpbv1.UpgradeDataPlaneRequest{
			DataPlaneId: "dp-1",
			K8SVersion:  "",
		}
		resp, err := client.UpgradeDataPlane(context.Background(), req)
		assert.Error(t, err)
		assert.Nil(t, resp)
		assert.Contains(t, err.Error(), "k8s_version")
		assert.Equal(t, codes.InvalidArgument, status.Code(err))
	})
}

func TestDataPlaneService_ListK8SInformation(t *testing.T) {
	scheme := runtime.NewScheme()
	_ = k8sscheme.AddToScheme(scheme)
	_ = v1alpha1.AddToScheme(scheme)
	_ = eksv1beta1.AddToScheme(scheme)

	t.Run("list k8s information with matching version", func(t *testing.T) {
		mockClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(
			&v1alpha1.APPBox{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "appbox-1",
					Namespace: "ns",
					Labels: map[string]string{
						consts.TenantLabelKey:     "tenant-1",
						consts.SharedPoolLabelKey: "pool-1",
						v1alpha1.DataPlaneIDKey:   "dp-1",
					},
				},
				Spec: v1alpha1.APPBoxSpec{
					Provider: consts.ProviderTypeEKS,
					Region:   "us-west-2",
					KubernetesConfig: v1alpha1.KubernetesConfig{
						Version: "1.34",
					},
				},
				Status: v1alpha1.APPBoxStatus{
					KubernetesClusterName: "k8s-cluster-1",
				},
			},
			&eksv1beta1.Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Name: "k8s-cluster-1",
				},
				Status: eksv1beta1.ClusterStatus{
					AtProvider: eksv1beta1.ClusterObservation{
						Version: pointer.String("1.34"),
					},
				},
			},
			&eksv1beta1.Addon{
				ObjectMeta: metav1.ObjectMeta{
					Name: "coredns",
					Labels: map[string]string{
						v1alpha1.DataPlaneIDKey: "dp-1",
					},
				},
				Spec: eksv1beta1.AddonSpec{
					ForProvider: eksv1beta1.AddonParameters{
						AddonName: pointer.String("coredns"),
					},
				},
				Status: eksv1beta1.AddonStatus{
					AtProvider: eksv1beta1.AddonObservation{
						AddonVersion: pointer.String("v1.12.1-eksbuild.2"),
					},
				},
			},
			&eksv1beta1.Addon{
				ObjectMeta: metav1.ObjectMeta{
					Name: "vpc-cni",
					Labels: map[string]string{
						v1alpha1.DataPlaneIDKey: "dp-1",
					},
				},
				Spec: eksv1beta1.AddonSpec{
					ForProvider: eksv1beta1.AddonParameters{
						AddonName: pointer.String("vpc-cni"),
					},
				},
				Status: eksv1beta1.AddonStatus{
					AtProvider: eksv1beta1.AddonObservation{
						AddonVersion: pointer.String("v1.19.5-eksbuild.1"),
					},
				},
			},
			&eksv1beta1.Addon{
				ObjectMeta: metav1.ObjectMeta{
					Name: "kube-proxy",
					Labels: map[string]string{
						"tidbcloud-namespace": "ns",
						"tidbcloud-appbox":    "appbox-1",
					},
				},
				Spec: eksv1beta1.AddonSpec{
					ForProvider: eksv1beta1.AddonParameters{
						AddonName: pointer.String("kube-proxy"),
					},
				},
				Status: eksv1beta1.AddonStatus{
					AtProvider: eksv1beta1.AddonObservation{
						AddonVersion: pointer.String("v1.33.0-eksbuild.2"),
					},
				},
			},
			&eksv1beta1.Addon{
				ObjectMeta: metav1.ObjectMeta{
					Name: "aws-ebs-csi-driver",
					Labels: map[string]string{
						"tidbcloud-namespace": "ns",
						"tidbcloud-appbox":    "appbox-1",
					},
				},
				Spec: eksv1beta1.AddonSpec{
					ForProvider: eksv1beta1.AddonParameters{
						AddonName: pointer.String("aws-ebs-csi-driver"),
					},
				},
				Status: eksv1beta1.AddonStatus{
					AtProvider: eksv1beta1.AddonObservation{
						AddonVersion: pointer.String("v1.48.0-eksbuild.2"),
					},
				},
			},
			&v1alpha1.APPBox{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "appbox-2",
					Namespace: "ns",
					Labels: map[string]string{
						consts.TenantLabelKey:     "tenant-2",
						consts.SharedPoolLabelKey: "pool-2",
						v1alpha1.DataPlaneIDKey:   "dp-2",
					},
				},
				Spec: v1alpha1.APPBoxSpec{
					Provider: consts.ProviderTypeEKS,
					Region:   "us-east-1",
					KubernetesConfig: v1alpha1.KubernetesConfig{
						Version: "1.33", // different version, should be filtered out
					},
				},
				Status: v1alpha1.APPBoxStatus{
					KubernetesClusterName: "k8s-cluster-2",
				},
			},
			&v1alpha1.DataPlane{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "dp-1",
					Namespace: "ns",
				},
				Spec: v1alpha1.DataPlaneSpec{
					AccountRef: v1alpha1.AccountReference{
						AccountID: "acc-1",
					},
				},
				Status: v1alpha1.DataPlaneStatus{
					AppBox: &v1alpha1.Reference{
						Name:      "appbox-1",
						Namespace: "ns",
					},
				},
			},
		).Build()

		dps := NewDataPlaneService(
			mockClient,
			repository.NewDataPlaneReaderWriter(mockClient),
			nil, nil, repository.NewAPPBoxReaderWriter(mockClient),
			nil,
			rsconfig.RootConfig{
				Namespace:          "ns",
				ResourceNamePrefix: "tidbcloud",
				ControllerConfig: rsconfig.ControllerConfig{
					RegionConfig: rsconfig.RegionConfig{
						Name:      "us-west-2",
						Zones:     []string{"us-west-2a", "us-west-2b", "us-west-2c"},
						NodeZones: []string{"us-west-2a", "us-west-2b"},
					},
					APPBoxConfig: rsconfig.APPBoxConfig{
						NodePools: []rsconfig.NodePoolConfig{
							{InstanceType: "m6i.large"},
							{InstanceType: "m6i.xlarge"},
						},
					},
				},
			},
			rsconfig.TemplatesConfig{},
		)

		resp, err := dps.ListK8SInformation(context.Background(), &dmpbv1.ListK8SInformationRequest{
			Version: "1.34",
		})

		assert.NoError(t, err)
		assert.NotNil(t, resp)
		assert.Equal(t, uint32(1), resp.Total)
		assert.Len(t, resp.K8SInfos, 1)
		assert.Equal(t, "k8s-cluster-1", resp.K8SInfos[0].ControlPlane.Name)
		assert.Equal(t, "1.34", resp.K8SInfos[0].ControlPlane.Version)
		assert.Equal(t, "1.34", resp.K8SInfos[0].TargetVersion)
		assert.Equal(t, "aws", resp.K8SInfos[0].CloudProvider)
		assert.Equal(t, "us-west-2", resp.K8SInfos[0].Region)
		assert.Equal(t, "tenant-1", resp.K8SInfos[0].TenantId)
		assert.Equal(t, "pool-1", resp.K8SInfos[0].ResourcePoolId)
		assert.Equal(t, "appbox-1", resp.K8SInfos[0].AppboxName)
		assert.Equal(t, "dp-1", resp.K8SInfos[0].DataPlaneId)
		assert.Equal(t, "acc-1", resp.K8SInfos[0].CloudAccount)
		assert.Equal(t, uint32(3), resp.K8SInfos[0].MaxSkewVersion) // EKS default is 3
		assert.Empty(t, resp.K8SInfos[0].NodePools)                 // should not fill node groups in List API
		assert.Empty(t, resp.K8SInfos[0].Addons)                    // should not fill addons in List API (getDetails=false)
	})

	t.Run("list k8s information with no matching version", func(t *testing.T) {
		mockClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(
			&v1alpha1.APPBox{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "appbox-1",
					Namespace: "ns",
				},
				Spec: v1alpha1.APPBoxSpec{
					KubernetesConfig: v1alpha1.KubernetesConfig{
						Version: "1.33",
					},
				},
			},
		).Build()

		dps := NewDataPlaneService(
			mockClient,
			repository.NewDataPlaneReaderWriter(mockClient),
			nil, nil, repository.NewAPPBoxReaderWriter(mockClient),
			nil,
			rsconfig.RootConfig{
				Namespace:          "ns",
				ResourceNamePrefix: "tidbcloud",
				ControllerConfig: rsconfig.ControllerConfig{
					RegionConfig: rsconfig.RegionConfig{
						Name:      "us-west-2",
						Zones:     []string{"us-west-2a", "us-west-2b", "us-west-2c"},
						NodeZones: []string{"us-west-2a", "us-west-2b"},
					},
					APPBoxConfig: rsconfig.APPBoxConfig{
						NodePools: []rsconfig.NodePoolConfig{
							{InstanceType: "m6i.large"},
							{InstanceType: "m6i.xlarge"},
						},
					},
				},
			},
			rsconfig.TemplatesConfig{},
		)

		resp, err := dps.ListK8SInformation(context.Background(), &dmpbv1.ListK8SInformationRequest{
			Version: "1.34",
		})

		assert.NoError(t, err)
		assert.NotNil(t, resp)
		assert.Equal(t, uint32(0), resp.Total)
		assert.Empty(t, resp.K8SInfos)
	})
}

func TestDataPlaneService_GetK8SInformation_EKS(t *testing.T) {
	scheme := runtime.NewScheme()
	_ = k8sscheme.AddToScheme(scheme)
	_ = v1alpha1.AddToScheme(scheme)
	_ = eksv1beta1.AddToScheme(scheme)

	t.Run("get k8s information for EKS cluster", func(t *testing.T) {
		mockClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(
			&v1alpha1.APPBox{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "appbox-1",
					Namespace: "ns",
					Labels: map[string]string{
						v1alpha1.DataPlaneIDKey: "dp-1",
					},
				},
				Spec: v1alpha1.APPBoxSpec{
					Provider: consts.ProviderTypeEKS,
					KubernetesConfig: v1alpha1.KubernetesConfig{
						Version:         "1.34",
						AddonTemplateID: "test-addon-template",
					},
				},
				Status: v1alpha1.APPBoxStatus{
					KubernetesClusterName: "eks-cluster-1",
				},
			},
			&v1alpha1.DataPlane{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "dp-1",
					Namespace: "ns",
					Labels: map[string]string{
						v1alpha1.DataPlaneIDKey: "dp-1",
					},
				},
				Spec: v1alpha1.DataPlaneSpec{
					AccountRef: v1alpha1.AccountReference{
						AccountID: "acc-1",
					},
				},
				Status: v1alpha1.DataPlaneStatus{
					AppBox: &v1alpha1.Reference{
						Name:      "appbox-1",
						Namespace: "ns",
					},
				},
			},
			&eksv1beta1.Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Name: "eks-cluster-1",
				},
				Status: eksv1beta1.ClusterStatus{
					AtProvider: eksv1beta1.ClusterObservation{
						Version: pointer.String("1.34"),
					},
				},
			},
			&eksv1beta1.NodeGroup{
				ObjectMeta: metav1.ObjectMeta{
					Name: "worker-ng-1",
					Labels: map[string]string{
						v1alpha1.DataPlaneIDKey: "dp-1",
					},
				},
				Status: eksv1beta1.NodeGroupStatus{
					AtProvider: eksv1beta1.NodeGroupObservation{
						Version: pointer.String("1.34"),
						ScalingConfig: []eksv1beta1.ScalingConfigObservation{
							{
								DesiredSize: pointer.Float64(2),
							},
						},
					},
				},
			},
			&eksv1beta1.Addon{
				ObjectMeta: metav1.ObjectMeta{
					Name: "coredns",
					Labels: map[string]string{
						"tidbcloud-namespace": "ns",
						"tidbcloud-appbox":    "appbox-1",
					},
				},
				Spec: eksv1beta1.AddonSpec{
					ForProvider: eksv1beta1.AddonParameters{
						AddonName: pointer.String("coredns"),
					},
				},
				Status: eksv1beta1.AddonStatus{
					AtProvider: eksv1beta1.AddonObservation{
						AddonVersion: pointer.String("v1.12.1-eksbuild.2"),
					},
				},
			},
			&eksv1beta1.Addon{
				ObjectMeta: metav1.ObjectMeta{
					Name: "vpc-cni",
					Labels: map[string]string{
						"tidbcloud-namespace": "ns",
						"tidbcloud-appbox":    "appbox-1",
					},
				},
				Spec: eksv1beta1.AddonSpec{
					ForProvider: eksv1beta1.AddonParameters{
						AddonName: pointer.String("vpc-cni"),
					},
				},
				Status: eksv1beta1.AddonStatus{
					AtProvider: eksv1beta1.AddonObservation{
						AddonVersion: pointer.String("v1.19.5-eksbuild.1"),
					},
				},
			},
		).Build()

		dps := NewDataPlaneService(
			mockClient,
			repository.NewDataPlaneReaderWriter(mockClient),
			nil, nil, repository.NewAPPBoxReaderWriter(mockClient),
			nil,
			rsconfig.RootConfig{
				Namespace:          "ns",
				ResourceNamePrefix: "tidbcloud",
				ControllerConfig: rsconfig.ControllerConfig{
					RegionConfig: rsconfig.RegionConfig{
						Name:      "us-west-2",
						Zones:     []string{"us-west-2a", "us-west-2b", "us-west-2c"},
						NodeZones: []string{"us-west-2a", "us-west-2b"},
					},
					APPBoxConfig: rsconfig.APPBoxConfig{
						NodePools: []rsconfig.NodePoolConfig{
							{InstanceType: "m6i.large"},
							{InstanceType: "m6i.xlarge"},
						},
					},
				},
			},
			rsconfig.TemplatesConfig{
				Addons: []rsconfig.AddonTemplateConfig{
					{
						TemplateId: "test-addon-template",
						Default:    true,
						EKS: map[string][]rsconfig.AddonConfig{
							"1.34": {},
						},
					},
				},
			},
		)

		resp, err := dps.GetK8SInformation(context.Background(), &dmpbv1.GetK8SInformationRequest{
			DataPlaneId: "dp-1",
		})

		assert.NoError(t, err)
		assert.NotNil(t, resp)
		assert.Equal(t, "eks-cluster-1", resp.ControlPlane.Name)
		assert.Equal(t, "1.34", resp.ControlPlane.Version)
		assert.Equal(t, "1.34", resp.TargetVersion)
		assert.Equal(t, "appbox-1", resp.AppboxName)
		assert.Equal(t, "dp-1", resp.DataPlaneId)
		assert.Equal(t, uint32(3), resp.MaxSkewVersion) // EKS max skew version is 3
		assert.Len(t, resp.NodePools, 1)
		assert.Equal(t, dmpbv1.K8SNodePool_WORKER_NODES, resp.NodePools[0].Kind)
		assert.Equal(t, "1.34", resp.NodePools[0].Version)
		assert.Equal(t, uint32(2), resp.NodePools[0].Count)
		// Verify addons (sorted by name)
		assert.Len(t, resp.Addons, 2)
		assert.Equal(t, "coredns", resp.Addons[0].Name)
		assert.Equal(t, "v1.12.1-eksbuild.2", resp.Addons[0].Version)
		assert.Equal(t, "vpc-cni", resp.Addons[1].Name)
		assert.Equal(t, "v1.19.5-eksbuild.1", resp.Addons[1].Version)
	})

	t.Run("get k8s information for EKS cluster without dataplane", func(t *testing.T) {
		mockClient := fake.NewClientBuilder().WithScheme(scheme).Build()

		dps := NewDataPlaneService(
			mockClient,
			repository.NewDataPlaneReaderWriter(mockClient),
			nil, nil, repository.NewAPPBoxReaderWriter(mockClient),
			nil,
			rsconfig.RootConfig{
				Namespace:          "ns",
				ResourceNamePrefix: "tidbcloud",
				ControllerConfig: rsconfig.ControllerConfig{
					RegionConfig: rsconfig.RegionConfig{
						Name:      "us-west-2",
						Zones:     []string{"us-west-2a", "us-west-2b", "us-west-2c"},
						NodeZones: []string{"us-west-2a", "us-west-2b"},
					},
					APPBoxConfig: rsconfig.APPBoxConfig{
						NodePools: []rsconfig.NodePoolConfig{
							{InstanceType: "m6i.large"},
							{InstanceType: "m6i.xlarge"},
						},
					},
				},
			},
			rsconfig.TemplatesConfig{},
		)

		resp, err := dps.GetK8SInformation(context.Background(), &dmpbv1.GetK8SInformationRequest{
			DataPlaneId: "non-existent",
		})

		assert.Error(t, err)
		assert.Nil(t, resp)
		assert.Contains(t, err.Error(), "failed to get dataplane")
	})
}

func TestDataPlaneService_GetK8SInformation_ACK(t *testing.T) {
	scheme := runtime.NewScheme()
	_ = k8sscheme.AddToScheme(scheme)
	_ = v1alpha1.AddToScheme(scheme)
	_ = ackv1alpha1.AddToScheme(scheme)

	t.Run("get k8s information for ACK cluster", func(t *testing.T) {
		mockClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(
			&v1alpha1.APPBox{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "appbox-1",
					Namespace: "ns",
					Labels: map[string]string{
						v1alpha1.DataPlaneIDKey: "dp-1",
					},
				},
				Spec: v1alpha1.APPBoxSpec{
					Provider: consts.ProviderTypeACK,
					KubernetesConfig: v1alpha1.KubernetesConfig{
						Version:         "1.33",
						AddonTemplateID: "test-addon-template",
					},
				},
				Status: v1alpha1.APPBoxStatus{
					KubernetesClusterName: "ack-cluster-1",
				},
			},
			&v1alpha1.DataPlane{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "dp-1",
					Namespace: "ns",
					Labels: map[string]string{
						v1alpha1.DataPlaneIDKey: "dp-1",
					},
				},
				Spec: v1alpha1.DataPlaneSpec{
					AccountRef: v1alpha1.AccountReference{
						AccountID: "acc-1",
					},
				},
				Status: v1alpha1.DataPlaneStatus{
					AppBox: &v1alpha1.Reference{
						Name:      "appbox-1",
						Namespace: "ns",
					},
				},
			},
			&ackv1alpha1.ManagedKubernetes{
				ObjectMeta: metav1.ObjectMeta{
					Name: "ack-cluster-1",
				},
				Status: ackv1alpha1.ManagedKubernetesStatus{
					AtProvider: ackv1alpha1.ManagedKubernetesObservation{
						Version: pointer.String("1.33"),
					},
				},
			},
			&ackv1alpha1.KubernetesNodePool{
				ObjectMeta: metav1.ObjectMeta{
					Name: "worker-np-1",
					Labels: map[string]string{
						v1alpha1.DataPlaneIDKey: "dp-1",
					},
				},
				Status: ackv1alpha1.KubernetesNodePoolStatus{
					AtProvider: ackv1alpha1.KubernetesNodePoolObservation{
						DesiredSize: pointer.String("3"),
					},
				},
			},
			&ackv1alpha1.KubernetesAddon{
				ObjectMeta: metav1.ObjectMeta{
					Name: "coredns",
					Labels: map[string]string{
						"tidbcloud-namespace": "ns",
						"tidbcloud-appbox":    "appbox-1",
					},
				},
				Spec: ackv1alpha1.KubernetesAddonSpec{
					ForProvider: ackv1alpha1.KubernetesAddonParameters{
						Name: pointer.String("coredns"),
					},
				},
				Status: ackv1alpha1.KubernetesAddonStatus{
					AtProvider: ackv1alpha1.KubernetesAddonObservation{
						Version: pointer.String("v1.12.1.3"),
					},
				},
			},
			&ackv1alpha1.KubernetesAddon{
				ObjectMeta: metav1.ObjectMeta{
					Name: "ack-goatscaler",
					Labels: map[string]string{
						"tidbcloud-namespace": "ns",
						"tidbcloud-appbox":    "appbox-1",
					},
				},
				Spec: ackv1alpha1.KubernetesAddonSpec{
					ForProvider: ackv1alpha1.KubernetesAddonParameters{
						Name: pointer.String("ack-goatscaler"),
					},
				},
				Status: ackv1alpha1.KubernetesAddonStatus{
					AtProvider: ackv1alpha1.KubernetesAddonObservation{
						Version: pointer.String("v0.5.1-fd1e3be-aliyun"),
					},
				},
			},
		).Build()

		dps := NewDataPlaneService(
			mockClient,
			repository.NewDataPlaneReaderWriter(mockClient),
			nil, nil, repository.NewAPPBoxReaderWriter(mockClient),
			nil,
			rsconfig.RootConfig{
				Namespace:          "ns",
				ResourceNamePrefix: "tidbcloud",
				ControllerConfig: rsconfig.ControllerConfig{
					RegionConfig: rsconfig.RegionConfig{
						Name:      "us-west-2",
						Zones:     []string{"us-west-2a", "us-west-2b", "us-west-2c"},
						NodeZones: []string{"us-west-2a", "us-west-2b"},
					},
					APPBoxConfig: rsconfig.APPBoxConfig{
						NodePools: []rsconfig.NodePoolConfig{
							{InstanceType: "m6i.large"},
							{InstanceType: "m6i.xlarge"},
						},
					},
				},
			},
			rsconfig.TemplatesConfig{
				Addons: []rsconfig.AddonTemplateConfig{
					{
						TemplateId: "test-addon-template",
						Default:    true,
						ACK: map[string][]rsconfig.AddonConfig{
							"1.33": {},
						},
					},
				},
			},
		)

		resp, err := dps.GetK8SInformation(context.Background(), &dmpbv1.GetK8SInformationRequest{
			DataPlaneId: "dp-1",
		})

		assert.NoError(t, err)
		assert.NotNil(t, resp)
		assert.Equal(t, "ack-cluster-1", resp.ControlPlane.Name)
		assert.Equal(t, "1.33", resp.ControlPlane.Version)
		assert.Equal(t, "1.33", resp.TargetVersion)
		assert.Equal(t, "alicloud", resp.CloudProvider)
		assert.Equal(t, uint32(1), resp.MaxSkewVersion) // ACK requires step-by-step upgrade
		assert.Len(t, resp.NodePools, 1)
		assert.Equal(t, dmpbv1.K8SNodePool_WORKER_NODES, resp.NodePools[0].Kind)
		assert.Equal(t, uint32(3), resp.NodePools[0].Count)
		// Verify addons (sorted by name)
		assert.Len(t, resp.Addons, 2)
		assert.Equal(t, "ack-goatscaler", resp.Addons[0].Name)
		assert.Equal(t, "v0.5.1-fd1e3be-aliyun", resp.Addons[0].Version)
		assert.Equal(t, "coredns", resp.Addons[1].Name)
		assert.Equal(t, "v1.12.1.3", resp.Addons[1].Version)
	})
}

func TestDataPlaneService_AppBox2K8sInfo(t *testing.T) {
	scheme := runtime.NewScheme()
	_ = k8sscheme.AddToScheme(scheme)
	_ = v1alpha1.AddToScheme(scheme)
	_ = eksv1beta1.AddToScheme(scheme)

	t.Run("convert appbox to k8s info without nodes info", func(t *testing.T) {
		appBox := &v1alpha1.APPBox{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "appbox-1",
				Namespace: "ns",
				Labels: map[string]string{
					consts.TenantLabelKey:     "tenant-1",
					consts.SharedPoolLabelKey: "pool-1",
					v1alpha1.DataPlaneIDKey:   "dp-1",
				},
			},
			Spec: v1alpha1.APPBoxSpec{
				Provider: consts.ProviderTypeEKS,
				Region:   "us-west-2",
				KubernetesConfig: v1alpha1.KubernetesConfig{
					Version: "1.34",
				},
			},
			Status: v1alpha1.APPBoxStatus{
				KubernetesClusterName: "k8s-cluster-1",
			},
		}

		dataplane := &v1alpha1.DataPlane{
			ObjectMeta: metav1.ObjectMeta{
				Name: "dp-1",
			},
			Spec: v1alpha1.DataPlaneSpec{
				NodeZones: []string{"us-west-2a", "us-west-2b"},
				AccountRef: v1alpha1.AccountReference{
					AccountID: "acc-1",
				},
			},
		}

		mockClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(
			&eksv1beta1.Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Name: "k8s-cluster-1",
				},
				Status: eksv1beta1.ClusterStatus{
					AtProvider: eksv1beta1.ClusterObservation{
						Version: pointer.String("1.34"),
					},
				},
			},
		).Build()

		dps := NewDataPlaneService(
			mockClient,
			repository.NewDataPlaneReaderWriter(mockClient),
			nil, nil, repository.NewAPPBoxReaderWriter(mockClient),
			nil,
			rsconfig.RootConfig{
				Namespace:          "ns",
				ResourceNamePrefix: "tidbcloud",
				ControllerConfig: rsconfig.ControllerConfig{
					RegionConfig: rsconfig.RegionConfig{
						Name:      "us-west-2",
						Zones:     []string{"us-west-2a", "us-west-2b", "us-west-2c"},
						NodeZones: []string{"us-west-2a", "us-west-2b"},
					},
					APPBoxConfig: rsconfig.APPBoxConfig{
						NodePools: []rsconfig.NodePoolConfig{
							{InstanceType: "m6i.large"},
							{InstanceType: "m6i.xlarge"},
						},
					},
				},
			},
			rsconfig.TemplatesConfig{},
		)

		k8sInfo, err := dps.appBox2K8sInfo(context.Background(), appBox, dataplane, false)

		assert.NoError(t, err)
		assert.NotNil(t, k8sInfo)
		assert.Equal(t, "aws", k8sInfo.CloudProvider)
		assert.Equal(t, "us-west-2", k8sInfo.Region)
		assert.Equal(t, "tenant-1", k8sInfo.TenantId)
		assert.Equal(t, "pool-1", k8sInfo.ResourcePoolId)
		assert.Equal(t, "k8s-cluster-1", k8sInfo.ControlPlane.Name)
		assert.Equal(t, "1.34", k8sInfo.ControlPlane.Version)
		assert.Equal(t, "1.34", k8sInfo.TargetVersion)
		assert.Equal(t, "appbox-1", k8sInfo.AppboxName)
		assert.Equal(t, "dp-1", k8sInfo.DataPlaneId)
		assert.Equal(t, "acc-1", k8sInfo.CloudAccount)
		assert.Equal(t, uint32(3), k8sInfo.MaxSkewVersion) // EKS default is 3
		assert.Equal(t, uint32(4), k8sInfo.ExpectedWorkerNodePoolCount)
		assert.Empty(t, k8sInfo.NodePools)
	})

	t.Run("convert appbox to k8s info with EKS nodes info", func(t *testing.T) {
		appBox := &v1alpha1.APPBox{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "appbox-1",
				Namespace: "ns",
				Labels: map[string]string{
					v1alpha1.DataPlaneIDKey: "dp-1",
				},
			},
			Spec: v1alpha1.APPBoxSpec{
				Provider: consts.ProviderTypeEKS,
				KubernetesConfig: v1alpha1.KubernetesConfig{
					Version:         "1.34",
					AddonTemplateID: "test-addon-template",
				},
			},
			Status: v1alpha1.APPBoxStatus{
				KubernetesClusterName: "eks-cluster-1",
			},
		}

		dataplane := &v1alpha1.DataPlane{
			ObjectMeta: metav1.ObjectMeta{
				Name: "dp-1",
			},
			Spec: v1alpha1.DataPlaneSpec{
				AccountRef: v1alpha1.AccountReference{
					AccountID: "acc-1",
				},
			},
		}

		mockClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(
			&eksv1beta1.Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Name: "eks-cluster-1",
				},
				Status: eksv1beta1.ClusterStatus{
					AtProvider: eksv1beta1.ClusterObservation{
						Version: pointer.String("1.34"),
					},
				},
			},
			&eksv1beta1.NodeGroup{
				ObjectMeta: metav1.ObjectMeta{
					Name: "worker-ng-1",
					Labels: map[string]string{
						v1alpha1.DataPlaneIDKey: "dp-1",
					},
				},
				Status: eksv1beta1.NodeGroupStatus{
					AtProvider: eksv1beta1.NodeGroupObservation{
						Version: pointer.String("1.34"),
						ScalingConfig: []eksv1beta1.ScalingConfigObservation{
							{
								DesiredSize: pointer.Float64(2),
							},
						},
					},
				},
			},
		).Build()

		dps := NewDataPlaneService(
			mockClient,
			repository.NewDataPlaneReaderWriter(mockClient),
			nil, nil, repository.NewAPPBoxReaderWriter(mockClient),
			nil,
			rsconfig.RootConfig{
				Namespace:          "ns",
				ResourceNamePrefix: "tidbcloud",
				ControllerConfig: rsconfig.ControllerConfig{
					RegionConfig: rsconfig.RegionConfig{Name: "us-east-1"},
				},
			},
			rsconfig.TemplatesConfig{
				Addons: []rsconfig.AddonTemplateConfig{
					{
						TemplateId: "test-addon-template",
						Default:    true,
						EKS:        map[string][]rsconfig.AddonConfig{"1.34": {}},
					},
				},
			},
		)

		k8sInfo, err := dps.appBox2K8sInfo(context.Background(), appBox, dataplane, true)

		assert.NoError(t, err)
		assert.NotNil(t, k8sInfo)
		assert.Len(t, k8sInfo.NodePools, 1)
		assert.Equal(t, "1.34", k8sInfo.NodePools[0].Version)
		assert.Equal(t, uint32(2), k8sInfo.NodePools[0].Count)
	})

	t.Run("unsupported provider", func(t *testing.T) {
		appBox := &v1alpha1.APPBox{
			Spec: v1alpha1.APPBoxSpec{
				Provider: "unsupported",
			},
		}

		dataplane := &v1alpha1.DataPlane{
			ObjectMeta: metav1.ObjectMeta{
				Name: "dp-1",
			},
			Spec: v1alpha1.DataPlaneSpec{
				AccountRef: v1alpha1.AccountReference{
					AccountID: "acc-1",
				},
			},
		}

		mockClient := fake.NewClientBuilder().WithScheme(scheme).Build()

		dps := NewDataPlaneService(
			mockClient,
			repository.NewDataPlaneReaderWriter(mockClient),
			nil, nil, repository.NewAPPBoxReaderWriter(mockClient),
			nil,
			rsconfig.RootConfig{
				Namespace:          "ns",
				ResourceNamePrefix: "tidbcloud",
				ControllerConfig: rsconfig.ControllerConfig{
					RegionConfig: rsconfig.RegionConfig{Name: "us-east-1"},
				},
			},
			rsconfig.TemplatesConfig{},
		)

		k8sInfo, err := dps.appBox2K8sInfo(context.Background(), appBox, dataplane, true)

		assert.Error(t, err)
		assert.Nil(t, k8sInfo)
		assert.Contains(t, err.Error(), "unsupported provider")
	})
}

func TestDataPlaneService_UpgradeDataPlane(t *testing.T) {
	scheme := runtime.NewScheme()
	_ = k8sscheme.AddToScheme(scheme)
	_ = v1alpha1.AddToScheme(scheme)

	t.Run("upgrade k8s successfully", func(t *testing.T) {
		mockClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(
			&v1alpha1.APPBox{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "appbox-1",
					Namespace: "ns",
				},
				Spec: v1alpha1.APPBoxSpec{
					KubernetesConfig: v1alpha1.KubernetesConfig{
						Version: "1.33",
					},
				},
			},
			&v1alpha1.DataPlane{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "dp-1",
					Namespace: "ns",
					Labels: map[string]string{
						v1alpha1.DataPlaneIDKey: "dp-1",
					},
				},
				Status: v1alpha1.DataPlaneStatus{
					AppBox: &v1alpha1.Reference{
						Name:      "appbox-1",
						Namespace: "ns",
					},
				},
			},
		).Build()

		dps := NewDataPlaneService(
			mockClient,
			repository.NewDataPlaneReaderWriter(mockClient),
			nil, nil, repository.NewAPPBoxReaderWriter(mockClient),
			nil,
			rsconfig.RootConfig{
				Namespace:          "ns",
				ResourceNamePrefix: "tidbcloud",
				ControllerConfig: rsconfig.ControllerConfig{
					RegionConfig: rsconfig.RegionConfig{Name: "us-east-1"},
				},
			},
			rsconfig.TemplatesConfig{},
		)

		resp, err := dps.UpgradeDataPlane(context.Background(), &dmpbv1.UpgradeDataPlaneRequest{
			DataPlaneId: "dp-1",
			K8SVersion:  "1.34",
		})

		assert.NoError(t, err)
		assert.NotNil(t, resp)

		// Verify the appbox was updated
		var updatedAppBox v1alpha1.APPBox
		err = mockClient.Get(context.Background(),
			client.ObjectKey{Namespace: "ns", Name: "appbox-1"},
			&updatedAppBox)
		assert.NoError(t, err)
		assert.Equal(t, "1.34", updatedAppBox.Spec.KubernetesConfig.Version)
	})

	t.Run("upgrade k8s with appbox in different namespace", func(t *testing.T) {
		mockClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(
			&v1alpha1.APPBox{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "appbox-1",
					Namespace: "other-ns",
				},
				Spec: v1alpha1.APPBoxSpec{
					KubernetesConfig: v1alpha1.KubernetesConfig{
						Version: "1.33",
					},
				},
			},
			&v1alpha1.DataPlane{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "dp-1",
					Namespace: "ns",
					Labels: map[string]string{
						v1alpha1.DataPlaneIDKey: "dp-1",
					},
				},
				Status: v1alpha1.DataPlaneStatus{
					AppBox: &v1alpha1.Reference{
						Name:      "appbox-1",
						Namespace: "other-ns",
					},
				},
			},
		).Build()

		dps := NewDataPlaneService(
			mockClient,
			repository.NewDataPlaneReaderWriter(mockClient),
			nil, nil, repository.NewAPPBoxReaderWriter(mockClient),
			nil,
			rsconfig.RootConfig{
				Namespace:          "ns",
				ResourceNamePrefix: "tidbcloud",
				ControllerConfig: rsconfig.ControllerConfig{
					RegionConfig: rsconfig.RegionConfig{Name: "us-east-1"},
				},
			},
			rsconfig.TemplatesConfig{},
		)

		resp, err := dps.UpgradeDataPlane(context.Background(), &dmpbv1.UpgradeDataPlaneRequest{
			DataPlaneId: "dp-1",
			K8SVersion:  "1.34",
		})

		assert.NoError(t, err)
		assert.NotNil(t, resp)
	})

	t.Run("upgrade k8s with non-existent dataplane", func(t *testing.T) {
		mockClient := fake.NewClientBuilder().WithScheme(scheme).Build()

		dps := NewDataPlaneService(
			mockClient,
			repository.NewDataPlaneReaderWriter(mockClient),
			nil, nil, repository.NewAPPBoxReaderWriter(mockClient),
			nil,
			rsconfig.RootConfig{
				Namespace:          "ns",
				ResourceNamePrefix: "tidbcloud",
				ControllerConfig: rsconfig.ControllerConfig{
					RegionConfig: rsconfig.RegionConfig{Name: "us-east-1"},
				},
			},
			rsconfig.TemplatesConfig{},
		)

		resp, err := dps.UpgradeDataPlane(context.Background(), &dmpbv1.UpgradeDataPlaneRequest{
			DataPlaneId: "non-existent",
			K8SVersion:  "1.34",
		})

		assert.Error(t, err)
		assert.Nil(t, resp)
		assert.Contains(t, err.Error(), "failed to get dataplane")
	})
}

func Test_CloudProviders(t *testing.T) {
	assert.Equal(t, "aws", cloudProviders[consts.ProviderTypeEKS])
	assert.Equal(t, "alicloud", cloudProviders[consts.ProviderTypeACK])
	assert.Equal(t, "azure", cloudProviders[consts.ProviderTypeAKS])
	assert.Equal(t, "gcp", cloudProviders[consts.ProviderTypeGKE])
}
