package config

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

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/tiinf/tiinf"
)

const minimalRUComponentsYAML = `
      pd:
        "2c4g":
          min: 0
          max: 0
      pd_scheduling:
        "2c4g":
          min: 0
          max: 0
      pd_tso:
        "2c4g":
          min: 0
          max: 0
      tikv:
        "4c16g":
          min: 0
          max: 0
      tikv_worker:
        "2c4g":
          min: 0
          max: 0
      coprocessor_worker:
        "2c4g":
          min: 0
          max: 0
      tidb:
        "2c4g":
          min: 0
          max: 0
      tidb_worker:
        "2c4g":
          min: 0
          max: 0
      tiflash_compute:
        "4c16g":
          min: 0
          max: 0
      tiflash_write:
        "4c16g":
          min: 0
          max: 0
      tiproxy:
        "2c4g":
          min: 0
          max: 0
`

func TestInitConfigFromFile(t *testing.T) {
	// 创建临时目录
	tmpDir := t.TempDir()

	t.Run("valid config file", func(t *testing.T) {
		// 创建有效的配置文件
		configContent := `
tiinf:
  log:
    level: "info"
  trace:
    enable: true
server:
  cloudProvider: "aws"
  region: "us-west-2"
  templatePath: "/templates"
  image:
    registry: "my-registry.com"
    repoPrefix: "tidbcloud"
    imagePullPolicy: "Always"
    slowLogImage: "slowlog:latest"
    auditLogImage: "audit:latest"
    tikvPreStopImage: "tikv-prestop:latest"
    clusterAgentImage: "cluster-agent:latest"
    dataCleanerImage: "data-cleaner:latest"
    tikvNgImageTag: "v1.0.0"
    tidbNgImageTag: "v1.0.0"
    tiflashNgImageTag: "v1.0.0"
    pdNgImageTag: "v1.0.0"
    tiproxyNgImageTag: "v1.0.0"
  biz:
    defaultDataplaneAccount: "test-account"
    o11yBizType: "test"
    storageSizePolicy:
      tikv:
        data:
          defaultSizeGi: 500
          minSizeGi: 100
          maxSizeGi: 4000
        raftlog:
          defaultSizeGi: 50
          minSizeGi: 20
          maxSizeGi: 500
      tiflashCompute:
        data:
          defaultSizeGi: 500
          minSizeGi: 100
          maxSizeGi: 4000
    o11yAgentNamespace: "cp9-premium"
    enableHpa: true
    enableAutoGenNodeSpec: true
    kongDataplanePlugins: "plugin1,plugin2"
    kongControlplanePlugins: "plugin3,plugin4"
    placeholderReplicas: 3
    pcaArn: "arn:aws:acm-pca:us-west-2:123456789:certificate-authority/test"
    certificateDuration:
      tidbServer: 144h
      tiProxyServer: 120h
      renewBefore: 24h
    defaultTiBRConfig:
      scheduleType: "per-minute"
      scheduleAt: "0"
      cpu: "4"
      memory: "16Gi"
      storage_size: "200Gi"
      storage_class: "gp3-3000-125"
      min_t2_data_age_days: 1
    dataCleanerCron: "0 2 * * *"
    defaultTiKVSize: 2000
    defaultTiFlashSize: 1000
  mysql:
    host: "localhost"
    port: 3306
    user: "root"
    password: "password"
    database: "test"
  executorGrpcAddr: "localhost:9090"
  dataPlaneManagerAddr: "localhost:8080"
  cronJob:
    enableCleanSharedPool: true
    cleanSharedPoolCron: "0 */12 * * *"
    enableDMWorkerAutoscaler: true
    dmWorkerAutoscalerCron: "*/15 * * * * *"
    dmWorkerScaleDownStability: 10m
  ruConfig:
    components:
      pd:
        "2c4g":
          min: 0
          max: 1000
        "4c8g":
          min: 1000
          max: 0
      pd_scheduling:
        "2c4g":
          min: 0
          max: 0
      pd_tso:
        "2c4g":
          min: 0
          max: 0
      tikv:
        "4c16g":
          min: 0
          max: 5000
        "8c32g":
          min: 5000
          max: 0
      tikv_worker:
        "2c4g":
          min: 0
          max: 0
      coprocessor_worker:
        "2c4g":
          min: 0
          max: 0
      tidb:
        "2c4g":
          min: 0
          max: 1000
        "4c8g":
          min: 1000
          max: 0
      tidb_worker:
        "2c4g":
          min: 0
          max: 0
      tiflash_compute:
        "4c16g":
          min: 0
          max: 0
      tiflash_write:
        "4c16g":
          min: 0
          max: 0
      tiproxy:
        "2c4g":
          min: 0
          max: 0
`

		configFile := filepath.Join(tmpDir, "config.yaml")
		err := os.WriteFile(configFile, []byte(configContent), 0o600)
		require.NoError(t, err)

		cfg, err := InitConfigFromFile(configFile)
		require.NoError(t, err)
		assert.NotNil(t, cfg)
		assert.NotNil(t, cfg.TiInfConfig)
		assert.NotNil(t, cfg.ServerConfig)

		// 验证 ServerConfig
		assert.Equal(t, "aws", cfg.ServerConfig.CloudProvider)
		assert.Equal(t, "us-west-2", cfg.ServerConfig.Region)
		assert.Equal(t, "/templates", cfg.ServerConfig.TemplatePath)

		// 验证 ImageConfig
		assert.Equal(t, "my-registry.com", cfg.ServerConfig.ImageConfig.Registry)
		assert.Equal(t, "tidbcloud", cfg.ServerConfig.ImageConfig.RepoPrefix)
		assert.Equal(t, "Always", cfg.ServerConfig.ImageConfig.ImagePullPolicy)

		// 验证 BizConfig
		assert.Equal(t, "test-account", cfg.ServerConfig.BizConfig.DefaultDataplaneAccount)
		assert.Equal(t, "cp9-premium", cfg.ServerConfig.BizConfig.O11yAgentNamespace)
		assert.True(t, cfg.ServerConfig.BizConfig.EnableHpa)
		assert.Equal(t, AutoscalePolicyModeDisabled, cfg.ServerConfig.BizConfig.AutoscalePolicy.EffectiveMode())
		assert.False(t, cfg.ServerConfig.BizConfig.AutoscalePolicy.IsEnforced())
		assert.Empty(t, cfg.ServerConfig.BizConfig.DefaultTiBRConfig.CopyBackupMaxThroughput)
		assert.Equal(t, uint32(1), cfg.ServerConfig.BizConfig.DefaultTiBRConfig.MinT2DataAgeDays)
		assert.Equal(t, "cloud-ssd", cfg.ServerConfig.BizConfig.ClusterAgentAudit.StorageClass)
		assert.Equal(t, "1Gi", cfg.ServerConfig.BizConfig.ClusterAgentAudit.StorageSize)
		assert.Equal(t, 144*time.Hour, cfg.ServerConfig.BizConfig.CertificateDuration.TiDBServer)
		assert.Equal(t, 120*time.Hour, cfg.ServerConfig.BizConfig.CertificateDuration.TiProxyServer)
		assert.Equal(t, 24*time.Hour, cfg.ServerConfig.BizConfig.CertificateDuration.RenewBefore)
		assert.Equal(t, DefaultStorageSizePolicy(), cfg.ServerConfig.BizConfig.StorageSizePolicy)

		// 验证 CronJobConfig
		assert.True(t, cfg.ServerConfig.CronJob.EnableCleanSharedPool)
		assert.Equal(t, "0 */12 * * *", cfg.ServerConfig.CronJob.CleanSharedPoolCron)
		assert.True(t, cfg.ServerConfig.CronJob.EnableDMWorkerAutoscaler)
		assert.Equal(t, "*/15 * * * * *", cfg.ServerConfig.CronJob.DMWorkerAutoscalerCron)
		assert.Equal(t, 10*time.Minute, cfg.ServerConfig.CronJob.DMWorkerScaleDownStability)

		// 验证 MysqlConfig
		assert.Equal(t, "localhost", cfg.ServerConfig.MysqlConfig.Host)
		assert.Equal(t, uint16(3306), cfg.ServerConfig.MysqlConfig.Port)
		assert.Equal(t, "test", cfg.ServerConfig.MysqlConfig.Database)

		// 验证 RUConfig
		assert.NotNil(t, cfg.ServerConfig.RUConfig)
	})

	t.Run("file not found", func(t *testing.T) {
		cfg, err := InitConfigFromFile("/nonexistent/config.yaml")
		require.Error(t, err)
		assert.Nil(t, cfg)
		assert.Contains(t, err.Error(), "failed to open config file")
	})

	t.Run("invalid yaml", func(t *testing.T) {
		invalidConfigContent := `
invalid: yaml: content:
  - missing
    - quotes
`
		configFile := filepath.Join(tmpDir, "invalid_config.yaml")
		err := os.WriteFile(configFile, []byte(invalidConfigContent), 0o600)
		require.NoError(t, err)

		cfg, err := InitConfigFromFile(configFile)
		require.Error(t, err)
		assert.Nil(t, cfg)
		assert.Contains(t, err.Error(), "failed to unmarshal yaml config file")
	})

	t.Run("missing RU config", func(t *testing.T) {
		configContent := `
tiinf:
  log:
    level: "info"
server:
  cloudProvider: "aws"
  region: "us-west-2"
`
		configFile := filepath.Join(tmpDir, "no_ru_config.yaml")
		err := os.WriteFile(configFile, []byte(configContent), 0o600)
		require.NoError(t, err)

		cfg, err := InitConfigFromFile(configFile)
		require.Error(t, err)
		assert.Nil(t, cfg)
		assert.Contains(t, err.Error(), "missing RU config")
	})

	t.Run("invalid RU config", func(t *testing.T) {
		configContent := `
tiinf:
  log:
    level: "info"
server:
  cloudProvider: "aws"
  region: "us-west-2"
  ruConfig:
    components:
      pd:
        "2c4g":
          min: 1000
          max: 500  # max < min, invalid
`
		configFile := filepath.Join(tmpDir, "invalid_ru_config.yaml")
		err := os.WriteFile(configFile, []byte(configContent), 0o600)
		require.NoError(t, err)

		cfg, err := InitConfigFromFile(configFile)
		require.Error(t, err)
		assert.Nil(t, cfg)
		assert.Contains(t, err.Error(), "invalid RU config")
	})

	t.Run("RDS environment variables", func(t *testing.T) {
		// 设置环境变量
		originalUsername := os.Getenv("RDS_USERNAME")
		originalPassword := os.Getenv("RDS_PASSWORD")
		defer func() {
			if originalUsername != "" {
				_ = os.Setenv("RDS_USERNAME", originalUsername)
			} else {
				_ = os.Unsetenv("RDS_USERNAME")
			}
			if originalPassword != "" {
				_ = os.Setenv("RDS_PASSWORD", originalPassword)
			} else {
				_ = os.Unsetenv("RDS_PASSWORD")
			}
		}()

		t.Setenv("RDS_USERNAME", "env_user")
		t.Setenv("RDS_PASSWORD", "env_password")

		configContent := `
tiinf:
  log:
    level: "info"
server:
  cloudProvider: "aws"
  mysql:
    host: "localhost"
    user: "config_user"
    password: "config_password"
  ruConfig:
    components:
      pd:
        "2c4g":
          min: 0
          max: 0
      pd_scheduling:
        "2c4g":
          min: 0
          max: 0
      pd_tso:
        "2c4g":
          min: 0
          max: 0
      tikv:
        "4c16g":
          min: 0
          max: 0
      tikv_worker:
        "2c4g":
          min: 0
          max: 0
      coprocessor_worker:
        "2c4g":
          min: 0
          max: 0
      tidb:
        "2c4g":
          min: 0
          max: 0
      tidb_worker:
        "2c4g":
          min: 0
          max: 0
      tiflash_compute:
        "4c16g":
          min: 0
          max: 0
      tiflash_write:
        "4c16g":
          min: 0
          max: 0
      tiproxy:
        "2c4g":
          min: 0
          max: 0
`
		configFile := filepath.Join(tmpDir, "env_config.yaml")
		err := os.WriteFile(configFile, []byte(configContent), 0o600)
		require.NoError(t, err)

		cfg, err := InitConfigFromFile(configFile)
		require.NoError(t, err)

		assert.Equal(t, "env_user", cfg.ServerConfig.MysqlConfig.User)
		assert.Equal(t, "env_password", cfg.ServerConfig.MysqlConfig.Password)
	})

	t.Run("example config file", func(t *testing.T) {
		cfg, err := InitConfigFromFile("example.yaml")
		require.NoError(t, err)
		assert.NotNil(t, cfg)
		assert.NotNil(t, cfg.TiInfConfig)
		assert.NotNil(t, cfg.ServerConfig)

		assert.Equal(t, "aws", cfg.ServerConfig.CloudProvider)
		assert.Equal(t, "us-east-1", cfg.ServerConfig.Region)
		assert.Empty(t, cfg.ServerConfig.BizConfig.DefaultTiBRConfig.CopyBackupMaxThroughput)

		assert.NotNil(t, cfg.ServerConfig.RUConfig)
	})

	t.Run("RU config default values apply when field omitted", func(t *testing.T) {
		configContent := `
tiinf:
  log:
    level: "info"
server:
  cloudProvider: "aws"
  region: "us-west-2"
  ruConfig:
    components:
` + minimalRUComponentsYAML
		configFile := filepath.Join(tmpDir, "ru_defaults.yaml")
		err := os.WriteFile(configFile, []byte(configContent), 0o600)
		require.NoError(t, err)

		cfg, err := InitConfigFromFile(configFile)
		require.NoError(t, err)
		require.NotNil(t, cfg.ServerConfig.RUConfig)
		assert.False(t, cfg.ServerConfig.RUConfig.EnableScaleSharedPoolByRu)
		assert.False(t, cfg.ServerConfig.RUConfig.EnableScaleTiDBGroupByRu)
		assert.False(t, cfg.ServerConfig.RUConfig.EnableAutoscaleTikvCpuScaleOut)
		assert.False(t, cfg.ServerConfig.RUConfig.EnableAutoscaleDiskExpand)
		assert.False(t, cfg.ServerConfig.RUConfig.EnableAutoscaleIopsThroughputExpand)
		assert.False(t, cfg.ServerConfig.RUConfig.EnableAutoscaleTiproxyScaleIn)
	})

	t.Run("RU config defaultNodeCount preserves zero presence", func(t *testing.T) {
		configContent := `
tiinf:
  log:
    level: "info"
server:
  cloudProvider: "aws"
  region: "us-west-2"
  ruConfig:
    components:
      tiflash_compute:
        "4c16g":
          min: 0
          max: 0
          defaultNodeCount: 0
      tiproxy:
        "2c4g":
          min: 0
          max: 0
          defaultNodeCount: 1
`
		configFile := filepath.Join(tmpDir, "ru_default_node_count.yaml")
		err := os.WriteFile(configFile, []byte(configContent), 0o600)
		require.NoError(t, err)

		cfg, err := InitConfigFromFile(configFile)
		require.NoError(t, err)
		require.NotNil(t, cfg.ServerConfig.RUConfig)

		tiflashRange := cfg.ServerConfig.RUConfig.Components["tiflash_compute"]["4c16g"]
		require.NotNil(t, tiflashRange.DefaultNodeCount)
		assert.Equal(t, uint32(0), *tiflashRange.DefaultNodeCount)

		tiproxyRange := cfg.ServerConfig.RUConfig.Components["tiproxy"]["2c4g"]
		require.NotNil(t, tiproxyRange.DefaultNodeCount)
		assert.Equal(t, uint32(1), *tiproxyRange.DefaultNodeCount)
	})

	t.Run("scale operation defaults disabled", func(t *testing.T) {
		configContent := `
tiinf:
  log:
    level: "info"
server:
  cloudProvider: "aws"
  region: "us-west-2"
  ruConfig:
    components:
` + minimalRUComponentsYAML
		configFile := filepath.Join(tmpDir, "scale_operation_defaults.yaml")
		err := os.WriteFile(configFile, []byte(configContent), 0o600)
		require.NoError(t, err)

		cfg, err := InitConfigFromFile(configFile)
		require.NoError(t, err)
		assert.False(t, cfg.ServerConfig.ScaleOperation.Enabled)
		assert.Equal(t, 30*time.Second, cfg.ServerConfig.ScaleOperation.WorkerInterval)
		assert.Equal(t, 30*time.Minute, cfg.ServerConfig.ScaleOperation.StaleOperationTimeout)
	})

	t.Run("scale operation explicit config overrides defaults", func(t *testing.T) {
		configContent := `
tiinf:
  log:
    level: "info"
server:
  cloudProvider: "aws"
  region: "us-west-2"
  ruConfig:
    components:
` + minimalRUComponentsYAML + `
  scaleOperation:
    enabled: true
    workerInterval: 2m
    staleOperationTimeout: 45m
`
		configFile := filepath.Join(tmpDir, "scale_operation_explicit.yaml")
		err := os.WriteFile(configFile, []byte(configContent), 0o600)
		require.NoError(t, err)

		cfg, err := InitConfigFromFile(configFile)
		require.NoError(t, err)
		assert.True(t, cfg.ServerConfig.ScaleOperation.Enabled)
		assert.Equal(t, 2*time.Minute, cfg.ServerConfig.ScaleOperation.WorkerInterval)
		assert.Equal(t, 45*time.Minute, cfg.ServerConfig.ScaleOperation.StaleOperationTimeout)
	})

	t.Run("RU config explicit false overrides default", func(t *testing.T) {
		configContent := `
tiinf:
  log:
    level: "info"
server:
  cloudProvider: "aws"
  region: "us-west-2"
  ruConfig:
    enableScaleSharedPoolByRu: false
    enableScaleTiDBGroupByRu: false
    enableAutoscaleTikvCpuScaleOut: false
    enableAutoscaleDiskExpand: false
    enableAutoscaleIopsThroughputExpand: false
    enableAutoscaleTiproxyScaleIn: false
    components:
` + minimalRUComponentsYAML
		configFile := filepath.Join(tmpDir, "ru_explicit_false.yaml")
		err := os.WriteFile(configFile, []byte(configContent), 0o600)
		require.NoError(t, err)

		cfg, err := InitConfigFromFile(configFile)
		require.NoError(t, err)
		require.NotNil(t, cfg.ServerConfig.RUConfig)
		assert.False(t, cfg.ServerConfig.RUConfig.EnableScaleSharedPoolByRu)
		assert.False(t, cfg.ServerConfig.RUConfig.EnableScaleTiDBGroupByRu)
		assert.False(t, cfg.ServerConfig.RUConfig.EnableAutoscaleTikvCpuScaleOut)
		assert.False(t, cfg.ServerConfig.RUConfig.EnableAutoscaleDiskExpand)
		assert.False(t, cfg.ServerConfig.RUConfig.EnableAutoscaleIopsThroughputExpand)
		assert.False(t, cfg.ServerConfig.RUConfig.EnableAutoscaleTiproxyScaleIn)
	})

	t.Run("worker node metering explicit nested config", func(t *testing.T) {
		configContent := `
tiinf:
  log:
    level: "info"
server:
  cloudProvider: aws
  region: us-west-2
  templatePath: /tmp/templates
  executorGrpcAddr: 127.0.0.1:5001
  dataPlaneManagerAddr: 127.0.0.1:5002
  configCenterAddr: 127.0.0.1:5003
  mysql:
    host: 127.0.0.1
    port: 4000
    database: test
  ruConfig:
    components:
` + minimalRUComponentsYAML + `
  controlPlaneMetering:
    enable: false
    type: s3
    bucket: metering-bucket
    region: us-west-2
    workerNode:
      enable: true
      compensationWindow: 45m
`
		configFile := filepath.Join(tmpDir, "worker_node_explicit.yaml")
		err := os.WriteFile(configFile, []byte(configContent), 0o600)
		require.NoError(t, err)

		cfg, err := InitConfigFromFile(configFile)
		require.NoError(t, err)
		require.NotNil(t, cfg.ServerConfig.ControlPlaneMetering)
		require.False(t, cfg.ServerConfig.ControlPlaneMetering.Enable)
		require.NotNil(t, cfg.ServerConfig.ControlPlaneMetering.WorkerNode)
		require.True(t, cfg.ServerConfig.ControlPlaneMetering.WorkerNode.Enable)
		require.Equal(t, 45*time.Minute, cfg.ServerConfig.ControlPlaneMetering.WorkerNode.CompensationWindow)
		require.NotNil(t, cfg.ServerConfig.DataPlaneMetering)
	})

	t.Run("worker node metering requires parent metering storage config", func(t *testing.T) {
		configContent := `
tiinf:
  log:
    level: "info"
server:
  cloudProvider: aws
  region: us-west-2
  templatePath: /tmp/templates
  executorGrpcAddr: 127.0.0.1:5001
  dataPlaneManagerAddr: 127.0.0.1:5002
  configCenterAddr: 127.0.0.1:5003
  mysql:
    host: 127.0.0.1
    port: 4000
    database: test
  ruConfig:
    components:
` + minimalRUComponentsYAML + `
  controlPlaneMetering:
    workerNode:
      enable: true
`
		configFile := filepath.Join(tmpDir, "worker_node_missing_storage.yaml")
		err := os.WriteFile(configFile, []byte(configContent), 0o600)
		require.NoError(t, err)

		cfg, err := InitConfigFromFile(configFile)
		require.Error(t, err)
		require.Nil(t, cfg)
		require.Contains(t, err.Error(), "invalid control plane metering config for worker node metering")
	})

	t.Run("worker node metering requires S3 bucket when using S3 provider", func(t *testing.T) {
		configContent := `
tiinf:
  log:
    level: "info"
server:
  cloudProvider: aws
  region: us-west-2
  templatePath: /tmp/templates
  executorGrpcAddr: 127.0.0.1:5001
  dataPlaneManagerAddr: 127.0.0.1:5002
  configCenterAddr: 127.0.0.1:5003
  mysql:
    host: 127.0.0.1
    port: 4000
    database: test
  ruConfig:
    components:
` + minimalRUComponentsYAML + `
  controlPlaneMetering:
    enable: false
    type: s3
    region: us-west-2
    workerNode:
      enable: true
`
		configFile := filepath.Join(tmpDir, "worker_node_s3_missing_bucket.yaml")
		err := os.WriteFile(configFile, []byte(configContent), 0o600)
		require.NoError(t, err)

		cfg, err := InitConfigFromFile(configFile)
		require.Error(t, err)
		require.Nil(t, cfg)
		require.Contains(t, err.Error(), "bucket is required for S3 metering provider")
	})

	t.Run("worker node metering independent from controlPlaneMetering.enable", func(t *testing.T) {
		configContent := `
tiinf:
  log:
    level: "info"
server:
  cloudProvider: aws
  region: us-west-2
  templatePath: /tmp/templates
  executorGrpcAddr: 127.0.0.1:5001
  dataPlaneManagerAddr: 127.0.0.1:5002
  configCenterAddr: 127.0.0.1:5003
  mysql:
    host: 127.0.0.1
    port: 4000
    database: test
  ruConfig:
    components:
` + minimalRUComponentsYAML + `
  controlPlaneMetering:
    enable: false
    type: s3
    bucket: metering-bucket
    region: us-west-2
    workerNode:
      enable: true
      compensationWindow: 45m
`
		configFile := filepath.Join(tmpDir, "worker_node_independent.yaml")
		err := os.WriteFile(configFile, []byte(configContent), 0o600)
		require.NoError(t, err)

		cfg, err := InitConfigFromFile(configFile)
		require.NoError(t, err)
		require.False(t, cfg.ServerConfig.ControlPlaneMetering.Enable)
		require.True(t, cfg.ServerConfig.ControlPlaneMetering.WorkerNode.Enable)
	})

	t.Run("worker node metering inherits default compensation window when parent storage config is valid", func(t *testing.T) {
		configContent := `
tiinf:
  log:
    level: "info"
server:
  cloudProvider: aws
  region: us-west-2
  templatePath: /tmp/templates
  executorGrpcAddr: 127.0.0.1:5001
  dataPlaneManagerAddr: 127.0.0.1:5002
  configCenterAddr: 127.0.0.1:5003
  mysql:
    host: 127.0.0.1
    port: 4000
    database: test
  ruConfig:
    components:
` + minimalRUComponentsYAML + `
  controlPlaneMetering:
    enable: false
    type: localfs
    prefix: /tmp/metering
    workerNode:
      enable: true
`
		configFile := filepath.Join(tmpDir, "worker_node_default_with_storage.yaml")
		err := os.WriteFile(configFile, []byte(configContent), 0o600)
		require.NoError(t, err)

		cfg, err := InitConfigFromFile(configFile)
		require.NoError(t, err)
		require.NotNil(t, cfg.ServerConfig.ControlPlaneMetering)
		require.NotNil(t, cfg.ServerConfig.ControlPlaneMetering.WorkerNode)
		require.False(t, cfg.ServerConfig.ControlPlaneMetering.Enable)
		require.True(t, cfg.ServerConfig.ControlPlaneMetering.WorkerNode.Enable)
		require.Equal(t, 30*time.Minute, cfg.ServerConfig.ControlPlaneMetering.WorkerNode.CompensationWindow)
	})
}

