package app

import (
	"testing"

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/cluster-service-ng/pkg/autoscale/framework"
	"github.com/tidbcloud/cluster-service-ng/pkg/autoscale/modules/input_k8s_v1"
	"github.com/tidbcloud/cluster-service-ng/pkg/autoscale/modules/output_blackhole"
	"github.com/tidbcloud/cluster-service-ng/pkg/autoscale/modules/output_direct_hpa_v1"
	scaler_k8s_v1 "github.com/tidbcloud/cluster-service-ng/pkg/autoscale/modules/scaler_k8s_res_based_v1"
	"github.com/tidbcloud/cluster-service-ng/pkg/autoscale/modules/scaler_o11y_res_based_v1"
	"gopkg.in/yaml.v2"
)

func TestFrameworkConfigParsing(t *testing.T) {
	yamlContent := `
autoscale:
  inputs:
    - uniqName: "input-mock-1"
      type: "InputProviderMockV1"
      interfaceVersion: "InputInterfaceV1"
      config:
        step: 1
  outputs:
    - uniqName: "output-mock-1"
      type: "OutputMockEmpty"
      interfaceVersion: "OutputInterfaceV1"
  scalers:
    - uniqName: "scaler-1"
      type: "ClusterScalerPhasedComponentScalerV1"
      interfaceVersion: "ClusterScalerInterfaceV1"
      inputUniqName: "input-mock-1"
      outputUniqName: "output-mock-1"
      defaultConfig:
        componentName: "tidb"
  clusterScalers:
    - scalerUniqName: "scaler-1"
      targetClusterId: "cluster-1"
      executorName: "ScalerInterfaceV1ExecutorSyncLoop"
      scalerConfig:
        componentName: "tidb"
      executorConfig:
        pauseSecs: 60
`
	var cfg configFile
	err := yaml.UnmarshalStrict([]byte(yamlContent), &cfg)
	require.NoError(t, err)

	fw := cfg.Autoscale
	require.Len(t, fw.Inputs, 1)
	require.Equal(t, "input-mock-1", fw.Inputs[0].UniqName)
	require.Equal(t, "InputProviderMockV1", fw.Inputs[0].Type)
	require.Equal(t, "InputInterfaceV1", fw.Inputs[0].InterfaceVersion)
	require.NotNil(t, fw.Inputs[0].Config)
	require.Equal(t, 1, fw.Inputs[0].Config["step"])

	require.Len(t, fw.Outputs, 1)
	require.Equal(t, "output-mock-1", fw.Outputs[0].UniqName)
	require.Equal(t, "OutputMockEmpty", fw.Outputs[0].Type)

	require.Len(t, fw.Scalers, 1)
	require.Equal(t, "scaler-1", fw.Scalers[0].UniqName)
	require.Equal(t, "ClusterScalerPhasedComponentScalerV1", fw.Scalers[0].Type)
	require.Equal(t, "input-mock-1", fw.Scalers[0].InputUniqName)
	require.Equal(t, "output-mock-1", fw.Scalers[0].OutputUniqName)

	require.Len(t, fw.ClusterScalers, 1)
	require.Equal(t, "scaler-1", fw.ClusterScalers[0].ScalerUniqName)
	require.Equal(t, "cluster-1", fw.ClusterScalers[0].TargetClusterId)
	require.Equal(t, "ScalerInterfaceV1ExecutorSyncLoop", fw.ClusterScalers[0].ExecutorName)
}

func TestConvertToInputProviderConfig(t *testing.T) {
	t.Run("converts InputProviderMockV1", func(t *testing.T) {
		cfg := InputConfig{
			UniqName:         "input-1",
			Type:             "InputProviderMockV1",
			InterfaceVersion: "InputInterfaceV1",
			Config: map[string]any{
				"step": 2,
			},
		}

		result, err := ConvertToInputProviderConfig(cfg)
		require.NoError(t, err)
		require.Equal(t, "input-1", result.UniqName)
		require.Equal(t, framework.InputProviderMockV1IdStr, result.Type)
		require.Equal(t, framework.InputInterfaceV1IdStr, result.InterfaceVersion)
		require.NotNil(t, result.Config)

		mockCfg, ok := result.Config.(*framework.InputProviderMockConfigV1)
		require.True(t, ok)
		require.Equal(t, uint(2), mockCfg.Step)
	})

	t.Run("rejects unknown type", func(t *testing.T) {
		cfg := InputConfig{
			UniqName:         "input-1",
			Type:             "UnknownType",
			InterfaceVersion: "InputInterfaceV1",
			Config: map[string]any{
				"step": 1,
			},
		}

		_, err := ConvertToInputProviderConfig(cfg)
		require.Error(t, err)
		require.Contains(t, err.Error(), "unknown input type")
	})
}

