package loadbalancer

import (
	"context"
	"errors"
	"net"
	"testing"

	"buf.build/go/protovalidate"
	protovalidate_middleware "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/protovalidate"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	regionalapiv1 "github.com/tidbcloud/cluster-service-ng/api/tidbcloud/dataplane-manager/v1"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/config"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/consts"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/status"
	"google.golang.org/grpc/test/bufconn"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

type MockLB struct {
	mock.Mock
}

func (mock *MockLB) Create(ctx context.Context, lb *v1alpha1.LoadBalancer) error {
	args := mock.Called(ctx, lb)
	return args.Error(0)
}

func (mock *MockLB) Get(ctx context.Context, key client.ObjectKey, lb *v1alpha1.LoadBalancer) error {
	args := mock.Called(ctx, key, lb)
	return args.Error(0)
}

func (mock *MockLB) Update(ctx context.Context, lb *v1alpha1.LoadBalancer) error {
	args := mock.Called(ctx, lb)
	return args.Error(0)
}

func (mock *MockLB) Delete(ctx context.Context, lb *v1alpha1.LoadBalancer) error {
	args := mock.Called(ctx, lb)
	return args.Error(0)
}

func (mock *MockLB) Patch(ctx context.Context, lb *v1alpha1.LoadBalancer, patch client.Patch) error {
	args := mock.Called(ctx, lb, patch)
	return args.Error(0)
}

func (m *MockLB) List(ctx context.Context, list *v1alpha1.LoadBalancerList, opts ...client.ListOption) error {
	args := m.Called(ctx, list)
	return args.Error(0)
}

func (m *MockLB) UpdateStatus(ctx context.Context, lb *v1alpha1.LoadBalancer) error {
	args := m.Called(ctx, lb)
	return args.Error(0)
}

type MockDP struct {
	mock.Mock
}

func (mock *MockDP) Get(ctx context.Context, key client.ObjectKey, dp *v1alpha1.DataPlane) error {
	args := mock.Called(ctx, key, dp)
	return args.Error(0)
}

func (mock *MockDP) List(ctx context.Context, list *v1alpha1.DataPlaneList, opts ...client.ListOption) error {
	args := mock.Called(ctx, list)
	return args.Error(0)
}

func newReq() *regionalapiv1.CreateLoadBalancerRequest {
	return &regionalapiv1.CreateLoadBalancerRequest{
		Id:          "lb-123",
		DataPlaneId: "dp-1",
		Loadbalancer: &regionalapiv1.LoadBalancer{
			Identity:    "loadbalancer-identity",
			Namespace:   "default",
			Tags:        map[string]string{consts.TenantLabelKey: "tenant-1", "env": "test"},
			Port:        443,
			Selectors:   map[string]string{"app": "foo"},
			IpWhiteList: []string{"1.2.3.4"},
			Type:        regionalapiv1.LoadBalancer_PUBLIC,
		},
	}
}

func newTestGRPCServer(t *testing.T, lb *MockLB, dp *MockDP) *grpc.Server {
	validator, err := protovalidate.New()
	require.NoError(t, err)

	opts := []grpc.ServerOption{
		grpc.ChainUnaryInterceptor(
			protovalidate_middleware.UnaryServerInterceptor(validator),
		),
	}

	srv := grpc.NewServer(opts...)

	svc := &LoadBalancerService{
		loadbalancerrw: lb,
		dataPlaneR:     dp,
		rootCfg: config.RootConfig{
			Namespace: "default",
			ControllerConfig: config.ControllerConfig{
				RegionConfig: config.RegionConfig{
					Name: "us-west-2",
				},
			},
		},
	}

	regionalapiv1.RegisterLoadBalancerServiceServer(srv, svc)

	return srv
}

func newTestGRPCClient(t *testing.T, srv *grpc.Server) regionalapiv1.LoadBalancerServiceClient {
	lis := bufconn.Listen(1024 * 1024)

	go func() {
		_ = srv.Serve(lis)
	}()

	dialer := grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
		return lis.Dial()
	})

	conn, err := grpc.Dial("bufnet", dialer, grpc.WithTransportCredentials(insecure.NewCredentials()))
	require.NoError(t, err)

	return regionalapiv1.NewLoadBalancerServiceClient(conn)
}

