package server

import (
	"context"
	"errors"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
	"sigs.k8s.io/controller-runtime/pkg/client/interceptor"

	"github.com/tidbcloud/cluster-controller/server/internal/util"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	apiv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/cluster-server/v1"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	gkestatus "github.com/tidbcloud/infra-provider/pkg/gkeheterogeneous/status"
	"github.com/tidbcloud/infra-provider/pkg/scheme"
)

func newTestCluster(name, namespace string, featureAnnotations map[string]string) *cloudv1.Cluster {
	annotations := make(map[string]string)
	for k, v := range featureAnnotations {
		// FeatureEnabled checks annotations with prefix "feature.cloud.pingcap.com/"
		annotations[cloudv1.AnnoKeyFeatureNamePrefix+k] = v
	}
	return &cloudv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:        name,
			Namespace:   namespace,
			Annotations: annotations,
			Labels: map[string]string{
				"tags.tidbcloud.com/cluster": name,
			},
		},
		Status: cloudv1.ClusterStatus{
			Phase: "Running",
		},
	}
}

func newGKETestCluster(name, namespace string, featureAnnotations map[string]string) *cloudv1.Cluster {
	cluster := newTestCluster(name, namespace, featureAnnotations)
	cluster.Labels[cloudv1.ProviderTypeLabel] = cloudv1.ProviderTypeGKE
	return cluster
}

func newTestClusterServer(objs ...client.Object) *ClusterServer {
	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme.Scheme).
		WithObjects(objs...).
		Build()
	return &ClusterServer{
		KubeClient:    fakeClient,
		clusterReader: &util.LabelSelectorReader{},
	}
}

func newGKEHeterogeneousStatusConfigMap(clusterName, namespace, content string) *corev1.ConfigMap {
	return &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      gkestatus.ConfigMapNamePrefix + clusterName,
			Namespace: namespace,
		},
		Data: map[string]string{
			gkestatus.ConfigMapDataKey: content,
		},
	}
}

func TestListClusterResourceMetas(t *testing.T) {
	tests := []struct {
		name           string
		cluster        *cloudv1.Cluster
		extraObjs      []client.Object
		expectedKeys   []string
		unexpectedKeys []string
	}{
		{
			name:           "cluster without scale status feature",
			cluster:        newTestCluster("test-cluster", "test-ns", nil),
			expectedKeys:   []string{"cluster_cr"},
			unexpectedKeys: []string{"scale_status_cm", "gke_heterogeneous_status_cm"},
		},
		{
			name: "cluster with scale status feature enabled",
			cluster: newTestCluster("test-cluster", "test-ns", map[string]string{
				consts.ClusterFeatureScaleStatusTrack: "true",
			}),
			expectedKeys:   []string{"cluster_cr", "scale_status_cm"},
			unexpectedKeys: []string{"gke_heterogeneous_status_cm"},
		},
		{
			name:           "gke cluster without heterogeneous status configmap hides status meta",
			cluster:        newGKETestCluster("test-cluster", "test-ns", nil),
			expectedKeys:   []string{"cluster_cr"},
			unexpectedKeys: []string{"scale_status_cm", "gke_heterogeneous_status_cm"},
		},
		{
			name:    "gke cluster with heterogeneous status configmap includes status meta",
			cluster: newGKETestCluster("test-cluster", "test-ns", nil),
			extraObjs: []client.Object{
				newGKEHeterogeneousStatusConfigMap("test-cluster", "test-ns", "probeStatus: disabled\nenabled: false\n"),
			},
			expectedKeys:   []string{"cluster_cr", "gke_heterogeneous_status_cm"},
			unexpectedKeys: []string{"scale_status_cm"},
		},
		{
			name:    "gke cluster with empty heterogeneous status configmap includes status meta",
			cluster: newGKETestCluster("test-cluster", "test-ns", nil),
			extraObjs: []client.Object{
				newGKEHeterogeneousStatusConfigMap("test-cluster", "test-ns", ""),
			},
			expectedKeys:   []string{"cluster_cr", "gke_heterogeneous_status_cm"},
			unexpectedKeys: []string{"scale_status_cm"},
		},
		{
			name: "gke cluster with scale status feature enabled but no heterogeneous status configmap",
			cluster: newGKETestCluster("test-cluster", "test-ns", map[string]string{
				consts.ClusterFeatureScaleStatusTrack: "true",
			}),
			expectedKeys:   []string{"cluster_cr", "scale_status_cm"},
			unexpectedKeys: []string{"gke_heterogeneous_status_cm"},
		},
		{
			name: "gke cluster with scale status feature and heterogeneous status configmap",
			cluster: newGKETestCluster("test-cluster", "test-ns", map[string]string{
				consts.ClusterFeatureScaleStatusTrack: "true",
			}),
			extraObjs: []client.Object{
				newGKEHeterogeneousStatusConfigMap("test-cluster", "test-ns", "probeStatus: disabled\nenabled: false\n"),
			},
			expectedKeys:   []string{"cluster_cr", "scale_status_cm", "gke_heterogeneous_status_cm"},
			unexpectedKeys: []string{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			objs := append([]client.Object{tt.cluster}, tt.extraObjs...)
			s := newTestClusterServer(objs...)
			resp, err := s.ListClusterResourceMetas(context.Background(), &apiv1.ListClusterResourceMetasRequest{
				ClusterId: tt.cluster.Name,
			})
			require.NoError(t, err)
			require.NotNil(t, resp)

			keys := make(map[string]bool)
			for _, meta := range resp.Metas {
				keys[meta.Key] = true
				assert.NotEmpty(t, meta.DisplayName)
				assert.NotEqual(t, apiv1.Type_RESOURCE_TYPE_UNSPECIFIED, meta.Type)
			}

			for _, key := range tt.expectedKeys {
				assert.True(t, keys[key], "expected key %s not found", key)
			}
			for _, key := range tt.unexpectedKeys {
				assert.False(t, keys[key], "unexpected key %s found", key)
			}
		})
	}
}

