package namer

import (
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/validation"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/consts"

	. "github.com/onsi/gomega"
)

func TestUniqueIdentifier(t *testing.T) {
	cases := map[string]struct {
		meta           metav1.ObjectMeta
		options        []IdentifierOptions
		expectedPrefix string
		expectedLen    int
	}{
		"no option": {
			meta: metav1.ObjectMeta{
				Namespace: "staging-1",
				Name:      "aws-us-west-2",
			},
			expectedPrefix: "staging-1-aws-us-west-2",
			expectedLen:    32, // staging-1-aws-us-west-2-xxxxxxxx
		},
		"no option long name": {
			meta: metav1.ObjectMeta{
				Namespace: "staging-1372813089187791289",
				Name:      "aws-ap-southeast-1",
			},
			expectedPrefix: "staging-1372813089187791289-aws-ap-southeast-1",
			expectedLen:    55, // staging-1372813089187791289-aws-ap-southeast-1-xxxxxxxx
		},
		"with prefix": {
			meta: metav1.ObjectMeta{
				Namespace: "staging-1",
				Name:      "aws-us-west-2",
			},
			options:        []IdentifierOptions{WithPrefix("tidbcloud-")},
			expectedPrefix: "tidbcloud-staging-1-aws-us-west-2",
			expectedLen:    42, // tidbcloud-staging-1-aws-us-west-2-xxxxxxxx
		},
		"with limit": {
			meta: metav1.ObjectMeta{
				Namespace: "staging-1372813089187791289",
				Name:      "gke-asia-northeast1",
			},
			options:        []IdentifierOptions{WithLimit(40)},
			expectedPrefix: "staging-1372813089187791289-gke",
			expectedLen:    40, // staging-1372813089187791289-gke-xxxxxxxx
		},
		"long with prefix and limit": {
			meta: metav1.ObjectMeta{
				Namespace: "staging-1372813089187791289",
				Name:      "gke-asia-northeast1",
			},
			options:        []IdentifierOptions{WithPrefix("tidbcloud-"), WithLimit(40)},
			expectedPrefix: "tidbcloud-staging-1372813089187",
			expectedLen:    40, // tidbcloud-staging-1372813089187-xxxxxxxx
		},
		"short with prefix and limit": {
			meta: metav1.ObjectMeta{
				Namespace: "nightly-ms-12345",
				Name:      "gke-us-west1",
			},
			options:        []IdentifierOptions{WithPrefix("tidbcloud-"), WithLimit(40)},
			expectedPrefix: "tidbcloud-nightly-ms-12345-gke-",
			expectedLen:    40, // tidbcloud-nightly-ms-12345-gke--xxxxxxxx
		},
		"with limit and prefix": { // limit prior to prefix would result in the final uid exceed limit
			meta: metav1.ObjectMeta{
				Namespace: "staging-ms-1372813089187791289",
				Name:      "gke-asia-northeast1",
			},
			options:        []IdentifierOptions{WithLimit(40), WithPrefix("tidbcloud-")},
			expectedPrefix: "tidbcloud-staging-ms-1372813089187791289-",
			expectedLen:    50, // tidbcloud-staging-ms-1372813089187791289--xxxxxxxx
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			g := NewGomegaWithT(t)
			uid := UniqueIdentifier(tc.meta, tc.options...)
			idx := strings.LastIndex(uid, "-")
			prefix := uid[:idx]
			g.Expect(prefix).To(Equal(tc.expectedPrefix))
			g.Expect(len(uid)).To(Equal(tc.expectedLen))
		})
	}

}

