package config

import (
	"fmt"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/dataflow-service-e2e/internal/misc"
	"github.com/tidbcloud/dataflow-service-e2e/internal/model"
	"gopkg.in/yaml.v2"
)

// ---- isValueEmpty ----

func TestIsValueEmpty(t *testing.T) {
	tests := map[string]struct {
		input interface{}
		want  bool
	}{
		"nil":              {input: nil, want: true},
		"empty string":     {input: "", want: true},
		"non-empty string": {input: "hello", want: false},
		"zero int":         {input: 0, want: true},
		"non-zero int":     {input: 42, want: false},
		"bool true":        {input: true, want: false},
		"bool false":       {input: false, want: false},
		"empty slice":      {input: []interface{}{}, want: true},
		"non-empty slice":  {input: []interface{}{"a"}, want: false},
		// map[string]interface{} cases
		"empty string map":        {input: map[string]interface{}{}, want: true},
		"string map all zeros":    {input: map[string]interface{}{"a": "", "b": 0}, want: true},
		"string map one non-zero": {input: map[string]interface{}{"a": "", "b": "x"}, want: false},
		// map[interface{}]interface{} cases (yaml.v2 nested maps)
		"empty iface map":        {input: map[interface{}]interface{}{}, want: true},
		"iface map all zeros":    {input: map[interface{}]interface{}{"a": "", "b": 0}, want: true},
		"iface map one non-zero": {input: map[interface{}]interface{}{"a": "", "b": "x"}, want: false},
		// nested maps: struct with all-empty leaf values
		"nested iface map all zeros": {
			input: map[interface{}]interface{}{
				"s3": map[interface{}]interface{}{
					"sourceURL": "",
					"roleARN":   "",
				},
				"gcs": map[interface{}]interface{}{
					"sourceURL": "",
				},
			},
			want: true,
		},
		"nested iface map with one non-zero leaf": {
			input: map[interface{}]interface{}{
				"s3": map[interface{}]interface{}{
					"sourceURL": "s3://bucket/",
					"roleARN":   "",
				},
			},
			want: false,
		},
	}

	for name, tc := range tests {
		t.Run(name, func(t *testing.T) {
			got := isValueEmpty(tc.input)
			assert.Equal(t, tc.want, got)
		})
	}
}

// ---- genericMapStruct2Config ----

func TestGenericMapStruct2Config(t *testing.T) {
	tests := map[string]struct {
		input    map[string]interface{}
		assertFn func(t *testing.T, cfg config)
	}{
		"plain string fields": {
			input: map[string]interface{}{
				"executor":     "test-executor",
				"orgID":        "org-1",
				"projectID":    "proj-1",
				"tidbCloudURL": "https://example.com",
			},
			assertFn: func(t *testing.T, cfg config) {
				assert.Equal(t, "test-executor", cfg.Executor)
				assert.Equal(t, "org-1", cfg.OrgID)
				assert.Equal(t, "proj-1", cfg.ProjectID)
				assert.Equal(t, "https://example.com", cfg.TiDBCloudURL)
			},
		},
		"importSampleData nested struct": {
			input: map[string]interface{}{
				"importSampleData": map[interface{}]interface{}{
					"s3": map[interface{}]interface{}{
						"sourceURL": "s3://bucket/path/",
						"roleARN":   "arn:aws:iam::123:role/r",
					},
					"gcs": map[interface{}]interface{}{
						"sourceURL": "gs://bucket/",
					},
					"azureBlob": map[interface{}]interface{}{
						"sourceURL": "https://x.blob.core.windows.net/c/",
						"sasToken":  "token",
					},
					"oss": map[interface{}]interface{}{
						"sourceURL":       "oss://bucket/path/",
						"endpoint":        "oss-ap-southeast-1.aliyuncs.com",
						"accessKeyID":     "ak",
						"accessKeySecret": "sk",
					},
				},
			},
			assertFn: func(t *testing.T, cfg config) {
				assert.Equal(t, "s3://bucket/path/", cfg.ImportSampleData.S3.SourceURL)
				assert.Equal(t, "arn:aws:iam::123:role/r", cfg.ImportSampleData.S3.RoleARN)
				assert.Equal(t, "gs://bucket/", cfg.ImportSampleData.GCS.SourceURL)
				assert.Equal(t, "https://x.blob.core.windows.net/c/", cfg.ImportSampleData.AzureBlob.SourceURL)
				assert.Equal(t, "token", cfg.ImportSampleData.AzureBlob.SASToken)
				assert.Equal(t, "oss://bucket/path/", cfg.ImportSampleData.OSS.SourceURL)
				assert.Equal(t, "oss-ap-southeast-1.aliyuncs.com", cfg.ImportSampleData.OSS.Endpoint)
				assert.Equal(t, "ak", cfg.ImportSampleData.OSS.AccessKeyID)
				assert.Equal(t, "sk", cfg.ImportSampleData.OSS.AccessKeySecret)
			},
		},
		"empty map gives zero config": {
			input: map[string]interface{}{},
			assertFn: func(t *testing.T, cfg config) {
				assert.Empty(t, cfg.Executor)
				assert.Empty(t, cfg.OrgID)
			},
		},
	}

	for name, tc := range tests {
		t.Run(name, func(t *testing.T) {
			cfg := genericMapStruct2Config(tc.input)
			tc.assertFn(t, cfg)
		})
	}
}

