package s3

import (
	"context"
	"errors"
	"os"
	"testing"

	. "github.com/onsi/gomega"
	observabilitypingcapcomv1 "github.com/tidbcloud/observability/api/regionalserver/resourceworker/v1"
	"github.com/tidbcloud/observability/pkg/common/constants"
	"github.com/tidbcloud/observability/pkg/common/message"
	"github.com/tidbcloud/observability/pkg/common/metrics"
	"github.com/tidbcloud/observability/pkg/common/structs"
	"github.com/tidbcloud/observability/pkg/resourceworker/controllers/fake"
	mock_manager "github.com/tidbcloud/observability/pkg/resourceworker/controllers/fake/manager"
	"github.com/tidbcloud/observability/pkg/resourceworker/controllers/reconciler"
	"github.com/tidbcloud/observability/pkg/resourceworker/providers"
	mocks "github.com/tidbcloud/observability/test/mockresourceworker/providers"
	"go.uber.org/mock/gomock"
	"k8s.io/apimachinery/pkg/runtime"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	ctrlconfig "sigs.k8s.io/controller-runtime/pkg/config"
)

type args struct {
	cli      func(g Gomega) client.Client
	s3cli    func(ctrl *gomock.Controller, g Gomega) providers.S3Client
	resource func() client.Object
}

type want struct {
	result  interface{}
	err     error
	paniced bool
}
type caseType map[string]struct {
	args args
	want want
}

func TestS3ControllerBuildStruct(t *testing.T) {
	g := NewGomegaWithT(t)
	ctl := gomock.NewController(t)
	defer ctl.Finish()
	manager := mock_manager.NewMockManager(ctl)
	errBoom := errors.New("boom")
	mockCli := &fake.MockClient{
		MockGet: fake.MockGetFn(func(ctx context.Context, key client.ObjectKey, obj runtime.Object) error {
			return errBoom
		}),
	}
	scheme := runtime.NewScheme()
	log := ctrl.Log.WithName("controllers").WithName("S3")
	region := "testregion"
	serviceinfo := structs.ServiceInfo{
		Region: region,
	}
	manager.EXPECT().GetClient().AnyTimes().Return(mockCli)
	s3r := NewS3Reconciler(manager, log, serviceinfo)
	g.Expect(s3r.Log).Should(Equal(log))
	g.Expect(s3r.reconciler.Client).Should(Equal(mockCli))
	utilruntime.Must(observabilitypingcapcomv1.AddToScheme(scheme))
	// manager.EXPECT().SetFields(gomock.Any()).AnyTimes().Return(nil)
	manager.EXPECT().Add(gomock.Any()).AnyTimes().Return(nil)
	manager.EXPECT().GetControllerOptions().Return(ctrlconfig.Controller{}).AnyTimes()
	manager.EXPECT().GetScheme().Return(scheme)
	manager.EXPECT().GetCache().Return(nil)
	manager.EXPECT().GetLogger().Return(log)
	g.Expect(s3r.SetupWithManager(manager)).Should(Succeed())
	g.Expect(s3r.Reconcile(context.TODO(), ctrl.Request{})).Error().Should(HaveOccurred())
}

func TestS3ControllerConnect(t *testing.T) {
	os.Setenv("AWS_ACCESS_KEY_ID", "test1")
	os.Setenv("AWS_SECRET_ACCESS_KEY", "testsec")
	os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "../../../../test/files/application_default_credentials.json")
	defer os.Unsetenv("GOOGLE_APPLICATION_CREDENTIALS")
	defer os.Unsetenv("AWS_SECRET_ACCESS_KEY")
	defer os.Unsetenv("AWS_ACCESS_KEY_ID")
	g := NewGomegaWithT(t)
	var connect = s3Connector{
		serviceInfo: structs.ServiceInfo{
			Vendor: string(constants.AWS),
		},
	}
	g.Expect(connect.Connect(context.TODO(), &observabilitypingcapcomv1.IAM{})).Error().Should(Succeed())
	connect.serviceInfo.Vendor = string(constants.GCP)
	g.Expect(connect.Connect(context.TODO(), &observabilitypingcapcomv1.IAM{})).Error().Should(Succeed())
	connect.serviceInfo.Vendor = string(constants.Azure)
	g.Expect(connect.Connect(context.TODO(), &observabilitypingcapcomv1.IAM{})).Error().Should(Succeed())

	os.Setenv("ALIBABA_CLOUD_ACCESS_KEY_ID", "test1")
	os.Setenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET", "testsec")
	defer os.Unsetenv("ALIBABA_CLOUD_ACCESS_KEY_ID")
	defer os.Unsetenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET")
	connect.serviceInfo.Vendor = string(constants.AliCloud)

	g.Expect(connect.Connect(context.TODO(), &observabilitypingcapcomv1.IAM{})).Error().Should(Succeed())

	connect.serviceInfo.Vendor = "ttt"
	g.Expect(connect.Connect(context.TODO(), &observabilitypingcapcomv1.IAM{})).Error()
}

