package server

import (
	"context"
	"errors"
	"testing"

	"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"
	schemav1 "github.com/tidbcloud/cluster-service-ng/pkg/storage/etcd/schema/v1"
	"github.com/tidbcloud/cluster-service-ng/pkg/storage/repo"
	repomnocks "github.com/tidbcloud/cluster-service-ng/pkg/storage/repo/mocks"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/proto"
)

func TestResolveSharedPoolRuConfigLegacyResolution(t *testing.T) {
	ctx := context.Background()
	tenantConfig := &entity.RuConfig{RuConfig: apiv1.RuConfig{
		FeatureFlags: &entity.RuConfigFeatureFlags{
			EnableScaleSharedPoolByRu:      proto.Bool(false),
			EnableAutoscaleTikvCpuScaleOut: proto.Bool(true),
			EnableTici:                     true,
			EnableResourceManager:          true,
		},
		Components: map[string]*entity.RuConfigComponentsMap{
			"tikv": {
				MaxNodeCount: 12,
			},
		},
	}}
	serverDefault := &entity.RuConfig{RuConfig: apiv1.RuConfig{
		FeatureFlags: &entity.RuConfigFeatureFlags{
			EnableScaleSharedPoolByRu:           proto.Bool(true),
			EnableScaleTidbGroupByRu:            proto.Bool(true),
			EnableAutoscaleTikvCpuScaleOut:      proto.Bool(false),
			EnableAutoscaleDiskExpand:           proto.Bool(true),
			EnableAutoscaleIopsThroughputExpand: proto.Bool(false),
		},
	}}

	ruConfigRepo := repomnocks.NewRuConfigRepo(t)
	ruConfigRepo.EXPECT().
		GetRuConfigWithFound(ctx, "tenant-1", apiv1.RuConfig_TENANT).
		Return(tenantConfig, true, nil)
	got, err := (&serverImpl{ruConfigRepo: ruConfigRepo}).
		resolveSharedPoolRuConfig(ctx, "tenant-1")

	require.NoError(t, err)
	require.Same(t, tenantConfig, got)
	require.True(t, serverDefault.GetFeatureFlags().GetEnableScaleTidbGroupByRu())
	require.False(t, got.GetFeatureFlags().GetEnableScaleTidbGroupByRu(), "disabled mode must not fill an unset tenant field from server defaults")
}