func TestConvertToOutputConfig(t *testing.T) {
	t.Run("converts OutputMockEmpty", func(t *testing.T) {
		cfg := OutputConfig{
			UniqName:         "output-1",
			Type:             "OutputMockEmpty",
			InterfaceVersion: "OutputInterfaceV1",
		}

		result, err := ConvertToOutputConfig(cfg)
		require.NoError(t, err)
		require.Equal(t, "output-1", result.UniqName)
		require.Equal(t, framework.OutputMockEmptyIdStr, result.Type)
	})

	t.Run("rejects unknown type", func(t *testing.T) {
		cfg := OutputConfig{
			UniqName:         "output-1",
			Type:             "UnknownType",
			InterfaceVersion: "OutputInterfaceV1",
			Config: map[string]any{
				"someField": "value",
			},
		}

		_, err := ConvertToOutputConfig(cfg)
		require.Error(t, err)
		require.Contains(t, err.Error(), "unknown output type")
	})
}

func TestConvertToScalerConfig(t *testing.T) {
	t.Run("converts ClusterScalerPhasedComponentScalerV1", func(t *testing.T) {
		cfg := ScalerConfig{
			UniqName:         "scaler-1",
			Type:             "ClusterScalerPhasedComponentScalerV1",
			InterfaceVersion: "ClusterScalerInterfaceV1",
			InputUniqName:    "input-1",
			OutputUniqName:   "output-1",
			DefaultConfig: map[string]any{
				"componentName": "tidb",
			},
		}

		result, err := ConvertToScalerConfig(&cfg, "InputProviderMockV1", "OutputMockEmpty")
		require.NoError(t, err)
		require.Equal(t, framework.ScalerUniqName("scaler-1"), result.UniqName)
		require.Equal(t, framework.ClusterScalerPhasedComponentScalerV1IdStr, result.ScalerMeta.Type)
		require.Equal(t, "input-1", result.InputUniqName)
		require.Equal(t, "output-1", result.OutputUniqName)
		require.NotNil(t, result.DefaultConfig)
	})

	t.Run("accepts nil defaultConfig", func(t *testing.T) {
		cfg := ScalerConfig{
			UniqName:         "scaler-1",
			Type:             "ClusterScalerPhasedComponentScalerV1",
			InterfaceVersion: "ClusterScalerInterfaceV1",
			InputUniqName:    "input-1",
			OutputUniqName:   "output-1",
			DefaultConfig:    nil,
		}

		result, err := ConvertToScalerConfig(&cfg, "InputProviderMockV1", "OutputMockEmpty")
		require.NoError(t, err)
		require.Nil(t, result.DefaultConfig)
	})

	t.Run("rejects unknown type", func(t *testing.T) {
		cfg := ScalerConfig{
			UniqName:         "scaler-1",
			Type:             "UnknownType",
			InterfaceVersion: "ClusterScalerInterfaceV1",
			DefaultConfig: map[string]any{
				"someField": "value",
			},
		}

		_, err := ConvertToScalerConfig(&cfg, "InputProviderMockV1", "OutputMockEmpty")
		require.Error(t, err)
		require.Contains(t, err.Error(), "unknown scaler type")
	})
}

