package resourcepool

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"go.uber.org/mock/gomock"
	"google.golang.org/protobuf/types/known/timestamppb"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	globalngv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/entity"
	mockcluster "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/cluster/mock_cluster"
	regionrepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/region"
	mockregion "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/region/mock_region"
	resourcepoolrepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/resourcepool"
	mockresourcepool "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/resourcepool/mock_resourcepool"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

type resourcePoolPassthroughTransactioner struct{}

func (resourcePoolPassthroughTransactioner) Transaction(
	ctx context.Context,
	fn func(context.Context) error,
) error {
	return fn(ctx)
}

func TestUpdateResourcePoolNameLocksNamespaceBeforeResourcePool(t *testing.T) {
	ctrl := gomock.NewController(t)
	repo := mockresourcepool.NewMockRepo(ctrl)
	service := &serviceImpl{
		transaction:      resourcePoolPassthroughTransactioner{},
		resourcePoolRepo: repo,
	}

	resourcePool := &entity.ResourcePool{ResourcePool: &globalngv1.ResourcePool{
		Id:             101,
		Name:           "old-name",
		OwnerType:      globalngv1.ResourcePool_ORGANIZATION,
		OwnerId:        42,
		ServicePlan:    globalcommonv1.ServicePlan_BYOC,
		LifecycleState: globalngv1.ResourcePool_ACTIVE,
	}}
	repo.EXPECT().Load(gomock.Any(), uint64(101), false).Return(resourcePool, nil)
	gomock.InOrder(
		repo.EXPECT().LockOrganizationNamespace(gomock.Any(), uint64(42)).Return(nil),
		repo.EXPECT().LoadForUpdate(gomock.Any(), uint64(101), false).Return(resourcePool, nil),
		repo.EXPECT().HasLiveNormalizedName(gomock.Any(), uint64(42), "new-name", uint64(101)).Return(false, nil),
		repo.EXPECT().Save(gomock.Any(), gomock.Any()).DoAndReturn(
			func(_ context.Context, saved *entity.ResourcePool) bizerr.BizError {
				require.Equal(t, "new-name", saved.GetName())
				return nil
			},
		),
	)

	updated, err := service.UpdateResourcePoolName(t.Context(), &globalngv1.UpdateResourcePoolNameRequest{
		ResourcePoolId: 101,
		Name:           "  new-name  ",
	})

	require.NoError(t, err)
	require.Equal(t, "new-name", updated.GetName())
}

func TestUpdateResourcePoolNamePremiumUsesOrganizationNamespaceValidation(t *testing.T) {
	ctrl := gomock.NewController(t)
	repo := mockresourcepool.NewMockRepo(ctrl)
	service := &serviceImpl{
		transaction:      resourcePoolPassthroughTransactioner{},
		resourcePoolRepo: repo,
	}

	resourcePool := &entity.ResourcePool{ResourcePool: &globalngv1.ResourcePool{
		Id:             101,
		Name:           "old-name",
		OwnerType:      globalngv1.ResourcePool_ORGANIZATION,
		OwnerId:        42,
		ServicePlan:    globalcommonv1.ServicePlan_Premium,
		LifecycleState: globalngv1.ResourcePool_ACTIVE,
	}}
	repo.EXPECT().Load(gomock.Any(), uint64(101), false).Return(resourcePool, nil)
	gomock.InOrder(
		repo.EXPECT().LockOrganizationNamespace(gomock.Any(), uint64(42)).Return(nil),
		repo.EXPECT().LoadForUpdate(gomock.Any(), uint64(101), false).Return(resourcePool, nil),
		repo.EXPECT().HasLiveNormalizedName(gomock.Any(), uint64(42), "new-name", uint64(101)).Return(false, nil),
		repo.EXPECT().Save(gomock.Any(), gomock.Any()).DoAndReturn(
			func(_ context.Context, saved *entity.ResourcePool) bizerr.BizError {
				require.Equal(t, "new-name", saved.GetName())
				return nil
			},
		),
	)

	updated, err := service.UpdateResourcePoolName(t.Context(), &globalngv1.UpdateResourcePoolNameRequest{
		ResourcePoolId: 101,
		Name:           "new-name",
	})

	require.NoError(t, err)
	require.Equal(t, "new-name", updated.GetName())
}

