package remediationticket

import (
	"context"
	"testing"

	"github.com/google/go-cmp/cmp"
	"go.uber.org/mock/gomock"

	"github.com/tidbcloud/devops/entity"
	apprent "github.com/tidbcloud/devops/entity/approval"
	tntent "github.com/tidbcloud/devops/entity/tenant"
	"github.com/tidbcloud/devops/pkg/infra/config"
	testutil "github.com/tidbcloud/devops/pkg/infra/utils/test"
	apprsvcapi "github.com/tidbcloud/devops/pkg/service/approval/api"
	rtsvcapi "github.com/tidbcloud/devops/pkg/service/remediationticket/api"
	rtphase "github.com/tidbcloud/devops/pkg/service/remediationticket/phase"
	rmrepo "github.com/tidbcloud/devops/pkg/service/remediationticket/repo"
)

func TestGetApprovalFlowUsesWorkflowDefinitionOrderAndLabels(t *testing.T) {
	ctrl := gomock.NewController(t)
	t.Cleanup(func() {
		if err := rtphase.ConfigureWorkflow(config.RemediationWorkflowConfig{}); err != nil {
			t.Fatalf("reset workflow config failed: %v", err)
		}
	})

	err := rtphase.ConfigureWorkflow(config.RemediationWorkflowConfig{
		Default: config.RemediationWorkflowDefinitionConfig{
			Phases: []string{
				entity.RemediationTicketPhaseCreated,
				entity.RemediationTicketPhaseRegionTSELeader,
				entity.RemediationTicketPhaseRDGSLAOwner,
				entity.RemediationTicketPhaseGlobalDealDesk,
				entity.RemediationTicketPhaseRDGFinalDecisionMaker,
				entity.RemediationTicketPhaseRegionalGM,
				entity.RemediationTicketPhaseSucceed,
				entity.RemediationTicketPhaseClosed,
			},
			Labels: map[string]string{
				entity.RemediationTicketPhaseRDGSLAOwner:           "EG SLA Owner",
				entity.RemediationTicketPhaseRDGFinalDecisionMaker: "EG Decision Maker",
			},
		},
	})
	if err != nil {
		t.Fatalf("ConfigureWorkflow returned error: %v", err)
	}

	repo := rmrepo.NewMockRemediationTicketRepo(ctrl)
	approvalSvc := apprsvcapi.NewMockApprovalServiceAPI(ctrl)
	ticket := &entity.RemediationTicket{
		ID:           42,
		Phase:        ptrPhase(entity.RemediationTicketPhaseRegionalGM),
		Status:       entity.RemediationTicketStatusPending,
		ExtraPercent: "0",
		BizRegion:    ptrRegion(tntent.TenantRegionAPAC),
		ClusterType:  entity.RemediationTicketClusterTypeServerless,
	}
	ticketApprovals := []*entity.RemediationTicketPhaseApproval{
		{TicketId: 42, FromPhase: entity.RemediationTicketPhaseCreated, ToPhase: entity.RemediationTicketPhaseRegionTSELeader, ApprovalID: 101},
		{TicketId: 42, FromPhase: entity.RemediationTicketPhaseRegionTSELeader, ToPhase: entity.RemediationTicketPhaseRDGSLAOwner, ApprovalID: 102},
		{TicketId: 42, FromPhase: entity.RemediationTicketPhaseRDGSLAOwner, ToPhase: entity.RemediationTicketPhaseGlobalDealDesk, ApprovalID: 103},
		{TicketId: 42, FromPhase: entity.RemediationTicketPhaseGlobalDealDesk, ToPhase: entity.RemediationTicketPhaseRDGFinalDecisionMaker, ApprovalID: 104},
	}
	repo.EXPECT().LoadTicket(gomock.Any(), uint64(42)).Return(ticket, nil)
	repo.EXPECT().QueryTicketPhaseApproval(gomock.Any(), gomock.Any()).Return(ticketApprovals, nil)
	approvalSvc.EXPECT().GetApproval(gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()

	service := &svc{
		conf: config.RemediationTicketConfig{
			ResponsiblePersons: config.RTResponsiblePersonConfig{},
		},
		repo:    repo,
		apprSvc: approvalSvc,
	}

	flow, err := service.GetApprovalFlow(context.Background(), 42)
	if err != nil {
		t.Fatalf("GetApprovalFlow returned error: %v", err)
	}

	gotPhases := make([]entity.RemediationTicketPhase, 0, len(flow.Nodes))
	gotLabels := make([]string, 0, len(flow.Nodes))
	for _, node := range flow.Nodes {
		gotPhases = append(gotPhases, node.Phase)
		gotLabels = append(gotLabels, node.Label)
	}

	wantPhases := []entity.RemediationTicketPhase{
		entity.RemediationTicketPhaseCreated,
		entity.RemediationTicketPhaseRegionTSELeader,
		entity.RemediationTicketPhaseRDGSLAOwner,
		entity.RemediationTicketPhaseGlobalDealDesk,
		entity.RemediationTicketPhaseRDGFinalDecisionMaker,
		entity.RemediationTicketPhaseRegionalGM,
		entity.RemediationTicketPhaseSucceed,
		entity.RemediationTicketPhaseClosed,
	}
	wantLabels := []string{
		"Creator",
		"Region TSE Leader",
		"EG SLA Owner",
		"Global Deal Desk",
		"EG Decision Maker",
		"Regional GM",
		"Succeed",
		"Closed",
	}

	if diff := cmp.Diff(wantPhases, gotPhases); diff != "" {
		t.Fatalf("unexpected approval flow phases (-want +got):\n%s", diff)
	}
	if diff := cmp.Diff(wantLabels, gotLabels); diff != "" {
		t.Fatalf("unexpected approval flow labels (-want +got):\n%s", diff)
	}
	if flow.CurrentPhase != entity.RemediationTicketPhaseRegionalGM {
		t.Fatalf("unexpected current phase: got %s want %s", flow.CurrentPhase, entity.RemediationTicketPhaseRegionalGM)
	}
}

func TestGetApprovalFlowReturnsPhaseMetadataAndAction(t *testing.T) {
	ctrl := gomock.NewController(t)
	t.Cleanup(func() {
		if err := rtphase.ConfigureWorkflow(config.RemediationWorkflowConfig{}); err != nil {
			t.Fatalf("reset workflow config failed: %v", err)
		}
	})

	repo := rmrepo.NewMockRemediationTicketRepo(ctrl)
	approvalSvc := apprsvcapi.NewMockApprovalServiceAPI(ctrl)
	ticket := &entity.RemediationTicket{
		ID:           42,
		Phase:        ptrPhase(entity.RemediationTicketPhaseGlobalDealDesk),
		Status:       entity.RemediationTicketStatusApproving,
		ApprID:       1001,
		ExtraPercent: "0",
		BizRegion:    ptrRegion(tntent.TenantRegionAPAC),
		ClusterType:  entity.RemediationTicketClusterTypeServerless,
	}
	ticketApprovals := []*entity.RemediationTicketPhaseApproval{
		{TicketId: 42, FromPhase: entity.RemediationTicketPhaseRDGSLAOwner, ToPhase: entity.RemediationTicketPhaseGlobalDealDesk, ApprovalID: 1001},
	}
	repo.EXPECT().LoadTicket(gomock.Any(), uint64(42)).Return(ticket, nil)
	repo.EXPECT().QueryTicketPhaseApproval(gomock.Any(), gomock.Any()).Return(ticketApprovals, nil)
	approvalSvc.EXPECT().GetApproval(gomock.Any(), uint64(1001)).Return(nil, nil).AnyTimes()

	service := &svc{
		conf: config.RemediationTicketConfig{
			ResponsiblePersons: config.RTResponsiblePersonConfig{},
		},
		repo:    repo,
		apprSvc: approvalSvc,
	}

	flow, err := service.GetApprovalFlow(context.Background(), 42)
	if err != nil {
		t.Fatalf("GetApprovalFlow returned error: %v", err)
	}

	var current *entity.RemediationApprovalFlowNode
	var noInput *entity.RemediationApprovalFlowNode
	for _, node := range flow.Nodes {
		if node.Current {
			current = node
		}
		if node.Phase == entity.RemediationTicketPhaseRDGFinalDecisionMaker {
			noInput = node
		}
	}
	if current == nil {
		t.Fatalf("expected current node")
	}
	if current.Phase != entity.RemediationTicketPhaseGlobalDealDesk {
		t.Fatalf("current phase = %s, want %s", current.Phase, entity.RemediationTicketPhaseGlobalDealDesk)
	}
	if !current.RequiresInput {
		t.Fatalf("global deal desk should require input")
	}
	if current.Action != entity.RemediationApprovalFlowNodeActionApproveWithInput {
		t.Fatalf("current action = %s, want %s", current.Action, entity.RemediationApprovalFlowNodeActionApproveWithInput)
	}
	if noInput == nil {
		t.Fatalf("expected RDG final decision maker node")
	}
	if noInput.RequiresInput {
		t.Fatalf("RDG final decision maker should not require input")
	}
	if !noInput.AutoAdvance {
		t.Fatalf("RDG final decision maker should auto advance")
	}
}

func TestApproveTicketPhaseAutoAdvancesNoInputPhase(t *testing.T) {
	t.Setenv("APP_ENV", "dev")
	t.Setenv("APP_PARTITION", "global")

	ctrl := gomock.NewController(t)
	repo := rmrepo.NewMockRemediationTicketRepo(ctrl)
	dbp := testutil.NewMockTransProvider(ctrl)
	approvalSvc := apprsvcapi.NewMockApprovalServiceAPI(ctrl)

	apprID := uint64(1001)
	nextApprID := uint64(1002)
	phase := entity.RemediationTicketPhaseGlobalDealDesk
	region := tntent.TenantRegionAPAC
	currentTicket := &entity.RemediationTicket{
		ID:                      42,
		Phase:                   &phase,
		Status:                  entity.RemediationTicketStatusApproving,
		ApprID:                  apprID,
		ApplicantEmail:          "creator@pingcap.com",
		CurrentResponsible:      "decision-maker@pingcap.com",
		ExtraPercent:            "0",
		AssociatedIncidentId:    29,
		BizRegion:               &region,
		ClusterType:             entity.RemediationTicketClusterTypeServerless,
		TotalCompensation:       "100",
		ClusterBilling:          "1000",
		AccidentReportLink:      "https://example.com/report",
		RootCauseFromGS:         "gs",
		RootCauseFromRDG:        "rdg",
		MonthlyUptimePercentage: "99.9",
		Version:                 7,
	}

	dbp.EXPECT().Transaction(gomock.Any(), gomock.Any()).DoAndReturn(
		func(ctx context.Context, cb func(context.Context) error) error {
			return cb(ctx)
		},
	).Times(1)
	approvalSvc.EXPECT().
		FinishApprovalTask(gomock.Any(), apprID, gomock.Any()).
		Return(nil).
		Times(1)
	repo.EXPECT().
		LoadTicket(gomock.Any(), uint64(42)).
		DoAndReturn(func(context.Context, uint64) (*entity.RemediationTicket, error) {
			return currentTicket, nil
		}).
		Times(4)
	repo.EXPECT().
		QueryTicketPhaseApproval(gomock.Any(), gomock.Any()).
		Return([]*entity.RemediationTicketPhaseApproval{{
			TicketId:   42,
			FromPhase:  entity.RemediationTicketPhaseGlobalDealDesk,
			ToPhase:    entity.RemediationTicketPhaseRDGFinalDecisionMaker,
			ApprovalID: apprID,
		}}, nil).
		Times(2)
	approvalSvc.EXPECT().
		GetApproval(gomock.Any(), apprID).
		Return(&apprent.Approval{ID: apprID, Status: apprent.ApprovalStatusApproved}, nil).
		Times(1)
	repo.EXPECT().
		FunctionalPatchTicket(gomock.Any(), gomock.Any(), gomock.Any()).
		DoAndReturn(func(ctx context.Context, current *entity.RemediationTicket, patch ...rmrepo.PatchRemediationTicket) error {
			cloned := *current
			for _, p := range patch {
				if err := p(&cloned); err != nil {
					return err
				}
			}
			currentTicket = &cloned
			return nil
		}).
		Times(2)
	approvalSvc.EXPECT().
		CreateApproval(gomock.Any(), gomock.Any()).
		DoAndReturn(func(ctx context.Context, param apprsvcapi.CreateApprovalParam) (*uint64, error) {
			if param.Applicant != "creator@pingcap.com" {
				t.Fatalf("approval applicant = %s, want creator@pingcap.com", param.Applicant)
			}
			if len(param.Nodes) != 1 || param.Nodes[0].Approver == nil || param.Nodes[0].Approver.Email != "regional-gm@pingcap.com" {
				t.Fatalf("approval nodes = %#v, want regional GM approver", param.Nodes)
			}
			return &nextApprID, nil
		}).
		Times(1)
	repo.EXPECT().
		SaveTicketPhaseApproval(gomock.Any(), gomock.Any()).
		DoAndReturn(func(ctx context.Context, approval *entity.RemediationTicketPhaseApproval) (uint64, error) {
			if approval.FromPhase != entity.RemediationTicketPhaseRDGFinalDecisionMaker || approval.ToPhase != entity.RemediationTicketPhaseRegionalGM {
				t.Fatalf("saved approval transition = %s -> %s, want rdg_final_decision_maker -> regional_gm", approval.FromPhase, approval.ToPhase)
			}
			if approval.ApprovalID != nextApprID {
				t.Fatalf("saved approval id = %d, want %d", approval.ApprovalID, nextApprID)
			}
			return uint64(10), nil
		}).
		Times(1)

	service := &svc{
		conf: config.RemediationTicketConfig{
			ResponsiblePersons: config.RTResponsiblePersonConfig{
				RegionalGM: config.RTRegionalGMConfig{
					APAC:   "regional-gm@pingcap.com",
					Backup: "regional-gm@pingcap.com",
				},
			},
		},
		repo:    repo,
		dbp:     dbp,
		apprSvc: approvalSvc,
	}

	if err := service.ApproveTicketPhase(context.Background(), 42, rtsvcapi.ApprovePhaseParam{
		ApprID:        apprID,
		ApproverEmail: "decision-maker@pingcap.com",
	}); err != nil {
		t.Fatalf("ApproveTicketPhase returned error: %v", err)
	}
	if currentTicket.Phase == nil || *currentTicket.Phase != entity.RemediationTicketPhaseRegionalGM {
		t.Fatalf("ticket phase = %v, want regional_gm", currentTicket.Phase)
	}
	if currentTicket.Status != entity.RemediationTicketStatusApproving {
		t.Fatalf("ticket status = %s, want approving", currentTicket.Status)
	}
	if currentTicket.ApprID != nextApprID {
		t.Fatalf("ticket appr id = %d, want %d", currentTicket.ApprID, nextApprID)
	}
	if currentTicket.CurrentResponsible != "regional-gm@pingcap.com" {
		t.Fatalf("current responsible = %s, want regional-gm@pingcap.com", currentTicket.CurrentResponsible)
	}
}

func TestApproveTicketPhaseSubmitsRequiredInput(t *testing.T) {
	t.Setenv("APP_ENV", "dev")
	t.Setenv("APP_PARTITION", "global")

	ctrl := gomock.NewController(t)
	repo := rmrepo.NewMockRemediationTicketRepo(ctrl)
	dbp := testutil.NewMockTransProvider(ctrl)
	approvalSvc := apprsvcapi.NewMockApprovalServiceAPI(ctrl)

	apprID := uint64(2001)
	nextApprID := uint64(2002)
	phase := entity.RemediationTicketPhaseGlobalDealDesk
	currentTicket := &entity.RemediationTicket{
		ID:                   42,
		Phase:                &phase,
		Status:               entity.RemediationTicketStatusApproving,
		ApprID:               apprID,
		ApplicantEmail:       "creator@pingcap.com",
		CurrentResponsible:   "deal-desk@pingcap.com",
		ExtraPercent:         "0",
		AssociatedIncidentId: 29,
		ClusterType:          entity.RemediationTicketClusterTypeServerless,
		Version:              7,
	}

	dbp.EXPECT().Transaction(gomock.Any(), gomock.Any()).DoAndReturn(
		func(ctx context.Context, cb func(context.Context) error) error {
			return cb(ctx)
		},
	).Times(1)
	repo.EXPECT().
		QueryTicketPhaseApproval(gomock.Any(), gomock.Any()).
		Return([]*entity.RemediationTicketPhaseApproval{{
			TicketId:   42,
			FromPhase:  entity.RemediationTicketPhaseRDGFinalDecisionMaker,
			ToPhase:    entity.RemediationTicketPhaseGlobalDealDesk,
			ApprovalID: apprID,
		}}, nil).
		Times(2)
	repo.EXPECT().
		LoadTicket(gomock.Any(), uint64(42)).
		DoAndReturn(func(context.Context, uint64) (*entity.RemediationTicket, error) {
			return currentTicket, nil
		}).
		Times(4)
	approvalSvc.EXPECT().
		FinishApprovalTask(gomock.Any(), apprID, gomock.Any()).
		Return(nil).
		Times(1)
	approvalSvc.EXPECT().
		GetApproval(gomock.Any(), apprID).
		Return(&apprent.Approval{ID: apprID, Status: apprent.ApprovalStatusApproved}, nil).
		Times(1)
	repo.EXPECT().
		FunctionalPatchTicket(gomock.Any(), gomock.Any(), gomock.Any()).
		DoAndReturn(func(ctx context.Context, current *entity.RemediationTicket, patch ...rmrepo.PatchRemediationTicket) error {
			cloned := *current
			for _, p := range patch {
				if err := p(&cloned); err != nil {
					return err
				}
			}
			currentTicket = &cloned
			return nil
		}).
		Times(2)
	approvalSvc.EXPECT().
		CreateApproval(gomock.Any(), gomock.Any()).
		DoAndReturn(func(ctx context.Context, param apprsvcapi.CreateApprovalParam) (*uint64, error) {
			if len(param.Nodes) != 1 || param.Nodes[0].Approver == nil || param.Nodes[0].Approver.Email != "commercial-ops@pingcap.com" {
				t.Fatalf("approval nodes = %#v, want commercial operation approver", param.Nodes)
			}
			return &nextApprID, nil
		}).
		Times(1)
	repo.EXPECT().
		SaveTicketPhaseApproval(gomock.Any(), gomock.Any()).
		DoAndReturn(func(ctx context.Context, approval *entity.RemediationTicketPhaseApproval) (uint64, error) {
			if approval.FromPhase != entity.RemediationTicketPhaseGlobalDealDesk || approval.ToPhase != entity.RemediationTicketPhaseCommercialOperation {
				t.Fatalf("saved approval transition = %s -> %s, want global_deal_desk -> commercial_operation", approval.FromPhase, approval.ToPhase)
			}
			return uint64(11), nil
		}).
		Times(1)

	service := &svc{
		conf: config.RemediationTicketConfig{
			ResponsiblePersons: config.RTResponsiblePersonConfig{
				CommercialOperation: config.RTCommercialOperationConfig{
					Person: "commercial-ops@pingcap.com",
				},
			},
		},
		repo:    repo,
		dbp:     dbp,
		apprSvc: approvalSvc,
	}

	if err := service.ApproveTicketPhase(context.Background(), 42, rtsvcapi.ApprovePhaseParam{
		ApprID:        apprID,
		ApproverEmail: "deal-desk@pingcap.com",
		Input: rtsvcapi.TransferTicketParam{
			ClusterBilling:    "1000",
			TotalCompensation: "100",
			ExtraPercent:      "0",
		},
	}); err != nil {
		t.Fatalf("ApproveTicketPhase returned error: %v", err)
	}
	if currentTicket.Phase == nil || *currentTicket.Phase != entity.RemediationTicketPhaseCommercialOperation {
		t.Fatalf("ticket phase = %v, want commercial_operation", currentTicket.Phase)
	}
	if currentTicket.Status != entity.RemediationTicketStatusApproving {
		t.Fatalf("ticket status = %s, want approving", currentTicket.Status)
	}
	if currentTicket.ApprID != nextApprID {
		t.Fatalf("ticket appr id = %d, want %d", currentTicket.ApprID, nextApprID)
	}
	if currentTicket.ClusterBilling != "1000" || currentTicket.TotalCompensation != "100" {
		t.Fatalf("ticket input was not persisted: billing=%s compensation=%s", currentTicket.ClusterBilling, currentTicket.TotalCompensation)
	}
}

func ptrPhase(phase entity.RemediationTicketPhase) *entity.RemediationTicketPhase {
	return &phase
}

func ptrRegion(region tntent.TenantRegion) *tntent.TenantRegion {
	return &region
}
