/*
 *
 * Copyright (c) 2025 Bocloud Technologies Co., Ltd.
 * installer is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *          <http://license.coscl.org.cn/MulanPSL2>
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 *
 */

package config

import (
	"fmt"
	"os"
	"strings"
	"testing"

	"github.com/agiledragon/gomonkey/v2"
	"github.com/stretchr/testify/assert"

	"gopkg.openfuyao.cn/bkeadm/pkg/global"
	"gopkg.openfuyao.cn/bkeadm/utils"
	"gopkg.openfuyao.cn/bkeadm/utils/log"
	configv1beta1 "gopkg.openfuyao.cn/cluster-api-provider-bke/api/bkecommon/v1beta1"
	confv1beta1 "gopkg.openfuyao.cn/cluster-api-provider-bke/api/bkecommon/v1beta1"
	configinit "gopkg.openfuyao.cn/cluster-api-provider-bke/common/cluster/initialize"
	"gopkg.openfuyao.cn/cluster-api-provider-bke/common/security"
	yaml2 "sigs.k8s.io/yaml"
)

const (
	testNumericZero   = 0
	testNumericOne    = 1
	testNumericTwo    = 2
	testNumericThree  = 3
	testNumericFour   = 4
	testDefaultPort   = "5000"
	testDefaultDomain = "test.domain.com"

	testIPv4SegmentA = 192
	testIPv4SegmentB = 168
	testIPv4SegmentC = 1
	testIPv4SegmentD = 1
)

func TestGenerateControllerParam(t *testing.T) {
	tests := []struct {
		name             string
		domain           string
		customExtraValue string
		wantSandbox      string
		wantOffline      string
	}{
		{
			name:             "normal domain without custom extra",
			domain:           "registry.example.com",
			customExtraValue: "",
			wantOffline:      "true",
		},
		{
			name:             "domain with custom other repo containing domain",
			domain:           "registry.example.com",
			customExtraValue: "custom.repo.com/image",
			wantOffline:      "false",
		},
		{
			name:             "domain with custom other repo not containing domain",
			domain:           "registry.example.com",
			customExtraValue: "other.repo.com/image:name",
			wantOffline:      "false",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			originalCustomExtra := global.CustomExtra["otherRepo"]
			global.CustomExtra["otherRepo"] = tt.customExtraValue
			defer func() {
				global.CustomExtra["otherRepo"] = originalCustomExtra
			}()

			sandbox, offline := GenerateControllerParam(tt.domain)

			assert.NotEmpty(t, sandbox)
			assert.Equal(t, tt.wantOffline, offline)
		})
	}
}

func TestGenerateControllerParamWithDifferentDomains(t *testing.T) {
	tests := []struct {
		name   string
		domain string
	}{
		{
			name:   "standard registry domain",
			domain: "registry.bocloud.com",
		},
		{
			name:   "IP address with port",
			domain: fmt.Sprintf("%d.%d.%d.%d:5000", testIPv4SegmentA, testIPv4SegmentB, testIPv4SegmentC, testIPv4SegmentD),
		},
		{
			name:   "localhost with port",
			domain: "localhost:8080",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			originalCustomExtra := global.CustomExtra["otherRepo"]
			global.CustomExtra["otherRepo"] = ""
			defer func() {
				global.CustomExtra["otherRepo"] = originalCustomExtra
			}()

			sandbox, offline := GenerateControllerParam(tt.domain)

			assert.Contains(t, sandbox, tt.domain)
			assert.Equal(t, "true", offline)
		})
	}
}

func TestOptionsEnsureDirectory(t *testing.T) {
	tests := []struct {
		name         string
		directory    string
		mockExists   func(string) bool
		mockMkdirAll func(string, os.FileMode) error
		expectError  bool
	}{
		{
			name:      "directory already exists",
			directory: "/existing/dir",
			mockExists: func(path string) bool {
				return true
			},
			expectError: false,
		},
		{
			name:      "directory creation succeeds",
			directory: "/new/dir",
			mockExists: func(path string) bool {
				return false
			},
			mockMkdirAll: func(path string, perm os.FileMode) error {
				return nil
			},
			expectError: false,
		},
		{
			name:      "directory creation fails",
			directory: "/fail/dir",
			mockExists: func(path string) bool {
				return false
			},
			mockMkdirAll: func(path string, perm os.FileMode) error {
				return os.ErrPermission
			},
			expectError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			patches := gomonkey.NewPatches()
			defer patches.Reset()
			patches.ApplyFunc(utils.Exists, tt.mockExists)

			if tt.mockMkdirAll != nil {
				patches.ApplyFunc(os.MkdirAll, tt.mockMkdirAll)
			}

			patches.ApplyFunc(log.SteppedInfo, func(stepName string, args ...any) {})

			opts := &Options{Directory: tt.directory}
			err := opts.ensureDirectory()

			if tt.expectError {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
			}
		})
	}
}

