package reconciler

import (
	"context"
	"testing"
	"time"

	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	"errors"

	"github.com/tidbcloud/observability/pkg/resourceworker/controllers/fake"

	kerrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/runtime/schema"

	. "github.com/onsi/gomega"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

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: &fake.MockClient{MockGet: fake.NewMockGetFn(errBoom)},
				resource: func() client.Object {
					return &fake.MockResource{}
				},
			},
			want: want{
				err: errBoom,
			},
		},

		"ResourceNotFound": {
			reason: "Not found resource",
			args: args{
				obj: &fake.MockClient{MockGet: fake.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, ""))},
				resource: func() client.Object {
					return &fake.MockResource{}
				},
			},
			want: want{
				result: reconcile.Result{},
			},
		},
		"ExternalConnectError": {
			reason: "connect to cluster failed",
			args: args{
				obj: &fake.MockClient{
					MockGet: fake.NewMockGetFn(nil),
				},
				resource: func() client.Object {
					return &fake.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: &fake.MockClient{
					MockGet: fake.NewMockGetFn(nil),
					MockUpdate: fake.MockUpdateFn(func(ctx context.Context, obj runtime.Object, opts ...client.UpdateOption) error {
						mr, _ := obj.(*fake.MockResource)
						existedFinalizer := controllerutil.ContainsFinalizer(mr, finalizer)
						g.Expect(existedFinalizer).Should(Equal(true))

						return nil
					}),
				},
				resource: func() client.Object {
					return &fake.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},
			},
		},
		"ObserveFailed": {
			reason: "observe failed",
			args: args{
				obj: &fake.MockClient{
					MockGet: fake.NewMockGetFn(nil),
				},
				resource: func() client.Object {
					return &fake.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{}, errBoom
							},
						}

						return c, nil
					},
				},
			},
			want: want{
				result: reconcile.Result{Requeue: true},
				err:    errBoom,
			},
		},
		"ObserveFailedandDrop": {
			reason: "observe failed",
			args: args{
				obj: &fake.MockClient{
					MockGet: fake.NewMockGetFn(nil),
				},
				resource: func() client.Object {
					return &fake.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{Droped: true}, errBoom
							},
						}

						return c, nil
					},
				},
			},
			want: want{
				result: reconcile.Result{Requeue: false},
				err:    nil,
			},
		},
		"CreateExternalError": {
			reason: "create to cluster failed",
			args: args{
				obj: &fake.MockClient{
					MockGet: fake.NewMockGetFn(nil),
					MockUpdate: fake.MockUpdateFn(func(ctx context.Context, obj runtime.Object, opts ...client.UpdateOption) error {
						return nil
					}),
				},
				resource: func() client.Object {
					return &fake.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: &fake.MockClient{
					MockGet: fake.NewMockGetFn(nil),
					MockUpdate: fake.MockUpdateFn(func(ctx context.Context, obj runtime.Object, opts ...client.UpdateOption) error {
						mr, _ := obj.(*fake.MockResource)
						g.Expect(mr.Status.Status).Should(Equal("Complete"))
						return nil
					}),
				},
				resource: func() client.Object {
					r := &fake.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.(*fake.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: &fake.MockClient{
					MockGet: fake.NewMockGetFn(nil),
					MockUpdate: fake.MockUpdateFn(func(ctx context.Context, obj runtime.Object, opts ...client.UpdateOption) error {
						mr, _ := obj.(*fake.MockResource)
						existedFinalizer := controllerutil.ContainsFinalizer(mr, finalizer)
						g.Expect(existedFinalizer).Should(Equal(true))
						return nil
					}),
				},
				resource: func() client.Object {
					r := &fake.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.(*fake.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},
			},
		},
		"DeleteWaitResource": {
			reason: "",
			args: args{
				obj: &fake.MockClient{
					MockGet: fake.NewMockGetFn(nil),
					MockUpdate: fake.MockUpdateFn(func(ctx context.Context, obj runtime.Object, opts ...client.UpdateOption) error {
						return nil
					}),
				},
				resource: func() client.Object {
					r := &fake.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.(*fake.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{PollInterval: defaultpollInterval}, nil
							},
						}

						return c, nil
					},
				},
			},
			want: want{
				result: reconcile.Result{RequeueAfter: defaultpollInterval},
			},
		},
		"DeleteSuccessful": {
			reason: "",
			args: args{
				obj: &fake.MockClient{
					MockGet: fake.NewMockGetFn(nil),
					MockUpdate: fake.MockUpdateFn(func(ctx context.Context, obj runtime.Object, opts ...client.UpdateOption) error {
						return nil
					}),
				},
				resource: func() client.Object {
					r := &fake.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.(*fake.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},
			},
		},
		"WaitObserveNextTime": {
			reason: "the resource will be observed in next time",
			args: args{
				obj: &fake.MockClient{
					MockGet: fake.NewMockGetFn(nil),
					MockUpdate: fake.MockUpdateFn(func(ctx context.Context, obj runtime.Object, opts ...client.UpdateOption) error {
						mr, _ := obj.(*fake.MockResource)
						g.Expect(mr.Status.Status).Should(Equal("Complete"))
						return nil
					}),
				},
				resource: func() client.Object {
					r := &fake.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.(*fake.MockResource)
								mr.Status.Status = "Complete"
								return ExternalObservation{ResourceExists: true, ResourceUpToDate: false, PollInterval: 5}, nil
							},
							CreateFn: func(ctx context.Context, obj client.Object) error {
								return errBoom
							},
						}

						return c, nil
					},
				},
			},
			want: want{
				result: reconcile.Result{RequeueAfter: 5},
			},
		},
		"UpdateFailed": {
			reason: "update failed",
			args: args{
				obj: &fake.MockClient{
					MockGet: fake.NewMockGetFn(nil),
				},
				resource: func() client.Object {
					r := &fake.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) {
								return ExternalObservation{ResourceExists: true}, nil
							},
							UpdateFn: func(ctx context.Context, obj client.Object) error {
								return errBoom
							},
						}

						return c, nil
					},
				},
			},
			want: want{
				result: reconcile.Result{Requeue: true},
				err:    errBoom,
			},
		}, "UpdateSuccess": {
			reason: "update success",
			args: args{
				obj: &fake.MockClient{
					MockGet: fake.NewMockGetFn(nil),
				},
				resource: func() client.Object {
					r := &fake.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) {
								return ExternalObservation{ResourceExists: true}, nil
							},
							UpdateFn: func(ctx context.Context, obj client.Object) error {
								return nil
							},
						}

						return c, nil
					},
				},
			},
			want: want{
				result: reconcile.Result{RequeueAfter: defaultpollInterval},
			},
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			g := NewGomegaWithT(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 tc.want.err == nil {
				g.Expect(err).Should(BeNil())
			} else {
				g.Expect(err).Should(Equal(tc.want.err))
			}
			g.Expect(got).Should(Equal(tc.want.result))
		})
	}
}

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)
}

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

	sub1 := SubResourceObservation{
		ResourceExists:     true,
		PartResourceExists: true,
	}
	sub2 := SubResourceObservation{
		ResourceExists:     false,
		PartResourceExists: false,
	}
	sub3 := SubResourceObservation{
		ResourceExists:     true,
		PartResourceExists: false,
	}

	result := Combine(sub1, sub2)
	g.Expect(result.ResourceExists).Should(Equal(false))
	g.Expect(result.PartResourceExists).Should(Equal(true))

	result = Combine(sub1, sub3)
	g.Expect(result.ResourceExists).Should(Equal(true))
	g.Expect(result.PartResourceExists).Should(Equal(true))

}
