package automation

import (
	"context"
	"strings"
	"testing"

	"github.com/nexuscrm/backend/internal/domain/ports"
	"github.com/nexuscrm/backend/pkg/logging"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
)

func newApprovalServiceWithEmailMocks(mockQ *MockApprovalQuery, mockP *MockApprovalPersistence, mockEmail *MockEmailSender) *ApprovalService {
	logger := logging.New(context.Background())
	actionSvc := NewActionService(nil, mockP, nil, nil, mockEmail, logger)
	return &ApprovalService{
		query:       mockQ,
		persistence: mockP,
		actions:     actionSvc,
		logger:      logger,
	}
}

func sentEmailRecipients(t *testing.T, mockEmail *MockEmailSender) []string {
	t.Helper()

	recipients := make([]string, 0, len(mockEmail.Calls))
	for _, call := range mockEmail.Calls {
		msg, ok := call.Arguments.Get(1).(*ports.EmailMessage)
		require.True(t, ok)
		if len(msg.To) > 0 {
			recipients = append(recipients, msg.To[0])
		}
	}
	return recipients
}

func sentEmailSubjects(t *testing.T, mockEmail *MockEmailSender) []string {
	t.Helper()

	subjects := make([]string, 0, len(mockEmail.Calls))
	for _, call := range mockEmail.Calls {
		msg, ok := call.Arguments.Get(1).(*ports.EmailMessage)
		require.True(t, ok)
		subjects = append(subjects, msg.Subject)
	}
	return subjects
}

