package tier

import (
	"strings"
	"testing"

	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/cluster-service-ng/internal/capacity-planner/api/v1alpha1"
)

type fakeTier struct {
	name      string
	resources []Resource
	patch     client.Patch
}

var _ Tier = (*fakeTier)(nil)

func (t *fakeTier) Name() string {
	return t.name
}

func (t *fakeTier) Resources() []Resource {
	return t.resources
}

func (t *fakeTier) Capacity(resourceName string) (resource.Quantity, bool) {
	for i := range t.resources {
		if t.resources[i].Name == resourceName {
			return t.resources[i].Capacity, true
		}
	}
	return resource.Quantity{}, false
}

func (t *fakeTier) Patch() client.Patch {
	return t.patch
}

func (t *fakeTier) Cost() int64 {
	return 0
}

func TestTierContract(t *testing.T) {
	patch := client.RawPatch(types.JSONPatchType, []byte(`[{"op":"replace","path":"/spec/tier","value":"4c"}]`))
	tier := &fakeTier{
		name: "4c",
		resources: []Resource{
			{Name: "cpu", Capacity: resource.MustParse("4")},
			{Name: "memory", Capacity: resource.MustParse("16Gi")},
		},
		patch: patch,
	}

	if tier.Name() != "4c" {
		t.Fatalf("Name() = %q, want %q", tier.Name(), "4c")
	}
	if got := tier.Resources(); len(got) != 2 ||
		got[0].Name != "cpu" || got[0].Capacity.Cmp(resource.MustParse("4")) != 0 ||
		got[1].Name != "memory" || got[1].Capacity.Cmp(resource.MustParse("16Gi")) != 0 {
		t.Fatalf("Resources() = %v, want cpu=4 memory=16Gi", got)
	}
	if got, ok := tier.Capacity("cpu"); !ok || got.Cmp(resource.MustParse("4")) != 0 {
		t.Fatalf("Capacity(cpu) = %s, %t, want 4, true", got.String(), ok)
	}
	if _, ok := tier.Capacity("storage"); ok {
		t.Fatal("Capacity(storage) found resource, want missing")
	}
	if tier.Patch().Type() != types.JSONPatchType {
		t.Fatalf("Patch().Type() = %s, want %s", tier.Patch().Type(), types.JSONPatchType)
	}
}

func TestNewMaterializesTiersFromTemplates(t *testing.T) {
	tiers, err := New(validTemplates(), validTiers())
	if err != nil {
		t.Fatalf("New() error = %v", err)
	}
	if len(tiers) != 5 {
		t.Fatalf("New() returned %d tiers, want 5", len(tiers))
	}

	assertTier(t, tiers[0], "tidb-node-0", []Resource{
		{Name: "cpu", Capacity: resource.MustParse("2")},
		{Name: "memory", Capacity: resource.MustParse("8Gi")},
		{Name: "iops", Capacity: resource.MustParse("2000")},
	}, `{"metadata":{"labels":{"tidbcloud.com/node-spec":"28g"}}}`)
	assertTier(t, tiers[1], "tidb-node-1", []Resource{
		{Name: "cpu", Capacity: resource.MustParse("2")},
		{Name: "memory", Capacity: resource.MustParse("8Gi")},
		{Name: "iops", Capacity: resource.MustParse("4000")},
	}, `{"metadata":{"labels":{"tidbcloud.com/node-spec":"28g"}}}`)
	assertTier(t, tiers[4], "tidb-node-4", []Resource{
		{Name: "cpu", Capacity: resource.MustParse("8")},
		{Name: "memory", Capacity: resource.MustParse("32Gi")},
	}, `{"metadata":{"labels":{"tidbcloud.com/node-spec":"832g"}}}`)
	if got := tiers[0].Cost(); got != 28 {
		t.Fatalf("tiers[0].Cost() = %d, want 28", got)
	}
	if got := tiers[1].Cost(); got != 28 {
		t.Fatalf("tiers[1].Cost() = %d, want 28", got)
	}
	if got := tiers[4].Cost(); got != 112 {
		t.Fatalf("tiers[4].Cost() = %d, want 112", got)
	}

	if got, ok := tiers[1].Capacity("iops"); !ok || got.Cmp(resource.MustParse("4000")) != 0 {
		t.Fatalf("Capacity(iops) = %s, %t, want 4000, true", got.String(), ok)
	}
	if _, ok := tiers[1].Capacity("storage"); ok {
		t.Fatal("Capacity(storage) found resource, want missing")
	}

	resources := tiers[0].Resources()
	resources[0].Capacity.Add(resource.MustParse("14"))
	if got := tiers[0].Resources()[0].Capacity; got.Cmp(resource.MustParse("2")) != 0 {
		t.Fatalf("Resources() shared backing storage, got cpu=%s", got.String())
	}
	if got, ok := tiers[0].Capacity("cpu"); !ok || got.Cmp(resource.MustParse("2")) != 0 {
		t.Fatalf("Capacity(cpu) changed through Resources() result, got %s, %t", got.String(), ok)
	}
}