func TestOptionsCreateBKECluster(t *testing.T) {
	opts := &Options{}
	cluster := opts.createBKECluster()

	assert.Equal(t, "bke-cluster", cluster.Name)
	assert.Equal(t, "bke-cluster", cluster.Namespace)
	assert.Equal(t, "bke.bocloud.com/v1beta1", cluster.APIVersion)
	assert.Equal(t, "BKECluster", cluster.Kind)
	assert.NotNil(t, cluster.Spec.KubeletConfigRef)
	assert.Equal(t, "bke-kubelet", cluster.Spec.KubeletConfigRef.Name)
	assert.Equal(t, "bke-kubelet", cluster.Spec.KubeletConfigRef.Namespace)
}

func TestOptionsApplyCustomConfig(t *testing.T) {
	tests := []struct {
		name        string
		customExtra map[string]string
		imageRepo   confv1beta1.Repo
		yumRepo     confv1beta1.Repo
		chartRepo   confv1beta1.Repo
		ntpServer   string
		checkFunc   func(*testing.T, *confv1beta1.BKEConfig)
	}{
		{
			name:        "empty configs should not modify",
			customExtra: nil,
			imageRepo:   confv1beta1.Repo{},
			yumRepo:     confv1beta1.Repo{},
			chartRepo:   confv1beta1.Repo{},
			ntpServer:   "",
			checkFunc: func(t *testing.T, cfg *confv1beta1.BKEConfig) {
				assert.Nil(t, cfg.CustomExtra)
			},
		},
		{
			name:        "with custom extra",
			customExtra: map[string]string{"key": "value"},
			imageRepo:   confv1beta1.Repo{},
			yumRepo:     confv1beta1.Repo{},
			chartRepo:   confv1beta1.Repo{},
			ntpServer:   "",
			checkFunc: func(t *testing.T, cfg *confv1beta1.BKEConfig) {
				assert.NotNil(t, cfg.CustomExtra)
				assert.Equal(t, "value", cfg.CustomExtra["key"])
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cfg := &confv1beta1.BKEConfig{}
			opts := &Options{}

			opts.applyCustomConfig(cfg, tt.customExtra, tt.imageRepo, tt.yumRepo, tt.chartRepo, tt.ntpServer)

			tt.checkFunc(t, cfg)
		})
	}
}

func TestOptionsOptimizeKubeClient(t *testing.T) {
	opts := &Options{}
	cfg := &confv1beta1.BKEConfig{}

	opts.optimizeKubeClient(cfg)

	assert.NotNil(t, cfg.Cluster.APIServer)
	assert.NotNil(t, cfg.Cluster.ControllerManager)
	assert.NotNil(t, cfg.Cluster.Scheduler)
	assert.NotNil(t, cfg.Cluster.Kubelet)

	assert.Contains(t, cfg.Cluster.APIServer.ExtraArgs, "max-mutating-requests-inflight")
	assert.Contains(t, cfg.Cluster.ControllerManager.ExtraArgs, "kube-api-qps")
	assert.Contains(t, cfg.Cluster.Scheduler.ExtraArgs, "kube-api-qps")
	assert.Contains(t, cfg.Cluster.Kubelet.ExtraArgs, "kube-api-qps")
}

func TestUpdateCorednsAntiAffinity(t *testing.T) {
	tests := []struct {
		name                 string
		nodeCount            int
		expectedAntiAffinity string
	}{
		{
			name:                 "single node",
			nodeCount:            1,
			expectedAntiAffinity: "false",
		},
		{
			name:                 "multiple nodes",
			nodeCount:            2,
			expectedAntiAffinity: "true",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cfg := &confv1beta1.BKEConfig{
				Addons: []confv1beta1.Product{
					{
						Name:    "coredns",
						Version: "v1.10.1",
						Param:   nil,
					},
				},
			}

			updateCorednsAntiAffinityByCount(cfg, tt.nodeCount)

			assert.Equal(t, tt.expectedAntiAffinity, cfg.Addons[testNumericZero].Param["EnableAntiAffinity"])
		})
	}
}

