package tier

import (
	"bytes"
	"encoding/json"
	"os"
	"path/filepath"
	"slices"
	"strings"
	"testing"

	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	"sigs.k8s.io/yaml"

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

func TestLoadTemplatesFileExample(t *testing.T) {
	templates, err := LoadTemplatesFile(filepath.Join("..", "..", "config", "tier-templates.yaml"))
	if err != nil {
		t.Fatalf("LoadTemplatesFile() error = %v", err)
	}

	tiers, err := New(templates, &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("3257m"), resource.MustParse("7220m"), resource.MustParse("15200m")}},
					{Name: corev1.ResourceMemory, Variants: []resource.Quantity{resource.MustParse("13115Mi"), resource.MustParse("13114Mi"), resource.MustParse("28041Mi")}},
				},
			}},
		}},
	})
	if err != nil {
		t.Fatalf("New() error = %v", err)
	}
	if len(tiers) != 3 {
		t.Fatalf("New() returned %d tiers, want 3", len(tiers))
	}
	assertTier(t, tiers[1], "tidb-node-1", []Resource{
		{Name: "cpu", Capacity: resource.MustParse("7220m")},
		{Name: "memory", Capacity: resource.MustParse("13114Mi")},
	}, `{"metadata":{"labels":{"tags.tidbcloud.com/node-spec":"8C16G"}},"spec":{"template":{"spec":{"overlay":{"pod":{"spec":{"containers":[{"env":[{"name":"GOMEMLIMIT","value":"11262MiB"}],"name":"tidb","resources":{"limits":{"cpu":"7910m","memory":"14078Mi"},"requests":{"cpu":"7220m","memory":"13114Mi"}}}],"nodeSelector":{"node-spec":"8C16G"}}}}}}}}`)
	if got := tiers[1].Cost(); got != 96 {
		t.Fatalf("tiers[1].Cost() = %d, want 96", got)
	}
}

func TestLoadTemplatesFileMaterializesTiDBGroupCatalog(t *testing.T) {
	templates, err := LoadTemplatesFile(filepath.Join("..", "..", "config", "tier-templates.yaml"))
	if err != nil {
		t.Fatalf("LoadTemplatesFile() error = %v", err)
	}
	tidbNodeTemplate := templates.Templates["tidb-node"]
	params := make(map[string]v1alpha1.TierParam, len(tidbNodeTemplate.Params))
	for _, param := range tidbNodeTemplate.Params {
		params[param.Name] = param
	}
	if got := params["nodespec"]; got.Matcher != "cpu" || !slices.Equal(got.Values, []string{"4C16G", "8C16G", "16C32G"}) {
		t.Fatalf("nodespec param = %#v, want production node specs", got)
	}
	if _, ok := params["cpuspec"]; ok {
		t.Fatal("cpuspec must not diverge from the production node-spec tuple")
	}
	if _, ok := params["memoryspec"]; ok {
		t.Fatal("memoryspec must not diverge from the production node-spec tuple")
	}

	tiers, err := New(templates, &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("3257m"), resource.MustParse("7220m"), resource.MustParse("15200m"),
					}},
					{Name: corev1.ResourceMemory, Variants: []resource.Quantity{
						resource.MustParse("13115Mi"), resource.MustParse("13114Mi"), resource.MustParse("28041Mi"),
					}},
				},
			}},
		}},
	})
	if err != nil {
		t.Fatalf("New() error = %v", err)
	}
	if len(tiers) != 3 {
		t.Fatalf("New() returned %d tiers, want 3", len(tiers))
	}

	// These exact tuples are emitted by CSNG provisioning. Matcher lookup is
	// exact, so this test deliberately fails if the catalog drifts on any field.
	expected := []struct {
		name     string
		nodeSpec string
		cpuReq   string
		memReq   string
		cpuLim   string
		memLim   string
		goMemLim string
		cost     int64
	}{
		{"tidb-node-0", "4C16G", "3257m", "13115Mi", "3920m", "14079Mi", "11263MiB", 56},
		{"tidb-node-1", "8C16G", "7220m", "13114Mi", "7910m", "14078Mi", "11262MiB", 96},
		{"tidb-node-2", "16C32G", "15200m", "28041Mi", "15890m", "29326Mi", "23460MiB", 192},
	}
	for i, got := range tiers {
		want := expected[i]
		if got.Name() != want.name {
			t.Fatalf("tier[%d].Name() = %q, want %q", i, got.Name(), want.name)
		}
		if got.Cost() != want.cost {
			t.Fatalf("tier[%d].Cost() = %d, want %d", i, got.Cost(), want.cost)
		}
		assertTierPatch(t, got, want.nodeSpec, want.cpuReq, want.memReq, want.cpuLim, want.memLim, want.goMemLim)
	}
}

