package kernelimageaction

import (
	"context"
	"encoding/json"
	"strings"
	"testing"
	"time"

	kernelimageent "github.com/tidbcloud/devops/entity/kernelimage"
	opent "github.com/tidbcloud/devops/entity/operation"
	kernelimagemod "github.com/tidbcloud/devops/models/kernelimage"
	"github.com/tidbcloud/devops/pkg/infra/clients/byocimagesynccli"
	"github.com/tidbcloud/devops/pkg/infra/clients/fidcli"
	testutil "github.com/tidbcloud/devops/pkg/infra/utils/test"
	kernelimagerepo "github.com/tidbcloud/devops/pkg/service/kernelimage/repo"

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

func TestKernelImageAction(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "Kernel Image Operation Action Suite")
}

var _ = Describe("DeliverKernelImage", func() {
	var (
		ctx         context.Context
		fid         *fidcli.MockClient
		byoc        *byocimagesynccli.MockClient
		releases    kernelimagerepo.Repo
		action      *deliverKernelImage
		byocAction  *syncBYOCImage
		resolver    *resolveKernelImageDigest
		instance    *opent.Instance
		completedAt time.Time
	)

	BeforeEach(func() {
		ctx = context.Background()
		completedAt = time.Date(2026, 8, 6, 7, 0, 0, 0, time.UTC)
		gormDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
		Expect(err).NotTo(HaveOccurred())
		Expect(gormDB.AutoMigrate(&kernelimagemod.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()
		fid = fidcli.NewMockClient(ctrl)
		byoc = byocimagesynccli.NewMockClient(ctrl)
		releases = kernelimagerepo.NewRepo(provider)
		action = &deliverKernelImage{fid: fid, releases: releases, now: func() time.Time { return completedAt }}
		byocAction = &syncBYOCImage{
			client: byoc, releases: releases, maxWait: 2 * time.Hour, now: func() time.Time { return completedAt },
		}
		resolver = &resolveKernelImageDigest{fid: fid, releases: releases}
		instance = validDeliveryInstance()

		for _, release := range validReleases() {
			_, err = releases.CreateRelease(ctx, release)
			Expect(err).NotTo(HaveOccurred())
		}
		_, err = releases.SetBatchDeliveryOperationInstance(
			ctx,
			instance.Params[opent.ParamKernelImageDeliveryCorrelationID],
			instance.ID,
		)
		Expect(err).NotTo(HaveOccurred())
	})

	It("accepts a complete delivery operation instance", func() {
		Expect(action.PreCheck(ctx, instance)).To(Succeed())
		Expect(action.Metadata().Type).To(Equal(opent.ActionTypeAuto))
	})

	It("submits all local images to FID with one stable correlation ID", func() {
		for _, artifactUID := range []string{"kia-test-tikv", "kia-test-br"} {
			_, err := releases.SetReleaseSourceDigest(ctx, artifactUID, instance.ID, "sha256:"+artifactUID)
			Expect(err).NotTo(HaveOccurred())
		}
		submittedAt := time.Date(2026, 8, 6, 6, 0, 0, 0, time.UTC)
		fid.EXPECT().Submit(gomock.Any(), fidcli.SubmitRequest{
			Environment: "dev",
			Images: []string{
				"registry.example/tikv:v8.5.4-nextgen.1",
				"registry.example/br:v8.5.4-nextgen.1",
			},
			CorrelationID: deliveryBatchUID,
			RequestedAt:   time.Unix(instance.CreatedAt, 0).UTC(),
		}).Return(&fidcli.Submission{
			IssueNumber: 123,
			CommentID:   456,
			CommentURL:  "https://github.com/tidbcloud/docker-image-controller/issues/123#issuecomment-456",
			CreatedAt:   submittedAt,
		}, nil)

		result, err := action.Start(ctx, instance)

		Expect(err).NotTo(HaveOccurred())
		Expect(result.Result).To(Equal(opent.ActionStatusRunning))
		Expect(result.InstanceOutputs).To(HaveKeyWithValue(outputFIDIssueNumber, "123"))
		Expect(result.InstanceOutputs).To(HaveKeyWithValue(outputFIDRequestComment, "456"))
		batch, err := releases.ListReleasesByDeliveryBatchUID(ctx, deliveryBatchUID)
		Expect(err).NotTo(HaveOccurred())
		Expect(batch).To(HaveEach(HaveField("Status", kernelimageent.ReleaseStatusSyncing)))
	})

	It("maps FID targets back to each release and marks the batch available", func() {
		setFIDOutputs(instance)
		_, err := releases.MarkBatchSyncing(ctx, deliveryBatchUID, instance.ID)
		Expect(err).NotTo(HaveOccurred())
		fid.EXPECT().GetResult(gomock.Any(), gomock.Any(), deliveryBatchUID).Return(
			&fidcli.DeliveryResult{
				Status:           fidcli.DeliveryStatusSucceeded,
				ResultCommentURL: "https://github.com/tidbcloud/docker-image-controller/issues/123#issuecomment-789",
				Images: []string{
					"registry.example/tikv:v8.5.4-nextgen.1",
					"registry.example/br:v8.5.4-nextgen.1",
				},
				TargetImages: []string{
					"385595570414.dkr.ecr.us-west-2.amazonaws.com/tidbcloud/tikv:v8.5.4-nextgen.1",
					"tidbclouddev.azurecr.io/tidbcloud/tikv:v8.5.4-nextgen.1",
					"385595570414.dkr.ecr.us-west-2.amazonaws.com/tidbcloud/br:v8.5.4-nextgen.1",
				},
			},
			nil,
		)

		result, err := action.Result(ctx, instance)

		Expect(err).NotTo(HaveOccurred())
		Expect(result.Result).To(Equal(opent.ActionStatusSucceeded))
		tikv, err := releases.GetReleaseByArtifactUID(ctx, "kia-test-tikv")
		Expect(err).NotTo(HaveOccurred())
		Expect(tikv.Status).To(Equal(kernelimageent.ReleaseStatusAvailable))
		Expect(tikv.DeliveryTargetImages).To(HaveLen(2))
		Expect(tikv.DeliveryTargetImages).To(HaveEach(ContainSubstring("/tikv:")))
		br, err := releases.GetReleaseByArtifactUID(ctx, "kia-test-br")
		Expect(err).NotTo(HaveOccurred())
		Expect(br.Status).To(Equal(kernelimageent.ReleaseStatusAvailable))
		Expect(br.DeliveryTargetImages).To(HaveLen(1))
		Expect(br.DeliveryTargetImages[0]).To(ContainSubstring("/br:"))
		Expect(*br.AvailableAt).To(BeTemporally("==", completedAt))
	})

	It("keeps the batch syncing after FID when the operation includes BYOC sync", func() {
		setFIDOutputs(instance)
		instance.Actions = []*opent.InstanceAction{{Name: ActionSyncBYOCImage.Name}}
		_, err := releases.MarkBatchSyncing(ctx, deliveryBatchUID, instance.ID)
		Expect(err).NotTo(HaveOccurred())
		fid.EXPECT().GetResult(gomock.Any(), gomock.Any(), deliveryBatchUID).Return(
			&fidcli.DeliveryResult{
				Status: fidcli.DeliveryStatusSucceeded,
				Images: []string{
					"registry.example/tikv:v8.5.4-nextgen.1",
					"registry.example/br:v8.5.4-nextgen.1",
				},
				TargetImages: []string{
					"385595570414.dkr.ecr.us-west-2.amazonaws.com/tidbcloud/tikv:v8.5.4-nextgen.1",
					"385595570414.dkr.ecr.us-west-2.amazonaws.com/tidbcloud/br:v8.5.4-nextgen.1",
				},
			},
			nil,
		)

		result, err := action.Result(ctx, instance)

		Expect(err).NotTo(HaveOccurred())
		Expect(result.Result).To(Equal(opent.ActionStatusSucceeded))
		batch, err := releases.ListReleasesByDeliveryBatchUID(ctx, deliveryBatchUID)
		Expect(err).NotTo(HaveOccurred())
		Expect(batch).To(HaveEach(HaveField("Status", kernelimageent.ReleaseStatusSyncing)))
		Expect(batch).To(HaveEach(HaveField("AvailableAt", BeNil())))
		Expect(batch).To(HaveEach(HaveField("DeliveryTargetImages", HaveLen(1))))
	})

	It("dispatches the exact AWS FID targets and digests to the BYOC workflow", func() {
		prepareBYOCBatch(ctx, releases, instance)
		dispatchedAt := completedAt.Add(-30 * time.Minute)
		byoc.EXPECT().Dispatch(gomock.Any(), byocimagesynccli.DispatchRequest{
			Environment: "dev",
			Images: []byocimagesynccli.Image{
				{
					Source: "385595570414.dkr.ecr.us-west-2.amazonaws.com/tidbcloud/br:v8.5.4-nextgen.1",
					Digest: "sha256:kia-test-br",
				},
				{
					Source: "385595570414.dkr.ecr.us-west-2.amazonaws.com/tidbcloud/tikv:v8.5.4-nextgen.1",
					Digest: "sha256:kia-test-tikv",
				},
			},
			CorrelationID: deliveryBatchUID,
			RequestedAt:   time.Unix(instance.CreatedAt, 0).UTC(),
		}).Return(&byocimagesynccli.WorkflowRun{
			ID: 123, URL: "https://github.test/actions/runs/123", Status: "queued", CreatedAt: dispatchedAt,
		}, nil)

		result, err := byocAction.Start(ctx, instance)

		Expect(err).NotTo(HaveOccurred())
		Expect(result.Result).To(Equal(opent.ActionStatusRunning))
		state := &byocImageSyncState{}
		Expect(json.Unmarshal([]byte(result.InstanceOutputs[outputBYOCImageSyncState]), state)).To(Succeed())
		Expect(state.WorkflowRunID).To(Equal(int64(123)))
		Expect(state.ImagesDigest).To(HavePrefix("sha256:"))
	})

	It("marks the batch available only after the BYOC workflow succeeds", func() {
		prepareBYOCBatch(ctx, releases, instance)
		setBYOCSyncOutput(instance, completedAt.Add(-time.Hour))
		byoc.EXPECT().GetRun(gomock.Any(), int64(123)).Return(&byocimagesynccli.WorkflowRun{
			ID: 123, Status: "completed", Conclusion: "success",
		}, nil)

		result, err := byocAction.Result(ctx, instance)

		Expect(err).NotTo(HaveOccurred())
		Expect(result.Result).To(Equal(opent.ActionStatusSucceeded))
		batch, err := releases.ListReleasesByDeliveryBatchUID(ctx, deliveryBatchUID)
		Expect(err).NotTo(HaveOccurred())
		Expect(batch).To(HaveEach(HaveField("Status", kernelimageent.ReleaseStatusAvailable)))
		Expect(batch).To(HaveEach(HaveField("AvailableAt", HaveValue(BeTemporally("==", completedAt)))))
	})

	It("marks the batch failed when the BYOC workflow fails", func() {
		prepareBYOCBatch(ctx, releases, instance)
		setBYOCSyncOutput(instance, completedAt.Add(-time.Hour))
		byoc.EXPECT().GetRun(gomock.Any(), int64(123)).Return(&byocimagesynccli.WorkflowRun{
			ID: 123, Status: "completed", Conclusion: "failure",
		}, nil)

		result, err := byocAction.Result(ctx, instance)

		Expect(err).NotTo(HaveOccurred())
		Expect(result.Result).To(Equal(opent.ActionStatusFailed))
		batch, err := releases.ListReleasesByDeliveryBatchUID(ctx, deliveryBatchUID)
		Expect(err).NotTo(HaveOccurred())
		Expect(batch).To(SatisfyAll(
			HaveEach(HaveField("Status", kernelimageent.ReleaseStatusSyncFailed)),
			HaveEach(HaveField("LastErrorCode", byocWorkflowFailedCode)),
		))
	})

	It("fails closed when the BYOC workflow exceeds its maximum wait", func() {
		prepareBYOCBatch(ctx, releases, instance)
		setBYOCSyncOutput(instance, completedAt.Add(-3*time.Hour))
		byoc.EXPECT().GetRun(gomock.Any(), int64(123)).Return(&byocimagesynccli.WorkflowRun{
			ID: 123, Status: "in_progress",
		}, nil)

		result, err := byocAction.Result(ctx, instance)

		Expect(err).NotTo(HaveOccurred())
		Expect(result.Result).To(Equal(opent.ActionStatusFailed))
		batch, err := releases.ListReleasesByDeliveryBatchUID(ctx, deliveryBatchUID)
		Expect(err).NotTo(HaveOccurred())
		Expect(batch).To(HaveEach(HaveField("LastErrorCode", byocWorkflowTimeoutCode)))
	})

	It("marks every release failed after FID fails", func() {
		setFIDOutputs(instance)
		_, err := releases.MarkBatchSyncing(ctx, deliveryBatchUID, instance.ID)
		Expect(err).NotTo(HaveOccurred())
		fid.EXPECT().GetResult(gomock.Any(), gomock.Any(), deliveryBatchUID).Return(
			&fidcli.DeliveryResult{Status: fidcli.DeliveryStatusFailed},
			nil,
		)

		result, err := action.Result(ctx, instance)

		Expect(err).NotTo(HaveOccurred())
		Expect(result.Result).To(Equal(opent.ActionStatusFailed))
		batch, err := releases.ListReleasesByDeliveryBatchUID(ctx, deliveryBatchUID)
		Expect(err).NotTo(HaveOccurred())
		Expect(batch).To(SatisfyAll(
			HaveEach(HaveField("Status", kernelimageent.ReleaseStatusSyncFailed)),
			HaveEach(HaveField("LastErrorCode", fidFailedErrorCode)),
		))
	})

	It("fails the batch when FID result images do not match", func() {
		setFIDOutputs(instance)
		_, err := releases.MarkBatchSyncing(ctx, deliveryBatchUID, instance.ID)
		Expect(err).NotTo(HaveOccurred())
		fid.EXPECT().GetResult(gomock.Any(), gomock.Any(), deliveryBatchUID).Return(
			&fidcli.DeliveryResult{
				Status: fidcli.DeliveryStatusSucceeded,
				Images: []string{"registry.example/tikv:v8.5.4-nextgen.1"},
			},
			nil,
		)

		result, err := action.Result(ctx, instance)

		Expect(err).NotTo(HaveOccurred())
		Expect(result.Result).To(Equal(opent.ActionStatusFailed))
		batch, err := releases.ListReleasesByDeliveryBatchUID(ctx, deliveryBatchUID)
		Expect(err).NotTo(HaveOccurred())
		Expect(batch).To(HaveEach(HaveField("LastErrorCode", fidResultInvalidErrorCode)))
	})

	It("rejects an invalid delivery correlation ID", func() {
		instance.Params[opent.ParamKernelImageDeliveryCorrelationID] = "not-a-uuid"

		Expect(action.PreCheck(ctx, instance)).To(MatchError(ContainSubstring("must be a UUID")))
	})

	It("dispatches one source digest query for every unresolved image", func() {
		for _, query := range digestQueries() {
			query := query
			fid.EXPECT().StartImageAvailabilityQuery(gomock.Any(), query.ImageRef).Return(&query, nil)
		}

		result, err := resolver.Start(ctx, instance)

		Expect(err).NotTo(HaveOccurred())
		Expect(result.Result).To(Equal(opent.ActionStatusRunning))
		states := []sourceDigestQueryState{}
		Expect(json.Unmarshal([]byte(result.InstanceOutputs[outputSourceDigestQueries]), &states)).To(Succeed())
		Expect(states).To(HaveLen(2))
	})

	It("stores every resolved source digest before delivery", func() {
		setSourceDigestQueryOutputs(instance)
		for _, query := range digestQueries() {
			query := query
			fid.EXPECT().GetImageAvailabilityQueryResult(gomock.Any(), query).Return(&fidcli.ImageAvailability{
				Status: fidcli.ImageAvailabilityStatusAvailable, Digest: "sha256:" + strings.ReplaceAll(query.ImageRef, "/", "-"),
			}, true, nil)
		}

		result, err := resolver.Result(ctx, instance)

		Expect(err).NotTo(HaveOccurred())
		Expect(result.Result).To(Equal(opent.ActionStatusSucceeded))
		batch, err := releases.ListReleasesByDeliveryBatchUID(ctx, deliveryBatchUID)
		Expect(err).NotTo(HaveOccurred())
		for _, release := range batch {
			Expect(release.SourceDigest).To(HavePrefix("sha256:"))
		}
	})

	It("keeps the digest action running while any workflow is incomplete", func() {
		setSourceDigestQueryOutputs(instance)
		queries := digestQueries()
		fid.EXPECT().GetImageAvailabilityQueryResult(gomock.Any(), queries[0]).Return(&fidcli.ImageAvailability{
			Status: fidcli.ImageAvailabilityStatusAvailable, Digest: "sha256:resolved",
		}, true, nil)
		fid.EXPECT().GetImageAvailabilityQueryResult(gomock.Any(), queries[1]).Return(nil, false, nil)

		result, err := resolver.Result(ctx, instance)

		Expect(err).NotTo(HaveOccurred())
		Expect(result.Result).To(Equal(opent.ActionStatusRunning))
	})

	It("marks every release failed when one source digest query fails", func() {
		setSourceDigestQueryOutputs(instance)
		queries := digestQueries()
		fid.EXPECT().GetImageAvailabilityQueryResult(gomock.Any(), queries[0]).Return(&fidcli.ImageAvailability{
			Status:       fidcli.ImageAvailabilityStatusMissing,
			ErrorMessage: "source image is unavailable",
		}, true, nil)

		result, err := resolver.Result(ctx, instance)

		Expect(err).NotTo(HaveOccurred())
		Expect(result.Result).To(Equal(opent.ActionStatusFailed))
		batch, err := releases.ListReleasesByDeliveryBatchUID(ctx, deliveryBatchUID)
		Expect(err).NotTo(HaveOccurred())
		Expect(batch).To(SatisfyAll(
			HaveEach(HaveField("Status", kernelimageent.ReleaseStatusSyncFailed)),
			HaveEach(HaveField("LastErrorCode", sourceDigestQueryFailedCode)),
		))
	})
})

const deliveryBatchUID = "21d4e84f-275a-5b07-8314-76ffcc736969"

func validDeliveryInstance() *opent.Instance {
	return &opent.Instance{ID: 42, CreatedAt: time.Date(2026, 8, 6, 5, 0, 0, 0, time.UTC).Unix(), Params: map[opent.ParamName]string{
		opent.ParamKernelImageArtifactUID:           "kia-test-tikv",
		opent.ParamKernelImageLocalDevOpsEnv:        "dev",
		opent.ParamKernelImageSourceRepository:      "registry.example/tikv",
		opent.ParamKernelImageSourceTag:             "v8.5.4-nextgen.1",
		opent.ParamKernelImageDeliveryCorrelationID: deliveryBatchUID,
	}, Outputs: map[opent.OutputName]string{}}
}

func validReleases() []*kernelimageent.Release {
	return []*kernelimageent.Release{
		validRelease("kia-test-tikv", "tikv"),
		validRelease("kia-test-br", "br"),
	}
}

func validRelease(artifactUID, component string) *kernelimageent.Release {
	return &kernelimageent.Release{
		ArtifactUID: artifactUID, LocalDevOpsEnv: "dev",
		Version: "v8.5.4", Repo: "pingcap/tidb", Branch: "release-nextgen",
		CommitSHA: strings.Repeat("a", 40), GitTag: "v8.5.4-nextgen.1",
		SourceRepository: "registry.example/" + component, SourceTag: "v8.5.4-nextgen.1",
		Status:            kernelimageent.ReleaseStatusBuilt,
		SourceApplicant:   "applicant@pingcap.com",
		FormalDeliveryUID: artifactUID + "-delivery",
		DeliveryBatchUID:  deliveryBatchUID,
		CreatedAt:         time.Now().UTC(),
	}
}

func setFIDOutputs(instance *opent.Instance) {
	instance.Outputs[outputFIDIssueNumber] = "123"
	instance.Outputs[outputFIDRequestComment] = "456"
	instance.Outputs[outputFIDRequestURL] = "https://github.com/tidbcloud/docker-image-controller/issues/123#issuecomment-456"
	instance.Outputs[outputFIDRequestCreated] = time.Date(2026, 8, 6, 6, 0, 0, 0, time.UTC).Format(time.RFC3339Nano)
}

func digestQueries() []fidcli.ImageAvailabilityQuery {
	return []fidcli.ImageAvailabilityQuery{
		{
			ImageRef: "registry.example/tikv:v8.5.4-nextgen.1", WorkflowRunID: 123,
			WorkflowRunURL: "https://github.com/tidbcloud/docker-image-controller/actions/runs/123",
		},
		{
			ImageRef: "registry.example/br:v8.5.4-nextgen.1", WorkflowRunID: 124,
			WorkflowRunURL: "https://github.com/tidbcloud/docker-image-controller/actions/runs/124",
		},
	}
}

func setSourceDigestQueryOutputs(instance *opent.Instance) {
	queries := digestQueries()
	states := []sourceDigestQueryState{
		{ArtifactUID: "kia-test-tikv", Query: queries[0]},
		{ArtifactUID: "kia-test-br", Query: queries[1]},
	}
	encoded, err := json.Marshal(states)
	Expect(err).NotTo(HaveOccurred())
	instance.Outputs[outputSourceDigestQueries] = string(encoded)
}

func prepareBYOCBatch(ctx context.Context, releases kernelimagerepo.Repo, instance *opent.Instance) {
	for _, artifactUID := range []string{"kia-test-tikv", "kia-test-br"} {
		_, err := releases.SetReleaseSourceDigest(ctx, artifactUID, instance.ID, "sha256:"+artifactUID)
		Expect(err).NotTo(HaveOccurred())
	}
	_, err := releases.MarkBatchSyncing(ctx, deliveryBatchUID, instance.ID)
	Expect(err).NotTo(HaveOccurred())
	_, err = releases.MarkBatchFIDDelivered(ctx, deliveryBatchUID, instance.ID, time.Now().UTC(), []kernelimagerepo.DeliveryResultUpdate{
		{
			ArtifactUID: "kia-test-tikv",
			TargetImages: []string{
				"385595570414.dkr.ecr.us-west-2.amazonaws.com/tidbcloud/tikv:v8.5.4-nextgen.1",
				"tidbclouddev.azurecr.io/tidbcloud/tikv:v8.5.4-nextgen.1",
			},
		},
		{
			ArtifactUID: "kia-test-br",
			TargetImages: []string{
				"385595570414.dkr.ecr.us-west-2.amazonaws.com/tidbcloud/br:v8.5.4-nextgen.1",
			},
		},
	})
	Expect(err).NotTo(HaveOccurred())
}

func setBYOCSyncOutput(instance *opent.Instance, dispatchedAt time.Time) {
	state := byocImageSyncState{
		ImagesDigest:  "sha256:manifest",
		WorkflowRunID: 123,
		WorkflowURL:   "https://github.test/actions/runs/123",
		DispatchedAt:  dispatchedAt,
	}
	encoded, err := json.Marshal(state)
	Expect(err).NotTo(HaveOccurred())
	instance.Outputs[outputBYOCImageSyncState] = string(encoded)
}
