package server

import (
	"context"
	"fmt"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/cluster-service-ng/pkg/dataplane"
	dataplanemocks "github.com/tidbcloud/cluster-service-ng/pkg/dataplane/mocks"
	repomocks "github.com/tidbcloud/cluster-service-ng/pkg/storage/repo/mocks"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

func TestGetSharedPoolVcpusUsage(t *testing.T) {
	t.Run("success", func(t *testing.T) {
		sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
		dataplaneManager := dataplanemocks.NewManager(t)

		scheme := runtime.NewScheme()
		require.NoError(t, corev1.AddToScheme(scheme))

		targetPoolID := "100"
		dataplaneName := "test-dataplane"
		otherPoolID := "999"
		k8sClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(
			newWorkerNode("worker-node-1", "8C16G", 8),
			newWorkerNode("worker-node-2", "4C8G", 4),
			newWorkerNode("worker-node-3", "broken", 4),
			newWorkerNode("worker-node-4", "2C4G", 2),
			newWorkerNode("worker-node-5", "6C12G", 6),
			newWorkerNode("worker-node-6", "10C20G", 10),
			newPoolPod("pod-1", "worker-node-1", "tenant-1", targetPoolID),
			newPoolPod("pod-2", "worker-node-2", "tenant-1", targetPoolID),
			newPoolPod("pod-3", "worker-node-2", "tenant-2", otherPoolID),
			newPoolPod("pod-4", "worker-node-3", "tenant-1", targetPoolID),
			newPoolPodMissingTenant("pod-5", "worker-node-4", targetPoolID),
			newPoolPod("pod-6", "worker-node-6", "tenant-2", otherPoolID),
			newDaemonSetPoolPod("pod-ds", "worker-node-1", "tenant-1", targetPoolID),
		).Build()

		sharedPoolRepo.On("LoadDataplaneName", mock.Anything, targetPoolID, false).Return(dataplaneName, nil)
		dataplaneManager.On("GetClient", dataplaneName).Return(k8sClient, nil)

		server := &serverImpl{
			sharedPoolRepo:   sharedPoolRepo,
			dataplaneManager: dataplaneManager,
		}

		resp, err := server.GetSharedPoolVcpusUsage(context.Background(), targetPoolID)
		require.NoError(t, err)
		require.NotNil(t, resp)
		assert.Equal(t, targetPoolID, resp.GetSharedPoolId())
		assert.EqualValues(t, 8, resp.GetVcpusUsed())
		assert.EqualValues(t, 1, resp.GetCountedNodes())
		assert.EqualValues(t, 3, resp.GetDroppedNodes())
		require.NotNil(t, resp.GetDropReasons())
		assert.EqualValues(t, 1, resp.GetDropReasons().GetConflictingOwnership())
		assert.EqualValues(t, 1, resp.GetDropReasons().GetMissingIdentity())
		assert.EqualValues(t, 1, resp.GetDropReasons().GetInvalidNodeSpec())
		assert.NotNil(t, resp.GetSampleTime())
	})

	t.Run("empty shared pool id", func(t *testing.T) {
		server := &serverImpl{}
		resp, err := server.GetSharedPoolVcpusUsage(context.Background(), "")
		require.Error(t, err)
		assert.Nil(t, resp)
		assert.Equal(t, codes.InvalidArgument, status.Code(err))
	})

	t.Run("shared pool not found", func(t *testing.T) {
		sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
		sharedPoolRepo.On("LoadDataplaneName", mock.Anything, "404", false).Return("", status.Error(codes.NotFound, "not found"))

		server := &serverImpl{sharedPoolRepo: sharedPoolRepo}
		resp, err := server.GetSharedPoolVcpusUsage(context.Background(), "404")
		require.Error(t, err)
		assert.Nil(t, resp)
		assert.Equal(t, codes.NotFound, status.Code(err))
	})

	t.Run("dataplane provider is not ready", func(t *testing.T) {
		sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
		dataplaneManager := dataplanemocks.NewManager(t)

		targetPoolID := "100"
		dataplaneName := "test-dataplane"
		errDataplaneNotReady := fmt.Errorf("wrapped: %w", dataplane.ErrDataplaneProviderNotReady)

		sharedPoolRepo.On("LoadDataplaneName", mock.Anything, targetPoolID, false).Return(dataplaneName, nil)
		dataplaneManager.On("GetClient", dataplaneName).Return(nil, errDataplaneNotReady)

		server := &serverImpl{
			sharedPoolRepo:   sharedPoolRepo,
			dataplaneManager: dataplaneManager,
		}

		resp, err := server.GetSharedPoolVcpusUsage(context.Background(), targetPoolID)
		require.Error(t, err)
		assert.Nil(t, resp)
		assert.Equal(t, codes.FailedPrecondition, status.Code(err))
		assert.Contains(t, err.Error(), "dataplane test-dataplane is not ready")
	})
}

func newWorkerNode(name, nodeSpec string, cpu int64) *corev1.Node {
	return &corev1.Node{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
			Labels: map[string]string{
				"node-spec": nodeSpec,
			},
		},
		Spec: corev1.NodeSpec{
			Taints: []corev1.Taint{{
				Key:    "dedicated",
				Value:  "worker",
				Effect: corev1.TaintEffectNoSchedule,
			}},
		},
		Status: corev1.NodeStatus{
			Capacity: corev1.ResourceList{
				corev1.ResourceCPU: *resource.NewQuantity(cpu, resource.DecimalSI),
			},
		},
	}
}

func newPoolPod(name, nodeName, tenantID, poolID string) *corev1.Pod {
	return &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
			Labels: map[string]string{
				"tags.tidbcloud.com/tenant":      tenantID,
				"tags.tidbcloud.com/shared-pool": poolID,
			},
		},
		Spec: corev1.PodSpec{
			NodeName: nodeName,
		},
		Status: corev1.PodStatus{Phase: corev1.PodRunning},
	}
}

func newPoolPodMissingTenant(name, nodeName, poolID string) *corev1.Pod {
	return &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
			Labels: map[string]string{
				"tags.tidbcloud.com/shared-pool": poolID,
			},
		},
		Spec: corev1.PodSpec{
			NodeName: nodeName,
		},
		Status: corev1.PodStatus{Phase: corev1.PodRunning},
	}
}

func newDaemonSetPoolPod(name, nodeName, tenantID, poolID string) *corev1.Pod {
	controller := true
	return &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
			Labels: map[string]string{
				"tags.tidbcloud.com/tenant":      tenantID,
				"tags.tidbcloud.com/shared-pool": poolID,
			},
			OwnerReferences: []metav1.OwnerReference{{
				Kind:       "DaemonSet",
				Controller: &controller,
			}},
		},
		Spec: corev1.PodSpec{
			NodeName: nodeName,
		},
		Status: corev1.PodStatus{Phase: corev1.PodRunning},
	}
}