func TestLoadBalancerService_Protovalidate_InvalidRequests(t *testing.T) {
	mockLB := &MockLB{}
	mockDP := &MockDP{}

	defaultDP := &v1alpha1.DataPlane{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "dp-1",
			Namespace: "default",
		},
	}

	mockDP.On("Get", mock.Anything, types.NamespacedName{Namespace: "default", Name: "dp-1"}, mock.Anything).
		Run(func(args mock.Arguments) {
			if dpArg, ok := args[2].(*v1alpha1.DataPlane); ok {
				*dpArg = *defaultDP
			}
		}).
		Return(nil)

	mockLB.On("Create", mock.Anything, mock.AnythingOfType("*v1alpha1.LoadBalancer")).
		Return(nil)

	srv := newTestGRPCServer(t, mockLB, mockDP)
	client := newTestGRPCClient(t, srv)

	t.Run("empty DataPlaneId in CreateLoadBalancerRequest", func(t *testing.T) {
		req := &regionalapiv1.CreateLoadBalancerRequest{
			DataPlaneId: "",
			Id:          "lb-123",
			Loadbalancer: &regionalapiv1.LoadBalancer{
				Identity:  "identity",
				Namespace: "default",
			},
		}

		resp, err := client.CreateLoadBalancer(context.Background(), req)

		assert.Nil(t, resp)
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "data_plane_id")
		assert.Equal(t, codes.InvalidArgument, status.Code(err))
	})

	t.Run("empty Id in CreateLoadBalancerRequest", func(t *testing.T) {
		req := &regionalapiv1.CreateLoadBalancerRequest{
			Id:          "",
			DataPlaneId: "dp-1",
			Loadbalancer: &regionalapiv1.LoadBalancer{
				Identity:  "identity",
				Namespace: "default",
			},
		}

		resp, err := client.CreateLoadBalancer(context.Background(), req)

		assert.Error(t, err)
		assert.Nil(t, resp)
		assert.Contains(t, err.Error(), "id")
		assert.Equal(t, codes.InvalidArgument, status.Code(err))
	})

	t.Run("missing Loadbalancer in CreateLoadBalancerRequest", func(t *testing.T) {
		req := &regionalapiv1.CreateLoadBalancerRequest{
			Id:           "lb-123",
			DataPlaneId:  "dp-1",
			Loadbalancer: nil,
		}

		resp, err := client.CreateLoadBalancer(context.Background(), req)

		assert.Error(t, err)
		assert.Nil(t, resp)
		assert.Contains(t, err.Error(), "loadbalancer")
		assert.Equal(t, codes.InvalidArgument, status.Code(err))
	})

	t.Run("empty DataPlaneId in UpdateLoadBalancerRequest", func(t *testing.T) {
		req := &regionalapiv1.UpdateLoadBalancerRequest{
			Id:          "lb-123",
			DataPlaneId: "",
			Loadbalancer: &regionalapiv1.LoadBalancer{
				Identity:  "identity",
				Namespace: "default",
			},
		}

		resp, err := client.UpdateLoadBalancer(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 Id in UpdateLoadBalancerRequest", func(t *testing.T) {
		req := &regionalapiv1.UpdateLoadBalancerRequest{
			Id:          "",
			DataPlaneId: "dp-1",
			Loadbalancer: &regionalapiv1.LoadBalancer{
				Identity:  "identity",
				Namespace: "default",
			},
		}

		resp, err := client.UpdateLoadBalancer(context.Background(), req)

		assert.Error(t, err)
		assert.Nil(t, resp)
		assert.Contains(t, err.Error(), "id")
		assert.Equal(t, codes.InvalidArgument, status.Code(err))
	})

	t.Run("missing Loadbalancer in UpdateLoadBalancerRequest", func(t *testing.T) {
		req := &regionalapiv1.UpdateLoadBalancerRequest{
			Id:           "lb-123",
			DataPlaneId:  "dp-1",
			Loadbalancer: nil,
		}

		resp, err := client.UpdateLoadBalancer(context.Background(), req)

		assert.Error(t, err)
		assert.Nil(t, resp)
		assert.Contains(t, err.Error(), "loadbalancer")
		assert.Equal(t, codes.InvalidArgument, status.Code(err))
	})

	t.Run("empty DataPlaneId in GetLoadBalancerRequest", func(t *testing.T) {
		req := &regionalapiv1.GetLoadBalancerRequest{
			Id:          "lb-123",
			DataPlaneId: "",
		}

		resp, err := client.GetLoadBalancer(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 Id in GetLoadBalancerRequest", func(t *testing.T) {
		req := &regionalapiv1.GetLoadBalancerRequest{
			Id:          "",
			DataPlaneId: "dp-1",
		}

		resp, err := client.GetLoadBalancer(context.Background(), req)

		assert.Error(t, err)
		assert.Nil(t, resp)
		assert.Contains(t, err.Error(), "id")
		assert.Equal(t, codes.InvalidArgument, status.Code(err))
	})

	t.Run("empty DataPlaneId in DeleteLoadBalancerRequest", func(t *testing.T) {
		req := &regionalapiv1.DeleteLoadBalancerRequest{
			Id:          "lb-123",
			DataPlaneId: "",
		}

		resp, err := client.DeleteLoadBalancer(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 Id in DeleteLoadBalancerRequest", func(t *testing.T) {
		req := &regionalapiv1.DeleteLoadBalancerRequest{
			Id:          "",
			DataPlaneId: "dp-1",
		}

		resp, err := client.DeleteLoadBalancer(context.Background(), req)

		assert.Error(t, err)
		assert.Nil(t, resp)
		assert.Contains(t, err.Error(), "id")
		assert.Equal(t, codes.InvalidArgument, status.Code(err))
	})
}

func TestCreateLoadBalancer_Success(t *testing.T) {
	mockLB := &MockLB{}
	mockDP := &MockDP{}
	svc := NewLoadBalancerService(mockLB, mockDP, config.RootConfig{Namespace: "test-ns"})

	req := newReq()
	var created *v1alpha1.LoadBalancer

	mockDP.On("Get",
		mock.Anything,
		mock.MatchedBy(func(key any) bool {
			_, ok := key.(types.NamespacedName)
			return ok
		}),
		mock.AnythingOfType("*v1alpha1.DataPlane"),
	).Run(func(args mock.Arguments) {
		obj := args.Get(2).(*v1alpha1.DataPlane)
		obj.Namespace = "dp-ns"
	}).Return(nil)

	mockLB.On("Create", mock.Anything, mock.AnythingOfType("*v1alpha1.LoadBalancer")).Run(func(args mock.Arguments) {
		created = args.Get(1).(*v1alpha1.LoadBalancer)
	}).Return(nil)

	resp, err := svc.CreateLoadBalancer(context.Background(), req)

	assert.NoError(t, err)
	assert.Equal(t, "lb-123", resp.Id)
	assert.Equal(t, regionalapiv1.LoadBalancer_CREATING, resp.Phase)
	require.NotNil(t, created)
	assert.Nil(t, created.Spec.PreserveClientIP)

	mockLB.AssertExpectations(t)
	mockDP.AssertExpectations(t)
}

func TestCreateLoadBalancer_TenantTagExists(t *testing.T) {
	mockLB := &MockLB{}
	mockDP := &MockDP{}
	svc := NewLoadBalancerService(mockLB, mockDP, config.RootConfig{Namespace: "test-ns"})

	req := newReq()

	mockDP.On("Get", mock.Anything, types.NamespacedName{Namespace: "test-ns", Name: "dp-1"}, mock.Anything).
		Run(func(args mock.Arguments) {
			dp := args.Get(2).(*v1alpha1.DataPlane)
			dp.Namespace = "dp-ns"
		}).
		Return(nil)

	mockLB.On("Create", mock.Anything, mock.AnythingOfType("*v1alpha1.LoadBalancer")).Return(nil)

	resp, err := svc.CreateLoadBalancer(context.Background(), req)

	assert.NoError(t, err)
	assert.NotNil(t, resp)
	assert.Equal(t, "lb-123", resp.Id)
}

func TestCreateLoadBalancer_MissingTenantTag(t *testing.T) {
	mockLB := &MockLB{}
	mockDP := &MockDP{}
	svc := NewLoadBalancerService(mockLB, mockDP, config.RootConfig{Namespace: "test-ns"})

	req := newReq()

	delete(req.Loadbalancer.Tags, consts.TenantLabelKey)

	mockDP.On("Get", mock.Anything, types.NamespacedName{Namespace: "test-ns", Name: "dp-1"}, mock.AnythingOfType("*v1alpha1.DataPlane")).
		Run(func(args mock.Arguments) {
			dp := args.Get(2).(*v1alpha1.DataPlane)
			dp.Namespace = "dp-ns"
		}).
		Return(nil)

	resp, err := svc.CreateLoadBalancer(context.Background(), req)

	assert.Nil(t, resp)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "missing required tag: tags.tidbcloud.com/tenant")
}

func TestCreateLoadBalancer_DPNotFound(t *testing.T) {
	mockLB := &MockLB{}
	mockDP := &MockDP{}
	svc := NewLoadBalancerService(mockLB, mockDP, config.RootConfig{Namespace: "ns"})

	req := newReq()
	mockDP.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("datapalne not found"))

	resp, err := svc.CreateLoadBalancer(context.Background(), req)
	assert.Nil(t, resp)
	assert.ErrorContains(t, err, "failed get dataplane")
}

