package cron

import (
	"context"
	"fmt"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"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"
	"github.com/tidbcloud/cluster-service-ng/domain/server"
	servermocks "github.com/tidbcloud/cluster-service-ng/domain/server/mocks"
	"github.com/tidbcloud/cluster-service-ng/pkg/storage/mysql/schema"
	repomocks "github.com/tidbcloud/cluster-service-ng/pkg/storage/repo/mocks"
	"github.com/tidbcloud/cluster-service-ng/pkg/utils"
)

func TestProcessSharedPoolSkipsEssentialV2(t *testing.T) {
	lockRepo := repomocks.NewLockRepo(t)
	job := &RcuScaleDownJob{lockRepo: lockRepo}
	sharedPool := &entity.SharedPool{
		SharedPool: apiv1.SharedPool{Id: "essential-v2-pool"},
		DeployType: schema.SharedPoolDeployTypeSharedTiProxy,
	}

	require.NoError(t, job.processSharedPool(context.Background(), sharedPool))
	lockRepo.AssertNotCalled(t, "Lock", mock.Anything, mock.Anything)
}

func TestRcuScaleDownJobDoesNotSkipPremiumCreatingPool(t *testing.T) {
	service := servermocks.NewService(t)
	lockRepo := repomocks.NewLockRepo(t)
	sharedPool := &entity.SharedPool{SharedPool: apiv1.SharedPool{
		Id:    "premium-creating-pool",
		Phase: apiv1.SharedPool_CREATING,
	}}
	service.On("ListSharedPools", mock.Anything, mock.Anything, mock.Anything).
		Return([]*entity.SharedPool{sharedPool}, nil)
	service.On("GetRuConfig", mock.Anything, sharedPool.GetId(), apiv1.RuConfig_SHARED_POOL).
		Return(&entity.RuConfig{}, nil)
	lockRepo.On("Lock", mock.Anything, mock.Anything).
		Return(func() error { return nil }, nil)

	job := NewRcuScaleDownJob(service, lockRepo, "", 0)
	require.NoError(t, job.Execute(context.Background()))
	lockRepo.AssertCalled(t, "Lock", mock.Anything, mock.Anything)
}

func TestNeedsDowngrade(t *testing.T) {
	cases := []struct {
		name    string
		current *entity.NodeSpec
		target  *entity.NodeSpec
		want    bool
	}{
		{
			name:    "current larger CPU only -> no downgrade (AND logic)",
			current: &entity.NodeSpec{Cpu: 16, MemoryGi: 32},
			target:  &entity.NodeSpec{Cpu: 8, MemoryGi: 32},
			want:    false,
		},
		{
			name:    "current larger memory only -> no downgrade (AND logic)",
			current: &entity.NodeSpec{Cpu: 8, MemoryGi: 64},
			target:  &entity.NodeSpec{Cpu: 8, MemoryGi: 32},
			want:    false,
		},
		{
			name:    "current larger both CPU and memory -> downgrade",
			current: &entity.NodeSpec{Cpu: 16, MemoryGi: 64},
			target:  &entity.NodeSpec{Cpu: 8, MemoryGi: 32},
			want:    true,
		},
		{
			name:    "mixed change (CPU down, memory up) -> no downgrade",
			current: &entity.NodeSpec{Cpu: 16, MemoryGi: 32},
			target:  &entity.NodeSpec{Cpu: 8, MemoryGi: 64},
			want:    false,
		},
		{
			name:    "same spec -> no downgrade",
			current: &entity.NodeSpec{Cpu: 8, MemoryGi: 32},
			target:  &entity.NodeSpec{Cpu: 8, MemoryGi: 32},
			want:    false,
		},
		{
			name:    "target larger -> no downgrade",
			current: &entity.NodeSpec{Cpu: 8, MemoryGi: 16},
			target:  &entity.NodeSpec{Cpu: 16, MemoryGi: 64},
			want:    false,
		},
		{
			name:    "nil current -> no downgrade",
			current: nil,
			target:  &entity.NodeSpec{Cpu: 8, MemoryGi: 16},
			want:    false,
		},
		{
			name:    "nil target -> no downgrade",
			current: &entity.NodeSpec{Cpu: 8, MemoryGi: 16},
			target:  nil,
			want:    false,
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got := needsDowngrade(tc.current, tc.target)
			if got != tc.want {
				t.Errorf("needsDowngrade(%v, %v) = %v, want %v", tc.current, tc.target, got, tc.want)
			}
		})
	}
}

