package config

import (
	"errors"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"helm.sh/helm/v3/pkg/chartutil"
	"helm.sh/helm/v3/pkg/engine"
	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
)

func TestTemplateConfig(t *testing.T) {
	cfg, err := LoadTemplatesConfig("../../examples/k8s-addons-templates.yaml",
		"../../examples/k8s-infra-templates.yaml",
		"../../examples/k8s-dedicated-templates.yaml",
		"../../examples/k8s-dp-templates.yaml")
	assert.NoError(t, err)
	assert.NotEmpty(t, cfg.Addons)
	assert.NotEmpty(t, cfg.Infra)
}

func TestTemplateConfigLoadsDataplaneExporter(t *testing.T) {
	cfg, err := LoadTemplatesConfig(
		"../../examples/k8s-addons-templates.yaml",
		"../../examples/k8s-infra-templates.yaml",
		"../../examples/k8s-dedicated-templates.yaml",
		"../../examples/k8s-dp-templates.yaml",
	)
	require.NoError(t, err)
	require.NotEmpty(t, cfg.Infra)
	require.Equal(t, "tidb-admin", cfg.Infra[0].DataplaneExporter.Namespace)
	require.Equal(t, "dataplane-exporter", cfg.Infra[0].DataplaneExporter.ReleaseName)
	require.NotEmpty(t, cfg.Infra[0].DataplaneExporter.ChartPath)
}

func TestDataplaneExporterChartLoadsFromExample(t *testing.T) {
	cfg, err := LoadTemplatesConfig(
		"../../examples/k8s-addons-templates.yaml",
		"../../examples/k8s-infra-templates.yaml",
		"../../examples/k8s-dedicated-templates.yaml",
		"../../examples/k8s-dp-templates.yaml",
	)
	require.NoError(t, err)

	component := cfg.Infra[0].DataplaneExporter
	component.ChartPath = filepath.Clean(filepath.Join("../../examples", component.ChartPath))
	chart, err := component.ToHelmChart()
	require.NoError(t, err)
	require.Equal(t, "dataplane-exporter", chart.Metadata.Name)

	values := component.ToHelmValues()
	image := values["image"].(map[string]interface{})
	require.NotEmpty(t, image["repository"])
	require.NotEmpty(t, image["tag"])
}

func TestDataplaneExporterChartRendersExpectedResources(t *testing.T) {
	cfg, err := LoadTemplatesConfig(
		"../../examples/k8s-addons-templates.yaml",
		"../../examples/k8s-infra-templates.yaml",
		"../../examples/k8s-dedicated-templates.yaml",
		"../../examples/k8s-dp-templates.yaml",
	)
	require.NoError(t, err)

	component := cfg.Infra[0].DataplaneExporter
	component.ChartPath = filepath.Clean(filepath.Join("../../examples", component.ChartPath))
	chart, err := component.ToHelmChart()
	require.NoError(t, err)

	values, err := chartutil.CoalesceValues(chart, component.ToHelmValues())
	require.NoError(t, err)

	renderValues, err := chartutil.ToRenderValues(chart, values, chartutil.ReleaseOptions{
		Name:      component.ReleaseName,
		Namespace: component.Namespace,
		IsInstall: true,
		Revision:  1,
	}, chartutil.DefaultCapabilities)
	require.NoError(t, err)

	rendered, err := engine.Render(chart, renderValues)
	require.NoError(t, err)

	manifest := strings.Join(mapsValues(rendered), "\n---\n")
	require.Contains(t, manifest, "kind: Deployment")
	require.Contains(t, manifest, "kind: Service")
	require.Contains(t, manifest, "kind: ServiceAccount")
	require.Contains(t, manifest, "kind: ClusterRole")
	require.Contains(t, manifest, "kind: ClusterRoleBinding")
}

