package server

import (
	"context"
	"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/workflow"
	"github.com/tidbcloud/cluster-service-ng/domain/workflow/dag"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/executor"
	executormocks "github.com/tidbcloud/cluster-service-ng/pkg/runtime/executor/mocks"
	repomocks "github.com/tidbcloud/cluster-service-ng/pkg/storage/repo/mocks"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

type mockTiProxyExternalDNSTransactioner struct{}

func (m *mockTiProxyExternalDNSTransactioner) Transaction(ctx context.Context, fn func(context.Context) error) error {
	return fn(ctx)
}

func TestSetTiProxyExternalDNS_Validation(t *testing.T) {
	tests := []struct {
		name         string
		req          *entity.SetTiProxyExternalDNSRequest
		expectedCode codes.Code
		expectedMsg  string
	}{
		{
			name:         "nil request",
			req:          nil,
			expectedCode: codes.InvalidArgument,
			expectedMsg:  "request is nil",
		},
		{
			name:         "empty cluster id",
			req:          &entity.SetTiProxyExternalDNSRequest{},
			expectedCode: codes.InvalidArgument,
			expectedMsg:  "cluster id is empty",
		},
		{
			name: "empty tidb group id",
			req: &entity.SetTiProxyExternalDNSRequest{
				ClusterId: "100",
			},
			expectedCode: codes.InvalidArgument,
			expectedMsg:  "tidb group id is empty",
		},
		{
			name: "invalid ttl",
			req: &entity.SetTiProxyExternalDNSRequest{
				ClusterId:   "100",
				TidbGroupId: "200",
				Enable:      true,
				Ttl:         int32Ptr(0),
			},
			expectedCode: codes.InvalidArgument,
			expectedMsg:  "ttl must be greater than 0",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := &serverImpl{}
			err := s.SetTiProxyExternalDNS(context.Background(), tt.req)
			require.Error(t, err)
			assert.Equal(t, tt.expectedCode, status.Code(err))
			assert.Contains(t, err.Error(), tt.expectedMsg)
		})
	}
}

func TestSetTiProxyExternalDNS_Success(t *testing.T) {
	tidbGroupRepo := repomocks.NewTiDBGroupRepo(t)
	tidbClusterRepo := repomocks.NewTiDBClusterRepo(t)
	sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
	lockRepo := repomocks.NewLockRepo(t)
	dagExecutor := executormocks.NewRemoteExecutor(t)

	req := &entity.SetTiProxyExternalDNSRequest{
		ClusterId:   "100",
		TidbGroupId: "200",
		Enable:      true,
		Ttl:         int32Ptr(60),
	}
	tidbGroup := &entity.TiDBGroup{TiDBGroup: apiv1.TiDBGroup{Id: "200", ClusterId: "100", SharedPoolId: "1"}}
	tidbCluster := &entity.TiDBCluster{Id: "100"}
	sharedPool := &entity.SharedPool{SharedPool: apiv1.SharedPool{Id: "1"}}

	tidbGroupRepo.On("Load", mock.Anything, "200", false).Return(tidbGroup, nil).Once()
	lockRepo.On("Lock", mock.Anything, mock.Anything).Return(func() error { return nil }, nil).Once()
	sharedPoolRepo.On("Load", mock.Anything, "1", false).Return(sharedPool, nil).Once()
	tidbClusterRepo.On("Load", mock.Anything, "100", false).Return(tidbCluster, nil).Once()
	dagExecutor.On("QueryRunnings", mock.Anything, dag.PlanSetTiProxyExternalDNS, mock.Anything).Return(executor.NewExecTaskIdStatus(), nil).Once()
	dagExecutor.On("Submit", mock.Anything, dag.PlanSetTiProxyExternalDNS, mock.MatchedBy(func(args actions.ActionArgs) bool {
		sharedPoolId, err := args.GetString(workflow.SharedPoolIdKey)
		if err != nil || sharedPoolId != "1" {
			return false
		}
		clusterId, err := args.GetString(workflow.TiDBClusterIdKey)
		if err != nil || clusterId != "100" {
			return false
		}
		groupId, err := args.GetString(workflow.TiDBGroupIdKey)
		if err != nil || groupId != "200" {
			return false
		}
		enable, err := args.GetString(workflow.EnableExternalDNSKey)
		if err != nil || enable != "true" {
			return false
		}
		ttl, err := args.GetString(workflow.ExternalDNSTTLKey)
		return err == nil && ttl == "60"
	}), mock.AnythingOfType("string")).Return(nil).Once()

	s := &serverImpl{
		tidbGroupRepo:   tidbGroupRepo,
		tidbClusterRepo: tidbClusterRepo,
		sharedPoolRepo:  sharedPoolRepo,
		lockRepo:        lockRepo,
		dagExecutor:     dagExecutor,
		transactioner:   &mockTiProxyExternalDNSTransactioner{},
	}

	err := s.SetTiProxyExternalDNS(context.Background(), req)
	require.NoError(t, err)
}

func TestSetTiProxyExternalDNS_FailsWhenTidbGroupBelongsToDifferentCluster(t *testing.T) {
	tidbGroupRepo := repomocks.NewTiDBGroupRepo(t)
	lockRepo := repomocks.NewLockRepo(t)
	sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
	dagExecutor := executormocks.NewRemoteExecutor(t)

	req := &entity.SetTiProxyExternalDNSRequest{
		ClusterId:   "100",
		TidbGroupId: "200",
		Enable:      true,
	}
	tidbGroup := &entity.TiDBGroup{TiDBGroup: apiv1.TiDBGroup{Id: "200", ClusterId: "999", SharedPoolId: "1"}}

	tidbGroupRepo.On("Load", mock.Anything, "200", false).Return(tidbGroup, nil).Once()

	s := &serverImpl{
		tidbGroupRepo:  tidbGroupRepo,
		lockRepo:       lockRepo,
		sharedPoolRepo: sharedPoolRepo,
		dagExecutor:    dagExecutor,
		transactioner:  &mockTiProxyExternalDNSTransactioner{},
	}

	err := s.SetTiProxyExternalDNS(context.Background(), req)
	require.Error(t, err)
	assert.Equal(t, codes.FailedPrecondition, status.Code(err))
}

func int32Ptr(v int32) *int32 {
	return &v
}
