package tidbscaler

import (
	"context"
	"strconv"
	"testing"
	"time"

	"github.com/go-logr/logr"
	. "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/clientfake"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
	"sigs.k8s.io/controller-runtime/pkg/log"
)

func pTime(t time.Time) *time.Time {
	return &t
}

func pInt(i int) *int {
	return &i
}

func TestTidbScaler_Do(t *testing.T) {
	g := NewGomegaWithT(t)

	tests := []struct {
		name             string
		poolSize         int
		scalerMinSize    int
		poolMinSize      int
		scalerMaxSize    int
		poolMaxSize      int
		scalerRevision   string
		poolRevision     string
		creating         int
		createTime       *time.Time
		abnormalPods     int
		expectedCreating int
		expectedPodCount *int
	}{
		{
			name:             "Expand Pool without Abnormal Pods, Positive Step",
			poolSize:         3,
			scalerMinSize:    5,
			poolMinSize:      5,
			scalerMaxSize:    10,
			poolMaxSize:      10,
			scalerRevision:   "1234567",
			poolRevision:     "1234567",
			creating:         0,
			abnormalPods:     0,
			expectedCreating: 2,
		},
		{
			name:             "Expand Pool with Abnormal Pods, Skip Expansion",
			poolSize:         3,
			scalerMinSize:    5,
			poolMinSize:      5,
			scalerMaxSize:    10,
			poolMaxSize:      10,
			scalerRevision:   "1234567",
			poolRevision:     "1234567",
			creating:         0,
			abnormalPods:     types.MaxAbnormalTidbPods + 1,
			expectedCreating: 0,
		},
		{
			name:             "Expand Pool, Negative Step",
			poolSize:         4,
			scalerMinSize:    5,
			poolMinSize:      5,
			scalerMaxSize:    10,
			poolMaxSize:      10,
			scalerRevision:   "1234567",
			poolRevision:     "1234567",
			creating:         2,
			abnormalPods:     0,
			expectedCreating: 1,
		},
		{
			name:             "Expand Pool, minSize change cause creating reset",
			poolSize:         4,
			scalerMinSize:    5,
			poolMinSize:      6,
			scalerMaxSize:    10,
			poolMaxSize:      10,
			scalerRevision:   "1234567",
			poolRevision:     "1234567",
			creating:         2,
			abnormalPods:     0,
			expectedCreating: 2,
		},
		{
			name:             "Expand Pool, revision change cause creating reset",
			poolSize:         3,
			scalerMinSize:    5,
			poolMinSize:      5,
			scalerMaxSize:    10,
			poolMaxSize:      10,
			scalerRevision:   "1234567",
			poolRevision:     "7654321",
			creating:         2,
			abnormalPods:     0,
			expectedCreating: 2,
		},
		{
			name:             "Expand Pool, maxCreatingDuration cause creating reset",
			poolSize:         3,
			scalerMinSize:    5,
			poolMinSize:      5,
			scalerMaxSize:    10,
			poolMaxSize:      10,
			scalerRevision:   "1234567",
			poolRevision:     "1234567",
			creating:         2,
			createTime:       pTime(time.Now().Add(-maxCreatingDuration * time.Second)), //nolint:durationcheck
			abnormalPods:     0,
			expectedCreating: 0,
		},
		{
			name:             "Reduce Pool, reset creating",
			poolSize:         2,
			scalerMinSize:    0,
			poolMinSize:      0,
			scalerMaxSize:    0,
			poolMaxSize:      0,
			scalerRevision:   "1234567",
			poolRevision:     "7654321",
			creating:         1,
			abnormalPods:     0,
			expectedCreating: 0,
			expectedPodCount: pInt(0),
		},
		{
			name:             "Scale to zero when pool minSize is zero and free cache exists",
			poolSize:         2,
			scalerMinSize:    0,
			poolMinSize:      0,
			scalerMaxSize:    5,
			poolMaxSize:      5,
			scalerRevision:   "1234567",
			poolRevision:     "1234567",
			creating:         0,
			abnormalPods:     0,
			expectedCreating: 0,
			expectedPodCount: pInt(0),
		},
		{
			name:             "Default Action, reset creating",
			poolSize:         5,
			scalerMinSize:    5,
			poolMinSize:      5,
			scalerMaxSize:    10,
			poolMaxSize:      10,
			scalerRevision:   "1234567",
			poolRevision:     "1234567",
			creating:         2,
			abnormalPods:     0,
			expectedCreating: 0,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			fakeAbnormalPods := make([]corev1.Pod, tt.abnormalPods)
			for i := 0; i < tt.abnormalPods; i++ {
				fakeAbnormalPods[i] = corev1.Pod{
					ObjectMeta: metav1.ObjectMeta{
						Name: "abnormal-pod-" + strconv.Itoa(i),
					},
				}
			}
			fakeTidbPool := &clientfake.FakeTidbPool{
				TidbPod:       &corev1.Pod{},
				PoolNamespace: "tidb-pool",
				Revision:      tt.poolRevision,
				MinSize:       tt.poolMinSize,
				MaxSize:       tt.poolMaxSize,
				AbnormalPods:  fakeAbnormalPods,
			}
			kubeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()

			ts := &TidbScaler{
				Logger:            logr.New(log.NullLogSink{}),
				tidbPoolNamespace: fakeTidbPool.Namespace(),
				client:            kubeClient,
				pool:              fakeTidbPool,
				revision:          tt.scalerRevision,
				minSize:           tt.scalerMinSize,
				maxSize:           tt.scalerMaxSize,
				creating:          tt.creating,
			}
			tidbPods := make([]string, 0, tt.poolSize)
			for i := 0; i < tt.poolSize; i++ {
				initTidbPod := fakeTidbPool.GetPool(context.Background()).TidbPod.DeepCopy()
				initTidbPod.Labels["init"] = "true"
				g.Expect(ts.client.Create(context.Background(), initTidbPod)).To(Succeed())
				tidbPods = append(tidbPods, initTidbPod.Name)
			}
			ts.tManager = &clientfake.FakeTidbManager{
				FreeCacheSizeR:           tt.poolSize,
				GetOldTidbFromFreeCacheR: tidbPods,
				TidbPool:                 fakeTidbPool,
				TryCreatePodFn: func(ctx context.Context, pod *corev1.Pod) error {
					return kubeClient.Create(ctx, pod)
				},
			}
			if tt.creating > 0 {
				if tt.createTime == nil {
					tt.createTime = pTime(time.Now())
				}
				ts.createTime = tt.createTime
			}

			ts.do()

			g.Expect(ts.minSize).To(Equal(tt.poolMinSize))
			g.Expect(ts.maxSize).To(Equal(tt.poolMaxSize))
			g.Expect(ts.revision).To(Equal(tt.poolRevision))
			g.Expect(ts.creating).To(Equal(tt.expectedCreating))
			if tt.expectedPodCount != nil {
				pods := &corev1.PodList{}
				g.Expect(ts.client.List(context.Background(), pods, client.InNamespace(ts.tidbPoolNamespace))).To(Succeed())
				g.Expect(pods.Items).To(HaveLen(*tt.expectedPodCount))
			}
			if ts.creating == 0 {
				g.Expect(ts.createTime).To(BeNil())
			} else {
				if tt.creating > 0 {
					if tt.poolMinSize == tt.scalerMinSize && tt.scalerRevision == tt.poolRevision {
						g.Expect(*ts.createTime).To(Equal(*tt.createTime)) // createTime should not change
					} else {
						// minSize or revision change cause creating reset at first
						g.Expect(*ts.createTime).ToNot(Equal(*tt.createTime)) // createTime should change
					}
				} else {
					g.Expect(ts.createTime).ToNot(BeNil()) // createTime should be set
				}
			}
			if ts.creating-tt.creating > 0 {
				selector, _ := labels.Parse("init!=true")
				pods := &corev1.PodList{}
				g.Expect(ts.client.List(context.Background(), pods,
					client.InNamespace(ts.tidbPoolNamespace),
					client.MatchingLabelsSelector{Selector: selector},
				)).To(Succeed())
				g.Expect(len(pods.Items)).To(Equal(tt.expectedCreating))
				for _, po := range pods.Items {
					g.Expect(po.Name).To(ContainSubstring(tt.poolRevision))
					g.Expect(po.Name).To(HavePrefix(types.TidbServerNamePrefix + ts.revision + "-"))
				}
			}
		})
	}
}
