package cluster

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

	"github.com/gin-gonic/gin"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	userapiv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/core-server/user/v1"
	mgmtv1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"go.uber.org/mock/gomock"

	"github.com/tidbcloud/devops/api"
	clusterapi "github.com/tidbcloud/devops/api/cluster"
	ctrl "github.com/tidbcloud/devops/pkg/controller"
	"github.com/tidbcloud/devops/pkg/infra/clients/coresvccli"
	"github.com/tidbcloud/devops/pkg/infra/consts"
	"github.com/tidbcloud/devops/pkg/infra/utils/test"
)

func TestMigrationPrecheckController(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "Migration Precheck Controller Suite", test.ReporterConfig())
}

var _ = Describe("DedicatedToPremiumPrecheck Controller", func() {
	var (
		precheckClient *coresvccli.MockDedicatedToPremiumPrecheckServiceClient
		controller     *ClusterController
	)

	BeforeEach(func() {
		gin.SetMode(gin.TestMode)
		mockController := gomock.NewController(GinkgoT())
		precheckClient = coresvccli.NewMockDedicatedToPremiumPrecheckServiceClient(mockController)
		controller = &ClusterController{
			coreSvcCli: &coresvccli.CoreSvcCli{
				Mgmt: &coresvccli.MgmtService{
					DedicatedToPremiumPrecheck: precheckClient,
				},
			},
		}
	})

	newContext := func(method, target string, body []byte, params gin.Params) (*gin.Context, *httptest.ResponseRecorder) {
		recorder := httptest.NewRecorder()
		ctx, _ := gin.CreateTestContext(recorder)
		ctx.Request = httptest.NewRequest(method, target, bytes.NewReader(body))
		ctx.Request.Header.Set("Content-Type", "application/json")
		ctx.Params = params
		return ctx, recorder
	}

	It("lists prechecks with source cluster and pagination", func() {
		precheckClient.EXPECT().ListDedicatedToPremiumPrechecks(gomock.Any(), &mgmtv1.ListDedicatedToPremiumPrechecksRequest{
			SourceClusterId: 101,
			Page:            2,
			PageSize:        30,
		}).Return(&mgmtv1.ListDedicatedToPremiumPrechecksResponse{
			Prechecks: []*mgmtv1.DedicatedToPremiumPrecheck{{Id: 201}},
			Total:     1,
		}, nil)
		ctx, recorder := newContext(http.MethodGet, "/?source_cluster_id=101&page=2&per_page=30", nil, nil)

		controller.ListDedicatedToPremiumPrechecks(ctx)

		Expect(recorder.Code).To(Equal(http.StatusOK))
		resp := &clusterapi.ListDedicatedToPremiumPrechecksResp{}
		Expect(json.Unmarshal(recorder.Body.Bytes(), resp)).To(Succeed())
		Expect(resp.ErrCode).To(Equal(api.ErrCodeOK))
		Expect(resp.Total).To(Equal(uint32(1)))
		Expect(resp.Items).To(HaveLen(1))
		Expect(resp.Items[0].ID).To(Equal("201"))
	})

	It("uses default pagination", func() {
		precheckClient.EXPECT().ListDedicatedToPremiumPrechecks(gomock.Any(), &mgmtv1.ListDedicatedToPremiumPrechecksRequest{
			SourceClusterId: 0,
			Page:            1,
			PageSize:        100,
		}).Return(&mgmtv1.ListDedicatedToPremiumPrechecksResponse{}, nil)
		ctx, recorder := newContext(http.MethodGet, "/", nil, nil)

		controller.ListDedicatedToPremiumPrechecks(ctx)

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

	It("rejects invalid pagination", func() {
		ctx, recorder := newContext(http.MethodGet, "/?page=0", nil, nil)

		controller.ListDedicatedToPremiumPrechecks(ctx)

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

	It("rejects an invalid optional source cluster id", func() {
		ctx, recorder := newContext(http.MethodGet, "/?source_cluster_id=invalid", nil, nil)

		controller.ListDedicatedToPremiumPrechecks(ctx)

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

	It("creates a precheck directly and injects the applicant", func() {
		precheckClient.EXPECT().CreateDedicatedToPremiumPrecheck(gomock.Any(), &mgmtv1.CreateDedicatedToPremiumPrecheckRequest{
			SourceClusterId:        101,
			MigrationAccountEmail:  "migration@pingcap.com",
			TargetPremiumProjectId: 301,
			Applicant:              "applicant@pingcap.com",
		}).Return(&mgmtv1.DedicatedToPremiumPrecheck{Id: 201}, nil)
		body := []byte(`{"source_cluster_id":"101","migration_account_email":"migration@pingcap.com","target_premium_project_id":"301"}`)
		ctx, recorder := newContext(http.MethodPost, "/", body, nil)
		ctx.Set(consts.CtxUserProfile, &userapiv1.PrincipalProfile{UserEmail: "applicant@pingcap.com"})

		controller.CreateDedicatedToPremiumPrecheck(ctx)

		Expect(recorder.Code).To(Equal(http.StatusOK))
		resp := &clusterapi.DedicatedToPremiumPrecheckResp{}
		Expect(json.Unmarshal(recorder.Body.Bytes(), resp)).To(Succeed())
		Expect(resp.Precheck.ID).To(Equal("201"))
	})

	It("creates a precheck without a target premium project id", func() {
		precheckClient.EXPECT().CreateDedicatedToPremiumPrecheck(gomock.Any(), &mgmtv1.CreateDedicatedToPremiumPrecheckRequest{
			SourceClusterId:       101,
			MigrationAccountEmail: "migration@pingcap.com",
			Applicant:             "applicant@pingcap.com",
		}).Return(&mgmtv1.DedicatedToPremiumPrecheck{Id: 201}, nil)
		body := []byte(`{"source_cluster_id":"101","migration_account_email":"migration@pingcap.com"}`)
		ctx, recorder := newContext(http.MethodPost, "/", body, nil)
		ctx.Set(consts.CtxUserProfile, &userapiv1.PrincipalProfile{UserEmail: "applicant@pingcap.com"})

		controller.CreateDedicatedToPremiumPrecheck(ctx)

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

	It("creates a precheck without a migration account email", func() {
		precheckClient.EXPECT().CreateDedicatedToPremiumPrecheck(gomock.Any(), &mgmtv1.CreateDedicatedToPremiumPrecheckRequest{
			SourceClusterId:        101,
			TargetPremiumProjectId: 301,
			Applicant:              "applicant@pingcap.com",
		}).Return(&mgmtv1.DedicatedToPremiumPrecheck{Id: 201}, nil)
		body := []byte(`{"source_cluster_id":"101","target_premium_project_id":"301"}`)
		ctx, recorder := newContext(http.MethodPost, "/", body, nil)
		ctx.Set(consts.CtxUserProfile, &userapiv1.PrincipalProfile{UserEmail: "applicant@pingcap.com"})

		controller.CreateDedicatedToPremiumPrecheck(ctx)

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

	It("gets a precheck only by precheck id", func() {
		precheckClient.EXPECT().GetDedicatedToPremiumPrecheck(gomock.Any(), &mgmtv1.GetDedicatedToPremiumPrecheckRequest{
			Id: 201,
		}).Return(&mgmtv1.DedicatedToPremiumPrecheck{
			Id:              201,
			SourceClusterId: 999,
		}, nil)
		ctx, recorder := newContext(http.MethodGet, "/", nil, gin.Params{
			{Key: "precheck_id", Value: "201"},
		})

		controller.GetDedicatedToPremiumPrecheck(ctx)

		Expect(recorder.Code).To(Equal(http.StatusOK))
		resp := &clusterapi.DedicatedToPremiumPrecheckResp{}
		Expect(json.Unmarshal(recorder.Body.Bytes(), resp)).To(Succeed())
		Expect(resp.Precheck.SourceClusterID).To(Equal("999"))
	})

	DescribeTable("rejects invalid input",
		func(invoke func(*gin.Context), method string, body []byte, params gin.Params, withProfile bool) {
			ctx, recorder := newContext(method, "/", body, params)
			if withProfile {
				ctx.Set(consts.CtxUserProfile, &userapiv1.PrincipalProfile{UserEmail: "applicant@pingcap.com"})
			}

			invoke(ctx)

			Expect(recorder.Code).To(Equal(http.StatusBadRequest))
		},
		Entry("create with missing source cluster id", func(ctx *gin.Context) {
			controller.CreateDedicatedToPremiumPrecheck(ctx)
		}, http.MethodPost, []byte(`{"migration_account_email":"migration@pingcap.com","target_premium_project_id":"301"}`), nil, true),
		Entry("create with invalid source cluster id", func(ctx *gin.Context) {
			controller.CreateDedicatedToPremiumPrecheck(ctx)
		}, http.MethodPost, []byte(`{"source_cluster_id":"invalid","migration_account_email":"migration@pingcap.com","target_premium_project_id":"301"}`), nil, true),
		Entry("create with invalid project id", func(ctx *gin.Context) {
			controller.CreateDedicatedToPremiumPrecheck(ctx)
		}, http.MethodPost, []byte(`{"source_cluster_id":"101","migration_account_email":"migration@pingcap.com","target_premium_project_id":"invalid"}`), nil, true),
		Entry("get with invalid precheck id", func(ctx *gin.Context) {
			controller.GetDedicatedToPremiumPrecheck(ctx)
		}, http.MethodGet, nil, gin.Params{{Key: "precheck_id", Value: "invalid"}}, false),
	)

	It("returns unauthenticated when the user profile is missing", func() {
		body := []byte(`{"source_cluster_id":"101","migration_account_email":"migration@pingcap.com","target_premium_project_id":"301"}`)
		ctx, recorder := newContext(http.MethodPost, "/", body, nil)

		controller.CreateDedicatedToPremiumPrecheck(ctx)

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

	It("passes downstream errors through the standard failure response", func() {
		precheckClient.EXPECT().ListDedicatedToPremiumPrechecks(gomock.Any(), gomock.Any()).Return(nil, errors.New("downstream failed"))
		ctx, recorder := newContext(http.MethodGet, "/", nil, nil)

		controller.ListDedicatedToPremiumPrechecks(ctx)

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

	It("registers only the cluster-level precheck routes", func() {
		engine := gin.New()
		Expect(controller.RegisterAPI(&ctrl.Suite{Router: engine})).To(Succeed())

		routes := map[string]bool{}
		for _, route := range engine.Routes() {
			routes[route.Method+" "+route.Path] = true
		}

		basePath := "/cluster/migration/dedicate2premium/prechecks"
		Expect(routes).To(HaveKey(http.MethodGet + " " + basePath))
		Expect(routes).To(HaveKey(http.MethodPost + " " + basePath))
		Expect(routes).To(HaveKey(http.MethodGet + " " + basePath + "/:precheck_id"))
		Expect(routes).NotTo(HaveKey(http.MethodGet + " /clusters/:cluster_id/migration/dedicate2premium/prechecks"))
		Expect(routes).NotTo(HaveKey(http.MethodPost + " /clusters/:cluster_id/migration/dedicate2premium/prechecks"))
		Expect(routes).NotTo(HaveKey(http.MethodGet + " /clusters/:cluster_id/migration/dedicate2premium/prechecks/:precheck_id"))
	})
})