func TestParseO11yClusterCIDR(t *testing.T) {
	tests := []struct {
		name       string
		value      string
		configured bool
		want       string
		wantErr    bool
	}{
		{name: "empty", value: "", configured: false},
		{name: "whitespace", value: "  ", configured: false},
		{name: "malformed", value: "not-a-cidr", wantErr: true},
		{name: "noncanonical", value: "10.0.1.1/16", wantErr: true},
		{name: "ipv6", value: "2001:db8::/32", wantErr: true},
		{name: "canonical", value: " 10.0.0.0/16 ", configured: true, want: "10.0.0.0/16"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			prefix, configured, err := ParseO11yClusterCIDR(tt.value)
			if tt.wantErr {
				require.Error(t, err)
				return
			}
			require.NoError(t, err)
			assert.Equal(t, tt.configured, configured)
			if configured {
				assert.Equal(t, tt.want, prefix.String())
			}
		})
	}
}

func TestStorageSizePolicyValidate(t *testing.T) {
	tests := []struct {
		name    string
		mutate  func(*StorageSizePolicy)
		wantErr string
	}{
		{name: "valid"},
		{
			name: "zero minimum",
			mutate: func(policy *StorageSizePolicy) {
				policy.TiKV.Data.MinSizeGi = 0
			},
			wantErr: "tikv.data: minSizeGi must be positive",
		},
		{
			name: "default below minimum",
			mutate: func(policy *StorageSizePolicy) {
				policy.TiKV.Raftlog.DefaultSizeGi = 19
			},
			wantErr: "tikv.raftlog: defaultSizeGi must be greater than or equal to minSizeGi",
		},
		{
			name: "default above maximum",
			mutate: func(policy *StorageSizePolicy) {
				policy.TiFlashCompute.Data.DefaultSizeGi = 4001
			},
			wantErr: "tiflashCompute.data: defaultSizeGi must be less than or equal to maxSizeGi",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			policy := DefaultStorageSizePolicy()
			if tt.mutate != nil {
				tt.mutate(&policy)
			}
			err := policy.Validate()
			if tt.wantErr == "" {
				require.NoError(t, err)
				return
			}
			require.EqualError(t, err, tt.wantErr)
		})
	}
}