func TestUpdateLoadBalancer_Success(t *testing.T) {
	mockLB := &MockLB{}
	mockDP := &MockDP{}
	svc := NewLoadBalancerService(mockLB, mockDP, config.RootConfig{Namespace: "ns"})

	req := &regionalapiv1.UpdateLoadBalancerRequest{
		Id: "lb-123",
		Loadbalancer: &regionalapiv1.LoadBalancer{
			Identity:    "loadbalancer-identity",
			Namespace:   "ns",
			Tags:        map[string]string{},
			Port:        80,
			Selectors:   map[string]string{},
			IpWhiteList: []string{},
			Type:        regionalapiv1.LoadBalancer_PRIVATE,
		},
	}
	var updated *v1alpha1.LoadBalancer

	mockLB.On("Get", mock.Anything, types.NamespacedName{Namespace: "ns", Name: "lb-123"}, mock.AnythingOfType("*v1alpha1.LoadBalancer")).
		Return(nil)

	mockLB.On("Update", mock.Anything, mock.AnythingOfType("*v1alpha1.LoadBalancer")).Run(func(args mock.Arguments) {
		updated = args.Get(1).(*v1alpha1.LoadBalancer)
	}).Return(nil)

	resp, err := svc.UpdateLoadBalancer(context.Background(), req)
	assert.NoError(t, err)
	assert.Equal(t, req.Id, resp.Id)
	require.NotNil(t, updated)
	assert.Nil(t, updated.Spec.PreserveClientIP)
}

