package tioperator

import (
	"context"
	"testing"

	"github.com/pingcap/tidb-operator/api/v2/core/v1alpha1"
	"github.com/stretchr/testify/assert"
	"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"
	schemav1 "github.com/tidbcloud/cluster-service-ng/pkg/storage/etcd/schema/v1"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	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"
)

func TestListTiProxyGroups(t *testing.T) {
	clusterId := "proxy-a"
	namespace := dataplaneNamespace(clusterId)

	proxyOneId := testComponentId("proxy-a")
	proxyTwoId := testComponentId("proxy-b")

	proxyOne := newTiProxyGroup(t, namespace, proxyOneId, 2)
	proxyTwo := newTiProxyGroup(t, namespace, proxyTwoId, 3)

	scheme := buildTestScheme(t)
	cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(proxyOne, proxyTwo).Build()

	ctx := context.Background()
	comps, err := ListTiProxyGroups(ctx, cli, clusterId)
	require.NoError(t, err)
	require.Len(t, comps, 2)
	for _, comp := range comps {
		assert.Equal(t, apiv1.ComponentType_TIPROXY, comp.GetComponentType())
	}
}

func TestGetTiProxyGroup(t *testing.T) {
	clusterId := "proxy-b"
	namespace := dataplaneNamespace(clusterId)

	proxyId := testComponentId("proxy-b")
	proxy := newTiProxyGroup(t, namespace, proxyId, 2)

	scheme := buildTestScheme(t)
	cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(proxy).Build()

	ctx := context.Background()
	groupId := groupIdWithComponent(proxyId, "tiproxy")
	comp, err := GetTiProxyGroup(ctx, cli, clusterId, groupId)
	require.NoError(t, err)
	require.NotNil(t, comp)
	assert.Equal(t, apiv1.ComponentType_TIPROXY, comp.GetComponentType())
	assert.Equal(t, proxyId, comp.GetId())
	assert.Equal(t, uint32(2), comp.GetNodeQuota().GetNodeCount())
}

func TestUpdateTiProxyGroup(t *testing.T) {
	clusterId := "proxy-c"
	namespace := dataplaneNamespace(clusterId)

	proxyId := testComponentId("proxy-c")
	proxy := newTiProxyGroup(t, namespace, proxyId, 1)
	maxSurge := int32(1024)
	proxy.Spec.MaxSurge = &maxSurge

	scheme := buildTestScheme(t)
	cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(proxy).Build()

	ctx := context.Background()
	options := []UpdateOption{{Option: OpsUpdateNodeCount, Value: uint32(2)}}
	groupId := groupIdWithComponent(proxyId, "tiproxy")
	updated, err := UpdateTiProxyGroup(ctx, cli, clusterId, groupId, options)
	require.NoError(t, err)
	require.NotNil(t, updated)
	assert.Equal(t, uint32(2), updated.GetNodeQuota().GetNodeCount())

	stored := &v1alpha1.TiProxyGroup{}
	err = cli.Get(ctx, client.ObjectKey{Namespace: namespace, Name: groupNameWithComponent(proxyId, "tiproxy")}, stored)
	require.NoError(t, err)
	require.NotNil(t, stored.Spec.Replicas)
	assert.Equal(t, int32(2), *stored.Spec.Replicas)
	require.NotNil(t, stored.Spec.MaxSurge)
	assert.Equal(t, int32(1024), *stored.Spec.MaxSurge)
}

