package cluster

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

	"github.com/gin-gonic/gin"
	dedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	mgmtv1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"go.uber.org/mock/gomock"
	"google.golang.org/grpc"
	"google.golang.org/protobuf/encoding/protojson"

	clusterapi "github.com/tidbcloud/devops/api/cluster"
	opent "github.com/tidbcloud/devops/entity/operation"
	"github.com/tidbcloud/devops/pkg/infra/clients/coresvccli"
	"github.com/tidbcloud/devops/pkg/infra/consts"
	opsvcapi "github.com/tidbcloud/devops/pkg/service/operation"
	opsvcclusteraction "github.com/tidbcloud/devops/pkg/service/operation/actions/clusteraction"
)

type fakeHeterogeneousDeployOperationService struct {
	createID    uint64
	createErr   error
	createCalls int
	createParam opsvcapi.CreateInstanceParam
}

func (f *fakeHeterogeneousDeployOperationService) ListInstances(context.Context, opsvcapi.ListInstanceParam) ([]*opent.Instance, int64, error) {
	return nil, 0, nil
}

func (f *fakeHeterogeneousDeployOperationService) GetInstance(context.Context, uint64) (*opent.Instance, error) {
	return nil, nil
}

func (f *fakeHeterogeneousDeployOperationService) CreateInstance(_ context.Context, param opsvcapi.CreateInstanceParam) (*uint64, error) {
	f.createCalls++
	f.createParam = param
	if f.createErr != nil {
		return nil, f.createErr
	}
	return &f.createID, nil
}

func (f *fakeHeterogeneousDeployOperationService) PreviewInstance(context.Context, opsvcapi.CreateInstanceParam) (*opsvcapi.PreviewInstanceResult, error) {
	return nil, nil
}

func (f *fakeHeterogeneousDeployOperationService) SubmitInstance(context.Context, uint64) error {
	return nil
}

func (f *fakeHeterogeneousDeployOperationService) UpdateInstance(context.Context, uint64, opsvcapi.UpdateInstanceParam) error {
	return nil
}

func (f *fakeHeterogeneousDeployOperationService) CancelInstance(context.Context, uint64) error {
	return nil
}

func (f *fakeHeterogeneousDeployOperationService) StartInstanceAction(context.Context, uint64, opent.ActionOrder, string) error {
	return nil
}

func (f *fakeHeterogeneousDeployOperationService) FinishInstanceAction(context.Context, uint64, opent.ActionOrder, opent.ActionStatus, string) error {
	return nil
}

func (f *fakeHeterogeneousDeployOperationService) ListOperations(context.Context, opsvcapi.ListOperationParam) ([]*opent.Operation, error) {
	return nil, nil
}

func (f *fakeHeterogeneousDeployOperationService) GetOperation(context.Context, string, opsvcapi.GetOperationParam) (*opent.Operation, error) {
	return nil, nil
}

func (f *fakeHeterogeneousDeployOperationService) ListOperationCategories(context.Context) ([]string, error) {
	return nil, nil
}

func (f *fakeHeterogeneousDeployOperationService) ListActions(context.Context) ([]*opent.ActionMetadata, error) {
	return nil, nil
}

func (f *fakeHeterogeneousDeployOperationService) GetAction(context.Context, opent.ActionName) (*opent.ActionMetadata, error) {
	return nil, nil
}

func (f *fakeHeterogeneousDeployOperationService) GetOperationClusterWhiteListInfo(context.Context, string) (string, error) {
	return "", nil
}

func newHeterogeneousDeployController(clusterClient mgmtv1.ClusterServiceClient, opSvc opsvcapi.OperationServiceAPI) *ClusterController {
	return &ClusterController{
		coreSvcCli: &coresvccli.CoreSvcCli{
			Mgmt: &coresvccli.MgmtService{
				Cluster: clusterClient,
			},
		},
		operationSvc: opSvc,
	}
}

