package server

import (
	"testing"

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

func TestTikvCPUChangeOnly(t *testing.T) {
	tests := []struct {
		name     string
		current  *entity.NodeSpec
		target   *entity.NodeSpec
		expected bool
	}{
		{
			name:     "8c16g to 16c64g — CPU changes, allow",
			current:  &entity.NodeSpec{Cpu: 8, MemoryGi: 16},
			target:   &entity.NodeSpec{Cpu: 16, MemoryGi: 64},
			expected: true,
		},
		{
			name:     "16c64g to 8c32g — CPU changes, allow",
			current:  &entity.NodeSpec{Cpu: 16, MemoryGi: 64},
			target:   &entity.NodeSpec{Cpu: 8, MemoryGi: 32},
			expected: true,
		},
		{
			name:     "8c16g to 8c32g — same CPU, veto",
			current:  &entity.NodeSpec{Cpu: 8, MemoryGi: 16},
			target:   &entity.NodeSpec{Cpu: 8, MemoryGi: 32},
			expected: false,
		},
		{
			name:     "8c32g to 8c16g — same CPU, veto",
			current:  &entity.NodeSpec{Cpu: 8, MemoryGi: 32},
			target:   &entity.NodeSpec{Cpu: 8, MemoryGi: 16},
			expected: false,
		},
		{
			name:     "same spec — veto",
			current:  &entity.NodeSpec{Cpu: 8, MemoryGi: 16},
			target:   &entity.NodeSpec{Cpu: 8, MemoryGi: 16},
			expected: false,
		},
		{
			name:     "nil current — CPU 0 vs non-zero, allow",
			current:  nil,
			target:   &entity.NodeSpec{Cpu: 8, MemoryGi: 16},
			expected: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.Equal(t, tt.expected, tikvCPUChangeOnly(tt.current, tt.target))
		})
	}
}

func TestGetRcuScaleComponents_ReturnsDefensiveCopy(t *testing.T) {
	original := GetRcuScaleComponents()
	require.NotEmpty(t, original)

	// Mutate the returned copy.
	original[0].ComponentType = apiv1.ComponentType_TIPROXY

	// The package-level slice must be unchanged.
	fresh := GetRcuScaleComponents()
	assert.NotEqual(t, apiv1.ComponentType_TIPROXY, fresh[0].ComponentType,
		"mutating the returned slice must not affect the package-level whitelist")
}

func TestRcuScaleComponentSet(t *testing.T) {
	set := rcuScaleComponentSet()

	// All whitelist entries are in the set.
	for _, cfg := range GetRcuScaleComponents() {
		got, ok := set[cfg.ComponentType]
		require.True(t, ok, "component %s should be in set", cfg.ComponentType)
		assert.Equal(t, cfg.Scope, got.Scope)
	}

	// Non-whitelist components are absent.
	_, ok := set[apiv1.ComponentType_TIPROXY]
	assert.False(t, ok, "TIPROXY should not be in the whitelist set")
	_, ok = set[apiv1.ComponentType_PD]
	assert.False(t, ok, "PD should not be in the whitelist set")
}

