package ruconfig

import (
	"strings"
	"testing"

	"github.com/samber/lo"
	"github.com/stretchr/testify/require"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/cluster-service-ng/config"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
)

func TestIsValid(t *testing.T) {
	c := &entity.RuConfig{
		RuConfig: apiv1.RuConfig{
			Components: map[string]*apiv1.RuConfig_ComponentsMap{},
		},
	}
	for _, comp := range []apiv1.ComponentType{
		apiv1.ComponentType_PD,
		apiv1.ComponentType_PD_SCHEDULING,
		apiv1.ComponentType_PD_TSO,
		apiv1.ComponentType_TIKV,
		apiv1.ComponentType_TIKV_WORKER,
		apiv1.ComponentType_COPROCESSOR_WORKER,
		apiv1.ComponentType_TIDB,
		apiv1.ComponentType_TIDB_WORKER,
		apiv1.ComponentType_TIFLASH_COMPUTE,
		apiv1.ComponentType_TIFLASH_WRITE,
		apiv1.ComponentType_TIPROXY,
		apiv1.ComponentType_RESOURCE_MANAGER,
	} {
		c.Components[strings.ToLower(comp.String())] = &apiv1.RuConfig_ComponentsMap{
			NodeSpecRuRange: map[string]*apiv1.RuConfig_RuRange{
				"8c16g": {Min: 0, Max: 100},
			},
		}
	}
	require.NoError(t, isValid(c))

	// missing a component
	c2 := &entity.RuConfig{
		RuConfig: apiv1.RuConfig{
			Components: map[string]*apiv1.RuConfig_ComponentsMap{},
		},
	}
	c2.Components["pd"] = &apiv1.RuConfig_ComponentsMap{
		NodeSpecRuRange: map[string]*apiv1.RuConfig_RuRange{"8c16g": {Min: 0, Max: 100}},
	}

	require.Error(t, isValid(c2))

	// node spec is invalid
	c3 := &entity.RuConfig{
		RuConfig: apiv1.RuConfig{
			Components: map[string]*apiv1.RuConfig_ComponentsMap{
				"pd": {
					NodeSpecRuRange: map[string]*apiv1.RuConfig_RuRange{"invalid-spec": {Min: 0, Max: 100}},
				},
			},
		},
	}
	require.Error(t, isValid(c3))

	// max <= min
	c4 := &entity.RuConfig{
		RuConfig: apiv1.RuConfig{
			Components: map[string]*apiv1.RuConfig_ComponentsMap{
				"pd": {
					NodeSpecRuRange: map[string]*apiv1.RuConfig_RuRange{"8c16g": {Min: 10, Max: 5}},
				},
			},
		},
	}
	require.Error(t, isValid(c4))

	// not having a range starting from 0
	c5 := &entity.RuConfig{
		RuConfig: apiv1.RuConfig{
			Components: map[string]*apiv1.RuConfig_ComponentsMap{
				"pd": {
					NodeSpecRuRange: map[string]*apiv1.RuConfig_RuRange{"8c16g": {Min: 1, Max: 100}},
				},
			},
		},
	}
	require.Error(t, isValid(c5))

	// components is nil
	c6 := &entity.RuConfig{}
	require.Error(t, isValid(c6))

	// default node count exceeds effective max node count
	tooLargeDefaultNodeCount := uint32(61)
	c.Components["tiproxy"].NodeSpecRuRange["8c16g"].DefaultNodeCount = &tooLargeDefaultNodeCount
	require.Error(t, isValid(c))

	// default node count below component minimum
	zeroDefaultNodeCount := uint32(0)
	c.Components["tiproxy"].NodeSpecRuRange["8c16g"].DefaultNodeCount = &zeroDefaultNodeCount
	require.Error(t, isValid(c))

	// unsupported components must not opt into defaultNodeCount in phase 1
	unsupportedDefaultNodeCount := uint32(1)
	c.Components["tiproxy"].NodeSpecRuRange["8c16g"].DefaultNodeCount = nil
	c.Components["pd"].NodeSpecRuRange["8c16g"].DefaultNodeCount = &unsupportedDefaultNodeCount
	require.Error(t, isValid(c))

	// tikv default node count is capped by the effective multiple-of-3 max
	c.Components["pd"].NodeSpecRuRange["8c16g"].DefaultNodeCount = nil
	c.Components["tiproxy"].NodeSpecRuRange["8c16g"].DefaultNodeCount = nil
	c.Components["tikv"].MaxNodeCount = 10
	tikvTooLargeDefaultNodeCount := uint32(10)
	c.Components["tikv"].NodeSpecRuRange["8c16g"].DefaultNodeCount = &tikvTooLargeDefaultNodeCount
	require.Error(t, isValid(c))

	tikvNonMultipleDefaultNodeCount := uint32(4)
	c.Components["tikv"].NodeSpecRuRange["8c16g"].DefaultNodeCount = &tikvNonMultipleDefaultNodeCount
	require.Error(t, isValid(c))

	tikvValidDefaultNodeCount := uint32(9)
	c.Components["tikv"].NodeSpecRuRange["8c16g"].DefaultNodeCount = &tikvValidDefaultNodeCount
	require.NoError(t, isValid(c))
}