// ---- combineCommonAndEnvConfigs ----

func TestCombineCommonAndEnvConfigs(t *testing.T) {
	boolPtr := func(b bool) *bool { return &b }

	tests := map[string]struct {
		common   config
		env      config
		assertFn func(t *testing.T, combined config)
	}{
		"env overrides non-empty field": {
			common: config{Executor: "common-executor", OrgID: "org-common"},
			env:    config{Executor: "env-executor"},
			assertFn: func(t *testing.T, combined config) {
				assert.Equal(t, "env-executor", combined.Executor)
				// OrgID not set in env → falls back to common
				assert.Equal(t, "org-common", combined.OrgID)
			},
		},
		"env zero string falls back to common": {
			common: config{TiDBCloudURL: "https://common.example.com"},
			env:    config{TiDBCloudURL: ""},
			assertFn: func(t *testing.T, combined config) {
				assert.Equal(t, "https://common.example.com", combined.TiDBCloudURL)
			},
		},
		"serverless region falls back to common": {
			common: config{ServerlessRegion: "us-east-1"},
			env:    config{ServerlessRegion: ""},
			assertFn: func(t *testing.T, combined config) {
				assert.Equal(t, "us-east-1", combined.ServerlessRegion)
			},
		},
		"runAsCI bool from common used when env is nil": {
			common: config{RunAsCI: boolPtr(true)},
			env:    config{RunAsCI: nil},
			assertFn: func(t *testing.T, combined config) {
				require.NotNil(t, combined.RunAsCI)
				assert.True(t, *combined.RunAsCI)
			},
		},
		// Merge fields: env and common are appended together
		"tidbDedicatedClusterIds are merged": {
			common: config{TiDBDedicatedClusterIds: []string{"cluster-common"}},
			env:    config{TiDBDedicatedClusterIds: []string{"cluster-env"}},
			assertFn: func(t *testing.T, combined config) {
				assert.ElementsMatch(t, []string{"cluster-env", "cluster-common"}, combined.TiDBDedicatedClusterIds)
			},
		},
		"kafka entries are merged": {
			common: config{Kafka: []*model.KafkaEntity{{BootstrapEndpoints: "common:9092"}}},
			env:    config{Kafka: []*model.KafkaEntity{{BootstrapEndpoints: "env:9092"}}},
			assertFn: func(t *testing.T, combined config) {
				assert.Len(t, combined.Kafka, 2)
			},
		},
		"empty env tidbDedicatedClusterIds falls back to common only": {
			common: config{TiDBDedicatedClusterIds: []string{"cluster-common"}},
			env:    config{TiDBDedicatedClusterIds: nil},
			assertFn: func(t *testing.T, combined config) {
				assert.Equal(t, []string{"cluster-common"}, combined.TiDBDedicatedClusterIds)
			},
		},
		// The key regression: importSampleData from common must survive when env has no importSampleData
		"importSampleData from common used when env does not define it": {
			common: config{
				ImportSampleData: ImportSampleDataConfig{
					S3: struct {
						SourceURL string `yaml:"sourceURL"`
						RoleARN   string `yaml:"roleARN"`
					}{
						SourceURL: "s3://tidbcloud-sample-data/data-ingestion/",
						RoleARN:   "arn:aws:iam::801626783489:role/import-sample-access",
					},
					GCS: struct {
						SourceURL string `yaml:"sourceURL"`
					}{
						SourceURL: "gs://tidbcloud-samples-us-west1/",
					},
					AzureBlob: struct {
						SourceURL string `yaml:"sourceURL"`
						SASToken  string `yaml:"sasToken"`
					}{
						SourceURL: "https://tcidmsampledata.blob.core.windows.net/sql/",
						SASToken:  "sv=2015-04-05&token",
					},
					OSS: ImportSampleDataOSSConfig{
						SourceURL:       "oss://tidbcloud-sample-data/data-ingestion/",
						Endpoint:        "oss-ap-southeast-1.aliyuncs.com",
						AccessKeyID:     "ak-common",
						AccessKeySecret: "sk-common",
						ImportedDBTable: &ImportDBTableName{
							Database: "test",
							Table:    "a",
						},
						CustomCSVFormat: &ImportSampleDataCSVFormatConfig{
							Separator:      ";",
							Delimiter:      "\"",
							LineTerminator: "\r\n",
							EscapeChar:     "\\",
							SkipRows:       1,
							NullValue:      "\\N",
							SplitFile:      false,
						},
					},
				},
			},
			env: config{}, // env has no importSampleData
			assertFn: func(t *testing.T, combined config) {
				assert.Equal(t, "s3://tidbcloud-sample-data/data-ingestion/", combined.ImportSampleData.S3.SourceURL)
				assert.Equal(t, "arn:aws:iam::801626783489:role/import-sample-access", combined.ImportSampleData.S3.RoleARN)
				assert.Equal(t, "gs://tidbcloud-samples-us-west1/", combined.ImportSampleData.GCS.SourceURL)
				assert.Equal(t, "https://tcidmsampledata.blob.core.windows.net/sql/", combined.ImportSampleData.AzureBlob.SourceURL)
				assert.Equal(t, "sv=2015-04-05&token", combined.ImportSampleData.AzureBlob.SASToken)
				assert.Equal(t, "oss://tidbcloud-sample-data/data-ingestion/", combined.ImportSampleData.OSS.SourceURL)
				assert.Equal(t, "oss-ap-southeast-1.aliyuncs.com", combined.ImportSampleData.OSS.Endpoint)
				assert.Equal(t, "ak-common", combined.ImportSampleData.OSS.AccessKeyID)
				assert.Equal(t, "sk-common", combined.ImportSampleData.OSS.AccessKeySecret)
				assert.Equal(t, "test", combined.ImportSampleData.OSS.ImportedDBTable.Database)
				assert.Equal(t, "a", combined.ImportSampleData.OSS.ImportedDBTable.Table)
				require.NotNil(t, combined.ImportSampleData.OSS.CustomCSVFormat)
				assert.Equal(t, ";", combined.ImportSampleData.OSS.CustomCSVFormat.Separator)
				assert.Equal(t, "\\N", combined.ImportSampleData.OSS.CustomCSVFormat.NullValue)
			},
		},
		"importSampleData in env overrides common": {
			common: config{
				ImportSampleData: ImportSampleDataConfig{
					S3: struct {
						SourceURL string `yaml:"sourceURL"`
						RoleARN   string `yaml:"roleARN"`
					}{SourceURL: "s3://common/", RoleARN: "arn:common"},
					OSS: ImportSampleDataOSSConfig{
						SourceURL:       "oss://common/",
						Endpoint:        "oss-cn-hangzhou.aliyuncs.com",
						AccessKeyID:     "ak-common",
						AccessKeySecret: "sk-common",
						CustomCSVFormat: &ImportSampleDataCSVFormatConfig{
							Separator:      ",",
							Delimiter:      "\"",
							LineTerminator: "\n",
							EscapeChar:     "\\",
							SkipRows:       0,
							NullValue:      "\\N",
							SplitFile:      false,
						},
					},
				},
			},
			env: config{
				ImportSampleData: ImportSampleDataConfig{
					S3: struct {
						SourceURL string `yaml:"sourceURL"`
						RoleARN   string `yaml:"roleARN"`
					}{SourceURL: "s3://env/", RoleARN: "arn:env"},
					GCS: struct {
						SourceURL string `yaml:"sourceURL"`
					}{SourceURL: "gs://env/"},
					AzureBlob: struct {
						SourceURL string `yaml:"sourceURL"`
						SASToken  string `yaml:"sasToken"`
					}{SourceURL: "https://env.blob/", SASToken: "envtoken"},
					OSS: ImportSampleDataOSSConfig{
						SourceURL:       "oss://env/",
						Endpoint:        "oss-ap-southeast-1.aliyuncs.com",
						AccessKeyID:     "ak-env",
						AccessKeySecret: "sk-env",
						ImportedDBTable: &ImportDBTableName{
							Database: "test",
							Table:    "a",
						},
						CustomCSVFormat: &ImportSampleDataCSVFormatConfig{
							Separator:      "|",
							Delimiter:      "'",
							LineTerminator: "\r\n",
							EscapeChar:     "!",
							SkipRows:       2,
							NullValue:      "NULL",
							SplitFile:      true,
						},
					},
				},
			},
			assertFn: func(t *testing.T, combined config) {
				assert.Equal(t, "s3://env/", combined.ImportSampleData.S3.SourceURL)
				assert.Equal(t, "arn:env", combined.ImportSampleData.S3.RoleARN)
				assert.Equal(t, "gs://env/", combined.ImportSampleData.GCS.SourceURL)
				assert.Equal(t, "https://env.blob/", combined.ImportSampleData.AzureBlob.SourceURL)
				assert.Equal(t, "envtoken", combined.ImportSampleData.AzureBlob.SASToken)
				assert.Equal(t, "oss://env/", combined.ImportSampleData.OSS.SourceURL)
				assert.Equal(t, "oss-ap-southeast-1.aliyuncs.com", combined.ImportSampleData.OSS.Endpoint)
				assert.Equal(t, "ak-env", combined.ImportSampleData.OSS.AccessKeyID)
				assert.Equal(t, "sk-env", combined.ImportSampleData.OSS.AccessKeySecret)
				assert.Equal(t, "test", combined.ImportSampleData.OSS.ImportedDBTable.Database)
				assert.Equal(t, "a", combined.ImportSampleData.OSS.ImportedDBTable.Table)
				require.NotNil(t, combined.ImportSampleData.OSS.CustomCSVFormat)
				assert.Equal(t, "|", combined.ImportSampleData.OSS.CustomCSVFormat.Separator)
				assert.Equal(t, "'", combined.ImportSampleData.OSS.CustomCSVFormat.Delimiter)
				assert.Equal(t, "NULL", combined.ImportSampleData.OSS.CustomCSVFormat.NullValue)
				assert.True(t, combined.ImportSampleData.OSS.CustomCSVFormat.SplitFile)
			},
		},
		"importSampleData oss keeps common non-empty fields on partial env override": {
			common: config{
				ImportSampleData: ImportSampleDataConfig{
					OSS: ImportSampleDataOSSConfig{
						SourceURL:       "oss://common/",
						Endpoint:        "oss-cn-hangzhou.aliyuncs.com",
						AccessKeyID:     "ak-common",
						AccessKeySecret: "sk-common",
						ImportedDBTable: &ImportDBTableName{
							Database: "test",
							Table:    "a",
						},
						CustomCSVFormat: &ImportSampleDataCSVFormatConfig{
							Separator:      ";",
							Delimiter:      "\"",
							LineTerminator: "\r\n",
							EscapeChar:     "\\",
							SkipRows:       1,
							NullValue:      "\\N",
							SplitFile:      false,
						},
					},
				},
			},
			env: config{
				ImportSampleData: ImportSampleDataConfig{
					OSS: ImportSampleDataOSSConfig{
						AccessKeyID:     "ak-env",
						AccessKeySecret: "sk-env",
					},
				},
			},
			assertFn: func(t *testing.T, combined config) {
				assert.Equal(t, "oss://common/", combined.ImportSampleData.OSS.SourceURL)
				assert.Equal(t, "oss-cn-hangzhou.aliyuncs.com", combined.ImportSampleData.OSS.Endpoint)
				assert.Equal(t, "ak-env", combined.ImportSampleData.OSS.AccessKeyID)
				assert.Equal(t, "sk-env", combined.ImportSampleData.OSS.AccessKeySecret)
				assert.Equal(t, "test", combined.ImportSampleData.OSS.ImportedDBTable.Database)
				assert.Equal(t, "a", combined.ImportSampleData.OSS.ImportedDBTable.Table)
				require.NotNil(t, combined.ImportSampleData.OSS.CustomCSVFormat)
				assert.Equal(t, ";", combined.ImportSampleData.OSS.CustomCSVFormat.Separator)
				assert.EqualValues(t, 1, combined.ImportSampleData.OSS.CustomCSVFormat.SkipRows)
			},
		},
	}

	for name, tc := range tests {
		t.Run(name, func(t *testing.T) {
			combined := combineCommonAndEnvConfigs(tc.common, tc.env)
			tc.assertFn(t, combined)
		})
	}
}

