package kernelimage

import (
	"bytes"
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/gin-gonic/gin"
	devopsapi "github.com/tidbcloud/devops/api"
	imageapi "github.com/tidbcloud/devops/api/kernelimage"
	ent "github.com/tidbcloud/devops/entity/kernelimage"
	ctrl "github.com/tidbcloud/devops/pkg/controller"
	testutil "github.com/tidbcloud/devops/pkg/infra/utils/test"
	svcapi "github.com/tidbcloud/devops/pkg/service/kernelimage/api"
	imagesvc "github.com/tidbcloud/devops/pkg/service/kernelimage/service"

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

func TestController(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "Kernel Image Controller Suite", testutil.ReporterConfig())
}

var _ = Describe("Kernel image controller", func() {
	var (
		router   *gin.Engine
		recorder *httptest.ResponseRecorder
		service  *svcapi.MockService
	)

	BeforeEach(func() {
		gin.SetMode(gin.TestMode)
		router = gin.New()
		recorder = httptest.NewRecorder()
		service = svcapi.NewMockService(gomock.NewController(GinkgoT()))
		controller := &Controller{service: service}
		Expect(controller.RegisterAPI(&ctrl.Suite{
			Router: router.Group("/api/v1/devops"), InternalRouter: router.Group("/api/v1/devops-internal"),
		})).To(Succeed())
	})

	It("accepts a build callback on the authenticated DevOps route", func() {
		param := svcapi.BuildCallbackParam{
			SourceApplicant: "applicant@pingcap.com", SourceReleaseID: "30082", SourceChangeID: "62440",
			Repo:      "tidbcloud/cloud-storage-engine",
			CommitSHA: "ff685313539da0046e0951a02f8de2f0e2791602",
			Images: []svcapi.BuildCallbackImage{
				{SourceRepository: "registry.example/tikv", SourceTag: "v8.5.4-nextgen.202510.35"},
				{SourceRepository: "registry.example/pd", SourceTag: "v8.5.4-nextgen.202510.35"},
			},
		}
		operationInstanceID := uint64(42)
		service.EXPECT().AcceptBuildCallback(gomock.Any(), param).Return(&svcapi.BuildCallbackResult{
			Releases: []*ent.Release{{
				ArtifactUID: "derived-artifact-uid-1", SourceApplicant: param.SourceApplicant,
				DeliveryOperationInstanceID: &operationInstanceID,
			}, {
				ArtifactUID: "derived-artifact-uid-2", SourceApplicant: param.SourceApplicant,
				DeliveryOperationInstanceID: &operationInstanceID,
			}},
			InstanceID: &operationInstanceID,
		}, nil)
		images := make([]imageapi.BuildCallbackImage, 0, len(param.Images))
		for _, image := range param.Images {
			images = append(images, imageapi.BuildCallbackImage{
				SourceRepository: image.SourceRepository,
				SourceTag:        image.SourceTag,
				SourceDigest:     image.SourceDigest,
			})
		}
		body, err := json.Marshal(&imageapi.BuildCallbackReq{
			SourceApplicant: param.SourceApplicant,
			SourceReleaseID: param.SourceReleaseID, SourceChangeID: param.SourceChangeID,
			Repo: param.Repo, Branch: param.Branch, CommitSHA: param.CommitSHA, GitTag: param.GitTag,
			Images: images,
		})
		Expect(err).NotTo(HaveOccurred())
		req, err := http.NewRequestWithContext(context.Background(), http.MethodPost,
			"/api/v1/devops/kernel-images/build-callback", bytes.NewReader(body))
		Expect(err).NotTo(HaveOccurred())
		req.Header.Set("Content-Type", "application/json")

		router.ServeHTTP(recorder, req)

		Expect(recorder.Code).To(Equal(http.StatusOK))
		response := &imageapi.BuildCallbackResp{}
		Expect(json.Unmarshal(recorder.Body.Bytes(), response)).To(Succeed())
		Expect(response.Releases).To(HaveLen(2))
		Expect(response.Releases[0].SourceApplicant).To(Equal("applicant@pingcap.com"))
		Expect(response.Releases[0].DeliveryOperationInstanceID).NotTo(BeNil())
		Expect(*response.Releases[0].DeliveryOperationInstanceID).To(Equal(operationInstanceID))
		Expect(response.InstanceID).NotTo(BeNil())
		Expect(*response.InstanceID).To(Equal(operationInstanceID))
	})

	It("accepts a build callback without a source applicant", func() {
		param := svcapi.BuildCallbackParam{
			Repo:      "tidbcloud/cloud-storage-engine",
			CommitSHA: "ff685313539da0046e0951a02f8de2f0e2791602",
			Images: []svcapi.BuildCallbackImage{{
				SourceRepository: "registry.example/tikv",
				SourceTag:        "v8.5.4-nextgen.202510.35",
			}},
		}
		service.EXPECT().AcceptBuildCallback(gomock.Any(), param).Return(&svcapi.BuildCallbackResult{
			Releases: []*ent.Release{{SourceApplicant: "devops-internal@pingcap.com"}},
		}, nil)
		body, err := json.Marshal(&imageapi.BuildCallbackReq{
			Repo:      param.Repo,
			CommitSHA: param.CommitSHA,
			Images: []imageapi.BuildCallbackImage{{
				SourceRepository: param.Images[0].SourceRepository,
				SourceTag:        param.Images[0].SourceTag,
			}},
		})
		Expect(err).NotTo(HaveOccurred())
		req, err := http.NewRequestWithContext(context.Background(), http.MethodPost,
			"/api/v1/devops/kernel-images/build-callback", bytes.NewReader(body))
		Expect(err).NotTo(HaveOccurred())
		req.Header.Set("Content-Type", "application/json")

		router.ServeHTTP(recorder, req)

		Expect(recorder.Code).To(Equal(http.StatusOK))
	})

	It("rejects a build callback without images", func() {
		body := []byte(`{"repo":"pingcap/tidb","commit_sha":"ff685313539da0046e0951a02f8de2f0e2791602"}`)
		req, err := http.NewRequestWithContext(context.Background(), http.MethodPost,
			"/api/v1/devops/kernel-images/build-callback", bytes.NewReader(body))
		Expect(err).NotTo(HaveOccurred())
		req.Header.Set("Content-Type", "application/json")

		router.ServeHTTP(recorder, req)

		Expect(recorder.Code).To(Equal(http.StatusBadRequest))
	})

	It("does not expose the build callback on the unauthenticated internal route", func() {
		req, err := http.NewRequestWithContext(context.Background(), http.MethodPost,
			"/api/v1/devops-internal/kernel-images/build-callback", bytes.NewReader([]byte(`{}`)))
		Expect(err).NotTo(HaveOccurred())

		router.ServeHTTP(recorder, req)

		Expect(recorder.Code).To(Equal(http.StatusNotFound))
	})

	It("lists release facts", func() {
		service.EXPECT().ListReleases(gomock.Any(), svcapi.ListReleasesParam{
			Repository: "registry.example/tiflash", Version: "v26.3.99",
			Statuses: []ent.ReleaseStatus{}, Page: 1, PerPage: 20,
		}).Return([]*ent.Release{{
			ArtifactUID: "kia_1", SourceRepository: "registry.example/tiflash", Status: ent.ReleaseStatusBuilt,
		}}, int64(1), nil)

		req, err := http.NewRequestWithContext(context.Background(), http.MethodGet,
			"/api/v1/devops/kernel-images/releases?repository=registry.example%2Ftiflash&version=v26.3.99&page=1&per_page=20", nil)
		Expect(err).NotTo(HaveOccurred())
		router.ServeHTTP(recorder, req)

		Expect(recorder.Code).To(Equal(http.StatusOK))
		response := &imageapi.ListReleasesResp{}
		Expect(json.Unmarshal(recorder.Body.Bytes(), response)).To(Succeed())
		Expect(response.ErrCode).To(Equal(devopsapi.ErrCodeOK))
		Expect(response.Total).To(Equal(int64(1)))
		Expect(response.Items).To(HaveLen(1))
	})

	It("starts an asynchronous availability refresh", func() {
		service.EXPECT().RefreshAvailability(gomock.Any(), "kia_1").Return(&ent.Release{
			ArtifactUID: "kia_1", AvailabilityStatus: ent.AvailabilityStatusChecking,
		}, nil)
		req, err := http.NewRequestWithContext(context.Background(), http.MethodPost,
			"/api/v1/devops/kernel-images/releases/kia_1/availability-check", nil)
		Expect(err).NotTo(HaveOccurred())

		router.ServeHTTP(recorder, req)

		Expect(recorder.Code).To(Equal(http.StatusOK))
		response := &imageapi.GetReleaseResp{}
		Expect(json.Unmarshal(recorder.Body.Bytes(), response)).To(Succeed())
		Expect(response.Release.AvailabilityStatus).To(Equal(ent.AvailabilityStatusChecking))
	})

	It("hides the APIs while the feature is disabled", func() {
		service.EXPECT().GetRelease(gomock.Any(), "kia_1").Return(nil, imagesvc.ErrDisabled)

		req, err := http.NewRequestWithContext(context.Background(), http.MethodGet,
			"/api/v1/devops/kernel-images/releases/kia_1", nil)
		Expect(err).NotTo(HaveOccurred())
		router.ServeHTTP(recorder, req)

		Expect(recorder.Code).To(Equal(http.StatusNotFound))
		response := &devopsapi.BaseResp{}
		Expect(json.Unmarshal(recorder.Body.Bytes(), response)).To(Succeed())
		Expect(response.ErrCode).To(Equal(devopsapi.ErrCodeNotFound))
	})
})
