package kubernetes

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	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"
)

type fakeStarterCache struct {
	startErr   error
	startDelay time.Duration
	synced     bool
	started    bool
}

func (f *fakeStarterCache) Start(context.Context) error {
	f.started = true
	if f.startDelay > 0 {
		time.Sleep(f.startDelay)
	}
	return f.startErr
}

func (f *fakeStarterCache) WaitForCacheSync(context.Context) bool {
	for !f.started {
		time.Sleep(time.Millisecond)
	}
	return f.synced
}

func TestNodeAndPodModelsHoldExpectedFields(t *testing.T) {
	node := Node{Name: "n1", CapacityCPU: 8, NodeSpec: "8C16G", Taints: []Taint{{Key: "dedicated", Value: "worker"}}}
	namespace := Namespace{Name: "tidbpool-1", Labels: map[string]string{"tags.tidbcloud.com/shared-pool": "pool-1"}}
	pod := Pod{Name: "p1", NodeName: "n1"}
	resourceQuota := ResourceQuota{Name: "sharedpool-vcpu-ceiling", Namespace: "tidbpool-1", HardCPU: 8}
	require.Equal(t, "n1", node.Name)
	require.Equal(t, "8C16G", node.NodeSpec)
	require.Equal(t, []Taint{{Key: "dedicated", Value: "worker"}}, node.Taints)
	require.Equal(t, "tidbpool-1", namespace.Name)
	require.Equal(t, "n1", pod.NodeName)
	require.InDelta(t, 8.0, resourceQuota.HardCPU, 0.0)
}

func TestInClusterSourceMapsKubernetesObjects(t *testing.T) {
	scheme := runtime.NewScheme()
	require.NoError(t, corev1.AddToScheme(scheme))

	cli := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(
			&corev1.Node{
				ObjectMeta: metav1.ObjectMeta{
					Name: "worker-1",
					Labels: map[string]string{
						"node-spec": "8C16G",
					},
				},
				Spec: corev1.NodeSpec{
					Taints: []corev1.Taint{{
						Key:   "dedicated",
						Value: "worker",
					}},
				},
				Status: corev1.NodeStatus{
					Capacity: corev1.ResourceList{
						corev1.ResourceCPU: resource.MustParse("8"),
					},
				},
			},
			&corev1.Pod{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "tidb-0",
					Namespace: "default",
					Labels: map[string]string{
						"tags.tidbcloud.com/component": "tidb",
					},
				},
				Spec: corev1.PodSpec{
					NodeName: "worker-1",
				},
				Status: corev1.PodStatus{
					Phase: corev1.PodRunning,
				},
			},
			&corev1.Namespace{
				ObjectMeta: metav1.ObjectMeta{
					Name: "tidbpool-1",
					Labels: map[string]string{
						"tags.tidbcloud.com/shared-pool": "pool-1",
						"tags.tidbcloud.com/tenant":      "tenant-1",
					},
				},
			},
			&corev1.ResourceQuota{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "sharedpool-vcpu-ceiling",
					Namespace: "tidbpool-1",
				},
				Spec: corev1.ResourceQuotaSpec{
					Hard: corev1.ResourceList{
						corev1.ResourceRequestsCPU: resource.MustParse("8"),
					},
				},
			},
		).
		Build()

	source := NewSource(cli)
	nodes, err := source.ListNodes(context.Background())
	require.NoError(t, err)
	require.Len(t, nodes, 1)
	require.Equal(t, Node{
		Name:        "worker-1",
		Labels:      map[string]string{"node-spec": "8C16G"},
		CapacityCPU: 8,
		NodeSpec:    "8C16G",
		Taints:      []Taint{{Key: "dedicated", Value: "worker"}},
	}, nodes[0])

	pods, err := source.ListPods(context.Background())
	require.NoError(t, err)
	require.Len(t, pods, 1)
	require.Equal(t, Pod{
		Name:      "tidb-0",
		NodeName:  "worker-1",
		Labels:    map[string]string{"tags.tidbcloud.com/component": "tidb"},
		OwnerKind: "",
		Phase:     string(corev1.PodRunning),
	}, pods[0])

	namespaces, err := source.ListNamespaces(context.Background())
	require.NoError(t, err)
	require.Len(t, namespaces, 1)
	require.Equal(t, Namespace{
		Name: "tidbpool-1",
		Labels: map[string]string{
			"tags.tidbcloud.com/shared-pool": "pool-1",
			"tags.tidbcloud.com/tenant":      "tenant-1",
		},
	}, namespaces[0])

	resourceQuotas, err := source.ListResourceQuotas(context.Background())
	require.NoError(t, err)
	require.Len(t, resourceQuotas, 1)
	require.Equal(t, ResourceQuota{
		Name:      "sharedpool-vcpu-ceiling",
		Namespace: "tidbpool-1",
		HardCPU:   8,
	}, resourceQuotas[0])
}

func TestStartCacheReturnsStartError(t *testing.T) {
	cache := &fakeStarterCache{startErr: errors.New("boom")}

	err := startCache(context.Background(), cache)
	require.EqualError(t, err, "start cache: boom")
	require.True(t, cache.started)
}

func TestStartCacheReturnsSyncError(t *testing.T) {
	cache := &fakeStarterCache{synced: false}

	err := startCache(context.Background(), cache)
	require.ErrorContains(t, err, "cache is not synced")
	require.True(t, cache.started)
}

func TestStartCacheReturnsDelayedStartErrorAfterSyncFailure(t *testing.T) {
	cache := &fakeStarterCache{
		startErr:   errors.New("boom"),
		startDelay: 5 * time.Millisecond,
		synced:     false,
	}

	err := startCache(context.Background(), cache)
	require.EqualError(t, err, "start cache: boom")
	require.True(t, cache.started)
}