func TestNewName(t *testing.T) {
	cases := []struct {
		desc     string
		identity string
		prefix   string
		suffix   string
		limit    int
		result   string
	}{
		{
			desc:     "only identity",
			identity: "test-test",
			prefix:   "",
			suffix:   "",
			limit:    -1,
			result:   "test-test-1af1a690",
		},
		{
			desc:     "with prefix",
			identity: "test-test",
			prefix:   "prefix",
			suffix:   "",
			limit:    -1,
			result:   "prefix-test-test-1af1a690",
		},
		{
			desc:     "with suffix",
			identity: "test-test",
			prefix:   "",
			suffix:   "suffix",
			limit:    -1,
			result:   "test-test-1af1a690-suffix",
		},
		{
			desc:     "with prefix and suffix",
			identity: "test-test",
			prefix:   "prefix",
			suffix:   "suffix",
			limit:    -1,
			result:   "prefix-test-test-1af1a690-suffix",
		},
		{
			desc:     "with limit=10",
			identity: "test-test",
			prefix:   "",
			suffix:   "",
			limit:    10,
			result:   "t-1af1a690",
		},
		{
			desc:     "with limit=len(identity)+len(hash)+1",
			identity: "test-test",
			prefix:   "",
			suffix:   "",
			limit:    9 + 8 + 1,
			result:   "test-test-1af1a690",
		},
		{
			desc:     "with limit<len(identity)+len(hash)+1",
			identity: "test-test",
			prefix:   "",
			suffix:   "",
			limit:    9 + 8 + 1 - 5,
			result:   "test-1af1a690",
		},
		{
			desc:     "with limit<len(prefix)+len(identity)+len(hash)+len(suffix)+3",
			identity: "test-test",
			prefix:   "prefix",
			suffix:   "suffix",
			limit:    6 + 9 + 8 + 6 + 3 - 5,
			result:   "prefix-test-1af1a690-suffix",
		},
	}

	for _, c := range cases {
		t.Run(c.desc, func(t *testing.T) {
			g := NewGomegaWithT(t)
			n := NewName(c.identity, c.prefix, c.suffix, c.limit)
			g.Expect(n).To(Equal(c.result))
		})
	}
}

func TestGKEHeterogeneousNodePoolNameNilCluster(t *testing.T) {
	assert.Empty(t, GKEHeterogeneousNodePoolName(nil, "n2-standard-8", "1.28.3-gke.1286000"))
}

func TestGKEHeterogeneousNodePoolNameGolden(t *testing.T) {
	const version = "1.30.4-gke.1348000"

	threeLayerCluster := &cloudv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "cluster-three-layer",
			Namespace: "namespace-three-layer",
		},
		Spec: cloudv1.ClusterSpec{
			Kubernetes: cloudv1.Kubernetes{
				Name: "dedicated-dev-us-west1-23fba3a9-8c97494d",
			},
		},
	}
	threeLayerCluster.EnableFeature(consts.ClusterFeatureGCP3LayerNodePool)

	tests := []struct {
		name        string
		cluster     *cloudv1.Cluster
		machineType string
		want        string
	}{
		{
			name: "legacy network name",
			cluster: &cloudv1.Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "cluster-legacy",
					Namespace: "namespace-legacy",
				},
				Spec: cloudv1.ClusterSpec{
					NetworkName: "network-namespace/shared-network",
				},
			},
			machineType: "n2-standard-8",
			want:        "h-n2-standard-8-1304gke1348000--d1fff45b",
		},
		{
			name:        "3-layer gke name",
			cluster:     threeLayerCluster,
			machineType: "n2-standard-8",
			want:        "h-n2-standard-8-d8b62980-1304gke1348000",
		},
		{
			name: "cluster scope fallback",
			cluster: &cloudv1.Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "cluster-fallback",
					Namespace: "namespace-fallback",
				},
			},
			machineType: "n2-standard-8",
			want:        "h-n2-standard-8-973d1b55-1304gke1348000",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.Equal(t, tt.want, GKEHeterogeneousNodePoolName(tt.cluster, tt.machineType, version))
		})
	}
}

func TestGKEHeterogeneousNodePoolNameMachineTypeTrim(t *testing.T) {
	cluster := &cloudv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "cluster-three-layer",
			Namespace: "namespace-three-layer",
		},
		Spec: cloudv1.ClusterSpec{
			Kubernetes: cloudv1.Kubernetes{
				Name: "dedicated-dev-us-west1-23fba3a9-8c97494d",
			},
		},
	}
	cluster.EnableFeature(consts.ClusterFeatureGCP3LayerNodePool)

	assert.Equal(t,
		GKEHeterogeneousNodePoolName(cluster, "n2-standard-8", "1.30.4-gke.1348000"),
		GKEHeterogeneousNodePoolName(cluster, " n2-standard-8 ", "1.30.4-gke.1348000"),
	)
}