func TestNewAppliesAvailablePercentagesToCapacity(t *testing.T) {
	templates := validTemplates()
	tmpl := templates.Templates["tidb-node"]
	tmpl.Available = []v1alpha1.TierAvailable{
		{
			Name:        corev1.ResourceCPU,
			Matcher:     "cpu",
			Percentages: []int32{50, 75, 100},
		},
		{
			Name:        corev1.ResourceMemory,
			Matcher:     "memory",
			Percentages: []int32{75, 90, 100},
		},
	}
	templates.Templates["tidb-node"] = tmpl

	tiers, err := New(templates, validTiers())
	if err != nil {
		t.Fatalf("New() error = %v", err)
	}
	assertTier(t, tiers[0], "tidb-node-0", []Resource{
		{Name: "cpu", Capacity: resource.MustParse("1")},
		{Name: "memory", Capacity: resource.MustParse("6Gi")},
		{Name: "iops", Capacity: resource.MustParse("2000")},
	}, `{"metadata":{"labels":{"tidbcloud.com/node-spec":"28g"}}}`)
	assertTier(t, tiers[2], "tidb-node-2", []Resource{
		{Name: "cpu", Capacity: resource.MustParse("3")},
		{Name: "memory", Capacity: resource.MustParse("14.4Gi")},
		{Name: "iops", Capacity: resource.MustParse("2000")},
	}, `{"metadata":{"labels":{"tidbcloud.com/node-spec":"416g"}}}`)
	if got := tiers[2].Cost(); got != 56 {
		t.Fatalf("tiers[2].Cost() = %d, want unchanged cost 56", got)
	}
}