func TestUpdateCorednsAntiAffinityWithoutCoredns(t *testing.T) {
	cfg := &confv1beta1.BKEConfig{
		Addons: []confv1beta1.Product{
			{
				Name:    "other-addon",
				Version: "v1.0.0",
			},
		},
	}

	updateCorednsAntiAffinityByCount(cfg, 2)

	assert.Nil(t, cfg.Addons[testNumericZero].Param)
}

func TestOptionsApplyProductSpecificConfig(t *testing.T) {
	tests := []struct {
		name        string
		product     string
		expectPanic bool
		checkFunc   func(*testing.T, *confv1beta1.BKEConfig)
	}{
		{
			name:    "fuyao-portal product",
			product: "fuyao-portal",
			checkFunc: func(t *testing.T, cfg *confv1beta1.BKEConfig) {
				assert.GreaterOrEqual(t, len(cfg.Addons), testNumericTwo)
			},
		},
		{
			name:    "fuyao-business product",
			product: "fuyao-business",
			checkFunc: func(t *testing.T, cfg *confv1beta1.BKEConfig) {
				assert.NotNil(t, cfg)
			},
		},
		{
			name:    "fuyao-allinone product",
			product: "fuyao-allinone",
			checkFunc: func(t *testing.T, cfg *confv1beta1.BKEConfig) {
				assert.GreaterOrEqual(t, len(cfg.Addons), testNumericTwo)
			},
		},
		{
			name:    "unsupported product",
			product: "unknown-product",
			checkFunc: func(t *testing.T, cfg *confv1beta1.BKEConfig) {
				assert.NotNil(t, cfg)
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			patches := gomonkey.NewPatches()
			defer patches.Reset()
			patches.ApplyFunc(log.SteppedInfo, func(stepName string, args ...any) {})

			cfg := &confv1beta1.BKEConfig{}
			opts := &Options{Product: tt.product}

			opts.applyProductSpecificConfig(cfg, "sandbox-image", "false")

			tt.checkFunc(t, cfg)
		})
	}
}

func TestOptionsSetBaseAddons(t *testing.T) {
	opts := &Options{}
	cfg := &confv1beta1.BKEConfig{}

	opts.setBaseAddons(cfg)

	assert.NotEmpty(t, cfg.Addons)
	assert.Equal(t, testNumericFour, len(cfg.Addons))

	var addonNames []string
	for _, addon := range cfg.Addons {
		addonNames = append(addonNames, addon.Name)
	}
	assert.Contains(t, addonNames, "kubeproxy")
	assert.Contains(t, addonNames, "calico")
	assert.Contains(t, addonNames, "coredns")
	assert.Contains(t, addonNames, "bkeagent-deployer")
}

func TestOptionsCreateClusterAPIAddon(t *testing.T) {
	opts := &Options{}
	addon := opts.createClusterAPIAddon("test-sandbox", "false")

	assert.Equal(t, "cluster-api", addon.Name)
	assert.Equal(t, "v1.4.3", addon.Version)
	assert.True(t, addon.Block)
	assert.Equal(t, "false", addon.Param["offline"])
	assert.Equal(t, "test-sandbox", addon.Param["sandbox"])
	assert.Equal(t, configinit.DefaultNTPServer, addon.Param["ntpServer"])
	assert.Equal(t, utils.DefaultAgentHealthPort, addon.Param["healthPort"])

	opts.NtpServer = "ntp.custom:123"
	opts.AgentHealthPort = "9090"
	addon = opts.createClusterAPIAddon("test-sandbox", "false")
	assert.Equal(t, "ntp.custom:123", addon.Param["ntpServer"])
	assert.Equal(t, "9090", addon.Param["healthPort"])
}

func TestOptionsCreateSystemControllerAddon(t *testing.T) {
	opts := &Options{}
	addon := opts.createSystemControllerAddon()

	assert.Equal(t, "openfuyao-system-controller", addon.Name)
	assert.Equal(t, "latest", addon.Version)
	assert.Contains(t, addon.Param["helmRepo"], "helm.openfuyao.cn")
}

func TestOptionsLogUnsupportedProduct(t *testing.T) {
	opts := &Options{Product: "unknown-product"}

	var capturedMsg string
	patches := gomonkey.NewPatches()
	defer patches.Reset()
	patches.ApplyFunc(log.Warnf, func(template string, args ...any) {
		capturedMsg = fmt.Sprintf(template, args...)
	})

	opts.logUnsupportedProduct()

	assert.Contains(t, capturedMsg, "unknown-product")
}