func TestConvertToClusterScalerConfig(t *testing.T) {
	t.Run("converts with ClusterScalerPhasedComponentScalerV1", func(t *testing.T) {
		cfg := ClusterScalerConfig{
			ScalerUniqName:  "scaler-1",
			TargetClusterId: "cluster-1",
			ExecutorName:    "ScalerInterfaceV1ExecutorSyncLoop",
			ScalerConfig: map[string]any{
				"componentName": "tidb",
			},
			ExecutorConfig: map[string]any{
				"pauseSecs": 60,
			},
		}

		result, err := ConvertToClusterScalerConfig(&cfg, "ClusterScalerPhasedComponentScalerV1", "InputProviderMockV1", "OutputMockEmpty")
		require.NoError(t, err)
		require.Equal(t, framework.ScalerUniqName("scaler-1"), result.ScalerUniqName)
		require.Equal(t, framework.ClusterId("cluster-1"), result.TargetClusterId)
		require.Equal(t, framework.ScalerInterfaceV1ExecutorSyncLoopIdStr, result.ExecutorName)
		require.NotNil(t, result.ScalerConfig)
		require.NotNil(t, result.ExecutorConfig)

		execCfg, ok := result.ExecutorConfig.(*framework.ScalerInterfaceV1ExecutorSyncLoopConfig)
		require.True(t, ok)
		require.Equal(t, uint(60), execCfg.PauseSecs)
	})

	t.Run("accepts nil scalerConfig", func(t *testing.T) {
		cfg := ClusterScalerConfig{
			ScalerUniqName:  "scaler-1",
			TargetClusterId: "cluster-1",
			ExecutorName:    "ScalerInterfaceV1ExecutorSyncLoop",
			ScalerConfig:    nil,
			ExecutorConfig: map[string]any{
				"pauseSecs": 60,
			},
		}

		result, err := ConvertToClusterScalerConfig(&cfg, "ClusterScalerPhasedComponentScalerV1", "InputProviderMockV1", "OutputMockEmpty")
		require.NoError(t, err)
		require.Nil(t, result.ScalerConfig)
	})

	t.Run("rejects unknown executor", func(t *testing.T) {
		cfg := ClusterScalerConfig{
			ScalerUniqName:  "scaler-1",
			TargetClusterId: "cluster-1",
			ExecutorName:    "UnknownExecutor",
			ScalerConfig: map[string]any{
				"componentName": "tidb",
			},
			ExecutorConfig: map[string]any{},
		}

		_, err := ConvertToClusterScalerConfig(&cfg, "ClusterScalerPhasedComponentScalerV1", "InputProviderMockV1", "OutputMockEmpty")
		require.Error(t, err)
		require.Contains(t, err.Error(), "unknown executor name")
	})
}

func TestEmptyConfigFile(t *testing.T) {
	t.Run("empty yaml content", func(t *testing.T) {
		yamlContent := ``
		var cfg configFile
		err := yaml.UnmarshalStrict([]byte(yamlContent), &cfg)
		require.NoError(t, err)
		require.Empty(t, cfg.Autoscale.Inputs)
		require.Empty(t, cfg.Autoscale.Outputs)
		require.Empty(t, cfg.Autoscale.Scalers)
		require.Empty(t, cfg.Autoscale.ClusterScalers)
	})

	t.Run("whitespace only", func(t *testing.T) {
		yamlContent := `   `
		var cfg configFile
		err := yaml.UnmarshalStrict([]byte(yamlContent), &cfg)
		require.NoError(t, err)
		require.Empty(t, cfg.Autoscale.Inputs)
	})

	t.Run("empty autoscale key", func(t *testing.T) {
		yamlContent := `autoscale:`
		var cfg configFile
		err := yaml.UnmarshalStrict([]byte(yamlContent), &cfg)
		require.NoError(t, err)
		require.Empty(t, cfg.Autoscale.Inputs)
	})

	t.Run("autoscale with empty sub-keys", func(t *testing.T) {
		yamlContent := `
autoscale:
  inputs: []
  outputs: []
  scalers: []
  clusterScalers: []
`
		var cfg configFile
		err := yaml.UnmarshalStrict([]byte(yamlContent), &cfg)
		require.NoError(t, err)
		require.Empty(t, cfg.Autoscale.Inputs)
		require.Empty(t, cfg.Autoscale.Outputs)
		require.Empty(t, cfg.Autoscale.Scalers)
		require.Empty(t, cfg.Autoscale.ClusterScalers)
	})
}

// --- LoadBaseConfig / RunOneClusterScaler tests ---