func TestFindDefaultComponent(t *testing.T) {
	spID := "test-pool-123"
	defaultID := utils.FnvHash(spID)

	sp := &entity.SharedPool{
		SharedPool: apiv1.SharedPool{
			Id: spID,
			ComponentGroups: []*entity.Component{
				{
					Id:            defaultID,
					ComponentType: apiv1.ComponentType_TIKV,
					NodeQuota:     &entity.NodeQuota{NodeSpec: &entity.NodeSpec{Cpu: 8, MemoryGi: 16}},
				},
				{
					Id:            "other-id",
					ComponentType: apiv1.ComponentType_TIKV,
					NodeQuota:     &entity.NodeQuota{NodeSpec: &entity.NodeSpec{Cpu: 4, MemoryGi: 8}},
				},
			},
		},
	}

	comp := findDefaultComponent(sp, apiv1.ComponentType_TIKV)
	if comp == nil {
		t.Fatal("expected to find default TiKV component")
	}
	if comp.GetNodeQuota().GetNodeSpec().GetCpu() != 8 {
		t.Errorf("expected CPU=8, got %d", comp.GetNodeQuota().GetNodeSpec().GetCpu())
	}

	comp = findDefaultComponent(sp, apiv1.ComponentType_TIPROXY)
	if comp != nil {
		t.Error("expected nil for non-existing default TiProxy component")
	}
}

func TestFindComponentByTypeAndID(t *testing.T) {
	sp := &entity.SharedPool{
		SharedPool: apiv1.SharedPool{
			Id: "pool-1",
			ComponentGroups: []*entity.Component{
				{
					Id:            "cluster-a",
					ComponentType: apiv1.ComponentType_TIDB,
					NodeQuota:     &entity.NodeQuota{NodeSpec: &entity.NodeSpec{Cpu: 4, MemoryGi: 8}},
				},
				{
					Id:            "cluster-b",
					ComponentType: apiv1.ComponentType_TIPROXY,
					NodeQuota:     &entity.NodeQuota{NodeSpec: &entity.NodeSpec{Cpu: 8, MemoryGi: 16}},
				},
			},
		},
	}

	comp := findComponentByTypeAndId(sp, apiv1.ComponentType_TIPROXY, "cluster-b")
	if comp == nil {
		t.Fatal("expected to find TiProxy component for cluster-b")
	}
	if comp.GetNodeQuota().GetNodeSpec().GetCpu() != 8 {
		t.Errorf("expected CPU=8, got %d", comp.GetNodeQuota().GetNodeSpec().GetCpu())
	}

	comp = findComponentByTypeAndId(sp, apiv1.ComponentType_TIDB, "non-existent")
	if comp != nil {
		t.Error("expected nil for non-existent component id")
	}
}

func TestCloneScaleComponent(t *testing.T) {
	curr := &entity.Component{
		Id:            "cluster-a",
		ComponentType: apiv1.ComponentType_TIPROXY,
		NodeQuota: &entity.NodeQuota{
			NodeCount:    2,
			MinNodeCount: 1,
			MaxNodeCount: 3,
			NodeSpec:     &entity.NodeSpec{Cpu: 8, MemoryGi: 16},
		},
	}

	cloned := cloneScaleComponent(curr, &entity.NodeSpec{Cpu: 4, MemoryGi: 8}, apiv1.ComponentType_TIPROXY)
	if cloned.GetId() != curr.GetId() {
		t.Fatalf("expected same id, got %s", cloned.GetId())
	}
	if cloned.GetNodeQuota().GetNodeCount() != curr.GetNodeQuota().GetNodeCount() {
		t.Fatalf("expected same node count, got %d", cloned.GetNodeQuota().GetNodeCount())
	}
	if cloned.GetNodeQuota().GetNodeSpec().GetCpu() != 4 {
		t.Fatalf("expected cloned cpu 4, got %d", cloned.GetNodeQuota().GetNodeSpec().GetCpu())
	}
}

// --- Tests for collectSharedPoolDowngrades / checkAndAppendDowngrade ---

