package action

import (
	"errors"
	"testing"

	"github.com/pingcap/tidb-operator/api/v2/core/v1alpha1"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/domain/workflow"
	clusterconfigmocks "github.com/tidbcloud/cluster-service-ng/pkg/clusterconfig/mocks"
	dataplanemocks "github.com/tidbcloud/cluster-service-ng/pkg/dataplane/mocks"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
	schemav1 "github.com/tidbcloud/cluster-service-ng/pkg/storage/etcd/schema/v1"
	repomocks "github.com/tidbcloud/cluster-service-ng/pkg/storage/repo/mocks"
	"github.com/tidbcloud/cluster-service-ng/pkg/utils"
	"google.golang.org/protobuf/encoding/protojson"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	k8sruntime "k8s.io/apimachinery/pkg/runtime"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

func TestApplyPDMicroserviceComponentsActionEnsuresConfigCenterMetadataBeforeDataplane(t *testing.T) {
	sharedPool := &entity.SharedPool{
		ConfigCenterMetaProfileId: "1001",
		SharedPool: apiv1.SharedPool{
			Id:            "sp-1",
			Version:       "v8.5.0",
			DataplaneName: "dp-1",
			ComponentGroups: []*entity.Component{
				{Id: "default", ComponentType: apiv1.ComponentType_PD, Phase: apiv1.Component_AVAILABLE},
			},
		},
	}
	targetSharedPool := &entity.SharedPool{
		SharedPool: apiv1.SharedPool{
			Id:               "sp-1",
			Version:          "v8.5.0",
			DataplaneName:    "dp-1",
			PdDeploymentMode: apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MICROSERVICE,
			ComponentGroups: []*entity.Component{
				{Id: "default", ComponentType: apiv1.ComponentType_PD, Phase: apiv1.Component_MODIFYING},
				{Id: "default", ComponentType: apiv1.ComponentType_PD_TSO, Phase: apiv1.Component_MODIFYING},
			},
		},
	}
	targetJSON, err := protojson.Marshal(targetSharedPool)
	require.NoError(t, err)

	sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
	sharedPoolRepo.On("Load", mock.Anything, "sp-1", false).Return(sharedPool, nil)
	clusterConfigClient := clusterconfigmocks.NewClient(t)
	clusterConfigClient.EXPECT().
		EnsurePDMicroserviceComponents(mock.Anything, mock.MatchedBy(func(sharedPool *entity.SharedPool) bool {
			return sharedPool.ConfigCenterMetaProfileId == "1001"
		})).
		Return(errors.New("config center unavailable"))
	dataplaneManager := dataplanemocks.NewManager(t)
	action := NewApplyPDMicroserviceComponentsAction(
		sharedPoolRepo,
		dataplaneManager,
		clusterConfigClient,
		nil,
		nil,
		nil,
		nil,
	)

	err = action.Invoke(actions.ActionArgs{
		workflow.SharedPoolIdKey: actions.ActionArgString("sp-1"),
		workflow.SharedPoolKey:   actions.ActionArgString(string(targetJSON)),
		workflow.ComponentIdsKey: actions.ActionArgString("default"),
		utils.TraceIdKey:         actions.ActionArgString("0123456789abcdef0123456789abcdef"),
	})

	require.Error(t, err)
	require.ErrorContains(t, err, "failed to ensure pd microservice components")
	dataplaneManager.AssertNotCalled(t, "GetClient", "dp-1")
}

func TestApplyPDMicroserviceComponentsActionContinuesAfterConfigCenterMetadataIsEnsured(t *testing.T) {
	sharedPool := &entity.SharedPool{
		SharedPool: apiv1.SharedPool{
			Id:            "sp-1",
			Version:       "v8.5.0",
			DataplaneName: "dp-1",
		},
	}
	targetSharedPool := &entity.SharedPool{
		SharedPool: apiv1.SharedPool{
			Id:               "sp-1",
			Version:          "v8.5.0",
			DataplaneName:    "dp-1",
			PdDeploymentMode: apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MICROSERVICE,
			ComponentGroups: []*entity.Component{
				{Id: "default", ComponentType: apiv1.ComponentType_PD_TSO, Phase: apiv1.Component_MODIFYING},
			},
		},
	}
	targetJSON, err := protojson.Marshal(targetSharedPool)
	require.NoError(t, err)

	sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
	sharedPoolRepo.On("Load", mock.Anything, "sp-1", false).Return(sharedPool, nil)
	clusterConfigClient := clusterconfigmocks.NewClient(t)
	clusterConfigClient.EXPECT().
		EnsurePDMicroserviceComponents(mock.Anything, mock.Anything).
		Return(nil)
	dataplaneManager := dataplanemocks.NewManager(t)
	dataplaneManager.On("GetClient", "dp-1").Return(nil, errors.New("dataplane unavailable")).Once()
	action := NewApplyPDMicroserviceComponentsAction(
		sharedPoolRepo,
		dataplaneManager,
		clusterConfigClient,
		nil,
		nil,
		nil,
		nil,
	)

	err = action.Invoke(actions.ActionArgs{
		workflow.SharedPoolIdKey: actions.ActionArgString("sp-1"),
		workflow.SharedPoolKey:   actions.ActionArgString(string(targetJSON)),
		workflow.ComponentIdsKey: actions.ActionArgString("default"),
		utils.TraceIdKey:         actions.ActionArgString("0123456789abcdef0123456789abcdef"),
	})

	require.ErrorContains(t, err, "failed to get remote client")
	dataplaneManager.AssertExpectations(t)
}

func TestDeletePDMicroserviceComponentsActionSkipsConfigCenterReconciliation(t *testing.T) {
	sharedPool := &entity.SharedPool{SharedPool: apiv1.SharedPool{
		Id:            "sp-1",
		DataplaneName: "dp-1",
	}}
	targetSharedPool := &entity.SharedPool{SharedPool: apiv1.SharedPool{
		Id:               "sp-1",
		DataplaneName:    "dp-1",
		PdDeploymentMode: apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MONOLITHIC,
		ComponentGroups: []*entity.Component{
			{Id: "default", ComponentType: apiv1.ComponentType_PD_TSO, Phase: apiv1.Component_DELETING},
		},
	}}
	targetJSON, err := protojson.Marshal(targetSharedPool)
	require.NoError(t, err)

	sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
	sharedPoolRepo.On("Load", mock.Anything, "sp-1", false).Return(sharedPool, nil)
	dataplaneManager := dataplanemocks.NewManager(t)
	dataplaneManager.On("GetClient", "dp-1").Return(nil, errors.New("dataplane unavailable")).Once()
	// A nil Config Center client makes an accidental reconciliation call fail
	// immediately and keeps the delete-direction contract explicit.
	action := NewDeletePDMicroserviceComponentsAction(
		sharedPoolRepo,
		dataplaneManager,
		nil,
		nil,
		nil,
		nil,
		nil,
	)

	err = action.Invoke(actions.ActionArgs{
		workflow.SharedPoolIdKey: actions.ActionArgString("sp-1"),
		workflow.SharedPoolKey:   actions.ActionArgString(string(targetJSON)),
		workflow.ComponentIdsKey: actions.ActionArgString("default"),
		utils.TraceIdKey:         actions.ActionArgString("0123456789abcdef0123456789abcdef"),
	})

	require.ErrorContains(t, err, "failed to get remote client")
	dataplaneManager.AssertExpectations(t)
}

func TestPDMicroserviceTenantSourceUsesDataplaneTag(t *testing.T) {
	tests := []struct {
		name       string
		sharedPool *entity.SharedPool
		want       string
	}{
		{
			name: "dataplane tag is authoritative",
			sharedPool: &entity.SharedPool{
				TenantId: "different-entity-tenant",
				SharedPool: apiv1.SharedPool{Provider: &apiv1.Provider{Tags: map[string]string{
					schemav1.LabelUserTenantKey: "12345",
				}}},
			},
			want: "dataplane-tag",
		},
		{
			name:       "missing tag",
			sharedPool: &entity.SharedPool{TenantId: "entity-only-tenant"},
			want:       "unavailable",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.Equal(t, tt.want, pdMicroserviceTenantSource(tt.sharedPool))
		})
	}
}

