package tioperator

import (
	"crypto/sha1" //nolint:gosec // sha1 is only used for generating test IDs, not for security
	"encoding/hex"
	"fmt"
	"strings"
	"testing"

	v1alpha1br "github.com/pingcap/tidb-operator/api/v2/br/v1alpha1"
	"github.com/pingcap/tidb-operator/api/v2/core/v1alpha1"
	"github.com/samber/lo"
	"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"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
)

const (
	testNodeSpecLabel = "4C16G"
	testConfigValue   = "test-config"
)

func buildTestScheme(t *testing.T) *runtime.Scheme {
	t.Helper()

	scheme := runtime.NewScheme()
	require.NoError(t, clientgoscheme.AddToScheme(scheme))
	require.NoError(t, v1alpha1.AddToScheme(scheme))
	require.NoError(t, v1alpha1br.AddToScheme(scheme))
	return scheme
}

func testComponentId(seed string) string {
	sum := sha1.Sum([]byte(seed)) //nolint:gosec // sha1 is only used for generating test IDs, not for security
	return hex.EncodeToString(sum[:])[:12]
}

func readySyncedConditions() []metav1.Condition {
	return []metav1.Condition{
		{
			Type:   v1alpha1.CondReady,
			Status: metav1.ConditionTrue,
		},
		{
			Type:   v1alpha1.CondSynced,
			Status: metav1.ConditionTrue,
		},
	}
}

func syncedNotReadyConditions() []metav1.Condition {
	return []metav1.Condition{
		{
			Type:   v1alpha1.CondReady,
			Status: metav1.ConditionFalse,
		},
		{
			Type:   v1alpha1.CondSynced,
			Status: metav1.ConditionTrue,
		},
	}
}

func defaultComponentLabels(componentLabel string) map[string]string {
	return map[string]string{
		schemav1.LabelComponentKey:     componentLabel,
		schemav1.LabelNodeSpecTypeKey:  testNodeSpecLabel,
		schemav1.LabelUseWithLocalDisk: "false",
	}
}

func defaultAnnotations() map[string]string {
	return map[string]string{
		schemav1.AnnotationsComponentMinCount: "1",
		schemav1.AnnotationsComponentMaxCount: "5",
	}
}

func defaultResources() corev1.ResourceRequirements {
	cpuQty := resource.MustParse("4")
	memQty := resource.MustParse("16Gi")

	return corev1.ResourceRequirements{
		Requests: corev1.ResourceList{
			corev1.ResourceCPU:    cpuQty,
			corev1.ResourceMemory: memQty,
		},
		Limits: corev1.ResourceList{
			corev1.ResourceCPU:    cpuQty,
			corev1.ResourceMemory: memQty,
		},
	}
}

func roundTripNodeSpec() *entity.NodeSpec {
	return &entity.NodeSpec{Cpu: 4, MemoryGi: 16, AvailableCpu: 4, AvailableMemoryMi: 16 * 1024}
}

func resourcesForComponent(compType apiv1.ComponentType) corev1.ResourceRequirements {
	rr := corev1.ResourceRequirements{
		Requests: corev1.ResourceList{},
		Limits:   corev1.ResourceList{},
	}
	sidecarCPU, sidecarMem := SidecarResourcesFor(compType)
	setMainContainerResources(&rr, roundTripNodeSpec(), sidecarCPU, sidecarMem)
	return rr
}

func assertMainContainerResources(t *testing.T, got corev1.ResourceRequirements, nodeSpec *entity.NodeSpec, compType apiv1.ComponentType) {
	t.Helper()
	sidecarCPU, sidecarMem := SidecarResourcesFor(compType)
	assert.Equal(t, int64(ComputeRequestCPU(EffectiveAvailableCPUMilli(nodeSpec), sidecarCPU)), got.Requests.Cpu().MilliValue())
	assert.Equal(t, int64(EffectiveMaxCPUMilli(nodeSpec)), got.Limits.Cpu().MilliValue())
	expectedReqMem := resource.NewQuantity(int64(ComputeRequestMemory(uint64(nodeSpec.GetAvailableMemoryMi()), sidecarMem))*1024*1024, resource.BinarySI)
	assert.Equal(t, expectedReqMem.String(), got.Requests.Memory().String())
	expectedLimMem := resource.NewQuantity(int64(EffectiveMaxMemoryMi(nodeSpec))*1024*1024, resource.BinarySI)
	assert.Equal(t, expectedLimMem.String(), got.Limits.Memory().String())
}

