package clustermaintenance

import (
	"context"
	"errors"
	"time"

	maintent "github.com/tidbcloud/devops/entity/maintenance"

	"github.com/tidbcloud/devops/pkg/infra/utils/repoopt"
	testutil "github.com/tidbcloud/devops/pkg/infra/utils/test"
	cmxapi "github.com/tidbcloud/devops/pkg/service/clustermaintenance/api"
	"github.com/tidbcloud/devops/pkg/service/clustermaintenance/repo"
	opsewrepo "github.com/tidbcloud/devops/pkg/service/clustermaintenance/repo/ops_execution_window"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"go.uber.org/mock/gomock"
)

var _ = Describe("OpsExecutionWindowService", func() {
	var (
		ctrl    *gomock.Controller
		dbp     *testutil.MockTransProvider
		opsRepo *opsewrepo.MockRepo
		opsSvc  cmxapi.OpsExecutionWindowServiceAPI
		ctx     context.Context

		commitTxCB func(context.Context, func(context.Context) error) error
	)

	BeforeEach(func() {
		ctrl = gomock.NewController(GinkgoT())
		dbp = testutil.NewMockTransProvider(ctrl)
		opsRepo = opsewrepo.NewMockRepo(ctrl)
		opsSvc = &svc{
			dbp: dbp,
			repo: &repo.Repo{
				OpsExecutionWindow: opsRepo,
			},
		}
		ctx = context.Background()
		commitTxCB = func(txCtx context.Context, cb func(context.Context) error) error {
			return cb(txCtx)
		}
	})

	AfterEach(func() {
		ctrl.Finish()
	})

	It("delegates create and update to repo", func() {
		startAt := time.Now().Add(time.Hour).Unix()
		endAt := startAt + int64(time.Hour.Seconds())
		expectedCreated := &maintent.OpsExecutionWindow{
			ID:      1,
			StartAt: startAt,
			EndAt:   endAt,
		}
		dbp.EXPECT().Transaction(ctx, gomock.Any()).DoAndReturn(commitTxCB).Times(1)
		opsRepo.EXPECT().Create(ctx, &maintent.OpsExecutionWindow{
			StartAt: startAt,
			EndAt:   endAt,
		}).Return(expectedCreated, nil)

		created, err := opsSvc.CreateOpsExecutionWindow(ctx, cmxapi.CreateOpsExecutionWindowParam{
			StartAt: startAt,
			EndAt:   endAt,
		})
		Expect(err).ToNot(HaveOccurred())
		Expect(created).To(Equal(expectedCreated))

		updatedStartAt := startAt + int64(2*time.Hour.Seconds())
		updatedEndAt := endAt + int64(2*time.Hour.Seconds())
		expectedUpdated := &maintent.OpsExecutionWindow{
			ID:      1,
			StartAt: updatedStartAt,
			EndAt:   updatedEndAt,
		}
		dbp.EXPECT().Transaction(ctx, gomock.Any()).DoAndReturn(commitTxCB).Times(1)
		opsRepo.EXPECT().Update(ctx, &maintent.OpsExecutionWindow{
			ID:      1,
			StartAt: updatedStartAt,
			EndAt:   updatedEndAt,
		}).Return(expectedUpdated, nil)

		updated, err := opsSvc.UpdateOpsExecutionWindow(ctx, 1, cmxapi.UpdateOpsExecutionWindowParam{
			StartAt: updatedStartAt,
			EndAt:   updatedEndAt,
		})
		Expect(err).ToNot(HaveOccurred())
		Expect(updated).To(Equal(expectedUpdated))
	})

	It("lists with order and pager and returns total", func() {
		expectedItems := []*maintent.OpsExecutionWindow{
			{ID: 1, StartAt: 100, EndAt: 200},
		}
		opsRepo.EXPECT().List(ctx, gomock.Any(), gomock.Any()).DoAndReturn(
			func(_ context.Context, opts ...repoopt.Option) ([]*maintent.OpsExecutionWindow, error) {
				Expect(opts).To(HaveLen(2))
				order, ok := opts[0].(*repoopt.Order)
				Expect(ok).To(BeTrue())
				Expect(order.Column).To(Equal("updated_at"))
				Expect(order.Desc).To(BeTrue())

				pager, ok := opts[1].(*repoopt.Pager)
				Expect(ok).To(BeTrue())
				Expect(pager.Page).To(Equal(2))
				Expect(pager.PerPage).To(Equal(10))
				return expectedItems, nil
			},
		)
		opsRepo.EXPECT().Count(ctx).Return(int64(1), nil)

		items, total, err := opsSvc.ListOpsExecutionWindows(ctx, cmxapi.ListOpsExecutionWindowParam{
			Page:    2,
			PerPage: 10,
		})
		Expect(err).ToNot(HaveOccurred())
		Expect(items).To(Equal(expectedItems))
		Expect(total).To(Equal(int64(1)))
	})

	It("lists by ids without default pager", func() {
		expectedItems := []*maintent.OpsExecutionWindow{
			{ID: 1, StartAt: 100, EndAt: 200},
			{ID: 2, StartAt: 300, EndAt: 400},
		}
		opsRepo.EXPECT().List(ctx, gomock.Any(), gomock.Any()).DoAndReturn(
			func(_ context.Context, opts ...repoopt.Option) ([]*maintent.OpsExecutionWindow, error) {
				Expect(opts).To(HaveLen(2))
				filter, ok := opts[0].(*repoopt.Filter[uint64])
				Expect(ok).To(BeTrue())
				Expect(filter.Column).To(Equal("id"))
				Expect(filter.Values).To(ConsistOf(uint64(1), uint64(2)))

				order, ok := opts[1].(*repoopt.Order)
				Expect(ok).To(BeTrue())
				Expect(order.Column).To(Equal("updated_at"))
				Expect(order.Desc).To(BeTrue())
				return expectedItems, nil
			},
		)
		opsRepo.EXPECT().Count(ctx, gomock.Any()).DoAndReturn(
			func(_ context.Context, opts ...repoopt.Option) (int64, error) {
				Expect(opts).To(HaveLen(1))
				filter, ok := opts[0].(*repoopt.Filter[uint64])
				Expect(ok).To(BeTrue())
				Expect(filter.Column).To(Equal("id"))
				Expect(filter.Values).To(ConsistOf(uint64(1), uint64(2)))
				return int64(2), nil
			},
		)

		items, total, err := opsSvc.ListOpsExecutionWindows(ctx, cmxapi.ListOpsExecutionWindowParam{
			IDs: []uint64{1, 2},
		})
		Expect(err).ToNot(HaveOccurred())
		Expect(items).To(Equal(expectedItems))
		Expect(total).To(Equal(int64(2)))
	})

	It("delegates get and delete when id is valid", func() {
		expected := &maintent.OpsExecutionWindow{ID: 1, StartAt: 100, EndAt: 200}
		opsRepo.EXPECT().Get(ctx, uint64(1)).Return(expected, nil)

		got, err := opsSvc.GetOpsExecutionWindow(ctx, 1)
		Expect(err).ToNot(HaveOccurred())
		Expect(got).To(Equal(expected))

		dbp.EXPECT().Transaction(ctx, gomock.Any()).DoAndReturn(commitTxCB).Times(1)
		opsRepo.EXPECT().Delete(ctx, uint64(1)).Return(nil)
		Expect(opsSvc.DeleteOpsExecutionWindow(ctx, 1)).To(Succeed())
	})

	It("delegates zero id handling to repo", func() {
		expectedErr := errors.New("id must not be 0")
		opsRepo.EXPECT().Get(ctx, uint64(0)).Return(nil, expectedErr)
		_, err := opsSvc.GetOpsExecutionWindow(ctx, 0)
		Expect(err).To(MatchError(expectedErr))

		dbp.EXPECT().Transaction(ctx, gomock.Any()).DoAndReturn(commitTxCB).Times(1)
		opsRepo.EXPECT().Update(ctx, &maintent.OpsExecutionWindow{
			ID:      0,
			StartAt: 100,
			EndAt:   200,
		}).Return(nil, expectedErr)
		_, err = opsSvc.UpdateOpsExecutionWindow(ctx, 0, cmxapi.UpdateOpsExecutionWindowParam{
			StartAt: 100,
			EndAt:   200,
		})
		Expect(err).To(MatchError(expectedErr))

		dbp.EXPECT().Transaction(ctx, gomock.Any()).DoAndReturn(commitTxCB).Times(1)
		opsRepo.EXPECT().Delete(ctx, uint64(0)).Return(expectedErr)
		err = opsSvc.DeleteOpsExecutionWindow(ctx, 0)
		Expect(err).To(MatchError(ContainSubstring("delete ops execution window failed")))
		Expect(err).To(MatchError(ContainSubstring(expectedErr.Error())))
	})
})
