package grpc_service

import (
	"context"
	"strconv"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/config-center/internal/db"
	"github.com/tidbcloud/config-center/internal/service"
	pb "github.com/tidbcloud/config-center/proto/config_center"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/structpb"
	"k8s.io/utils/ptr"
)

func TestDeleteClusterMetaSoftDeletesAndIsIdempotent(t *testing.T) {
	assert := assert.New(t)
	c := newClusterMetaTestService(t)

	seedClusterMeta(t, c, "tidb123123")

	meta, err := c.GetClusterMeta(context.TODO(), &pb.GetClusterMetaRequest{
		ClusterType:   pb.ClusterType_DEDICATED_TIDB,
		ClusterId:     "tidb123123",
		MainComponent: "tidb",
	})
	assert.Nil(err)
	assert.Len(meta.GetComponents(), 1)
	assert.Len(meta.GetConfigurationFiles(), 1)

	_, err = c.DeleteClusterMeta(context.TODO(), &pb.DeleteClusterMetaRequest{
		ClusterType: pb.ClusterType_DEDICATED_TIDB,
		ClusterId:   "tidb123123",
	})
	assert.Nil(err)

	_, err = c.GetClusterMeta(context.TODO(), &pb.GetClusterMetaRequest{
		ClusterType:   pb.ClusterType_DEDICATED_TIDB,
		ClusterId:     "tidb123123",
		MainComponent: "tidb",
	})
	assert.Equal(codes.NotFound, status.Code(err))

	_, err = c.DeleteClusterMeta(context.TODO(), &pb.DeleteClusterMetaRequest{
		ClusterType: pb.ClusterType_DEDICATED_TIDB,
		ClusterId:   "tidb123123",
	})
	assert.Nil(err)

	_, err = c.DeleteClusterMeta(context.TODO(), &pb.DeleteClusterMetaRequest{
		ClusterType: pb.ClusterType_DEDICATED_TIDB,
		ClusterId:   "missing-cluster",
	})
	assert.Nil(err)
}

func TestBatchDeleteClusterMetaSoftDeletesAndIsIdempotent(t *testing.T) {
	assert := assert.New(t)
	c := newClusterMetaTestService(t)

	seedClusterMeta(t, c, "tidb123123")
	seedClusterMeta(t, c, "tidb456456")

	_, err := c.BatchDeleteClusterMeta(context.TODO(), &pb.BatchDeleteClusterMetaRequest{
		ClusterType: pb.ClusterType_DEDICATED_TIDB,
		ClusterIds:  []string{"tidb123123", "tidb456456", "missing-cluster"},
	})
	assert.Nil(err)

	for _, clusterID := range []string{"tidb123123", "tidb456456"} {
		_, err = c.GetClusterMeta(context.TODO(), &pb.GetClusterMetaRequest{
			ClusterType:   pb.ClusterType_DEDICATED_TIDB,
			ClusterId:     clusterID,
			MainComponent: "tidb",
		})
		assert.Equal(codes.NotFound, status.Code(err))
	}

	_, err = c.BatchDeleteClusterMeta(context.TODO(), &pb.BatchDeleteClusterMetaRequest{
		ClusterType: pb.ClusterType_DEDICATED_TIDB,
		ClusterIds:  []string{"tidb123123", "tidb456456", "missing-cluster"},
	})
	assert.Nil(err)
}

