package server

import (
	"context"
	"testing"

	"github.com/pingcap/tidb-operator/api/v2/core/v1alpha1"
	"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"
	"github.com/tidbcloud/cluster-service-ng/domain/workflow/dag"
	dataplanemocks "github.com/tidbcloud/cluster-service-ng/pkg/dataplane/mocks"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/executor"
	executormocks "github.com/tidbcloud/cluster-service-ng/pkg/runtime/executor/mocks"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	k8sruntime "k8s.io/apimachinery/pkg/runtime"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

func TestBuildPDDeploymentStatus(t *testing.T) {
	tests := []struct {
		name               string
		desiredMode        apiv1.PDDeploymentMode
		specMode           v1alpha1.PDMode
		statusMode         v1alpha1.PDMode
		condition          *metav1.Condition
		allowSpecInference bool
		wantCurrentMode    apiv1.PDDeploymentMode
		wantPhase          apiv1.PDDeploymentStatusPhase
		wantReason         string
	}{
		{
			name:            "stable microservice from actual status",
			desiredMode:     apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MICROSERVICE,
			specMode:        v1alpha1.PDModeMS,
			statusMode:      v1alpha1.PDModeMS,
			wantCurrentMode: apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MICROSERVICE,
			wantPhase:       apiv1.PDDeploymentStatusPhase_PD_DEPLOYMENT_STATUS_PHASE_STABLE,
		},
		{
			name:        "mode switching condition stays switching even when actual reaches target",
			desiredMode: apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MICROSERVICE,
			specMode:    v1alpha1.PDModeMS,
			statusMode:  v1alpha1.PDModeMS,
			condition: &metav1.Condition{
				Type:    v1alpha1.CondModeSwitching,
				Status:  metav1.ConditionTrue,
				Reason:  v1alpha1.ReasonSwitchingPDInstances,
				Message: "rolling PD instances",
			},
			wantCurrentMode: apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MICROSERVICE,
			wantPhase:       apiv1.PDDeploymentStatusPhase_PD_DEPLOYMENT_STATUS_PHASE_SWITCHING,
			wantReason:      v1alpha1.ReasonSwitchingPDInstances,
		},
		{
			name:               "legacy microservice read path infers from spec",
			desiredMode:        apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MICROSERVICE,
			specMode:           v1alpha1.PDModeMS,
			statusMode:         v1alpha1.PDModeNormal,
			allowSpecInference: true,
			wantCurrentMode:    apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MICROSERVICE,
			wantPhase:          apiv1.PDDeploymentStatusPhase_PD_DEPLOYMENT_STATUS_PHASE_STABLE,
			wantReason:         pdDeploymentStatusReasonInferredFromSpec,
		},
		{
			name:            "strict path does not infer from spec",
			desiredMode:     apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MICROSERVICE,
			specMode:        v1alpha1.PDModeMS,
			statusMode:      v1alpha1.PDModeNormal,
			wantCurrentMode: apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MONOLITHIC,
			wantPhase:       apiv1.PDDeploymentStatusPhase_PD_DEPLOYMENT_STATUS_PHASE_SWITCHING,
			wantReason:      pdDeploymentStatusReasonSwitching,
		},
		{
			name:            "status target with opposite spec stays switching",
			desiredMode:     apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MICROSERVICE,
			specMode:        v1alpha1.PDModeNormal,
			statusMode:      v1alpha1.PDModeMS,
			wantCurrentMode: apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MICROSERVICE,
			wantPhase:       apiv1.PDDeploymentStatusPhase_PD_DEPLOYMENT_STATUS_PHASE_SWITCHING,
			wantReason:      pdDeploymentStatusReasonSwitching,
		},
		{
			name:            "unknown operator status mode is not treated as monolithic",
			desiredMode:     apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MONOLITHIC,
			statusMode:      v1alpha1.PDMode("future"),
			wantCurrentMode: apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_UNSPECIFIED,
			wantPhase:       apiv1.PDDeploymentStatusPhase_PD_DEPLOYMENT_STATUS_PHASE_UNKNOWN,
			wantReason:      pdDeploymentStatusReasonUnknownPDMode,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			sharedPool := &entity.SharedPool{
				SharedPool: apiv1.SharedPool{
					PdDeploymentMode: tt.desiredMode,
				},
			}
			pdGroup := &v1alpha1.PDGroup{}
			pdGroup.Spec.Template.Spec.Mode = tt.specMode
			pdGroup.Status.Mode = tt.statusMode
			if tt.condition != nil {
				pdGroup.Status.Conditions = []metav1.Condition{*tt.condition}
			}

			got := buildPDDeploymentStatus(sharedPool, pdGroup, tt.allowSpecInference)

			require.Equal(t, tt.wantCurrentMode, got.GetCurrentMode())
			require.Equal(t, tt.wantPhase, got.GetPhase())
			require.Equal(t, tt.wantReason, got.GetReason())
		})
	}
}