func TestServiceProxyChartRendersAppBoxProxyIngress(t *testing.T) {
	component := InfraComponentConfig{
		HelmConfig:  HelmConfig{ChartPath: filepath.Clean("../../helm/service-proxy")},
		ReleaseName: "service-proxy",
		Namespace:   "tidb-admin",
		HelmValues: map[string]interface{}{
			"ingress": map[string]interface{}{
				"enabled":     true,
				"protocols":   "http,https",
				"regexPrefix": "",
				"stripPath":   false,
				"paths": []interface{}{
					map[string]interface{}{
						"path":     "/proxy",
						"pathType": "Prefix",
					},
				},
			},
		},
	}
	chart, err := component.ToHelmChart()
	require.NoError(t, err)

	values, err := chartutil.CoalesceValues(chart, component.ToHelmValues())
	require.NoError(t, err)

	renderValues, err := chartutil.ToRenderValues(chart, values, chartutil.ReleaseOptions{
		Name:      component.ReleaseName,
		Namespace: component.Namespace,
		IsInstall: true,
		Revision:  1,
	}, chartutil.DefaultCapabilities)
	require.NoError(t, err)

	rendered, err := engine.Render(chart, renderValues)
	require.NoError(t, err)

	manifest := strings.Join(mapsValues(rendered), "\n---\n")
	require.Equal(t, 2, strings.Count(manifest, `konghq.com/strip-path: "false"`), "service and audit-check ingresses should both render explicit boolean false strip-path")
	for _, want := range []string{
		"kind: Ingress",
		"name: service-proxy",
		"namespace: tidb-admin",
		`konghq.com/protocols: "http,https"`,
		`konghq.com/strip-path: "false"`,
		"name: service-proxy",
		"number: 80",
		"path: /proxy",
		"pathType: Prefix",
	} {
		require.Contains(t, manifest, want)
	}
	require.NotContains(t, manifest, `konghq.com/regex-prefix: "/@"`)
}

func TestDataplaneExporterChartUsesConfiguredPorts(t *testing.T) {
	cfg, err := LoadTemplatesConfig(
		"../../examples/k8s-addons-templates.yaml",
		"../../examples/k8s-infra-templates.yaml",
		"../../examples/k8s-dedicated-templates.yaml",
		"../../examples/k8s-dp-templates.yaml",
	)
	require.NoError(t, err)

	component := cfg.Infra[0].DataplaneExporter
	component.ChartPath = filepath.Clean(filepath.Join("../../examples", component.ChartPath))
	component.HelmValues["containerPort"] = 9090
	componentService := component.HelmValues["service"].(map[string]interface{})
	componentService["port"] = 9191

	chart, err := component.ToHelmChart()
	require.NoError(t, err)

	values, err := chartutil.CoalesceValues(chart, component.ToHelmValues())
	require.NoError(t, err)

	renderValues, err := chartutil.ToRenderValues(chart, values, chartutil.ReleaseOptions{
		Name:      component.ReleaseName,
		Namespace: component.Namespace,
		IsInstall: true,
		Revision:  1,
	}, chartutil.DefaultCapabilities)
	require.NoError(t, err)

	rendered, err := engine.Render(chart, renderValues)
	require.NoError(t, err)

	manifest := strings.Join(mapsValues(rendered), "\n---\n")
	require.Contains(t, manifest, "containerPort: 9090")
	require.Contains(t, manifest, "--listen-addr=:9090")
	require.Contains(t, manifest, "port: 9191")
	require.Contains(t, manifest, "targetPort: http")
}