func TestAutoscalePolicyConfig(t *testing.T) {
	tests := []struct {
		name       string
		config     AutoscalePolicyConfig
		wantMode   AutoscalePolicyMode
		wantActive bool
		wantError  string
	}{
		{
			name:     "zero value is disabled",
			wantMode: AutoscalePolicyModeDisabled,
		},
		{
			name:     "explicit disabled",
			config:   AutoscalePolicyConfig{Mode: AutoscalePolicyModeDisabled},
			wantMode: AutoscalePolicyModeDisabled,
		},
		{
			name: "explicit enforce",
			config: AutoscalePolicyConfig{
				Mode:            AutoscalePolicyModeEnforce,
				AllowAllTenants: true,
			},
			wantMode:   AutoscalePolicyModeEnforce,
			wantActive: true,
		},
		{
			name:      "enforce requires global acknowledgement",
			config:    AutoscalePolicyConfig{Mode: AutoscalePolicyModeEnforce},
			wantMode:  AutoscalePolicyModeEnforce,
			wantError: `mode "enforce" requires allowAllTenants=true`,
		},
		{
			name: "disabled rejects latent global acknowledgement",
			config: AutoscalePolicyConfig{
				Mode:            AutoscalePolicyModeDisabled,
				AllowAllTenants: true,
			},
			wantMode:  AutoscalePolicyModeDisabled,
			wantError: `allowAllTenants requires mode "enforce"`,
		},
		{
			name:      "unknown mode fails closed",
			config:    AutoscalePolicyConfig{Mode: "shadow"},
			wantMode:  "shadow",
			wantError: `mode must be "disabled" or "enforce"`,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			require.Equal(t, tt.wantMode, tt.config.EffectiveMode())
			require.Equal(t, tt.wantActive, tt.config.IsEnforced())
			err := tt.config.Validate()
			if tt.wantError == "" {
				require.NoError(t, err)
				return
			}
			require.EqualError(t, err, tt.wantError)
		})
	}
}