func TestInitClusterMetaAppliesMetaProfileComponentVersion(t *testing.T) {
	c := newClusterMetaTestService(t)
	ctx := context.TODO()

	_, err := c.UpdateComponentImage(ctx, &pb.UpdateComponentImageRequest{
		ClusterType:      pb.ClusterType_DEDICATED_TIDB,
		ComponentName:    "tidb",
		ComponentVersion: "v8.5.4",
		BaseImage:        "docker.io/pingcap/tidb",
		Tag:              "v8.5.4-default",
	})
	require.NoError(t, err)

	profile, err := c.CreateMetaProfile(ctx, &pb.CreateMetaProfileRequest{
		ClusterType: pb.ClusterType_DEDICATED_TIDB,
		ProfileName: "v8.5.4 profile",
		Description: "test profile",
		ComponentVersions: map[string]string{
			"tidb": "v8.5.4",
		},
	})
	require.NoError(t, err)

	meta, err := c.InitClusterMeta(ctx, &pb.InitClusterMetaRequest{
		ClusterType:   pb.ClusterType_DEDICATED_TIDB,
		ClusterId:     "profile-version-only",
		MetaProfileId: ptr.To(profile.GetProfileId()),
		Components: []*pb.CreateClusterComponentRequest{
			{
				ComponentName: "tidb",
				Version:       "v7.5.0",
				CloudProvider: "aws",
			},
		},
	})
	require.NoError(t, err)
	require.Len(t, meta.GetComponents(), 1)
	assert.Equal(t, "v8.5.4", meta.GetComponents()[0].GetVersion())
	assert.Equal(t, "v8.5.4-default", meta.GetComponents()[0].GetEffectiveImageTag())
	assert.Empty(t, meta.GetComponents()[0].GetOverwriteImageTag())
}

func TestInitClusterMetaAppliesMetaProfileComponentVersionAndImageTag(t *testing.T) {
	c := newClusterMetaTestService(t)
	ctx := context.TODO()

	_, err := c.UpdateComponentImage(ctx, &pb.UpdateComponentImageRequest{
		ClusterType:      pb.ClusterType_DEDICATED_TIDB,
		ComponentName:    "tidb",
		ComponentVersion: "v8.5.4",
		BaseImage:        "docker.io/pingcap/tidb",
		Tag:              "v8.5.4-default",
	})
	require.NoError(t, err)

	profile, err := c.CreateMetaProfile(ctx, &pb.CreateMetaProfileRequest{
		ClusterType: pb.ClusterType_DEDICATED_TIDB,
		ProfileName: "v8.5.4 hotfix profile",
		Description: "test profile",
		ComponentVersions: map[string]string{
			"tidb": "v8.5.4",
		},
		ComponentImageTags: map[string]string{
			"tidb": "tidb-hotfix",
		},
	})
	require.NoError(t, err)

	meta, err := c.InitClusterMeta(ctx, &pb.InitClusterMetaRequest{
		ClusterType:   pb.ClusterType_DEDICATED_TIDB,
		ClusterId:     "profile-version-and-tag",
		MetaProfileId: ptr.To(profile.GetProfileId()),
		Components: []*pb.CreateClusterComponentRequest{
			{
				ComponentName: "tidb",
				Version:       "v7.5.0",
				CloudProvider: "aws",
			},
		},
	})
	require.NoError(t, err)
	require.Len(t, meta.GetComponents(), 1)
	assert.Equal(t, "v8.5.4", meta.GetComponents()[0].GetVersion())
	assert.Equal(t, "tidb-hotfix", meta.GetComponents()[0].GetEffectiveImageTag())
	assert.Equal(t, "tidb-hotfix", meta.GetComponents()[0].GetOverwriteImageTag())
}