// resourcesWithAvailableMemory creates resource requirements using the same
// setNodeSpec formula as sidecar-less components (TiKV/PD/...). Used by
// round-trip tests so CR1 and CR2 match after OpsUpdateNodeSpec.
func resourcesWithAvailableMemory() corev1.ResourceRequirements {
	return resourcesForComponent(apiv1.ComponentType_TIKV)
}

func resourcesWithTiDBRequestMemory() corev1.ResourceRequirements {
	return resourcesForComponent(apiv1.ComponentType_TIDB)
}

func testImage(component string) string {
	sanitized := strings.ReplaceAll(component, "/", "-")
	sanitized = strings.ReplaceAll(sanitized, " ", "-")
	return fmt.Sprintf("repo/%s:test", sanitized)
}

func groupIdWithComponent(id, component string) string {
	if component == "" {
		return id
	}
	return fmt.Sprintf("%s-%s", id, component)
}

func groupNameWithComponent(id, component string) string {
	return fmt.Sprintf("%s%s", componentGroupPrefix, groupIdWithComponent(id, component))
}

func newTiDBGroup(t *testing.T, namespace, id, componentName, componentLabel string, replicas int32) *v1alpha1.TiDBGroup {
	t.Helper()

	group := &v1alpha1.TiDBGroup{}
	group.Name = groupNameWithComponent(id, componentName)
	group.Namespace = namespace
	group.Labels = defaultComponentLabels(componentLabel)
	group.Annotations = defaultAnnotations()
	group.Spec.Replicas = lo.ToPtr(replicas)
	group.Spec.Template.Spec.Version = "v1.0.0"
	image := testImage(componentLabel)
	group.Spec.Template.Spec.Image = &image
	config := v1alpha1.ConfigFile(testConfigValue)
	group.Spec.Template.Spec.Config = config
	group.Spec.Template.Spec.Overlay = &v1alpha1.Overlay{
		Pod: &v1alpha1.PodOverlay{
			Spec: &corev1.PodSpec{
				Containers: []corev1.Container{
					{
						Name:      "tidb",
						Resources: defaultResources(),
					},
				},
			},
		},
	}
	group.Status.CommonStatus = v1alpha1.CommonStatus{
		CurrentRevision:    "rev",
		UpdateRevision:     "rev",
		ObservedGeneration: 1,
	}
	group.Status.Replicas = replicas
	group.Status.ReadyReplicas = replicas
	group.Status.UpdatedReplicas = replicas
	group.Status.Conditions = readySyncedConditions()
	group.Generation = 1
	return group
}

func newTiKVGroup(t *testing.T, namespace, id string, replicas int32) *v1alpha1.TiKVGroup {
	t.Helper()

	group := &v1alpha1.TiKVGroup{}
	group.Name = groupNameWithComponent(id, "tikv")
	group.Namespace = namespace
	group.Labels = defaultComponentLabels("tikv")
	group.Annotations = defaultAnnotations()
	group.Spec.Replicas = lo.ToPtr(replicas)
	group.Spec.Template.Spec.Version = "v1.1.0"
	image := testImage("tikv")
	group.Spec.Template.Spec.Image = &image
	config := v1alpha1.ConfigFile(testConfigValue)
	group.Spec.Template.Spec.Config = config
	group.Spec.Template.Spec.Overlay = &v1alpha1.Overlay{
		Pod: &v1alpha1.PodOverlay{
			Spec: &corev1.PodSpec{
				Containers: []corev1.Container{
					{
						Name:      "tikv",
						Resources: defaultResources(),
					},
				},
			},
		},
	}
	group.Status.CommonStatus = v1alpha1.CommonStatus{
		CurrentRevision:    "rev",
		UpdateRevision:     "rev",
		ObservedGeneration: 1,
	}
	group.Status.Replicas = replicas
	group.Status.ReadyReplicas = replicas
	group.Status.UpdatedReplicas = replicas
	group.Status.Conditions = readySyncedConditions()
	group.Generation = 1
	return group
}

