package service

import (
	"context"
	"errors"
	"net/http"
	"testing"

	"github.com/golang/mock/gomock"
	"github.com/stretchr/testify/assert"
	cspb "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	regionalpb "github.com/tidbcloud/cluster-service-ng/api/tidbcloud/dataplane-manager/v1"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/tidbcloud/ticdc-service/v1"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	"github.com/tidbcloud/ticdc-service-ng/pkg/config"
	regionalservicemocks "github.com/tidbcloud/ticdc-service-ng/pkg/regionalservice/mocks"
	"github.com/tidbcloud/ticdc-service-ng/pkg/util"
)

func TestDescribeEndpointServiceAvailabilityZones(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	mockRegionalCli := regionalservicemocks.NewMockClient(ctrl)
	mockRegionalCli.EXPECT().GetSharedPoolByID(gomock.Any(), "shared-pool-1").Return(&cspb.SharedPool{
		DataplaneName: "dp-1",
	}, nil)
	mockRegionalCli.EXPECT().DescribePrivateLinkEndpointServiceZones(gomock.Any(), &regionalpb.DescribePrivateLinkEndpointServiceZonesRequest{
		DataPlaneId:   "dp-1",
		ServiceName:   "com.amazonaws.vpce.us-east-1.vpce-svc-123",
		ServiceRegion: "us-east-1",
	}).Return(&regionalpb.DescribePrivateLinkEndpointServiceZonesResponse{
		Zones: []*regionalpb.EndpointServiceAvailabilityZone{
			{AvailabilityZone: "us-east-1a", AvailabilityZoneId: "use1-az1"},
			{AvailabilityZone: "us-east-1c", AvailabilityZoneId: "use1-az2"},
		},
	}, nil)
	server := &sinkEndpointServer{
		cfg:         &config.Config{Provider: config.ProviderAWS, Region: "us-east-1"},
		regionalCli: mockRegionalCli,
	}

	resp, err := server.DescribeEndpointServiceAvailabilityZones(context.Background(), &apiv1.DescribeEndpointServiceAvailabilityZonesRequest{
		SharedPoolId:  "shared-pool-1",
		ServiceName:   "com.amazonaws.vpce.us-east-1.vpce-svc-123",
		ServiceRegion: "us-east-1",
	})
	assert.NoError(t, err)
	assert.Equal(t, []*apiv1.EndpointServiceAvailabilityZone{
		{AvailabilityZone: "us-east-1a", AvailabilityZoneId: "use1-az1"},
		{AvailabilityZone: "us-east-1c", AvailabilityZoneId: "use1-az2"},
	}, resp.Zones)
}

func TestDescribeEndpointServiceAvailabilityZonesForAliCloud(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	mockRegionalCli := regionalservicemocks.NewMockClient(ctrl)
	mockRegionalCli.EXPECT().GetSharedPoolByID(gomock.Any(), "shared-pool-1").Return(&cspb.SharedPool{
		DataplaneName: "dp-1",
	}, nil)
	mockRegionalCli.EXPECT().DescribePrivateLinkEndpointServiceZones(
		gomock.Any(),
		&regionalpb.DescribePrivateLinkEndpointServiceZonesRequest{
			DataPlaneId:   "dp-1",
			ServiceName:   "com.aliyuncs.privatelink.ap-southeast-1.epsrv-123",
			ServiceRegion: "ap-southeast-1",
		},
	).Return(&regionalpb.DescribePrivateLinkEndpointServiceZonesResponse{
		Zones: []*regionalpb.EndpointServiceAvailabilityZone{
			{AvailabilityZone: "ap-southeast-1a", AvailabilityZoneId: "ap-southeast-1a"},
			{AvailabilityZone: "ap-southeast-1c", AvailabilityZoneId: "ap-southeast-1c"},
		},
	}, nil)
	server := &sinkEndpointServer{
		cfg:         &config.Config{Provider: config.ProviderAliCloud, Region: "ap-southeast-1"},
		regionalCli: mockRegionalCli,
	}

	resp, err := server.DescribeEndpointServiceAvailabilityZones(
		context.Background(),
		&apiv1.DescribeEndpointServiceAvailabilityZonesRequest{
			SharedPoolId:  "shared-pool-1",
			ServiceName:   "com.aliyuncs.privatelink.ap-southeast-1.epsrv-123",
			ServiceRegion: "ap-southeast-1",
		},
	)
	assert.NoError(t, err)
	assert.Equal(t, []*apiv1.EndpointServiceAvailabilityZone{
		{AvailabilityZone: "ap-southeast-1a", AvailabilityZoneId: "ap-southeast-1a"},
		{AvailabilityZone: "ap-southeast-1c", AvailabilityZoneId: "ap-southeast-1c"},
	}, resp.Zones)
}

func TestDescribeEndpointServiceAvailabilityZonesRejectsUnsupportedProvider(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	server := &sinkEndpointServer{
		cfg:         &config.Config{Provider: config.ProviderAzure},
		regionalCli: regionalservicemocks.NewMockClient(ctrl),
	}

	_, err := server.DescribeEndpointServiceAvailabilityZones(context.Background(), &apiv1.DescribeEndpointServiceAvailabilityZonesRequest{
		SharedPoolId:  "shared-pool-1",
		ServiceName:   "service",
		ServiceRegion: "us-east-1",
	})
	assert.Error(t, err)
	assert.Equal(t, codes.InvalidArgument, status.Code(err))
}

