package config

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

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/infra-provider/pkg/config"
	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
)

func TestTemplateConfig(t *testing.T) {
	config, 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, config.Addons)
	assert.NotEmpty(t, config.Infra)
}

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: config.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: config.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: config.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: config.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: config.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: config.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: config.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: config.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: config.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: config.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)
			}
		})
	}
}