func TestInitConfigFromFileRejectsInvalidAutoscalePolicyMode(t *testing.T) {
	configContent := `
server:
  biz:
    autoscalePolicy:
      mode: shadow
  ruConfig:
    components:
` + minimalRUComponentsYAML

	configFile := filepath.Join(t.TempDir(), "invalid-autoscale-policy.yaml")
	require.NoError(t, os.WriteFile(configFile, []byte(configContent), 0o600))

	cfg, err := InitConfigFromFile(configFile)
	require.Error(t, err)
	assert.Nil(t, cfg)
	assert.Contains(t, err.Error(), "invalid autoscalePolicy config")
}

func TestInitConfigFromFileRejectsInvalidStorageSizePolicy(t *testing.T) {
	configContent := `
server:
  biz:
    storageSizePolicy:
      tikv:
        data:
          defaultSizeGi: 99
          minSizeGi: 100
          maxSizeGi: 4000
  ruConfig:
    components:
` + minimalRUComponentsYAML

	configFile := filepath.Join(t.TempDir(), "invalid-storage-policy.yaml")
	require.NoError(t, os.WriteFile(configFile, []byte(configContent), 0o600))

	cfg, err := InitConfigFromFile(configFile)
	require.Error(t, err)
	assert.Nil(t, cfg)
	assert.Contains(t, err.Error(), "invalid storageSizePolicy config")
	assert.Contains(t, err.Error(), "tikv.data")
}