func TestNewRejectsInvalidAvailable(t *testing.T) {
	tests := []struct {
		name      string
		available []v1alpha1.TierAvailable
		want      string
	}{
		{
			name: "missing name",
			available: []v1alpha1.TierAvailable{{
				Matcher:     "cpu",
				Percentages: []int32{80, 85, 90},
			}},
			want: "available name is required",
		},
		{
			name: "missing matcher",
			available: []v1alpha1.TierAvailable{{
				Name:        corev1.ResourceCPU,
				Percentages: []int32{80, 85, 90},
			}},
			want: `available "cpu" matcher is required`,
		},
		{
			name: "undefined matcher",
			available: []v1alpha1.TierAvailable{{
				Name:        corev1.ResourceCPU,
				Matcher:     "missing",
				Percentages: []int32{80, 85, 90},
			}},
			want: `available "cpu" references undefined matcher "missing"`,
		},
		{
			name: "percentages length mismatch",
			available: []v1alpha1.TierAvailable{{
				Name:        corev1.ResourceCPU,
				Matcher:     "cpu",
				Percentages: []int32{80, 85},
			}},
			want: `available "cpu" has 2 percentages, want 3`,
		},
		{
			name: "zero percentage",
			available: []v1alpha1.TierAvailable{{
				Name:        corev1.ResourceCPU,
				Matcher:     "cpu",
				Percentages: []int32{80, 0, 90},
			}},
			want: `available "cpu" percentage at index 1 must be greater than 0 and less than or equal to 100`,
		},
		{
			name: "percentage greater than 100",
			available: []v1alpha1.TierAvailable{{
				Name:        corev1.ResourceCPU,
				Matcher:     "cpu",
				Percentages: []int32{80, 101, 90},
			}},
			want: `available "cpu" percentage at index 1 must be greater than 0 and less than or equal to 100`,
		},
		{
			name: "duplicate resource",
			available: []v1alpha1.TierAvailable{
				{Name: corev1.ResourceCPU, Matcher: "cpu", Percentages: []int32{80, 85, 90}},
				{Name: corev1.ResourceCPU, Matcher: "memory", Percentages: []int32{80, 85, 90}},
			},
			want: `duplicate available resource "cpu"`,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			templates := validTemplates()
			tmpl := templates.Templates["tidb-node"]
			tmpl.Available = tt.available
			templates.Templates["tidb-node"] = tmpl

			_, err := New(templates, validTiers())
			if err == nil {
				t.Fatal("New() error = nil")
			}
			if !strings.Contains(err.Error(), tt.want) {
				t.Fatalf("New() error = %q, want substring %q", err, tt.want)
			}
		})
	}
}

func TestNewRejectsUnmatchedAvailable(t *testing.T) {
	tests := []struct {
		name      string
		configure func(*v1alpha1.TierTemplate)
		want      string
	}{
		{
			name: "available resource missing from tier",
			configure: func(tmpl *v1alpha1.TierTemplate) {
				tmpl.Available = []v1alpha1.TierAvailable{{
					Name:        corev1.ResourceStorage,
					Matcher:     "cpu",
					Percentages: []int32{80, 85, 90},
				}}
			},
			want: `available "storage": resource "storage" is missing`,
		},
		{
			name: "matcher resource value not in variants",
			configure: func(tmpl *v1alpha1.TierTemplate) {
				tmpl.Matchers = append(tmpl.Matchers, v1alpha1.TierMatcher{
					Name: "iops",
					Resource: &v1alpha1.TierMatcherResource{
						Name:     corev1.ResourceName("iops"),
						Variants: []resource.Quantity{resource.MustParse("1000")},
					},
				})
				tmpl.Available = []v1alpha1.TierAvailable{{
					Name:        corev1.ResourceCPU,
					Matcher:     "iops",
					Percentages: []int32{80},
				}}
			},
			want: `available "cpu": resource "iops" value "2k" does not match any variant`,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			templates := validTemplates()
			tmpl := templates.Templates["tidb-node"]
			tt.configure(&tmpl)
			templates.Templates["tidb-node"] = tmpl

			_, err := New(templates, validTiers())
			if err == nil {
				t.Fatal("New() error = nil")
			}
			if !strings.Contains(err.Error(), tt.want) {
				t.Fatalf("New() error = %q, want substring %q", err, tt.want)
			}
		})
	}
}

func TestNewSumsCostComponentsIntoInt64(t *testing.T) {
	templates := validTemplates()
	tmpl := templates.Templates["tidb-node"]
	tmpl.Costs = []v1alpha1.TierCost{
		{
			Name:    "cpu-compute",
			Matcher: "cpu",
			Values:  []int32{int32Max, int32Max, int32Max},
		},
		{
			Name:    "cpu-license",
			Matcher: "cpu",
			Values:  []int32{int32Max, int32Max, int32Max},
		},
	}
	templates.Templates["tidb-node"] = tmpl

	tiers, err := New(templates, validTiers())
	if err != nil {
		t.Fatalf("New() error = %v", err)
	}
	if got, want := tiers[0].Cost(), int64(int32Max)*2; got != want {
		t.Fatalf("tiers[0].Cost() = %d, want %d", got, want)
	}
}