func TestDescribeEndpointServiceAvailabilityZonesRejectsMissingRequiredFields(t *testing.T) {
	tests := []struct {
		name    string
		request *apiv1.DescribeEndpointServiceAvailabilityZonesRequest
	}{
		{
			name:    "nil request",
			request: nil,
		},
		{
			name: "missing shared pool id",
			request: &apiv1.DescribeEndpointServiceAvailabilityZonesRequest{
				ServiceName:   "service",
				ServiceRegion: "us-east-1",
			},
		},
		{
			name: "missing service name",
			request: &apiv1.DescribeEndpointServiceAvailabilityZonesRequest{
				SharedPoolId:  "shared-pool-1",
				ServiceRegion: "us-east-1",
			},
		},
		{
			name: "missing service region",
			request: &apiv1.DescribeEndpointServiceAvailabilityZonesRequest{
				SharedPoolId: "shared-pool-1",
				ServiceName:  "service",
			},
		},
		{
			name: "blank values",
			request: &apiv1.DescribeEndpointServiceAvailabilityZonesRequest{
				SharedPoolId:  " ",
				ServiceName:   "\t",
				ServiceRegion: "\n",
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()

			server := &sinkEndpointServer{
				cfg:         &config.Config{Provider: config.ProviderAWS, Region: "us-east-1"},
				regionalCli: regionalservicemocks.NewMockClient(ctrl),
			}

			_, err := server.DescribeEndpointServiceAvailabilityZones(context.Background(), tt.request)
			assert.Error(t, err)
			assert.Equal(t, codes.InvalidArgument, status.Code(err))
		})
	}
}

func TestDescribeEndpointServiceAvailabilityZonesRejectsCrossRegion(t *testing.T) {
	for _, tt := range []struct {
		name          string
		provider      string
		region        string
		serviceRegion string
	}{
		{name: "aws", provider: config.ProviderAWS, region: "us-east-1", serviceRegion: "us-west-2"},
		{name: "alicloud", provider: config.ProviderAliCloud, region: "ap-southeast-1", serviceRegion: "cn-hangzhou"},
	} {
		t.Run(tt.name, func(t *testing.T) {
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()
			server := &sinkEndpointServer{
				cfg:         &config.Config{Provider: tt.provider, Region: tt.region},
				regionalCli: regionalservicemocks.NewMockClient(ctrl),
			}

			_, err := server.DescribeEndpointServiceAvailabilityZones(context.Background(), &apiv1.DescribeEndpointServiceAvailabilityZonesRequest{
				SharedPoolId:  "shared-pool-1",
				ServiceName:   "service",
				ServiceRegion: tt.serviceRegion,
			})
			assert.Error(t, err)
			assert.Equal(t, codes.InvalidArgument, status.Code(err))
			assert.Contains(t, err.Error(), "cross-region endpoint service availability zone discovery is not supported")
		})
	}
}

func TestDescribeEndpointServiceAvailabilityZonesPropagatesDPMError(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	mockRegionalCli := regionalservicemocks.NewMockClient(ctrl)
	mockRegionalCli.EXPECT().GetSharedPoolByID(gomock.Any(), "shared-pool-1").Return(&cspb.SharedPool{
		DataplaneName: "dp-1",
	}, nil)
	mockRegionalCli.EXPECT().DescribePrivateLinkEndpointServiceZones(gomock.Any(), gomock.Any()).Return(nil, errors.New("aws error"))
	server := &sinkEndpointServer{
		cfg:         &config.Config{Provider: config.ProviderAWS, Region: "us-east-1"},
		regionalCli: mockRegionalCli,
	}

	_, err := server.DescribeEndpointServiceAvailabilityZones(context.Background(), &apiv1.DescribeEndpointServiceAvailabilityZonesRequest{
		SharedPoolId:  "shared-pool-1",
		ServiceName:   "service",
		ServiceRegion: "us-east-1",
	})
	assert.Error(t, err)
	assert.Equal(t, codes.Internal, status.Code(err))
	assert.Contains(t, err.Error(), "aws error")
}

func TestDescribeEndpointServiceAvailabilityZonesMapsDPMError(t *testing.T) {
	tests := []struct {
		name     string
		err      error
		wantCode codes.Code
	}{
		{
			name:     "bad request maps to invalid argument",
			err:      &util.StatusError{StatusCode: http.StatusBadRequest},
			wantCode: codes.InvalidArgument,
		},
		{
			name:     "not found maps to not found",
			err:      &util.StatusError{StatusCode: http.StatusNotFound},
			wantCode: codes.NotFound,
		},
		{
			name:     "server error maps to unavailable",
			err:      &util.StatusError{StatusCode: http.StatusInternalServerError},
			wantCode: codes.Unavailable,
		},
		{
			name:     "context canceled maps to canceled",
			err:      context.Canceled,
			wantCode: codes.Canceled,
		},
		{
			name:     "context deadline maps to deadline exceeded",
			err:      context.DeadlineExceeded,
			wantCode: codes.DeadlineExceeded,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()

			mockRegionalCli := regionalservicemocks.NewMockClient(ctrl)
			mockRegionalCli.EXPECT().GetSharedPoolByID(gomock.Any(), "shared-pool-1").Return(&cspb.SharedPool{
				DataplaneName: "dp-1",
			}, nil)
			mockRegionalCli.EXPECT().DescribePrivateLinkEndpointServiceZones(gomock.Any(), gomock.Any()).Return(nil, tt.err)
			server := &sinkEndpointServer{
				cfg:         &config.Config{Provider: config.ProviderAWS, Region: "us-east-1"},
				regionalCli: mockRegionalCli,
			}

			_, err := server.DescribeEndpointServiceAvailabilityZones(context.Background(), &apiv1.DescribeEndpointServiceAvailabilityZonesRequest{
				SharedPoolId:  "shared-pool-1",
				ServiceName:   "service",
				ServiceRegion: "us-east-1",
			})
			assert.Error(t, err)
			assert.Equal(t, tt.wantCode, status.Code(err))
		})
	}
}