func TestSubmit_EmailAlertSendsTemplatedEmailsToCreatedApprovers(t *testing.T) {
	mockQ := new(MockApprovalQuery)
	mockP := new(MockApprovalPersistence)
	mockEmail := new(MockEmailSender)
	mockEmail.On("Send", mock.Anything, mock.Anything).Return(nil).Twice()
	svc := newApprovalServiceWithEmailMocks(mockQ, mockP, mockEmail)

	ctx := context.Background()
	user := &models.UserSession{ID: "submitter-1"}
	processDef := models.SObject{
		constants.FieldID: "pd-1",
		"Metadata": `{
			"approvalStep": {
				"name": "Region_Approval",
				"label": "Region Approval",
				"assignedApprover": {
					"approver": [
						{ "name": "kevin.lu@pingcap.com", "type": "user" },
						{ "name": "stephan.debray@pingcap.com", "type": "user" }
					],
					"whenMultipleApprovers": "Unanimous"
				}
			}
		}`,
		"TableEnumOrId": "Opportunity",
	}
	firstNode := models.SObject{
		constants.FieldID: "node-1",
		"DeveloperName":   "Region_Approval",
		"Name":            "Region Approval",
	}

	mockQ.On("QueryWithFilter", mock.Anything, constants.TableProcessDefinition, mock.Anything, user, constants.ColProcessDefinition_ProcessOrder, "ASC", 100).
		Return(&models.QueryResult{TotalSize: 1, Records: []models.SObject{processDef}}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, constants.TableProcessInstance, mock.Anything, user, "", "", 1).
		Return(&models.QueryResult{TotalSize: 0, Records: []models.SObject{}}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, constants.TableProcessNode, mock.Anything, user, "StepOrder", "ASC", 100).
		Return(&models.QueryResult{TotalSize: 1, Records: []models.SObject{firstNode}}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, constants.TableUser, mock.MatchedBy(func(filter string) bool {
		return strings.Contains(filter, "Id == 'kevin.lu@pingcap.com'")
	}), user, "", "", 1).
		Return(&models.QueryResult{TotalSize: 0, Records: []models.SObject{}}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, constants.TableUser, mock.MatchedBy(func(filter string) bool {
		return strings.Contains(filter, "Username == 'kevin.lu@pingcap.com'")
	}), user, "", "", 1).
		Return(&models.QueryResult{TotalSize: 1, Records: []models.SObject{{constants.FieldID: "user-kevin"}}}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, constants.TableUser, mock.MatchedBy(func(filter string) bool {
		return strings.Contains(filter, "Id == 'stephan.debray@pingcap.com'")
	}), user, "", "", 1).
		Return(&models.QueryResult{TotalSize: 0, Records: []models.SObject{}}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, constants.TableUser, mock.MatchedBy(func(filter string) bool {
		return strings.Contains(filter, "Username == 'stephan.debray@pingcap.com'")
	}), user, "", "", 1).
		Return(&models.QueryResult{TotalSize: 1, Records: []models.SObject{{constants.FieldID: "user-stephan"}}}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, "ProcessAction", mock.MatchedBy(func(filter string) bool {
		return strings.Contains(filter, "ProcessDefinitionId == 'pd-1'") && strings.Contains(filter, "ActionEvent == 'Submit'")
	}), user, "Order", "ASC", 100).
		Return(&models.QueryResult{TotalSize: 1, Records: []models.SObject{{
			constants.FieldID: "act-submit-email",
			"Name":            "Submit Notification",
			"ActionType":      "Alert",
			"ActionEvent":     "Submit",
			"EmailTemplateId": "tmpl-submit",
		}}}, nil).Once()

	mockP.On("Insert", mock.MatchedBy(isElevatedSecurityContext), constants.TableProcessInstance, mock.Anything, user).
		Return(models.SObject{constants.FieldID: "inst-1"}, nil).Once()
	mockP.On("Insert", mock.MatchedBy(isElevatedSecurityContext), constants.TableProcessInstanceNode, mock.Anything, user).
		Return(models.SObject{constants.FieldID: "pin-1"}, nil).Once()
	mockP.On("Insert", mock.MatchedBy(isElevatedSecurityContext), constants.TableProcessInstanceWorkitem, mock.Anything, user).
		Return(models.SObject{constants.FieldID: "wi-1"}, nil).Once()
	mockP.On("Insert", mock.MatchedBy(isElevatedSecurityContext), constants.TableProcessInstanceWorkitem, mock.Anything, user).
		Return(models.SObject{constants.FieldID: "wi-2"}, nil).Once()
	mockP.On("GetRaw", mock.Anything, "Opportunity", "opp-1").
		Return(models.SObject{constants.FieldID: "opp-1", "Name": "Enterprise Deal"}, nil).Once()
	mockP.On("GetRaw", mock.Anything, constants.TableEmailTemplate, "tmpl-submit").
		Return(models.SObject{
			constants.FieldID:                        "tmpl-submit",
			constants.FieldSysEmailTemplate_Subject:  "Approval Submitted",
			constants.FieldSysEmailTemplate_Body:     "Please review {{Name}}",
			constants.FieldSysEmailTemplate_IsActive: int64(1),
		}, nil).Twice()
	mockP.On("GetRaw", mock.Anything, constants.TableUser, "user-kevin").
		Return(models.SObject{constants.FieldID: "user-kevin", "Email": "kevin@test.com"}, nil).Once()
	mockP.On("GetRaw", mock.Anything, constants.TableUser, "user-stephan").
		Return(models.SObject{constants.FieldID: "user-stephan", "Email": "stephan@test.com"}, nil).Once()

	result, err := svc.Submit(ctx, SubmitRequest{
		ObjectAPIName: "Opportunity",
		RecordID:      "opp-1",
		Comments:      "Please review",
	}, user)

	require.NoError(t, err)
	assert.Equal(t, []string{"user-kevin", "user-stephan"}, result["ActorIds"])
	assert.ElementsMatch(t, []string{"kevin@test.com", "stephan@test.com"}, sentEmailRecipients(t, mockEmail))
	assert.ElementsMatch(t, []string{"Approval Submitted", "Approval Submitted"}, sentEmailSubjects(t, mockEmail))
	mockQ.AssertExpectations(t)
	mockP.AssertExpectations(t)
	mockEmail.AssertExpectations(t)
}