func TestNewRejectsInvalidCosts(t *testing.T) {
	tests := []struct {
		name  string
		costs []v1alpha1.TierCost
		want  string
	}{
		{
			name: "missing costs",
			want: "costs are required",
		},
		{
			name: "missing name",
			costs: []v1alpha1.TierCost{{
				Matcher: "cpu",
				Values:  []int32{1, 2, 3},
			}},
			want: "cost name is required",
		},
		{
			name: "missing matcher",
			costs: []v1alpha1.TierCost{{
				Name:   "cpu",
				Values: []int32{1, 2, 3},
			}},
			want: `cost "cpu" matcher is required`,
		},
		{
			name: "undefined matcher",
			costs: []v1alpha1.TierCost{{
				Name:    "cpu",
				Matcher: "missing",
				Values:  []int32{1, 2, 3},
			}},
			want: `cost "cpu" references undefined matcher "missing"`,
		},
		{
			name: "values length mismatch",
			costs: []v1alpha1.TierCost{{
				Name:    "cpu",
				Matcher: "cpu",
				Values:  []int32{1, 2},
			}},
			want: `cost "cpu" has 2 values, want 3`,
		},
		{
			name: "negative value",
			costs: []v1alpha1.TierCost{{
				Name:    "cpu",
				Matcher: "cpu",
				Values:  []int32{1, -1, 3},
			}},
			want: `cost "cpu" value at index 1 must not be negative`,
		},
		{
			name: "duplicate name",
			costs: []v1alpha1.TierCost{
				{Name: "compute", Matcher: "cpu", Values: []int32{1, 2, 3}},
				{Name: "compute", Matcher: "memory", Values: []int32{1, 2, 3}},
			},
			want: `duplicate cost "compute"`,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			templates := validTemplates()
			tmpl := templates.Templates["tidb-node"]
			tmpl.Costs = tt.costs
			templates.Templates["tidb-node"] = tmpl

			_, err := New(templates, validTiers())
			if err == nil {
				t.Fatal("New() error = nil")
			}
			if !strings.Contains(err.Error(), tt.want) {
				t.Fatalf("New() error = %q, want substring %q", err, tt.want)
			}
		})
	}
}

func TestNewRejectsUnmatchedCosts(t *testing.T) {
	tests := []struct {
		name         string
		resourceName corev1.ResourceName
		variants     []resource.Quantity
		want         string
	}{
		{
			name:         "resource missing from tier",
			resourceName: corev1.ResourceStorage,
			variants:     []resource.Quantity{resource.MustParse("10Gi")},
			want:         `cost "extra": resource "storage" is missing`,
		},
		{
			name:         "resource value not in matcher variants",
			resourceName: corev1.ResourceName("iops"),
			variants:     []resource.Quantity{resource.MustParse("1000")},
			want:         `cost "extra": resource "iops" value "2k" does not match any variant`,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			templates := validTemplates()
			tmpl := templates.Templates["tidb-node"]
			tmpl.Matchers = append(tmpl.Matchers, v1alpha1.TierMatcher{
				Name: "extra",
				Resource: &v1alpha1.TierMatcherResource{
					Name:     tt.resourceName,
					Variants: tt.variants,
				},
			})
			tmpl.Costs = []v1alpha1.TierCost{{
				Name:    "extra",
				Matcher: "extra",
				Values:  []int32{1},
			}}
			templates.Templates["tidb-node"] = tmpl

			_, err := New(templates, validTiers())
			if err == nil {
				t.Fatal("New() error = nil")
			}
			if !strings.Contains(err.Error(), tt.want) {
				t.Fatalf("New() error = %q, want substring %q", err, tt.want)
			}
		})
	}
}