func TestUpdateResourcePoolNameSharedPoolSkipsOrganizationNamespaceValidation(t *testing.T) {
	ctrl := gomock.NewController(t)
	repo := mockresourcepool.NewMockRepo(ctrl)
	service := &serviceImpl{
		transaction:      resourcePoolPassthroughTransactioner{},
		resourcePoolRepo: repo,
	}

	resourcePool := &entity.ResourcePool{ResourcePool: &globalngv1.ResourcePool{
		Id:             102,
		Name:           "old-name",
		OwnerType:      globalngv1.ResourcePool_SHARED,
		ServicePlan:    globalcommonv1.ServicePlan_Essential_V2,
		LifecycleState: globalngv1.ResourcePool_ACTIVE,
	}}
	repo.EXPECT().Load(gomock.Any(), uint64(102), false).Return(resourcePool, nil)
	repo.EXPECT().LoadForUpdate(gomock.Any(), uint64(102), false).Return(resourcePool, nil)
	repo.EXPECT().Save(gomock.Any(), gomock.Any()).DoAndReturn(
		func(_ context.Context, saved *entity.ResourcePool) bizerr.BizError {
			require.Equal(t, "new-name", saved.GetName())
			return nil
		},
	)

	updated, err := service.UpdateResourcePoolName(t.Context(), &globalngv1.UpdateResourcePoolNameRequest{
		ResourcePoolId: 102,
		Name:           "new-name",
	})

	require.NoError(t, err)
	require.Equal(t, "new-name", updated.GetName())
}

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

	servicePlans := []globalcommonv1.ServicePlan{
		globalcommonv1.ServicePlan_Premium,
		globalcommonv1.ServicePlan_Premium_Reserved,
		globalcommonv1.ServicePlan_Essential_V2,
	}
	for _, servicePlan := range servicePlans {
		t.Run(servicePlan.String(), func(t *testing.T) {
			t.Parallel()

			ctrl := gomock.NewController(t)
			repo := mockresourcepool.NewMockRepo(ctrl)
			service := &serviceImpl{
				resourcePoolRepo: repo,
			}
			resourcePool := &globalngv1.ResourcePool{
				OwnerType:   globalngv1.ResourcePool_ORGANIZATION,
				OwnerId:     42,
				ServicePlan: servicePlan,
				Name:        "pool-" + servicePlan.String(),
				Cidr:        "not-a-cidr",
			}
			repo.EXPECT().HasLiveNormalizedName(
				gomock.Any(), uint64(42), entity.NormalizeResourcePoolName(resourcePool.Name), uint64(0),
			).Return(false, nil).Times(3)
			repo.EXPECT().LockOrganizationNamespace(gomock.Any(), uint64(42)).Return(nil).Times(2)

			require.NoError(t, service.validateOrganizationNamespaceLocked(
				t.Context(),
				resourcePool,
				0,
				true,
			))
			require.NoError(t, service.lockAndValidateOrganizationNamespace(
				t.Context(),
				resourcePool,
				0,
				true,
			))
			require.NoError(t, service.lockAndValidateOrganizationNamespaceForCreate(
				t.Context(),
				resourcePool,
			))
		})
	}
}

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

	tests := []struct {
		name         string
		resourceName string
		cidr         string
		nameExists   bool
		cidrOverlaps bool
		wantMessage  string
	}{
		{
			name:        "missing name",
			cidr:        "10.0.0.0/16",
			wantMessage: "resource pool name is required",
		},
		{
			name:         "duplicate name",
			resourceName: "orders",
			cidr:         "10.0.0.0/16",
			nameExists:   true,
			wantMessage:  "resource pool name already exists",
		},
		{
			name:         "invalid CIDR",
			resourceName: "orders",
			cidr:         "not-a-cidr",
			wantMessage:  "invalid resource pool CIDR",
		},
		{
			name:         "overlapping CIDR",
			resourceName: "orders",
			cidr:         "10.0.0.0/16",
			cidrOverlaps: true,
			wantMessage:  "resource pool CIDR overlaps an existing resource pool",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			ctrl := gomock.NewController(t)
			repo := mockresourcepool.NewMockRepo(ctrl)
			service := &serviceImpl{resourcePoolRepo: repo}
			resourcePool := &globalngv1.ResourcePool{
				OwnerType:   globalngv1.ResourcePool_ORGANIZATION,
				OwnerId:     42,
				ServicePlan: globalcommonv1.ServicePlan_BYOC,
				ProviderId:  globalcommonv1.CloudProvider_AWS,
				RegionId:    globalcommonv1.Region_AWS_US_EAST_1,
				Name:        tt.resourceName,
				Cidr:        tt.cidr,
			}

			if tt.resourceName != "" {
				repo.EXPECT().
					HasLiveNormalizedName(gomock.Any(), uint64(42), tt.resourceName, uint64(0)).
					Return(tt.nameExists, nil)
			}
			if tt.resourceName != "" && !tt.nameExists && tt.cidr != "not-a-cidr" {
				repo.EXPECT().
					HasLiveCIDROverlap(
						gomock.Any(),
						uint64(42),
						globalcommonv1.CloudProvider_AWS,
						globalcommonv1.Region_AWS_US_EAST_1,
						[]byte{10, 0, 0, 0},
						[]byte{10, 0, 0xff, 0xff},
						uint64(0),
					).
					Return(tt.cidrOverlaps, nil)
			}

			err := service.validateOrganizationNamespaceLocked(t.Context(), resourcePool, 0, true)

			require.Error(t, err)
			require.Equal(t, bizerr.ErrCodeParams, err.ErrCode())
			require.ErrorContains(t, err, tt.wantMessage)
		})
	}
}