func TestDataplaneExporterChartCanReuseServiceAccountWhileKeepingRBAC(t *testing.T) {
	cfg, err := LoadTemplatesConfig(
		"../../examples/k8s-addons-templates.yaml",
		"../../examples/k8s-infra-templates.yaml",
		"../../examples/k8s-dedicated-templates.yaml",
		"../../examples/k8s-dp-templates.yaml",
	)
	require.NoError(t, err)

	component := cfg.Infra[0].DataplaneExporter
	component.ChartPath = filepath.Clean(filepath.Join("../../examples", component.ChartPath))
	component.HelmValues["serviceAccount"] = map[string]interface{}{
		"create": false,
		"name":   "existing-dataplane-exporter",
	}
	component.HelmValues["rbac"] = map[string]interface{}{
		"create": true,
	}

	chart, err := component.ToHelmChart()
	require.NoError(t, err)

	values, err := chartutil.CoalesceValues(chart, component.ToHelmValues())
	require.NoError(t, err)

	renderValues, err := chartutil.ToRenderValues(chart, values, chartutil.ReleaseOptions{
		Name:      component.ReleaseName,
		Namespace: component.Namespace,
		IsInstall: true,
		Revision:  1,
	}, chartutil.DefaultCapabilities)
	require.NoError(t, err)

	rendered, err := engine.Render(chart, renderValues)
	require.NoError(t, err)

	manifest := strings.Join(mapsValues(rendered), "\n---\n")
	require.NotContains(t, manifest, "\nkind: ServiceAccount\nmetadata:\n")
	require.Contains(t, manifest, "kind: ClusterRole")
	require.Contains(t, manifest, "kind: ClusterRoleBinding")
	require.Contains(t, manifest, "name: existing-dataplane-exporter")
}

func mapsValues(m map[string]string) []string {
	values := make([]string, 0, len(m))
	for _, value := range m {
		values = append(values, value)
	}
	return values
}

func TestGetTemplateConfig(t *testing.T) {
	cases := map[string]struct {
		config        TemplatesConfig
		version       string
		expectInfraId string
		expectAddonId string
	}{
		"return the default template": {
			config: TemplatesConfig{
				Infra: []InfraTemplateConfig{
					{
						TemplateConfigMeta: TemplateConfigMeta{
							TemplateId: "test-v1",
							SupportedVersions: []string{
								"1.24",
								"1.25",
								"1.26",
							},
							Default: true,
						},
					},
				},
				Addons: []AddonTemplateConfig{
					{
						TemplateId: "test-v1",
						Default:    true,
					},
				},
			},
			version:       "1.25",
			expectInfraId: "test-v1",
			expectAddonId: "test-v1",
		},
		"return empty if version is not supported": {
			config: TemplatesConfig{
				Infra: []InfraTemplateConfig{
					{
						TemplateConfigMeta: TemplateConfigMeta{
							TemplateId: "test-v1",
							SupportedVersions: []string{
								"1.24",
								"1.25",
								"1.26",
							},
							Default: true,
						},
					},
				},
				Addons: []AddonTemplateConfig{
					{
						TemplateId: "test-v1",
						Default:    true,
					},
				},
			},
			version:       "1.28",
			expectInfraId: "",
			expectAddonId: "test-v1",
		},
		"return the default template that support the version": {
			config: TemplatesConfig{
				Infra: []InfraTemplateConfig{
					{
						TemplateConfigMeta: TemplateConfigMeta{
							TemplateId: "test-v1",
							SupportedVersions: []string{
								"1.24",
								"1.25",
								"1.26",
							},
							Default: true,
						},
					},
					{
						TemplateConfigMeta: TemplateConfigMeta{
							TemplateId: "test-v2",
							SupportedVersions: []string{
								"1.27",
								"1.28",
							},
						},
					},
					{
						TemplateConfigMeta: TemplateConfigMeta{
							TemplateId: "test-v3",
							Default:    true,
							SupportedVersions: []string{
								"1.27",
								"1.28",
							},
						},
					},
				},
			},
			version:       "1.27",
			expectInfraId: "test-v3",
		},
	}

	for name, c := range cases {
		t.Run(name, func(t *testing.T) {
			addonTmplId := c.config.GetDefaultAddonTemplateId()
			infraTmplId := c.config.GetDefaultInfraTemplateIdWithVersion(c.version)
			assert.Equal(t, c.expectAddonId, addonTmplId)
			assert.Equal(t, c.expectInfraId, infraTmplId)
			tmplId := getDefaultTemplateIdWithVersion(c.version, c.config.Infra)
			assert.Equal(t, c.expectInfraId, tmplId)
			if tmplId != "" {
				tmpl, exists := getTemplateWithVersion(c.config.Infra, tmplId, c.version)
				assert.Equal(t, c.expectInfraId, tmpl.TemplateId)
				assert.Equal(t, true, exists)
			}
		})
	}
}