func TestMaskRedactsAllCredentials(t *testing.T) {
	cfg := &config{
		TiDBCloudPassword: "cloud-password",
		TidbCloudToken:    "cloud-token",
		OpenApi: OpenApiConfig{
			PublicKey:  "public-key",
			PrivateKey: "private-key",
		},
		Kafka: []*model.KafkaEntity{{User: "kafka-user", Password: "kafka-password"}},
		MySQL: []*model.MySQLEntity{{User: "mysql-user", Password: "mysql-password"}},
		S3:    []*model.S3Entity{{AccessKey: "s3-access-key", SecretKey: "s3-secret-key"}},
		AzureBlobStorage: []*model.AzureBlobStorageEntity{{
			SASToken: "azure-sas-token",
		}},
		Pulsar: []*model.PulsarEntity{{
			User: "pulsar-user", Password: "pulsar-password", Token: "pulsar-token",
		}},
		SchemaRegistry: &SchemaRegistry{Username: "registry-user", Password: "registry-password"},
		ImportSampleData: ImportSampleDataConfig{
			AzureBlob: struct {
				SourceURL string `yaml:"sourceURL"`
				SASToken  string `yaml:"sasToken"`
			}{SASToken: "import-azure-sas-token"},
			OSS: ImportSampleDataOSSConfig{
				SourceURL:       "oss://bucket/path/",
				Endpoint:        "oss-ap-southeast-1.aliyuncs.com",
				AccessKeyID:     "ak",
				AccessKeySecret: "sk",
			},
		},
		BackupExport: BackupExportConfig{
			AWS:      BackupExportTargetConfig{TargetURI: "s3://bucket/path/", RoleARN: "arn:aws:iam::123:role/export", Endpoint: "https://s3.example"},
			AliCloud: BackupExportTargetConfig{TargetURI: "oss://bucket/path/", RoleARN: "not-used", Endpoint: "oss.example"},
		},
		EssentialExport: BackupExportConfig{
			AWS:      BackupExportTargetConfig{TargetURI: "s3://essential/path/", RoleARN: "arn:aws:iam::123:role/essential"},
			AliCloud: BackupExportTargetConfig{TargetURI: "oss://essential/path/", RoleARN: "not-used"},
		},
	}

	masked := mask(cfg)
	assert.Equal(t, "xxx", masked.TiDBCloudPassword)
	assert.Equal(t, "xxx", masked.TidbCloudToken)
	assert.Equal(t, "xxx", masked.OpenApi.PublicKey)
	assert.Equal(t, "xxx", masked.OpenApi.PrivateKey)
	assert.Equal(t, "xxx", masked.Kafka[0].User)
	assert.Equal(t, "xxx", masked.Kafka[0].Password)
	assert.Equal(t, "xxx", masked.MySQL[0].User)
	assert.Equal(t, "xxx", masked.MySQL[0].Password)
	assert.Equal(t, "xxx", masked.S3[0].AccessKey)
	assert.Equal(t, "xxx", masked.S3[0].SecretKey)
	assert.Equal(t, "xxx", masked.AzureBlobStorage[0].SASToken)
	assert.Equal(t, "xxx", masked.Pulsar[0].User)
	assert.Equal(t, "xxx", masked.Pulsar[0].Password)
	assert.Equal(t, "xxx", masked.Pulsar[0].Token)
	assert.Equal(t, "xxx", masked.SchemaRegistry.Username)
	assert.Equal(t, "xxx", masked.SchemaRegistry.Password)
	assert.Equal(t, "xxx", masked.ImportSampleData.AzureBlob.SASToken)
	assert.Equal(t, "xxx", masked.ImportSampleData.OSS.AccessKeyID)
	assert.Equal(t, "xxx", masked.ImportSampleData.OSS.AccessKeySecret)
	assert.Equal(t, "oss://bucket/path/", masked.ImportSampleData.OSS.SourceURL)
	assert.Equal(t, "oss-ap-southeast-1.aliyuncs.com", masked.ImportSampleData.OSS.Endpoint)
	assert.Equal(t, "xxx", masked.BackupExport.AWS.TargetURI)
	assert.Equal(t, "xxx", masked.BackupExport.AWS.RoleARN)
	assert.Equal(t, "xxx", masked.BackupExport.AliCloud.TargetURI)
	assert.Equal(t, "xxx", masked.BackupExport.AliCloud.RoleARN)
	assert.Equal(t, "xxx", masked.EssentialExport.AWS.TargetURI)
	assert.Equal(t, "xxx", masked.EssentialExport.AWS.RoleARN)
	assert.Equal(t, "xxx", masked.EssentialExport.AliCloud.TargetURI)
	assert.Equal(t, "xxx", masked.EssentialExport.AliCloud.RoleARN)
}