func newPDGroup(t *testing.T, namespace, id string, replicas int32) *v1alpha1.PDGroup {
	t.Helper()

	group := &v1alpha1.PDGroup{}
	group.Name = groupNameWithComponent(id, "pd")
	group.Namespace = namespace
	group.Labels = defaultComponentLabels("pd")
	group.Annotations = defaultAnnotations()
	group.Spec.Replicas = lo.ToPtr(replicas)
	group.Spec.Template.Spec.Version = "v1.2.0"
	image := testImage("pd")
	group.Spec.Template.Spec.Image = &image
	config := v1alpha1.ConfigFile(testConfigValue)
	group.Spec.Template.Spec.Config = config
	group.Spec.Template.Spec.Overlay = &v1alpha1.Overlay{
		Pod: &v1alpha1.PodOverlay{
			Spec: &corev1.PodSpec{
				Containers: []corev1.Container{
					{
						Name:      "pd",
						Resources: defaultResources(),
					},
				},
			},
		},
	}
	group.Status.CommonStatus = v1alpha1.CommonStatus{
		CurrentRevision:    "rev",
		UpdateRevision:     "rev",
		ObservedGeneration: 1,
	}
	group.Status.Replicas = replicas
	group.Status.ReadyReplicas = replicas
	group.Status.UpdatedReplicas = replicas
	group.Status.Conditions = readySyncedConditions()
	group.Generation = 1
	return group
}

func newTsoGroup(t *testing.T, namespace, id string, replicas int32) *v1alpha1.TSOGroup {
	t.Helper()

	group := &v1alpha1.TSOGroup{}
	group.Name = groupNameWithComponent(id, "tso")
	group.Namespace = namespace
	group.Labels = defaultComponentLabels("tso")
	group.Annotations = defaultAnnotations()
	group.Spec.Replicas = lo.ToPtr(replicas)
	group.Spec.Template.Spec.Version = "v1.3.0"
	image := testImage("tso")
	group.Spec.Template.Spec.Image = &image
	config := v1alpha1.ConfigFile(testConfigValue)
	group.Spec.Template.Spec.Config = config
	group.Spec.Template.Spec.Overlay = &v1alpha1.Overlay{
		Pod: &v1alpha1.PodOverlay{
			Spec: &corev1.PodSpec{
				Containers: []corev1.Container{
					{
						Name:      "tso",
						Resources: defaultResources(),
					},
				},
			},
		},
	}
	group.Status.CommonStatus = v1alpha1.CommonStatus{
		CurrentRevision:    "rev",
		UpdateRevision:     "rev",
		ObservedGeneration: 1,
	}
	group.Status.Replicas = replicas
	group.Status.ReadyReplicas = replicas
	group.Status.UpdatedReplicas = replicas
	group.Status.Conditions = readySyncedConditions()
	group.Generation = 1
	return group
}

func newSchedulingGroup(t *testing.T, namespace, id string, replicas int32) *v1alpha1.SchedulingGroup {
	t.Helper()

	group := &v1alpha1.SchedulingGroup{}
	group.Name = groupNameWithComponent(id, "scheduling")
	group.Namespace = namespace
	group.Labels = defaultComponentLabels("scheduling")
	group.Annotations = defaultAnnotations()
	group.Spec.Replicas = lo.ToPtr(replicas)
	group.Spec.Template.Spec.Version = "v1.4.0"
	image := testImage("scheduling")
	group.Spec.Template.Spec.Image = &image
	config := v1alpha1.ConfigFile(testConfigValue)
	group.Spec.Template.Spec.Config = config
	group.Spec.Template.Spec.Overlay = &v1alpha1.Overlay{
		Pod: &v1alpha1.PodOverlay{
			Spec: &corev1.PodSpec{
				Containers: []corev1.Container{
					{
						Name:      "scheduling",
						Resources: defaultResources(),
					},
				},
			},
		},
	}
	group.Status.CommonStatus = v1alpha1.CommonStatus{
		CurrentRevision:    "rev",
		UpdateRevision:     "rev",
		ObservedGeneration: 1,
	}
	group.Status.Replicas = replicas
	group.Status.ReadyReplicas = replicas
	group.Status.UpdatedReplicas = replicas
	group.Status.Conditions = readySyncedConditions()
	group.Generation = 1
	return group
}