func TestBuildPDDeploymentStatusUsesSwitchingMessageWhenConditionIsNotActive(t *testing.T) {
	sharedPool := &entity.SharedPool{
		SharedPool: apiv1.SharedPool{
			PdDeploymentMode: apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MONOLITHIC,
		},
	}
	pdGroup := &v1alpha1.PDGroup{}
	pdGroup.Spec.Template.Spec.Mode = v1alpha1.PDModeMS
	pdGroup.Status.Mode = v1alpha1.PDModeMS
	pdGroup.Status.Conditions = []metav1.Condition{{
		Type:    v1alpha1.CondModeSwitching,
		Status:  metav1.ConditionFalse,
		Reason:  v1alpha1.ReasonModeSwitchComplete,
		Message: "PD mode switch is complete",
	}}

	got := buildPDDeploymentStatus(sharedPool, pdGroup, false)

	require.Equal(t, apiv1.PDDeploymentStatusPhase_PD_DEPLOYMENT_STATUS_PHASE_SWITCHING, got.GetPhase())
	require.Equal(t, pdDeploymentStatusReasonSwitching, got.GetReason())
	require.Equal(t, pdDeploymentStatusMessageSwitching, got.GetMessage())
}

func TestIsPDDeploymentModeStableInTargetIgnoresStaleDesiredMode(t *testing.T) {
	pdGroup := &v1alpha1.PDGroup{}
	pdGroup.Spec.Template.Spec.Mode = v1alpha1.PDModeMS
	pdGroup.Status.Mode = v1alpha1.PDModeMS

	staleSharedPool := &entity.SharedPool{
		SharedPool: apiv1.SharedPool{
			PdDeploymentMode: apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MONOLITHIC,
		},
	}
	statusFromStaleDesired := buildPDDeploymentStatus(staleSharedPool, pdGroup, false)
	require.Equal(t, apiv1.PDDeploymentStatusPhase_PD_DEPLOYMENT_STATUS_PHASE_SWITCHING, statusFromStaleDesired.GetPhase())

	require.True(t, isPDDeploymentModeStableInTarget(pdGroup, apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MICROSERVICE))

	pdGroup.Spec.Template.Spec.Mode = v1alpha1.PDModeNormal
	require.False(t, isPDDeploymentModeStableInTarget(pdGroup, apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MICROSERVICE))
	pdGroup.Spec.Template.Spec.Mode = v1alpha1.PDModeMS

	pdGroup.Status.Conditions = []metav1.Condition{{
		Type:   v1alpha1.CondModeSwitching,
		Status: metav1.ConditionTrue,
	}}
	require.False(t, isPDDeploymentModeStableInTarget(pdGroup, apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MICROSERVICE))
}

func TestBuildRunningPDDeploymentStatus(t *testing.T) {
	sharedPool := &entity.SharedPool{
		SharedPool: apiv1.SharedPool{
			PdDeploymentMode: apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MICROSERVICE,
		},
	}

	t.Run("does not infer stable microservice from spec while dag is running", func(t *testing.T) {
		pdGroup := &v1alpha1.PDGroup{}
		pdGroup.Spec.Template.Spec.Mode = v1alpha1.PDModeMS
		pdGroup.Status.Mode = v1alpha1.PDModeNormal

		inferred := buildPDDeploymentStatus(sharedPool, pdGroup, true)
		require.Equal(t, apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MICROSERVICE, inferred.GetCurrentMode())
		require.Equal(t, apiv1.PDDeploymentStatusPhase_PD_DEPLOYMENT_STATUS_PHASE_STABLE, inferred.GetPhase())

		running := buildRunningPDDeploymentStatus(buildPDDeploymentStatus(sharedPool, pdGroup, false))
		require.Equal(t, apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MONOLITHIC, running.GetCurrentMode())
		require.Equal(t, apiv1.PDDeploymentStatusPhase_PD_DEPLOYMENT_STATUS_PHASE_SWITCHING, running.GetPhase())
	})

	t.Run("keeps target mode as switching while dag is running", func(t *testing.T) {
		pdGroup := &v1alpha1.PDGroup{}
		pdGroup.Spec.Template.Spec.Mode = v1alpha1.PDModeMS
		pdGroup.Status.Mode = v1alpha1.PDModeMS
		pdGroup.Status.Conditions = []metav1.Condition{{
			Type:    v1alpha1.CondModeSwitching,
			Status:  metav1.ConditionFalse,
			Reason:  v1alpha1.ReasonModeSwitchComplete,
			Message: "PD mode switch is complete",
		}}

		running := buildRunningPDDeploymentStatus(buildPDDeploymentStatus(sharedPool, pdGroup, false))
		require.Equal(t, apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MICROSERVICE, running.GetCurrentMode())
		require.Equal(t, apiv1.PDDeploymentStatusPhase_PD_DEPLOYMENT_STATUS_PHASE_SWITCHING, running.GetPhase())
		require.Equal(t, pdDeploymentStatusReasonSwitching, running.GetReason())
		require.Equal(t, pdDeploymentStatusMessageSwitching, running.GetMessage())
	})

	t.Run("preserves unknown status while dag is running", func(t *testing.T) {
		pdGroup := &v1alpha1.PDGroup{}
		pdGroup.Status.Mode = v1alpha1.PDMode("future")

		running := buildRunningPDDeploymentStatus(buildPDDeploymentStatus(sharedPool, pdGroup, false))
		require.Equal(t, apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_UNSPECIFIED, running.GetCurrentMode())
		require.Equal(t, apiv1.PDDeploymentStatusPhase_PD_DEPLOYMENT_STATUS_PHASE_UNKNOWN, running.GetPhase())
	})
}