func newHeterogeneousDeployContext(method, path, clusterID string, body any) (*gin.Context, *httptest.ResponseRecorder) {
	gin.SetMode(gin.TestMode)
	recorder := httptest.NewRecorder()
	ctx, _ := gin.CreateTestContext(recorder)

	var reqBody *bytes.Reader
	if body == nil {
		reqBody = bytes.NewReader(nil)
	} else {
		data, err := json.Marshal(body)
		if err != nil {
			panic(err)
		}
		reqBody = bytes.NewReader(data)
	}
	ctx.Request = httptest.NewRequest(method, path, reqBody)
	ctx.Request.Header.Set("Content-Type", "application/json")
	ctx.Params = gin.Params{{Key: "cluster_id", Value: clusterID}}
	return ctx, recorder
}

func expectHeterogeneousDeployClusterLookup(t *testing.T, mockClient *coresvccli.MockClusterServiceClient, clusterID, tenantID, projectID uint64) {
	t.Helper()
	mockClient.EXPECT().
		ListClusters(gomock.Any(), gomock.Any()).
		DoAndReturn(func(_ context.Context, req *mgmtv1.ListClustersRequest, _ ...grpc.CallOption) (*mgmtv1.ListClustersResponse, error) {
			if len(req.GetFilter().GetClusterIds()) != 1 || req.GetFilter().GetClusterIds()[0] != clusterID {
				t.Fatalf("unexpected cluster lookup request: %+v", req)
			}
			return &mgmtv1.ListClustersResponse{
				Clusters: []*mgmtv1.Cluster{
					{
						ClusterId:      clusterID,
						OrganizationId: tenantID,
						ProjectId:      projectID,
					},
				},
			}, nil
		})
}

func TestApplyHeterogeneousDeployCreatesOperation(t *testing.T) {
	ctrl := gomock.NewController(t)
	mockClient := coresvccli.NewMockClusterServiceClient(ctrl)
	expectHeterogeneousDeployClusterLookup(t, mockClient, 123, 456, 789)

	opSvc := &fakeHeterogeneousDeployOperationService{createID: 999}
	controller := newHeterogeneousDeployController(mockClient, opSvc)
	ctx, recorder := newHeterogeneousDeployContext(http.MethodPost, "/clusters/123/hetero-deploy/apply", "123", map[string]any{
		"component_type":            clusterapi.ComponentTypeTidb,
		"current_instance_type":     "n2-standard-4",
		"alternative_instance_type": "n2-standard-8",
		"tidb_group_id":             "9007199254740993",
		"reason":                    "scale heterogeneous tidb",
	})
	ctx.Set(consts.CtxUserEmail, "user@example.com")

	controller.ApplyHeterogeneousDeploy(ctx)

	if recorder.Code != http.StatusOK {
		t.Fatalf("unexpected status: %d body=%s", recorder.Code, recorder.Body.String())
	}
	var resp clusterapi.ApplyHeterogeneousDeployResponse
	if err := json.Unmarshal(recorder.Body.Bytes(), &resp); err != nil {
		t.Fatal(err)
	}
	if resp.InstanceID != 999 {
		t.Fatalf("unexpected instance id: %d", resp.InstanceID)
	}
	if opSvc.createCalls != 1 {
		t.Fatalf("expected one CreateInstance call, got %d", opSvc.createCalls)
	}
	param := opSvc.createParam
	if param.Type != opent.OperationTargetCluster || param.Operation != "ApplyHeterogeneousDeployForDedicatedCluster" ||
		param.Reason != "scale heterogeneous tidb" || param.Applicant != "user@example.com" {
		t.Fatalf("unexpected create param: %+v", param)
	}
	if param.Params[opent.ParamClusterID] != "123" {
		t.Fatalf("unexpected cluster id param: %s", param.Params[opent.ParamClusterID])
	}
	if len(param.Actions) != 1 ||
		param.Actions[0].Name != opsvcclusteraction.ActionApplyHeterogeneousDeploy.Name ||
		param.Actions[0].StartPolicy != opent.ActionStartPolicyManual {
		t.Fatalf("unexpected actions: %+v", param.Actions)
	}

	var opReq mgmtv1.ApplyGKEHeterogeneousDeployRequest
	if err := protojson.Unmarshal([]byte(param.Params[opent.ParamApplyHeterogeneousDeployRequest]), &opReq); err != nil {
		t.Fatal(err)
	}
	if opReq.GetClusterId() != 123 || opReq.GetTenantId() != 456 || opReq.GetProjectId() != 789 ||
		opReq.GetComponentType() != dedicatedv1.ComponentType_TIDB || opReq.GetCurrentInstanceType() != "n2-standard-4" ||
		opReq.GetAlternativeInstanceType() != "n2-standard-8" || opReq.GetTidbGroupId() != 9007199254740993 {
		t.Fatalf("unexpected operation request: %+v", &opReq)
	}
}

