package gkeheterogeneous

import (
	"testing"

	gkev1beta1 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta1"
	commonv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/stretchr/testify/require"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/validation"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	apicommon "github.com/tidbcloud/infra-provider/api/tidbcloud/common"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	nodepoolutil "github.com/tidbcloud/infra-provider/pkg/nodepool"
)

func TestConfiguredTiDBGroupIDsUsesDefaultGroupIDForUnsuffixedBinding(t *testing.T) {
	cluster := newTestGKEIntentCluster("default-group")
	setTestTiDBComputeClassBinding(t, cluster, cloudv1.AnnoKeyGKEComputeClassForTiDBBindings)

	require.Equal(t, []string{"default-group"}, ConfiguredTiDBGroupIDs(cluster))
}

func TestConfiguredTiDBGroupIDsKeepsEmptyDefaultGroupSentinel(t *testing.T) {
	cluster := newTestGKEIntentCluster("")
	computeClassName := setTestTiDBComputeClassBinding(t, cluster, cloudv1.AnnoKeyGKEComputeClassForTiDBBindings)

	require.Equal(t, []string{""}, ConfiguredTiDBGroupIDs(cluster))

	refs, err := computeClassIntentRefsForCluster(cluster, "")
	require.NoError(t, err)
	require.Equal(t, []ComputeClassIntentRef{
		{
			ClusterName:      "cluster-a",
			ClusterNamespace: "ns-a",
			ComponentType:    apicommon.ComponentType_TIDB,
			TiDBGroupID:      "",
			ComputeClassName: computeClassName,
		},
	}, refs)
}

func TestConfiguredTiDBGroupIDsSkipsEmptySuffixBinding(t *testing.T) {
	cluster := newTestGKEIntentCluster("")
	cluster.UpdateAnnotation(cloudv1.AnnoKeyGKEComputeClassForTiDBBindings+"-", "not-empty")

	require.Empty(t, ConfiguredTiDBGroupIDs(cluster))
}

func TestConfiguredTiDBGroupIDsIncludesOrphanSuffixBinding(t *testing.T) {
	cluster := newTestGKEIntentCluster("default-group")
	setTestTiDBComputeClassBinding(t, cluster, cloudv1.AnnoKeyGKEComputeClassForTiDBBindings)
	setTestTiDBComputeClassBinding(t, cluster, cluster.TiDBGKEComputeClassBindingsAnnoKey("orphan"))

	require.Equal(t, []string{"default-group", "orphan"}, ConfiguredTiDBGroupIDs(cluster))
}

func TestComputeClassIntentRefsSkipsWellFormedUnrelatedInvalidBinding(t *testing.T) {
	target := newTestGKEIntentCluster("")
	targetComputeClassName := setTestTiDBComputeClassBinding(t, target, cloudv1.AnnoKeyGKEComputeClassForTiDBBindings)
	unrelated := newTestGKEIntentCluster("")
	unrelated.Name = "cluster-b"
	unrelated.UpdateAnnotation(
		cloudv1.AnnoKeyGKEComputeClassForTiDBBindings,
		`{"computeClassName":"other-priority","nodePools":[{"role":"extra","instanceType":"n2-standard-16","nodePoolName":"np-extra"}]}`,
	)

	refs, err := ComputeClassIntentRefs([]*cloudv1.Cluster{target, unrelated}, targetComputeClassName)
	require.NoError(t, err)
	require.Equal(t, []ComputeClassIntentRef{
		{
			ClusterName:      "cluster-a",
			ClusterNamespace: "ns-a",
			ComponentType:    apicommon.ComponentType_TIDB,
			TiDBGroupID:      "",
			ComputeClassName: targetComputeClassName,
		},
	}, refs)
}

func TestComputeClassIntentRefsFailsClosedForMalformedBinding(t *testing.T) {
	cluster := newTestGKEIntentCluster("")
	cluster.UpdateAnnotation(cloudv1.AnnoKeyGKEComputeClassForTiDBBindings, `{"computeClassName":`)

	_, err := ComputeClassIntentRefs([]*cloudv1.Cluster{cluster}, "target-priority")
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "decode gke compute class binding annotation")
}