func TestApprove_StepApprovalEmailAlertSendsToNextApprover(t *testing.T) {
	mockQ := new(MockApprovalQuery)
	mockP := new(MockApprovalPersistence)
	mockEmail := new(MockEmailSender)
	mockEmail.On("Send", mock.Anything, mock.Anything).Return(nil).Once()
	svc := newApprovalServiceWithEmailMocks(mockQ, mockP, mockEmail)

	ctx := context.Background()
	user := &models.UserSession{ID: "user-1"}
	processDef := models.SObject{
		constants.FieldID: "pd-1",
		"TableEnumOrId":   "Opportunity",
	}
	workItem := models.SObject{constants.FieldID: "wi-1", "ProcessInstanceId": "inst-1", "ActorId": "user-1"}
	instance := models.SObject{constants.FieldID: "inst-1", "ProcessDefinitionId": "pd-1", "TargetObjectId": "opp-1"}
	nodeInstance := models.SObject{constants.FieldID: "pin-1", "ProcessNodeId": "node-1", "ProcessNodeName": "Manager Review"}
	currentNode := models.SObject{constants.FieldID: "node-1", "DeveloperName": "Manager_Review", "Name": "Manager Review", "StepOrder": 1}
	nextNode := models.SObject{constants.FieldID: "node-2", "DeveloperName": "Finance_Review", "Name": "Finance Review", "StepOrder": 2, "ApproverType": "User", "ApproverUserId": "user-next"}

	mockQ.On("QueryWithFilter", mock.Anything, constants.TableProcessInstanceWorkitem, mock.Anything, user, "", "", 1).
		Return(&models.QueryResult{TotalSize: 1, Records: []models.SObject{workItem}}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, constants.TableProcessInstance, mock.Anything, user, "", "", 1).
		Return(&models.QueryResult{TotalSize: 1, Records: []models.SObject{instance}}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, constants.TableProcessDefinition, mock.Anything, user, "", "", 1).
		Return(&models.QueryResult{TotalSize: 1, Records: []models.SObject{processDef}}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, constants.TableProcessInstanceNode, mock.Anything, user, "", "", 1).
		Return(&models.QueryResult{TotalSize: 1, Records: []models.SObject{nodeInstance}}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, constants.TableProcessNode, "Id == 'node-1'", user, "", "", 1).
		Return(&models.QueryResult{TotalSize: 1, Records: []models.SObject{currentNode}}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, constants.TableProcessInstanceWorkitem, mock.Anything, user, "", "", 100).
		Return(&models.QueryResult{TotalSize: 1, Records: []models.SObject{workItem}}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, "ProcessNode", mock.Anything, user, "StepOrder", "ASC", 100).
		Return(&models.QueryResult{TotalSize: 2, Records: []models.SObject{currentNode, nextNode}}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, "User", mock.MatchedBy(func(filter string) bool {
		return strings.Contains(filter, "Id == 'user-next'")
	}), user, "", "", 1).
		Return(&models.QueryResult{TotalSize: 1, Records: []models.SObject{{constants.FieldID: "user-next"}}}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, "ProcessAction", mock.MatchedBy(func(filter string) bool {
		return strings.Contains(filter, "ProcessDefinitionId == 'pd-1'") && strings.Contains(filter, "ActionEvent == 'StepApproval'")
	}), user, "Order", "ASC", 100).
		Return(&models.QueryResult{}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, "ProcessAction", mock.MatchedBy(func(filter string) bool {
		return strings.Contains(filter, "ProcessDefinitionId == 'pd-1'") && strings.Contains(filter, "ActionEvent == 'StepApproval'")
	}), user, "Order", "ASC", 100).
		Return(&models.QueryResult{TotalSize: 1, Records: []models.SObject{{
			constants.FieldID: "act-step-email",
			"Name":            "Step Approval Notification",
			"ActionType":      "Alert",
			"ActionEvent":     "StepApproval",
			"EmailTemplateId": "tmpl-step",
		}}}, nil).Once()

	mockP.On("Delete", mock.MatchedBy(isElevatedSecurityContext), constants.TableProcessInstanceWorkitem, "wi-1", user).Return(nil).Once()
	mockP.On("Update", mock.MatchedBy(isElevatedSecurityContext), constants.TableProcessInstanceNode, "pin-1", mock.MatchedBy(func(data models.SObject) bool {
		return data["NodeStatus"] == constants.ApprovalStatusApproved && data["LastActorId"] == "user-1"
	}), user).Return(nil).Once()
	mockP.On("Insert", mock.MatchedBy(isElevatedSecurityContext), constants.TableProcessInstanceNode, mock.MatchedBy(func(data models.SObject) bool {
		return data["ProcessNodeId"] == "node-2" && data["ProcessNodeName"] == "Finance Review"
	}), user).Return(models.SObject{constants.FieldID: "pin-2"}, nil).Once()
	mockP.On("Insert", mock.MatchedBy(isElevatedSecurityContext), constants.TableProcessInstanceWorkitem, mock.MatchedBy(func(data models.SObject) bool {
		return data["ProcessInstanceId"] == "inst-1" && data["ActorId"] == "user-next"
	}), user).Return(models.SObject{constants.FieldID: "wi-2"}, nil).Once()
	mockP.On("GetRaw", mock.Anything, "Opportunity", "opp-1").
		Return(models.SObject{constants.FieldID: "opp-1", "Name": "Enterprise Deal"}, nil).Once()
	mockP.On("GetRaw", mock.Anything, constants.TableEmailTemplate, "tmpl-step").
		Return(models.SObject{
			constants.FieldID:                        "tmpl-step",
			constants.FieldSysEmailTemplate_Subject:  "Next Approval Step",
			constants.FieldSysEmailTemplate_Body:     "Please review {{Name}}",
			constants.FieldSysEmailTemplate_IsActive: int64(1),
		}, nil).Once()
	mockP.On("GetRaw", mock.Anything, constants.TableUser, "user-next").
		Return(models.SObject{constants.FieldID: "user-next", "Email": "next@test.com"}, nil).Once()

	result, err := svc.Approve(ctx, "wi-1", "Looks good", user)

	require.NoError(t, err)
	assert.Equal(t, constants.ApprovalStatusPending, result["InstanceStatus"])
	assert.Equal(t, []string{"user-next"}, result["ActorIds"])
	assert.Equal(t, []string{"next@test.com"}, sentEmailRecipients(t, mockEmail))
	assert.Equal(t, []string{"Next Approval Step"}, sentEmailSubjects(t, mockEmail))
	mockQ.AssertExpectations(t)
	mockP.AssertExpectations(t)
	mockEmail.AssertExpectations(t)
}

