package throttler

import (
	"context"
	"errors"
	"testing"

	"github.com/go-logr/logr"
	"github.com/stretchr/testify/assert"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	k8sruntime "k8s.io/apimachinery/pkg/runtime"
	apitypes "k8s.io/apimachinery/pkg/types"
	utilerrors "k8s.io/apimachinery/pkg/util/errors"
	"k8s.io/client-go/tools/record"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/cache"
	"sigs.k8s.io/controller-runtime/pkg/cache/informertest"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"

	"github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"
	"github.com/tidbcloud/shared-provider/pkg/scheme"
	"github.com/tidbcloud/shared-provider/pkg/types"
)

const (
	recorderBufSize = 4096
	nodeName        = "node1"
)

func TestReconcile(t *testing.T) {
	cases := []struct {
		namespace   string
		name        string
		throttleErr error
		found       bool

		objs []k8sruntime.Object
	}{
		{
			namespace:   "ns1",
			name:        "cluster-not-found",
			throttleErr: nil,
			found:       false,
		},
		{
			namespace:   "ns2",
			name:        "cluster-without-pod",
			throttleErr: nil,
			found:       true,
			objs: []k8sruntime.Object{
				&v1alpha1.TidbCluster{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: "ns2",
						Name:      "cluster-without-pod",
					},
				},
			},
		},
		{
			namespace:   "ns3",
			name:        "cluster-pod-label-mismatch",
			throttleErr: nil,
			found:       true,
			objs: []k8sruntime.Object{
				&v1alpha1.TidbCluster{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: "ns3",
						Name:      "cluster-pod-label-mismatch",
					},
				},
				&corev1.Pod{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: "ns3",
						Name:      "cluster-pod-label-mismatch-tidb-0",
						Labels: map[string]string{
							labelKeyManagedBy: labelValTiDBOperator,
							labelKeyComponent: labelValTiDB,
							labelKeyName:      labelValTiDBCluster,
							labelKeyInstance:  "another-tc",
						},
					},
				},
			},
		},
		{
			namespace:   "ns4",
			name:        "cluster-with-pod",
			throttleErr: nil,
			found:       true,
			objs: []k8sruntime.Object{
				&v1alpha1.TidbCluster{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: "ns4",
						Name:      "cluster-with-pod",
					},
				},
				&corev1.Pod{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: "ns4",
						Name:      "cluster-with-pod-tidb-0",
						Labels: map[string]string{
							labelKeyManagedBy: labelValTiDBOperator,
							labelKeyComponent: labelValTiDB,
							labelKeyName:      labelValTiDBCluster,
							labelKeyInstance:  "cluster-with-pod",
						},
					},
				},
			},
		},
		{
			namespace:   "ns5",
			name:        "cluster-wrong-anno",
			throttleErr: nil, // no error reported now
			found:       true,
			objs: []k8sruntime.Object{
				&v1alpha1.TidbCluster{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: "ns5",
						Name:      "cluster-wrong-anno",
						Annotations: map[string]string{
							types.AnnoKeyIngress:     "wrong-anno",
							types.AnnoKeyIngressPort: "123",
							types.AnnoKeyEgress:      "3kib/s",
							types.AnnoKeyEgressPort:  "456",
						},
					},
				},
			},
		},
		{
			namespace:   "ns6",
			name:        "cluster-right-anno",
			throttleErr: nil,
			found:       true,
			objs: []k8sruntime.Object{
				&v1alpha1.TidbCluster{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: "ns6",
						Name:      "cluster-right-anno",
						Annotations: map[string]string{
							types.AnnoKeyIngress:     "2mib/s",
							types.AnnoKeyIngressPort: "123",
							types.AnnoKeyEgress:      "3kib/s",
							types.AnnoKeyEgressPort:  "456",
						},
					},
				},
			},
		},
		{
			namespace:   "ns7",
			name:        "cluster-throttle-error",
			throttleErr: errors.New("fake throttle error"),
			found:       true,
			objs: []k8sruntime.Object{
				&v1alpha1.TidbCluster{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: "ns7",
						Name:      "cluster-throttle-error",
					},
				},
				&corev1.Pod{
					ObjectMeta: metav1.ObjectMeta{
						Namespace: "ns7",
						Name:      "cluster-throttle-error-tidb-0",
						Labels: map[string]string{
							labelKeyManagedBy: labelValTiDBOperator,
							labelKeyComponent: labelValTiDB,
							labelKeyName:      labelValTiDBCluster,
							labelKeyInstance:  "cluster-throttle-error",
						},
					},
				},
			},
		},
	}

	for _, c := range cases {
		t.Log("test", c.name)

		r := newFakeReconciler(c.objs, c.throttleErr)

		req := ctrl.Request{
			NamespacedName: apitypes.NamespacedName{
				Namespace: c.namespace,
				Name:      c.name,
			},
		}
		_, err := r.Reconcile(context.Background(), req)
		if err != nil {
			aggErr, ok := err.(utilerrors.Aggregate)
			assert.True(t, ok)
			assert.True(t, aggErr.Is(c.throttleErr))
			continue // skip following asserts
		}

		tc := &v1alpha1.TidbCluster{}
		err = r.Client.Get(context.Background(), client.ObjectKey{Namespace: c.namespace, Name: c.name}, tc)
		if c.found {
			assert.NoError(t, err)
		} else {
			assert.True(t, apierrors.IsNotFound(err))
		}
	}
}

func newFakeReconciler(objs []k8sruntime.Object, err error) *Reconciler {
	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme.Scheme).
		WithRuntimeObjects(objs...).
		Build()
	fCache := fakeCache{
		Reader:    fakeClient,
		Informers: &informertest.FakeInformers{},
	}
	fakeRecorder := record.NewFakeRecorder(recorderBufSize)

	return &Reconciler{
		Logger:   logr.Discard(),
		Client:   fakeClient,
		Cache:    fCache,
		Recorder: fakeRecorder,
		NodeName: nodeName,
		t:        &fakeThrottler{err: err},
	}
}

type fakeThrottler struct {
	err error
}

func (t *fakeThrottler) Throttle(*corev1.Pod, *ThrottleConfig) error {
	return t.err
}

type fakeCache struct {
	client.Reader
	cache.Informers
}
