package ops_execution_window

import (
	"context"
	"testing"
	"time"

	maintent "github.com/tidbcloud/devops/entity/maintenance"
	maintmod "github.com/tidbcloud/devops/models/maintenance"
	"github.com/tidbcloud/devops/pkg/infra/utils/test"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"github.com/tidbcloud/pkgv2/db"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

func TestRepo(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "OpsExecutionWindow Repo Suite", test.ReporterConfig())
}

var _ = Describe("[OpsExecutionWindowRepo]", func() {
	var (
		ctx  context.Context
		repo Repo
	)

	BeforeEach(func() {
		ctx = context.Background()

		gormDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
		Expect(err).To(BeNil())
		sqlDB, err := gormDB.DB()
		Expect(err).To(BeNil())
		sqlDB.SetMaxOpenConns(1)
		Expect(gormDB.AutoMigrate(&maintmod.OpsExecutionWindow{})).To(Succeed())

		repo = NewRepo(db.NewProvider(gormDB))
	})

	newWindow := func() *maintent.OpsExecutionWindow {
		startAt := time.Now().Add(time.Hour).Unix()
		return &maintent.OpsExecutionWindow{
			StartAt: startAt,
			EndAt:   startAt + int64(time.Hour.Seconds()),
		}
	}

	It("creates and gets an ops execution window", func() {
		created, err := repo.Create(ctx, newWindow())
		Expect(err).To(BeNil())
		Expect(created.ID).NotTo(BeZero())
		Expect(created.CreatedAt).NotTo(BeZero())
		Expect(created.StartAt).To(BeNumerically("<", created.EndAt))

		got, err := repo.Get(ctx, created.ID)
		Expect(err).To(BeNil())
		Expect(got.ID).To(Equal(created.ID))
		Expect(got.StartAt).To(Equal(created.StartAt))
		Expect(got.EndAt).To(Equal(created.EndAt))
	})

	It("lists and counts ops execution windows", func() {
		created, err := repo.Create(ctx, newWindow())
		Expect(err).To(BeNil())

		items, err := repo.List(ctx)
		Expect(err).To(BeNil())
		Expect(items).To(HaveLen(1))
		Expect(items[0].ID).To(Equal(created.ID))

		total, err := repo.Count(ctx)
		Expect(err).To(BeNil())
		Expect(total).To(Equal(int64(1)))
	})

	It("lists and counts ops execution windows by ids", func() {
		created1, err := repo.Create(ctx, newWindow())
		Expect(err).To(BeNil())
		created2, err := repo.Create(ctx, newWindow())
		Expect(err).To(BeNil())
		_, err = repo.Create(ctx, newWindow())
		Expect(err).To(BeNil())

		opt := repo.Options().InIDs([]uint64{created1.ID, created2.ID})
		items, err := repo.List(ctx, opt)
		Expect(err).To(BeNil())
		Expect(items).To(HaveLen(2))
		Expect([]uint64{items[0].ID, items[1].ID}).To(ConsistOf(created1.ID, created2.ID))

		total, err := repo.Count(ctx, opt)
		Expect(err).To(BeNil())
		Expect(total).To(Equal(int64(2)))
	})

	It("updates an ops execution window", func() {
		created, err := repo.Create(ctx, newWindow())
		Expect(err).To(BeNil())

		created.StartAt += int64(2 * time.Hour.Seconds())
		created.EndAt += int64(2 * time.Hour.Seconds())
		updated, err := repo.Update(ctx, created)
		Expect(err).To(BeNil())

		got, err := repo.Get(ctx, created.ID)
		Expect(err).To(BeNil())
		Expect(got.StartAt).To(Equal(updated.StartAt))
		Expect(got.EndAt).To(Equal(updated.EndAt))
	})

	It("deletes an ops execution window", func() {
		created, err := repo.Create(ctx, newWindow())
		Expect(err).To(BeNil())

		Expect(repo.Delete(ctx, created.ID)).To(Succeed())

		total, err := repo.Count(ctx)
		Expect(err).To(BeNil())
		Expect(total).To(Equal(int64(0)))

		_, err = repo.Get(ctx, created.ID)
		Expect(err).To(HaveOccurred())
	})

	It("rejects invalid id usage", func() {
		spec := newWindow()
		spec.ID = 1
		_, err := repo.Create(ctx, spec)
		Expect(err).To(MatchError("id must be 0"))

		spec = newWindow()
		_, err = repo.Update(ctx, spec)
		Expect(err).To(MatchError("id must not be 0"))

		_, err = repo.Get(ctx, 0)
		Expect(err).To(MatchError("id must not be 0"))

		err = repo.Delete(ctx, 0)
		Expect(err).To(MatchError("id must not be 0"))
	})

	DescribeTable("rejects invalid time windows",
		func(spec *maintent.OpsExecutionWindow, expectedErr string) {
			_, err := repo.Create(ctx, spec)
			Expect(err).To(MatchError(expectedErr))
		},
		Entry("zero start_at", &maintent.OpsExecutionWindow{EndAt: time.Now().Add(time.Hour).Unix()}, "start_at must not be 0"),
		Entry("zero end_at", &maintent.OpsExecutionWindow{StartAt: time.Now().Unix()}, "end_at must not be 0"),
		Entry("start_at equals end_at", &maintent.OpsExecutionWindow{StartAt: 100, EndAt: 100}, "start_at must be before end_at"),
		Entry("start_at after end_at", &maintent.OpsExecutionWindow{StartAt: 200, EndAt: 100}, "start_at must be before end_at"),
	)
})