func TestInitClusterMetaAppliesMetaProfileComponentVersionToSystemTemplate(t *testing.T) {
	c := newClusterMetaTestService(t)
	ctx := context.TODO()
	const tenantID = uint64(123)

	oldTemplate := createClusterMetaTestTemplate(t, c, pb.ClusterType_SHARED_POOL, pb.SHARED_POOL_CONFIG_TIDB, "old")
	newTemplate := createClusterMetaTestTemplate(t, c, pb.ClusterType_SHARED_POOL, pb.SHARED_POOL_CONFIG_TIDB, "new")
	updateClusterMetaTestDefaultTemplate(t, c, pb.ClusterType_SHARED_POOL, pb.SHARED_POOL_CONFIG_TIDB, "v7.5.0", tenantID, oldTemplate.GetTemplateId())
	updateClusterMetaTestDefaultTemplate(t, c, pb.ClusterType_SHARED_POOL, pb.SHARED_POOL_CONFIG_TIDB, "v8.5.4", tenantID, newTemplate.GetTemplateId())

	profile, err := c.CreateMetaProfile(ctx, &pb.CreateMetaProfileRequest{
		ClusterType: pb.ClusterType_SHARED_POOL,
		ProfileName: "v8.5.4 config profile",
		Description: "test profile",
		ComponentVersions: map[string]string{
			"tidb": "v8.5.4",
		},
	})
	require.NoError(t, err)

	meta, err := c.InitClusterMeta(ctx, &pb.InitClusterMetaRequest{
		ClusterType:   pb.ClusterType_SHARED_POOL,
		ClusterId:     "profile-config-version",
		MetaProfileId: ptr.To(profile.GetProfileId()),
		ConfigurationFiles: []*pb.CreateClusterConfigFileRequest{
			{
				ConfigurationFilename: pb.SHARED_POOL_CONFIG_TIDB,
				ConfigurationType:     pb.SHARED_POOL_CONFIG_TIDB,
				BelongToComponent:     ptr.To("tidb"),
				Format:                pb.Format_TOML,
				SystemTemplateSource: &pb.CreateClusterConfigFileRequest_FromSku{
					FromSku: &pb.FromSKU{
						Version:  "v7.5.0",
						TenantId: tenantID,
					},
				},
			},
		},
	})
	require.NoError(t, err)
	require.Len(t, meta.GetConfigurationFiles(), 1)
	assert.Equal(t, newTemplate.GetTemplateId(), meta.GetConfigurationFiles()[0].GetTemplateId())
	assert.Equal(t, "test = \"new\"\n", meta.GetConfigurationFiles()[0].GetText())
}

func TestInitClusterMetaKeepsSystemTemplateSourceWithoutMatchingMetaProfileVersion(t *testing.T) {
	c := newClusterMetaTestService(t)
	ctx := context.TODO()
	const tenantID = uint64(123)

	oldTemplate := createClusterMetaTestTemplate(t, c, pb.ClusterType_SHARED_POOL, pb.SHARED_POOL_CONFIG_TIDB, "old")
	newTemplate := createClusterMetaTestTemplate(t, c, pb.ClusterType_SHARED_POOL, pb.SHARED_POOL_CONFIG_TIDB, "new")
	updateClusterMetaTestDefaultTemplate(t, c, pb.ClusterType_SHARED_POOL, pb.SHARED_POOL_CONFIG_TIDB, "v8.5.4", tenantID, newTemplate.GetTemplateId())

	profile, err := c.CreateMetaProfile(ctx, &pb.CreateMetaProfileRequest{
		ClusterType: pb.ClusterType_SHARED_POOL,
		ProfileName: "tikv-only config profile",
		Description: "test profile",
		ComponentVersions: map[string]string{
			"tikv": "v8.5.4",
		},
	})
	require.NoError(t, err)

	meta, err := c.InitClusterMeta(ctx, &pb.InitClusterMetaRequest{
		ClusterType:   pb.ClusterType_SHARED_POOL,
		ClusterId:     "profile-config-no-matching-version",
		MetaProfileId: ptr.To(profile.GetProfileId()),
		ConfigurationFiles: []*pb.CreateClusterConfigFileRequest{
			{
				ConfigurationFilename: pb.SHARED_POOL_CONFIG_TIDB,
				ConfigurationType:     pb.SHARED_POOL_CONFIG_TIDB,
				Format:                pb.Format_TOML,
				SystemTemplateSource: &pb.CreateClusterConfigFileRequest_FromTemplate{
					FromTemplate: &pb.FromTemplate{
						TemplateId: oldTemplate.GetTemplateId(),
					},
				},
			},
		},
	})
	require.NoError(t, err)
	require.Len(t, meta.GetConfigurationFiles(), 1)
	assert.Equal(t, oldTemplate.GetTemplateId(), meta.GetConfigurationFiles()[0].GetTemplateId())
	assert.Equal(t, "test = \"old\"\n", meta.GetConfigurationFiles()[0].GetText())
}