func TestMergePDDeploymentModeComponentsPreservesCurrentNonPDComponents(t *testing.T) {
	currentComponents := []*entity.Component{
		{Id: "default", ComponentType: apiv1.ComponentType_PD, Phase: apiv1.Component_AVAILABLE},
		{Id: "default", ComponentType: apiv1.ComponentType_TIDB_WORKER, Phase: apiv1.Component_AVAILABLE},
	}
	targetComponents := []*entity.Component{
		{Id: "default", ComponentType: apiv1.ComponentType_PD, Phase: apiv1.Component_MODIFYING},
		{Id: "default", ComponentType: apiv1.ComponentType_PD_TSO, Phase: apiv1.Component_MODIFYING},
		{Id: "default", ComponentType: apiv1.ComponentType_TIDB_WORKER, Phase: apiv1.Component_DELETING},
		{Id: "default", ComponentType: apiv1.ComponentType_TIPROXY, Phase: apiv1.Component_CREATING},
	}

	merged := MergePDDeploymentModeComponents(currentComponents, targetComponents)

	assert.Len(t, merged, 3)
	assert.Equal(t, apiv1.Component_MODIFYING, requireComponentByType(t, merged, apiv1.ComponentType_PD).GetPhase())
	assert.Equal(t, apiv1.Component_MODIFYING, requireComponentByType(t, merged, apiv1.ComponentType_PD_TSO).GetPhase())
	assert.Equal(t, apiv1.Component_AVAILABLE, requireComponentByType(t, merged, apiv1.ComponentType_TIDB_WORKER).GetPhase())
	assert.Nil(t, findComponentByTypeForTest(merged, apiv1.ComponentType_TIPROXY))
}