func TestComputeClassIntentRefsFailsClosedForTargetInvalidBinding(t *testing.T) {
	cluster := newTestGKEIntentCluster("")
	cluster.UpdateAnnotation(
		cloudv1.AnnoKeyGKEComputeClassForTiDBBindings,
		`{"computeClassName":"target-priority","nodePools":[{"role":"extra","instanceType":"n2-standard-16","nodePoolName":"np-extra"}]}`,
	)

	_, err := ComputeClassIntentRefs([]*cloudv1.Cluster{cluster}, "target-priority")
	require.Error(t, err)
	require.Equal(t, codes.FailedPrecondition, status.Code(err))
	require.Contains(t, err.Error(), "must contain exactly 2 nodePools")
}

func TestNodePoolNameLongMachineTypeUsesLegalGKEName(t *testing.T) {
	cluster := newTestGKEIntentCluster("")
	cluster.EnableFeature(consts.ClusterFeatureGCP3LayerNodePool)
	cluster.UpdateAnnotation(consts.AnnoKeyControlPlaneVersion, "1.30.4-gke.1348000")

	got := NodePoolName(cluster, "n2-custom-128-524288")

	require.LessOrEqual(t, len(got), namer.GKENameLimit)
	require.Empty(t, validation.IsDNS1035Label(got), got)
}

func TestNodePoolReadyForGatekeeper(t *testing.T) {
	const computeClass = "shared-priority"

	readyConditions := []commonv1.Condition{
		{Type: commonv1.TypeReady, Status: corev1.ConditionTrue},
		{Type: commonv1.TypeSynced, Status: corev1.ConditionTrue},
	}
	unreadyConditions := []commonv1.Condition{
		{Type: commonv1.TypeReady, Status: corev1.ConditionTrue},
		{Type: commonv1.TypeSynced, Status: corev1.ConditionFalse},
	}

	tests := []struct {
		name              string
		desired           string
		applied           string
		specLabel         string
		specTaint         string
		isolated          bool
		conditions        []commonv1.Condition
		wantReady         bool
		wantErrCode       codes.Code
		wantErrContains   string
		wantSpecLabelKept string
	}{
		{
			name:        "ready when all metadata matches and isolation taint is gone",
			desired:     computeClass,
			applied:     computeClass,
			specLabel:   computeClass,
			specTaint:   computeClass,
			conditions:  readyConditions,
			wantReady:   true,
			wantErrCode: codes.OK,
		},
		{
			name:        "missing desired is not ready",
			applied:     computeClass,
			specLabel:   computeClass,
			specTaint:   computeClass,
			conditions:  readyConditions,
			wantErrCode: codes.OK,
		},
		{
			name:        "missing applied is not ready",
			desired:     computeClass,
			specLabel:   computeClass,
			specTaint:   computeClass,
			conditions:  readyConditions,
			wantErrCode: codes.OK,
		},
		{
			name:              "spec label mismatch fails closed",
			desired:           computeClass,
			applied:           computeClass,
			specLabel:         "old-priority",
			specTaint:         computeClass,
			conditions:        readyConditions,
			wantErrCode:       codes.FailedPrecondition,
			wantErrContains:   "spec label",
			wantSpecLabelKept: "old-priority",
		},
		{
			name:        "missing spec taint is not ready",
			desired:     computeClass,
			applied:     computeClass,
			specLabel:   computeClass,
			conditions:  readyConditions,
			wantErrCode: codes.OK,
		},
		{
			name:            "spec taint mismatch fails closed",
			desired:         computeClass,
			applied:         computeClass,
			specLabel:       computeClass,
			specTaint:       "old-priority",
			conditions:      readyConditions,
			wantErrCode:     codes.FailedPrecondition,
			wantErrContains: "spec taint",
		},
		{
			name:        "isolation taint present is not ready",
			desired:     computeClass,
			applied:     computeClass,
			specLabel:   computeClass,
			specTaint:   computeClass,
			isolated:    true,
			conditions:  readyConditions,
			wantErrCode: codes.OK,
		},
		{
			name:        "not ready when crossplane is not synced",
			desired:     computeClass,
			applied:     computeClass,
			specLabel:   computeClass,
			specTaint:   computeClass,
			conditions:  unreadyConditions,
			wantErrCode: codes.OK,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			nodePool := &gkev1beta1.NodePool{
				ObjectMeta: metav1.ObjectMeta{Name: "shared-pool"},
			}
			nodePool.Status.SetConditions(tt.conditions...)
			nodepoolutil.SetGKEDesiredComputeClassForNodePool(nodePool, tt.desired)
			nodepoolutil.SetGKEAppliedComputeClassForNodePool(nodePool, tt.applied)
			nodepoolutil.SetGKEComputeClassSpecLabelForNodePool(nodePool, tt.specLabel)
			nodepoolutil.SetGKEComputeClassSpecTaintForNodePool(nodePool, tt.specTaint)
			if tt.isolated {
				nodepoolutil.SetGKEHeterogeneousNodePoolIsolationTaint(nodePool)
			}

			ready, err := NodePoolReadyForGatekeeper(nodePool, computeClass)

			require.Equal(t, tt.wantReady, ready)
			require.Equal(t, tt.wantErrCode, status.Code(err))
			if tt.wantErrContains != "" {
				require.ErrorContains(t, err, tt.wantErrContains)
			}
			if tt.wantSpecLabelKept != "" {
				require.Equal(t, tt.wantSpecLabelKept, nodePool.Spec.ForProvider.Config.Labels[consts.GKEComputeClassLabelKey])
			}
		})
	}
}