func TestNewRejectsInvalidInputs(t *testing.T) {
	tests := []struct {
		name      string
		templates *v1alpha1.TierTemplates
		spec      *v1alpha1.Tiers
		want      string
	}{
		{
			name: "nil templates",
			spec: validTiers(),
			want: "nil templates",
		},
		{
			name:      "missing template",
			templates: validTemplates(),
			spec: &v1alpha1.Tiers{
				Template: v1alpha1.TierTemplateRef{Name: "missing"},
				Sets:     validTiers().Sets,
			},
			want: `template "missing" not found`,
		},
		{
			name:      "dimension variants length mismatch",
			templates: validTemplates(),
			spec: &v1alpha1.Tiers{
				Template: v1alpha1.TierTemplateRef{Name: "tidb-node"},
				Sets: []v1alpha1.TierSet{{
					Dimensions: []v1alpha1.TierDimension{{
						Resources: []v1alpha1.TierDimensionResource{
							{Name: corev1.ResourceCPU, Variants: []resource.Quantity{resource.MustParse("2"), resource.MustParse("4")}},
							{Name: corev1.ResourceMemory, Variants: []resource.Quantity{resource.MustParse("8Gi")}},
						},
					}},
				}},
			},
			want: `resource "memory" has 1 variants, want 2`,
		},
		{
			name:      "duplicate resource across dimensions",
			templates: validTemplates(),
			spec: &v1alpha1.Tiers{
				Template: v1alpha1.TierTemplateRef{Name: "tidb-node"},
				Sets: []v1alpha1.TierSet{{
					Dimensions: []v1alpha1.TierDimension{
						{Resources: []v1alpha1.TierDimensionResource{{Name: corev1.ResourceCPU, Variants: []resource.Quantity{resource.MustParse("2")}}}},
						{Resources: []v1alpha1.TierDimensionResource{{Name: corev1.ResourceCPU, Variants: []resource.Quantity{resource.MustParse("4")}}}},
					},
				}},
			},
			want: `duplicate resource "cpu" across dimensions`,
		},
		{
			name:      "range does not reach max",
			templates: validTemplates(),
			spec: &v1alpha1.Tiers{
				Template: v1alpha1.TierTemplateRef{Name: "tidb-node"},
				Sets: []v1alpha1.TierSet{{
					Dimensions: []v1alpha1.TierDimension{{
						Resources: []v1alpha1.TierDimensionResource{{
							Name: corev1.ResourceCPU,
							Range: &v1alpha1.TierResourceRange{
								Min:  resource.MustParse("2"),
								Max:  resource.MustParse("5"),
								Step: resource.MustParse("2"),
							},
						}},
					}},
				}},
			},
			want: `range step does not reach max`,
		},
		{
			name:      "missing matcher resource",
			templates: validTemplates(),
			spec: &v1alpha1.Tiers{
				Template: v1alpha1.TierTemplateRef{Name: "tidb-node"},
				Sets: []v1alpha1.TierSet{{
					Dimensions: []v1alpha1.TierDimension{{
						Resources: []v1alpha1.TierDimensionResource{
							{Name: corev1.ResourceCPU, Variants: []resource.Quantity{resource.MustParse("2")}},
						},
					}},
				}},
			},
			want: `resource "memory" is missing`,
		},
		{
			name:      "no matcher hit",
			templates: validTemplates(),
			spec: &v1alpha1.Tiers{
				Template: v1alpha1.TierTemplateRef{Name: "tidb-node"},
				Sets: []v1alpha1.TierSet{{
					Dimensions: []v1alpha1.TierDimension{{
						Resources: []v1alpha1.TierDimensionResource{
							{Name: corev1.ResourceCPU, Variants: []resource.Quantity{resource.MustParse("16")}},
							{Name: corev1.ResourceMemory, Variants: []resource.Quantity{resource.MustParse("8Gi")}},
						},
					}},
				}},
			},
			want: `resource "cpu" value "16" does not match any variant`,
		},
		{
			name: "matcher resource is required",
			templates: &v1alpha1.TierTemplates{Templates: map[string]v1alpha1.TierTemplate{
				"tidb-node": {
					Matchers: []v1alpha1.TierMatcher{{Name: "cpu", Type: v1alpha1.TierMatcherTypeResource}},
					Params: []v1alpha1.TierParam{{
						Name:    "cpuspec",
						Matcher: "cpu",
						Values:  []string{"2"},
					}},
					Template: `metadata:
  labels:
    tidbcloud.com/node-spec: "{{cpuspec}}"
`,
				},
			}},
			spec: validTiers(),
			want: `matcher "cpu" resource is required`,
		},
		{
			name: "unsupported matcher type",
			templates: &v1alpha1.TierTemplates{Templates: map[string]v1alpha1.TierTemplate{
				"tidb-node": {
					Matchers: []v1alpha1.TierMatcher{{
						Name: "cpu",
						Type: v1alpha1.TierMatcherType("CEL"),
						Resource: &v1alpha1.TierMatcherResource{
							Name:     corev1.ResourceCPU,
							Variants: []resource.Quantity{resource.MustParse("2")},
						},
					}},
					Params: []v1alpha1.TierParam{{
						Name:    "cpuspec",
						Matcher: "cpu",
						Values:  []string{"2"},
					}},
					Template: `metadata:
  labels:
    tidbcloud.com/node-spec: "{{cpuspec}}"
`,
				},
			}},
			spec: validTiers(),
			want: `matcher "cpu" type "CEL" is unsupported`,
		},
		{
			name: "param matcher is required",
			templates: &v1alpha1.TierTemplates{Templates: map[string]v1alpha1.TierTemplate{
				"tidb-node": {
					Matchers: []v1alpha1.TierMatcher{{
						Name: "cpu",
						Resource: &v1alpha1.TierMatcherResource{
							Name:     corev1.ResourceCPU,
							Variants: []resource.Quantity{resource.MustParse("2")},
						},
					}},
					Params: []v1alpha1.TierParam{{
						Name:   "cpuspec",
						Values: []string{"2"},
					}},
					Template: `metadata:
  labels:
    tidbcloud.com/node-spec: "{{cpuspec}}"
`,
				},
			}},
			spec: validTiers(),
			want: `param "cpuspec" matcher is required`,
		},
		{
			name: "duplicate matcher",
			templates: &v1alpha1.TierTemplates{Templates: map[string]v1alpha1.TierTemplate{
				"tidb-node": {
					Matchers: []v1alpha1.TierMatcher{
						{
							Name: "cpu",
							Resource: &v1alpha1.TierMatcherResource{
								Name:     corev1.ResourceCPU,
								Variants: []resource.Quantity{resource.MustParse("2")},
							},
						},
						{
							Name: "cpu",
							Resource: &v1alpha1.TierMatcherResource{
								Name:     corev1.ResourceCPU,
								Variants: []resource.Quantity{resource.MustParse("4")},
							},
						},
					},
					Params: []v1alpha1.TierParam{{
						Name:    "cpuspec",
						Matcher: "cpu",
						Values:  []string{"2"},
					}},
					Template: `metadata:
  labels:
    tidbcloud.com/node-spec: "{{cpuspec}}"
`,
				},
			}},
			spec: validTiers(),
			want: `duplicate matcher "cpu"`,
		},
		{
			name: "param references undefined matcher",
			templates: &v1alpha1.TierTemplates{Templates: map[string]v1alpha1.TierTemplate{
				"tidb-node": {
					Params: []v1alpha1.TierParam{{
						Name:    "cpuspec",
						Matcher: "missing",
						Values:  []string{"2"},
					}},
					Template: `metadata:
  labels:
    tidbcloud.com/node-spec: "{{cpuspec}}"
`,
				},
			}},
			spec: validTiers(),
			want: `param "cpuspec" references undefined matcher "missing"`,
		},
		{
			name: "param values length mismatch",
			templates: &v1alpha1.TierTemplates{Templates: map[string]v1alpha1.TierTemplate{
				"tidb-node": {
					Matchers: []v1alpha1.TierMatcher{{
						Name: "cpu",
						Resource: &v1alpha1.TierMatcherResource{
							Name:     corev1.ResourceCPU,
							Variants: []resource.Quantity{resource.MustParse("2"), resource.MustParse("4")},
						},
					}},
					Params: []v1alpha1.TierParam{{
						Name:    "cpuspec",
						Matcher: "cpu",
						Values:  []string{"2"},
					}},
					Template: `metadata:
  labels:
    tidbcloud.com/node-spec: "{{cpuspec}}"
`,
				},
			}},
			spec: validTiers(),
			want: `param "cpuspec" has 1 values, want 2`,
		},
		{
			name: "param matcher resource duplicate variants",
			templates: &v1alpha1.TierTemplates{Templates: map[string]v1alpha1.TierTemplate{
				"tidb-node": {
					Matchers: []v1alpha1.TierMatcher{{
						Name: "cpu",
						Resource: &v1alpha1.TierMatcherResource{
							Name:     corev1.ResourceCPU,
							Variants: []resource.Quantity{resource.MustParse("2"), resource.MustParse("2")},
						},
					}},
					Params: []v1alpha1.TierParam{{
						Name:    "cpuspec",
						Matcher: "cpu",
						Values:  []string{"2a", "2b"},
					}},
					Template: `metadata:
  labels:
    tidbcloud.com/node-spec: "{{cpuspec}}"
`,
				},
			}},
			spec: validTiers(),
			want: `matcher "cpu" resource.variants contains duplicate value "2"`,
		},
		{
			name:      "undefined patch param",
			templates: templateWithUndefinedPatchParam(),
			spec: &v1alpha1.Tiers{
				Template: v1alpha1.TierTemplateRef{Name: "tidb-node"},
				Sets: []v1alpha1.TierSet{{
					Dimensions: []v1alpha1.TierDimension{{
						Resources: []v1alpha1.TierDimensionResource{
							{Name: corev1.ResourceCPU, Variants: []resource.Quantity{resource.MustParse("2")}},
						},
					}},
				}},
			},
			want: `function "memspec" not defined`,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			_, err := New(tt.templates, tt.spec)
			if err == nil {
				t.Fatal("New() error = nil")
			}
			if !strings.Contains(err.Error(), tt.want) {
				t.Fatalf("New() error = %q, want substring %q", err, tt.want)
			}
		})
	}
}

