package action

import (
	"context"
	"errors"
	"testing"

	"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"
	repomocks "github.com/tidbcloud/cluster-service-ng/pkg/storage/repo/mocks"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
	"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
)

func TestApplyTiDBSemConfigAction(t *testing.T) {
	tests := []struct {
		name          string
		args          actions.ActionArgs
		setupMocks    func(*repomocks.SharedPoolRepo, *clusterconfigmocks.Client, *dataplanemocks.Manager)
		fakeClient    func() client.Client
		expectedError bool
		errorContains string
	}{
		{
			name: "missing sharedPoolId arg",
			args: actions.ActionArgs{
				"trace_id": actions.ActionArgString("test-trace"),
			},
			setupMocks: func(sp *repomocks.SharedPoolRepo, cc *clusterconfigmocks.Client, dp *dataplanemocks.Manager) {
			},
			expectedError: true,
			errorContains: "failed to get sharedPool id",
		},
		{
			name: "shared pool load failure",
			args: actions.ActionArgs{
				"trace_id":               actions.ActionArgString("test-trace"),
				workflow.SharedPoolIdKey: actions.ActionArgString("sp-1"),
			},
			setupMocks: func(sp *repomocks.SharedPoolRepo, cc *clusterconfigmocks.Client, dp *dataplanemocks.Manager) {
				sp.On("Load", mock.Anything, "sp-1", false).Return(nil, errors.New("not found"))
			},
			expectedError: true,
			errorContains: "failed to load sharedPool",
		},
		{
			name: "config-center GetClusterTiDBSemConfig failure",
			args: actions.ActionArgs{
				"trace_id":               actions.ActionArgString("test-trace"),
				workflow.SharedPoolIdKey: actions.ActionArgString("sp-1"),
			},
			setupMocks: func(sp *repomocks.SharedPoolRepo, cc *clusterconfigmocks.Client, dp *dataplanemocks.Manager) {
				sp.On("Load", mock.Anything, "sp-1", false).Return(newSemConfigTestSharedPool(), nil)
				cc.On("GetClusterTiDBSemConfig", mock.Anything, "sp-1").Return("", errors.New("config-center error"))
			},
			expectedError: true,
			errorContains: "failed to get tidb sem config",
		},
		{
			name: "dataplane GetClient failure",
			args: actions.ActionArgs{
				"trace_id":               actions.ActionArgString("test-trace"),
				workflow.SharedPoolIdKey: actions.ActionArgString("sp-1"),
			},
			setupMocks: func(sp *repomocks.SharedPoolRepo, cc *clusterconfigmocks.Client, dp *dataplanemocks.Manager) {
				pool := newSemConfigTestSharedPool()
				sp.On("Load", mock.Anything, "sp-1", false).Return(pool, nil)
				cc.On("GetClusterTiDBSemConfig", mock.Anything, "sp-1").Return(`{"sem": true}`, nil)
				dp.On("GetClient", pool.GetDataplaneName()).Return(nil, errors.New("client not found"))
			},
			expectedError: true,
			errorContains: "failed to get remote client for shared pool",
		},
		{
			name: "CreateOrUpdate failure",
			args: actions.ActionArgs{
				"trace_id":               actions.ActionArgString("test-trace"),
				workflow.SharedPoolIdKey: actions.ActionArgString("sp-1"),
			},
			setupMocks: func(sp *repomocks.SharedPoolRepo, cc *clusterconfigmocks.Client, dp *dataplanemocks.Manager) {
				pool := newSemConfigTestSharedPool()
				sp.On("Load", mock.Anything, "sp-1", false).Return(pool, nil)
				cc.On("GetClusterTiDBSemConfig", mock.Anything, "sp-1").Return(`{"sem": true}`, nil)
			},
			fakeClient: func() client.Client {
				scheme := runtime.NewScheme()
				_ = corev1.AddToScheme(scheme)
				return fake.NewClientBuilder().
					WithScheme(scheme).
					WithInterceptorFuncs(interceptor.Funcs{
						Get: func(ctx context.Context, client client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
							return errors.New("k8s api error")
						},
					}).
					Build()
			},
			expectedError: true,
			errorContains: "k8s api error",
		},
		{
			name: "success",
			args: actions.ActionArgs{
				"trace_id":               actions.ActionArgString("test-trace"),
				workflow.SharedPoolIdKey: actions.ActionArgString("sp-1"),
			},
			setupMocks: func(sp *repomocks.SharedPoolRepo, cc *clusterconfigmocks.Client, dp *dataplanemocks.Manager) {
				pool := newSemConfigTestSharedPool()
				sp.On("Load", mock.Anything, "sp-1", false).Return(pool, nil)
				cc.On("GetClusterTiDBSemConfig", mock.Anything, "sp-1").Return(`{"sem": true}`, nil)
			},
			fakeClient: func() client.Client {
				scheme := runtime.NewScheme()
				_ = corev1.AddToScheme(scheme)
				return fake.NewClientBuilder().WithScheme(scheme).Build()
			},
			expectedError: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
			clusterConfigClient := clusterconfigmocks.NewClient(t)
			dataplaneManager := dataplanemocks.NewManager(t)

			tt.setupMocks(sharedPoolRepo, clusterConfigClient, dataplaneManager)

			// Wire up the fake k8s client to the dataplane manager mock when needed
			if tt.fakeClient != nil {
				pool := newSemConfigTestSharedPool()
				dataplaneManager.On("GetClient", pool.GetDataplaneName()).Return(tt.fakeClient(), nil)
			}

			a := &ApplyTiDBSemConfigAction{
				sharedPoolRepo:      sharedPoolRepo,
				dataplaneManager:    dataplaneManager,
				clusterConfigClient: clusterConfigClient,
			}

			err := a.Invoke(tt.args)
			if tt.expectedError {
				require.Error(t, err)
				if tt.errorContains != "" {
					require.Contains(t, err.Error(), tt.errorContains)
				}
			} else {
				require.NoError(t, err)
			}
		})
	}
}