func TestListClusterResources(t *testing.T) {
	tests := []struct {
		name           string
		objs           []client.Object
		expectedKeys   []string
		unexpectedKeys []string
	}{
		{
			name: "cluster without scale status feature - only cluster_cr",
			objs: []client.Object{
				newTestCluster("test-cluster", "test-ns", nil),
			},
			expectedKeys:   []string{"cluster_cr"},
			unexpectedKeys: []string{"scale_status_cm", "gke_heterogeneous_status_cm"},
		},
		{
			name: "cluster with scale status feature but no configmap",
			objs: []client.Object{
				newTestCluster("test-cluster", "test-ns", map[string]string{
					consts.ClusterFeatureScaleStatusTrack: "true",
				}),
			},
			expectedKeys:   []string{"cluster_cr"},
			unexpectedKeys: []string{"scale_status_cm", "gke_heterogeneous_status_cm"}, // CMs not found, so not included
		},
		{
			name: "cluster with scale status feature and configmap exists",
			objs: []client.Object{
				newTestCluster("test-cluster", "test-ns", map[string]string{
					consts.ClusterFeatureScaleStatusTrack: "true",
				}),
				&corev1.ConfigMap{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "tidb-cluster-scale-status-test-cluster",
						Namespace: "test-ns",
					},
					Data: map[string]string{
						"current-scale-status": "a_cluster_msg: cluster is scaling\ncomponent_member_status: {}\n",
					},
				},
			},
			expectedKeys:   []string{"cluster_cr", "scale_status_cm"},
			unexpectedKeys: []string{"gke_heterogeneous_status_cm"},
		},
		{
			name: "cluster with scale status feature and configmap exists but empty data",
			objs: []client.Object{
				newTestCluster("test-cluster", "test-ns", map[string]string{
					consts.ClusterFeatureScaleStatusTrack: "true",
				}),
				&corev1.ConfigMap{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "tidb-cluster-scale-status-test-cluster",
						Namespace: "test-ns",
					},
					Data: map[string]string{
						"current-scale-status": "",
					},
				},
			},
			expectedKeys:   []string{"cluster_cr"},
			unexpectedKeys: []string{"scale_status_cm", "gke_heterogeneous_status_cm"}, // empty content, not included
		},
		{
			name: "gke cluster with heterogeneous status configmap exists",
			objs: []client.Object{
				newGKETestCluster("test-cluster", "test-ns", nil),
				newGKEHeterogeneousStatusConfigMap("test-cluster", "test-ns", "probeStatus: disabled\nenabled: false\n"),
			},
			expectedKeys:   []string{"cluster_cr", "gke_heterogeneous_status_cm"},
			unexpectedKeys: []string{"scale_status_cm"},
		},
		{
			name: "gke cluster with heterogeneous status configmap missing",
			objs: []client.Object{
				newGKETestCluster("test-cluster", "test-ns", nil),
			},
			expectedKeys:   []string{"cluster_cr"},
			unexpectedKeys: []string{"scale_status_cm", "gke_heterogeneous_status_cm"},
		},
		{
			name: "gke cluster with heterogeneous status configmap empty",
			objs: []client.Object{
				newGKETestCluster("test-cluster", "test-ns", nil),
				newGKEHeterogeneousStatusConfigMap("test-cluster", "test-ns", ""),
			},
			expectedKeys:   []string{"cluster_cr"},
			unexpectedKeys: []string{"scale_status_cm", "gke_heterogeneous_status_cm"},
		},
		{
			name: "non-gke cluster with heterogeneous status configmap exists",
			objs: []client.Object{
				newTestCluster("test-cluster", "test-ns", nil),
				newGKEHeterogeneousStatusConfigMap("test-cluster", "test-ns", "probeStatus: success\n"),
			},
			expectedKeys:   []string{"cluster_cr"},
			unexpectedKeys: []string{"scale_status_cm", "gke_heterogeneous_status_cm"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := newTestClusterServer(tt.objs...)
			resp, err := s.ListClusterResources(context.Background(), &apiv1.ListClusterResourcesRequest{
				ClusterId: "test-cluster",
			})
			require.NoError(t, err)
			require.NotNil(t, resp)

			for _, key := range tt.expectedKeys {
				res, ok := resp.Resources[key]
				assert.True(t, ok, "expected key %s not found", key)
				if ok {
					assert.NotEmpty(t, res.Content, "content for %s should not be empty", key)
				}
			}
			for _, key := range tt.unexpectedKeys {
				_, ok := resp.Resources[key]
				assert.False(t, ok, "unexpected key %s found", key)
			}
		})
	}
}