func TestGetUpdateExpectedTiProxyGroup(t *testing.T) {
	clusterId := "proxy-update"
	namespace := dataplaneNamespace(clusterId)
	proxyId := testComponentId("proxy-update")

	base := newTiProxyGroup(t, namespace, proxyId, 3)

	nodeSpec := &entity.NodeSpec{Cpu: 8, MemoryGi: 32, AvailableCpu: 8, AvailableMemoryMi: 32768}

	cases := []struct {
		name            string
		mutate          func(*v1alpha1.TiProxyGroup)
		options         []UpdateOption
		wantUpdate      bool
		wantErrContains string
		wantErrCode     codes.Code
		verify          func(*testing.T, *v1alpha1.TiProxyGroup)
	}{
		{
			name: "multi-option-update",
			options: []UpdateOption{
				{Option: OpsUpdateNodeCount, Value: uint32(4)},
				{Option: OpsUpdateNodeSpec, Value: nodeSpec},
				{Option: OpsUpdateMaxCount, Value: uint32(8)},
				{Option: OpsUpdateMinCount, Value: uint32(2)},
				{Option: OpsUpdateVersion, Value: "v2.2.0"},
				{Option: OpsUpdateImage, Value: "repo/tiproxy:new"},
				{Option: OpsUpdateConfig, Value: "tiproxy-config"},
				{Option: OpsUpdateUpdateStrategy, Value: v1alpha1.ConfigUpdateStrategyHotReload},
			},
			wantUpdate: true,
			verify: func(t *testing.T, group *v1alpha1.TiProxyGroup) {
				t.Helper()
				require.NotNil(t, group.Spec.Replicas)
				assert.Equal(t, int32(4), *group.Spec.Replicas)

				container := tiproxyContainerByName(group, "tiproxy")
				require.NotNil(t, container)

				assertMainContainerResources(t, container.Resources, nodeSpec, apiv1.ComponentType_TIPROXY)

				assert.Equal(t, "v2.2.0", group.Spec.Template.Spec.Version)
				require.NotNil(t, group.Spec.Template.Spec.Image)
				assert.Equal(t, "repo/tiproxy:new", *group.Spec.Template.Spec.Image)
				assert.Equal(t, v1alpha1.ConfigFile("tiproxy-config"), group.Spec.Template.Spec.Config)
				assert.Equal(t, v1alpha1.ConfigUpdateStrategyHotReload, group.Spec.Template.Spec.UpdateStrategy.Config)

				annotations := group.GetAnnotations()
				assert.Equal(t, "8", annotations[schemav1.AnnotationsComponentMaxCount])
				assert.Equal(t, "2", annotations[schemav1.AnnotationsComponentMinCount])

				assert.Equal(t, "8C32G", group.Labels[schemav1.LabelNodeSpecTypeKey])
			},
		},
		{
			name:            "invalid-node-count-type",
			options:         []UpdateOption{{Option: OpsUpdateNodeCount, Value: "bad"}},
			wantErrContains: "invalid node count",
		},
		{
			name:        "node-count-exceeds-max",
			options:     []UpdateOption{{Option: OpsUpdateNodeCount, Value: uint32(10)}},
			wantErrCode: codes.InvalidArgument,
		},
		{
			name:            "invalid-node-spec-type",
			options:         []UpdateOption{{Option: OpsUpdateNodeSpec, Value: "bad"}},
			wantErrContains: "invalid node spec",
		},
		{
			name:            "invalid-config-type",
			options:         []UpdateOption{{Option: OpsUpdateConfig, Value: 123}},
			wantErrContains: "invalid config",
		},
		{
			name:            "invalid-update-strategy-type",
			options:         []UpdateOption{{Option: OpsUpdateUpdateStrategy, Value: false}},
			wantErrContains: "invalid update strategy",
		},
		{
			name:       "no-op",
			options:    []UpdateOption{{Option: OpsUpdateNodeCount, Value: uint32(0)}},
			wantUpdate: false,
		},
		// OpsUpdateNodeVersion tests
		{
			name:       "update-node-version",
			options:    []UpdateOption{{Option: OpsUpdateNodeVersion, Value: "1.28"}},
			wantUpdate: true,
			verify: func(t *testing.T, tiproxy *v1alpha1.TiProxyGroup) {
				t.Helper()
				require.NotNil(t, tiproxy.Spec.Template.Spec.Overlay.Pod.Spec.NodeSelector)
				assert.Equal(t, "1.28", tiproxy.Spec.Template.Spec.Overlay.Pod.Spec.NodeSelector[schemav1.K8sVersionLabelKey])
			},
		},
		{
			name: "update-node-version-idempotent",
			mutate: func(tiproxy *v1alpha1.TiProxyGroup) {
				if tiproxy.Spec.Template.Spec.Overlay.Pod.Spec.NodeSelector == nil {
					tiproxy.Spec.Template.Spec.Overlay.Pod.Spec.NodeSelector = map[string]string{}
				}
				tiproxy.Spec.Template.Spec.Overlay.Pod.Spec.NodeSelector[schemav1.K8sVersionLabelKey] = "1.27"
			},
			options:    []UpdateOption{{Option: OpsUpdateNodeVersion, Value: "1.27"}},
			wantUpdate: false,
		},
		{
			name: "update-node-version-nil-nodeSelector",
			mutate: func(tiproxy *v1alpha1.TiProxyGroup) {
				tiproxy.Spec.Template.Spec.Overlay.Pod.Spec.NodeSelector = nil
			},
			options:    []UpdateOption{{Option: OpsUpdateNodeVersion, Value: "1.29"}},
			wantUpdate: true,
			verify: func(t *testing.T, tiproxy *v1alpha1.TiProxyGroup) {
				t.Helper()
				require.NotNil(t, tiproxy.Spec.Template.Spec.Overlay.Pod.Spec.NodeSelector)
				assert.Equal(t, "1.29", tiproxy.Spec.Template.Spec.Overlay.Pod.Spec.NodeSelector[schemav1.K8sVersionLabelKey])
			},
		},
		{
			name:        "update-node-version-invalid-type",
			options:     []UpdateOption{{Option: OpsUpdateNodeVersion, Value: 123}},
			wantErrCode: codes.InvalidArgument,
		},
		{
			name:        "update-node-version-empty",
			options:     []UpdateOption{{Option: OpsUpdateNodeVersion, Value: ""}},
			wantErrCode: codes.InvalidArgument,
		},
	}

	ctx := context.Background()

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			tiproxy := base.DeepCopy()
			if tc.mutate != nil {
				tc.mutate(tiproxy)
			}

			needUpdate, err := getUpdateExpectedTiProxyGroup(ctx, tiproxy, tc.options...)

			if tc.wantErrContains != "" {
				require.Error(t, err)
				assert.Contains(t, err.Error(), tc.wantErrContains)
				return
			}

			if tc.wantErrCode != codes.OK {
				require.Error(t, err)
				st, ok := status.FromError(err)
				require.True(t, ok)
				assert.Equal(t, tc.wantErrCode, st.Code())
				return
			}

			require.NoError(t, err)
			assert.Equal(t, tc.wantUpdate, needUpdate)
			if tc.verify != nil {
				tc.verify(t, tiproxy)
			}
		})
	}
}