func TestCreateLoadBalancer_ExplicitPreserveClientIPFalse(t *testing.T) {
	mockLB := &MockLB{}
	mockDP := &MockDP{}
	svc := NewLoadBalancerService(mockLB, mockDP, config.RootConfig{Namespace: "test-ns"})

	req := newReq()
	req.Loadbalancer.PreserveClientIp = ptr.To(false)
	var created *v1alpha1.LoadBalancer

	mockDP.On("Get", mock.Anything, mock.Anything, mock.AnythingOfType("*v1alpha1.DataPlane")).
		Run(func(args mock.Arguments) {
			obj := args.Get(2).(*v1alpha1.DataPlane)
			obj.Namespace = "dp-ns"
		}).Return(nil)
	mockLB.On("Create", mock.Anything, mock.AnythingOfType("*v1alpha1.LoadBalancer")).Run(func(args mock.Arguments) {
		created = args.Get(1).(*v1alpha1.LoadBalancer)
	}).Return(nil)

	resp, err := svc.CreateLoadBalancer(context.Background(), req)
	require.NoError(t, err)
	require.NotNil(t, resp)
	require.NotNil(t, created)
	require.NotNil(t, created.Spec.PreserveClientIP)
	assert.False(t, *created.Spec.PreserveClientIP)
}