func TestMergePDDeploymentStatusForReadBestEffortUsesRunningStatusDuringPDModeDAG(t *testing.T) {
	tests := []struct {
		name            string
		runningDAG      bool
		wantCurrentMode apiv1.PDDeploymentMode
		wantPhase       apiv1.PDDeploymentStatusPhase
	}{
		{
			name:            "no running dag keeps legacy spec inference",
			wantCurrentMode: apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MICROSERVICE,
			wantPhase:       apiv1.PDDeploymentStatusPhase_PD_DEPLOYMENT_STATUS_PHASE_STABLE,
		},
		{
			name:            "running dag disables spec inference",
			runningDAG:      true,
			wantCurrentMode: apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MONOLITHIC,
			wantPhase:       apiv1.PDDeploymentStatusPhase_PD_DEPLOYMENT_STATUS_PHASE_SWITCHING,
		},
	}

	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 = v1alpha1.PDModeMS
			pdGroup.Status.Mode = v1alpha1.PDModeNormal
			kubeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pdGroup).Build()

			dataplaneManager := dataplanemocks.NewManager(t)
			dataplaneManager.On("GetClient", "dp-1").Return(kubeClient, nil)
			dagExecutor := executormocks.NewRemoteExecutor(t)
			dagExecutor.On("QueryRunnings", mock.Anything, dag.PlanSetSharedPoolPDDeploymentModeToMonolithic, mock.AnythingOfType("executor.MatchFilter")).
				Return(executor.NewExecTaskIdStatus(), nil)
			if tt.runningDAG {
				runningDags := executor.NewExecTaskIdStatus()
				runningDags["pd-mode-dag"] = &executor.ExecTaskStatusEx{
					ExecTaskStatus: executor.ExecTaskStatus{
						ExecTask: &executor.ExecutorTask{
							RootName: dag.PlanSetSharedPoolPDDeploymentModeToMicroservice,
							Args: actions.ActionArgs{
								workflow.SharedPoolIdKey:     actions.ActionArgString("sp-1"),
								workflow.PDDeploymentModeKey: actions.ActionArgString(apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MICROSERVICE.String()),
							},
						},
					},
				}
				dagExecutor.On("QueryRunnings", mock.Anything, dag.PlanSetSharedPoolPDDeploymentModeToMicroservice, mock.AnythingOfType("executor.MatchFilter")).
					Return(runningDags, nil)
			} else {
				dagExecutor.On("QueryRunnings", mock.Anything, dag.PlanSetSharedPoolPDDeploymentModeToMicroservice, mock.AnythingOfType("executor.MatchFilter")).
					Return(executor.NewExecTaskIdStatus(), nil)
			}
			server := &serverImpl{
				dagExecutor:      dagExecutor,
				dataplaneManager: dataplaneManager,
			}
			sharedPool := &entity.SharedPool{
				SharedPool: apiv1.SharedPool{
					Id:               "sp-1",
					DataplaneName:    "dp-1",
					PdDeploymentMode: apiv1.PDDeploymentMode_PD_DEPLOYMENT_MODE_MICROSERVICE,
					ComponentGroups: []*entity.Component{
						{Id: "default", ComponentType: apiv1.ComponentType_PD},
					},
				},
			}

			server.mergePDDeploymentStatusForReadBestEffort(context.Background(), sharedPool)

			require.NotNil(t, sharedPool.GetPdDeploymentStatus())
			require.Equal(t, tt.wantCurrentMode, sharedPool.GetPdDeploymentStatus().GetCurrentMode())
			require.Equal(t, tt.wantPhase, sharedPool.GetPdDeploymentStatus().GetPhase())
		})
	}
}