func TestInitClusterMetaAppliesTidbMetaProfileVersionToSystemTemplatesWithoutComponent(t *testing.T) {
	c := newClusterMetaTestService(t)
	ctx := context.TODO()
	const tenantID = uint64(123)

	oldSemTemplate := createClusterMetaTestTemplate(t, c, pb.ClusterType_SHARED_POOL, pb.SHARED_POOL_CONFIG_TIDB_SEM, "old")
	newSemTemplate := createClusterMetaTestTemplate(t, c, pb.ClusterType_SHARED_POOL, pb.SHARED_POOL_CONFIG_TIDB_SEM, "new")
	updateClusterMetaTestDefaultTemplate(t, c, pb.ClusterType_SHARED_POOL, pb.SHARED_POOL_CONFIG_TIDB_SEM, "v7.5.0", tenantID, oldSemTemplate.GetTemplateId())
	updateClusterMetaTestDefaultTemplate(t, c, pb.ClusterType_SHARED_POOL, pb.SHARED_POOL_CONFIG_TIDB_SEM, "v8.5.4", tenantID, newSemTemplate.GetTemplateId())

	oldInitSQLTemplate := createClusterMetaTestLinesTemplate(t, c, pb.ClusterType_SHARED_POOL, pb.SHARED_POOL_CONFIG_INITSQL, "select 'old';")
	newInitSQLTemplate := createClusterMetaTestLinesTemplate(t, c, pb.ClusterType_SHARED_POOL, pb.SHARED_POOL_CONFIG_INITSQL, "select 'new';")
	updateClusterMetaTestDefaultTemplate(t, c, pb.ClusterType_SHARED_POOL, pb.SHARED_POOL_CONFIG_INITSQL, "v7.5.0", tenantID, oldInitSQLTemplate.GetTemplateId())
	updateClusterMetaTestDefaultTemplate(t, c, pb.ClusterType_SHARED_POOL, pb.SHARED_POOL_CONFIG_INITSQL, "v8.5.4", tenantID, newInitSQLTemplate.GetTemplateId())

	profile, err := c.CreateMetaProfile(ctx, &pb.CreateMetaProfileRequest{
		ClusterType: pb.ClusterType_SHARED_POOL,
		ProfileName: "shared pool v8.5.4 config profile",
		Description: "test profile",
		ComponentVersions: map[string]string{
			"tidb": "v8.5.4",
		},
	})
	require.NoError(t, err)

	meta, err := c.InitClusterMeta(ctx, &pb.InitClusterMetaRequest{
		ClusterType:   pb.ClusterType_SHARED_POOL,
		ClusterId:     "profile-config-version-without-component",
		MetaProfileId: ptr.To(profile.GetProfileId()),
		ConfigurationFiles: []*pb.CreateClusterConfigFileRequest{
			{
				ConfigurationFilename: pb.SHARED_POOL_CONFIG_TIDB_SEM,
				ConfigurationType:     pb.SHARED_POOL_CONFIG_TIDB_SEM,
				Format:                pb.Format_JSON,
				SystemTemplateSource: &pb.CreateClusterConfigFileRequest_FromSku{
					FromSku: &pb.FromSKU{
						Version:  "v7.5.0",
						TenantId: tenantID,
					},
				},
			},
			{
				ConfigurationFilename: pb.SHARED_POOL_CONFIG_INITSQL,
				ConfigurationType:     pb.SHARED_POOL_CONFIG_INITSQL,
				Format:                pb.Format_LINES,
				SystemTemplateSource: &pb.CreateClusterConfigFileRequest_FromSku{
					FromSku: &pb.FromSKU{
						Version:  "v7.5.0",
						TenantId: tenantID,
					},
				},
			},
		},
	})
	require.NoError(t, err)
	require.Len(t, meta.GetConfigurationFiles(), 2)
	assert.Equal(t, newSemTemplate.GetTemplateId(), meta.GetConfigurationFiles()[0].GetTemplateId())
	assert.Equal(t, "{\"test\":\"new\"}", meta.GetConfigurationFiles()[0].GetText())
	assert.Equal(t, newInitSQLTemplate.GetTemplateId(), meta.GetConfigurationFiles()[1].GetTemplateId())
	assert.Equal(t, "select 'new';\n", meta.GetConfigurationFiles()[1].GetText())
}