// TestLoadBalancer_CreateUpdateGetRoundTrip pins the declarative drift-fix
// path for the multi-port and annotations fields: Create with 1 port, Update
// with 2 ports + caller annotations, Get must see the updated state.
func TestLoadBalancer_CreateUpdateGetRoundTrip(t *testing.T) {
	mockLB := &MockLB{}
	mockDP := &MockDP{}
	svc := NewLoadBalancerService(mockLB, mockDP, config.RootConfig{Namespace: "ns"})

	// Simulate a single in-memory CR shared across Create/Update/Get.
	var stored *v1alpha1.LoadBalancer
	mockDP.On("Get", mock.Anything, mock.Anything, mock.AnythingOfType("*v1alpha1.DataPlane")).
		Run(func(args mock.Arguments) {
			dp := args.Get(2).(*v1alpha1.DataPlane)
			dp.Namespace = "dp-ns"
		}).Return(nil)
	mockLB.On("Create", mock.Anything, mock.AnythingOfType("*v1alpha1.LoadBalancer")).
		Run(func(args mock.Arguments) {
			stored = args.Get(1).(*v1alpha1.LoadBalancer).DeepCopy()
		}).Return(nil)
	mockLB.On("Get", mock.Anything, types.NamespacedName{Namespace: "ns", Name: "lb-rt"}, mock.AnythingOfType("*v1alpha1.LoadBalancer")).
		Run(func(args mock.Arguments) {
			out := args.Get(2).(*v1alpha1.LoadBalancer)
			if stored != nil {
				*out = *stored.DeepCopy()
			}
		}).Return(nil)
	mockLB.On("Update", mock.Anything, mock.AnythingOfType("*v1alpha1.LoadBalancer")).
		Run(func(args mock.Arguments) {
			stored = args.Get(1).(*v1alpha1.LoadBalancer).DeepCopy()
		}).Return(nil)

	createReq := &regionalapiv1.CreateLoadBalancerRequest{
		Id:          "lb-rt",
		DataPlaneId: "dp-1",
		Loadbalancer: &regionalapiv1.LoadBalancer{
			Identity:  "id",
			Namespace: "ns",
			Tags:      map[string]string{consts.TenantLabelKey: "tenant-1"},
			Ports: []*regionalapiv1.LoadBalancer_Port{
				{Name: "cassandra", Port: 9042, TargetPort: 9042, Protocol: "TCP"},
			},
			Type: regionalapiv1.LoadBalancer_PRIVATE,
		},
	}
	_, err := svc.CreateLoadBalancer(context.Background(), createReq)
	require.NoError(t, err)

	updateReq := &regionalapiv1.UpdateLoadBalancerRequest{
		Id:          "lb-rt",
		DataPlaneId: "dp-1",
		Loadbalancer: &regionalapiv1.LoadBalancer{
			Identity:  "id",
			Namespace: "ns",
			Tags:      map[string]string{consts.TenantLabelKey: "tenant-1"},
			Ports: []*regionalapiv1.LoadBalancer_Port{
				{Name: "cassandra", Port: 9042, TargetPort: 9042, Protocol: "TCP"},
				{Name: "cassandra-ssl", Port: 9142, TargetPort: 9142, Protocol: "TCP"},
			},
			Annotations: map[string]string{
				"service.beta.kubernetes.io/aws-load-balancer-healthcheck-path":        "/ready",
				"service.beta.kubernetes.io/aws-load-balancer-healthcheck-port":        "6060",
				"service.beta.kubernetes.io/aws-load-balancer-healthcheck-protocol":    "http",
				"service.beta.kubernetes.io/aws-load-balancer-target-group-attributes": "preserve_client_ip.enabled=true,stickiness.enabled=false",
			},
			Type: regionalapiv1.LoadBalancer_PRIVATE,
		},
	}
	_, err = svc.UpdateLoadBalancer(context.Background(), updateReq)
	require.NoError(t, err)

	getReq := &regionalapiv1.GetLoadBalancerRequest{Id: "lb-rt", DataPlaneId: "dp-1"}
	got, err := svc.GetLoadBalancer(context.Background(), getReq)
	require.NoError(t, err)
	require.NotNil(t, got)

	require.Len(t, got.Ports, 2)
	assert.Equal(t, int32(9042), got.Ports[0].Port)
	assert.Equal(t, int32(9142), got.Ports[1].Port)
	assert.Nil(t, got.PreserveClientIp)
	assert.Equal(t, "/ready", got.Annotations["service.beta.kubernetes.io/aws-load-balancer-healthcheck-path"])
	assert.Equal(t, "6060", got.Annotations["service.beta.kubernetes.io/aws-load-balancer-healthcheck-port"])
	assert.Equal(t, "http", got.Annotations["service.beta.kubernetes.io/aws-load-balancer-healthcheck-protocol"])
	assert.Equal(t, "preserve_client_ip.enabled=true,stickiness.enabled=false",
		got.Annotations["service.beta.kubernetes.io/aws-load-balancer-target-group-attributes"])
}