func TestRecall_EmailAlertSendsToPendingApproversBeforeDeletion(t *testing.T) {
	mockQ := new(MockApprovalQuery)
	mockP := new(MockApprovalPersistence)
	mockEmail := new(MockEmailSender)
	mockEmail.On("Send", mock.Anything, mock.Anything).Return(nil).Twice()
	svc := newApprovalServiceWithEmailMocks(mockQ, mockP, mockEmail)

	ctx := context.Background()
	user := &models.UserSession{ID: "submitter-1"}
	workItem := models.SObject{constants.FieldID: "wi-1", "ProcessInstanceId": "inst-1", "ActorId": "approver-1"}
	instance := models.SObject{
		constants.FieldID:     "inst-1",
		"ProcessDefinitionId": "pd-1",
		"TargetObjectId":      "opp-1",
		"Status":              constants.ApprovalStatusPending,
		"SubmittedById":       "submitter-1",
	}
	nodeInstance := models.SObject{constants.FieldID: "pin-1", "NodeStatus": constants.ApprovalStatusPending}
	processDef := models.SObject{constants.FieldID: "pd-1", "TableEnumOrId": "Opportunity"}

	mockQ.On("QueryWithFilter", mock.Anything, constants.TableProcessInstanceWorkitem, mock.Anything, user, "", "", 1).
		Return(&models.QueryResult{TotalSize: 1, Records: []models.SObject{workItem}}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, constants.TableProcessInstance, mock.Anything, user, "", "", 1).
		Return(&models.QueryResult{TotalSize: 1, Records: []models.SObject{instance}}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, constants.TableProcessInstanceNode, mock.Anything, user, "", "", 1).
		Return(&models.QueryResult{TotalSize: 1, Records: []models.SObject{nodeInstance}}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, constants.TableProcessInstanceWorkitem, mock.Anything, user, "", "", 100).
		Return(&models.QueryResult{TotalSize: 2, Records: []models.SObject{
			{constants.FieldID: "wi-1", "ActorId": "approver-1"},
			{constants.FieldID: "wi-2", "ActorId": "approver-2"},
		}}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, constants.TableProcessDefinition, mock.Anything, user, "", "", 1).
		Return(&models.QueryResult{TotalSize: 1, Records: []models.SObject{processDef}}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, "ProcessAction", mock.MatchedBy(func(filter string) bool {
		return strings.Contains(filter, "ProcessDefinitionId == 'pd-1'") && strings.Contains(filter, "ActionEvent == 'Recall'")
	}), user, "Order", "ASC", 100).
		Return(&models.QueryResult{TotalSize: 1, Records: []models.SObject{{
			constants.FieldID: "act-recall-email",
			"Name":            "Recall Notification",
			"ActionType":      "Alert",
			"ActionEvent":     "Recall",
			"EmailTemplateId": "tmpl-recall",
		}}}, nil).Once()

	mockP.On("Update", mock.MatchedBy(isElevatedSecurityContext), constants.TableProcessInstance, "inst-1", mock.MatchedBy(func(data models.SObject) bool {
		return data["Status"] == constants.ApprovalStatusRemoved
	}), user).Return(nil).Once()
	mockP.On("Update", mock.MatchedBy(isElevatedSecurityContext), constants.TableProcessInstanceNode, "pin-1", mock.MatchedBy(func(data models.SObject) bool {
		return data["NodeStatus"] == constants.ApprovalStatusRemoved
	}), user).Return(nil).Once()
	mockP.On("Delete", mock.MatchedBy(isElevatedSecurityContext), constants.TableProcessInstanceWorkitem, "wi-1", user).Return(nil).Once()
	mockP.On("Delete", mock.MatchedBy(isElevatedSecurityContext), constants.TableProcessInstanceWorkitem, "wi-2", user).Return(nil).Once()
	mockP.On("GetRaw", mock.Anything, "Opportunity", "opp-1").
		Return(models.SObject{constants.FieldID: "opp-1", "Name": "Enterprise Deal"}, nil).Once()
	mockP.On("GetRaw", mock.Anything, constants.TableEmailTemplate, "tmpl-recall").
		Return(models.SObject{
			constants.FieldID:                        "tmpl-recall",
			constants.FieldSysEmailTemplate_Subject:  "Approval Recalled",
			constants.FieldSysEmailTemplate_Body:     "This approval has been recalled",
			constants.FieldSysEmailTemplate_IsActive: int64(1),
		}, nil).Twice()
	mockP.On("GetRaw", mock.Anything, constants.TableUser, "approver-1").
		Return(models.SObject{constants.FieldID: "approver-1", "Email": "approver1@test.com"}, nil).Once()
	mockP.On("GetRaw", mock.Anything, constants.TableUser, "approver-2").
		Return(models.SObject{constants.FieldID: "approver-2", "Email": "approver2@test.com"}, nil).Once()

	result, err := svc.Recall(ctx, "wi-1", "Pulling this back", user)

	require.NoError(t, err)
	assert.Equal(t, "opp-1", result["TargetObjectId"])
	assert.ElementsMatch(t, []string{"approver1@test.com", "approver2@test.com"}, sentEmailRecipients(t, mockEmail))
	assert.ElementsMatch(t, []string{"Approval Recalled", "Approval Recalled"}, sentEmailSubjects(t, mockEmail))
	mockQ.AssertExpectations(t)
	mockP.AssertExpectations(t)
	mockEmail.AssertExpectations(t)
}