// buildRuConfig builds a minimal RuConfig with one spec tier per component.
func buildRuConfig(specs map[string]struct{ cpu, mem uint32 }) *entity.RuConfig {
	components := make(map[string]*apiv1.RuConfig_ComponentsMap)
	for comp, s := range specs {
		key := fmt.Sprintf("%dC%dG", s.cpu, s.mem)
		components[comp] = &apiv1.RuConfig_ComponentsMap{
			NodeSpecRuRange: map[string]*apiv1.RuConfig_RuRange{
				key: {Min: 0, Max: 0}, // max=0 → unlimited
			},
		}
	}
	return &entity.RuConfig{RuConfig: apiv1.RuConfig{Components: components}}
}

func TestCollectSharedPoolDowngrades_Downgrade(t *testing.T) {
	spID := "pool-1"
	defaultID := utils.FnvHash(spID)

	sp := &entity.SharedPool{SharedPool: apiv1.SharedPool{
		Id: spID,
		ComponentGroups: []*entity.Component{{
			Id: defaultID, ComponentType: apiv1.ComponentType_TIKV,
			NodeQuota: &entity.NodeQuota{NodeSpec: &entity.NodeSpec{Cpu: 16, MemoryGi: 64}, NodeCount: 3},
		}},
	}}

	// Target tier: 8c16g — strictly smaller in both dimensions → downgrade.
	ruConfig := buildRuConfig(map[string]struct{ cpu, mem uint32 }{"tikv": {8, 16}})
	cfg := server.RcuScaleComponentConfig{ComponentType: apiv1.ComponentType_TIKV, Scope: server.RcuScaleScopeSharedPool}

	var result []*entity.Component
	job := &RcuScaleDownJob{}
	job.collectSharedPoolDowngrades(context.Background(), sp, ruConfig, cfg, 1000, &result)

	require.Len(t, result, 1)
	assert.Equal(t, uint32(8), result[0].GetNodeQuota().GetNodeSpec().GetCpu())
	assert.Equal(t, uint32(16), result[0].GetNodeQuota().GetNodeSpec().GetMemoryGi())
}

func TestCollectSharedPoolDowngrades_HighRuSkip(t *testing.T) {
	spID := "pool-high-ru"
	defaultID := utils.FnvHash(spID)

	sp := &entity.SharedPool{SharedPool: apiv1.SharedPool{
		Id: spID,
		ComponentGroups: []*entity.Component{{
			Id: defaultID, ComponentType: apiv1.ComponentType_TIKV,
			NodeQuota: &entity.NodeQuota{NodeSpec: &entity.NodeSpec{Cpu: 32, MemoryGi: 128}, NodeCount: 3},
		}},
	}}

	// Config top tier is 16c64g and unlimited, so without the guard the
	// 32c component would be clobbered down to 16c. poolMaxRu above the
	// threshold must skip it instead.
	ruConfig := buildRuConfig(map[string]struct{ cpu, mem uint32 }{"tikv": {16, 64}})
	cfg := server.RcuScaleComponentConfig{ComponentType: apiv1.ComponentType_TIKV, Scope: server.RcuScaleScopeSharedPool, ScaleOverride: nil}

	var result []*entity.Component
	job := &RcuScaleDownJob{}
	job.collectSharedPoolDowngrades(context.Background(), sp, ruConfig, cfg, DefaultRcuScaleDownMaxRuThreshold+1, &result)

	assert.Empty(t, result)
}

func TestCheckAndAppendDowngrade_HighRuSkip(t *testing.T) {
	comp := &entity.Component{
		Id:            "cluster-a",
		ComponentType: apiv1.ComponentType_TIDB,
		NodeQuota: &entity.NodeQuota{
			NodeSpec: &entity.NodeSpec{Cpu: 16, MemoryGi: 64}, NodeCount: 2,
		},
	}
	targetSpec := &entity.NodeSpec{Cpu: 8, MemoryGi: 16}
	cfg := server.RcuScaleComponentConfig{ComponentType: apiv1.ComponentType_TIDB, Scope: server.RcuScaleScopePerCluster}

	var result []*entity.Component
	job := &RcuScaleDownJob{}
	job.checkAndAppendDowngrade(context.Background(), "pool", "cluster-a", cfg, comp, targetSpec, DefaultRcuScaleDownMaxRuThreshold+1, &result)

	assert.Empty(t, result)
}