func TestDeleteHeterogeneousDeployCreatesOperation(t *testing.T) {
	ctrl := gomock.NewController(t)
	mockClient := coresvccli.NewMockClusterServiceClient(ctrl)
	expectHeterogeneousDeployClusterLookup(t, mockClient, 123, 456, 789)

	opSvc := &fakeHeterogeneousDeployOperationService{createID: 998}
	controller := newHeterogeneousDeployController(mockClient, opSvc)
	ctx, recorder := newHeterogeneousDeployContext(http.MethodPost, "/clusters/123/hetero-deploy/delete", "123", map[string]any{
		"component_type": clusterapi.ComponentTypeTikv,
		"tidb_group_id":  "9007199254740995",
		"reason":         "delete heterogeneous tikv",
	})
	ctx.Set(consts.CtxUserEmail, "user@example.com")

	controller.DeleteHeterogeneousDeploy(ctx)

	if recorder.Code != http.StatusOK {
		t.Fatalf("unexpected status: %d body=%s", recorder.Code, recorder.Body.String())
	}
	var resp clusterapi.DeleteHeterogeneousDeployResponse
	if err := json.Unmarshal(recorder.Body.Bytes(), &resp); err != nil {
		t.Fatal(err)
	}
	if resp.InstanceID != 998 {
		t.Fatalf("unexpected instance id: %d", resp.InstanceID)
	}
	param := opSvc.createParam
	if param.Type != opent.OperationTargetCluster || param.Operation != "DeleteHeterogeneousDeployForDedicatedCluster" ||
		param.Reason != "delete heterogeneous tikv" || param.Applicant != "user@example.com" {
		t.Fatalf("unexpected create param: %+v", param)
	}
	if len(param.Actions) != 1 ||
		param.Actions[0].Name != opsvcclusteraction.ActionDeleteHeterogeneousDeploy.Name ||
		param.Actions[0].StartPolicy != opent.ActionStartPolicyManual {
		t.Fatalf("unexpected actions: %+v", param.Actions)
	}

	var opReq mgmtv1.DeleteGKEHeterogeneousDeployRequest
	if err := protojson.Unmarshal([]byte(param.Params[opent.ParamDeleteHeterogeneousDeployRequest]), &opReq); err != nil {
		t.Fatal(err)
	}
	if opReq.GetClusterId() != 123 || opReq.GetTenantId() != 456 || opReq.GetProjectId() != 789 ||
		opReq.GetComponentType() != dedicatedv1.ComponentType_TIKV || opReq.GetTidbGroupId() != 9007199254740995 {
		t.Fatalf("unexpected operation request: %+v", &opReq)
	}
}