func TestResolveSharedPoolRuConfigBundleResolution(t *testing.T) {
	ctx := context.Background()
	tenantConfig := &entity.RuConfig{RuConfig: apiv1.RuConfig{
		FeatureFlags: &entity.RuConfigFeatureFlags{
			// The bundle value must win over this legacy autoscale value.
			EnableAutoscaleTikvCpuScaleOut: proto.Bool(true),
			EnableTici:                     true,
			EnableResourceManager:          true,
		},
		Components: map[string]*entity.RuConfigComponentsMap{
			"tikv": {
				MaxNodeCount: 15,
			},
		},
	}}
	serverDefault := &entity.RuConfig{RuConfig: apiv1.RuConfig{
		FeatureFlags: &entity.RuConfigFeatureFlags{
			EnableScaleSharedPoolByRu:           proto.Bool(false),
			EnableScaleTidbGroupByRu:            proto.Bool(false),
			EnableAutoscaleTikvCpuScaleOut:      proto.Bool(true),
			EnableAutoscaleDiskExpand:           proto.Bool(false),
			EnableAutoscaleIopsThroughputExpand: proto.Bool(false),
		},
	}}
	defaultRow := &repo.AutoscalePolicyBundleRow{
		Scope:     apiv1.AutoscalePolicyScope_SCOPE_DEFAULT,
		SubjectID: "__default__",
		Specs: []*entity.AutoscalePolicySpec{{
			TargetType: apiv1.AutoscaleTargetType_TARGET_TIKV,
			Direction:  apiv1.AutoscalePolicy_SCALE_OUT,
			Capability: apiv1.AutoscalePolicy_DISK_SIZE,
			Trigger:    apiv1.AutoscalePolicy_DISK,
			State:      apiv1.AutoscalePolicy_ENABLED,
		}},
	}
	tenantRow := &repo.AutoscalePolicyBundleRow{
		Scope:     apiv1.AutoscalePolicyScope_SCOPE_TENANT,
		SubjectID: "tenant-1",
		Specs: []*entity.AutoscalePolicySpec{
			{
				TargetType: apiv1.AutoscaleTargetType_TARGET_TIKV,
				Direction:  apiv1.AutoscalePolicy_SCALE_OUT,
				Capability: apiv1.AutoscalePolicy_NODE_COUNT,
				Trigger:    apiv1.AutoscalePolicy_CPU,
				State:      apiv1.AutoscalePolicy_DISABLED,
			},
			{
				TargetType: apiv1.AutoscaleTargetType_SHARED_POOL,
				Direction:  apiv1.AutoscalePolicy_SCALE_OUT,
				Capability: apiv1.AutoscalePolicy_NODE_SPEC,
				Trigger:    apiv1.AutoscalePolicy_RCU,
				State:      apiv1.AutoscalePolicy_ENABLED,
			},
		},
	}

	ruConfigRepo := repomnocks.NewRuConfigRepo(t)
	ruConfigRepo.EXPECT().
		GetRuConfigWithFound(ctx, "tenant-1", apiv1.RuConfig_TENANT).
		Return(tenantConfig, true, nil)
	ruConfigRepo.EXPECT().
		GetServerDefaultRuConfig(ctx).
		Return(serverDefault, nil)
	bundleRepo := repomnocks.NewAutoscalePolicyBundleRepo(t)
	bundleRepo.EXPECT().
		GetBundle(ctx, apiv1.AutoscalePolicyScope_SCOPE_DEFAULT, "__default__").
		Return(defaultRow, nil)
	bundleRepo.EXPECT().
		GetBundle(ctx, apiv1.AutoscalePolicyScope_SCOPE_TENANT, "tenant-1").
		Return(tenantRow, nil)

	got, err := (&serverImpl{
		ruConfigRepo:              ruConfigRepo,
		autoscalePolicyBundleRepo: bundleRepo,
		bizCfg: &config.BizConfig{AutoscalePolicy: config.AutoscalePolicyConfig{
			Mode:            config.AutoscalePolicyModeEnforce,
			AllowAllTenants: true,
		}},
	}).resolveSharedPoolRuConfig(ctx, "tenant-1")

	require.NoError(t, err)
	require.NotNil(t, got)
	require.NotNil(t, got.GetFeatureFlags())
	require.True(t, got.GetFeatureFlags().GetEnableScaleSharedPoolByRu())
	require.False(t, got.GetFeatureFlags().GetEnableScaleTidbGroupByRu())
	require.False(t, got.GetFeatureFlags().GetEnableAutoscaleTikvCpuScaleOut(), "tenant bundle must win over the legacy tenant RuConfig")
	require.True(t, got.GetFeatureFlags().GetEnableAutoscaleDiskExpand(), "default bundle must win over the server default")
	require.False(t, got.GetFeatureFlags().GetEnableAutoscaleIopsThroughputExpand())
	require.True(t, got.GetFeatureFlags().GetEnableTici())
	require.True(t, got.GetFeatureFlags().GetEnableResourceManager())
	require.Equal(t, tenantConfig.GetComponents(), got.GetComponents())
}

func TestResolveSharedPoolRuConfigDisabledUsesServerDefaultForMissingTenantRow(t *testing.T) {
	ctx := context.Background()
	serverDefault := &entity.RuConfig{RuConfig: apiv1.RuConfig{
		FeatureFlags: &entity.RuConfigFeatureFlags{
			EnableScaleSharedPoolByRu:           proto.Bool(true),
			EnableScaleTidbGroupByRu:            proto.Bool(false),
			EnableAutoscaleTikvCpuScaleOut:      proto.Bool(true),
			EnableAutoscaleDiskExpand:           proto.Bool(false),
			EnableAutoscaleIopsThroughputExpand: proto.Bool(true),
		},
	}}

	ruConfigRepo := repomnocks.NewRuConfigRepo(t)
	ruConfigRepo.EXPECT().
		GetRuConfigWithFound(ctx, "tenant-1", apiv1.RuConfig_TENANT).
		Return(serverDefault, false, nil)

	got, err := (&serverImpl{ruConfigRepo: ruConfigRepo}).
		resolveSharedPoolRuConfig(ctx, "tenant-1")

	require.NoError(t, err)
	require.Same(t, serverDefault, got)
}