func TestListClusterResourcesSkipsGKEHeterogeneousStatusCMGetError(t *testing.T) {
	cluster := newGKETestCluster("test-cluster", "test-ns", nil)
	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme.Scheme).
		WithObjects(cluster).
		WithInterceptorFuncs(interceptor.Funcs{
			Get: func(ctx context.Context, c client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
				if _, ok := obj.(*corev1.ConfigMap); ok && key.Name == gkestatus.ConfigMapNamePrefix+cluster.Name {
					return errors.New("injected gke heterogeneous status configmap get failure")
				}
				return c.Get(ctx, key, obj, opts...)
			},
		}).
		Build()
	s := &ClusterServer{
		KubeClient:    fakeClient,
		clusterReader: &util.LabelSelectorReader{},
	}

	resp, err := s.ListClusterResources(context.Background(), &apiv1.ListClusterResourcesRequest{
		ClusterId: cluster.Name,
	})
	require.NoError(t, err)
	require.NotNil(t, resp)
	assert.Contains(t, resp.Resources, "cluster_cr")
	assert.NotContains(t, resp.Resources, "gke_heterogeneous_status_cm")
}

func TestClusterCRProviderGetContent(t *testing.T) {
	cluster := newTestCluster("test-cluster", "test-ns", nil)
	p := &clusterCRProvider{}

	content, err := p.GetContent(context.Background(), nil, cluster)
	require.NoError(t, err)
	assert.Contains(t, content, "name: test-cluster")
	assert.Contains(t, content, "namespace: test-ns")
}

func TestScaleStatusCMProviderIsEnabled(t *testing.T) {
	p := &scaleStatusCMProvider{}

	t.Run("disabled when feature not set", func(t *testing.T) {
		cluster := newTestCluster("test", "ns", nil)
		s := newTestClusterServer(cluster)
		assert.False(t, p.IsEnabled(context.Background(), s, cluster))
	})

	t.Run("enabled when feature set", func(t *testing.T) {
		cluster := newTestCluster("test", "ns", map[string]string{
			consts.ClusterFeatureScaleStatusTrack: "true",
		})
		s := newTestClusterServer(cluster)
		assert.True(t, p.IsEnabled(context.Background(), s, cluster))
	})
}

func TestScaleStatusCMProviderGetContent(t *testing.T) {
	p := &scaleStatusCMProvider{}

	t.Run("returns empty when configmap not found", func(t *testing.T) {
		cluster := newTestCluster("test-cluster", "test-ns", map[string]string{
			consts.ClusterFeatureScaleStatusTrack: "true",
		})
		s := newTestClusterServer(cluster)

		content, err := p.GetContent(context.Background(), s, cluster)
		require.NoError(t, err)
		assert.Empty(t, content)
	})

	t.Run("returns content when configmap exists", func(t *testing.T) {
		cluster := newTestCluster("test-cluster", "test-ns", map[string]string{
			consts.ClusterFeatureScaleStatusTrack: "true",
		})
		cm := &corev1.ConfigMap{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "tidb-cluster-scale-status-test-cluster",
				Namespace: "test-ns",
			},
			Data: map[string]string{
				"current-scale-status": "test-content",
			},
		}
		s := newTestClusterServer(cluster, cm)

		content, err := p.GetContent(context.Background(), s, cluster)
		require.NoError(t, err)
		assert.Equal(t, "test-content", content)
	})

	t.Run("returns empty when configmap data is nil", func(t *testing.T) {
		cluster := newTestCluster("test-cluster", "test-ns", map[string]string{
			consts.ClusterFeatureScaleStatusTrack: "true",
		})
		cm := &corev1.ConfigMap{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "tidb-cluster-scale-status-test-cluster",
				Namespace: "test-ns",
			},
		}
		s := newTestClusterServer(cluster, cm)

		content, err := p.GetContent(context.Background(), s, cluster)
		require.NoError(t, err)
		assert.Empty(t, content)
	})
}