func TestConfigStructTags(t *testing.T) {
	t.Run("yaml tags present", func(t *testing.T) {
		config := &Config{
			TiInfConfig: &tiinf.TiInfConfig{},
			ServerConfig: &ServerConfig{
				CloudProvider: "aws",
				Region:        "us-west-2",
				ImageConfig: ImageConfig{
					Registry:   "test.registry.com",
					RepoPrefix: "tidbcloud",
				},
				BizConfig: BizConfig{
					DefaultDataplaneAccount: "test-account",
				},
				CronJob: CronJobConfig{
					EnableCleanSharedPool: true,
					CleanSharedPoolCron:   "0 */6 * * *",
				},
			},
		}

		assert.NotNil(t, config)
		assert.NotNil(t, config.ServerConfig)
	})
}

func TestMeteringConfig_MetadataEnabled(t *testing.T) {
	t.Run("nil config disables metadata", func(t *testing.T) {
		var cfg *MeteringConfig
		require.False(t, cfg.MetadataEnabled())
	})

	t.Run("control plane metering enables metadata", func(t *testing.T) {
		cfg := &MeteringConfig{Enable: true}
		require.True(t, cfg.MetadataEnabled())
	})

	t.Run("worker node metering also enables metadata", func(t *testing.T) {
		cfg := &MeteringConfig{
			Enable: false,
			WorkerNode: &WorkerNodeMeteringConfig{
				Enable: true,
			},
		}
		require.True(t, cfg.MetadataEnabled())
	})

	t.Run("all switches disabled keeps metadata disabled", func(t *testing.T) {
		cfg := &MeteringConfig{
			Enable: false,
			WorkerNode: &WorkerNodeMeteringConfig{
				Enable: false,
			},
		}
		require.False(t, cfg.MetadataEnabled())
	})
}