func TestCvtDefaultRuConfigToEntity(t *testing.T) {
	// 构造一个默认 config.RuConfig
	defaultNodeCount := uint32(0)
	defaultRuConfig := &config.RuConfig{
		EnableScaleSharedPoolByRu:           true,
		EnableScaleTiDBGroupByRu:            false,
		EnableAutoscaleTikvCpuScaleOut:      true,
		EnableAutoscaleDiskExpand:           false,
		EnableAutoscaleIopsThroughputExpand: true,
		EnableAutoscaleTiproxyScaleIn:       true,
		Components: map[string]map[string]config.RuRange{
			"tikv": {
				"8c16g": {Min: 0, Max: 100},
			},
			"tidb": {
				"4c8g": {Min: 0, Max: 50},
			},
			"tiflash_compute": {
				"4c16g": {Min: 0, Max: 50, DefaultNodeCount: &defaultNodeCount},
			},
		},
		NodeCountQuota: map[string]uint32{"tikv": 10, "tidb": 5},
	}

	entityRuConfig, err := cvtDefaultRuConfigToEntity(defaultRuConfig)
	require.NoError(t, err)
	require.NotNil(t, entityRuConfig)
	require.Len(t, entityRuConfig.Components, 3)
	require.Contains(t, entityRuConfig.Components, "tikv")
	require.Contains(t, entityRuConfig.Components, "tidb")
	require.Contains(t, entityRuConfig.Components, "tiflash_compute")

	// keep the ru range
	tikv := entityRuConfig.Components["tikv"]
	require.NotNil(t, tikv)
	require.Contains(t, tikv.NodeSpecRuRange, "8c16g")
	require.Equal(t, uint32(100), tikv.NodeSpecRuRange["8c16g"].Max)

	tidb := entityRuConfig.Components["tidb"]
	require.NotNil(t, tidb)
	require.Contains(t, tidb.NodeSpecRuRange, "4c8g")
	require.Equal(t, uint32(50), tidb.NodeSpecRuRange["4c8g"].Max)

	tiflashCompute := entityRuConfig.Components["tiflash_compute"]
	require.NotNil(t, tiflashCompute)
	require.Contains(t, tiflashCompute.NodeSpecRuRange, "4c16g")
	require.NotNil(t, tiflashCompute.NodeSpecRuRange["4c16g"].DefaultNodeCount)
	require.Equal(t, uint32(0), tiflashCompute.NodeSpecRuRange["4c16g"].GetDefaultNodeCount())

	// check flags
	require.True(t, lo.FromPtr(entityRuConfig.GetFeatureFlags().EnableScaleSharedPoolByRu))
	require.False(t, lo.FromPtr(entityRuConfig.GetFeatureFlags().EnableScaleTidbGroupByRu))
	require.True(t, lo.FromPtr(entityRuConfig.GetFeatureFlags().EnableAutoscaleTikvCpuScaleOut))
	require.False(t, lo.FromPtr(entityRuConfig.GetFeatureFlags().EnableAutoscaleDiskExpand))
	require.True(t, lo.FromPtr(entityRuConfig.GetFeatureFlags().EnableAutoscaleIopsThroughputExpand))
	require.True(t, lo.FromPtr(entityRuConfig.GetFeatureFlags().EnableAutoscaleTiproxyScaleIn))

	// null config
	empty, err := cvtDefaultRuConfigToEntity(nil)
	require.Error(t, err)
	require.Nil(t, empty)
}