func TestGKEHeterogeneousNodePoolNameLongMachineTypes(t *testing.T) {
	const version = "1.30.4-gke.1348000"

	cluster := &cloudv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "cluster-three-layer",
			Namespace: "namespace-three-layer",
		},
		Spec: cloudv1.ClusterSpec{
			Kubernetes: cloudv1.Kubernetes{
				Name: "dedicated-dev-us-west1-23fba3a9-8c97494d",
			},
		},
	}
	cluster.EnableFeature(consts.ClusterFeatureGCP3LayerNodePool)

	tests := []struct {
		name        string
		machineType string
	}{
		{
			name:        "n2-standard-64",
			machineType: "n2-standard-64",
		},
		{
			name:        "n2-standard-128",
			machineType: "n2-standard-128",
		},
		{
			name:        "custom machine type",
			machineType: "n2-custom-128-524288",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := GKEHeterogeneousNodePoolName(cluster, tt.machineType, version)

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

func TestGKEHeterogeneousNodePoolNameDoesNotCollideWithOrdinary(t *testing.T) {
	cluster := &cloudv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "cluster-three-layer",
			Namespace: "namespace-three-layer",
		},
		Spec: cloudv1.ClusterSpec{
			Kubernetes: cloudv1.Kubernetes{
				Name: "dedicated-dev-us-west1-23fba3a9-8c97494d",
			},
		},
	}
	cluster.EnableFeature(consts.ClusterFeatureGCP3LayerNodePool)

	const (
		machineType = "n2-standard-8"
		version     = "1.30.4-gke.1348000"
	)

	ordinaryName := GKENodePoolName(cluster, machineType, version)
	heterogeneousName := GKEHeterogeneousNodePoolName(cluster, machineType, version)

	assert.NotEqual(t, ordinaryName, heterogeneousName)
	assert.Contains(t, heterogeneousName, "h-n2-standard-8-")
}

func TestBuildOverProvisioningNameV2(t *testing.T) {
	cases := []struct {
		desc         string
		compType     cloudv1.ComponentType
		groupId      string
		instanceType string
		expected     string
		expectedLen  int
	}{
		{
			desc:         "basic case",
			compType:     cloudv1.ComponentTiKV,
			groupId:      "1966380799393529856",
			instanceType: "Standard_D2s_v3",
			expected:     "op-1966380799393529856-standard-d2s-v3-tikv",
		},
		{
			desc:         "long instance type",
			compType:     cloudv1.ComponentTiFlash,
			groupId:      "1966380799393529856",
			instanceType: "Standard_D32s_v3_with_extra_long_suffix",
			expected:     "op-1966380799393529856-standar-8bfeb902-tiflash",
			expectedLen:  DeploymentNameLimit,
		},
		{
			desc:         "another long instance type",
			compType:     cloudv1.ComponentTiFlash,
			groupId:      "1966380799393529856",
			instanceType: "Standard_D32s_v3_with_extra_long_suffix2",
			expected:     "op-1966380799393529856-standar-156a6bf0-tiflash",
			expectedLen:  DeploymentNameLimit,
		},
		{
			desc:         "long instance type with another groupId",
			compType:     cloudv1.ComponentTiFlash,
			groupId:      "1966380799393529857",
			instanceType: "Standard_D32s_v3_with_extra_long_suffix2",
			expected:     "op-1966380799393529857-standar-810982c4-tiflash",
			expectedLen:  DeploymentNameLimit,
		},
		{
			desc:         "AWS instance type",
			compType:     cloudv1.ComponentTiFlash,
			groupId:      "1966380799393529856",
			instanceType: "m5.2xlarge",
			expected:     "op-1966380799393529856-m5.2xlarge-tiflash",
		},
		{
			desc:         "GCP instance type",
			compType:     cloudv1.ComponentTiFlash,
			groupId:      "1966380799393529856",
			instanceType: "n2-custom-64-131072",
			expected:     "op-1966380799393529856-n2-cust-4cbc8326-tiflash",
		},
		{
			desc:         "Azure instance type",
			compType:     cloudv1.ComponentTiFlash,
			groupId:      "1966380799393529856",
			instanceType: "Standard_D16as_v5",
			expected:     "op-1966380799393529856-standar-19d27003-tiflash",
		},
		{
			desc:         "Azure instance type",
			compType:     cloudv1.ComponentTiKV,
			groupId:      "1966380799393529856",
			instanceType: "Standard_D2as_v5",
			expected:     "op-1966380799393529856-standard-d2as-v5-tikv",
		},
	}

	for _, c := range cases {
		t.Run(c.desc, func(t *testing.T) {
			got := BuildOverProvisioningNameV2(c.compType, c.groupId, c.instanceType)
			assert.Equal(t, c.expected, got, "BuildOverProvisioningNameV2 mismatch")
			if c.expectedLen != 0 {
				assert.Equal(t, c.expectedLen, len(got), "BuildOverProvisioningNameV2 length exceed")
			}
		})
	}
}