func TestApplyTiDBSemConfigAction_Success_VerifyConfigMap(t *testing.T) {
	sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
	clusterConfigClient := clusterconfigmocks.NewClient(t)
	dataplaneManager := dataplanemocks.NewManager(t)

	pool := newSemConfigTestSharedPool()
	semConfigJSON := `{"sem_enabled": true, "rules": ["rule1", "rule2"]}`

	sharedPoolRepo.On("Load", mock.Anything, "sp-1", false).Return(pool, nil)
	clusterConfigClient.On("GetClusterTiDBSemConfig", mock.Anything, "sp-1").Return(semConfigJSON, nil)

	scheme := runtime.NewScheme()
	require.NoError(t, corev1.AddToScheme(scheme))
	remoteClient := fake.NewClientBuilder().WithScheme(scheme).Build()
	dataplaneManager.On("GetClient", pool.GetDataplaneName()).Return(remoteClient, nil)

	a := &ApplyTiDBSemConfigAction{
		sharedPoolRepo:      sharedPoolRepo,
		dataplaneManager:    dataplaneManager,
		clusterConfigClient: clusterConfigClient,
	}

	err := a.Invoke(actions.ActionArgs{
		"trace_id":               actions.ActionArgString("test-trace"),
		workflow.SharedPoolIdKey: actions.ActionArgString("sp-1"),
	})
	require.NoError(t, err)

	// Verify the ConfigMap was created with correct data
	cm := &corev1.ConfigMap{}
	err = remoteClient.Get(context.Background(), types.NamespacedName{
		Name:      semConfigMapName,
		Namespace: pool.GetDataPlaneNamespace(),
	}, cm)
	require.NoError(t, err)
	require.JSONEq(t, semConfigJSON, cm.Data["sem.json"])
}

func newSemConfigTestSharedPool() *entity.SharedPool {
	return &entity.SharedPool{
		SharedPool: apiv1.SharedPool{
			Id:            "sp-1",
			DataplaneName: "dp-test",
		},
	}
}
