package reconciler

import (
	"context"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	. "github.com/onsi/gomega"
	"github.com/pingcap/errors"
	kerrors "k8s.io/apimachinery/pkg/api/errors"
	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	"github.com/tidbcloud/eks-provider/controllers/reconciler/test"
)

const (
	finalizer = "backup.cloud.pingcap.com/finalizer"
)

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

	type args struct {
		obj               client.Client
		externalConnector ExternalObjConnecter
		resource          func() client.Object
	}

	type want struct {
		result reconcile.Result
		err    error
	}

	errBoom := errors.New("boom")

	cases := map[string]struct {
		reason string
		args   args
		want   want
	}{
		"GetResourceErr": {
			reason: "getting the resource failed",
			args: args{
				obj: &test.MockClient{MockGet: test.NewMockGetFn(errBoom)},
				resource: func() client.Object {
					return &test.MockResource{}
				},
			},
			want: want{
				err: errors.Wrap(errBoom, errGetResource),
			},
		},

		"ResourceNotFound": {
			reason: "Not found resource",
			args: args{
				obj: &test.MockClient{MockGet: test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, ""))},
				resource: func() client.Object {
					return &test.MockResource{}
				},
			},
			want: want{
				result: reconcile.Result{},
			},
		},
		"ExternalConnectError": {
			reason: "connect to cluster failed",
			args: args{
				obj: &test.MockClient{
					MockGet: test.NewMockGetFn(nil),
				},
				resource: func() client.Object {
					return &test.MockResource{}
				},
				externalConnector: MockExternalConnector{
					ExternalConnectorFn: func(ctx context.Context, obj client.Object) (ExternalObjClient, error) {
						return nil, errBoom
					},
				},
			},
			want: want{
				result: reconcile.Result{Requeue: true},
				err:    errBoom,
			},
		},
		"Observe": {
			reason: "observe finalizer",
			args: args{
				obj: &test.MockClient{
					MockGet: test.NewMockGetFn(nil),
					MockUpdate: test.MockUpdateFn(func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
						mr, _ := obj.(*test.MockResource)
						existedFinalizer := controllerutil.ContainsFinalizer(mr, finalizer)
						g.Expect(existedFinalizer).Should(Equal(true))

						return nil
					}),
				},
				resource: func() client.Object {
					return &test.MockResource{}
				},
				externalConnector: MockExternalConnector{
					ExternalConnectorFn: func(ctx context.Context, obj client.Object) (ExternalObjClient, error) {
						c := &MockExternalObjClient{
							ObserveFn: func(ctx context.Context, obj client.Object) (ExternalObservation, error) {
								return ExternalObservation{ResourceExists: false}, nil
							},
							CreateFn: func(ctx context.Context, obj client.Object) error {
								return nil
							},
						}

						return c, nil
					},
				},
			},
			want: want{
				result: reconcile.Result{Requeue: true},
			},
		},
		"CreateExternalError": {
			reason: "create to cluster failed",
			args: args{
				obj: &test.MockClient{
					MockGet: test.NewMockGetFn(nil),
					MockUpdate: test.MockUpdateFn(func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
						return nil
					}),
				},
				resource: func() client.Object {
					return &test.MockResource{}
				},
				externalConnector: MockExternalConnector{
					ExternalConnectorFn: func(ctx context.Context, obj client.Object) (ExternalObjClient, error) {
						c := &MockExternalObjClient{
							ObserveFn: func(ctx context.Context, obj client.Object) (ExternalObservation, error) {
								return ExternalObservation{ResourceExists: false}, nil
							},
							CreateFn: func(ctx context.Context, obj client.Object) error {
								return errBoom
							},
						}

						return c, nil
					},
				},
			},
			want: want{
				result: reconcile.Result{Requeue: true},
				err:    errBoom,
			},
		},
		"ExternalResourceUpToDate": {
			reason: "the resource is up to date",
			args: args{
				obj: &test.MockClient{
					MockGet: test.NewMockGetFn(nil),
					MockUpdate: test.MockUpdateFn(func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
						mr, _ := obj.(*test.MockResource)
						g.Expect(mr.Status.Status).Should(Equal("Complete"))
						return nil
					}),
				},
				resource: func() client.Object {
					r := &test.MockResource{}
					f := r.GetFinalizers()
					r.SetFinalizers(append(f, finalizer))

					return r
				},
				externalConnector: MockExternalConnector{
					ExternalConnectorFn: func(ctx context.Context, obj client.Object) (ExternalObjClient, error) {
						c := &MockExternalObjClient{
							ObserveFn: func(ctx context.Context, obj client.Object) (ExternalObservation, error) {
								mr, _ := obj.(*test.MockResource)
								mr.Status.Status = "Complete"
								return ExternalObservation{ResourceExists: true, ResourceUpToDate: true}, nil
							},
							CreateFn: func(ctx context.Context, obj client.Object) error {
								return errBoom
							},
						}

						return c, nil
					},
				},
			},
			want: want{
				result: reconcile.Result{Requeue: false},
			},
		},
		"WaitExternalResourceDeleted": {
			reason: "",
			args: args{
				obj: &test.MockClient{
					MockGet: test.NewMockGetFn(nil),
					MockUpdate: test.MockUpdateFn(func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
						mr, _ := obj.(*test.MockResource)
						existedFinalizer := controllerutil.ContainsFinalizer(mr, finalizer)
						g.Expect(existedFinalizer).Should(Equal(true))
						return nil
					}),
				},
				resource: func() client.Object {
					r := &test.MockResource{}
					f := r.GetFinalizers()
					r.SetFinalizers(append(f, finalizer))
					r.SetDeletionTimestamp(&v1.Time{
						Time: time.Now(),
					})

					return r
				},
				externalConnector: MockExternalConnector{
					ExternalConnectorFn: func(ctx context.Context, obj client.Object) (ExternalObjClient, error) {
						c := &MockExternalObjClient{
							ObserveFn: func(ctx context.Context, obj client.Object) (ExternalObservation, error) {
								mr, _ := obj.(*test.MockResource)
								mr.Status.Status = "Complete"
								return ExternalObservation{ResourceExists: true, ResourceUpToDate: true}, nil
							},
							CreateFn: func(ctx context.Context, obj client.Object) error {
								return errBoom
							},
							DeleteFn: func(ctx context.Context, obj client.Object) (ExternalDeletion, error) {
								return ExternalDeletion{ResourceDeleted: false}, nil
							},
						}

						return c, nil
					},
				},
			},
			want: want{
				result: reconcile.Result{Requeue: true},
			},
		},
		"DeleteSuccessful": {
			reason: "",
			args: args{
				obj: &test.MockClient{
					MockGet: test.NewMockGetFn(nil),
					MockUpdate: test.MockUpdateFn(func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
						return nil
					}),
				},
				resource: func() client.Object {
					r := &test.MockResource{}
					f := r.GetFinalizers()
					r.SetFinalizers(append(f, finalizer))
					r.SetDeletionTimestamp(&v1.Time{
						Time: time.Now(),
					})

					return r
				},
				externalConnector: MockExternalConnector{
					ExternalConnectorFn: func(ctx context.Context, obj client.Object) (ExternalObjClient, error) {
						c := &MockExternalObjClient{
							ObserveFn: func(ctx context.Context, obj client.Object) (ExternalObservation, error) {
								mr, _ := obj.(*test.MockResource)
								mr.Status.Status = "Complete"
								return ExternalObservation{ResourceExists: true, ResourceUpToDate: true}, nil
							},
							CreateFn: func(ctx context.Context, obj client.Object) error {
								return errBoom
							},
							DeleteFn: func(ctx context.Context, obj client.Object) (ExternalDeletion, error) {
								return ExternalDeletion{ResourceDeleted: true}, nil
							},
						}

						return c, nil
					},
				},
			},
			want: want{
				result: reconcile.Result{Requeue: false},
			},
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			r := Reconciler{
				Client:      tc.args.obj,
				NewResource: tc.args.resource,
				ExternalObj: tc.args.externalConnector,
			}
			got, err := r.Reconcile(context.Background(), reconcile.Request{}, finalizer)

			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
				t.Errorf("\nReason: %s\nr.Reconcile(...): -want error, +got error:\n%s", tc.reason, diff)
			}

			if diff := cmp.Diff(tc.want.result, got); diff != "" {
				t.Errorf("\nReason: %s\nr.Reconcile(...): -want, +got:\n%s", tc.reason, diff)
			}
		})
	}
}

type MockExternalConnector struct {
	ExternalConnectorFn func(ctx context.Context, obj client.Object) (ExternalObjClient, error)
}

func (m MockExternalConnector) Connect(ctx context.Context, obj client.Object) (ExternalObjClient, error) {
	return m.ExternalConnectorFn(ctx, obj)
}

type MockExternalObjClient struct {
	ObserveFn func(ctx context.Context, obj client.Object) (ExternalObservation, error)

	CreateFn func(ctx context.Context, obj client.Object) error

	UpdateFn func(ctx context.Context, obj client.Object) error

	DeleteFn func(ctx context.Context, obj client.Object) (ExternalDeletion, error)
}

func (e MockExternalObjClient) Observe(ctx context.Context, obj client.Object) (ExternalObservation, error) {
	return e.ObserveFn(ctx, obj)
}

func (e MockExternalObjClient) Create(ctx context.Context, obj client.Object) error {
	return e.CreateFn(ctx, obj)
}

func (e MockExternalObjClient) Update(ctx context.Context, obj client.Object) error {
	return e.UpdateFn(ctx, obj)
}

func (e MockExternalObjClient) Delete(ctx context.Context, obj client.Object) (ExternalDeletion, error) {
	return e.DeleteFn(ctx, obj)
}