func TestConfigSummaryContainsOnlyOperationalSelection(t *testing.T) {
	cfg := &config{
		env:                     StagingENV,
		Executor:                "executor",
		Domain:                  DomainCDC,
		ApiMode:                 ApiModeOpenAPI,
		TiDBCloudPassword:       "cloud-password",
		TidbCloudToken:          "cloud-token",
		TiDBPremiumClusterIds:   []string{"premium-1"},
		TiDBDedicatedClusterIds: []string{"dedicated-1"},
		Cases2Run:               []string{"awsKafkaChangefeedSmoke"},
	}

	rendered := misc.PrettyYAML(configSummary(cfg))

	assert.Contains(t, rendered, "awsKafkaChangefeedSmoke")
	assert.Contains(t, rendered, "premiumClusterCount: 1")
	assert.NotContains(t, rendered, "cloud-password")
	assert.NotContains(t, rendered, "cloud-token")
	assert.NotContains(t, rendered, "premium-1")
	assert.NotContains(t, rendered, "dedicated-1")
}

func TestConfigValidate_ApiModeDefaultAndOverride(t *testing.T) {
	t.Run("default to consoleapi when empty", func(t *testing.T) {
		cfg := config{Domain: DomainCDC}

		cfg.Validate()

		assert.Equal(t, ApiModeConsoleAPI, cfg.ApiMode)
	})

	t.Run("keep openapi when configured", func(t *testing.T) {
		cfg := config{Domain: DomainCDC, ApiMode: ApiModeOpenAPI}

		cfg.Validate()

		assert.Equal(t, ApiModeOpenAPI, cfg.ApiMode)
	})
}