func newResourceManagerGroup(t *testing.T, namespace, id string, replicas int32) *v1alpha1.ResourceManagerGroup {
	t.Helper()

	group := &v1alpha1.ResourceManagerGroup{}
	group.Name = groupNameWithComponent(id, "")
	group.Namespace = namespace
	group.Labels = defaultComponentLabels("resource-manager")
	group.Annotations = defaultAnnotations()
	group.Spec.Replicas = lo.ToPtr(replicas)
	group.Spec.Template.Spec.Version = "v1.0.0"
	image := testImage("resource-manager")
	group.Spec.Template.Spec.Image = &image
	config := v1alpha1.ConfigFile(testConfigValue)
	group.Spec.Template.Spec.Config = config
	group.Spec.Template.Spec.Overlay = &v1alpha1.Overlay{
		Pod: &v1alpha1.PodOverlay{
			Spec: &corev1.PodSpec{
				Containers: []corev1.Container{
					{
						Name:      "resource-manager",
						Resources: defaultResources(),
					},
				},
			},
		},
	}
	group.Status.CommonStatus = v1alpha1.CommonStatus{
		CurrentRevision:    "rev",
		UpdateRevision:     "rev",
		ObservedGeneration: 1,
	}
	group.Status.Replicas = replicas
	group.Status.ReadyReplicas = replicas
	group.Status.UpdatedReplicas = replicas
	group.Status.Conditions = readySyncedConditions()
	group.Generation = 1
	return group
}

func newTiProxyGroup(t *testing.T, namespace, id string, replicas int32) *v1alpha1.TiProxyGroup {
	t.Helper()

	group := &v1alpha1.TiProxyGroup{}
	group.Name = groupNameWithComponent(id, "tiproxy")
	group.Namespace = namespace
	group.Labels = defaultComponentLabels("tiproxy")
	group.Annotations = defaultAnnotations()
	group.Spec.Replicas = lo.ToPtr(replicas)
	group.Spec.Template.Spec.Version = "v1.5.0"
	image := testImage("tiproxy")
	group.Spec.Template.Spec.Image = &image
	config := v1alpha1.ConfigFile(testConfigValue)
	group.Spec.Template.Spec.Config = config
	group.Spec.Template.Spec.Overlay = &v1alpha1.Overlay{
		Pod: &v1alpha1.PodOverlay{
			Spec: &corev1.PodSpec{
				Containers: []corev1.Container{
					{
						Name:      "tiproxy",
						Resources: defaultResources(),
					},
				},
			},
		},
	}
	group.Status.CommonStatus = v1alpha1.CommonStatus{
		CurrentRevision:    "rev",
		UpdateRevision:     "rev",
		ObservedGeneration: 1,
	}
	group.Status.Replicas = replicas
	group.Status.ReadyReplicas = replicas
	group.Status.UpdatedReplicas = replicas
	group.Status.Conditions = readySyncedConditions()
	group.Generation = 1
	return group
}