func tiproxyContainerByName(group *v1alpha1.TiProxyGroup, name string) *corev1.Container {
	if group.Spec.Template.Spec.Overlay == nil || group.Spec.Template.Spec.Overlay.Pod == nil || group.Spec.Template.Spec.Overlay.Pod.Spec == nil {
		return nil
	}

	for i := range group.Spec.Template.Spec.Overlay.Pod.Spec.Containers {
		if group.Spec.Template.Spec.Overlay.Pod.Spec.Containers[i].Name == name {
			return &group.Spec.Template.Spec.Overlay.Pod.Spec.Containers[i]
		}
	}
	return nil
}

func TestGetTiProxyGroupStatus(t *testing.T) {
	clusterId := "tiproxy-status"
	namespace := dataplaneNamespace(clusterId)
	tiproxyId := testComponentId("tiproxy-status")
	base := newTiProxyGroup(t, namespace, tiproxyId, 3)

	cases := []struct {
		name   string
		mutate func(*v1alpha1.TiProxyGroup)
		phase  apiv1.Component_Phase
	}{
		{
			name:   "available",
			mutate: func(g *v1alpha1.TiProxyGroup) {},
			phase:  apiv1.Component_AVAILABLE,
		},
		{
			name: "not-synced",
			mutate: func(g *v1alpha1.TiProxyGroup) {
				g.Status.Conditions = []metav1.Condition{
					{Type: v1alpha1.CondReady, Status: metav1.ConditionTrue},
					{Type: v1alpha1.CondSynced, Status: metav1.ConditionFalse},
				}
			},
			phase: apiv1.Component_MODIFYING,
		},
		{
			name: "not-up-to-date",
			mutate: func(g *v1alpha1.TiProxyGroup) {
				g.Status.UpdateRevision = "rev-new"
			},
			phase: apiv1.Component_MODIFYING,
		},
		{
			name: "not-ready",
			mutate: func(g *v1alpha1.TiProxyGroup) {
				g.Status.Conditions = []metav1.Condition{
					{Type: v1alpha1.CondReady, Status: metav1.ConditionFalse},
					{Type: v1alpha1.CondSynced, Status: metav1.ConditionTrue},
				}
			},
			phase: apiv1.Component_UNAVAILABLE,
		},
	}

	ctx := context.Background()

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			tiproxyGroup := base.DeepCopy()
			tc.mutate(tiproxyGroup)
			phase := getTiProxyGroupStatus(ctx, tiproxyGroup)
			assert.Equal(t, tc.phase, phase)
		})
	}
}

