package repo

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

	ent "github.com/tidbcloud/devops/entity/kernelimage"
	mod "github.com/tidbcloud/devops/models/kernelimage"
	testutil "github.com/tidbcloud/devops/pkg/infra/utils/test"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"go.uber.org/mock/gomock"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

func TestRepo(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "Kernel Image Repo Suite")
}

var _ = Describe("Kernel image repository", func() {
	var (
		ctx      context.Context
		gormDB   *gorm.DB
		repo     Repo
		provider *testutil.MockTransProvider
	)

	BeforeEach(func() {
		ctx = context.Background()
		var err error
		gormDB, err = gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
		Expect(err).NotTo(HaveOccurred())
		Expect(gormDB.AutoMigrate(&mod.Release{})).To(Succeed())

		ctrl := gomock.NewController(GinkgoT())
		provider = testutil.NewMockTransProvider(ctrl)
		provider.EXPECT().ReadDB(gomock.Any()).Return(gormDB).AnyTimes()
		provider.EXPECT().WriteDB(gomock.Any()).Return(gormDB).AnyTimes()
		provider.EXPECT().Transaction(gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, callback func(context.Context) error) error { return callback(ctx) },
		).AnyTimes()
		repo = NewRepo(provider)
	})

	It("creates and idempotently returns the same release", func() {
		release := validRelease("kia_1", "v26.3.8-nextgen")

		created, err := repo.CreateRelease(ctx, release)
		Expect(err).NotTo(HaveOccurred())
		Expect(created.ID).NotTo(BeZero())

		repeated, err := repo.CreateRelease(ctx, release)
		Expect(err).NotTo(HaveOccurred())
		Expect(repeated.ID).To(Equal(created.ID))

		var releaseCount int64
		Expect(gormDB.Model(&mod.Release{}).Count(&releaseCount).Error).To(Succeed())
		Expect(releaseCount).To(Equal(int64(1)))
	})

	It("rejects an immutable artifact change", func() {
		release := validRelease("kia_2", "v26.3.9-nextgen")
		_, err := repo.CreateRelease(ctx, release)
		Expect(err).NotTo(HaveOccurred())

		changed := validRelease("kia_2", "v26.3.9-nextgen")
		changed.SourceDigest = "sha256:def"
		_, err = repo.CreateRelease(ctx, changed)
		Expect(errors.Is(err, ErrImmutableArtifactConflict)).To(BeTrue())
	})

	It("sets an initially unknown source digest exactly once", func() {
		release := validRelease("kia_digest", "v26.3.9-nextgen")
		release.SourceDigest = ""
		_, err := repo.CreateRelease(ctx, release)
		Expect(err).NotTo(HaveOccurred())
		_, err = repo.SetReleaseDeliveryOperationInstance(ctx, release.ArtifactUID, 42)
		Expect(err).NotTo(HaveOccurred())

		resolved, err := repo.SetReleaseSourceDigest(ctx, release.ArtifactUID, 42, "sha256:resolved")
		Expect(err).NotTo(HaveOccurred())
		Expect(resolved.SourceDigest).To(Equal("sha256:resolved"))

		resolved, err = repo.SetReleaseSourceDigest(ctx, release.ArtifactUID, 42, "sha256:resolved")
		Expect(err).NotTo(HaveOccurred())
		Expect(resolved.SourceDigest).To(Equal("sha256:resolved"))

		_, err = repo.SetReleaseSourceDigest(ctx, release.ArtifactUID, 42, "sha256:different")
		Expect(errors.Is(err, ErrImmutableArtifactConflict)).To(BeTrue())
	})

	It("enriches optional callback metadata on an idempotent retry", func() {
		release := validRelease("kia_metadata", "v26.3.9-nextgen")
		release.Branch = ""
		release.GitTag = ""
		release.SourceDigest = ""
		release.SourceReleaseID = ""
		release.SourceChangeID = ""
		_, err := repo.CreateRelease(ctx, release)
		Expect(err).NotTo(HaveOccurred())

		retry := *release
		retry.Branch = "release-nextgen"
		retry.GitTag = "v26.3.9"
		retry.SourceDigest = "sha256:resolved"
		retry.SourceReleaseID = "123"
		retry.SourceChangeID = "456"
		enriched, err := repo.CreateRelease(ctx, &retry)

		Expect(err).NotTo(HaveOccurred())
		Expect(enriched.Branch).To(Equal("release-nextgen"))
		Expect(enriched.GitTag).To(Equal("v26.3.9"))
		Expect(enriched.SourceDigest).To(Equal("sha256:resolved"))
		Expect(enriched.SourceReleaseID).To(Equal("123"))
		Expect(enriched.SourceChangeID).To(Equal("456"))
	})

	It("keeps multiple tags for the same image repository and logical version", func() {
		first := validRelease("kia_same_version_1", "v26.3.9-nextgen")
		first.SourceTag = "build-1"
		_, err := repo.CreateRelease(ctx, first)
		Expect(err).NotTo(HaveOccurred())

		rebuilt := validRelease("kia_same_version_2", "v26.3.9-nextgen")
		rebuilt.CommitSHA = first.CommitSHA
		rebuilt.SourceTag = "build-2"
		_, err = repo.CreateRelease(ctx, rebuilt)
		Expect(err).NotTo(HaveOccurred())

		items, total, err := repo.ListReleases(ctx, ListFilter{
			LocalDevOpsEnv: "dev", Repository: "registry.example/tiflash", Version: "v26.3.99",
		})
		Expect(err).NotTo(HaveOccurred())
		Expect(total).To(Equal(int64(2)))
		Expect(items).To(HaveLen(2))
		Expect(items).To(ConsistOf(
			HaveField("ArtifactUID", "kia_same_version_1"),
			HaveField("ArtifactUID", "kia_same_version_2"),
		))
	})

	It("filters releases", func() {
		_, err := repo.CreateRelease(ctx, validRelease("kia_3", "v26.3.10-nextgen"))
		Expect(err).NotTo(HaveOccurred())
		other := validRelease("kia_4", "v26.3.11-nextgen")
		other.Repo = "pingcap/tikv"
		other.SourceRepository = "registry.example/tikv"
		_, err = repo.CreateRelease(ctx, other)
		Expect(err).NotTo(HaveOccurred())

		items, total, err := repo.ListReleases(ctx, ListFilter{
			LocalDevOpsEnv: "dev", Repository: "registry.example/tiflash", Page: 1, PerPage: 20,
		})
		Expect(err).NotTo(HaveOccurred())
		Expect(total).To(Equal(int64(1)))
		Expect(items).To(HaveLen(1))
		Expect(items[0].ArtifactUID).To(Equal("kia_3"))
	})

	It("records the delivery operation and clears a previous creation error", func() {
		_, err := repo.CreateRelease(ctx, validRelease("kia_approval", "v26.3.12-nextgen"))
		Expect(err).NotTo(HaveOccurred())

		failed, err := repo.MarkReleaseDeliveryOperationCreateFailed(ctx, "kia_approval", "CREATE_FAILED", "operation unavailable")
		Expect(err).NotTo(HaveOccurred())
		Expect(failed.Status).To(Equal(ent.ReleaseStatusDeliveryOperationCreateFailed))
		Expect(failed.LastErrorCode).To(Equal("CREATE_FAILED"))

		updated, err := repo.SetReleaseDeliveryOperationInstance(ctx, "kia_approval", 42)
		Expect(err).NotTo(HaveOccurred())
		Expect(updated.Status).To(Equal(ent.ReleaseStatusDeliveryApproving))
		Expect(updated.DeliveryOperationInstanceID).NotTo(BeNil())
		Expect(*updated.DeliveryOperationInstanceID).To(Equal(uint64(42)))
		Expect(updated.LastErrorCode).To(BeEmpty())
		Expect(updated.LastErrorMessage).To(BeEmpty())
	})

	It("records one delivery operation for every release in a batch", func() {
		first := validRelease("kia_batch_1", "v26.3.12-nextgen")
		first.DeliveryBatchUID = "batch-shared"
		second := validRelease("kia_batch_2", "v26.3.12-nextgen")
		second.SourceRepository = "registry.example/pd"
		second.DeliveryBatchUID = "batch-shared"
		_, err := repo.CreateRelease(ctx, first)
		Expect(err).NotTo(HaveOccurred())
		_, err = repo.CreateRelease(ctx, second)
		Expect(err).NotTo(HaveOccurred())

		batch, err := repo.SetBatchDeliveryOperationInstance(ctx, "batch-shared", 42)

		Expect(err).NotTo(HaveOccurred())
		Expect(batch).To(HaveLen(2))
		for _, release := range batch {
			Expect(release.Status).To(Equal(ent.ReleaseStatusDeliveryApproving))
			Expect(release.DeliveryOperationInstanceID).NotTo(BeNil())
			Expect(*release.DeliveryOperationInstanceID).To(Equal(uint64(42)))
		}
	})

	It("rejects moving an artifact into another delivery batch", func() {
		release := validRelease("kia_batch_conflict", "v26.3.12-nextgen")
		_, err := repo.CreateRelease(ctx, release)
		Expect(err).NotTo(HaveOccurred())
		retry := *release
		retry.DeliveryBatchUID = "another-batch"

		_, err = repo.CreateRelease(ctx, &retry)

		Expect(errors.Is(err, ErrImmutableArtifactConflict)).To(BeTrue())
	})

	It("tracks FID delivery status for the linked operation", func() {
		_, err := repo.CreateRelease(ctx, validRelease("kia_fid", "v26.3.13-nextgen"))
		Expect(err).NotTo(HaveOccurred())
		_, err = repo.SetReleaseDeliveryOperationInstance(ctx, "kia_fid", 42)
		Expect(err).NotTo(HaveOccurred())

		syncing, err := repo.MarkReleaseSyncing(ctx, "kia_fid", 42)
		Expect(err).NotTo(HaveOccurred())
		Expect(syncing.Status).To(Equal(ent.ReleaseStatusSyncing))

		availableAt := time.Now().UTC().Truncate(time.Millisecond)
		targetImages := []string{"registry.example/tiflash:v26.3.13-nextgen"}
		availability := []*ent.PlatformAvailability{{
			CloudProvider: "AWS", ImageRef: targetImages[0], Status: ent.PlatformAvailabilityStatusAvailable,
			CheckedAt: availableAt, Source: "FID_DELIVERY",
		}}
		available, err := repo.MarkReleaseSyncSucceeded(ctx, "kia_fid", 42, availableAt, targetImages, availability)
		Expect(err).NotTo(HaveOccurred())
		Expect(available.Status).To(Equal(ent.ReleaseStatusAvailable))
		Expect(available.AvailabilityStatus).To(Equal(ent.AvailabilityStatusAvailable))
		Expect(available.DeliveryTargetImages).To(Equal(targetImages))
		Expect(available.Availability).To(HaveLen(1))
		Expect(available.AvailableAt).NotTo(BeNil())
		Expect(*available.AvailableAt).To(BeTemporally("==", availableAt))

		failed, err := repo.MarkReleaseSyncFailed(ctx, "kia_fid", 42, "FID_FAILED", "sync failed")
		Expect(err).NotTo(HaveOccurred())
		Expect(failed.Status).To(Equal(ent.ReleaseStatusSyncFailed))
		Expect(failed.LastErrorCode).To(Equal("FID_FAILED"))
		Expect(failed.LastErrorMessage).To(Equal("sync failed"))
	})

	It("starts one availability check and permits takeover after it becomes stale", func() {
		release := validRelease("kia_check", "v26.3.14-nextgen")
		release.Status = ent.ReleaseStatusAvailable
		_, err := repo.CreateRelease(ctx, release)
		Expect(err).NotTo(HaveOccurred())
		startedAt := time.Now().UTC().Truncate(time.Millisecond)

		first, err := repo.MarkAvailabilityChecking(ctx, "kia_check", "check-1", startedAt, nil)
		Expect(err).NotTo(HaveOccurred())
		Expect(first.AvailabilityCheckUID).To(Equal("check-1"))

		concurrent, err := repo.MarkAvailabilityChecking(ctx, "kia_check", "check-2", startedAt.Add(time.Minute), nil)
		Expect(err).NotTo(HaveOccurred())
		Expect(concurrent.AvailabilityCheckUID).To(Equal("check-1"))

		takeover, err := repo.MarkAvailabilityChecking(ctx, "kia_check", "check-3", startedAt.Add(31*time.Minute), nil)
		Expect(err).NotTo(HaveOccurred())
		Expect(takeover.AvailabilityCheckUID).To(Equal("check-3"))

		checkedAt := startedAt.Add(32 * time.Minute)
		_, err = repo.UpdateAvailability(
			ctx,
			"kia_check",
			"check-3",
			ent.AvailabilityStatusAvailable,
			nil,
			checkedAt,
			"",
		)
		Expect(err).NotTo(HaveOccurred())
		dueBefore := checkedAt.Add(-23 * time.Hour)
		notDue, err := repo.MarkAvailabilityChecking(ctx, "kia_check", "check-4", checkedAt.Add(time.Minute), &dueBefore)
		Expect(err).NotTo(HaveOccurred())
		Expect(notDue.AvailabilityCheckUID).To(Equal("check-3"))
	})
})

func validRelease(artifactUID, version string) *ent.Release {
	now := time.Now().UTC()
	return &ent.Release{
		ArtifactUID: artifactUID, LocalDevOpsEnv: "dev",
		Version: "v26.3.99", Repo: "pingcap/tiflash",
		Branch: "release-nextgen-202603", CommitSHA: artifactUID + "00000000000000000000000000000000000",
		GitTag: version, SourceRepository: "registry.example/tiflash", SourceTag: version,
		SourceDigest: "sha256:abc", Status: ent.ReleaseStatusBuilt,
		SourceApplicant:   "applicant@pingcap.com",
		FormalDeliveryUID: "delivery-" + artifactUID, DeliveryBatchUID: "batch-" + artifactUID, CreatedAt: now,
	}
}