func TestResolveSharedPoolRuConfigDisabledDoesNotFreezeYAMLTiProxyScaleIn(t *testing.T) {
	ctx := context.Background()
	yamlTrue := &entity.RuConfig{RuConfig: apiv1.RuConfig{
		FeatureFlags: &entity.RuConfigFeatureFlags{
			EnableAutoscaleTiproxyScaleIn: proto.Bool(true),
		},
	}}

	ruConfigRepo := repomnocks.NewRuConfigRepo(t)
	ruConfigRepo.EXPECT().
		GetRuConfigWithFound(ctx, "tenant-1", apiv1.RuConfig_TENANT).
		Return(yamlTrue, false, nil)

	got, err := (&serverImpl{ruConfigRepo: ruConfigRepo}).
		resolveSharedPoolRuConfig(ctx, "tenant-1")

	require.NoError(t, err)
	require.NotSame(t, yamlTrue, got, "must clone before clearing the YAML-sourced flag")
	require.Nil(t, got.GetFeatureFlags().EnableAutoscaleTiproxyScaleIn,
		"YAML enableAutoscaleTiproxyScaleIn must not freeze into the pool row")
	require.True(t, yamlTrue.GetFeatureFlags().GetEnableAutoscaleTiproxyScaleIn(),
		"must not mutate the shared YAML default")
}

func TestResolveSharedPoolRuConfigDisabledPreservesTenantTiProxyScaleInOverride(t *testing.T) {
	ctx := context.Background()
	tenantConfig := &entity.RuConfig{RuConfig: apiv1.RuConfig{
		FeatureFlags: &entity.RuConfigFeatureFlags{
			EnableAutoscaleTiproxyScaleIn: proto.Bool(true),
		},
	}}

	ruConfigRepo := repomnocks.NewRuConfigRepo(t)
	ruConfigRepo.EXPECT().
		GetRuConfigWithFound(ctx, "tenant-1", apiv1.RuConfig_TENANT).
		Return(tenantConfig, true, nil)

	got, err := (&serverImpl{ruConfigRepo: ruConfigRepo}).
		resolveSharedPoolRuConfig(ctx, "tenant-1")

	require.NoError(t, err)
	require.Same(t, tenantConfig, got)
	require.True(t, got.GetFeatureFlags().GetEnableAutoscaleTiproxyScaleIn())
}

func TestResolveSharedPoolRuConfigEnforceLeavesMultiTenantPoolOnLegacyPath(t *testing.T) {
	ctx := context.Background()
	legacyConfig := &entity.RuConfig{}
	ruConfigRepo := repomnocks.NewRuConfigRepo(t)
	ruConfigRepo.EXPECT().
		GetRuConfigWithFound(ctx, schemav1.MultiTenantTag, apiv1.RuConfig_TENANT).
		Return(legacyConfig, true, nil)
	bundleRepo := repomnocks.NewAutoscalePolicyBundleRepo(t)
	server := &serverImpl{
		ruConfigRepo:              ruConfigRepo,
		autoscalePolicyBundleRepo: bundleRepo,
		bizCfg: &config.BizConfig{AutoscalePolicy: config.AutoscalePolicyConfig{
			Mode:            config.AutoscalePolicyModeEnforce,
			AllowAllTenants: true,
		}},
	}

	got, err := server.resolveSharedPoolRuConfig(ctx, schemav1.MultiTenantTag)

	require.NoError(t, err)
	require.Same(t, legacyConfig, got)
}

func TestResolveSharedPoolRuConfigEnforceRequiresBundleRepo(t *testing.T) {
	ctx := context.Background()
	tenantConfig := &entity.RuConfig{}
	ruConfigRepo := repomnocks.NewRuConfigRepo(t)
	ruConfigRepo.EXPECT().
		GetRuConfigWithFound(ctx, "tenant-1", apiv1.RuConfig_TENANT).
		Return(tenantConfig, true, nil)

	server := &serverImpl{
		ruConfigRepo: ruConfigRepo,
		bizCfg: &config.BizConfig{AutoscalePolicy: config.AutoscalePolicyConfig{
			Mode:            config.AutoscalePolicyModeEnforce,
			AllowAllTenants: true,
		}},
	}

	got, err := server.resolveSharedPoolRuConfig(ctx, "tenant-1")
	require.Nil(t, got)
	require.Equal(t, codes.Internal, status.Code(err))
	require.Contains(t, err.Error(), "autoscale policy bundle repository is unavailable")
}