func TestFinalizePDDeploymentModeSharedPoolToMicroservice(t *testing.T) {
	sharedPool := &entity.SharedPool{
		SharedPool: apiv1.SharedPool{
			Phase:            apiv1.SharedPool_MODIFYING,
			PdDeploymentMode: apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MONOLITHIC,
			ComponentGroups: []*entity.Component{
				{Id: "default", ComponentType: apiv1.ComponentType_PD, Phase: apiv1.Component_MODIFYING},
				{Id: "default", ComponentType: apiv1.ComponentType_PD_TSO, Phase: apiv1.Component_MODIFYING},
				{Id: "default", ComponentType: apiv1.ComponentType_PD_SCHEDULING, Phase: apiv1.Component_MODIFYING},
				{Id: "default", ComponentType: apiv1.ComponentType_RESOURCE_MANAGER, Phase: apiv1.Component_MODIFYING},
				{Id: "tikv", ComponentType: apiv1.ComponentType_TIKV, Phase: apiv1.Component_AVAILABLE},
			},
		},
	}

	FinalizePDDeploymentModeSharedPool(sharedPool, apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MICROSERVICE)

	assert.Equal(t, apiv1.SharedPool_AVAILABLE, sharedPool.GetPhase())
	assert.Equal(t, apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MICROSERVICE, sharedPool.GetPdDeploymentMode())
	require.NotNil(t, sharedPool.GetPdDeploymentStatus())
	assert.Equal(t, apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MICROSERVICE, sharedPool.GetPdDeploymentStatus().GetCurrentMode())
	assert.Equal(t, apiv1.PDDeploymentStatusPhase_PD_DEPLOYMENT_STATUS_PHASE_STABLE, sharedPool.GetPdDeploymentStatus().GetPhase())
	for _, comp := range sharedPool.GetComponentGroups() {
		assert.Equal(t, apiv1.Component_AVAILABLE, comp.GetPhase())
	}
}

func requireComponentByType(t *testing.T, components []*entity.Component, componentType entity.ComponentType) *entity.Component {
	t.Helper()

	component := findComponentByTypeForTest(components, componentType)
	require.NotNil(t, component)
	return component
}

func findComponentByTypeForTest(components []*entity.Component, componentType entity.ComponentType) *entity.Component {
	for _, comp := range components {
		if comp.GetComponentType() == componentType {
			return comp
		}
	}
	return nil
}