func TestMeteringConfig_WorkerNodeEnabled(t *testing.T) {
	t.Run("nil config disables worker node metering", func(t *testing.T) {
		var cfg *MeteringConfig
		require.False(t, cfg.WorkerNodeEnabled())
	})

	t.Run("missing worker node config disables worker node metering", func(t *testing.T) {
		cfg := &MeteringConfig{Enable: true}
		require.False(t, cfg.WorkerNodeEnabled())
	})

	t.Run("worker node config enables worker node metering", func(t *testing.T) {
		cfg := &MeteringConfig{
			WorkerNode: &WorkerNodeMeteringConfig{
				Enable: true,
			},
		}
		require.True(t, cfg.WorkerNodeEnabled())
	})
}

func TestMeteringConfig_MetadataPlan(t *testing.T) {
	t.Run("nil config defaults to premium", func(t *testing.T) {
		var cfg *MeteringConfig
		require.Equal(t, "premium", cfg.MetadataPlan())
	})

	t.Run("empty plan defaults to premium", func(t *testing.T) {
		cfg := &MeteringConfig{}
		require.Equal(t, "premium", cfg.MetadataPlan())
	})

	t.Run("configured plan is preserved", func(t *testing.T) {
		cfg := &MeteringConfig{Plan: "byoc"}
		require.Equal(t, "byoc", cfg.MetadataPlan())
	})
}