func TestOptionsEncryptDecryptString(t *testing.T) {
	tests := []struct {
		name      string
		args      []string
		isEncrypt bool
	}{
		{
			name:      "encrypt single string",
			args:      []string{"password123"},
			isEncrypt: true,
		},
		{
			name:      "decrypt single string",
			args:      []string{"encrypted-password"},
			isEncrypt: false,
		},
		{
			name:      "encrypt multiple strings",
			args:      []string{"pass1", "pass2", "pass3"},
			isEncrypt: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			patches := gomonkey.NewPatches()
			defer patches.Reset()
			patches.ApplyFunc(security.AesEncrypt, func(s string) (string, error) {
				return "encrypted-" + s, nil
			})

			patches.ApplyFunc(security.AesDecrypt, func(s string) (string, error) {
				if strings.HasPrefix(s, "encrypted-") {
					return strings.TrimPrefix(s, "encrypted-"), nil
				}
				return s, nil
			})

			opts := &Options{Args: tt.args}

			if tt.isEncrypt {
				err := opts.EncryptString()
				assert.NoError(t, err)
			} else {
				err := opts.DecryptString()
				assert.NoError(t, err)
			}
		})
	}
}

func TestOptionsEncryptDecryptStringWithError(t *testing.T) {
	patches := gomonkey.NewPatches()
	defer patches.Reset()
	patches.ApplyFunc(security.AesEncrypt, func(s string) (string, error) {
		return "", os.ErrPermission
	})

	opts := &Options{Args: []string{"test"}}
	err := opts.EncryptString()

	assert.Error(t, err)
}

func TestOptionsDecryptStringWithError(t *testing.T) {
	patches := gomonkey.NewPatches()
	defer patches.Reset()
	patches.ApplyFunc(security.AesDecrypt, func(s string) (string, error) {
		return "", os.ErrPermission
	})

	opts := &Options{Args: []string{"encrypted-test"}}
	err := opts.DecryptString()

	assert.Error(t, err)
}

func TestOptionsLoadClusterConfig(t *testing.T) {
	tests := []struct {
		name         string
		fileContent  string
		mockReadFile func(string) ([]byte, error)
		expectError  bool
	}{
		{
			name:        "valid yaml content",
			fileContent: "spec:\n  clusterConfig:\n    nodes: []",
			mockReadFile: func(filename string) ([]byte, error) {
				return []byte("spec:\n  clusterConfig:\n    nodes: []"), nil
			},
			expectError: true,
		},
		{
			name:        "file read error",
			fileContent: "",
			mockReadFile: func(filename string) ([]byte, error) {
				return nil, os.ErrNotExist
			},
			expectError: true,
		},
		{
			name:        "invalid yaml",
			fileContent: "invalid: yaml: content: [",
			mockReadFile: func(filename string) ([]byte, error) {
				return []byte("invalid: yaml: content: ["), nil
			},
			expectError: true,
		},
		{
			name:        "empty cluster config",
			fileContent: "spec: {}",
			mockReadFile: func(filename string) ([]byte, error) {
				return []byte("spec: {}"), nil
			},
			expectError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			patches := gomonkey.NewPatches()
			defer patches.Reset()
			patches.ApplyFunc(os.ReadFile, tt.mockReadFile)

			patches.ApplyFunc(yaml2.Unmarshal, func(data []byte, v interface{}) error {
				if strings.Contains(string(data), "invalid") {
					return os.ErrInvalid
				}
				return nil
			})

			opts := &Options{File: "/test/config.yaml"}
			_, err := opts.loadClusterConfig()

			if tt.expectError {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
			}
		})
	}
}