func assertTierPatch(t *testing.T, tier Tier, nodeSpec, cpuReq, memReq, cpuLim, memLim, goMemLim string) {
	t.Helper()
	patchBytes, err := tier.Patch().Data(&corev1.Pod{})
	if err != nil {
		t.Fatalf("tier %q patch data error = %v", tier.Name(), err)
	}
	var patch map[string]interface{}
	if err := json.Unmarshal(patchBytes, &patch); err != nil {
		t.Fatalf("tier %q patch unmarshal error = %v", tier.Name(), err)
	}
	labels := patch["metadata"].(map[string]interface{})["labels"].(map[string]interface{})
	if labels[nodeSpecLabelKeyForTest] != nodeSpec {
		t.Fatalf("tier %q node-spec label = %v, want %s", tier.Name(), labels[nodeSpecLabelKeyForTest], nodeSpec)
	}
	podSpec := patch["spec"].(map[string]interface{})["template"].(map[string]interface{})["spec"].(map[string]interface{})["overlay"].(map[string]interface{})["pod"].(map[string]interface{})["spec"].(map[string]interface{})
	if podSpec["nodeSelector"].(map[string]interface{})["node-spec"] != nodeSpec {
		t.Fatalf("tier %q nodeSelector does not use %s", tier.Name(), nodeSpec)
	}
	containers := podSpec["containers"].([]interface{})
	if len(containers) != 1 {
		t.Fatalf("tier %q containers len = %d, want 1", tier.Name(), len(containers))
	}
	container := containers[0].(map[string]interface{})
	resources := container["resources"].(map[string]interface{})
	requests := resources["requests"].(map[string]interface{})
	limits := resources["limits"].(map[string]interface{})
	if container["name"] != "tidb" || requests["cpu"] != cpuReq || requests["memory"] != memReq ||
		limits["cpu"] != cpuLim || limits["memory"] != memLim {
		t.Fatalf("tier %q TiDB resources do not match expected catalog values", tier.Name())
	}
	env := container["env"].([]interface{})
	if len(env) != 1 || env[0].(map[string]interface{})["name"] != "GOMEMLIMIT" || env[0].(map[string]interface{})["value"] != goMemLim {
		t.Fatalf("tier %q GOMEMLIMIT does not match %s", tier.Name(), goMemLim)
	}
}

const nodeSpecLabelKeyForTest = "tags.tidbcloud.com/node-spec"

func TestHelmTierTemplateMatchesCanonicalConfig(t *testing.T) {
	canonical, err := LoadTemplatesFile(filepath.Join("..", "..", "config", "tier-templates.yaml"))
	if err != nil {
		t.Fatalf("LoadTemplatesFile() error = %v", err)
	}
	valuesData, err := os.ReadFile(filepath.Join("..", "..", "charts", "capacity-planner", "values.yaml"))
	if err != nil {
		t.Fatalf("read Helm values: %v", err)
	}
	var values struct {
		TierTemplates struct {
			Config string `json:"config"`
		} `json:"tierTemplates"`
	}
	if err := yaml.Unmarshal(valuesData, &values); err != nil {
		t.Fatalf("parse Helm values: %v", err)
	}
	helmTemplates, err := ParseTemplatesConfig([]byte(values.TierTemplates.Config))
	if err != nil {
		t.Fatalf("parse Helm tier templates: %v", err)
	}
	// The outer Helm |-/file boundary can differ only in the final newline of
	// the inner patch template; normalize it before comparing the catalog.
	canonicalTemplate := canonical.Templates["tidb-node"]
	canonicalTemplate.Template = strings.TrimSpace(canonicalTemplate.Template)
	canonical.Templates["tidb-node"] = canonicalTemplate
	helmTemplate := helmTemplates.Templates["tidb-node"]
	helmTemplate.Template = strings.TrimSpace(helmTemplate.Template)
	helmTemplates.Templates["tidb-node"] = helmTemplate
	canonicalJSON, err := json.Marshal(canonical)
	if err != nil {
		t.Fatalf("marshal canonical tier templates: %v", err)
	}
	helmJSON, err := json.Marshal(helmTemplates)
	if err != nil {
		t.Fatalf("marshal Helm tier templates: %v", err)
	}
	if !bytes.Equal(canonicalJSON, helmJSON) {
		t.Fatalf("Helm tier templates differ from config/tier-templates.yaml")
	}
}

func TestParseTemplatesConfigRejectsUnknownFields(t *testing.T) {
	_, err := ParseTemplatesConfig([]byte(`
templates:
  tidb-node:
    unknown: true
`))
	if err == nil {
		t.Fatal("ParseTemplatesConfig() error = nil")
	}
	if !strings.Contains(err.Error(), "unknown field") {
		t.Fatalf("ParseTemplatesConfig() error = %q, want unknown field", err)
	}
}

func TestParseTemplatesConfigRejectsCostOutsideInt32(t *testing.T) {
	_, err := ParseTemplatesConfig([]byte(`
templates:
  tidb-node:
    matchers:
    - name: cpu
      resource:
        name: cpu
        variants: ["1"]
    costs:
    - name: cpu
      matcher: cpu
      values: [2147483648]
    template: '{}'
`))
	if err == nil {
		t.Fatal("ParseTemplatesConfig() error = nil")
	}
	if !strings.Contains(err.Error(), "int32") {
		t.Fatalf("ParseTemplatesConfig() error = %q, want int32 range error", err)
	}
}
