package service

import (
	"testing"

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

func setupTestData(mockDB db.DB) {
	// Add test component image data
	mockDB.InsertComponentImage("DEDICATED_TIDB", "tidb", "v4.0.0", "docker.io/pingcap/tidb", "v4.0.0-123", 0)
	mockDB.InsertComponentImage("DEDICATED_TIDB", "tikv", "v4.0.0", "docker.io/pingcap/tikv", "v4.0.0-456", 0)
	mockDB.InsertComponentImage("DEDICATED_TIDB", "tidb", "v3.0.0", "docker.io/pingcap/tidb", "v3.0.0-old", 0)
	mockDB.InsertComponentImage("DEDICATED_TIDB", "tikv", "v3.0.0", "docker.io/pingcap/tikv", "v3.0.0-old", 0)

	// Add existing cluster component for update tests
	mockDB.InsertClusterComponent("DEDICATED_TIDB", "existing-cluster", "tidb", "v3.0.0", "aws", "c5.large", "", "docker.io/pingcap/tidb", "v3.0.0-old", 0)
}

func TestUpdateClusterComponent_CreateNewComponent(t *testing.T) {
	assert := assert.New(t)
	require := require.New(t)

	mockDB := db.NewMockDB()
	setupTestData(mockDB)

	// Test: Create new component when AllowMissing is true
	req := &pb.UpdateClusterComponentRequest{
		ClusterType:   pb.ClusterType_DEDICATED_TIDB,
		ClusterId:     "new-cluster",
		ComponentName: "tidb",
		Version:       ptr.To("v4.0.0"),
		CloudProvider: ptr.To("aws"),
		NodeInstance:  ptr.To("c5.large"),
		AllowMissing:  true,
	}

	component, err := UpdateClusterComponent(mockDB, req)
	require.Nil(err)
	require.NotNil(component)

	// Verify the created component
	assert.Equal(pb.ClusterType_DEDICATED_TIDB, component.ClusterType)
	assert.Equal("new-cluster", component.ClusterId)
	assert.Equal("tidb", component.ComponentName)
	assert.Equal("v4.0.0", component.Version)
	assert.Equal("aws", component.CloudProvider)
	assert.Equal("c5.large", component.NodeInstance)
	assert.Equal("docker.io/pingcap/tidb", component.BaseImage)
	assert.Equal("v4.0.0-123", component.EffectiveImageTag)
	assert.Equal("", component.OverwriteImageTag)
	assert.Equal(pb.UpdateStrategy_UPDATE_STRATEGY_UNSPECIFIED, component.UpdateStrategy)

	// Verify component was actually inserted in DB
	stored, err := mockDB.GetClusterComponent("DEDICATED_TIDB", "new-cluster", "tidb")
	require.Nil(err)
	assert.Equal("v4.0.0", stored.Version)
	assert.Equal("aws", stored.CloudProvider)
}

func TestSearchClusterComponents(t *testing.T) {
	assert := assert.New(t)
	require := require.New(t)

	mockDB := db.NewMockDB()
	setupTestData(mockDB)
	require.Nil(mockDB.InsertClusterComponent("DEDICATED_TIDB", "cluster-a", "tidb", "v4.0.0", "aws", "c5.large", "", "docker.io/pingcap/tidb", "v4.0.0-123", 0))
	require.Nil(mockDB.InsertClusterComponent("DEDICATED_TIDB", "cluster-b", "tidb", "v4.0.0", "aws", "c5.large", "custom-tag", "docker.io/pingcap/tidb", "custom-tag", 0))
	require.Nil(mockDB.InsertClusterComponent("DEDICATED_TIDB", "cluster-b", "tikv", "v4.0.0", "aws", "c5.large", "", "docker.io/pingcap/tikv", "v4.0.0-456", 0))

	resp, err := SearchClusterComponents(mockDB, &pb.SearchClusterComponentsRequest{
		ClusterType:      pb.ClusterType_DEDICATED_TIDB,
		ComponentName:    "tidb",
		ComponentVersion: "v4.0.0",
		PageSize:         1,
		Page:             1,
	})
	require.Nil(err)
	require.NotNil(resp)
	assert.Equal(int64(2), resp.Total)
	require.Len(resp.Components, 1)
	assert.Equal("tidb", resp.Components[0].ComponentName)
	assert.Equal("v4.0.0", resp.Components[0].Version)
	assert.NotEmpty(resp.Components[0].ClusterId)

	resp, err = SearchClusterComponents(mockDB, &pb.SearchClusterComponentsRequest{
		ClusterType: pb.ClusterType_DEDICATED_TIDB,
		ClusterIds:  []string{"cluster-b"},
		ImageTag:    "custom-tag",
	})
	require.Nil(err)
	require.NotNil(resp)
	assert.Equal(int64(1), resp.Total)
	require.Len(resp.Components, 1)
	assert.Equal("cluster-b", resp.Components[0].ClusterId)
	assert.Equal("custom-tag", resp.Components[0].EffectiveImageTag)
	assert.Equal("custom-tag", resp.Components[0].OverwriteImageTag)

	resp, err = SearchClusterComponents(mockDB, &pb.SearchClusterComponentsRequest{
		ClusterType:      pb.ClusterType_DEDICATED_TIDB,
		ClusterIds:       []string{"cluster-a", "cluster-b"},
		ComponentVersion: "v4.0.0",
	})
	require.Nil(err)
	require.NotNil(resp)
	assert.Equal(int64(3), resp.Total)
	require.Len(resp.Components, 3)
}

func TestSearchClusterComponentsValidation(t *testing.T) {
	assert := assert.New(t)
	mockDB := db.NewMockDB()

	resp, err := SearchClusterComponents(mockDB, &pb.SearchClusterComponentsRequest{
		ClusterIds: []string{"cluster-a"},
	})
	assert.Nil(resp)
	assert.NotNil(err)
	assert.Equal(codes.InvalidArgument, status.Code(err))

	resp, err = SearchClusterComponents(mockDB, &pb.SearchClusterComponentsRequest{
		ClusterType: pb.ClusterType_DEDICATED_TIDB,
	})
	assert.Nil(resp)
	assert.NotNil(err)
	assert.Equal(codes.InvalidArgument, status.Code(err))
}

func TestUpdateClusterComponent_ComponentNotFound(t *testing.T) {
	assert := assert.New(t)
	mockDB := db.NewMockDB()
	setupTestData(mockDB)

	// Test: Component doesn't exist and AllowMissing is false
	req := &pb.UpdateClusterComponentRequest{
		ClusterType:   pb.ClusterType_DEDICATED_TIDB,
		ClusterId:     "nonexistent-cluster",
		ComponentName: "tidb",
		Version:       ptr.To("v4.0.0"),
		AllowMissing:  false,
	}

	component, err := UpdateClusterComponent(mockDB, req)
	assert.Nil(component)
	assert.NotNil(err)
	assert.Equal(codes.NotFound, status.Code(err))
	assert.Contains(err.Error(), "component tidb not found")
}

func TestUpdateClusterComponent_MissingVersionOnCreate(t *testing.T) {
	assert := assert.New(t)
	mockDB := db.NewMockDB()
	setupTestData(mockDB)

	// Test: Component doesn't exist, AllowMissing is true but version is missing
	req := &pb.UpdateClusterComponentRequest{
		ClusterType:   pb.ClusterType_DEDICATED_TIDB,
		ClusterId:     "new-cluster",
		ComponentName: "tidb",
		CloudProvider: ptr.To("aws"),
		AllowMissing:  true,
		// Version is nil
	}

	component, err := UpdateClusterComponent(mockDB, req)
	assert.Nil(component)
	assert.NotNil(err)
	assert.Equal(codes.InvalidArgument, status.Code(err))
	assert.Contains(err.Error(), "version is required for missing component")
}

func TestUpdateClusterComponent_UpdateExistingComponent(t *testing.T) {
	assert := assert.New(t)
	require := require.New(t)

	mockDB := db.NewMockDB()
	setupTestData(mockDB)

	// Test: Update existing component
	req := &pb.UpdateClusterComponentRequest{
		ClusterType:    pb.ClusterType_DEDICATED_TIDB,
		ClusterId:      "existing-cluster",
		ComponentName:  "tidb",
		Version:        ptr.To("v4.0.0"),
		CloudProvider:  ptr.To("gcp"),
		NodeInstance:   ptr.To("n1-standard-4"),
		UpdateStrategy: ptr.To(pb.UpdateStrategy_UPDATE_ALL),
	}

	component, err := UpdateClusterComponent(mockDB, req)
	require.Nil(err)
	require.NotNil(component)

	// Verify the updated component
	assert.Equal(pb.ClusterType_DEDICATED_TIDB, component.ClusterType)
	assert.Equal("tidb", component.ComponentName)
	assert.Equal("v4.0.0", component.Version)
	assert.Equal("gcp", component.CloudProvider)
	assert.Equal("n1-standard-4", component.NodeInstance)
	assert.Equal("docker.io/pingcap/tidb", component.BaseImage)
	assert.Equal("v4.0.0-123", component.EffectiveImageTag)
	assert.Equal(pb.UpdateStrategy_UPDATE_ALL, component.UpdateStrategy)

	// Verify component was actually updated in DB
	stored, err := mockDB.GetClusterComponent("DEDICATED_TIDB", "existing-cluster", "tidb")
	require.Nil(err)
	assert.Equal("v4.0.0", stored.Version)
	assert.Equal("gcp", stored.CloudProvider)
	assert.Equal("n1-standard-4", stored.NodeInstance)
}

func TestUpdateClusterComponent_ValidateOnly(t *testing.T) {
	assert := assert.New(t)
	require := require.New(t)

	mockDB := db.NewMockDB()
	setupTestData(mockDB)

	// Test: ValidateOnly mode should return expected component without persisting
	req := &pb.UpdateClusterComponentRequest{
		ClusterType:   pb.ClusterType_DEDICATED_TIDB,
		ClusterId:     "validate-cluster",
		ComponentName: "tidb",
		Version:       ptr.To("v4.0.0"),
		CloudProvider: ptr.To("aws"),
		AllowMissing:  true,
		ValidateOnly:  true,
	}

	component, err := UpdateClusterComponent(mockDB, req)
	require.Nil(err)
	require.NotNil(component)

	// Verify the expected component structure
	assert.Equal(pb.ClusterType_DEDICATED_TIDB, component.ClusterType)
	assert.Equal("tidb", component.ComponentName)
	assert.Equal("v4.0.0", component.Version)
	assert.Equal("aws", component.CloudProvider)
	assert.Equal("docker.io/pingcap/tidb", component.BaseImage)
	assert.Equal("v4.0.0-123", component.EffectiveImageTag)

	// Verify component was NOT persisted in DB
	_, err = mockDB.GetClusterComponent("DEDICATED_TIDB", "validate-cluster", "tidb")
	assert.Equal(db.ErrNotFound, err)
}

func TestUpdateClusterComponent_OverwriteImageTag(t *testing.T) {
	assert := assert.New(t)
	require := require.New(t)

	mockDB := db.NewMockDB()
	setupTestData(mockDB)

	// Test: OverwriteImageTag should have priority over default image tag
	req := &pb.UpdateClusterComponentRequest{
		ClusterType:       pb.ClusterType_DEDICATED_TIDB,
		ClusterId:         "existing-cluster",
		ComponentName:     "tidb",
		OverwriteImageTag: ptr.To("custom-tag-v1.2.3"),
	}

	component, err := UpdateClusterComponent(mockDB, req)
	require.Nil(err)
	require.NotNil(component)

	// Verify overwrite image tag is used
	assert.Equal("custom-tag-v1.2.3", component.EffectiveImageTag)
	assert.Equal("custom-tag-v1.2.3", component.OverwriteImageTag)

	// Verify component was updated in DB
	stored, err := mockDB.GetClusterComponent("DEDICATED_TIDB", "existing-cluster", "tidb")
	require.Nil(err)
	assert.Equal("custom-tag-v1.2.3", stored.OverwriteImageTag)
	assert.Equal("custom-tag-v1.2.3", stored.ImageTag)
}

func TestUpdateClusterComponent_PartialUpdate(t *testing.T) {
	assert := assert.New(t)
	require := require.New(t)

	mockDB := db.NewMockDB()
	setupTestData(mockDB)
	mockDB.UpdateComponentImage("DEDICATED_TIDB", "tidb", "v3.0.0", "docker.io/pingcap/tidb", "v3.0.0-new", 0)

	// Test: Update only some fields, others should remain unchanged
	req := &pb.UpdateClusterComponentRequest{
		ClusterType:   pb.ClusterType_DEDICATED_TIDB,
		ClusterId:     "existing-cluster",
		ComponentName: "tidb",
		NodeInstance:  ptr.To("c5.xlarge"),
		// CloudProvider and Version are not provided
	}

	component, err := UpdateClusterComponent(mockDB, req)
	require.Nil(err)
	require.NotNil(component)

	// Verify updated field
	assert.Equal("c5.xlarge", component.NodeInstance)

	// Verify unchanged fields (from original setup)
	assert.Equal("aws", component.CloudProvider)
	assert.Equal("v3.0.0", component.Version) // Should remain the original version
	assert.Equal("docker.io/pingcap/tidb", component.BaseImage)
	assert.Equal("v3.0.0-old", component.EffectiveImageTag)
}

func TestUpdateClusterComponent_MissingImageInfo(t *testing.T) {
	assert := assert.New(t)
	mockDB := db.NewMockDB()
	setupTestData(mockDB)

	// Test: Update to a version that doesn't have image info
	req := &pb.UpdateClusterComponentRequest{
		ClusterType:   pb.ClusterType_DEDICATED_TIDB,
		ClusterId:     "existing-cluster",
		ComponentName: "tidb",
		Version:       ptr.To("v999.0.0"), // Non-existent version
	}

	component, err := UpdateClusterComponent(mockDB, req)
	assert.Nil(component)
	assert.NotNil(err)
	assert.Equal(codes.Internal, status.Code(err))
	assert.Contains(err.Error(), "cannot find default image info of component tidb")
}