func TestS3ControllerObserve(t *testing.T) {
	metrics.SetupMetrics()
	errBoom := errors.New("boom")
	cases := caseType{
		"ObserveErrorResourceType": {
			args: args{
				cli: func(g Gomega) client.Client {
					return &fake.MockClient{}
				},
				s3cli: func(ctl *gomock.Controller, g Gomega) providers.S3Client {
					return mocks.NewMockS3Client(ctl)
				},
				resource: func() client.Object {
					return &observabilitypingcapcomv1.IAM{}
				},
			},
			want: want{
				result: reconciler.ExternalObservation{Droped: true},
				err:    errors.New(errNotS3),
			},
		},
		"ObserveFinalStatus": {
			args: args{
				cli: func(g Gomega) client.Client {
					return &fake.MockClient{}
				},
				s3cli: func(ctl *gomock.Controller, g Gomega) providers.S3Client {
					return mocks.NewMockS3Client(ctl)
				},
				resource: func() client.Object {
					res := &observabilitypingcapcomv1.S3{}
					res.Status.Phase = constants.CREATION_FAILED
					return res
				},
			},
			want: want{
				result: reconciler.ExternalObservation{ResourceUpToDate: true, ResourceExists: true},
			},
		},
		"ObserveBucketExistsError": {
			args: args{
				cli: func(g Gomega) client.Client {
					return &fake.MockClient{}
				},
				s3cli: func(ctl *gomock.Controller, g Gomega) providers.S3Client {
					cli := mocks.NewMockS3Client(ctl)
					cli.EXPECT().BucketExists(context.Background(), gomock.Any()).Return(false, errBoom)
					return cli
				},
				resource: func() client.Object {
					res := &observabilitypingcapcomv1.S3{}
					res.Spec.Name = "test1"
					return res
				},
			},
			want: want{
				result: reconciler.ExternalObservation{},
				err:    errBoom,
			},
		},
		"ObserveBucketNotExists": {
			args: args{
				cli: func(g Gomega) client.Client {
					return &fake.MockClient{}
				},
				s3cli: func(ctl *gomock.Controller, g Gomega) providers.S3Client {
					cli := mocks.NewMockS3Client(ctl)
					cli.EXPECT().BucketExists(context.Background(), gomock.Any()).Return(false, nil)
					return cli
				},
				resource: func() client.Object {
					res := &observabilitypingcapcomv1.S3{}
					res.Spec.Name = "test1"
					return res
				},
			},
			want: want{
				result: reconciler.ExternalObservation{ResourceExists: false},
			},
		},
		"ObserveBucketExists": {
			args: args{
				cli: func(g Gomega) client.Client {
					return &fake.MockClient{}
				},
				s3cli: func(ctl *gomock.Controller, g Gomega) providers.S3Client {
					cli := mocks.NewMockS3Client(ctl)
					cli.EXPECT().BucketExists(context.Background(), gomock.Any()).Return(true, nil)
					return cli
				},
				resource: func() client.Object {
					res := &observabilitypingcapcomv1.S3{}
					res.Spec.Name = "test1"
					return res
				},
			},
			want: want{
				result: reconciler.ExternalObservation{ResourceExists: true},
			},
		},
	}
	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			g := NewGomegaWithT(t)
			ctl := gomock.NewController(t)
			defer ctl.Finish()
			r := S3{
				s3cli: tc.args.s3cli(ctl, g),
				cli:   tc.args.cli(g),
			}
			if tc.want.paniced {
				g.Expect(func() { r.Observe(context.Background(), tc.args.resource()) }).Should(PanicWith(errNotS3))
			} else {
				got, err := r.Observe(context.Background(), tc.args.resource())
				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))
			}

		})
	}
}

