package clustermaintenance

import (
	"context"
	"errors"
	"testing"
	"time"

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

	"github.com/tidbcloud/devops/api"
	"github.com/tidbcloud/devops/pkg/infra/errs"
	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"
	opsmwrepo "github.com/tidbcloud/devops/pkg/service/clustermaintenance/repo/ops_maintenance_window"

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

func TestClusterOpsMaintenanceService(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "ClusterOpsMaintenanceService Suite")
}

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

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

	BeforeEach(func() {
		ctrl = gomock.NewController(GinkgoT())
		dbp = testutil.NewMockTransProvider(ctrl)
		opsRepo = opsmwrepo.NewMockRepo(ctrl)
		opsSvc = &svc{
			dbp: dbp,
			repo: &repo.Repo{
				OpsMaintenanceWindow: 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() {
		createWindows := []maintent.ClusterOpsMaintenanceWindowSlot{
			{StartCronSpec: "3 2 * * 1", Duration: "1h", Description: "weekly window"},
		}
		createApprovers := []string{"approver_1@pingcap.com"}
		expectedCreated := &maintent.ClusterOpsMaintenanceWindow{
			ClusterID: "1001",
			Windows:   createWindows,
			Approvers: createApprovers,
		}
		dbp.EXPECT().Transaction(ctx, gomock.Any()).DoAndReturn(commitTxCB).Times(1)
		opsRepo.EXPECT().Create(ctx, &maintent.ClusterOpsMaintenanceWindow{
			ClusterID: "1001",
			Windows:   createWindows,
			Approvers: createApprovers,
		}).Return(expectedCreated, nil)

		created, err := opsSvc.CreateClusterOpsMaintenanceWindow(ctx, cmxapi.CreateClusterOpsMaintenanceWindowParam{
			ClusterID: "1001",
			Windows:   createWindows,
			Approvers: createApprovers,
		})
		Expect(err).ToNot(HaveOccurred())
		Expect(created).To(Equal(expectedCreated))

		updateWindows := []maintent.ClusterOpsMaintenanceWindowSlot{
			{StartCronSpec: "4 3 * * 2", Duration: "2h", Description: "updated window"},
		}
		updateApprovers := []string{"approver_2@pingcap.com"}
		expectedUpdated := &maintent.ClusterOpsMaintenanceWindow{
			ClusterID: "1001",
			Windows:   updateWindows,
			Approvers: updateApprovers,
		}
		dbp.EXPECT().Transaction(ctx, gomock.Any()).DoAndReturn(commitTxCB).Times(1)
		opsRepo.EXPECT().Update(ctx, &maintent.ClusterOpsMaintenanceWindow{
			ClusterID: "1001",
			Windows:   updateWindows,
			Approvers: updateApprovers,
		}).Return(expectedUpdated, nil)

		updated, err := opsSvc.UpdateClusterOpsMaintenanceWindow(ctx, "1001", cmxapi.UpdateClusterOpsMaintenanceWindowParam{
			Windows:   updateWindows,
			Approvers: updateApprovers,
		})
		Expect(err).ToNot(HaveOccurred())
		Expect(updated).To(Equal(expectedUpdated))
	})

	It("validates windows and cluster id", func() {
		expectedErr := errs.APIErrorf(api.ErrCodeInvalidArgument, "invalid create param")
		dbp.EXPECT().Transaction(ctx, gomock.Any()).DoAndReturn(commitTxCB).Times(1)
		opsRepo.EXPECT().Create(ctx, &maintent.ClusterOpsMaintenanceWindow{
			ClusterID: "",
			Windows: []maintent.ClusterOpsMaintenanceWindowSlot{
				{StartCronSpec: "3 2 * * 1", Duration: "1h", Description: "weekly window"},
			},
		}).Return(nil, expectedErr)
		_, err := opsSvc.CreateClusterOpsMaintenanceWindow(ctx, cmxapi.CreateClusterOpsMaintenanceWindowParam{
			ClusterID: "",
			Windows: []maintent.ClusterOpsMaintenanceWindowSlot{
				{StartCronSpec: "3 2 * * 1", Duration: "1h", Description: "weekly window"},
			},
		})
		Expect(err).To(HaveOccurred())
		apiErr, ok := errs.AsAPIError(err)
		Expect(ok).To(BeTrue())
		Expect(apiErr.ErrCode()).To(Equal(api.ErrCodeInvalidArgument))

		expectedErr = errs.APIErrorf(api.ErrCodeInvalidArgument, "invalid create param")
		dbp.EXPECT().Transaction(ctx, gomock.Any()).DoAndReturn(commitTxCB).Times(1)
		opsRepo.EXPECT().Create(ctx, &maintent.ClusterOpsMaintenanceWindow{
			ClusterID: "1001",
			Windows: []maintent.ClusterOpsMaintenanceWindowSlot{
				{StartCronSpec: "invalid cron", Duration: "1d", Description: "bad cron"},
			},
		}).Return(nil, expectedErr)
		_, err = opsSvc.CreateClusterOpsMaintenanceWindow(ctx, cmxapi.CreateClusterOpsMaintenanceWindowParam{
			ClusterID: "1001",
			Windows: []maintent.ClusterOpsMaintenanceWindowSlot{
				{StartCronSpec: "invalid cron", Duration: "1d", Description: "bad cron"},
			},
		})
		Expect(err).To(HaveOccurred())
		apiErr, ok = errs.AsAPIError(err)
		Expect(ok).To(BeTrue())
		Expect(apiErr.ErrCode()).To(Equal(api.ErrCodeInvalidArgument))

		expectedErr = errs.APIErrorf(api.ErrCodeInvalidArgument, "invalid create param")
		dbp.EXPECT().Transaction(ctx, gomock.Any()).DoAndReturn(commitTxCB).Times(1)
		opsRepo.EXPECT().Create(ctx, &maintent.ClusterOpsMaintenanceWindow{
			ClusterID: "1001",
			Windows: []maintent.ClusterOpsMaintenanceWindowSlot{
				{StartCronSpec: "3 2 * * 1", Duration: "invalid duration", Description: "bad duration"},
			},
		}).Return(nil, expectedErr)
		_, err = opsSvc.CreateClusterOpsMaintenanceWindow(ctx, cmxapi.CreateClusterOpsMaintenanceWindowParam{
			ClusterID: "1001",
			Windows: []maintent.ClusterOpsMaintenanceWindowSlot{
				{StartCronSpec: "3 2 * * 1", Duration: "invalid duration", Description: "bad duration"},
			},
		})
		Expect(err).To(HaveOccurred())
		apiErr, ok = errs.AsAPIError(err)
		Expect(ok).To(BeTrue())
		Expect(apiErr.ErrCode()).To(Equal(api.ErrCodeInvalidArgument))

		_, err = opsSvc.UpdateClusterOpsMaintenanceWindow(ctx, "", cmxapi.UpdateClusterOpsMaintenanceWindowParam{
			Windows: []maintent.ClusterOpsMaintenanceWindowSlot{
				{StartCronSpec: "3 2 * * 1", Duration: "1h", Description: "weekly window"},
			},
		})
		Expect(err).To(HaveOccurred())
		apiErr, ok = errs.AsAPIError(err)
		Expect(ok).To(BeTrue())
		Expect(apiErr.ErrCode()).To(Equal(api.ErrCodeInvalidArgument))
	})

	It("delegates get and delete when cluster id is valid", func() {
		expected := &maintent.ClusterOpsMaintenanceWindow{
			ClusterID: "1001",
			Windows: []maintent.ClusterOpsMaintenanceWindowSlot{
				{StartCronSpec: "3 2 * * 1", Duration: "1h", Description: "weekly window"},
			},
		}
		opsRepo.EXPECT().Get(ctx, "1001").Return(expected, nil)

		got, err := opsSvc.GetClusterOpsMaintenanceWindow(ctx, "1001")
		Expect(err).ToNot(HaveOccurred())
		Expect(got).To(Equal(expected))

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

	Describe("IsInClusterOpsMaintenanceWindow", func() {
		It("returns true when ops maintenance window record is not found", func() {
			opsRepo.EXPECT().Get(ctx, "1001").Return(nil, errs.APIErrorf(api.ErrCodeNotFound, "not found"))

			inWindow, window, err := opsSvc.IsInClusterOpsMaintenanceWindow(ctx, "1001", nil)
			Expect(err).ToNot(HaveOccurred())
			Expect(inWindow).To(BeTrue())
			Expect(window).To(BeNil())
		})

		It("returns false and window when cluster is out of maintenance window", func() {
			expectedWindow := &maintent.ClusterOpsMaintenanceWindow{
				ClusterID: "1001",
				Windows: []maintent.ClusterOpsMaintenanceWindowSlot{
					{StartCronSpec: "0 0 1 1 *", Duration: "1m", Description: "far away"},
				},
			}
			opsRepo.EXPECT().Get(ctx, "1001").Return(expectedWindow, nil)

			inWindow, window, err := opsSvc.IsInClusterOpsMaintenanceWindow(ctx, "1001", nil)
			Expect(err).ToNot(HaveOccurred())
			Expect(inWindow).To(BeFalse())
			Expect(window).To(Equal(expectedWindow))
		})

		It("returns maintenance window when cluster is in maintenance window", func() {
			expectedWindow := &maintent.ClusterOpsMaintenanceWindow{
				ClusterID: "1001",
				Windows: []maintent.ClusterOpsMaintenanceWindowSlot{
					{StartCronSpec: "* * * * *", Duration: "2m", Description: "rolling"},
				},
				Approvers: []string{"approver_1@pingcap.com", "approver_2@pingcap.com"},
			}
			opsRepo.EXPECT().Get(ctx, "1001").Return(expectedWindow, nil)

			inWindow, window, err := opsSvc.IsInClusterOpsMaintenanceWindow(ctx, "1001", nil)
			Expect(err).ToNot(HaveOccurred())
			Expect(inWindow).To(BeTrue())
			Expect(window).To(Equal(expectedWindow))
		})

		It("returns true when all execute times are in maintenance window", func() {
			expectedWindow := &maintent.ClusterOpsMaintenanceWindow{
				ClusterID: "1001",
				Windows: []maintent.ClusterOpsMaintenanceWindowSlot{
					{StartCronSpec: "0 10 * * *", Duration: "2h", Description: "daily window"},
				},
			}
			opsRepo.EXPECT().Get(ctx, "1001").Return(expectedWindow, nil)

			inWindow, window, err := opsSvc.IsInClusterOpsMaintenanceWindow(
				ctx,
				"1001",
				[]time.Time{
					time.Date(2026, 5, 20, 10, 30, 0, 0, time.UTC),
					time.Date(2026, 5, 20, 11, 30, 0, 0, time.UTC),
				},
			)
			Expect(err).ToNot(HaveOccurred())
			Expect(inWindow).To(BeTrue())
			Expect(window).To(Equal(expectedWindow))
		})

		It("returns false when any execute time is out of maintenance window", func() {
			expectedWindow := &maintent.ClusterOpsMaintenanceWindow{
				ClusterID: "1001",
				Windows: []maintent.ClusterOpsMaintenanceWindowSlot{
					{StartCronSpec: "0 10 * * *", Duration: "2h", Description: "daily window"},
				},
			}
			opsRepo.EXPECT().Get(ctx, "1001").Return(expectedWindow, nil)

			inWindow, window, err := opsSvc.IsInClusterOpsMaintenanceWindow(
				ctx,
				"1001",
				[]time.Time{
					time.Date(2026, 5, 20, 10, 30, 0, 0, time.UTC),
					time.Date(2026, 5, 20, 12, 1, 0, 0, time.UTC),
				},
			)
			Expect(err).ToNot(HaveOccurred())
			Expect(inWindow).To(BeFalse())
			Expect(window).To(Equal(expectedWindow))
		})

		It("returns wrapped error when repo get fails", func() {
			expectedErr := errors.New("repo unavailable")
			opsRepo.EXPECT().Get(ctx, "1001").Return(nil, expectedErr)

			inWindow, window, err := opsSvc.IsInClusterOpsMaintenanceWindow(ctx, "1001", nil)
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(ContainSubstring("get cluster ops maintenance window failed"))
			Expect(err).To(MatchError(ContainSubstring(expectedErr.Error())))
			Expect(inWindow).To(BeFalse())
			Expect(window).To(BeNil())
		})

		It("returns error when maintenance window config is invalid", func() {
			opsRepo.EXPECT().Get(ctx, "1001").Return(&maintent.ClusterOpsMaintenanceWindow{
				ClusterID: "1001",
				Windows: []maintent.ClusterOpsMaintenanceWindowSlot{
					{StartCronSpec: "invalid cron", Duration: "2m", Description: "broken"},
				},
			}, nil)

			inWindow, window, err := opsSvc.IsInClusterOpsMaintenanceWindow(ctx, "1001", nil)
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(ContainSubstring("check cluster ops maintenance window failed"))
			Expect(inWindow).To(BeFalse())
			Expect(window).To(BeNil())
		})
	})
})