func assertTier(t *testing.T, got Tier, wantName string, wantResources []Resource, wantPatch string) {
	t.Helper()
	if got.Name() != wantName {
		t.Fatalf("Name() = %q, want %q", got.Name(), wantName)
	}
	gotResources := got.Resources()
	if len(gotResources) != len(wantResources) {
		t.Fatalf("Resources() length = %d, want %d: %v", len(gotResources), len(wantResources), gotResources)
	}
	for i := range wantResources {
		if gotResources[i].Name != wantResources[i].Name || gotResources[i].Capacity.Cmp(wantResources[i].Capacity) != 0 {
			t.Fatalf("Resources()[%d] = %s=%s, want %s=%s", i, gotResources[i].Name, gotResources[i].Capacity.String(), wantResources[i].Name, wantResources[i].Capacity.String())
		}
	}

	data, err := got.Patch().Data(&unstructured.Unstructured{})
	if err != nil {
		t.Fatalf("Patch().Data(): %v", err)
	}
	if string(data) != wantPatch {
		t.Fatalf("Patch().Data() = %s, want %s", data, wantPatch)
	}
}

func validTiers() *v1alpha1.Tiers {
	return &v1alpha1.Tiers{
		Template: v1alpha1.TierTemplateRef{Name: "tidb-node"},
		Sets: []v1alpha1.TierSet{
			{
				Dimensions: []v1alpha1.TierDimension{
					{
						Resources: []v1alpha1.TierDimensionResource{
							{Name: corev1.ResourceCPU, Variants: []resource.Quantity{resource.MustParse("2"), resource.MustParse("4")}},
							{Name: corev1.ResourceMemory, Variants: []resource.Quantity{resource.MustParse("8Gi"), resource.MustParse("16Gi")}},
						},
					},
					{
						Resources: []v1alpha1.TierDimensionResource{{
							Name: corev1.ResourceName("iops"),
							Range: &v1alpha1.TierResourceRange{
								Min:  resource.MustParse("2000"),
								Max:  resource.MustParse("4000"),
								Step: resource.MustParse("2000"),
							},
						}},
					},
				},
			},
			{
				Dimensions: []v1alpha1.TierDimension{{
					Resources: []v1alpha1.TierDimensionResource{
						{Name: corev1.ResourceCPU, Variants: []resource.Quantity{resource.MustParse("8")}},
						{Name: corev1.ResourceMemory, Variants: []resource.Quantity{resource.MustParse("32Gi")}},
					},
				}},
			},
		},
	}
}