func TestS3ControllerCreate(t *testing.T) {
	metrics.SetupMetrics()
	errBoom := errors.New("boom")
	cases := caseType{
		"CreateErrorResourceType": {
			args: args{
				cli: func(g Gomega) client.Client {
					return &fake.MockClient{}
				},
				s3cli: func(ctl *gomock.Controller, g Gomega) providers.S3Client {
					return mocks.NewMockS3Client(ctl)
				},
				resource: func() client.Object {
					return &observabilitypingcapcomv1.IAM{}
				},
			},
			want: want{
				err: errors.New(errNotS3),
			},
		},
		"CreateUpdateStatusFailed": {
			args: args{
				cli: func(g Gomega) client.Client {
					return &fake.MockClient{
						MockStatusUpdate: fake.MockStatusUpdateFn(func(ctx context.Context, obj client.Object, opts ...client.SubResourceUpdateOption) error {
							return errBoom
						}),
					}
				},
				s3cli: func(ctl *gomock.Controller, g Gomega) providers.S3Client {
					return mocks.NewMockS3Client(ctl)
				},
				resource: func() client.Object {
					res := &observabilitypingcapcomv1.S3{}
					return res
				},
			},
			want: want{
				err: errBoom,
			},
		},
		"CreateCreateBucketError": {
			args: args{
				cli: func(g Gomega) client.Client {
					return &fake.MockClient{
						MockStatusUpdate: fake.MockStatusUpdateFn(func(ctx context.Context, obj client.Object, opts ...client.SubResourceUpdateOption) error {
							return nil
						}),
					}
				},
				s3cli: func(ctl *gomock.Controller, g Gomega) providers.S3Client {
					cli := mocks.NewMockS3Client(ctl)
					cli.EXPECT().CreateBucket(context.Background(), gomock.Any()).Return(errBoom)
					return cli
				},
				resource: func() client.Object {
					res := &observabilitypingcapcomv1.S3{}
					return res
				},
			},
			want: want{
				err: errBoom,
			},
		},
		"CreateCreateBucketSuccess": {
			args: args{
				cli: func(g Gomega) client.Client {
					return &fake.MockClient{
						MockStatusUpdate: fake.MockStatusUpdateFn(func(ctx context.Context, obj client.Object, opts ...client.SubResourceUpdateOption) error {
							return nil
						}),
					}
				},
				s3cli: func(ctl *gomock.Controller, g Gomega) providers.S3Client {
					cli := mocks.NewMockS3Client(ctl)
					cli.EXPECT().CreateBucket(context.Background(), gomock.Any()).Return(nil)
					return cli
				},
				resource: func() client.Object {
					res := &observabilitypingcapcomv1.S3{}
					res.Spec.Name = "test1"
					return res
				},
			},
			want: want{
				err: nil,
			},
		},
	}
	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			g := NewGomegaWithT(t)
			ctl := gomock.NewController(t)
			defer ctl.Finish()
			r := S3{
				s3cli: tc.args.s3cli(ctl, g),
				cli:   tc.args.cli(g),
			}
			if tc.want.paniced {
				g.Expect(func() { r.Create(context.Background(), tc.args.resource()) }).Should(PanicWith(errNotS3))
			} else {
				err := r.Create(context.Background(), tc.args.resource())
				if tc.want.err == nil {
					g.Expect(err).Should(BeNil())
				} else {
					g.Expect(err).Should(Equal(tc.want.err))
				}
			}

		})
	}
}

