package assigncachelimit

import (
	"context"
	"testing"

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

const testNamespace = "tidb-pool"

func TestAssignCacheSizeLimit(t *testing.T) {
	tests := []struct {
		name         string
		data         map[string]string
		hasConfigMap bool
		wantLimit    int
		wantErr      bool
	}{
		{
			name:         "valid positive limit",
			data:         map[string]string{providertypes.AssignCacheSizeLimitKey: "3"},
			hasConfigMap: true,
			wantLimit:    3,
		},
		{
			name:         "missing key is unlimited",
			data:         map[string]string{},
			hasConfigMap: true,
			wantLimit:    0,
		},
		{
			name:         "invalid value is unlimited",
			data:         map[string]string{providertypes.AssignCacheSizeLimitKey: "bad"},
			hasConfigMap: true,
			wantLimit:    0,
		},
		{
			name:         "zero is unlimited",
			data:         map[string]string{providertypes.AssignCacheSizeLimitKey: "0"},
			hasConfigMap: true,
			wantLimit:    0,
		},
		{
			name:         "negative is unlimited",
			data:         map[string]string{providertypes.AssignCacheSizeLimitKey: "-1"},
			hasConfigMap: true,
			wantLimit:    0,
		},
		{
			name:      "configmap not found is unlimited",
			wantLimit: 0,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			g := NewGomegaWithT(t)
			objs := []client.Object{}
			if tt.hasConfigMap {
				objs = append(objs, &corev1.ConfigMap{
					ObjectMeta: metav1.ObjectMeta{
						Name:      providertypes.ServerlessPoolConfig,
						Namespace: testNamespace,
					},
					Data: tt.data,
				})
			}
			kubeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(objs...).Build()
			r := &Reconciler{Client: kubeClient, Namespace: testNamespace}

			gotLimit, err := r.assignCacheSizeLimit(context.Background(), ctrl.Request{
				NamespacedName: apitypes.NamespacedName{
					Name:      providertypes.ServerlessPoolConfig,
					Namespace: testNamespace,
				},
			})

			if tt.wantErr {
				g.Expect(err).To(HaveOccurred())
			} else {
				g.Expect(err).NotTo(HaveOccurred())
			}
			g.Expect(gotLimit).To(Equal(tt.wantLimit))
		})
	}
}

func TestTrimAssignedPods(t *testing.T) {
	tests := []struct {
		name         string
		needDelete   int
		activeTokens []clientfake.ActiveToken
		existingPods []string
		wantDeleted  int
		wantExisting []string
		wantNotFound []string
	}{
		{
			name:       "deletes at most needDelete endpoints in one walk",
			needDelete: 2,
			activeTokens: []clientfake.ActiveToken{
				{Token: "token-a", Endpoints: []string{"pod1,ip1", "pod2,ip2"}, Replicas: 2},
				{Token: "token-b", Endpoints: []string{"pod3,ip3"}, Replicas: 1},
				{Token: "token-c", Endpoints: []string{"pod4,ip4"}, Replicas: 1},
			},
			existingPods: []string{"pod1", "pod2", "pod3", "pod4"},
			wantDeleted:  2,
			wantExisting: []string{"pod2", "pod4"},
			wantNotFound: []string{"pod1", "pod3"},
		},
		{
			name:       "skips tokens without endpoints",
			needDelete: 2,
			activeTokens: []clientfake.ActiveToken{
				{Token: "token-a", Endpoints: nil, Replicas: 0},
				{Token: "token-b", Endpoints: []string{"pod1,ip1"}, Replicas: 1},
				{Token: "token-c", Endpoints: []string{"pod2,ip2"}, Replicas: 1},
			},
			existingPods: []string{"pod1", "pod2"},
			wantDeleted:  2,
			wantNotFound: []string{"pod1", "pod2"},
		},
		{
			name:       "missing pod is treated as deleted",
			needDelete: 2,
			activeTokens: []clientfake.ActiveToken{
				{Token: "token-a", Endpoints: []string{"missing-pod,ip1"}, Replicas: 1},
				{Token: "token-b", Endpoints: []string{"pod2,ip2"}, Replicas: 1},
			},
			existingPods: []string{"pod2"},
			wantDeleted:  2,
			wantNotFound: []string{"missing-pod", "pod2"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			g := NewGomegaWithT(t)
			objs := make([]client.Object, 0, len(tt.existingPods))
			for _, podName := range tt.existingPods {
				objs = append(objs, newPod(testNamespace, podName))
			}
			kubeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(objs...).Build()
			r := &Reconciler{
				Client:    kubeClient,
				Namespace: testNamespace,
				TidbManager: &clientfake.FakeTidbManager{
					WalkActiveTokenFromAssignCacheR: tt.activeTokens,
				},
			}

			gotDeleted := r.trimAssignedPods(context.Background(), tt.needDelete)

			g.Expect(gotDeleted).To(Equal(tt.wantDeleted))
			for _, podName := range tt.wantExisting {
				expectPodExists(g, kubeClient, podName)
			}
			for _, podName := range tt.wantNotFound {
				expectPodNotFound(g, kubeClient, podName)
			}
		})
	}
}

func newPod(namespace, name string) *corev1.Pod {
	return &corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: namespace,
		},
	}
}

func expectPodExists(g Gomega, kubeClient client.Client, name string) {
	g.Expect(kubeClient.Get(context.Background(), client.ObjectKey{
		Name:      name,
		Namespace: testNamespace,
	}, &corev1.Pod{})).To(Succeed())
}

func expectPodNotFound(g Gomega, kubeClient client.Client, name string) {
	err := kubeClient.Get(context.Background(), client.ObjectKey{
		Name:      name,
		Namespace: testNamespace,
	}, &corev1.Pod{})
	g.Expect(apierrors.IsNotFound(err)).To(BeTrue())
}