func TestCollectSharedPoolDowngrades_NoDowngrade(t *testing.T) {
	spID := "pool-2"
	defaultID := utils.FnvHash(spID)

	sp := &entity.SharedPool{SharedPool: apiv1.SharedPool{
		Id: spID,
		ComponentGroups: []*entity.Component{{
			Id: defaultID, ComponentType: apiv1.ComponentType_TIKV,
			NodeQuota: &entity.NodeQuota{NodeSpec: &entity.NodeSpec{Cpu: 8, MemoryGi: 16}, NodeCount: 3},
		}},
	}}

	// Target tier same as current → no downgrade.
	ruConfig := buildRuConfig(map[string]struct{ cpu, mem uint32 }{"tikv": {8, 16}})
	cfg := server.RcuScaleComponentConfig{ComponentType: apiv1.ComponentType_TIKV, Scope: server.RcuScaleScopeSharedPool}

	var result []*entity.Component
	job := &RcuScaleDownJob{}
	job.collectSharedPoolDowngrades(context.Background(), sp, ruConfig, cfg, 1000, &result)

	assert.Empty(t, result)
}

func TestCollectSharedPoolDowngrades_ScaleOverrideVeto(t *testing.T) {
	spID := "pool-3"
	defaultID := utils.FnvHash(spID)

	sp := &entity.SharedPool{SharedPool: apiv1.SharedPool{
		Id: spID,
		ComponentGroups: []*entity.Component{{
			Id: defaultID, ComponentType: apiv1.ComponentType_TIKV,
			NodeQuota: &entity.NodeQuota{NodeSpec: &entity.NodeSpec{Cpu: 8, MemoryGi: 32}, NodeCount: 3},
		}},
	}}

	// Target: 8c16g — memory-only downgrade (same CPU).
	ruConfig := buildRuConfig(map[string]struct{ cpu, mem uint32 }{"tikv": {8, 16}})
	// ScaleOverride requires CPU change; same CPU → veto.
	veto := func(current, target *entity.NodeSpec) bool {
		return current.GetCpu() != target.GetCpu()
	}
	cfg := server.RcuScaleComponentConfig{
		ComponentType: apiv1.ComponentType_TIKV,
		Scope:         server.RcuScaleScopeSharedPool,
		ScaleOverride: veto,
	}

	var result []*entity.Component
	job := &RcuScaleDownJob{}
	job.collectSharedPoolDowngrades(context.Background(), sp, ruConfig, cfg, 1000, &result)

	assert.Empty(t, result, "ScaleOverride should veto memory-only downgrade")
}

func TestCheckAndAppendDowngrade_WithAndWithoutOverride(t *testing.T) {
	comp := &entity.Component{
		Id:            "cluster-a",
		ComponentType: apiv1.ComponentType_TIDB,
		NodeQuota: &entity.NodeQuota{
			NodeSpec: &entity.NodeSpec{Cpu: 16, MemoryGi: 64}, NodeCount: 2,
		},
	}
	targetSpec := &entity.NodeSpec{Cpu: 8, MemoryGi: 16}

	t.Run("no override — downgrade proceeds", func(t *testing.T) {
		cfg := server.RcuScaleComponentConfig{ComponentType: apiv1.ComponentType_TIDB, Scope: server.RcuScaleScopePerCluster}
		var result []*entity.Component
		job := &RcuScaleDownJob{}
		job.checkAndAppendDowngrade(context.Background(), "pool", "cluster-a", cfg, comp, targetSpec, 500, &result)

		require.Len(t, result, 1)
		assert.Equal(t, uint32(8), result[0].GetNodeQuota().GetNodeSpec().GetCpu())
	})

	t.Run("override vetoes", func(t *testing.T) {
		alwaysVeto := func(_, _ *entity.NodeSpec) bool { return false }
		cfg := server.RcuScaleComponentConfig{
			ComponentType: apiv1.ComponentType_TIDB,
			Scope:         server.RcuScaleScopePerCluster,
			ScaleOverride: alwaysVeto,
		}
		var result []*entity.Component
		job := &RcuScaleDownJob{}
		job.checkAndAppendDowngrade(context.Background(), "pool", "cluster-a", cfg, comp, targetSpec, 500, &result)

		assert.Empty(t, result)
	})

	t.Run("no downgrade needed — skip", func(t *testing.T) {
		sameSpec := &entity.NodeSpec{Cpu: 16, MemoryGi: 64}
		cfg := server.RcuScaleComponentConfig{ComponentType: apiv1.ComponentType_TIDB, Scope: server.RcuScaleScopePerCluster}
		var result []*entity.Component
		job := &RcuScaleDownJob{}
		job.checkAndAppendDowngrade(context.Background(), "pool", "cluster-a", cfg, comp, sameSpec, 500, &result)

		assert.Empty(t, result)
	})
}