func TestS3ControllerUpdate(t *testing.T) {
	metrics.SetupMetrics()
	errBoom := errors.New("boom")
	cases := caseType{
		"UpdateErrorResourceType": {
			args: args{
				cli: func(g Gomega) client.Client {
					return &fake.MockClient{}
				},
				s3cli: func(ctl *gomock.Controller, g Gomega) providers.S3Client {
					return mocks.NewMockS3Client(ctl)
				},
				resource: func() client.Object {
					return &observabilitypingcapcomv1.IAM{}
				},
			},
			want: want{
				err: errors.New(errNotS3),
			},
		},
		"UpdateStatusFailed": {
			args: args{
				cli: func(g Gomega) client.Client {
					return &fake.MockClient{
						MockStatusUpdate: fake.MockStatusUpdateFn(func(ctx context.Context, obj client.Object, opts ...client.SubResourceUpdateOption) error {
							return errBoom
						}),
					}
				},
				s3cli: func(ctl *gomock.Controller, g Gomega) providers.S3Client {
					return mocks.NewMockS3Client(ctl)
				},
				resource: func() client.Object {
					res := &observabilitypingcapcomv1.S3{}
					return res
				},
			},
			want: want{
				err: errBoom,
			},
		},
		"UpdateStatusSuccess": {
			args: args{
				cli: func(g Gomega) client.Client {
					return &fake.MockClient{
						MockStatusUpdate: fake.MockStatusUpdateFn(func(ctx context.Context, obj client.Object, opts ...client.SubResourceUpdateOption) error {
							return nil
						}),
					}
				},
				s3cli: func(ctl *gomock.Controller, g Gomega) providers.S3Client {
					return mocks.NewMockS3Client(ctl)
				},
				resource: func() client.Object {
					res := &observabilitypingcapcomv1.S3{}
					res.Spec.Name = "test1"
					return res
				},
			},
			want: want{
				err: nil,
			},
		},
		"UpdateAppliesRetentionLifecycle": {
			args: args{
				cli: func(g Gomega) client.Client {
					return &fake.MockClient{
						MockStatusUpdate: fake.MockStatusUpdateFn(func(ctx context.Context, obj client.Object, opts ...client.SubResourceUpdateOption) error {
							return nil
						}),
					}
				},
				s3cli: func(ctl *gomock.Controller, g Gomega) providers.S3Client {
					cli := mocks.NewMockS3Client(ctl)
					cli.EXPECT().AddLifecycle(gomock.Any(), "tenant-bucket",
						[]message.LifecycleRuleParam{{RuleName: s3ExportExpirationRuleName, ExpirationDays: 14}},
						nil).Return(nil)
					return cli
				},
				resource: func() client.Object {
					res := &observabilitypingcapcomv1.S3{}
					res.Spec.Name = "tenant-bucket"
					res.Spec.ExpirationDays = 14
					return res
				},
			},
			want: want{
				err: nil,
			},
		},
		"UpdateRetentionLifecycleFails": {
			args: args{
				cli: func(g Gomega) client.Client {
					return &fake.MockClient{}
				},
				s3cli: func(ctl *gomock.Controller, g Gomega) providers.S3Client {
					cli := mocks.NewMockS3Client(ctl)
					cli.EXPECT().AddLifecycle(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(errBoom)
					return cli
				},
				resource: func() client.Object {
					res := &observabilitypingcapcomv1.S3{}
					res.Spec.Name = "tenant-bucket"
					res.Spec.ExpirationDays = 14
					return res
				},
			},
			want: want{
				err: errBoom,
			},
		},
	}
	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			g := NewGomegaWithT(t)
			ctl := gomock.NewController(t)
			defer ctl.Finish()
			r := S3{
				s3cli: tc.args.s3cli(ctl, g),
				cli:   tc.args.cli(g),
			}
			if tc.want.paniced {
				g.Expect(func() { r.Update(context.Background(), tc.args.resource()) }).Should(PanicWith(errNotS3))
			} else {
				err := r.Update(context.Background(), tc.args.resource())
				if tc.want.err == nil {
					g.Expect(err).Should(BeNil())
				} else {
					g.Expect(err).Should(Equal(tc.want.err))
				}
			}

		})
	}
}

func TestS3ControllerDelete(t *testing.T) {
	metrics.SetupMetrics()
	g := NewGomegaWithT(t)
	ctl := gomock.NewController(t)
	defer ctl.Finish()
	r := S3{
		s3cli: mocks.NewMockS3Client(ctl),
		cli:   &fake.MockClient{},
	}
	g.Expect(r.Delete(context.Background(), &observabilitypingcapcomv1.S3{})).Should(Equal(reconciler.ExternalDeletion{ResourceDeleted: true}))
}