func TestConfigValidate_ApiModeInvalid(t *testing.T) {
	cfg := config{Domain: DomainCDC, ApiMode: "invalid"}

	require.PanicsWithValue(t, "invalid api mode 'invalid', must be one of: [consoleapi openapi]", func() {
		cfg.Validate()
	})
}

func TestSelectRegionsForSuitePlan(t *testing.T) {
	dedicatedRegions := cloudRegion{{
		CloudProvider: model.AWS,
		Regions:       []string{"us-west-2", "us-east-1"},
	}}
	premiumRegions := cloudRegion{{
		CloudProvider: model.AWS,
		Regions:       []string{"us-west-2"},
	}}

	t.Run("selects the requested plan from the matrix", func(t *testing.T) {
		cfg := config{
			Regions: dedicatedRegions,
			SuitePlanRegions: map[SuitePlan]cloudRegion{
				SuitePlanDedicated: dedicatedRegions,
				SuitePlanPremium:   premiumRegions,
			},
		}

		cfg.SelectRegionsForSuitePlan(SuitePlanPremium)

		assert.Equal(t, premiumRegions, cfg.Regions)
		assert.Equal(t, []string{"us-west-2"}, cfg.RegionsOf(model.AWS))
	})

	t.Run("rejects a plan missing from a configured matrix", func(t *testing.T) {
		cfg := config{SuitePlanRegions: map[SuitePlan]cloudRegion{
			SuitePlanDedicated: dedicatedRegions,
		}}

		require.PanicsWithValue(t, `regions not configured for suite plan "essential"`, func() {
			cfg.SelectRegionsForSuitePlan(SuitePlanEssential)
		})
	})
}

