package config

import (
	"context"
	"testing"

	ccent "github.com/tidbcloud/devops/entity/cloudconfig"
	cccli "github.com/tidbcloud/devops/pkg/infra/clients/configcentercli"
	"github.com/tidbcloud/devops/pkg/service/cloudconfig/api"

	ccproto "github.com/tidbcloud/config-center/proto/config_center"
	"go.uber.org/mock/gomock"
)

func TestFindComponentForConfigFileFallsBackToConfigTypeMapping(t *testing.T) {
	components := map[string]*ccent.Component{
		ccproto.SHARED_POOL_COMPONENT_TIFLASH_COMPUTE: {
			Name: ccproto.SHARED_POOL_COMPONENT_TIFLASH_COMPUTE,
		},
	}
	file := &ccent.FileConfiguration{
		ConfigType:        ccproto.SHARED_POOL_CONFIG_TIFLASH_COMPUTE_LEARNER,
		BelongToComponent: ccproto.SHARED_POOL_CONFIG_TIFLASH_COMPUTE_LEARNER,
	}

	comp := findComponentForConfigFile(ccent.ClusterTypeSharedPool, components, file)
	if comp == nil {
		t.Fatal("expected config type mapping to find tiflash_compute component")
	}
	if comp.Name != ccproto.SHARED_POOL_COMPONENT_TIFLASH_COMPUTE {
		t.Fatalf("expected component %q, got %q", ccproto.SHARED_POOL_COMPONENT_TIFLASH_COMPUTE, comp.Name)
	}
}

func TestPartialVersionChangeComponentsOnlyForPremiumClusterTypes(t *testing.T) {
	params := []*api.ChangeComponentParam{
		{Component: ccproto.SHARED_POOL_COMPONENT_TIFLASH_COMPUTE},
	}

	sharedPool := partialVersionChangeComponents(&ccent.ClusterConfigurationLocator{
		ClusterType: ccent.ClusterTypeSharedPool,
	}, params)
	if _, ok := sharedPool[ccproto.SHARED_POOL_COMPONENT_TIFLASH_COMPUTE]; !ok {
		t.Fatalf("expected shared pool partial component to be selected")
	}

	dedicated := partialVersionChangeComponents(&ccent.ClusterConfigurationLocator{
		ClusterType: ccent.ClusterTypeDedicatedTiDB,
	}, params)
	if dedicated != nil {
		t.Fatalf("expected dedicated version change to keep full component upgrade")
	}
}

func TestVersionChangeComponentNamesUsesSelectedComponentsWhenPartial(t *testing.T) {
	current := map[string]*ccent.Component{
		ccproto.LOGICAL_CLUSTER_COMPONENT_TIDB: {
			Name: ccproto.LOGICAL_CLUSTER_COMPONENT_TIDB,
		},
		ccproto.LOGICAL_CLUSTER_COMPONENT_TIPROXY: {
			Name: ccproto.LOGICAL_CLUSTER_COMPONENT_TIPROXY,
		},
		ccproto.LOGICAL_CLUSTER_COMPONENT_DUMPLING: {
			Name: ccproto.LOGICAL_CLUSTER_COMPONENT_DUMPLING,
		},
	}
	partial := map[string]struct{}{
		ccproto.LOGICAL_CLUSTER_COMPONENT_TIDB: {},
	}

	components := versionChangeComponentNames(current, partial)
	if len(components) != 1 || components[0] != ccproto.LOGICAL_CLUSTER_COMPONENT_TIDB {
		t.Fatalf("expected only tidb to be upgraded, got %v", components)
	}
}