func TestFinalizePDDeploymentModeSharedPoolToMonolithic(t *testing.T) {
	sharedPool := &entity.SharedPool{
		SharedPool: apiv1.SharedPool{
			Phase:            apiv1.SharedPool_MODIFYING,
			PdDeploymentMode: apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MICROSERVICE,
			ComponentGroups: []*entity.Component{
				{Id: "default", ComponentType: apiv1.ComponentType_PD, Phase: apiv1.Component_MODIFYING},
				{Id: "default", ComponentType: apiv1.ComponentType_PD_TSO, Phase: apiv1.Component_DELETING},
				{Id: "default", ComponentType: apiv1.ComponentType_PD_SCHEDULING, Phase: apiv1.Component_DELETING},
				{Id: "default", ComponentType: apiv1.ComponentType_RESOURCE_MANAGER, Phase: apiv1.Component_DELETING},
				{Id: "tikv", ComponentType: apiv1.ComponentType_TIKV, Phase: apiv1.Component_AVAILABLE},
			},
		},
	}

	FinalizePDDeploymentModeSharedPool(sharedPool, apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MONOLITHIC)

	assert.Equal(t, apiv1.SharedPool_AVAILABLE, sharedPool.GetPhase())
	assert.Equal(t, apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MONOLITHIC, sharedPool.GetPdDeploymentMode())
	require.NotNil(t, sharedPool.GetPdDeploymentStatus())
	assert.Equal(t, apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MONOLITHIC, sharedPool.GetPdDeploymentStatus().GetCurrentMode())
	assert.Equal(t, apiv1.PDDeploymentStatusPhase_PD_DEPLOYMENT_STATUS_PHASE_STABLE, sharedPool.GetPdDeploymentStatus().GetPhase())

	assert.Len(t, sharedPool.GetComponentGroups(), 2)
	for _, comp := range sharedPool.GetComponentGroups() {
		assert.False(t, isPDMicroserviceComponent(comp))
		assert.Equal(t, apiv1.Component_AVAILABLE, comp.GetPhase())
	}
}

func TestPatchPDDeploymentModeActionCheckFinishRequiresSpecAndStatusTarget(t *testing.T) {
	tests := []struct {
		name       string
		specMode   v1alpha1.PDMode
		statusMode v1alpha1.PDMode
		condition  *metav1.Condition
		wantFinish bool
	}{
		{
			name:       "spec and status target",
			specMode:   v1alpha1.PDModeMS,
			statusMode: v1alpha1.PDModeMS,
			wantFinish: true,
		},
		{
			name:       "status target but spec stale",
			specMode:   v1alpha1.PDModeNormal,
			statusMode: v1alpha1.PDModeMS,
		},
		{
			name:       "spec target but status stale",
			specMode:   v1alpha1.PDModeMS,
			statusMode: v1alpha1.PDModeNormal,
		},
		{
			name:       "switching condition is still true",
			specMode:   v1alpha1.PDModeMS,
			statusMode: v1alpha1.PDModeMS,
			condition: &metav1.Condition{
				Type:   v1alpha1.CondModeSwitching,
				Status: metav1.ConditionTrue,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			scheme := k8sruntime.NewScheme()
			require.NoError(t, v1alpha1.AddToScheme(scheme))
			pdGroup := &v1alpha1.PDGroup{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "db-default",
					Namespace: "tidbsp-1",
				},
			}
			pdGroup.Spec.Template.Spec.Mode = tt.specMode
			pdGroup.Status.Mode = tt.statusMode
			if tt.condition != nil {
				pdGroup.Status.Conditions = []metav1.Condition{*tt.condition}
			}
			kubeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pdGroup).Build()
			sharedPool := &entity.SharedPool{
				SharedPool: apiv1.SharedPool{
					Id:            "sp-1",
					DataplaneName: "dp-1",
					ComponentGroups: []*entity.Component{
						{Id: "default", ComponentType: apiv1.ComponentType_PD},
					},
				},
			}
			sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
			sharedPoolRepo.On("Load", mock.Anything, "sp-1", false).Return(sharedPool, nil)
			dataplaneManager := dataplanemocks.NewManager(t)
			dataplaneManager.On("GetClient", "dp-1").Return(kubeClient, nil)
			action := NewPatchPDDeploymentModeAction(sharedPoolRepo, dataplaneManager)

			finished, err := action.CheckFinish(actions.ActionArgs{
				workflow.SharedPoolIdKey:     actions.ActionArgString("sp-1"),
				workflow.PDDeploymentModeKey: actions.ActionArgString(apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MICROSERVICE.String()),
			})

			require.NoError(t, err)
			assert.Equal(t, tt.wantFinish, finished)
		})
	}
}