func TestNewNameHashOnlyExceed(t *testing.T) {
	cases := map[string]struct {
		identity    string
		prefix      string
		suffix      string
		limit       int
		expected    string
		expectedLen int
	}{
		"identity fits within limit, no hash": {
			identity:    "shortname",
			prefix:      "pre",
			suffix:      "suf",
			limit:       30,
			expected:    "pre-shortname-suf",
			expectedLen: len("pre-shortname-suf"),
		},
		"identity exactly fits limit, no hash": {
			identity:    "abcdefghij",
			prefix:      "",
			suffix:      "",
			limit:       10,
			expected:    "abcdefghij",
			expectedLen: 10,
		},
		"identity exceeds limit, hash applied": {
			identity:    "averyverylongnameexceedinglimit",
			prefix:      "pre",
			suffix:      "suf",
			limit:       20,
			expected:    "pre-ave-9bd153c0-suf",
			expectedLen: 20,
		},
		"identity exceeds limit, no prefix/suffix": {
			identity:    "averyverylongnameexceedinglimit",
			prefix:      "",
			suffix:      "",
			limit:       12,
			expected:    "ave-9bd153c0",
			expectedLen: 12,
		},
		"identity exceeds limit, only prefix": {
			identity:    "averyverylongnameexceedinglimit",
			prefix:      "pre",
			suffix:      "",
			limit:       15,
			expected:    "pre-av-9bd153c0",
			expectedLen: 15,
		},
		"identity exceeds limit, only suffix": {
			identity:    "averyverylongnameexceedinglimit",
			prefix:      "",
			suffix:      "suf",
			limit:       15,
			expected:    "av-9bd153c0-suf",
			expectedLen: 15,
		},
		"limit is maxLimit, long identity": {
			identity:    "averyverylongnameexceedinglimitaveryverylongnameexceedinglimit", // len is 62
			prefix:      "pre",
			suffix:      "suf",
			limit:       63,
			expected:    "pre-averyverylongnameexceedinglimitaveryverylongna-e8055321-suf",
			expectedLen: 63,
		},
	}

	for name, c := range cases {
		t.Run(name, func(t *testing.T) {
			got := NewNameHashOnlyExceed(c.identity, c.prefix, c.suffix, c.limit)
			assert.Equalf(t, c.expected, got, "check got value")
			assert.LessOrEqualf(t, len(got), c.limit, "'got' don't exceed limit")
			assert.Lenf(t, got, c.expectedLen, "check len with expectedLen")
		})
	}
}
func TestHashIfExceed(t *testing.T) {
	cases := []struct {
		desc     string
		original string
		maxLen   int
		expected string
	}{
		{
			desc:     "original shorter than maxLen",
			original: "shortname",
			maxLen:   20,
			expected: "shortname",
		},
		{
			desc:     "original equal to maxLen",
			original: "12345678",
			maxLen:   8,
			expected: "12345678",
		},
		{
			desc:     "original longer than maxLen",
			original: "averyverylongnameexceedinglimit",
			maxLen:   15,
			expected: "averyv-9bd153c0", // keep first 6 chars, hash of original
		},
		{
			desc:     "original much longer than maxLen",
			original: "thisisareallylongstringthatexceedsthelimitbyalot",
			maxLen:   10,
			expected: "t-ae6e5d67", // keep first 1 char, hash of original
		},
	}

	for _, c := range cases {
		t.Run(c.desc, func(t *testing.T) {
			processor := HashIfExceed(c.maxLen)
			got := processor.Process(c.original, "")
			if len(c.original) <= c.maxLen {
				assert.Equal(t, c.expected, got)
			} else {
				assert.Lenf(t, got, c.maxLen, "check result's len")
				parts := strings.Split(got, "-")
				assert.Equalf(t, c.expected[:len(parts[0])], parts[0], "check the 'keep' part")
				assert.Equalf(t,
					c.expected,
					got,
					"check got",
				)
			}
		})
	}
}

func TestClusterServerSAName(t *testing.T) {
	assert.Equalf(t, "dev-cluster-server@pingcap-gardener.iam.gserviceaccount.com", ClusterServerIAMSAEmail("dev-cluster-server", "pingcap-gardener"), "ClusterServerIAMSAEmail mismatch")
}

