package region

import (
	"context"
	"testing"

	. "github.com/onsi/gomega"
	"go.uber.org/mock/gomock"

	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"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/region/mock_region"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/idgenerator/mock_idgenerator"
)

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

	mockRegionRepo := mock_region.NewMockRepo(ctrl)
	mockProviderIdentityRepo := mock_region.NewMockProviderIdentityRepo(ctrl)

	svc := NewService(mockRegionRepo, nil, mockProviderIdentityRepo, nil, nil, nil, nil, nil, nil, nil)

	ctx := context.Background()
	regionId := uint64(100)
	tenantId := uint64(200)
	providerId := uint64(1)
	servicePlan := "Premium"

	testRegion := &entity.Region{
		Region: &globalcommonv1.Region{
			ServicePlan: globalcommonv1.ServicePlan_Premium,
		},
		RegionID:   regionId,
		ProviderID: providerId,
	}

	tests := []struct {
		name           string
		setupMocks     func()
		wantErr        bool
		wantIdentity   string
		expectedErrMsg string
	}{
		{
			name: "Priority 1: Match both regionId and tenantId",
			setupMocks: func() {
				mockRegionRepo.EXPECT().Get(ctx, regionId).Return(testRegion, nil)
				mockProviderIdentityRepo.EXPECT().
					QueryDefaultProviderIdentity(ctx, providerId, servicePlan, regionId, tenantId).
					Return([]*entity.DefaultProviderIdentity{
						{
							ID:          1,
							ProviderId:  providerId,
							ServicePlan: servicePlan,
							RegionId:    regionId,
							TenantId:    tenantId,
							Identity:    "identity1",
						},
						{
							ID:          2,
							ProviderId:  providerId,
							ServicePlan: servicePlan,
							RegionId:    0,
							TenantId:    tenantId,
							Identity:    "identity2",
						},
					}, nil)
				mockProviderIdentityRepo.EXPECT().
					GetProviderIdentityByIdentity(ctx, regionId, "identity1").
					Return(&entity.RegionProviderIdentity{
						RegionProviderIdentity: &globalngv1.RegionProviderIdentity{
							Id:       100,
							Identity: "identity1",
						},
						RegionId: regionId,
					}, nil)
			},
			wantErr:      false,
			wantIdentity: "identity1",
		},
		{
			name: "Priority 2: Match tenantId with regionId = 0",
			setupMocks: func() {
				mockRegionRepo.EXPECT().Get(ctx, regionId).Return(testRegion, nil)
				mockProviderIdentityRepo.EXPECT().
					QueryDefaultProviderIdentity(ctx, providerId, servicePlan, regionId, tenantId).
					Return([]*entity.DefaultProviderIdentity{
						{
							ID:          2,
							ProviderId:  providerId,
							ServicePlan: servicePlan,
							RegionId:    0,
							TenantId:    tenantId,
							Identity:    "identity2",
						},
						{
							ID:          3,
							ProviderId:  providerId,
							ServicePlan: servicePlan,
							RegionId:    regionId,
							TenantId:    0,
							Identity:    "identity3",
						},
					}, nil)
				mockProviderIdentityRepo.EXPECT().
					GetProviderIdentityByIdentity(ctx, regionId, "identity2").
					Return(&entity.RegionProviderIdentity{
						RegionProviderIdentity: &globalngv1.RegionProviderIdentity{
							Id:       200,
							Identity: "identity2",
						},
						RegionId: regionId,
					}, nil)
			},
			wantErr:      false,
			wantIdentity: "identity2",
		},
		{
			name: "Priority 3: Match regionId with tenantId = 0",
			setupMocks: func() {
				mockRegionRepo.EXPECT().Get(ctx, regionId).Return(testRegion, nil)
				mockProviderIdentityRepo.EXPECT().
					QueryDefaultProviderIdentity(ctx, providerId, servicePlan, regionId, tenantId).
					Return([]*entity.DefaultProviderIdentity{
						{
							ID:          3,
							ProviderId:  providerId,
							ServicePlan: servicePlan,
							RegionId:    regionId,
							TenantId:    0,
							Identity:    "identity3",
						},
						{
							ID:          4,
							ProviderId:  providerId,
							ServicePlan: servicePlan,
							RegionId:    0,
							TenantId:    0,
							Identity:    "identity4",
						},
					}, nil)
				mockProviderIdentityRepo.EXPECT().
					GetProviderIdentityByIdentity(ctx, regionId, "identity3").
					Return(&entity.RegionProviderIdentity{
						RegionProviderIdentity: &globalngv1.RegionProviderIdentity{
							Id:       300,
							Identity: "identity3",
						},
						RegionId: regionId,
					}, nil)
			},
			wantErr:      false,
			wantIdentity: "identity3",
		},
		{
			name: "Priority 4: Both regionId = 0 and tenantId = 0",
			setupMocks: func() {
				mockRegionRepo.EXPECT().Get(ctx, regionId).Return(testRegion, nil)
				mockProviderIdentityRepo.EXPECT().
					QueryDefaultProviderIdentity(ctx, providerId, servicePlan, regionId, tenantId).
					Return([]*entity.DefaultProviderIdentity{
						{
							ID:          4,
							ProviderId:  providerId,
							ServicePlan: servicePlan,
							RegionId:    0,
							TenantId:    0,
							Identity:    "identity4",
						},
					}, nil)
				mockProviderIdentityRepo.EXPECT().
					GetProviderIdentityByIdentity(ctx, regionId, "identity4").
					Return(&entity.RegionProviderIdentity{
						RegionProviderIdentity: &globalngv1.RegionProviderIdentity{
							Id:       400,
							Identity: "identity4",
						},
						RegionId: regionId,
					}, nil)
			},
			wantErr:      false,
			wantIdentity: "identity4",
		},
		{
			name: "Skip invalid identity and use next priority",
			setupMocks: func() {
				mockRegionRepo.EXPECT().Get(ctx, regionId).Return(testRegion, nil)
				mockProviderIdentityRepo.EXPECT().
					QueryDefaultProviderIdentity(ctx, providerId, servicePlan, regionId, tenantId).
					Return([]*entity.DefaultProviderIdentity{
						{
							ID:          1,
							ProviderId:  providerId,
							ServicePlan: servicePlan,
							RegionId:    regionId,
							TenantId:    tenantId,
							Identity:    "invalid_identity",
						},
						{
							ID:          2,
							ProviderId:  providerId,
							ServicePlan: servicePlan,
							RegionId:    0,
							TenantId:    tenantId,
							Identity:    "identity2",
						},
					}, nil)
				// First call fails (invalid identity)
				mockProviderIdentityRepo.EXPECT().
					GetProviderIdentityByIdentity(ctx, regionId, "invalid_identity").
					Return(nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "identity not found"))
				// Second call succeeds
				mockProviderIdentityRepo.EXPECT().
					GetProviderIdentityByIdentity(ctx, regionId, "identity2").
					Return(&entity.RegionProviderIdentity{
						RegionProviderIdentity: &globalngv1.RegionProviderIdentity{
							Id:       200,
							Identity: "identity2",
						},
						RegionId: regionId,
					}, nil)
			},
			wantErr:      false,
			wantIdentity: "identity2",
		},
		{
			name: "Fallback to first provider identity when no default found",
			setupMocks: func() {
				mockRegionRepo.EXPECT().Get(ctx, regionId).Return(testRegion, nil)
				mockProviderIdentityRepo.EXPECT().
					QueryDefaultProviderIdentity(ctx, providerId, servicePlan, regionId, tenantId).
					Return([]*entity.DefaultProviderIdentity{}, nil)
				mockProviderIdentityRepo.EXPECT().
					ListProviderIdentityByRegion(ctx, regionId).
					Return([]*entity.RegionProviderIdentity{
						{
							RegionProviderIdentity: &globalngv1.RegionProviderIdentity{
								Id:       500,
								Identity: "fallback_identity",
							},
							RegionId: regionId,
						},
					}, nil)
			},
			wantErr:      false,
			wantIdentity: "fallback_identity",
		},
		{
			name: "Error when no provider identities exist",
			setupMocks: func() {
				mockRegionRepo.EXPECT().Get(ctx, regionId).Return(testRegion, nil)
				mockProviderIdentityRepo.EXPECT().
					QueryDefaultProviderIdentity(ctx, providerId, servicePlan, regionId, tenantId).
					Return([]*entity.DefaultProviderIdentity{}, nil)
				mockProviderIdentityRepo.EXPECT().
					ListProviderIdentityByRegion(ctx, regionId).
					Return([]*entity.RegionProviderIdentity{}, nil)
			},
			wantErr:        true,
			expectedErrMsg: "no provider identities found",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tt.setupMocks()

			result, err := svc.PickRegionProviderIdentity(ctx, regionId, tenantId)

			if tt.wantErr {
				g.Expect(err).ToNot(BeNil())
				if tt.expectedErrMsg != "" {
					g.Expect(err.Error()).To(ContainSubstring(tt.expectedErrMsg))
				}
			} else {
				g.Expect(err).To(BeNil())
				g.Expect(result).ToNot(BeNil())
				g.Expect(result.Identity).To(Equal(tt.wantIdentity))
			}
		})
	}
}

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

	mockRegionRepo := mock_region.NewMockRepo(ctrl)
	mockProviderIdentityRepo := mock_region.NewMockProviderIdentityRepo(ctrl)
	mockidGeneratorAdapter := mock_idgenerator.NewMockIDGenerator(ctrl)

	svc := &service{
		regionRepo:                 mockRegionRepo,
		regionProviderIdentityRepo: mockProviderIdentityRepo,
		idGeneratorAdapter:         mockidGeneratorAdapter,
	}

	ctx := context.Background()
	regionLogicalName := "us-west-1"
	regionId := uint64(100)
	providerId := uint64(1)
	servicePlan := globalcommonv1.ServicePlan_Premium
	newIdentityId := uint64(999)

	testRegion := &entity.Region{
		Region: &globalcommonv1.Region{
			ServicePlan: servicePlan,
		},
		RegionID:   regionId,
		ProviderID: providerId,
	}

	tests := []struct {
		name       string
		request    *globalngv1.RegisterRegionProviderIdentityRequest
		setupMocks func()
		wantErr    bool
	}{
		{
			name: "Success without is_default",
			request: &globalngv1.RegisterRegionProviderIdentityRequest{
				RegionLogicalName: regionLogicalName,
				RegionProviderIdentity: &globalngv1.RegionProviderIdentity{
					Identity:  "test-identity",
					IsDefault: false,
				},
			},
			setupMocks: func() {
				mockRegionRepo.EXPECT().GetByLogicalName(ctx, regionLogicalName).Return(testRegion, nil)
				mockidGeneratorAdapter.EXPECT().MustNew().Return(newIdentityId)
				mockProviderIdentityRepo.EXPECT().Save(ctx, gomock.Any()).Return(nil)
			},
			wantErr: false,
		},
		{
			name: "Success with is_default=true",
			request: &globalngv1.RegisterRegionProviderIdentityRequest{
				RegionLogicalName: regionLogicalName,
				RegionProviderIdentity: &globalngv1.RegionProviderIdentity{
					Identity:  "default-identity",
					IsDefault: true,
				},
			},
			setupMocks: func() {
				mockRegionRepo.EXPECT().GetByLogicalName(ctx, regionLogicalName).Return(testRegion, nil)
				mockidGeneratorAdapter.EXPECT().MustNew().Return(newIdentityId)
				mockProviderIdentityRepo.EXPECT().Save(ctx, gomock.Any()).Return(nil)
				mockProviderIdentityRepo.EXPECT().SaveDefaultProviderIdentity(ctx, gomock.Any()).DoAndReturn(
					func(ctx context.Context, defaultIdentity *entity.DefaultProviderIdentity) bizerr.BizError {
						// Verify the default identity has correct values
						g.Expect(defaultIdentity.ProviderId).To(Equal(providerId))
						g.Expect(defaultIdentity.ServicePlan).To(Equal(servicePlan.String()))
						g.Expect(defaultIdentity.RegionId).To(Equal(regionId))
						g.Expect(defaultIdentity.TenantId).To(Equal(uint64(0)))
						g.Expect(defaultIdentity.Identity).To(Equal("default-identity"))
						return nil
					},
				)
			},
			wantErr: false,
		},
		{
			name: "Error when region not found",
			request: &globalngv1.RegisterRegionProviderIdentityRequest{
				RegionLogicalName: regionLogicalName,
				RegionProviderIdentity: &globalngv1.RegionProviderIdentity{
					Identity: "test-identity",
				},
			},
			setupMocks: func() {
				mockRegionRepo.EXPECT().
					GetByLogicalName(ctx, regionLogicalName).
					Return(nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "region not found"))
			},
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tt.setupMocks()

			result, err := svc.RegisterRegionProviderIdentity(ctx, tt.request)

			if tt.wantErr {
				g.Expect(err).ToNot(BeNil())
			} else {
				g.Expect(err).To(BeNil())
				g.Expect(result).ToNot(BeNil())
			}
		})
	}
}