func TestOptionsEncryptBKENodePassword(t *testing.T) {
	tests := []struct {
		name             string
		password         string
		mockDecryptError bool
		expectedPassword string
	}{
		{
			name:             "already encrypted",
			password:         "enc-pass",
			mockDecryptError: false,
			expectedPassword: "enc-pass",
		},
		{
			name:             "needs encryption",
			password:         "plain-pass",
			mockDecryptError: true,
			expectedPassword: "enc-plain-pass",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if tt.mockDecryptError {
				patches := gomonkey.NewPatches()
				defer patches.Reset()
				patches.ApplyFunc(security.AesDecrypt, func(s string) (string, error) {
					return "", os.ErrInvalid
				})
			} else {
				patches := gomonkey.NewPatches()
				defer patches.Reset()
				patches.ApplyFunc(security.AesDecrypt, func(s string) (string, error) {
					return strings.TrimPrefix(s, "enc-"), nil
				})
			}

			patches := gomonkey.NewPatches()

			defer patches.Reset()

			patches.ApplyFunc(security.AesEncrypt, func(s string) (string, error) {
				return "enc-" + s, nil
			})

			node := confv1beta1.BKENode{}
			node.Spec.Password = tt.password
			node.Spec.Hostname = "test-node"
			opts := &Options{}

			result := opts.encryptBKENodePassword(node)

			assert.Equal(t, tt.expectedPassword, result.Spec.Password)
		})
	}
}

func TestOptionsDecryptBKENodePassword(t *testing.T) {
	tests := []struct {
		name             string
		password         string
		mockDecryptError bool
		expectedPassword string
	}{
		{
			name:             "valid encrypted password",
			password:         "enc-pass",
			mockDecryptError: false,
			expectedPassword: "dec-enc-pass",
		},
		{
			name:             "decrypt fails",
			password:         "invalid-enc",
			mockDecryptError: true,
			expectedPassword: "invalid-enc",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if !tt.mockDecryptError {
				patches := gomonkey.NewPatches()
				defer patches.Reset()
				patches.ApplyFunc(security.AesDecrypt, func(s string) (string, error) {
					return "dec-" + s, nil
				})
			} else {
				patches := gomonkey.NewPatches()
				defer patches.Reset()
				patches.ApplyFunc(security.AesDecrypt, func(s string) (string, error) {
					return "", os.ErrInvalid
				})
			}

			node := confv1beta1.BKENode{}
			node.Spec.Password = tt.password
			node.Spec.Hostname = "test-node"
			opts := &Options{}

			result := opts.decryptBKENodePassword(node)

			assert.Equal(t, tt.expectedPassword, result.Spec.Password)
		})
	}
}

func TestOptionsSaveProcessedConfig(t *testing.T) {
	tests := []struct {
		name          string
		isEncrypt     bool
		mockMarshal   func(interface{}) ([]byte, error)
		mockGetwd     func() (string, error)
		mockWriteFile func(string, []byte, os.FileMode) error
		expectError   bool
	}{
		{
			name:      "encrypt success",
			isEncrypt: true,
			mockMarshal: func(v interface{}) ([]byte, error) {
				return []byte("yaml content"), nil
			},
			mockGetwd: func() (string, error) {
				return "/tmp", nil
			},
			mockWriteFile: func(filename string, data []byte, perm os.FileMode) error {
				return nil
			},
			expectError: false,
		},
		{
			name:      "decrypt success",
			isEncrypt: false,
			mockMarshal: func(v interface{}) ([]byte, error) {
				return []byte("yaml content"), nil
			},
			mockGetwd: func() (string, error) {
				return "/tmp", nil
			},
			mockWriteFile: func(filename string, data []byte, perm os.FileMode) error {
				return nil
			},
			expectError: false,
		},
		{
			name:      "marshal fails",
			isEncrypt: true,
			mockMarshal: func(v interface{}) ([]byte, error) {
				return nil, os.ErrInvalid
			},
			expectError: true,
		},
		{
			name:      "write file fails",
			isEncrypt: true,
			mockMarshal: func(v interface{}) ([]byte, error) {
				return []byte("yaml content"), nil
			},
			mockGetwd: func() (string, error) {
				return "/tmp", nil
			},
			mockWriteFile: func(filename string, data []byte, perm os.FileMode) error {
				return os.ErrPermission
			},
			expectError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			patches := gomonkey.NewPatches()
			defer patches.Reset()
			patches.ApplyFunc(yaml2.Marshal, tt.mockMarshal)

			if tt.mockGetwd != nil {
				patches.ApplyFunc(os.Getwd, tt.mockGetwd)
			}

			if tt.mockWriteFile != nil {
				patches.ApplyFunc(os.WriteFile, tt.mockWriteFile)
			}

			patches.ApplyFunc(log.SteppedInfo, func(stepName string, args ...any) {})

			conf := &configv1beta1.BKECluster{}
			conf.Name = "test-cluster"
			opts := &Options{}

			err := opts.saveProcessedConfig(conf, tt.isEncrypt)

			if tt.expectError {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
			}
		})
	}
}