func newTiFlashGroup(t *testing.T, namespace, id, componentName, componentLabel string, replicas int32) *v1alpha1.TiFlashGroup {
	t.Helper()

	group := &v1alpha1.TiFlashGroup{}
	group.Name = groupNameWithComponent(id, componentName)
	group.Namespace = namespace
	group.Labels = defaultComponentLabels(componentLabel)
	group.Annotations = defaultAnnotations()
	group.Spec.Replicas = lo.ToPtr(replicas)
	group.Spec.Template.Spec.Version = "v1.6.0"
	image := testImage(componentLabel)
	group.Spec.Template.Spec.Image = &image
	config := v1alpha1.ConfigFile(testConfigValue)
	proxyConfig := v1alpha1.ConfigFile("proxy-config")
	group.Spec.Template.Spec.Config = config
	group.Spec.Template.Spec.ProxyConfig = proxyConfig
	group.Spec.Template.Spec.Overlay = &v1alpha1.Overlay{
		Pod: &v1alpha1.PodOverlay{
			Spec: &corev1.PodSpec{
				Containers: []corev1.Container{
					{
						Name:      "tiflash",
						Resources: defaultResources(),
					},
				},
			},
		},
	}
	group.Status.CommonStatus = v1alpha1.CommonStatus{
		CurrentRevision:    "rev",
		UpdateRevision:     "rev",
		ObservedGeneration: 1,
	}
	group.Status.Replicas = replicas
	group.Status.ReadyReplicas = replicas
	group.Status.UpdatedReplicas = replicas
	group.Status.Conditions = readySyncedConditions()
	group.Generation = 1
	return group
}

func newTiKVWorkerGroup(t *testing.T, namespace, id, componentLabel string, replicas int32) *v1alpha1.TiKVWorkerGroup {
	t.Helper()

	// tikv worker group name: db-xxxx-default
	// coprocessor worker group name: db-xxxx-coprocessor
	var groupName string
	if componentLabel == "coprocessor-worker" {
		groupName = fmt.Sprintf("%s%s-coprocessor", componentGroupPrefix, id)
	} else {
		groupName = fmt.Sprintf("%s%s-default", componentGroupPrefix, id)
	}

	group := &v1alpha1.TiKVWorkerGroup{}
	group.Name = groupName
	group.Namespace = namespace
	group.Labels = defaultComponentLabels(componentLabel)
	group.Annotations = defaultAnnotations()
	group.Spec.Replicas = lo.ToPtr(replicas)
	group.Spec.Template.Spec.Version = "v1.1.0"
	image := testImage(componentLabel)
	group.Spec.Template.Spec.Image = &image
	config := v1alpha1.ConfigFile(testConfigValue)
	group.Spec.Template.Spec.Config = config
	group.Spec.Template.Spec.Overlay = &v1alpha1.Overlay{
		Pod: &v1alpha1.PodOverlay{
			Spec: &corev1.PodSpec{
				Containers: []corev1.Container{
					{
						Name:      "tikv-worker",
						Resources: defaultResources(),
					},
				},
				NodeSelector: map[string]string{
					"node-spec": strings.ToLower(testNodeSpecLabel),
				},
			},
		},
	}
	group.Status.CommonStatus = v1alpha1.CommonStatus{
		CurrentRevision:    "rev",
		UpdateRevision:     "rev",
		ObservedGeneration: 1,
	}
	group.Status.Replicas = replicas
	group.Status.ReadyReplicas = replicas
	group.Status.UpdatedReplicas = replicas
	group.Status.Conditions = readySyncedConditions()
	group.Generation = 1
	return group
}

// Helper functions for round-trip tests with correct memory calculations

func newPDGroupForRoundTrip(t *testing.T, namespace, id string, replicas int32) *v1alpha1.PDGroup {
	t.Helper()
	group := newPDGroup(t, namespace, id, replicas)
	// Update resources to use available memory calculation
	group.Spec.Template.Spec.Overlay.Pod.Spec.Containers[0].Resources = resourcesWithAvailableMemory()
	return group
}

func newTiDBGroupForRoundTrip(t *testing.T, namespace, id, componentName, componentLabel string, replicas int32) *v1alpha1.TiDBGroup {
	t.Helper()
	group := newTiDBGroup(t, namespace, id, componentName, componentLabel, replicas)
	group.Spec.Template.Spec.Overlay.Pod.Spec.Containers[0].Resources = resourcesWithTiDBRequestMemory()
	return group
}