func TestFillDefaultOptionalComponents(t *testing.T) {
	// Create a default config with resource_manager
	defaultConfig := &entity.RuConfig{
		RuConfig: apiv1.RuConfig{
			Components: map[string]*apiv1.RuConfig_ComponentsMap{
				"resource_manager": {
					MaxNodeCount: 2,
					NodeSpecRuRange: map[string]*apiv1.RuConfig_RuRange{
						"2c4g": {Min: 0, Max: 0},
					},
				},
			},
		},
	}

	t.Run("fill resource_manager when missing", func(t *testing.T) {
		// Create a config without resource_manager
		c := &entity.RuConfig{
			RuConfig: apiv1.RuConfig{
				Components: map[string]*apiv1.RuConfig_ComponentsMap{
					"tikv": {
						NodeSpecRuRange: map[string]*apiv1.RuConfig_RuRange{
							"8c16g": {Min: 0, Max: 100},
						},
					},
				},
			},
		}

		// Before fill, resource_manager should not exist
		_, exists := c.Components["resource_manager"]
		require.False(t, exists)

		// Fill default optional components
		fillDefaultOptionalComponents(c, defaultConfig)

		// After fill, resource_manager should exist with default config
		resourceManager, exists := c.Components["resource_manager"]
		require.True(t, exists)
		require.NotNil(t, resourceManager)
		require.Equal(t, uint32(2), resourceManager.MaxNodeCount)
		require.Contains(t, resourceManager.NodeSpecRuRange, "2c4g")

		// tikv should still exist
		_, tikvExists := c.Components["tikv"]
		require.True(t, tikvExists)
	})

	t.Run("do not overwrite existing resource_manager", func(t *testing.T) {
		// Create a config with custom resource_manager
		c := &entity.RuConfig{
			RuConfig: apiv1.RuConfig{
				Components: map[string]*apiv1.RuConfig_ComponentsMap{
					"resource_manager": {
						MaxNodeCount: 4, // custom value
						NodeSpecRuRange: map[string]*apiv1.RuConfig_RuRange{
							"4c8g": {Min: 0, Max: 200}, // custom spec
						},
					},
				},
			},
		}

		// Fill default optional components
		fillDefaultOptionalComponents(c, defaultConfig)

		// resource_manager should keep custom config, not be overwritten
		resourceManager := c.Components["resource_manager"]
		require.Equal(t, uint32(4), resourceManager.MaxNodeCount)
		require.Contains(t, resourceManager.NodeSpecRuRange, "4c8g")
		require.NotContains(t, resourceManager.NodeSpecRuRange, "2c4g")
	})

	t.Run("handle nil components map", func(t *testing.T) {
		c := &entity.RuConfig{}

		// Fill default optional components
		fillDefaultOptionalComponents(c, defaultConfig)

		// Components should be initialized and resource_manager should exist
		require.NotNil(t, c.Components)
		_, exists := c.Components["resource_manager"]
		require.True(t, exists)
	})
}

// TestCvtDefaultRuConfigToEntity_YAMLDefaultsReq1Req2False verifies that when
// YAML omits all feature flags (simulating a new deployment), the creasty/defaults
// library fills REQ-1/REQ-2 with false and leaves the autoscale flags true via
// the struct default tags. This is the
// "new tenant, no DB row" path: GetRuConfig returns the in-memory default.
func TestCvtDefaultRuConfigToEntity_YAMLDefaultsReq1Req2False(t *testing.T) {
	// Simulate a YAML config where feature flags are not set — creasty/defaults
	// would have filled REQ-1/REQ-2 with false and the autoscale flags with
	// true before this function is called.
	cfg := &config.RuConfig{
		EnableScaleSharedPoolByRu:           false,
		EnableScaleTiDBGroupByRu:            false,
		EnableAutoscaleTikvCpuScaleOut:      true,
		EnableAutoscaleDiskExpand:           true,
		EnableAutoscaleIopsThroughputExpand: true,
		EnableAutoscaleTiproxyScaleIn:       true,
		Components: map[string]map[string]config.RuRange{
			"tikv": {"8c16g": {Min: 0, Max: 100}},
		},
	}

	entityCfg, err := cvtDefaultRuConfigToEntity(cfg)
	require.NoError(t, err)
	require.False(t, entityCfg.GetFeatureFlags().GetEnableScaleSharedPoolByRu())
	require.False(t, entityCfg.GetFeatureFlags().GetEnableScaleTidbGroupByRu())
	require.True(t, entityCfg.GetFeatureFlags().GetEnableAutoscaleTikvCpuScaleOut())
	require.True(t, entityCfg.GetFeatureFlags().GetEnableAutoscaleDiskExpand())
	require.True(t, entityCfg.GetFeatureFlags().GetEnableAutoscaleIopsThroughputExpand())
	require.True(t, entityCfg.GetFeatureFlags().GetEnableAutoscaleTiproxyScaleIn())
}
