package action

import (
	"context"
	"testing"
	"time"

	"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"
	dplanemgrv1 "github.com/tidbcloud/cluster-service-ng/api/dataplane-manager/v1"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/domain/workflow"
	dpmgrmocks "github.com/tidbcloud/cluster-service-ng/pkg/dataplanemgr/mocks"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
	repomocks "github.com/tidbcloud/cluster-service-ng/pkg/storage/repo/mocks"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/encoding/protojson"
)

func TestBuildCassandraProxyPLS(t *testing.T) {
	comp := &entity.Component{
		Id:            "1234567890",
		ComponentType: apiv1.ComponentType_CASSANDRA_PROXY,
	}
	cl := &entity.TiDBCluster{Id: "1234567890", TenantId: "tenant-x"}
	sp := &entity.SharedPool{}
	sp.Id = "sp-1"

	pls := buildCassandraProxyPLS(comp, cl, sp)

	assert.Equal(t, "cassandra-private-1234567890", pls.GetId())
	assert.Equal(t, "cassandra-private-1234567890", pls.GetLoadBalancerName(),
		"PLS LoadBalancerName must equal LB CR id (1:1 pair)")

	tags := pls.GetTags()
	assert.Equal(t, "tenant-x", tags["tags.tidbcloud.com/tenant"])
	assert.Equal(t, "cassandra-proxy", tags["pingcap.com/component"])
}

func TestValidateCassandraProxyPLSLoadBalancerName(t *testing.T) {
	desired := buildCassandraProxyPLS(
		&entity.Component{Id: "c1", ComponentType: apiv1.ComponentType_CASSANDRA_PROXY},
		&entity.TiDBCluster{Id: "c1", TenantId: "t"},
		&entity.SharedPool{},
	)

	t.Run("same load balancer succeeds even if tags differ", func(t *testing.T) {
		current := buildCassandraProxyPLS(
			&entity.Component{Id: "c1", ComponentType: apiv1.ComponentType_CASSANDRA_PROXY},
			&entity.TiDBCluster{Id: "c1", TenantId: "t"},
			&entity.SharedPool{},
		)
		delete(current.Tags, "pingcap.com/component")

		require.NoError(t, validateCassandraProxyPLSLoadBalancerName(current, desired))
	})

	t.Run("load balancer mismatch fails", func(t *testing.T) {
		current := buildCassandraProxyPLS(
			&entity.Component{Id: "c1", ComponentType: apiv1.ComponentType_CASSANDRA_PROXY},
			&entity.TiDBCluster{Id: "c1", TenantId: "t"},
			&entity.SharedPool{},
		)
		current.LoadBalancerName = "stale-lb"

		err := validateCassandraProxyPLSLoadBalancerName(current, desired)
		require.Error(t, err)
		assert.Equal(t, codes.FailedPrecondition, status.Code(err))
		assert.Contains(t, status.Convert(err).Message(), "points to loadbalancer stale-lb")
	})
}

func TestApplyCassandraProxyPLSActionInvokeValidatesLoadBalancerName(t *testing.T) {
	t.Run("existing PLS mismatch fails before wait", func(t *testing.T) {
		sharedPoolRepo, tidbClusterRepo, args := newApplyCassandraProxyPLSTestArgs(t)
		plsClient := dpmgrmocks.NewClient(t)
		plsClient.On("GetPrivateLinkService", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.GetPrivateLinkServiceRequest) bool {
			return req.GetDataPlaneId() == "dp-1" && req.GetId() == "cassandra-private-1234567890"
		})).Return(&dplanemgrv1.PrivateLinkService{LoadBalancerName: "stale-lb"}, nil).Once()

		action := NewApplyCassandraProxyPLSAction(sharedPoolRepo, tidbClusterRepo, plsClient)
		err := action.Invoke(args)
		require.Error(t, err)
		assert.Equal(t, codes.FailedPrecondition, status.Code(err))
		assert.Contains(t, status.Convert(err).Message(), "points to loadbalancer stale-lb")
	})

	t.Run("create conflict revalidates existing PLS", func(t *testing.T) {
		sharedPoolRepo, tidbClusterRepo, args := newApplyCassandraProxyPLSTestArgs(t)
		plsClient := dpmgrmocks.NewClient(t)
		plsClient.On("GetPrivateLinkService", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.GetPrivateLinkServiceRequest) bool {
			return req.GetDataPlaneId() == "dp-1" && req.GetId() == "cassandra-private-1234567890"
		})).Return(nil, status.Error(codes.NotFound, "not found")).Once()
		plsClient.On("CreatePrivateLinkService", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.CreatePrivateLinkServiceRequest) bool {
			return req.GetDataPlaneId() == "dp-1" &&
				req.GetId() == "cassandra-private-1234567890" &&
				req.GetPrivateLinkService().GetLoadBalancerName() == "cassandra-private-1234567890"
		})).Return(nil, status.Error(codes.AlreadyExists, "exists")).Once()
		plsClient.On("GetPrivateLinkService", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.GetPrivateLinkServiceRequest) bool {
			return req.GetDataPlaneId() == "dp-1" && req.GetId() == "cassandra-private-1234567890"
		})).Return(&dplanemgrv1.PrivateLinkService{LoadBalancerName: "stale-lb"}, nil).Once()

		action := NewApplyCassandraProxyPLSAction(sharedPoolRepo, tidbClusterRepo, plsClient)
		err := action.Invoke(args)
		require.Error(t, err)
		assert.Equal(t, codes.FailedPrecondition, status.Code(err))
		assert.Contains(t, status.Convert(err).Message(), "points to loadbalancer stale-lb")
	})
}