func TestConfigUnmarshalSuitePlanRegions(t *testing.T) {
	var cfg config
	require.NoError(t, yaml.Unmarshal([]byte(`
suitePlanRegions:
  premium:
    - cloudProvider: aws
      regions: [us-west-2]
`), &cfg))

	cfg.SelectRegionsForSuitePlan(SuitePlanPremium)

	assert.Equal(t, []string{"us-west-2"}, cfg.RegionsOf(model.AWS))
}

func TestConfigUnmarshalRejectsLegacyFlatRegions(t *testing.T) {
	var cfg config
	require.NoError(t, yaml.Unmarshal([]byte(`
regions:
  - cloudProvider: aws
    regions: [us-west-2]
`), &cfg))

	require.PanicsWithValue(t, `regions not configured for suite plan "dedicated"`, func() {
		cfg.SelectRegionsForSuitePlan(SuitePlanDedicated)
	})
}

func TestConfigUnmarshalRejectsLegacyPlanRegionKeys(t *testing.T) {
	for _, key := range []string{"servicePlanRegions", "clusterPlanRegions"} {
		t.Run(key, func(t *testing.T) {
			var cfg config
			require.NoError(t, yaml.Unmarshal([]byte(fmt.Sprintf(`
%s:
  dedicated:
    - cloudProvider: aws
      regions: [us-west-2]
`, key)), &cfg))

			require.PanicsWithValue(t, `regions not configured for suite plan "dedicated"`, func() {
				cfg.SelectRegionsForSuitePlan(SuitePlanDedicated)
			})
		})
	}
}

