package applier

import (
	"context"
	"testing"

	"github.com/go-logr/logr"
	"github.com/stretchr/testify/assert"
	appsv1 "k8s.io/api/apps/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	"github.com/tidbcloud/infra-provider/regional-server/pkg/applier/mutatefuncs"
)

func TestApply(t *testing.T) {
	cases := []struct {
		desc     string
		current  *appsv1.Deployment
		expected *appsv1.Deployment
		result   controllerutil.OperationResult
	}{
		{
			desc:    "obj doesn't exist",
			current: nil,
			expected: &appsv1.Deployment{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test",
					Namespace: "test",
					Labels:    map[string]string{"aaa": "bbb"},
				},
				Spec: appsv1.DeploymentSpec{},
			},
			result: controllerutil.OperationResultCreated,
		},
		{
			desc: "hash doesn't exist",
			current: &appsv1.Deployment{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test",
					Namespace: "test",
				},
				Spec: appsv1.DeploymentSpec{},
			},
			expected: &appsv1.Deployment{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test",
					Namespace: "test",
					Labels:    map[string]string{"aaa": "bbb"},
				},
				Spec: appsv1.DeploymentSpec{},
			},
			result: controllerutil.OperationResultUpdated,
		},
		{
			desc: "hash is not equal before",
			current: &appsv1.Deployment{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test",
					Namespace: "test",
					Annotations: map[string]string{
						AnnoLastAppliedHash: "123456",
					},
				},
				Spec: appsv1.DeploymentSpec{},
			},
			expected: &appsv1.Deployment{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test",
					Namespace: "test",
					Labels:    map[string]string{"aaa": "bbb"},
				},
				Spec: appsv1.DeploymentSpec{},
			},
			result: controllerutil.OperationResultUpdated,
		},
		{
			desc: "hash is equal before",
			current: &appsv1.Deployment{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test",
					Namespace: "test",
					Annotations: map[string]string{
						AnnoLastAppliedHash: "1428925143192456447",
					},
				},
				Spec: appsv1.DeploymentSpec{},
			},
			expected: &appsv1.Deployment{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test",
					Namespace: "test",
					Labels:    map[string]string{"aaa": "bbb"},
				},
				Spec: appsv1.DeploymentSpec{},
			},
			result: controllerutil.OperationResultNone,
		},
	}

	for _, c := range cases {
		var fc client.Client
		if c.current == nil {
			fc = fakeClient()
		} else {
			fc = fakeClient(c.current)
		}
		sink := &testLogSink{}
		ctx := logr.NewContext(context.Background(), logr.New(sink))

		obj, err := NewApplier(fc, mutatefuncs.MutateDeployment).Expected(c.expected).Apply(ctx)
		assert.NoError(t, err)

		assert.NoError(t, err)

		anno := obj.GetAnnotations()
		cur, ok := anno[AnnoLastAppliedHash]
		hash, err := Hash(c.expected)
		assert.NoError(t, err)
		assert.True(t, ok)
		assert.Equal(t, hash, cur)

		assert.Equal(t, c.result, sink.result)
	}
}

type testLogSink struct {
	result controllerutil.OperationResult
}

func (l *testLogSink) Init(logr.RuntimeInfo) {
}

func (l *testLogSink) Enabled(int) bool {
	return true
}

func (l *testLogSink) Info(level int, msg string, args ...interface{}) {
	// the 8th arg is apply result
	l.result = args[7].(controllerutil.OperationResult)
}

func (l *testLogSink) Error(error, string, ...interface{}) {
}

func (l *testLogSink) WithValues(...interface{}) logr.LogSink {
	return l
}

func (l *testLogSink) WithName(string) logr.LogSink {
	return l
}

func fakeClient(current ...client.Object) client.Client {
	return fake.NewClientBuilder().WithObjects(current...).Build()
}