func newTiKVGroupForRoundTrip(t *testing.T, namespace, id string, replicas int32) *v1alpha1.TiKVGroup {
	t.Helper()
	group := newTiKVGroup(t, namespace, id, replicas)
	group.Spec.Template.Spec.Overlay.Pod.Spec.Containers[0].Resources = resourcesWithAvailableMemory()
	return group
}

func newTiFlashGroupForRoundTrip(t *testing.T, namespace, id, componentName, componentLabel string, replicas int32) *v1alpha1.TiFlashGroup {
	t.Helper()
	group := newTiFlashGroup(t, namespace, id, componentName, componentLabel, replicas)
	group.Spec.Template.Spec.Overlay.Pod.Spec.Containers[0].Resources = resourcesForComponent(apiv1.ComponentType_TIFLASH_COMPUTE)
	return group
}

func newTiProxyGroupForRoundTrip(t *testing.T, namespace, id string, replicas int32) *v1alpha1.TiProxyGroup {
	t.Helper()
	group := newTiProxyGroup(t, namespace, id, replicas)
	group.Spec.Template.Spec.Overlay.Pod.Spec.Containers[0].Resources = resourcesWithAvailableMemory()
	return group
}

func newTsoGroupForRoundTrip(t *testing.T, namespace, id string, replicas int32) *v1alpha1.TSOGroup {
	t.Helper()
	group := newTsoGroup(t, namespace, id, replicas)
	group.Spec.Template.Spec.Overlay.Pod.Spec.Containers[0].Resources = resourcesWithAvailableMemory()
	return group
}

func newSchedulingGroupForRoundTrip(t *testing.T, namespace, id string, replicas int32) *v1alpha1.SchedulingGroup {
	t.Helper()
	group := newSchedulingGroup(t, namespace, id, replicas)
	group.Spec.Template.Spec.Overlay.Pod.Spec.Containers[0].Resources = resourcesWithAvailableMemory()
	return group
}

func newResourceManagerGroupForRoundTrip(t *testing.T, namespace, id string, replicas int32) *v1alpha1.ResourceManagerGroup {
	t.Helper()
	group := newResourceManagerGroup(t, namespace, id, replicas)
	group.Spec.Template.Spec.Overlay.Pod.Spec.Containers[0].Resources = resourcesWithAvailableMemory()
	return group
}

func newTiKVWorkerGroupForRoundTrip(t *testing.T, namespace, id, componentLabel string, replicas int32) *v1alpha1.TiKVWorkerGroup {
	t.Helper()
	group := newTiKVWorkerGroup(t, namespace, id, componentLabel, replicas)
	group.Spec.Template.Spec.Overlay.Pod.Spec.Containers[0].Resources = resourcesWithAvailableMemory()
	return group
}

// newTiBR creates a TiBR object for testing
func newTiBR(t *testing.T, namespace string, ready, synced bool) *v1alpha1br.TiBR {
	t.Helper()

	var conditions []metav1.Condition
	if ready {
		conditions = append(conditions, metav1.Condition{
			Type:   "Ready",
			Status: metav1.ConditionTrue,
		})
	} else {
		conditions = append(conditions, metav1.Condition{
			Type:   "Ready",
			Status: metav1.ConditionFalse,
		})
	}

	if synced {
		conditions = append(conditions, metav1.Condition{
			Type:   "Synced",
			Status: metav1.ConditionTrue,
		})
	} else {
		conditions = append(conditions, metav1.Condition{
			Type:   "Synced",
			Status: metav1.ConditionFalse,
		})
	}

	return &v1alpha1br.TiBR{
		ObjectMeta: metav1.ObjectMeta{
			Name:      DefaultTiBRName,
			Namespace: namespace,
		},
		Status: v1alpha1br.TiBRStatus{
			CommonStatus: v1alpha1.CommonStatus{
				Conditions: conditions,
			},
		},
	}
}

// newTiBRGC creates a TiBRGC object for testing
func newTiBRGC(t *testing.T, namespace string) *v1alpha1br.TiBRGC {
	t.Helper()

	return &v1alpha1br.TiBRGC{
		ObjectMeta: metav1.ObjectMeta{
			Name:      DefaultTiBRGCRName,
			Namespace: namespace,
		},
	}
}