func TestValidateLoadBalancerPorts(t *testing.T) {
	t.Parallel()

	cases := []struct {
		name    string
		lb      *regionalapiv1.LoadBalancer
		wantErr string
	}{
		{
			name:    "nil loadbalancer",
			lb:      nil,
			wantErr: "loadbalancer is required",
		},
		{
			name:    "both port and ports empty",
			lb:      &regionalapiv1.LoadBalancer{},
			wantErr: "port or ports",
		},
		{
			name: "nil entry rejected",
			lb: &regionalapiv1.LoadBalancer{Ports: []*regionalapiv1.LoadBalancer_Port{
				{Port: 9042, Name: "cassandra"}, nil,
			}},
			wantErr: "ports[1] is nil",
		},
		{
			name: "non-positive port rejected",
			lb: &regionalapiv1.LoadBalancer{Ports: []*regionalapiv1.LoadBalancer_Port{
				{Port: 0, Name: "x"},
			}},
			wantErr: "port must be > 0",
		},
		{
			name: "duplicate port rejected",
			lb: &regionalapiv1.LoadBalancer{Ports: []*regionalapiv1.LoadBalancer_Port{
				{Port: 9042, Name: "a"}, {Port: 9042, Name: "b"},
			}},
			wantErr: "duplicate port 9042",
		},
		{
			name: "multi-port empty name rejected",
			lb: &regionalapiv1.LoadBalancer{Ports: []*regionalapiv1.LoadBalancer_Port{
				{Port: 9042, Name: "cassandra"}, {Port: 9142, Name: ""},
			}},
			wantErr: "name is required for multi-port",
		},
		{
			name: "multi-port duplicate name rejected",
			lb: &regionalapiv1.LoadBalancer{Ports: []*regionalapiv1.LoadBalancer_Port{
				{Port: 9042, Name: "x"}, {Port: 9142, Name: "x"},
			}},
			wantErr: `duplicate name "x"`,
		},
		{
			name: "single-port empty name accepted",
			lb: &regionalapiv1.LoadBalancer{Ports: []*regionalapiv1.LoadBalancer_Port{
				{Port: 9042, Name: ""},
			}},
		},
		{
			name: "multi-port valid",
			lb: &regionalapiv1.LoadBalancer{Ports: []*regionalapiv1.LoadBalancer_Port{
				{Port: 9042, Name: "cassandra"}, {Port: 9142, Name: "cassandra-ssl"},
			}},
		},
		{
			name: "legacy single port accepted",
			lb:   &regionalapiv1.LoadBalancer{Port: 4000},
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			err := validateLoadBalancerPorts(tc.lb)
			if tc.wantErr == "" {
				assert.NoError(t, err)
				return
			}
			require.Error(t, err)
			assert.Contains(t, err.Error(), tc.wantErr)
		})
	}
}