func TestNamerForTiDBGroupName(t *testing.T) {
	testCases := []struct {
		desc                         string
		cluster                      *cloudv1.Cluster
		groupSpec                    *cloudv1.ExtraGroupSpec
		publicHostedZoneDomain       string
		expectedTiDBName             string
		expectedClusterName          string
		expectedDNSNames             []string
		expectedPublicEndpoint       string
		expectedPrivateEndpoint      string
		expectedPrivateLinkEndpoint  string
		expectedGCPPSCEndpointDomain string
	}{
		{
			desc: "basic case",
			cluster: &cloudv1.Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Name: "123",
				},
			},
			groupSpec: &cloudv1.ExtraGroupSpec{
				Name: "group1",
				SqlEndpoints: cloudv1.SqlEndpoints{
					DNS: cloudv1.DNS{
						Identity: "test-identity",
					},
				},
			},
			publicHostedZoneDomain: "example.com",
			expectedTiDBName:       "db-group1-513772ee",
			expectedClusterName:    "123-group1",
			expectedDNSNames: []string{
				"db-group1-513772ee-tidb.tidb123",
				"db-group1-513772ee-tidb.tidb123.svc",
				"tidb.test-identity.example.com",
				"private-tidb.test-identity.example.com",
				"privatelink-tidb.test-identity.example.com",
				"*.test-identity.example.com",
			},
			expectedPublicEndpoint:       "tidb.test-identity.example.com",
			expectedPrivateEndpoint:      "private-tidb.test-identity.example.com",
			expectedPrivateLinkEndpoint:  "privatelink-tidb.test-identity.example.com",
			expectedGCPPSCEndpointDomain: "test-identity.example.com",
		},
		{
			desc: "with different identity and domain",
			cluster: &cloudv1.Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Name: "123",
				},
			},
			groupSpec: &cloudv1.ExtraGroupSpec{
				Name: "analytics",
				SqlEndpoints: cloudv1.SqlEndpoints{
					DNS: cloudv1.DNS{
						Identity: "prod-analytics",
					},
				},
			},
			publicHostedZoneDomain: "pingcap.net",
			expectedTiDBName:       "db-analytics-68c4283d",
			expectedClusterName:    "123-analytics",
			expectedDNSNames: []string{
				"db-analytics-68c4283d-tidb.tidb123",
				"db-analytics-68c4283d-tidb.tidb123.svc",
				"tidb.prod-analytics.pingcap.net",
				"private-tidb.prod-analytics.pingcap.net",
				"privatelink-tidb.prod-analytics.pingcap.net",
				"*.prod-analytics.pingcap.net",
			},
			expectedPublicEndpoint:       "tidb.prod-analytics.pingcap.net",
			expectedPrivateEndpoint:      "private-tidb.prod-analytics.pingcap.net",
			expectedPrivateLinkEndpoint:  "privatelink-tidb.prod-analytics.pingcap.net",
			expectedGCPPSCEndpointDomain: "prod-analytics.pingcap.net",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.desc, func(t *testing.T) {
			namer := NewNamer().
				WithTiDBNamespace(tc.cluster.PrefixedId()).
				WithComponent(string(cloudv1.ComponentTiDB))
			result := namer.ForTiDBGroup(tc.cluster, tc.groupSpec, tc.publicHostedZoneDomain)

			assert.Equalf(t, tc.expectedTiDBName, result.GetTiDBName(), "TiDB name mismatch")
			assert.Equalf(t, tc.expectedClusterName, result.GetClusterName(), "Cluster name mismatch")
			assert.Equalf(t, tc.expectedDNSNames, result.GetClusterServerDNSNames(false), "DNS names mismatch")
			assert.Equalf(t, tc.expectedPublicEndpoint, result.GetPublicEndpointAddress(), "Public endpoint mismatch")
			assert.Equalf(t, tc.expectedPrivateEndpoint, result.GetPrivateEndpointAddress(), "Private endpoint mismatch")
			assert.Equalf(t, tc.expectedPrivateLinkEndpoint, result.GetPrivateLinkEndpointAddress(), "Private link endpoint mismatch")
			assert.Equalf(t, tc.expectedGCPPSCEndpointDomain, result.GetPrivateServiceConnectDomain(), "GCP PSC name mismatch")
		})
	}
}