func TestGetHeterogeneousDeployCallsMgmt(t *testing.T) {
	ctrl := gomock.NewController(t)
	mockClient := coresvccli.NewMockClusterServiceClient(ctrl)
	expectHeterogeneousDeployClusterLookup(t, mockClient, 123, 456, 789)
	mockClient.EXPECT().
		GetGKEHeterogeneousDeploy(gomock.Any(), gomock.Any()).
		DoAndReturn(func(_ context.Context, req *mgmtv1.GetGKEHeterogeneousDeployRequest, _ ...grpc.CallOption) (*mgmtv1.GKEHeterogeneousDeploy, error) {
			if req.GetClusterId() != 123 || req.GetTenantId() != 456 || req.GetProjectId() != 789 ||
				req.GetComponentType() != dedicatedv1.ComponentType_TIDB || req.GetTidbGroupId() != 9007199254740993 {
				t.Fatalf("unexpected get request: %+v", req)
			}
			return &mgmtv1.GKEHeterogeneousDeploy{
				Enabled:                 true,
				ComponentType:           dedicatedv1.ComponentType_TIDB,
				CurrentInstanceType:     "n2-standard-4",
				AlternativeInstanceType: "n2-standard-8",
				TidbGroupId:             9007199254740993,
				Status:                  "applied",
				PodCounts: []*dedicatedv1.GKEHeterogeneousDeployPodCount{
					{
						MachineType: "n2-standard-4",
						PodCount:    2,
					},
					{
						MachineType: "n2-standard-8",
						PodCount:    1,
					},
				},
			}, nil
		})
	controller := newHeterogeneousDeployController(mockClient, nil)
	ctx, recorder := newHeterogeneousDeployContext(http.MethodGet, "/clusters/123/hetero-deploy/get?component_type=tidb&tidb_group_id=9007199254740993", "123", nil)

	controller.GetHeterogeneousDeploy(ctx)

	if recorder.Code != http.StatusOK {
		t.Fatalf("unexpected status: %d body=%s", recorder.Code, recorder.Body.String())
	}
	var resp clusterapi.GetHeterogeneousDeployResponse
	if err := json.Unmarshal(recorder.Body.Bytes(), &resp); err != nil {
		t.Fatal(err)
	}
	if resp.Deploy == nil || !resp.Deploy.Enabled || resp.Deploy.ComponentType != clusterapi.ComponentTypeTidb ||
		resp.Deploy.CurrentInstanceType != "n2-standard-4" || resp.Deploy.AlternativeInstanceType != "n2-standard-8" ||
		resp.Deploy.TiDBGroupID != "9007199254740993" || resp.Deploy.Status != "applied" {
		t.Fatalf("unexpected response: %+v", resp.Deploy)
	}
	if len(resp.Deploy.PodCounts) != 2 ||
		resp.Deploy.PodCounts[0].MachineType != "n2-standard-4" || resp.Deploy.PodCounts[0].PodCount != 2 ||
		resp.Deploy.PodCounts[1].MachineType != "n2-standard-8" || resp.Deploy.PodCounts[1].PodCount != 1 {
		t.Fatalf("unexpected pod counts: %+v", resp.Deploy.PodCounts)
	}
}

func TestHeterogeneousDeployRejectsInvalidClusterID(t *testing.T) {
	controller := &ClusterController{}
	ctx, recorder := newHeterogeneousDeployContext(http.MethodGet, "/clusters/bad/hetero-deploy/get?component_type=tidb", "bad", nil)

	controller.GetHeterogeneousDeploy(ctx)

	if recorder.Code != http.StatusBadRequest {
		t.Fatalf("unexpected status: %d body=%s", recorder.Code, recorder.Body.String())
	}
}

func TestApplyHeterogeneousDeployRejectsInvalidComponentType(t *testing.T) {
	ctrl := gomock.NewController(t)
	mockClient := coresvccli.NewMockClusterServiceClient(ctrl)
	expectHeterogeneousDeployClusterLookup(t, mockClient, 123, 456, 789)
	opSvc := &fakeHeterogeneousDeployOperationService{createID: 999}
	controller := newHeterogeneousDeployController(mockClient, opSvc)
	ctx, recorder := newHeterogeneousDeployContext(http.MethodPost, "/clusters/123/hetero-deploy/apply", "123", map[string]any{
		"component_type": "BAD",
	})
	ctx.Set(consts.CtxUserEmail, "user@example.com")

	controller.ApplyHeterogeneousDeploy(ctx)

	if recorder.Code != http.StatusBadRequest {
		t.Fatalf("unexpected status: %d body=%s", recorder.Code, recorder.Body.String())
	}
	if opSvc.createCalls != 0 {
		t.Fatalf("unexpected CreateInstance calls: %d", opSvc.createCalls)
	}
}