func TestCvtTiProxyComponentToEn(t *testing.T) {
	clusterId := "tiproxy-cvt"
	namespace := dataplaneNamespace(clusterId)
	tiproxyId := testComponentId("tiproxy-cvt")
	tiproxyGroup := newTiProxyGroup(t, namespace, tiproxyId, 3)

	ctx := context.Background()
	comp, err := cvtTiProxyComponentToEn(ctx, tiproxyGroup)
	require.NoError(t, err)
	require.NotNil(t, comp)

	assert.Equal(t, apiv1.ComponentType_TIPROXY, comp.GetComponentType())
	assert.Equal(t, tiproxyId, comp.GetId())
	assert.Equal(t, uint32(3), comp.GetNodeQuota().GetNodeCount())
	assert.Equal(t, "v1.5.0", comp.GetVersion())
}

func TestGetTiProxyGroupNotFound(t *testing.T) {
	clusterId := "tiproxy-missing"
	scheme := buildTestScheme(t)
	cli := fake.NewClientBuilder().WithScheme(scheme).Build()

	ctx := context.Background()
	_, err := GetTiProxyGroup(ctx, cli, clusterId, "non-existent")
	require.Error(t, err)
	st, ok := status.FromError(err)
	require.True(t, ok)
	assert.Equal(t, codes.NotFound, st.Code())
}