func TestSnapshotComponentSpecs(t *testing.T) {
	sp := &entity.SharedPool{
		SharedPool: apiv1.SharedPool{
			ComponentGroups: []*apiv1.Component{
				{
					Id:            "default",
					ComponentType: apiv1.ComponentType_TIKV,
					NodeQuota: &apiv1.NodeQuota{
						NodeSpec: &apiv1.NodeSpec{Cpu: 8, MemoryGi: 16},
					},
				},
				{
					Id:            "default",
					ComponentType: apiv1.ComponentType_PD,
					NodeQuota: &apiv1.NodeQuota{
						NodeSpec: &apiv1.NodeSpec{Cpu: 4, MemoryGi: 8},
					},
				},
				{
					// Component with no NodeQuota — should be skipped.
					Id:            "no-quota",
					ComponentType: apiv1.ComponentType_TIDB_WORKER,
				},
			},
		},
	}

	snap := snapshotComponentSpecs(sp)

	// Check snapshot captured the right values.
	tikvKey := componentSpecKey{compType: apiv1.ComponentType_TIKV, id: "default"}
	require.Contains(t, snap, tikvKey)
	assert.Equal(t, uint32(8), snap[tikvKey].GetCpu())
	assert.Equal(t, uint32(16), snap[tikvKey].GetMemoryGi())

	pdKey := componentSpecKey{compType: apiv1.ComponentType_PD, id: "default"}
	require.Contains(t, snap, pdKey)

	// Component without NodeQuota should not be in snapshot.
	workerKey := componentSpecKey{compType: apiv1.ComponentType_TIDB_WORKER, id: "no-quota"}
	assert.NotContains(t, snap, workerKey)

	// Verify snapshot is independent of original — mutate original spec.
	sp.ComponentGroups[0].NodeQuota.NodeSpec.Cpu = 16
	assert.Equal(t, uint32(8), snap[tikvKey].GetCpu(),
		"snapshot must be independent of original proto object")
}

func TestScaleUpWhitelistFiltersTiKVMemoryOnlyChange(t *testing.T) {
	// Simulate: TiKV spec changes from 8c16g to 8c32g (memory-only).
	// The whitelist ScaleOverride should veto this.
	set := rcuScaleComponentSet()
	cfg := set[apiv1.ComponentType_TIKV]
	require.NotNil(t, cfg.ScaleOverride)

	oldSpec := &entity.NodeSpec{Cpu: 8, MemoryGi: 16}
	newSpec := &entity.NodeSpec{Cpu: 8, MemoryGi: 32}

	assert.False(t, cfg.ScaleOverride(oldSpec, newSpec),
		"TiKV memory-only change (8c16g→8c32g) should be vetoed")
}

func TestScaleUpWhitelistAllowsTiKVCPUChange(t *testing.T) {
	set := rcuScaleComponentSet()
	cfg := set[apiv1.ComponentType_TIKV]
	require.NotNil(t, cfg.ScaleOverride)

	// 8c16g → 16c64g (CPU changes)
	assert.True(t, cfg.ScaleOverride(
		&entity.NodeSpec{Cpu: 8, MemoryGi: 16},
		&entity.NodeSpec{Cpu: 16, MemoryGi: 64},
	), "TiKV CPU change should be allowed")

	// 16c64g → 8c32g (CPU changes, scale down)
	assert.True(t, cfg.ScaleOverride(
		&entity.NodeSpec{Cpu: 16, MemoryGi: 64},
		&entity.NodeSpec{Cpu: 8, MemoryGi: 32},
	), "TiKV CPU change (downgrade) should be allowed")
}

func TestWhitelistComponentsAndScopes(t *testing.T) {
	components := GetRcuScaleComponents()

	expected := map[apiv1.ComponentType]RcuScaleScope{
		apiv1.ComponentType_TIKV:               RcuScaleScopeSharedPool,
		apiv1.ComponentType_TIDB:               RcuScaleScopePerCluster,
		apiv1.ComponentType_TIKV_WORKER:        RcuScaleScopeSharedPool,
		apiv1.ComponentType_TIDB_WORKER:        RcuScaleScopeSharedPool,
		apiv1.ComponentType_COPROCESSOR_WORKER: RcuScaleScopeSharedPool,
	}

	assert.Len(t, components, len(expected))
	for _, cfg := range components {
		scope, ok := expected[cfg.ComponentType]
		require.True(t, ok, "unexpected component in whitelist: %s", cfg.ComponentType)
		assert.Equal(t, scope, cfg.Scope, "wrong scope for %s", cfg.ComponentType)
	}

	// Only TiKV should have a ScaleOverride.
	for _, cfg := range components {
		if cfg.ComponentType == apiv1.ComponentType_TIKV {
			assert.NotNil(t, cfg.ScaleOverride, "TiKV should have ScaleOverride")
		} else {
			assert.Nil(t, cfg.ScaleOverride, "%s should not have ScaleOverride", cfg.ComponentType)
		}
	}
}