func TestGKEHeterogeneousStatusCMProviderIsEnabled(t *testing.T) {
	p := &gkeHeterogeneousStatusCMProvider{}

	t.Run("disabled for non-gke cluster", func(t *testing.T) {
		cluster := newTestCluster("test", "ns", nil)
		s := newTestClusterServer(cluster)
		assert.False(t, p.IsEnabled(context.Background(), s, cluster))
	})

	t.Run("disabled for gke cluster without heterogeneous status configmap", func(t *testing.T) {
		cluster := newGKETestCluster("test", "ns", nil)
		s := newTestClusterServer(cluster)
		assert.False(t, p.IsEnabled(context.Background(), s, cluster))
	})

	t.Run("enabled for gke cluster with heterogeneous status configmap", func(t *testing.T) {
		cluster := newGKETestCluster("test", "ns", nil)
		cm := newGKEHeterogeneousStatusConfigMap("test", "ns", "probeStatus: disabled\nenabled: false\n")
		s := newTestClusterServer(cluster, cm)
		assert.True(t, p.IsEnabled(context.Background(), s, cluster))
	})

	t.Run("enabled for gke cluster with empty heterogeneous status configmap", func(t *testing.T) {
		cluster := newGKETestCluster("test", "ns", nil)
		cm := newGKEHeterogeneousStatusConfigMap("test", "ns", "")
		s := newTestClusterServer(cluster, cm)
		assert.True(t, p.IsEnabled(context.Background(), s, cluster))
	})

	t.Run("disabled for gke cluster with heterogeneous intent but no status configmap", func(t *testing.T) {
		cluster := newGKETestCluster("test", "ns", nil)
		cluster.UpdateAnnotation(cloudv1.AnnoKeyGKEComputeClassForTiKVBindings, `{"computeClassName":"tikv-priority","nodePools":[]}`)
		s := newTestClusterServer(cluster)
		assert.False(t, p.IsEnabled(context.Background(), s, cluster))
	})
}

func TestGKEHeterogeneousStatusCMProviderGetContent(t *testing.T) {
	p := &gkeHeterogeneousStatusCMProvider{}

	t.Run("returns empty when configmap not found", func(t *testing.T) {
		cluster := newGKETestCluster("test-cluster", "test-ns", nil)
		s := newTestClusterServer(cluster)

		content, err := p.GetContent(context.Background(), s, cluster)
		require.NoError(t, err)
		assert.Empty(t, content)
	})

	t.Run("returns content when configmap exists", func(t *testing.T) {
		cluster := newGKETestCluster("test-cluster", "test-ns", nil)
		cm := newGKEHeterogeneousStatusConfigMap("test-cluster", "test-ns", "probeStatus: failed\nprobeError: test\n")
		s := newTestClusterServer(cluster, cm)

		content, err := p.GetContent(context.Background(), s, cluster)
		require.NoError(t, err)
		assert.Equal(t, "probeStatus: failed\nprobeError: test\n", content)
	})

	t.Run("returns empty when configmap data is nil", func(t *testing.T) {
		cluster := newGKETestCluster("test-cluster", "test-ns", nil)
		cm := &corev1.ConfigMap{
			ObjectMeta: metav1.ObjectMeta{
				Name:      gkestatus.ConfigMapNamePrefix + "test-cluster",
				Namespace: "test-ns",
			},
		}
		s := newTestClusterServer(cluster, cm)

		content, err := p.GetContent(context.Background(), s, cluster)
		require.NoError(t, err)
		assert.Empty(t, content)
	})
}

func TestListClusterResourcesClusterNotFound(t *testing.T) {
	s := newTestClusterServer() // no objects
	_, err := s.ListClusterResources(context.Background(), &apiv1.ListClusterResourcesRequest{
		ClusterId: "nonexistent",
	})
	assert.Error(t, err)
}

func TestListClusterResourceMetasClusterNotFound(t *testing.T) {
	s := newTestClusterServer() // no objects
	_, err := s.ListClusterResourceMetas(context.Background(), &apiv1.ListClusterResourceMetasRequest{
		ClusterId: "nonexistent",
	})
	assert.Error(t, err)
}
