package noderecover

import (
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	. "github.com/onsi/gomega"
	"github.com/tidbcloud/aws-shared-provider/pkg/scheme"
	"github.com/tidbcloud/aws-shared-provider/pkg/types"
	"github.com/tidbcloud/aws-shared-provider/pkg/utils/query"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	apitypes "k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/tools/record"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

func TestReconcile(t *testing.T) {
	now := time.Now()
	noEvictData := `{"status":"success","data":{"resultType":"matrix","result":[` +
		`{"metric":{"instance":"node1"},"value":[1667460382,"0.93"]}]}}`
	evictData := `{"status":"success","data":{"resultType":"matrix","result":[` +
		`{"metric":{"instance":"node1"},"value":[1667460382,"0.33"]}]}}`

	cases := []struct {
		name string

		promData string
		node     *corev1.Node
		wantNode *corev1.Node
	}{
		{
			name: "less than checkInterval since the last force evict",
			node: &corev1.Node{
				ObjectMeta: metav1.ObjectMeta{
					Name: "node1",
					Annotations: map[string]string{
						types.AnnoNodeForceEvictTimestamp: now.Format(time.RFC3339),
					},
				},
				Spec: corev1.NodeSpec{
					Unschedulable: true,
				},
			},
			wantNode: &corev1.Node{
				ObjectMeta: metav1.ObjectMeta{
					Name: "node1",
					Annotations: map[string]string{
						types.AnnoNodeForceEvictTimestamp: now.Format(time.RFC3339),
					},
				},
				Spec: corev1.NodeSpec{
					Unschedulable: true,
				},
			},
		},
		{
			name: "less than checkInterval since the last check",
			node: &corev1.Node{
				ObjectMeta: metav1.ObjectMeta{
					Name: "node1",
					Annotations: map[string]string{
						types.AnnoNodeForceEvictTimestamp: now.Add(-checkInterval).Format(time.RFC3339),
						nodeRecoverCheckTimestamp:         now.Format(time.RFC3339),
					},
				},
				Spec: corev1.NodeSpec{
					Unschedulable: true,
				},
			},
			wantNode: &corev1.Node{
				ObjectMeta: metav1.ObjectMeta{
					Name: "node1",
					Annotations: map[string]string{
						types.AnnoNodeForceEvictTimestamp: now.Add(-checkInterval).Format(time.RFC3339),
						nodeRecoverCheckTimestamp:         now.Format(time.RFC3339),
					},
				},
				Spec: corev1.NodeSpec{
					Unschedulable: true,
				},
			},
		},
		{
			name: "more than maxEvictDuration since the last force evict, recover",
			node: &corev1.Node{
				ObjectMeta: metav1.ObjectMeta{
					Name: "node1",
					Annotations: map[string]string{
						types.AnnoNodeForceEvictTimestamp: now.Add(-24 * time.Hour).Format(time.RFC3339),
						nodeRecoverCheckTimestamp:         now.Add(-checkInterval).Format(time.RFC3339),
						nodeRecoverCheckResult:            "abc",
					},
				},
				Spec: corev1.NodeSpec{
					Unschedulable: true,
				},
			},
			wantNode: &corev1.Node{
				ObjectMeta: metav1.ObjectMeta{
					Name: "node1",
				},
				Spec: corev1.NodeSpec{
					Unschedulable: false,
				},
			},
		},
		{
			name:     "the node is idle, recover",
			promData: noEvictData,
			node: &corev1.Node{
				ObjectMeta: metav1.ObjectMeta{
					Name: "node1",
					Annotations: map[string]string{
						types.AnnoNodeForceEvictTimestamp: now.Add(-time.Hour).Format(time.RFC3339),
						nodeRecoverCheckTimestamp:         now.Add(-checkInterval).Format(time.RFC3339),
						nodeRecoverCheckResult:            "abc",
					},
				},
				Spec: corev1.NodeSpec{
					Unschedulable: true,
				},
			},
			wantNode: &corev1.Node{
				ObjectMeta: metav1.ObjectMeta{
					Name: "node1",
				},
				Spec: corev1.NodeSpec{
					Unschedulable: false,
				},
			},
		},
		{
			name:     "the node is not idle, continue to check",
			promData: evictData,
			node: &corev1.Node{
				ObjectMeta: metav1.ObjectMeta{
					Name: "node1",
					Annotations: map[string]string{
						types.AnnoNodeForceEvictTimestamp: now.Add(-time.Hour).Format(time.RFC3339),
					},
				},
				Spec: corev1.NodeSpec{
					Unschedulable: true,
				},
			},
			wantNode: &corev1.Node{
				ObjectMeta: metav1.ObjectMeta{
					Name: "node1",
					Annotations: map[string]string{
						types.AnnoNodeForceEvictTimestamp: now.Add(-time.Hour).Format(time.RFC3339),
						nodeRecoverCheckTimestamp:         "not-check-value",
						nodeRecoverCheckResult:            "not-check-value",
					},
				},
				Spec: corev1.NodeSpec{
					Unschedulable: true,
				},
			},
		},
	}

	var data string
	promServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, data)
	}))
	defer promServer.Close()
	querier := query.NewPromQueryer(nil, promServer.URL)

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			g := NewGomegaWithT(t)
			data = c.promData
			r := newFakeReconciler([]runtime.Object{c.node}, querier)
			req := ctrl.Request{
				NamespacedName: apitypes.NamespacedName{
					Name: c.node.Name,
				},
			}
			_, err := r.Reconcile(context.Background(), req)
			g.Expect(err).To(Succeed())

			node := &corev1.Node{}
			g.Expect(r.Client.Get(context.Background(), client.ObjectKeyFromObject(c.node), node)).To(Succeed())
			for k, v := range c.wantNode.Annotations {
				if v == "not-check-value" {
					g.Expect(node.Annotations[k]).NotTo(BeEmpty())
					node.Annotations[k] = v
				}
			}
			g.Expect(node.Annotations).To(Equal(c.wantNode.Annotations))
			g.Expect(node.Spec).To(Equal(c.wantNode.Spec))
		})
	}
}

func newFakeReconciler(objs []runtime.Object, querier query.PromQuerier) *Reconciler {
	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme.Scheme).
		WithRuntimeObjects(objs...).
		Build()

	return &Reconciler{
		Client:      fakeClient,
		Recorder:    record.NewFakeRecorder(4096),
		o11yQuerier: querier,
	}
}