func TestGetCRDs(t *testing.T) {
	const validCRDContent = `
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: tests.example.com
`
	const anotherValidCRDContent = `
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: anothertest.example.com
`
	const invalidCRDContent = `apiVersion: v1 kind: Pod`

	testCases := []struct {
		name           string
		setup          func(t *testing.T, chartPath string) InfraComponentConfig
		expectErr      bool
		expectErrAs    interface{}
		expectCRDCount int
		validateCRDs   func(t *testing.T, crds []*apiextensionsv1.CustomResourceDefinition)
	}{
		{
			name: "Success with CRDVersion and one yaml file",
			setup: func(t *testing.T, chartPath string) InfraComponentConfig {
				v1Dir := filepath.Join(chartPath, "v1")
				require.NoError(t, os.Mkdir(v1Dir, 0755))
				crdFile := filepath.Join(v1Dir, "my-crd.yaml")
				require.NoError(t, os.WriteFile(crdFile, []byte(validCRDContent), 0644))
				return InfraComponentConfig{
					HelmConfig: HelmConfig{ChartPath: chartPath},
					CRDVersion: "v1",
				}
			},
			expectCRDCount: 1,
			validateCRDs: func(t *testing.T, crds []*apiextensionsv1.CustomResourceDefinition) {
				assert.Equal(t, "tests.example.com", crds[0].Name)
			},
		},
		{
			name: "Success with CRDVersion and one yml file",
			setup: func(t *testing.T, chartPath string) InfraComponentConfig {
				v1Dir := filepath.Join(chartPath, "v1")
				require.NoError(t, os.Mkdir(v1Dir, 0755))
				crdFile := filepath.Join(v1Dir, "my-crd.yml")
				require.NoError(t, os.WriteFile(crdFile, []byte(validCRDContent), 0644))
				return InfraComponentConfig{
					HelmConfig: HelmConfig{ChartPath: chartPath},
					CRDVersion: "v1",
				}
			},
			expectCRDCount: 1,
			validateCRDs: func(t *testing.T, crds []*apiextensionsv1.CustomResourceDefinition) {
				assert.Equal(t, "tests.example.com", crds[0].Name)
			},
		},
		{
			name: "Success with CRDVersion and multiple yaml files",
			setup: func(t *testing.T, chartPath string) InfraComponentConfig {
				v1Dir := filepath.Join(chartPath, "v1")
				require.NoError(t, os.Mkdir(v1Dir, 0755))
				crdFile1 := filepath.Join(v1Dir, "my-crd1.yaml")
				require.NoError(t, os.WriteFile(crdFile1, []byte(validCRDContent), 0644))
				crdFile2 := filepath.Join(v1Dir, "my-crd2.yaml")
				require.NoError(t, os.WriteFile(crdFile2, []byte(anotherValidCRDContent), 0644))
				return InfraComponentConfig{
					HelmConfig: HelmConfig{ChartPath: chartPath},
					CRDVersion: "v1",
				}
			},
			expectCRDCount: 2,
			validateCRDs: func(t *testing.T, crds []*apiextensionsv1.CustomResourceDefinition) {
				names := []string{crds[0].Name, crds[1].Name}
				assert.Contains(t, names, "tests.example.com")
				assert.Contains(t, names, "anothertest.example.com")
			},
		},
		{
			name: "Error when CRDVersion directory not found",
			setup: func(t *testing.T, chartPath string) InfraComponentConfig {
				return InfraComponentConfig{
					HelmConfig: HelmConfig{ChartPath: chartPath},
					CRDVersion: "non-existent-version",
				}
			},
			expectErr:   true,
			expectErrAs: &CRDVersionError{},
		},
		{
			name: "Error when CRDVersion directory is empty",
			setup: func(t *testing.T, chartPath string) InfraComponentConfig {
				v1Dir := filepath.Join(chartPath, "v1-empty")
				require.NoError(t, os.Mkdir(v1Dir, 0755))
				return InfraComponentConfig{
					HelmConfig: HelmConfig{ChartPath: chartPath},
					CRDVersion: "v1-empty",
				}
			},
			expectErr:   true,
			expectErrAs: &CRDVersionError{},
		},
		{
			name: "Error when CRDVersion directory contains no yaml files",
			setup: func(t *testing.T, chartPath string) InfraComponentConfig {
				v1Dir := filepath.Join(chartPath, "v1-no-yaml")
				require.NoError(t, os.Mkdir(v1Dir, 0755))
				otherFile := filepath.Join(v1Dir, "some-file.txt")
				require.NoError(t, os.WriteFile(otherFile, []byte("hello"), 0644))
				return InfraComponentConfig{
					HelmConfig: HelmConfig{ChartPath: chartPath},
					CRDVersion: "v1-no-yaml",
				}
			},
			expectErr:   true,
			expectErrAs: &CRDVersionError{},
		},
		{
			name: "Error when CRD file is invalid",
			setup: func(t *testing.T, chartPath string) InfraComponentConfig {
				v1Dir := filepath.Join(chartPath, "v1-invalid")
				require.NoError(t, os.Mkdir(v1Dir, 0755))
				crdFile := filepath.Join(v1Dir, "my-crd.yaml")
				require.NoError(t, os.WriteFile(crdFile, []byte(invalidCRDContent), 0644))
				return InfraComponentConfig{
					HelmConfig: HelmConfig{ChartPath: chartPath},
					CRDVersion: "v1-invalid",
				}
			},
			expectErr: true,
		},
		{
			name: "Success with CRDPaths for backward compatibility",
			setup: func(t *testing.T, chartPath string) InfraComponentConfig {
				crdFile := filepath.Join(chartPath, "legacy-crd.yaml")
				require.NoError(t, os.WriteFile(crdFile, []byte(validCRDContent), 0644))
				return InfraComponentConfig{
					HelmConfig: HelmConfig{
						ChartPath: chartPath,
						CRDPaths:  []string{crdFile},
					},
				}
			},
			expectCRDCount: 1,
			validateCRDs: func(t *testing.T, crds []*apiextensionsv1.CustomResourceDefinition) {
				assert.Equal(t, "tests.example.com", crds[0].Name)
			},
		},
		{
			name: "CRDVersion takes precedence over CRDPaths",
			setup: func(t *testing.T, chartPath string) InfraComponentConfig {
				// Setup CRDPaths
				legacyCrdFile := filepath.Join(chartPath, "legacy-crd.yaml")
				require.NoError(t, os.WriteFile(legacyCrdFile, []byte(anotherValidCRDContent), 0644))

				// Setup CRDVersion
				v1Dir := filepath.Join(chartPath, "v1")
				require.NoError(t, os.Mkdir(v1Dir, 0755))
				crdFile := filepath.Join(v1Dir, "my-crd.yaml")
				require.NoError(t, os.WriteFile(crdFile, []byte(validCRDContent), 0644))

				return InfraComponentConfig{
					HelmConfig: HelmConfig{
						ChartPath: chartPath,
						CRDPaths:  []string{legacyCrdFile},
					},
					CRDVersion: "v1",
				}
			},
			expectCRDCount: 1,
			validateCRDs: func(t *testing.T, crds []*apiextensionsv1.CustomResourceDefinition) {
				// Should load from CRDVersion, not CRDPaths
				assert.Equal(t, "tests.example.com", crds[0].Name)
			},
		},
		{
			name: "Success with no CRDs to load",
			setup: func(t *testing.T, chartPath string) InfraComponentConfig {
				return InfraComponentConfig{
					HelmConfig: HelmConfig{ChartPath: chartPath},
				}
			},
			expectCRDCount: 0,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			chartPath := t.TempDir()
			componentConfig := tc.setup(t, chartPath)

			crds, err := componentConfig.GetCRDs()

			if tc.expectErr {
				require.Error(t, err)
				if tc.expectErrAs != nil {
					assert.True(t, errors.As(err, &tc.expectErrAs), "Expected error to be of type %T", tc.expectErrAs)
				}
			} else {
				require.NoError(t, err)
			}

			require.Len(t, crds, tc.expectCRDCount)
			if tc.validateCRDs != nil {
				tc.validateCRDs(t, crds)
			}
		})
	}
}