func TestApplyCassandraProxyPLSActionWaitForActive(t *testing.T) {
	originalInterval := cassandraProxyPLSWaitInterval
	originalTimeout := cassandraProxyPLSWaitTimeout
	cassandraProxyPLSWaitInterval = time.Millisecond
	cassandraProxyPLSWaitTimeout = 50 * time.Millisecond
	t.Cleanup(func() {
		cassandraProxyPLSWaitInterval = originalInterval
		cassandraProxyPLSWaitTimeout = originalTimeout
	})

	t.Run("polls until active", func(t *testing.T) {
		plsClient := dpmgrmocks.NewClient(t)
		desired := &dplanemgrv1.PrivateLinkService{Id: "pls-1", LoadBalancerName: "lb-1"}
		plsClient.On("GetPrivateLinkService", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.GetPrivateLinkServiceRequest) bool {
			return req.GetDataPlaneId() == "dp-1" && req.GetId() == "pls-1"
		})).Return(&dplanemgrv1.PrivateLinkService{LoadBalancerName: "lb-1", Status: dplanemgrv1.PrivateLinkService_CREATING}, nil).Once()
		plsClient.On("GetPrivateLinkService", mock.Anything, mock.Anything).
			Return(&dplanemgrv1.PrivateLinkService{LoadBalancerName: "lb-1", Status: dplanemgrv1.PrivateLinkService_ACTIVE}, nil).Once()

		action := &ApplyCassandraProxyPLSAction{plsClient: plsClient}
		require.NoError(t, action.waitForActive(context.Background(), "dp-1", desired))
	})

	t.Run("active with mismatched load balancer fails", func(t *testing.T) {
		plsClient := dpmgrmocks.NewClient(t)
		desired := &dplanemgrv1.PrivateLinkService{Id: "pls-1", LoadBalancerName: "lb-1"}
		plsClient.On("GetPrivateLinkService", mock.Anything, mock.MatchedBy(func(req *dplanemgrv1.GetPrivateLinkServiceRequest) bool {
			return req.GetDataPlaneId() == "dp-1" && req.GetId() == "pls-1"
		})).Return(&dplanemgrv1.PrivateLinkService{LoadBalancerName: "lb-1", Status: dplanemgrv1.PrivateLinkService_CREATING}, nil).Once()
		plsClient.On("GetPrivateLinkService", mock.Anything, mock.Anything).
			Return(&dplanemgrv1.PrivateLinkService{LoadBalancerName: "stale-lb", Status: dplanemgrv1.PrivateLinkService_ACTIVE}, nil).Once()

		action := &ApplyCassandraProxyPLSAction{plsClient: plsClient}
		err := action.waitForActive(context.Background(), "dp-1", desired)
		require.Error(t, err)
		assert.Equal(t, codes.FailedPrecondition, status.Code(err))
		assert.Contains(t, status.Convert(err).Message(), "points to loadbalancer stale-lb")
	})

	t.Run("timeout returns failed precondition", func(t *testing.T) {
		plsClient := dpmgrmocks.NewClient(t)
		desired := &dplanemgrv1.PrivateLinkService{Id: "pls-1", LoadBalancerName: "lb-1"}
		plsClient.On("GetPrivateLinkService", mock.Anything, mock.Anything).
			Return(&dplanemgrv1.PrivateLinkService{LoadBalancerName: "lb-1", Status: dplanemgrv1.PrivateLinkService_CREATING}, nil)

		action := &ApplyCassandraProxyPLSAction{plsClient: plsClient}
		err := action.waitForActive(context.Background(), "dp-1", desired)
		require.Error(t, err)
		assert.Equal(t, codes.FailedPrecondition, status.Code(err))
	})
}

func newApplyCassandraProxyPLSTestArgs(t *testing.T) (*repomocks.SharedPoolRepo, *repomocks.TiDBClusterRepo, actions.ActionArgs) {
	t.Helper()
	sharedPool := newCassandraProxyLBTestSharedPool()
	sharedPool.ComponentGroups = []*entity.Component{
		newCassandraProxyLBTestComp(),
		{Id: "c2", ComponentType: 9999},
	}
	sharedPoolBytes, err := protojson.Marshal(sharedPool)
	require.NoError(t, err)

	sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
	sharedPoolRepo.On("Load", mock.Anything, "sp-1", false).Return(sharedPool, nil).Once()

	tidbClusterRepo := repomocks.NewTiDBClusterRepo(t)
	tidbClusterRepo.On("Load", mock.Anything, "1234567890", false).Return(newCassandraProxyLBTestCluster(), nil).Once()

	return sharedPoolRepo, tidbClusterRepo, actions.ActionArgs{
		workflow.SharedPoolIdKey:   actions.ActionArgString("sp-1"),
		workflow.SharedPoolKey:     actions.ActionArgString(string(sharedPoolBytes)),
		workflow.ComponentIdsKey:   actions.ActionArgString("c1,1234567890"),
		workflow.ComponentTypesKey: actions.ActionArgString(entity.GetComponentTypeName(newCassandraProxyLBTestComp())),
	}
}