func TestCreateLoadBalancer_RejectsEmptyPortAndPorts(t *testing.T) {
	mockLB := &MockLB{}
	mockDP := &MockDP{}
	svc := NewLoadBalancerService(mockLB, mockDP, config.RootConfig{Namespace: "ns"})

	mockDP.On("Get", mock.Anything, mock.Anything, mock.AnythingOfType("*v1alpha1.DataPlane")).
		Run(func(args mock.Arguments) {
			dp := args.Get(2).(*v1alpha1.DataPlane)
			dp.Namespace = "dp-ns"
		}).Return(nil)

	req := &regionalapiv1.CreateLoadBalancerRequest{
		Id:          "lb-empty",
		DataPlaneId: "dp-1",
		Loadbalancer: &regionalapiv1.LoadBalancer{
			Identity:  "id",
			Namespace: "ns",
			Tags:      map[string]string{consts.TenantLabelKey: "tenant-1"},
			Type:      regionalapiv1.LoadBalancer_PRIVATE,
		},
	}
	resp, err := svc.CreateLoadBalancer(context.Background(), req)
	assert.Nil(t, resp)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "port or ports")
}

func TestGetLoadBalancer_NotFound(t *testing.T) {
	mockLB := &MockLB{}
	mockDP := &MockDP{}
	svc := NewLoadBalancerService(mockLB, mockDP, config.RootConfig{Namespace: "ns"})

	req := &regionalapiv1.GetLoadBalancerRequest{Id: "not-found"}

	mockLB.On("Get", mock.Anything, mock.Anything, mock.Anything).
		Return(client.IgnoreNotFound(errors.New("not found")))
	resp, err := svc.GetLoadBalancer(context.Background(), req)
	assert.Nil(t, resp)
	assert.Error(t, err)
}

func TestDeleteLoadBalancer_Success(t *testing.T) {
	mockLB := &MockLB{}
	mockDP := &MockDP{}
	svc := NewLoadBalancerService(mockLB, mockDP, config.RootConfig{Namespace: "ns"})

	req := &regionalapiv1.DeleteLoadBalancerRequest{Id: "lb-123"}

	mockLB.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(nil)
	mockLB.On("Delete", mock.Anything, mock.AnythingOfType("*v1alpha1.LoadBalancer")).Return(nil)

	resp, err := svc.DeleteLoadBalancer(context.Background(), req)
	assert.NoError(t, err)
	assert.NotNil(t, resp)
}

func TestDeleteLoadBalancer_AlreadyDeleted(t *testing.T) {
	mockLB := &MockLB{}
	mockDP := &MockDP{}
	svc := NewLoadBalancerService(mockLB, mockDP, config.RootConfig{Namespace: "ns"})

	req := &regionalapiv1.DeleteLoadBalancerRequest{Id: "lb-deleted"}

	mockLB.On("Get", mock.Anything, mock.Anything, mock.Anything).
		Return(apierrors.NewNotFound(schema.GroupResource{Group: "infra.tidbcloud.io", Resource: "loadbalancers"}, req.Id))

	resp, err := svc.DeleteLoadBalancer(context.Background(), req)
	assert.NoError(t, err)
	assert.NotNil(t, resp)
}