func TestReassign_EmailAlertSendsToNewApprover(t *testing.T) {
	mockQ := new(MockApprovalQuery)
	mockP := new(MockApprovalPersistence)
	mockEmail := new(MockEmailSender)
	mockEmail.On("Send", mock.Anything, mock.Anything).Return(nil).Once()
	svc := newApprovalServiceWithEmailMocks(mockQ, mockP, mockEmail)

	ctx := context.Background()
	user := &models.UserSession{ID: "user-1"}
	workItem := models.SObject{constants.FieldID: "wi-1", "ProcessInstanceId": "inst-1", "ActorId": "user-1"}
	instance := models.SObject{constants.FieldID: "inst-1", "ProcessDefinitionId": "pd-1", "TargetObjectId": "opp-1"}
	nodeInstance := models.SObject{constants.FieldID: "node-inst-1", "ProcessNodeId": "node-1", "ProcessNodeName": "Manager Approval", "NodeStatus": constants.ApprovalStatusPending}
	processDef := models.SObject{
		constants.FieldID: "pd-1",
		"TableEnumOrId": "Opportunity",
		"Metadata": `{
			"approvalStep": {
				"name": "Manager_Approval",
				"label": "Manager Approval",
				"allowDelegate": true,
				"assignedApprover": {
					"approver": [
						{ "name": "user-1", "type": "user" }
					]
				}
			}
		}`,
	}

	mockQ.On("QueryWithFilter", mock.Anything, constants.TableProcessInstanceWorkitem, mock.Anything, user, "", "", 1).
		Return(&models.QueryResult{TotalSize: 1, Records: []models.SObject{workItem}}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, constants.TableProcessInstanceNode, "ProcessInstanceId == 'inst-1' && NodeStatus == 'Pending'", user, "", "", 1).
		Return(&models.QueryResult{TotalSize: 1, Records: []models.SObject{nodeInstance}}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, constants.TableProcessInstance, mock.Anything, user, "", "", 1).
		Return(&models.QueryResult{TotalSize: 1, Records: []models.SObject{instance}}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, constants.TableProcessDefinition, mock.Anything, user, "", "", 1).
		Return(&models.QueryResult{TotalSize: 1, Records: []models.SObject{processDef}}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, constants.TableProcessInstance, mock.Anything, user, "", "", 1). // Second call for executeProcessActions
		Return(&models.QueryResult{TotalSize: 1, Records: []models.SObject{instance}}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, constants.TableProcessDefinition, mock.Anything, user, "", "", 1). // Second call for executeProcessActions
		Return(&models.QueryResult{TotalSize: 1, Records: []models.SObject{processDef}}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, "ProcessAction", mock.MatchedBy(func(filter string) bool {
		return strings.Contains(filter, "ProcessDefinitionId == 'pd-1'") && strings.Contains(filter, "ActionEvent == 'Reassign'")
	}), user, "Order", "ASC", 100).
		Return(&models.QueryResult{TotalSize: 1, Records: []models.SObject{{
			constants.FieldID: "act-reassign-email",
			"Name":            "Reassign Notification",
			"ActionType":      "Alert",
			"ActionEvent":     "Reassign",
			"EmailTemplateId": "tmpl-reassign",
		}}}, nil).Once()

	mockP.On("Update", mock.MatchedBy(isElevatedSecurityContext), constants.TableProcessInstanceWorkitem, "wi-1", mock.MatchedBy(func(data models.SObject) bool {
		return data["ActorId"] == "user-new"
	}), user).Return(nil).Once()
	mockP.On("GetRaw", mock.Anything, "Opportunity", "opp-1").
		Return(models.SObject{constants.FieldID: "opp-1", "Name": "Enterprise Deal"}, nil).Once()
	mockP.On("GetRaw", mock.Anything, constants.TableEmailTemplate, "tmpl-reassign").
		Return(models.SObject{
			constants.FieldID:                        "tmpl-reassign",
			constants.FieldSysEmailTemplate_Subject:  "Approval Reassigned",
			constants.FieldSysEmailTemplate_Body:     "You have been assigned an approval",
			constants.FieldSysEmailTemplate_IsActive: int64(1),
		}, nil).Once()
	mockP.On("GetRaw", mock.Anything, constants.TableUser, "user-new").
		Return(models.SObject{constants.FieldID: "user-new", "Email": "new-approver@test.com"}, nil).Once()

	err := svc.Reassign(ctx, "wi-1", "user-new", user)

	require.NoError(t, err)
	assert.Equal(t, []string{"new-approver@test.com"}, sentEmailRecipients(t, mockEmail))
	assert.Equal(t, []string{"Approval Reassigned"}, sentEmailSubjects(t, mockEmail))
	mockQ.AssertExpectations(t)
	mockP.AssertExpectations(t)
	mockEmail.AssertExpectations(t)
}