func TestShouldUpdateFileTemplateForVersionUsesSelectedComponentMapping(t *testing.T) {
	partial := map[string]struct{}{
		ccproto.SHARED_POOL_COMPONENT_TIFLASH_COMPUTE: {},
	}

	learner := &ccent.FileConfiguration{
		ConfigType:        ccproto.SHARED_POOL_CONFIG_TIFLASH_COMPUTE_LEARNER,
		BelongToComponent: ccproto.SHARED_POOL_CONFIG_TIFLASH_COMPUTE_LEARNER,
	}
	if !shouldUpdateFileTemplateForVersion(ccent.ClusterTypeSharedPool, learner, partial) {
		t.Fatalf("expected tiflash_compute_learner template to follow selected tiflash_compute")
	}

	tikv := &ccent.FileConfiguration{
		ConfigType:        ccproto.SHARED_POOL_CONFIG_TIKV,
		BelongToComponent: ccproto.SHARED_POOL_COMPONENT_TIKV,
	}
	if shouldUpdateFileTemplateForVersion(ccent.ClusterTypeSharedPool, tikv, partial) {
		t.Fatalf("expected tikv template to be skipped when tikv is not selected")
	}
}

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

	configService := api.NewMockCloudConfigServiceAPI(ctrl)
	configCenterAPI := cccli.NewMockConfigCenterServiceClient(ctrl)
	builder := &ConfigurationChangeBuilder{
		Context: context.Background(),
		Comps: Components{
			ConfigService: configService,
			ConfigCenter:  &cccli.Client{ConfigAPI: configCenterAPI},
		},
		Param: api.CreateConfigurationChangeParam{
			CreateConfigurationChangeBaseParam: api.CreateConfigurationChangeBaseParam{
				TargetCluster: &ccent.ClusterConfigurationLocator{
					ClusterType: ccent.ClusterTypeSharedPool,
					ClusterID:   "resource-pool-1",
				},
				ChangeType: api.ChangeTypeSharedPool,
			},
		},
		Change: &ccent.ConfigurationChange{
			TargetCluster: &ccent.ClusterConfigurationLocator{
				ClusterType: ccent.ClusterTypeSharedPool,
				ClusterID:   "resource-pool-1",
			},
			TargetVersion: "v8.5.4",
			Scope:         ccent.ConfigurationChangeScope{},
		},
		CurrentConfig: &ccent.Configuration{
			TargetCluster: &ccent.ClusterConfigurationLocator{ClusterType: ccent.ClusterTypeSharedPool, ClusterID: "resource-pool-1"},
			Version:       "v8.5.4",
			Components: map[string]*ccent.Component{
				ccproto.SHARED_POOL_COMPONENT_PD: {
					Name:              ccproto.SHARED_POOL_COMPONENT_PD,
					Version:           "v8.5.4",
					EffectiveImageTag: "old-pd-tag",
				},
			},
			Files: map[string]*ccent.FileConfiguration{
				ccproto.SHARED_POOL_CONFIG_PD: {
					Filename:          ccproto.SHARED_POOL_CONFIG_PD,
					ConfigType:        ccproto.SHARED_POOL_CONFIG_PD,
					BelongToComponent: ccproto.SHARED_POOL_COMPONENT_PD,
					TemplateID:        11,
					Text:              "old pd config",
				},
			},
		},
		ChangedConfig: &ccent.Configuration{
			Components: map[string]*ccent.Component{
				ccproto.SHARED_POOL_COMPONENT_PD: {
					Name:              ccproto.SHARED_POOL_COMPONENT_PD,
					Version:           "v8.5.4",
					EffectiveImageTag: "old-pd-tag",
				},
			},
			Files: map[string]*ccent.FileConfiguration{
				ccproto.SHARED_POOL_CONFIG_PD: {
					Filename:          ccproto.SHARED_POOL_CONFIG_PD,
					ConfigType:        ccproto.SHARED_POOL_CONFIG_PD,
					BelongToComponent: ccproto.SHARED_POOL_COMPONENT_PD,
					TemplateID:        11,
					Text:              "old pd config",
				},
			},
		},
	}

	configService.EXPECT().GetTemplateByDefaultCase(gomock.Any(), ccent.ClusterTypeSharedPool, gomock.Any()).DoAndReturn(
		func(_ context.Context, _ ccent.ClusterType, defCase ccent.TemplateDefaultCase) (*ccent.Template, error) {
			if defCase.Version != "v26.3.99" {
				t.Fatalf("expected target template version v26.3.99, got %s", defCase.Version)
			}
			if defCase.ConfigType != ccproto.SHARED_POOL_CONFIG_PD {
				t.Fatalf("expected pd config type, got %s", defCase.ConfigType)
			}
			return &ccent.Template{ID: 101}, nil
		},
	)
	configCenterAPI.EXPECT().GetComponentImage(gomock.Any(), gomock.Any()).Return(&ccproto.ComponentImage{Tag: "new-pd-tag"}, nil)
	configService.EXPECT().GetTemplate(gomock.Any(), ccent.ClusterTypeSharedPool, uint64(101), gomock.Any()).Return(&ccent.Template{ID: 101}, nil)
	configCenterAPI.EXPECT().ListConfigurationDictItems(gomock.Any(), gomock.Any()).DoAndReturn(
		func(_ context.Context, req *ccproto.ListConfigurationDictItemsRequest, _ ...any) (*ccproto.ListConfigurationDictItemsResponse, error) {
			if req.GetVersion() != "v26.3.99" {
				t.Fatalf("expected component target version v26.3.99 for parameter attributes, got %s", req.GetVersion())
			}
			return &ccproto.ListConfigurationDictItemsResponse{}, nil
		},
	)

	err := builder.AssembleComponentChange(&api.ChangeComponentParam{
		Component: ccproto.SHARED_POOL_COMPONENT_PD,
		Version:   &api.ChangeVersionParam{ToVersion: "v26.3.99"},
	}, nil)
	if err != nil {
		t.Fatalf("assemble component change failed: %v", err)
	}

	idx, fileParam := builder.Param.FindFile(ccproto.SHARED_POOL_CONFIG_PD)
	if fileParam == nil || idx < 0 {
		t.Fatalf("expected pd file param to be added")
	}
	if fileParam.Template == nil || fileParam.Template.ToTemplateID != 101 {
		t.Fatalf("expected pd file template 101, got %#v", fileParam.Template)
	}
	if err := builder.AssembleFileChange(fileParam); err != nil {
		t.Fatalf("assemble component configuration file failed: %v", err)
	}
	if len(builder.Change.Scope.Components) != 1 || builder.Change.Scope.Components[0].ModifiedVersion == nil {
		t.Fatalf("expected component version change to be recorded")
	}
}

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

	configService := api.NewMockCloudConfigServiceAPI(ctrl)
	actor := &actorCreate{baseActor: &baseActor{Context: &Context{
		Context: context.Background(),
		Components: Components{
			ConfigService: configService,
		},
	}}}
	change := &ccent.ConfigurationChange{
		TargetCluster: &ccent.ClusterConfigurationLocator{
			ClusterType: ccent.ClusterTypeSharedPool,
		},
		Scope: ccent.ConfigurationChangeScope{
			Components: []*ccent.ConfigurationComponentChange{
				{
					Component: ccproto.SHARED_POOL_COMPONENT_PD,
					ModifiedVersion: &ccent.ChangedVersion{
						From: "v8.5.4",
						To:   "not-available",
					},
				},
			},
		},
	}

	configService.EXPECT().ListAvailableComponentVersions(
		gomock.Any(), ccent.ClusterTypeSharedPool, ccproto.SHARED_POOL_COMPONENT_PD,
	).Return([]string{"v8.5.4", "v26.3.99"}, nil)

	err := actor.validateCreate(change)
	if err == nil || err.Error() != "modified version not-available is not found" {
		t.Fatalf("expected unavailable component version error, got %v", err)
	}
}