func TestLoadBaseConfig_Success(t *testing.T) {
	a, err := New(nil)
	require.NoError(t, err)

	cfg := &AutoscaleConfig{
		Inputs: []InputConfig{{
			UniqName:         "input-1",
			Type:             "InputProviderMockV1",
			InterfaceVersion: "InputInterfaceV1",
			Config:           map[string]any{"step": 1},
		}},
		Outputs: []OutputConfig{{
			UniqName:         "output-1",
			Type:             "OutputMockEmpty",
			InterfaceVersion: "OutputInterfaceV1",
		}},
		Scalers: []ScalerConfig{{
			UniqName:         "scaler-1",
			Type:             "ClusterScalerPhasedComponentScalerV1",
			InterfaceVersion: "ClusterScalerInterfaceV1",
			InputUniqName:    "input-1",
			OutputUniqName:   "output-1",
			DefaultConfig:    map[string]any{"componentName": "tidb"},
		}},
	}

	refs, err := a.LoadBaseConfig(cfg)
	require.NoError(t, err)
	require.Len(t, refs, 1)

	ref, ok := refs["scaler-1"]
	require.True(t, ok)
	require.Equal(t, "ClusterScalerPhasedComponentScalerV1", ref.ScalerType)
	require.Equal(t, "InputProviderMockV1", ref.InputType)
	require.Equal(t, "OutputMockEmpty", ref.OutputType)
}

func TestLoadBaseConfig_MissingInput(t *testing.T) {
	a, err := New(nil)
	require.NoError(t, err)

	cfg := &AutoscaleConfig{
		Outputs: []OutputConfig{{
			UniqName:         "output-1",
			Type:             "OutputMockEmpty",
			InterfaceVersion: "OutputInterfaceV1",
		}},
		Scalers: []ScalerConfig{{
			UniqName:       "scaler-1",
			Type:           "ClusterScalerPhasedComponentScalerV1",
			InputUniqName:  "missing-input",
			OutputUniqName: "output-1",
			DefaultConfig:  map[string]any{"componentName": "tidb"},
		}},
	}

	_, err = a.LoadBaseConfig(cfg)
	require.Error(t, err)
	require.Contains(t, err.Error(), "missing-input")
}

func TestLoadBaseConfig_MissingOutput(t *testing.T) {
	a, err := New(nil)
	require.NoError(t, err)

	cfg := &AutoscaleConfig{
		Inputs: []InputConfig{{
			UniqName:         "input-1",
			Type:             "InputProviderMockV1",
			InterfaceVersion: "InputInterfaceV1",
			Config:           map[string]any{"step": 1},
		}},
		Scalers: []ScalerConfig{{
			UniqName:       "scaler-1",
			Type:           "ClusterScalerPhasedComponentScalerV1",
			InputUniqName:  "input-1",
			OutputUniqName: "missing-output",
			DefaultConfig:  map[string]any{"componentName": "tidb"},
		}},
	}

	_, err = a.LoadBaseConfig(cfg)
	require.Error(t, err)
	require.Contains(t, err.Error(), "missing-output")
}

func TestRunOneClusterScaler_Success(t *testing.T) {
	a, err := New(nil)
	require.NoError(t, err)

	cfg := &AutoscaleConfig{
		Inputs: []InputConfig{{
			UniqName:         "input-1",
			Type:             "InputProviderMockV1",
			InterfaceVersion: "InputInterfaceV1",
			Config:           map[string]any{"step": 1},
		}},
		Outputs: []OutputConfig{{
			UniqName:         "output-1",
			Type:             "OutputMockEmpty",
			InterfaceVersion: "OutputInterfaceV1",
		}},
		Scalers: []ScalerConfig{{
			UniqName:         "scaler-1",
			Type:             "ClusterScalerPhasedComponentScalerV1",
			InterfaceVersion: "ClusterScalerInterfaceV1",
			InputUniqName:    "input-1",
			OutputUniqName:   "output-1",
			DefaultConfig:    map[string]any{"componentName": "tidb"},
		}},
	}

	refs, err := a.LoadBaseConfig(cfg)
	require.NoError(t, err)

	csc := &ClusterScalerConfig{
		ScalerUniqName:  "scaler-1",
		TargetClusterId: "pool-abc",
		ExecutorName:    "ScalerInterfaceV1ExecutorSyncLoop",
	}
	cancel, _, err := a.RunOneClusterScaler(csc, refs)
	require.NoError(t, err)
	require.NotNil(t, cancel)

	// Cancel should stop the goroutine without panic.
	cancel()
}