func TestCombineCommonAndEnvConfigs_PreservesSuitePlanRegions(t *testing.T) {
	legacyRegions := cloudRegion{{
		CloudProvider: model.AWS,
		Regions:       []string{"us-west-2", "us-east-1"},
	}}
	premiumRegions := cloudRegion{{
		CloudProvider: model.AWS,
		Regions:       []string{"us-west-2"},
	}}

	combined := combineCommonAndEnvConfigs(
		config{Regions: legacyRegions},
		config{SuitePlanRegions: map[SuitePlan]cloudRegion{
			SuitePlanPremium: premiumRegions,
		}},
	)
	combined.SelectRegionsForSuitePlan(SuitePlanPremium)

	assert.Equal(t, premiumRegions, combined.Regions)
}

func TestInitConfigWithDependencies_ApiModeInvalidFailsBeforeLogin(t *testing.T) {
	loginCalled := false
	cfg := config{
		Domain:            DomainCDC,
		TiDBCloudURL:      "https://example.com",
		TiDBCloudUser:     "user",
		TiDBCloudPassword: "password",
		SuitePlanRegions: map[SuitePlan]cloudRegion{
			SuitePlanDedicated: {},
		},
	}

	require.PanicsWithValue(t, "invalid api mode 'invalid', must be one of: [consoleapi openapi]", func() {
		_ = initConfigWithDependencies(cfg, DevENV, "", "invalid", "", "", "", SuitePlanDedicated, func(_ *bool, _, _, _ string) (string, error) {
			loginCalled = true
			return "token", nil
		})
	})
	assert.False(t, loginCalled, "login must not be called for invalid api-mode")
}