func TestCertificateDurationConfigValidate(t *testing.T) {
	tests := []struct {
		name    string
		cfg     CertificateDurationConfig
		wantErr bool
	}{
		{
			name: "empty config keeps defaults",
		},
		{
			name: "valid overrides",
			cfg: CertificateDurationConfig{
				TiDBServer:    144 * time.Hour,
				TiProxyServer: 120 * time.Hour,
				RenewBefore:   24 * time.Hour,
			},
		},
		{
			name: "partial config is rejected",
			cfg: CertificateDurationConfig{
				TiDBServer: 144 * time.Hour,
			},
			wantErr: true,
		},
		{
			name: "renewBefore must fit tidb server duration",
			cfg: CertificateDurationConfig{
				TiDBServer:    24 * time.Hour,
				TiProxyServer: 120 * time.Hour,
				RenewBefore:   24 * time.Hour,
			},
			wantErr: true,
		},
		{
			name: "renewBefore must fit tiproxy server duration",
			cfg: CertificateDurationConfig{
				TiDBServer:    144 * time.Hour,
				TiProxyServer: 24 * time.Hour,
				RenewBefore:   24 * time.Hour,
			},
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := tt.cfg.Validate()
			if tt.wantErr {
				require.Error(t, err)
				return
			}
			require.NoError(t, err)
		})
	}
}