func TestApplyHeterogeneousDeployRejectsMissingUserEmail(t *testing.T) {
	ctrl := gomock.NewController(t)
	mockClient := coresvccli.NewMockClusterServiceClient(ctrl)
	expectHeterogeneousDeployClusterLookup(t, mockClient, 123, 456, 789)
	opSvc := &fakeHeterogeneousDeployOperationService{createID: 999}
	controller := newHeterogeneousDeployController(mockClient, opSvc)
	ctx, recorder := newHeterogeneousDeployContext(http.MethodPost, "/clusters/123/hetero-deploy/apply", "123", map[string]any{
		"component_type": clusterapi.ComponentTypeTidb,
	})

	controller.ApplyHeterogeneousDeploy(ctx)

	if recorder.Code != http.StatusInternalServerError {
		t.Fatalf("unexpected status: %d body=%s", recorder.Code, recorder.Body.String())
	}
	if opSvc.createCalls != 0 {
		t.Fatalf("unexpected CreateInstance calls: %d", opSvc.createCalls)
	}
}

func TestHeterogeneousDeployFailsWhenGetClusterFails(t *testing.T) {
	ctrl := gomock.NewController(t)
	mockClient := coresvccli.NewMockClusterServiceClient(ctrl)
	mockClient.EXPECT().
		ListClusters(gomock.Any(), gomock.Any()).
		Return(nil, errors.New("get cluster failed"))
	controller := newHeterogeneousDeployController(mockClient, nil)
	ctx, recorder := newHeterogeneousDeployContext(http.MethodGet, "/clusters/123/hetero-deploy/get?component_type=tidb", "123", nil)

	controller.GetHeterogeneousDeploy(ctx)

	if recorder.Code != http.StatusInternalServerError {
		t.Fatalf("unexpected status: %d body=%s", recorder.Code, recorder.Body.String())
	}
}

func TestGetHeterogeneousDeployFailsWhenMgmtFails(t *testing.T) {
	ctrl := gomock.NewController(t)
	mockClient := coresvccli.NewMockClusterServiceClient(ctrl)
	expectHeterogeneousDeployClusterLookup(t, mockClient, 123, 456, 789)
	mockClient.EXPECT().
		GetGKEHeterogeneousDeploy(gomock.Any(), gomock.Any()).
		Return(nil, errors.New("mgmt get failed"))
	controller := newHeterogeneousDeployController(mockClient, nil)
	ctx, recorder := newHeterogeneousDeployContext(http.MethodGet, "/clusters/123/hetero-deploy/get?component_type=tidb", "123", nil)

	controller.GetHeterogeneousDeploy(ctx)

	if recorder.Code != http.StatusInternalServerError {
		t.Fatalf("unexpected status: %d body=%s", recorder.Code, recorder.Body.String())
	}
}

func TestApplyHeterogeneousDeployFailsWhenCreateOperationFails(t *testing.T) {
	ctrl := gomock.NewController(t)
	mockClient := coresvccli.NewMockClusterServiceClient(ctrl)
	expectHeterogeneousDeployClusterLookup(t, mockClient, 123, 456, 789)
	opSvc := &fakeHeterogeneousDeployOperationService{createErr: errors.New("create operation failed")}
	controller := newHeterogeneousDeployController(mockClient, opSvc)
	ctx, recorder := newHeterogeneousDeployContext(http.MethodPost, "/clusters/123/hetero-deploy/apply", "123", map[string]any{
		"component_type": clusterapi.ComponentTypeTidb,
	})
	ctx.Set(consts.CtxUserEmail, "user@example.com")

	controller.ApplyHeterogeneousDeploy(ctx)

	if recorder.Code != http.StatusInternalServerError {
		t.Fatalf("unexpected status: %d body=%s", recorder.Code, recorder.Body.String())
	}
}