func validTemplates() *v1alpha1.TierTemplates {
	return &v1alpha1.TierTemplates{Templates: map[string]v1alpha1.TierTemplate{
		"tidb-node": {
			Matchers: []v1alpha1.TierMatcher{
				{
					Name: "cpu",
					Type: v1alpha1.TierMatcherTypeResource,
					Resource: &v1alpha1.TierMatcherResource{
						Name:     corev1.ResourceCPU,
						Variants: []resource.Quantity{resource.MustParse("2"), resource.MustParse("4"), resource.MustParse("8")},
					},
				},
				{
					Name: "memory",
					Resource: &v1alpha1.TierMatcherResource{
						Name:     corev1.ResourceMemory,
						Variants: []resource.Quantity{resource.MustParse("8Gi"), resource.MustParse("16Gi"), resource.MustParse("32Gi")},
					},
				},
			},
			Params: []v1alpha1.TierParam{
				{
					Name:    "cpuspec",
					Matcher: "cpu",
					Values:  []string{"2", "4", "8"},
				},
				{
					Name:    "memspec",
					Matcher: "memory",
					Values:  []string{"8g", "16g", "32g"},
				},
			},
			Costs: []v1alpha1.TierCost{
				{
					Name:    "cpu",
					Matcher: "cpu",
					Values:  []int32{20, 40, 80},
				},
				{
					Name:    "memory",
					Matcher: "memory",
					Values:  []int32{8, 16, 32},
				},
			},
			Template: `metadata:
  labels:
    tidbcloud.com/node-spec: "{{cpuspec}}{{memspec}}"
`,
		},
	}}
}

func templateWithUndefinedPatchParam() *v1alpha1.TierTemplates {
	return &v1alpha1.TierTemplates{Templates: map[string]v1alpha1.TierTemplate{
		"tidb-node": {
			Matchers: []v1alpha1.TierMatcher{{
				Name: "cpu",
				Resource: &v1alpha1.TierMatcherResource{
					Name:     corev1.ResourceCPU,
					Variants: []resource.Quantity{resource.MustParse("2")},
				},
			}},
			Params: []v1alpha1.TierParam{
				{
					Name:    "cpuspec",
					Matcher: "cpu",
					Values:  []string{"2"},
				},
			},
			Template: `metadata:
  labels:
    tidbcloud.com/node-spec: "{{memspec}}"
`,
		},
	}}
}

const int32Max = int32(^uint32(0) >> 1)