func newClusterMetaTestService(t *testing.T) *ConfigCenterService {
	t.Helper()

	mockDB := db.NewMockDB()
	c := &ConfigCenterService{
		DB: mockDB,
		UnifiedService: &service.ConfigCenterService{
			DB: mockDB,
		},
	}

	_, err := c.UpdateComponentImage(context.TODO(), &pb.UpdateComponentImageRequest{
		ClusterType:      pb.ClusterType_DEDICATED_TIDB,
		ComponentName:    "tidb",
		ComponentVersion: "v7.5.0",
		BaseImage:        "docker.io/pingcap/tidb",
		Tag:              "v7.5.0",
	})
	assert.NoError(t, err)

	return c
}

func createClusterMetaTestTemplate(t *testing.T, c *ConfigCenterService, clusterType pb.ClusterType, configType string, value string) *pb.ConfigurationTemplate {
	t.Helper()

	template, err := c.CreateConfigurationTemplate(context.TODO(), &pb.CreateConfigurationTemplateRequest{
		ClusterType:       clusterType,
		ConfigurationType: configType,
		TemplateName:      configType + "-" + value,
		Data: map[string]*pb.ParamItem{
			"test": {
				Value: structpb.NewStringValue(value),
			},
		},
	})
	require.NoError(t, err)
	return template
}

func createClusterMetaTestLinesTemplate(t *testing.T, c *ConfigCenterService, clusterType pb.ClusterType, configType string, line string) *pb.ConfigurationTemplate {
	t.Helper()

	template, err := c.CreateConfigurationTemplate(context.TODO(), &pb.CreateConfigurationTemplateRequest{
		ClusterType:       clusterType,
		ConfigurationType: configType,
		TemplateName:      configType + "-" + line,
		Data: map[string]*pb.ParamItem{
			"0": {
				Value: structpb.NewStringValue(line),
			},
		},
	})
	require.NoError(t, err)
	return template
}

func updateClusterMetaTestDefaultTemplate(t *testing.T, c *ConfigCenterService, clusterType pb.ClusterType, configType string, version string, tenantID uint64, templateID int64) {
	t.Helper()

	_, err := c.UpdateDefaultConfigurationTemplate(context.TODO(), &pb.UpdateDefaultConfigurationTemplateRequest{
		ClusterType:       clusterType,
		ConfigurationType: configType,
		Version:           version,
		TenantId:          strconv.FormatUint(tenantID, 10),
		TemplateId:        templateID,
	})
	require.NoError(t, err)
}

func seedClusterMeta(t *testing.T, c *ConfigCenterService, clusterID string) {
	t.Helper()

	_, err := c.UpdateClusterComponent(context.TODO(), &pb.UpdateClusterComponentRequest{
		ClusterType:   pb.ClusterType_DEDICATED_TIDB,
		ClusterId:     clusterID,
		ComponentName: "tidb",
		CloudProvider: ptr.To("aws"),
		Version:       ptr.To("v7.5.0"),
		AllowMissing:  true,
	})
	assert.NoError(t, err)

	_, err = c.CreateClusterConfigurationFile(context.TODO(), &pb.CreateClusterConfigurationFileRequest{
		ClusterType: pb.ClusterType_DEDICATED_TIDB,
		ClusterId:   clusterID,
		ConfigurationFile: &pb.ConfigurationFile{
			ConfigurationFilename: "tidb",
			ConfigurationType:     "tidb",
			BelongToComponent:     "tidb",
			Format:                pb.Format_TOML,
			TemplateId:            0,
			CustomParams: map[string]*pb.ParamItem{
				"log.level": {
					Value: structpb.NewStringValue("info"),
				},
			},
		},
	})
	assert.NoError(t, err)
}