func TestListResourcePoolsBasicUsesSeekPaginationAndBatchCounts(t *testing.T) {
	ctrl := gomock.NewController(t)
	resourcePoolRepo := mockresourcepool.NewMockRepo(ctrl)
	clusterRepo := mockcluster.NewMockRepo(ctrl)
	regionRepo := mockregion.NewMockRepo(ctrl)
	service := &serviceImpl{
		resourcePoolRepo: resourcePoolRepo,
		clusterRepo:      clusterRepo,
		regionRepo:       regionRepo,
	}

	createdAt := time.Date(2026, time.July, 21, 8, 0, 0, 0, time.UTC)
	resourcePools := []*entity.ResourcePool{
		{ResourcePool: &globalngv1.ResourcePool{
			Id:             3,
			CreateTime:     timestamppb.New(createdAt.Add(2 * time.Second)),
			LifecycleState: globalngv1.ResourcePool_MODIFYING,
		}, RegionId: 11},
		{ResourcePool: &globalngv1.ResourcePool{
			Id:             2,
			CreateTime:     timestamppb.New(createdAt.Add(time.Second)),
			LifecycleState: globalngv1.ResourcePool_ACTIVE,
		}, RegionId: 11},
		{ResourcePool: &globalngv1.ResourcePool{
			Id:             1,
			CreateTime:     timestamppb.New(createdAt),
			LifecycleState: globalngv1.ResourcePool_FAILED,
		}, RegionId: 11},
	}
	resourcePoolRepo.EXPECT().Query(gomock.Any(), gomock.Any()).DoAndReturn(
		func(_ context.Context, filter *resourcepoolrepo.Filter) ([]*entity.ResourcePool, int64, bizerr.BizError) {
			require.Equal(t, int64(3), filter.PageSize)
			require.Zero(t, filter.Offset)
			require.Nil(t, filter.Cursor)
			require.Equal(t, "strasse", filter.Name)
			require.Equal(t, []globalngv1.ResourcePool_LifecycleState{globalngv1.ResourcePool_ACTIVE}, filter.LifecycleStates)
			return resourcePools, 7, nil
		},
	)
	clusterRepo.EXPECT().CountLiveBySharedPoolIDs(gomock.Any(), []uint64{3, 2}).Return(
		map[uint64]int64{3: 4, 2: 1},
		nil,
	)
	regionRepo.EXPECT().ListRegions(gomock.Any(), regionrepo.Filter{
		DBRegionIds: []uint64{11},
		ShowDeleted: true,
	}).Return([]*entity.Region{{
		RegionID: 11,
		Region:   &globalcommonv1.Region{DisplayName: "Oregon (us-west-2)"},
	}}, nil)

	request := &globalngv1.ListResourcePoolsRequest{
		ResourcePoolName: " Straße ",
		LifecycleStates:  []globalngv1.ResourcePool_LifecycleState{globalngv1.ResourcePool_ACTIVE},
		PageSize:         2,
		View:             globalngv1.ListResourcePoolsRequest_BASIC,
	}
	response, err := service.ListResourcePools(t.Context(), request)

	require.NoError(t, err)
	require.EqualValues(t, 7, response.GetTotalSize())
	require.Len(t, response.GetResourcePools(), 2)
	require.EqualValues(t, 4, response.GetResourcePools()[0].GetResourcePool().GetInstanceCount())
	require.EqualValues(t, 1, response.GetResourcePools()[1].GetResourcePool().GetInstanceCount())
	require.Equal(t, "Oregon (us-west-2)", response.GetResourcePools()[0].GetRegion().GetDisplayName())
	require.Equal(t, globalngv1.ResourcePool_MODIFYING, response.GetResourcePools()[0].GetResourcePool().GetLifecycleState())
	require.Equal(t, globalngv1.ResourcePool_ACTIVE, response.GetResourcePools()[1].GetResourcePool().GetLifecycleState())
	require.NotEmpty(t, response.GetNextPageToken())

	request.PageToken = response.GetNextPageToken()
	cursor, tokenErr := parseResourcePoolListPageToken(request)
	require.NoError(t, tokenErr)
	require.Equal(t, uint64(2), cursor.ID)
	require.Equal(t, createdAt.Add(time.Second), cursor.CreatedAt)
}