func TestResolveSharedPoolRuConfigErrors(t *testing.T) {
	ctx := context.Background()
	tenantConfig := &entity.RuConfig{}
	serverDefault := &entity.RuConfig{}

	tests := []struct {
		name       string
		setup      func(*repomnocks.RuConfigRepo, *repomnocks.AutoscalePolicyBundleRepo)
		withBundle bool
		wantError  string
	}{
		{
			name: "tenant RuConfig read fails",
			setup: func(ruConfigRepo *repomnocks.RuConfigRepo, _ *repomnocks.AutoscalePolicyBundleRepo) {
				ruConfigRepo.EXPECT().
					GetRuConfigWithFound(ctx, "tenant-1", apiv1.RuConfig_TENANT).
					Return(nil, false, errors.New("tenant config unavailable"))
			},
			wantError: "failed to get ru config: tenant config unavailable",
		},
		{
			name: "server default read fails",
			setup: func(ruConfigRepo *repomnocks.RuConfigRepo, _ *repomnocks.AutoscalePolicyBundleRepo) {
				ruConfigRepo.EXPECT().
					GetRuConfigWithFound(ctx, "tenant-1", apiv1.RuConfig_TENANT).
					Return(tenantConfig, true, nil)
				ruConfigRepo.EXPECT().
					GetServerDefaultRuConfig(ctx).
					Return(nil, errors.New("server default unavailable"))
			},
			withBundle: true,
			wantError:  "failed to get server default ru config: server default unavailable",
		},
		{
			name: "default bundle read fails",
			setup: func(ruConfigRepo *repomnocks.RuConfigRepo, bundleRepo *repomnocks.AutoscalePolicyBundleRepo) {
				ruConfigRepo.EXPECT().
					GetRuConfigWithFound(ctx, "tenant-1", apiv1.RuConfig_TENANT).
					Return(tenantConfig, true, nil)
				ruConfigRepo.EXPECT().
					GetServerDefaultRuConfig(ctx).
					Return(serverDefault, nil)
				bundleRepo.EXPECT().
					GetBundle(ctx, apiv1.AutoscalePolicyScope_SCOPE_DEFAULT, "__default__").
					Return(nil, errors.New("bundle unavailable"))
			},
			withBundle: true,
			wantError:  "failed to resolve shared-pool ru config from bundle table: load DEFAULT bundle row: bundle unavailable",
		},
		{
			name: "tenant bundle read fails",
			setup: func(ruConfigRepo *repomnocks.RuConfigRepo, bundleRepo *repomnocks.AutoscalePolicyBundleRepo) {
				ruConfigRepo.EXPECT().
					GetRuConfigWithFound(ctx, "tenant-1", apiv1.RuConfig_TENANT).
					Return(tenantConfig, true, nil)
				ruConfigRepo.EXPECT().
					GetServerDefaultRuConfig(ctx).
					Return(serverDefault, nil)
				bundleRepo.EXPECT().
					GetBundle(ctx, apiv1.AutoscalePolicyScope_SCOPE_DEFAULT, "__default__").
					Return(nil, nil)
				bundleRepo.EXPECT().
					GetBundle(ctx, apiv1.AutoscalePolicyScope_SCOPE_TENANT, "tenant-1").
					Return(nil, errors.New("bundle unavailable"))
			},
			withBundle: true,
			wantError:  "failed to resolve shared-pool ru config from bundle table: load TENANT bundle row for tenant tenant-1: bundle unavailable",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ruConfigRepo := repomnocks.NewRuConfigRepo(t)
			bundleRepo := repomnocks.NewAutoscalePolicyBundleRepo(t)
			tt.setup(ruConfigRepo, bundleRepo)
			server := &serverImpl{ruConfigRepo: ruConfigRepo}
			if tt.withBundle {
				server.autoscalePolicyBundleRepo = bundleRepo
				server.bizCfg = &config.BizConfig{AutoscalePolicy: config.AutoscalePolicyConfig{
					Mode:            config.AutoscalePolicyModeEnforce,
					AllowAllTenants: true,
				}}
			}

			got, err := server.resolveSharedPoolRuConfig(ctx, "tenant-1")

			require.Nil(t, got)
			require.Equal(t, codes.Internal, status.Code(err))
			require.Contains(t, err.Error(), tt.wantError)
		})
	}
}