func TestReassign_EmailAlertWithoutTemplateIDDoesNotFailParentOperation(t *testing.T) {
	mockQ := new(MockApprovalQuery)
	mockP := new(MockApprovalPersistence)
	mockEmail := new(MockEmailSender)
	svc := newApprovalServiceWithEmailMocks(mockQ, mockP, mockEmail)

	ctx := context.Background()
	user := &models.UserSession{ID: "user-1"}
	workItem := models.SObject{constants.FieldID: "wi-1", "ProcessInstanceId": "inst-1", "ActorId": "user-1"}
	instance := models.SObject{constants.FieldID: "inst-1", "ProcessDefinitionId": "pd-1", "TargetObjectId": "opp-1"}
	nodeInstance := models.SObject{constants.FieldID: "node-inst-1", "ProcessNodeId": "node-1", "ProcessNodeName": "Manager Approval", "NodeStatus": constants.ApprovalStatusPending}
	processDef := models.SObject{
		constants.FieldID: "pd-1",
		"TableEnumOrId": "Opportunity",
		"Metadata": `{
			"approvalStep": {
				"name": "Manager_Approval",
				"label": "Manager Approval",
				"allowDelegate": true,
				"assignedApprover": {
					"approver": [
						{ "name": "user-1", "type": "user" }
					]
				}
			}
		}`,
	}

	mockQ.On("QueryWithFilter", mock.Anything, constants.TableProcessInstanceWorkitem, mock.Anything, user, "", "", 1).
		Return(&models.QueryResult{TotalSize: 1, Records: []models.SObject{workItem}}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, constants.TableProcessInstanceNode, "ProcessInstanceId == 'inst-1' && NodeStatus == 'Pending'", user, "", "", 1).
		Return(&models.QueryResult{TotalSize: 1, Records: []models.SObject{nodeInstance}}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, constants.TableProcessInstance, mock.Anything, user, "", "", 1).
		Return(&models.QueryResult{TotalSize: 1, Records: []models.SObject{instance}}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, constants.TableProcessDefinition, mock.Anything, user, "", "", 1).
		Return(&models.QueryResult{TotalSize: 1, Records: []models.SObject{processDef}}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, constants.TableProcessInstance, mock.Anything, user, "", "", 1). // Second call for executeProcessActions
		Return(&models.QueryResult{TotalSize: 1, Records: []models.SObject{instance}}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, constants.TableProcessDefinition, mock.Anything, user, "", "", 1). // Second call for executeProcessActions
		Return(&models.QueryResult{TotalSize: 1, Records: []models.SObject{processDef}}, nil).Once()
	mockQ.On("QueryWithFilter", mock.Anything, "ProcessAction", mock.MatchedBy(func(filter string) bool {
		return strings.Contains(filter, "ProcessDefinitionId == 'pd-1'") && strings.Contains(filter, "ActionEvent == 'Reassign'")
	}), user, "Order", "ASC", 100).
		Return(&models.QueryResult{TotalSize: 1, Records: []models.SObject{{
			constants.FieldID: "act-reassign-email",
			"Name":            "Reassign Notification",
			"ActionType":      "Alert",
			"ActionEvent":     "Reassign",
		}}}, nil).Once()

	mockP.On("Update", mock.MatchedBy(isElevatedSecurityContext), constants.TableProcessInstanceWorkitem, "wi-1", mock.MatchedBy(func(data models.SObject) bool {
		return data["ActorId"] == "user-new"
	}), user).Return(nil).Once()
	mockP.On("GetRaw", mock.Anything, "Opportunity", "opp-1").
		Return(models.SObject{constants.FieldID: "opp-1", "Name": "Enterprise Deal"}, nil).Once()

	err := svc.Reassign(ctx, "wi-1", "user-new", user)

	require.NoError(t, err)
	assert.Empty(t, mockEmail.Calls)
	mockQ.AssertExpectations(t)
	mockP.AssertExpectations(t)
}
