package server

import (
	"context"
	"slices"
	"testing"

	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	dplanemgrv1 "github.com/tidbcloud/cluster-service-ng/api/dataplane-manager/v1"
	"github.com/tidbcloud/cluster-service-ng/config"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	dataplanemgrmocks "github.com/tidbcloud/cluster-service-ng/pkg/dataplanemgr/mocks"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

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

	tests := []struct {
		name             string
		highAvailability apiv1.CreateSharedPoolRequest_InitSharedPoolConfig_HighAvailabilityType
		nodeZones        []string
		expected         dplanemgrv1.DataPlane_HighAvailabilityType
	}{
		{
			name:             "regional",
			highAvailability: apiv1.CreateSharedPoolRequest_InitSharedPoolConfig_REGIONAL,
			expected:         dplanemgrv1.DataPlane_REGIONAL,
		},
		{
			name:             "zonal with explicit node zone",
			highAvailability: apiv1.CreateSharedPoolRequest_InitSharedPoolConfig_ZONAL,
			nodeZones:        []string{"us-west-2b"},
			expected:         dplanemgrv1.DataPlane_ZONAL,
		},
		{
			name:             "zonal with configured fallback",
			highAvailability: apiv1.CreateSharedPoolRequest_InitSharedPoolConfig_ZONAL,
			expected:         dplanemgrv1.DataPlane_ZONAL,
		},
		{
			name:             "regional accepts multiple zones",
			highAvailability: apiv1.CreateSharedPoolRequest_InitSharedPoolConfig_REGIONAL,
			nodeZones:        []string{"us-west-2a", "us-west-2b", "us-west-2c"},
			expected:         dplanemgrv1.DataPlane_REGIONAL,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			ctx := context.Background()
			client := dataplanemgrmocks.NewClient(t)
			service := &serverImpl{
				dataplaneClient: client,
				bizCfg:          &config.BizConfig{DefaultDataplaneAccount: " 123456789012 "},
			}
			prefix := int32(20)
			client.EXPECT().EvaluateDataPlaneNetworkCapacity(
				ctx,
				mock.MatchedBy(func(req *dplanemgrv1.EvaluateDataPlaneNetworkCapacityRequest) bool {
					return req.CidrPrefixLength != nil &&
						req.GetCidrPrefixLength() == prefix &&
						req.GetHighAvailabilityType() == tt.expected &&
						req.GetAccountId() == "123456789012" &&
						slices.Equal(req.GetNodeZones(), tt.nodeZones)
				}),
			).Return(&dplanemgrv1.EvaluateDataPlaneNetworkCapacityResponse{
				Supported:          true,
				SupportedNodeCount: 60,
			}, nil).Once()

			response, err := service.EvaluateSharedPoolNetworkCapacity(ctx, &entity.EvaluateSharedPoolNetworkCapacityRequest{
				CidrPrefixLength:     &prefix,
				HighAvailabilityType: tt.highAvailability,
				NodeZones:            tt.nodeZones,
			})

			require.NoError(t, err)
			require.True(t, response.GetSupported())
			require.EqualValues(t, 60, response.GetSupportedNodeCount())
		})
	}
}

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

	minusOne := int32(-1)
	thirtyThree := int32(33)
	twenty := int32(20)
	tests := []struct {
		name    string
		request *entity.EvaluateSharedPoolNetworkCapacityRequest
	}{
		{name: "nil request"},
		{
			name: "missing prefix",
			request: &entity.EvaluateSharedPoolNetworkCapacityRequest{
				HighAvailabilityType: apiv1.CreateSharedPoolRequest_InitSharedPoolConfig_REGIONAL,
			},
		},
		{
			name: "prefix below IPv4 bound",
			request: &entity.EvaluateSharedPoolNetworkCapacityRequest{
				CidrPrefixLength:     &minusOne,
				HighAvailabilityType: apiv1.CreateSharedPoolRequest_InitSharedPoolConfig_REGIONAL,
			},
		},
		{
			name: "prefix above IPv4 bound",
			request: &entity.EvaluateSharedPoolNetworkCapacityRequest{
				CidrPrefixLength:     &thirtyThree,
				HighAvailabilityType: apiv1.CreateSharedPoolRequest_InitSharedPoolConfig_REGIONAL,
			},
		},
		{
			name: "unspecified availability",
			request: &entity.EvaluateSharedPoolNetworkCapacityRequest{
				CidrPrefixLength: &twenty,
			},
		},
		{
			name: "unknown availability",
			request: &entity.EvaluateSharedPoolNetworkCapacityRequest{
				CidrPrefixLength:     &twenty,
				HighAvailabilityType: 99,
			},
		},
		{
			name: "node zone on regional evaluation",
			request: &entity.EvaluateSharedPoolNetworkCapacityRequest{
				CidrPrefixLength:     &twenty,
				HighAvailabilityType: apiv1.CreateSharedPoolRequest_InitSharedPoolConfig_REGIONAL,
				NodeZones:            []string{"us-west-2a"},
			},
		},
		{
			name: "empty node zone on zonal evaluation",
			request: &entity.EvaluateSharedPoolNetworkCapacityRequest{
				CidrPrefixLength:     &twenty,
				HighAvailabilityType: apiv1.CreateSharedPoolRequest_InitSharedPoolConfig_ZONAL,
				NodeZones:            []string{""},
			},
		},
		{
			name: "multiple node zones on zonal evaluation",
			request: &entity.EvaluateSharedPoolNetworkCapacityRequest{
				CidrPrefixLength:     &twenty,
				HighAvailabilityType: apiv1.CreateSharedPoolRequest_InitSharedPoolConfig_ZONAL,
				NodeZones:            []string{"us-west-2a", "us-west-2b"},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			service := &serverImpl{dataplaneClient: dataplanemgrmocks.NewClient(t)}
			_, err := service.EvaluateSharedPoolNetworkCapacity(context.Background(), tt.request)
			require.Equal(t, codes.InvalidArgument, status.Code(err))
		})
	}
}

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

	ctx := context.Background()
	client := dataplanemgrmocks.NewClient(t)
	service := &serverImpl{dataplaneClient: client}
	prefix := int32(20)
	downstreamErr := status.Error(codes.Unavailable, "endpoint discovery unavailable")
	client.EXPECT().EvaluateDataPlaneNetworkCapacity(ctx, mock.Anything).Return(nil, downstreamErr).Once()

	_, err := service.EvaluateSharedPoolNetworkCapacity(ctx, &entity.EvaluateSharedPoolNetworkCapacityRequest{
		CidrPrefixLength:     &prefix,
		HighAvailabilityType: apiv1.CreateSharedPoolRequest_InitSharedPoolConfig_REGIONAL,
	})

	require.ErrorIs(t, err, downstreamErr)
}

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

	tests := []struct {
		name     string
		response *dplanemgrv1.EvaluateDataPlaneNetworkCapacityResponse
	}{
		{name: "nil response"},
		{name: "supported without capacity", response: &dplanemgrv1.EvaluateDataPlaneNetworkCapacityResponse{Supported: true}},
		{name: "unsupported with capacity", response: &dplanemgrv1.EvaluateDataPlaneNetworkCapacityResponse{SupportedNodeCount: 1}},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			ctx := context.Background()
			client := dataplanemgrmocks.NewClient(t)
			service := &serverImpl{dataplaneClient: client}
			prefix := int32(20)
			client.EXPECT().EvaluateDataPlaneNetworkCapacity(ctx, mock.Anything).Return(tt.response, nil).Once()

			_, err := service.EvaluateSharedPoolNetworkCapacity(ctx, &entity.EvaluateSharedPoolNetworkCapacityRequest{
				CidrPrefixLength:     &prefix,
				HighAvailabilityType: apiv1.CreateSharedPoolRequest_InitSharedPoolConfig_REGIONAL,
			})

			require.Equal(t, codes.Internal, status.Code(err))
		})
	}
}

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

	ctx := context.Background()
	client := dataplanemgrmocks.NewClient(t)
	service := &serverImpl{dataplaneClient: client}
	prefix := int32(24)
	client.EXPECT().EvaluateDataPlaneNetworkCapacity(ctx, mock.Anything).Return(
		&dplanemgrv1.EvaluateDataPlaneNetworkCapacityResponse{}, nil,
	).Once()

	response, err := service.EvaluateSharedPoolNetworkCapacity(ctx, &entity.EvaluateSharedPoolNetworkCapacityRequest{
		CidrPrefixLength:     &prefix,
		HighAvailabilityType: apiv1.CreateSharedPoolRequest_InitSharedPoolConfig_REGIONAL,
	})

	require.NoError(t, err)
	require.False(t, response.GetSupported())
	require.Zero(t, response.GetSupportedNodeCount())
}