func TestNodePoolComputeClassRefsIncludesSpecTaint(t *testing.T) {
	const computeClass = "shared-priority"
	specTaintOnly := gkev1beta1.NodePool{ObjectMeta: metav1.ObjectMeta{Name: "spec-taint-only"}}
	nodepoolutil.SetGKEComputeClassSpecTaintForNodePool(&specTaintOnly, computeClass)
	unrelated := gkev1beta1.NodePool{ObjectMeta: metav1.ObjectMeta{Name: "unrelated"}}
	nodepoolutil.SetGKEComputeClassSpecTaintForNodePool(&unrelated, "other-priority")

	refs := NodePoolComputeClassRefs([]gkev1beta1.NodePool{specTaintOnly, unrelated}, computeClass)

	require.Equal(t, []ComputeClassNodePoolRef{
		{
			NodePoolName:              "spec-taint-only",
			SpecTaintComputeClassName: computeClass,
		},
	}, refs)
	require.Contains(t, FormatComputeClassNodePoolRefs(refs), "spec_taint=shared-priority")

	nodepoolutil.ClearGKEComputeClassSpecTaintForNodePool(&specTaintOnly)
	require.Empty(t, NodePoolComputeClassRefs([]gkev1beta1.NodePool{specTaintOnly, unrelated}, computeClass))
}

func newTestGKEIntentCluster(defaultGroupID string) *cloudv1.Cluster {
	annotations := map[string]string{}
	if defaultGroupID != "" {
		annotations[cloudv1.DefaultLoadBalancerIDAnnotationKey] = defaultGroupID
	}
	return &cloudv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "cluster-a",
			Namespace: "ns-a",
			Labels: map[string]string{
				cloudv1.ProviderTypeLabel: cloudv1.ProviderTypeGKE,
			},
			Annotations: annotations,
		},
		Spec: cloudv1.ClusterSpec{
			Kubernetes: cloudv1.Kubernetes{
				Name: "gke-a",
			},
			Cluster: cloudv1.ClusterTopology{
				TiDB: cloudv1.TiDB{InstanceType: "n2-standard-8"},
			},
		},
	}
}

func setTestTiDBComputeClassBinding(t *testing.T, cluster *cloudv1.Cluster, key string) string {
	t.Helper()
	const (
		baseInstanceType  = "n2-standard-8"
		extraInstanceType = "n2-standard-16"
	)
	annotation := cloudv1.GKEComputeClassBindingAnnotation{
		NodePools: []cloudv1.GKEComputeClassNodePoolBinding{
			{
				Role:         cloudv1.GKEComputeClassNodePoolRoleBase,
				InstanceType: baseInstanceType,
				NodePoolName: NodePoolName(cluster, baseInstanceType),
			},
			{
				Role:         cloudv1.GKEComputeClassNodePoolRoleExtra,
				InstanceType: extraInstanceType,
				NodePoolName: NodePoolName(cluster, extraInstanceType),
			},
		},
	}
	computeClassName, err := cloudv1.GKEComputeClassNameForBindings(annotation.NodePools)
	require.NoError(t, err)
	annotation.ComputeClassName = computeClassName
	value, err := cloudv1.EncodeGKEComputeClassBindingAnnotation(annotation, key, baseInstanceType, NodePoolName(cluster, baseInstanceType))
	require.NoError(t, err)
	cluster.UpdateAnnotation(key, value)
	return computeClassName
}