func TestRunOneClusterScaler_MissingScalerRef(t *testing.T) {
	a, err := New(nil)
	require.NoError(t, err)

	csc := &ClusterScalerConfig{
		ScalerUniqName:  "nonexistent",
		TargetClusterId: "pool-abc",
		ExecutorName:    "ScalerInterfaceV1ExecutorSyncLoop",
	}
	_, _, err = a.RunOneClusterScaler(csc, map[string]ScalerRefInfo{})
	require.Error(t, err)
	require.Contains(t, err.Error(), "nonexistent")
}

func TestScalingRouteWithNestedComponentScalerConfig(t *testing.T) {
	t.Run("parses ScalingRouteO11y2BlackholeByResourceUsageBased with nested o11y scaler config", func(t *testing.T) {
		cfg := ScalerConfig{
			UniqName:         "scaler-o11y-route",
			Type:             string(ScalingRouteO11y2BlackholeByResourceUsageBasedType),
			InterfaceVersion: "ClusterScalerInterfaceV1",
			InputUniqName:    "input-1",
			OutputUniqName:   "output-1",
			DefaultConfig: map[string]any{
				"componentName": "tidb",
				"componentScalerConfig": map[string]any{
					"scaleOutStep":            3,
					"scaleInStep":             2,
					"maxReplicas":             50,
					"minReplicas":             2,
					"cooldownMinutes":         10,
					"cpuUsageScaleOutPercent": 75.0,
					"cpuUsageScaleInPercent":  25.0,
					"memUsageScaleOutPercent": 85.0,
					"memUsageScaleInPercent":  35.0,
				},
			},
		}

		result, err := ConvertToScalerConfig(&cfg, "InputO11yMetricsV1", string(output_blackhole.OutputBlackholeIdStr))
		require.NoError(t, err)
		require.NotNil(t, result.DefaultConfig)

		clusterScalerCfg, ok := result.DefaultConfig.(*framework.ClusterScalerPhasedComponentScalerV1Config)
		require.True(t, ok)
		require.Equal(t, framework.ComponentName("tidb"), clusterScalerCfg.ComponentName)
		require.NotNil(t, clusterScalerCfg.ComponentScalerConfig)

		o11yCfg, ok := clusterScalerCfg.ComponentScalerConfig.(*scaler_o11y_res_based_v1.ScalerO11yResBasedV1Config)
		require.True(t, ok)
		require.Equal(t, uint(3), o11yCfg.ScaleOutStep)
		require.Equal(t, uint(2), o11yCfg.ScaleInStep)
		require.Equal(t, uint(50), o11yCfg.MaxReplicas)
		require.Equal(t, uint(2), o11yCfg.MinReplicas)
		require.Equal(t, uint(10), o11yCfg.CooldownMinutes)
		require.InEpsilon(t, 75.0, o11yCfg.CPUUsageScaleOutPercent, 0.001)
		require.InEpsilon(t, 25.0, o11yCfg.CPUUsageScaleInPercent, 0.001)
		require.InEpsilon(t, 85.0, o11yCfg.MemUsageScaleOutPercent, 0.001)
		require.InEpsilon(t, 35.0, o11yCfg.MemUsageScaleInPercent, 0.001)
	})

	t.Run("parses ScalingRouteK8s2HPAByCPUUsageBased with nested k8s scaler config", func(t *testing.T) {
		cfg := ScalerConfig{
			UniqName:         "scaler-k8s-route",
			Type:             string(ScalingRouteK8s2HPAByCPUUsageBasedType),
			InterfaceVersion: "ClusterScalerInterfaceV1",
			InputUniqName:    "input-1",
			OutputUniqName:   "output-1",
			DefaultConfig: map[string]any{
				"componentName": "tikv",
				"componentScalerConfig": map[string]any{
					"scaleOutStep":                2,
					"scaleInStep":                 1,
					"maxReplicas":                 80,
					"minReplicas":                 3,
					"cooldownMinutes":             8,
					"cpuThresholdScaleOutPercent": 70.0,
					"cpuThresholdScaleInPercent":  20.0,
					"memThresholdScaleOutPercent": 80.0,
					"memThresholdScaleInPercent":  30.0,
				},
			},
		}

		result, err := ConvertToScalerConfig(&cfg, string(input_k8s_v1.InputK8sV1IdStr), string(output_direct_hpa_v1.OutputDirectHPAV1IdStr))
		require.NoError(t, err)
		require.NotNil(t, result.DefaultConfig)

		clusterScalerCfg, ok := result.DefaultConfig.(*framework.ClusterScalerPhasedComponentScalerV1Config)
		require.True(t, ok)
		require.Equal(t, framework.ComponentName("tikv"), clusterScalerCfg.ComponentName)
		require.NotNil(t, clusterScalerCfg.ComponentScalerConfig)

		k8sCfg, ok := clusterScalerCfg.ComponentScalerConfig.(*scaler_k8s_v1.ScalerK8sV1Config)
		require.True(t, ok)
		require.Equal(t, uint(2), k8sCfg.ScaleOutStep)
		require.Equal(t, uint(1), k8sCfg.ScaleInStep)
		require.Equal(t, uint(80), k8sCfg.MaxReplicas)
		require.Equal(t, uint(3), k8sCfg.MinReplicas)
		require.Equal(t, uint(8), k8sCfg.CooldownMinutes)
		require.InEpsilon(t, 70.0, k8sCfg.CPUThresholdScaleOutPercent, 0.001)
		require.InEpsilon(t, 20.0, k8sCfg.CPUThresholdScaleInPercent, 0.001)
		require.InEpsilon(t, 80.0, k8sCfg.MemThresholdScaleOutPercent, 0.001)
		require.InEpsilon(t, 30.0, k8sCfg.MemThresholdScaleInPercent, 0.001)
	})

	t.Run("accepts nil componentScalerConfig", func(t *testing.T) {
		cfg := ScalerConfig{
			UniqName:         "scaler-route-nil",
			Type:             string(ScalingRouteO11y2BlackholeByResourceUsageBasedType),
			InterfaceVersion: "ClusterScalerInterfaceV1",
			InputUniqName:    "input-1",
			OutputUniqName:   "output-1",
			DefaultConfig: map[string]any{
				"componentName": "tidb",
			},
		}

		result, err := ConvertToScalerConfig(&cfg, "InputO11yMetricsV1", string(output_blackhole.OutputBlackholeIdStr))
		require.NoError(t, err)
		require.NotNil(t, result.DefaultConfig)

		clusterScalerCfg, ok := result.DefaultConfig.(*framework.ClusterScalerPhasedComponentScalerV1Config)
		require.True(t, ok)
		require.Equal(t, framework.ComponentName("tidb"), clusterScalerCfg.ComponentName)
		require.Nil(t, clusterScalerCfg.ComponentScalerConfig)
	})

	t.Run("clusterScalerConfig with nested componentScalerConfig", func(t *testing.T) {
		cfg := ClusterScalerConfig{
			ScalerUniqName:  "scaler-1",
			TargetClusterId: "cluster-1",
			ExecutorName:    "ScalerInterfaceV1ExecutorSyncLoop",
			ScalerConfig: map[string]any{
				"componentName": "tidb",
				"componentScalerConfig": map[string]any{
					"scaleOutStep":            5,
					"cpuUsageScaleOutPercent": 90.0,
				},
			},
			ExecutorConfig: map[string]any{
				"pauseSecs": 30,
			},
		}

		result, err := ConvertToClusterScalerConfig(&cfg, string(ScalingRouteO11y2BlackholeByResourceUsageBasedType), "InputO11yMetricsV1", string(output_blackhole.OutputBlackholeIdStr))
		require.NoError(t, err)
		require.NotNil(t, result.ScalerConfig)

		clusterScalerCfg, ok := result.ScalerConfig.(*framework.ClusterScalerPhasedComponentScalerV1Config)
		require.True(t, ok)
		require.Equal(t, framework.ComponentName("tidb"), clusterScalerCfg.ComponentName)
		require.NotNil(t, clusterScalerCfg.ComponentScalerConfig)

		o11yCfg, ok := clusterScalerCfg.ComponentScalerConfig.(*scaler_o11y_res_based_v1.ScalerO11yResBasedV1Config)
		require.True(t, ok)
		require.Equal(t, uint(5), o11yCfg.ScaleOutStep)
		require.InEpsilon(t, 90.0, o11yCfg.CPUUsageScaleOutPercent, 0.001)
	})
}