// TestTiProxyGroupRoundTrip verifies that CR -> Component -> CR conversion is consistent
func TestTiProxyGroupRoundTrip(t *testing.T) {
	clusterId := "tiproxy-roundtrip"
	namespace := dataplaneNamespace(clusterId)
	tiproxyId := testComponentId("tiproxy-roundtrip")

	// Create original CR (CR1) with correct memory calculation for round-trip consistency
	cr1 := newTiProxyGroupForRoundTrip(t, namespace, tiproxyId, 2)

	scheme := buildTestScheme(t)
	cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(cr1).Build()
	ctx := context.Background()

	// Step 1: CR1 -> Component1
	comp1, err := cvtTiProxyComponentToEn(ctx, cr1)
	require.NoError(t, err)
	require.NotNil(t, comp1)

	// Step 2: Apply Component1 values to CR2
	cr2, err := getTiProxyGroupCR(ctx, cli, namespace, groupIdWithComponent(tiproxyId, "tiproxy"))
	require.NoError(t, err)

	options := []UpdateOption{
		{Option: OpsUpdateNodeCount, Value: comp1.GetNodeQuota().GetNodeCount()},
		{Option: OpsUpdateNodeSpec, Value: comp1.GetNodeQuota().GetNodeSpec()},
		{Option: OpsUpdateConfig, Value: comp1.GetConfiguration().GetConfigurationFileContent()},
	}
	needUpdate, err := getUpdateExpectedTiProxyGroup(ctx, cr2, options...)
	require.NoError(t, err)

	// Step 3: Compare CR1 and CR2
	assert.Equal(t, *cr1.Spec.Replicas, *cr2.Spec.Replicas, "Replicas should match")
	assert.Equal(t, cr1.Spec.Template.Spec.Version, cr2.Spec.Template.Spec.Version, "Version should match")
	assert.Equal(t, cr1.Spec.Template.Spec.Config, cr2.Spec.Template.Spec.Config, "Config should match")

	// Compare resource requirements - Memory must match
	if cr1.Spec.Template.Spec.Overlay != nil && cr2.Spec.Template.Spec.Overlay != nil {
		c1 := findContainer(cr1.Spec.Template.Spec.Overlay.Pod.Spec.Containers, "tiproxy")
		c2 := findContainer(cr2.Spec.Template.Spec.Overlay.Pod.Spec.Containers, "tiproxy")
		if c1 != nil && c2 != nil {
			assert.Equal(t, c1.Resources.Requests.Memory().Value(), c2.Resources.Requests.Memory().Value(), "Memory request must match")
			assert.Equal(t, c1.Resources.Requests.Cpu().MilliValue(), c2.Resources.Requests.Cpu().MilliValue(), "CPU request should match")
		}
	}

	// Step 4: CR2 -> Component2
	comp2, err := cvtTiProxyComponentToEn(ctx, cr2)
	require.NoError(t, err)
	require.NotNil(t, comp2)

	// Step 5: Compare Component1 and Component2
	assert.Equal(t, comp1.GetId(), comp2.GetId(), "Component ID should match")
	assert.Equal(t, comp1.GetComponentType(), comp2.GetComponentType(), "Component type should match")
	assert.Equal(t, comp1.GetNodeQuota().GetNodeCount(), comp2.GetNodeQuota().GetNodeCount(), "Node count should match")
	assert.Equal(t, comp1.GetVersion(), comp2.GetVersion(), "Version should match")

	// Verify node spec consistency
	if comp1.GetNodeQuota().GetNodeSpec() != nil && comp2.GetNodeQuota().GetNodeSpec() != nil {
		assert.Equal(t, comp1.GetNodeQuota().GetNodeSpec().GetCpu(), comp2.GetNodeQuota().GetNodeSpec().GetCpu(), "CPU should match")
		assert.Equal(t, comp1.GetNodeQuota().GetNodeSpec().GetMemoryGi(), comp2.GetNodeQuota().GetNodeSpec().GetMemoryGi(), "Memory should match")
	}

	// Verify configuration consistency
	if comp1.GetConfiguration() != nil && comp2.GetConfiguration() != nil {
		assert.Equal(t, comp1.GetConfiguration().GetImageRepo(), comp2.GetConfiguration().GetImageRepo(), "Image repo should match")
		assert.Equal(t, comp1.GetConfiguration().GetImageTag(), comp2.GetConfiguration().GetImageTag(), "Image tag should match")
		assert.Equal(t, comp1.GetConfiguration().GetConfigurationFileContent(), comp2.GetConfiguration().GetConfigurationFileContent(), "Config content should match")
	}

	assert.True(t, needUpdate || !needUpdate, "Update status recorded")
}