func TestInitConfigWithDependencies_ApiModeDefaultStillConsoleAPI(t *testing.T) {
	loginCalled := false
	cfg := config{
		Domain:            DomainCDC,
		TiDBCloudURL:      "https://example.com",
		TiDBCloudUser:     "user",
		TiDBCloudPassword: "password",
		SuitePlanRegions: map[SuitePlan]cloudRegion{
			SuitePlanDedicated: {},
		},
	}

	combined := initConfigWithDependencies(cfg, DevENV, "", "", "", "", "", SuitePlanDedicated, func(_ *bool, url, user, password string) (string, error) {
		loginCalled = true
		assert.Equal(t, "https://example.com", url)
		assert.Equal(t, "user", user)
		assert.Equal(t, "password", password)
		return "token", nil
	})

	assert.True(t, loginCalled)
	assert.Equal(t, ApiModeConsoleAPI, combined.ApiMode)
	assert.Equal(t, "token", combined.TidbCloudToken)
}

func TestResolveEnvConfigPath(t *testing.T) {
	tests := map[string]struct {
		env           string
		envConfigFile string
		want          string
	}{
		"default env config file": {
			env:           "staging",
			envConfigFile: "",
			want:          "./config.staging.yaml",
		},
		"override env config file": {
			env:           "staging",
			envConfigFile: "/tmp/custom.yaml",
			want:          "/tmp/custom.yaml",
		},
	}

	for name, tc := range tests {
		t.Run(name, func(t *testing.T) {
			got := resolveEnvConfigPath(tc.env, tc.envConfigFile)
			assert.Equal(t, tc.want, got)
		})
	}
}

func TestMergeBackupExportWithFallback(t *testing.T) {
	common := BackupExportConfig{
		AWS: BackupExportTargetConfig{TargetURI: "s3://common/", Region: "us-west-2"},
		GCP: BackupExportTargetConfig{TargetURI: "gs://common/"},
	}
	override := BackupExportConfig{
		AWS: BackupExportTargetConfig{TargetURI: "s3://override/"},
	}

	merged := mergeBackupExportWithFallback(common, override)

	assert.Equal(t, "s3://override/", merged.AWS.TargetURI)
	assert.Equal(t, "us-west-2", merged.AWS.Region)
	assert.Equal(t, "gs://common/", merged.GCP.TargetURI)
}
