package action

import (
	"context"
	"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"
	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"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	k8sruntime "k8s.io/apimachinery/pkg/runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

func TestEnsureClusterUpgradePolicyAction_Start(t *testing.T) {
	revisionHistoryLimit := int32(7)
	cluster := &v1alpha1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:       "db",
			Namespace:  "tidbsp-1",
			Generation: 3,
		},
		Spec: v1alpha1.ClusterSpec{
			UpgradePolicy:        v1alpha1.UpgradePolicyDefault,
			Paused:               true,
			RevisionHistoryLimit: &revisionHistoryLimit,
		},
		Status: v1alpha1.ClusterStatus{
			ObservedGeneration: 3,
		},
	}
	remoteCli := newUpgradePolicyActionTestClient(t, cluster)
	action := newUpgradePolicyActionForTest(t, remoteCli)

	err := action.Start(upgradePolicyActionArgs(true))
	require.NoError(t, err)

	updated := &v1alpha1.Cluster{}
	require.NoError(t, remoteCli.Get(
		context.Background(),
		client.ObjectKey{Name: "db", Namespace: "tidbsp-1"},
		updated,
	))
	assert.Equal(t, v1alpha1.UpgradePolicyNoConstraints, updated.Spec.UpgradePolicy)
	assert.True(t, updated.Spec.Paused)
	require.NotNil(t, updated.Spec.RevisionHistoryLimit)
	assert.Equal(t, revisionHistoryLimit, *updated.Spec.RevisionHistoryLimit)
}

func TestEnsureClusterUpgradePolicyAction_CheckFinish(t *testing.T) {
	testCases := []struct {
		name               string
		generation         int64
		observedGeneration int64
		expectedFinished   bool
	}{
		{
			name:               "waits for updated generation",
			generation:         4,
			observedGeneration: 3,
			expectedFinished:   false,
		},
		{
			name:               "finishes after updated generation is observed",
			generation:         4,
			observedGeneration: 4,
			expectedFinished:   true,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			cluster := &v1alpha1.Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Name:       "db",
					Namespace:  "tidbsp-1",
					Generation: tc.generation,
				},
				Spec: v1alpha1.ClusterSpec{
					UpgradePolicy: v1alpha1.UpgradePolicyNoConstraints,
				},
				Status: v1alpha1.ClusterStatus{
					ObservedGeneration: tc.observedGeneration,
				},
			}
			action := newUpgradePolicyActionForTest(t, newUpgradePolicyActionTestClient(t, cluster))

			finished, err := action.CheckFinish(upgradePolicyActionArgs(true))
			require.NoError(t, err)
			assert.Equal(t, tc.expectedFinished, finished)
		})
	}
}

func TestEnsureClusterUpgradePolicyAction_SkipsUnrelatedUpdates(t *testing.T) {
	action := &EnsureClusterUpgradePolicyAction{}

	require.NoError(t, action.Start(actions.ActionArgs{}))
	finished, err := action.CheckFinish(actions.ActionArgs{})
	require.NoError(t, err)
	assert.True(t, finished)
}

func TestShouldEnsureNoConstraintsUpgradePolicy(t *testing.T) {
	testCases := []struct {
		name            string
		args            actions.ActionArgs
		expected        bool
		expectedErrText string
	}{
		{
			name:     "missing flag",
			args:     actions.ActionArgs{},
			expected: false,
		},
		{
			name: "false flag",
			args: actions.ActionArgs{
				workflow.EnsureNoConstraintsUpgradePolicyKey: actions.ActionArgBool(false),
			},
			expected: false,
		},
		{
			name: "true flag",
			args: actions.ActionArgs{
				workflow.EnsureNoConstraintsUpgradePolicyKey: actions.ActionArgBool(true),
			},
			expected: true,
		},
		{
			name: "invalid flag",
			args: actions.ActionArgs{
				workflow.EnsureNoConstraintsUpgradePolicyKey: actions.ActionArgString("invalid"),
			},
			expectedErrText: "failed to get cluster upgrade policy flag",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			got, err := shouldEnsureNoConstraintsUpgradePolicy(tc.args)
			if tc.expectedErrText != "" {
				require.ErrorContains(t, err, tc.expectedErrText)
				return
			}
			require.NoError(t, err)
			assert.Equal(t, tc.expected, got)
		})
	}
}

func newUpgradePolicyActionForTest(
	t *testing.T,
	remoteCli client.Client,
) *EnsureClusterUpgradePolicyAction {
	t.Helper()

	sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
	sharedPoolRepo.On("Load", mock.Anything, "sp-1", false).Return(&entity.SharedPool{
		SharedPool: apiv1.SharedPool{
			Id:            "sp-1",
			DataplaneName: "dp-1",
		},
	}, nil)
	dataplaneManager := dataplanemocks.NewManager(t)
	dataplaneManager.On("GetClient", "dp-1").Return(remoteCli, nil)

	return &EnsureClusterUpgradePolicyAction{
		sharedPoolRepo:   sharedPoolRepo,
		dataplaneManager: dataplaneManager,
	}
}

func newUpgradePolicyActionTestClient(
	t *testing.T,
	cluster *v1alpha1.Cluster,
) client.Client {
	t.Helper()

	scheme := k8sruntime.NewScheme()
	require.NoError(t, v1alpha1.AddToScheme(scheme))
	return fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(cluster).
		Build()
}

func upgradePolicyActionArgs(required bool) actions.ActionArgs {
	return actions.ActionArgs{
		workflow.SharedPoolIdKey:                     actions.ActionArgString("sp-1"),
		workflow.EnsureNoConstraintsUpgradePolicyKey: actions.ActionArgBool(required),
	}
}