// TestOpsUpdateTiProxyLabels tests updating TiProxy group labels
func TestOpsUpdateTiProxyLabels(t *testing.T) {
	clusterId := "tiproxy-labels"
	namespace := dataplaneNamespace(clusterId)
	proxyId := testComponentId("tiproxy-labels")

	tests := []struct {
		name          string
		initialLabels map[string]string
		updateLabels  map[string]string
		wantLabels    map[string]string
		wantUpdate    bool
	}{
		{
			name:          "add labels to empty",
			initialLabels: nil,
			updateLabels: map[string]string{
				"key1": "value1",
				"key2": "value2",
			},
			wantLabels: map[string]string{
				"key1": "value1",
				"key2": "value2",
			},
			wantUpdate: true,
		},
		{
			name: "merge with existing labels",
			initialLabels: map[string]string{
				"existing": "value",
			},
			updateLabels: map[string]string{
				"key1": "value1",
				"key2": "value2",
			},
			wantLabels: map[string]string{
				"existing": "value",
				"key1":     "value1",
				"key2":     "value2",
			},
			wantUpdate: true,
		},
		{
			name: "override existing label",
			initialLabels: map[string]string{
				"key1": "old-value",
				"key2": "value2",
			},
			updateLabels: map[string]string{
				"key1": "new-value",
			},
			wantLabels: map[string]string{
				"key1": "new-value",
				"key2": "value2",
			},
			wantUpdate: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tiproxyGroup := newTiProxyGroup(t, namespace, proxyId, 2)
			if tt.initialLabels != nil {
				tiproxyGroup.Spec.Template.Spec.Server.Labels = tt.initialLabels
			}

			scheme := buildTestScheme(t)
			cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(tiproxyGroup).Build()

			ctx := context.Background()
			groupId := groupIdWithComponent(proxyId, "tiproxy")
			options := []UpdateOption{{Option: OpsUpdateLabels, Value: tt.updateLabels}}

			updated, err := UpdateTiProxyGroup(ctx, cli, clusterId, groupId, options)
			require.NoError(t, err)
			require.NotNil(t, updated)

			// Verify the component entity has labels
			if tt.wantLabels != nil {
				assert.Equal(t, tt.wantLabels, updated.GetLabels())
			}

			// Verify the stored CR
			stored := &v1alpha1.TiProxyGroup{}
			err = cli.Get(ctx, client.ObjectKey{Namespace: namespace, Name: groupNameWithComponent(proxyId, "tiproxy")}, stored)
			require.NoError(t, err)

			if tt.wantUpdate && tt.wantLabels != nil {
				require.NotNil(t, stored.Spec.Template.Spec.Server.Labels)
				assert.Equal(t, tt.wantLabels, stored.Spec.Template.Spec.Server.Labels)
			}
		})
	}
}

// TestCvtTiProxyComponentLabels tests that labels are correctly converted from CR to entity
func TestCvtTiProxyComponentLabels(t *testing.T) {
	clusterId := "tiproxy-cvt-labels"
	namespace := dataplaneNamespace(clusterId)
	proxyId := testComponentId("tiproxy-cvt-labels")

	tests := []struct {
		name          string
		serverLabels  map[string]string
		expectedEmpty bool
	}{
		{
			name: "with labels",
			serverLabels: map[string]string{
				"keyspace": "test-keyspace",
				"custom":   "label",
			},
			expectedEmpty: false,
		},
		{
			name:          "without labels",
			serverLabels:  nil,
			expectedEmpty: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tiproxyGroup := newTiProxyGroup(t, namespace, proxyId, 2)
			if tt.serverLabels != nil {
				tiproxyGroup.Spec.Template.Spec.Server.Labels = tt.serverLabels
			}

			ctx := context.Background()
			component, err := cvtTiProxyComponentToEn(ctx, tiproxyGroup)
			require.NoError(t, err)
			require.NotNil(t, component)

			if tt.expectedEmpty {
				assert.Nil(t, component.GetLabels())
			} else {
				assert.Equal(t, tt.serverLabels, component.GetLabels())
			}
		})
	}
}
