package automation

import (
	"bytes"
	"context"
	"errors"
	"reflect"
	"strings"
	"testing"
	"time"

	"github.com/nexuscrm/kernel/pkg/ports"
	infraemail "github.com/nexuscrm/outbound/email"
	"github.com/nexuscrm/kernel/pkg/formula"
	"github.com/nexuscrm/kernel/pkg/logging"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/rs/zerolog"
)

func TestFlowSendEmailCanonicalSendsTemplateWithCcVariablesAndOutputs(t *testing.T) {
	persistence := &emailTemplatePersistenceStub{
		records: map[string]models.SObject{
			"tmpl-1": {
				constants.FieldSysEmailTemplate_IsActive:  true,
				constants.FieldSysEmailTemplate_Subject:   "Review {{Variables.ApprovalUrl}} for {{Record.Name}}",
				constants.FieldSysEmailTemplate_HTMLValue: "<p>{{Record.Name}}</p>",
				constants.FieldSysEmailTemplate_Body:      "Plain {{User.Name}}",
			},
		},
	}
	sender := &recordingEmailSender{
		result: &ports.EmailSendResult{
			SentCount: 3,
			RecipientResults: []ports.EmailRecipientResult{
				{Address: "primary@example.com", Role: "to", Status: "accepted"},
				{Address: "secondary@example.com", Role: "to", Status: "accepted"},
				{Address: "reviewer@example.com", Role: "cc", Status: "accepted"},
			},
			MessageID: "msg-1",
		},
	}
	actionService := &ActionService{
		persistence: persistence,
		emailSender: sender,
		logger:      logging.New(context.Background()),
	}
	executor := newFlowExecutorWithPlatformActionCalls(nil, actionService, nil, nil, nil, nil, logging.New(context.Background()))
	interview := &models.FlowInterview{
		Id: "interview-1",
		ContextData: map[string]interface{}{
			"$Record":     models.SObject{"Name": "Quarterly Plan"},
			"ApprovalUrl": "https://nexuscrm.example/approval/1",
			"extraTo":     []interface{}{"secondary@example.com"},
			"ccList":      []interface{}{"reviewer@example.com"},
		},
	}
	flowMeta := &models.FlowMetadata{Start: &models.FlowStart{Object: "Opportunity"}}
	action := &models.FlowActionCall{
		Name:       "send_review_email",
		ActionName: "send_review_email",
		ActionType: "sendEmail",
		InputParameters: []models.FlowActionParameter{
			{Name: "templateId", Value: "tmpl-1"},
			{Name: constants.ConfigTo, Value: []interface{}{
				"primary@example.com",
				map[string]interface{}{"elementReference": "extraTo"},
			}},
			{Name: constants.ConfigCc, Value: map[string]interface{}{"elementReference": "ccList"}},
			{Name: constants.ConfigVariables, Value: map[string]interface{}{
				"ApprovalUrl": map[string]interface{}{"elementReference": "ApprovalUrl"},
			}},
		},
		OutputParameters: []models.FlowActionParameter{
			{Name: "sentCount", Value: map[string]interface{}{"elementReference": "emailSentCount"}},
			{Name: "recipientResults", Value: "emailRecipientResults"},
			{Name: "messageId", Value: "emailMessageId"},
		},
		Connector: &models.FlowConnector{TargetReference: "done"},
	}

	next, err := executor.executeActionCall(context.Background(), interview, flowMeta, action, &models.UserSession{ID: "005-user", Name: "Flow User"})
	if err != nil {
		t.Fatalf("executeActionCall(sendEmail): %v", err)
	}
	if next != "done" {
		t.Fatalf("next = %q; want done", next)
	}
	if len(sender.messages) != 1 {
		t.Fatalf("email sender calls = %d; want 1", len(sender.messages))
	}
	msg := sender.messages[0]
	if !reflect.DeepEqual(msg.To, []string{"primary@example.com", "secondary@example.com"}) {
		t.Fatalf("To = %#v", msg.To)
	}
	if !reflect.DeepEqual(msg.Cc, []string{"reviewer@example.com"}) {
		t.Fatalf("Cc = %#v", msg.Cc)
	}
	if msg.From != "" {
		t.Fatalf("From = %q; want empty so provider default sender is used", msg.From)
	}
	if msg.Subject != "Review https://nexuscrm.example/approval/1 for Quarterly Plan" {
		t.Fatalf("Subject = %q", msg.Subject)
	}
	if msg.HTML != "<p>Quarterly Plan</p>" {
		t.Fatalf("HTML = %q", msg.HTML)
	}
	if msg.Body != "Plain Flow User" {
		t.Fatalf("Body = %q", msg.Body)
	}
	if got := interview.ContextData["emailSentCount"]; got != 3 {
		t.Fatalf("emailSentCount = %#v; want 3", got)
	}
	if got := interview.ContextData["emailMessageId"]; got != "msg-1" {
		t.Fatalf("emailMessageId = %#v; want msg-1", got)
	}
	recipients, ok := interview.ContextData["emailRecipientResults"].([]interface{})
	if !ok {
		t.Fatalf("emailRecipientResults = %T %#v; want []interface{}", interview.ContextData["emailRecipientResults"], interview.ContextData["emailRecipientResults"])
	}
	if len(recipients) != 3 {
		t.Fatalf("emailRecipientResults len = %d; want 3", len(recipients))
	}
	first, ok := recipients[0].(map[string]interface{})
	if !ok {
		t.Fatalf("emailRecipientResults[0] = %T %#v; want map", recipients[0], recipients[0])
	}
	if first["address"] != "primary@example.com" || first["role"] != "to" || first["status"] != "accepted" {
		t.Fatalf("emailRecipientResults[0] = %#v", first)
	}
	assertNoGetRawCall(t, persistence, constants.TableWorkflowAlert)
	assertGetRawCalls(t, persistence, []emailTemplateGetRawCall{{objectName: constants.TableEmailTemplate, id: "tmpl-1"}})
}

func TestFlowSendEmailCanonicalAcceptsDefaultSenderPolicy(t *testing.T) {
	persistence := &emailTemplatePersistenceStub{
		records: map[string]models.SObject{
			"tmpl-1": {
				constants.FieldSysEmailTemplate_IsActive: true,
				constants.FieldSysEmailTemplate_Subject:  "Hello",
			},
		},
	}
	sender := &recordingEmailSender{}
	actionService := &ActionService{
		persistence: persistence,
		emailSender: sender,
		logger:      logging.New(context.Background()),
	}
	executor := newFlowExecutorWithPlatformActionCalls(nil, actionService, nil, nil, nil, nil, logging.New(context.Background()))
	action := &models.FlowActionCall{
		Name:       "send_review_email",
		ActionName: "send_review_email",
		ActionType: "sendEmail",
		InputParameters: []models.FlowActionParameter{
			{Name: "templateId", Value: "tmpl-1"},
			{Name: constants.ConfigTo, Value: "person@example.com"},
			{Name: "from", Value: map[string]interface{}{"type": "default"}},
		},
	}

	_, err := executor.executeActionCall(context.Background(), &models.FlowInterview{ContextData: map[string]interface{}{}}, &models.FlowMetadata{}, action, &models.UserSession{ID: "005-user", Name: "Sender User"})
	if err != nil {
		t.Fatalf("executeActionCall(sendEmail): %v", err)
	}
	if len(sender.messages) != 1 {
		t.Fatalf("email sender calls = %d; want 1", len(sender.messages))
	}
	if got := sender.messages[0].From; got != "" {
		t.Fatalf("From = %q; want empty so provider default sender is used", got)
	}
	assertGetRawCalls(t, persistence, []emailTemplateGetRawCall{{objectName: constants.TableEmailTemplate, id: "tmpl-1"}})
	assertNoGetRawCall(t, persistence, constants.TableWorkflowAlert)
}

func TestFlowSendEmailCanonicalRejectsCurrentUserSenderBeforeRender(t *testing.T) {
	persistence := &emailTemplatePersistenceStub{
		records: map[string]models.SObject{
			"tmpl-1": {
				constants.FieldSysEmailTemplate_IsActive: true,
				constants.FieldSysEmailTemplate_Subject:  "Hello",
			},
		},
	}
	sender := &recordingEmailSender{}
	actionService := &ActionService{
		persistence: persistence,
		emailSender: sender,
		logger:      logging.New(context.Background()),
	}
	executor := newFlowExecutorWithPlatformActionCalls(nil, actionService, nil, nil, nil, nil, logging.New(context.Background()))
	action := &models.FlowActionCall{
		Name:       "send_review_email",
		ActionName: "send_review_email",
		ActionType: "sendEmail",
		InputParameters: []models.FlowActionParameter{
			{Name: "templateId", Value: "tmpl-1"},
			{Name: constants.ConfigTo, Value: "person@example.com"},
			{Name: "from", Value: map[string]interface{}{"type": "currentUser"}},
		},
	}
	userEmail := "sender@example.com"

	_, err := executor.executeActionCall(context.Background(), &models.FlowInterview{ContextData: map[string]interface{}{}}, &models.FlowMetadata{}, action, &models.UserSession{ID: "005-user", Name: "Sender User", Email: &userEmail})
	if err == nil {
		t.Fatal("executeActionCall(sendEmail) succeeded; want unsupported currentUser sender validation error")
	}
	if !strings.Contains(err.Error(), "unsupported sendEmail sender policy \"currentUser\"") ||
		!strings.Contains(err.Error(), "generic sender-identity source of truth") {
		t.Fatalf("error = %q; want unsupported currentUser sender-policy validation", err.Error())
	}
	if len(sender.messages) != 0 {
		t.Fatalf("email sender calls = %d; want 0", len(sender.messages))
	}
	assertNoGetRawCall(t, persistence, constants.TableEmailTemplate)
	assertNoGetRawCall(t, persistence, constants.TableWorkflowAlert)
}

func TestFlowSendEmailCanonicalRejectsExplicitSenderAddress(t *testing.T) {
	persistence := &emailTemplatePersistenceStub{
		records: map[string]models.SObject{
			"tmpl-1": {
				constants.FieldSysEmailTemplate_IsActive: true,
				constants.FieldSysEmailTemplate_Subject:  "Hello",
			},
		},
	}
	sender := &recordingEmailSender{}
	actionService := &ActionService{
		persistence: persistence,
		emailSender: sender,
		logger:      logging.New(context.Background()),
	}
	executor := newFlowExecutorWithPlatformActionCalls(nil, actionService, nil, nil, nil, nil, logging.New(context.Background()))
	action := &models.FlowActionCall{
		Name:       "send_review_email",
		ActionName: "send_review_email",
		ActionType: "sendEmail",
		InputParameters: []models.FlowActionParameter{
			{Name: "templateId", Value: "tmpl-1"},
			{Name: constants.ConfigTo, Value: "person@example.com"},
			{Name: "from", Value: "alerts@example.com"},
		},
	}

	_, err := executor.executeActionCall(context.Background(), &models.FlowInterview{ContextData: map[string]interface{}{}}, &models.FlowMetadata{}, action, &models.UserSession{ID: "005-user"})
	if err == nil {
		t.Fatal("executeActionCall(sendEmail) succeeded; want explicit sender validation error")
	}
	if !strings.Contains(err.Error(), "explicit sender addresses are not supported by sendEmail") {
		t.Fatalf("error = %q; want explicit sender address validation", err.Error())
	}
	if len(sender.messages) != 0 {
		t.Fatalf("email sender calls = %d; want 0", len(sender.messages))
	}
	assertNoGetRawCall(t, persistence, constants.TableEmailTemplate)
	assertNoGetRawCall(t, persistence, constants.TableWorkflowAlert)
}

func TestFlowEmailAlertFailsClosedAfterWorkflowAlertCutover(t *testing.T) {
	sender := &recordingEmailSender{}
	actionService := &ActionService{
		emailSender: sender,
		logger:      logging.New(context.Background()),
	}
	executor := newFlowExecutorWithPlatformActionCalls(nil, actionService, nil, nil, nil, nil, logging.New(context.Background()))
	interview := &models.FlowInterview{
		Id:          "interview-legacy-alert",
		ContextData: map[string]interface{}{"$Record": models.SObject{"Id": "001-legacy"}},
	}
	flowMeta := &models.FlowMetadata{Start: &models.FlowStart{Object: "Account"}}
	action := &models.FlowActionCall{
		Name:       "Legacy_EmailAlert",
		ActionName: "Account.Legacy_Alert",
		ActionType: "emailAlert",
		Connector:  &models.FlowConnector{TargetReference: "must-not-continue"},
	}

	next, err := executor.executeActionCall(context.Background(), interview, flowMeta, action, &models.UserSession{ID: "005-user"})
	if err == nil {
		t.Fatal("executeActionCall(emailAlert) succeeded; want cutover failure")
	}
	if next != "" {
		t.Fatalf("next = %q, want empty when emailAlert fails closed", next)
	}
	for _, want := range []string{"WorkflowAlert runtime dispatch is disabled", "Flow sendEmail", "remove it"} {
		if !strings.Contains(err.Error(), want) {
			t.Fatalf("emailAlert error = %q, want mention %q", err.Error(), want)
		}
	}
	if len(sender.messages) != 0 {
		t.Fatalf("email sender calls = %d, want 0 for legacy emailAlert", len(sender.messages))
	}
}

func TestFlowSendEmailCanonicalFailsClosedForMissingRecipientReferenceInCollection(t *testing.T) {
	persistence := &emailTemplatePersistenceStub{
		records: map[string]models.SObject{
			"tmpl-1": {
				constants.FieldSysEmailTemplate_IsActive: true,
				constants.FieldSysEmailTemplate_Subject:  "Hello",
			},
		},
	}
	sender := &recordingEmailSender{}
	actionService := &ActionService{
		persistence: persistence,
		emailSender: sender,
		logger:      logging.New(context.Background()),
	}
	executor := newFlowExecutorWithPlatformActionCalls(nil, actionService, nil, nil, nil, nil, logging.New(context.Background()))
	action := &models.FlowActionCall{
		Name:       "send_review_email",
		ActionName: "send_review_email",
		ActionType: "sendEmail",
		InputParameters: []models.FlowActionParameter{
			{Name: "templateId", Value: "tmpl-1"},
			{Name: constants.ConfigTo, Value: []interface{}{
				"primary@example.com",
				map[string]interface{}{"elementReference": "MissingRecipients"},
			}},
		},
	}

	_, err := executor.executeActionCall(context.Background(), &models.FlowInterview{ContextData: map[string]interface{}{}}, &models.FlowMetadata{}, action, &models.UserSession{ID: "005-user"})
	if err == nil {
		t.Fatal("executeActionCall(sendEmail) succeeded; want unresolved recipient reference error")
	}
	if !strings.Contains(err.Error(), "to recipient reference MissingRecipients is unresolved") {
		t.Fatalf("error = %q; want unresolved recipient reference", err.Error())
	}
	if len(sender.messages) != 0 {
		t.Fatalf("email sender calls = %d; want 0", len(sender.messages))
	}
	assertNoGetRawCall(t, persistence, constants.TableEmailTemplate)
	assertNoGetRawCall(t, persistence, constants.TableWorkflowAlert)
}

func TestFlowSendEmailCanonicalUnwrapsReferencedTypedValueTemplateAndVariables(t *testing.T) {
	persistence := &emailTemplatePersistenceStub{
		records: map[string]models.SObject{
			"tmpl-typed": {
				constants.FieldSysEmailTemplate_IsActive: true,
				constants.FieldSysEmailTemplate_Subject:  "Typed {{Variables.ApprovalText}} {{Variables.RetryCount}}",
			},
		},
	}
	sender := &recordingEmailSender{}
	actionService := &ActionService{
		persistence: persistence,
		emailSender: sender,
		logger:      logging.New(context.Background()),
	}
	executor := newFlowExecutorWithPlatformActionCalls(nil, actionService, nil, nil, nil, nil, logging.New(context.Background()))
	interview := &models.FlowInterview{
		Id: "interview-1",
		ContextData: map[string]interface{}{
			"SelectedTemplateId": map[string]interface{}{"stringValue": "tmpl-typed"},
			"ApprovalText":       map[string]interface{}{"stringValue": "Ready"},
			"RetryCount":         map[string]interface{}{"numberValue": 2},
		},
	}
	action := &models.FlowActionCall{
		Name:       "send_review_email",
		ActionName: "send_review_email",
		ActionType: "sendEmail",
		InputParameters: []models.FlowActionParameter{
			{Name: "templateId", Value: map[string]interface{}{"elementReference": "SelectedTemplateId"}},
			{Name: constants.ConfigTo, Value: "person@example.com"},
			{Name: constants.ConfigVariables, Value: map[string]interface{}{
				"ApprovalText": map[string]interface{}{"elementReference": "ApprovalText"},
				"RetryCount":   map[string]interface{}{"elementReference": "RetryCount"},
			}},
		},
	}

	_, err := executor.executeActionCall(context.Background(), interview, &models.FlowMetadata{}, action, &models.UserSession{ID: "005-user"})
	if err != nil {
		t.Fatalf("executeActionCall(sendEmail): %v", err)
	}
	if len(sender.messages) != 1 {
		t.Fatalf("email sender calls = %d; want 1", len(sender.messages))
	}
	if got := sender.messages[0].Subject; got != "Typed Ready 2" {
		t.Fatalf("Subject = %q; want typed variable values unwrapped", got)
	}
	assertGetRawCalls(t, persistence, []emailTemplateGetRawCall{{objectName: constants.TableEmailTemplate, id: "tmpl-typed"}})
	assertNoGetRawCall(t, persistence, constants.TableWorkflowAlert)
}

func TestFlowSendEmailCanonicalUsesStampedRecordObjectForAutoLaunchedRecordMerge(t *testing.T) {
	persistence := &emailTemplatePersistenceStub{
		records: map[string]models.SObject{
			"tmpl-1": {
				constants.FieldSysEmailTemplate_IsActive: true,
				constants.FieldSysEmailTemplate_Subject:  "Record {{Record.Name}}",
			},
		},
	}
	sender := &recordingEmailSender{}
	actionService := &ActionService{
		persistence: persistence,
		emailSender: sender,
		permissions: allowingFieldVisibilityChecker{},
		logger:      logging.New(context.Background()),
	}
	executor := newFlowExecutorWithPlatformActionCalls(nil, actionService, nil, nil, nil, nil, logging.New(context.Background()))
	record := models.SObject{"Name": "Subflow Plan"}
	models.StampObjectType(record, "Opportunity")
	interview := &models.FlowInterview{
		Id: "interview-1",
		ContextData: map[string]interface{}{
			"$Record": record,
		},
	}
	action := &models.FlowActionCall{
		Name:       "send_review_email",
		ActionName: "send_review_email",
		ActionType: "sendEmail",
		InputParameters: []models.FlowActionParameter{
			{Name: "templateId", Value: "tmpl-1"},
			{Name: constants.ConfigTo, Value: "person@example.com"},
		},
	}

	_, err := executor.executeActionCall(context.Background(), interview, &models.FlowMetadata{}, action, &models.UserSession{ID: "005-user", ProfileID: "00e-profile"})
	if err != nil {
		t.Fatalf("executeActionCall(sendEmail): %v", err)
	}
	if len(sender.messages) != 1 {
		t.Fatalf("email sender calls = %d; want 1", len(sender.messages))
	}
	if got := sender.messages[0].Subject; got != "Record Subflow Plan" {
		t.Fatalf("subject = %q; want rendered Record.Name", got)
	}
}

func TestFlowSendEmailCanonicalLogOmitsRenderedContentAndRecipients(t *testing.T) {
	persistence := &emailTemplatePersistenceStub{
		records: map[string]models.SObject{
			"tmpl-sensitive": {
				constants.FieldSysEmailTemplate_IsActive:  true,
				constants.FieldSysEmailTemplate_Subject:   "Private {{Record.Name}}",
				constants.FieldSysEmailTemplate_HTMLValue: "<p>{{Variables.Secret}}</p>",
				constants.FieldSysEmailTemplate_Body:      "Plain {{User.Name}}",
			},
		},
	}
	var logBuf bytes.Buffer
	logger := newSendEmailActionTestLogger(&logBuf)
	actionService := &ActionService{
		persistence: persistence,
		emailSender: infraemail.NewNoopSender(logger),
		logger:      logger,
	}
	record := models.SObject{"Name": "Sensitive Plan"}

	_, err := actionService.executeCanonicalSendEmail(context.Background(), canonicalSendEmailRequest{
		TemplateID:   "tmpl-sensitive",
		To:           []string{"sensitive-recipient@example.com"},
		Cc:           []string{"sensitive-reviewer@example.com"},
		SenderPolicy: &canonicalSendEmailSenderPolicy{Type: canonicalSendEmailSenderDefault},
		Record:       record,
		User:         &models.UserSession{ID: "005-user", Name: "Sensitive User"},
		Variables:    map[string]interface{}{"Secret": "Sensitive Variable"},
	})
	if err != nil {
		t.Fatalf("executeCanonicalSendEmail: %v", err)
	}

	logged := logBuf.String()
	for _, forbidden := range []string{
		"sensitive-recipient@example.com",
		"sensitive-reviewer@example.com",
		"Private Sensitive Plan",
		"Sensitive Variable",
		"Sensitive User",
	} {
		if strings.Contains(logged, forbidden) {
			t.Fatalf("canonical sendEmail log leaked %q in %q", forbidden, logged)
		}
	}
	for _, want := range []string{"Template=tmpl-sensitive", "ToCount=1", "CcCount=1", "SenderPolicy=default", "FromOverride=false", "SentCount=2", "RecipientResultCount=2", "[NOOP EMAIL]", "RecipientCount=2"} {
		if !strings.Contains(logged, want) {
			t.Fatalf("canonical sendEmail log = %q; want non-sensitive marker %q", logged, want)
		}
	}
}

func TestFlowSendEmailCanonicalProviderFailureOmitsSenderContentFromErrorAndLogs(t *testing.T) {
	persistence := &emailTemplatePersistenceStub{
		records: map[string]models.SObject{
			"tmpl-sensitive": {
				constants.FieldSysEmailTemplate_IsActive:  true,
				constants.FieldSysEmailTemplate_Subject:   "Private {{Record.Name}}",
				constants.FieldSysEmailTemplate_HTMLValue: "<p>{{Variables.Secret}}</p>",
				constants.FieldSysEmailTemplate_Body:      "Plain {{User.Name}}",
			},
		},
	}
	var logBuf bytes.Buffer
	logger := newSendEmailActionTestLogger(&logBuf)
	sender := &failingEmailSender{err: errors.New("smtp rejected sensitive-recipient@example.com subject Private Sensitive Plan body Sensitive Variable")}
	actionService := &ActionService{
		persistence: persistence,
		emailSender: sender,
		logger:      logger,
	}

	_, err := actionService.executeCanonicalSendEmail(context.Background(), canonicalSendEmailRequest{
		TemplateID: "tmpl-sensitive",
		To:         []string{"sensitive-recipient@example.com"},
		Cc:         []string{"sensitive-reviewer@example.com"},
		Record:     models.SObject{"Name": "Sensitive Plan"},
		User:       &models.UserSession{ID: "005-user", Name: "Sensitive User"},
		Variables:  map[string]interface{}{"Secret": "Sensitive Variable"},
	})
	if err == nil {
		t.Fatal("executeCanonicalSendEmail succeeded; want provider failure")
	}
	if !strings.Contains(err.Error(), "failed to send email to 2 recipients") {
		t.Fatalf("error = %q; want sanitized recipient-count failure", err.Error())
	}
	assertSendEmailSurfacesOmitSensitiveContent(t, err.Error(), "canonical sendEmail error")
	assertSendEmailSurfacesOmitSensitiveContent(t, logBuf.String(), "canonical sendEmail logs")
	if len(sender.messages) != 1 {
		t.Fatalf("email sender calls = %d; want 1", len(sender.messages))
	}
}

func TestFlowSendEmailCanonicalProviderFailureRedactsFlowAuditLogsAndResult(t *testing.T) {
	persistence := &emailTemplatePersistenceStub{
		records: map[string]models.SObject{
			"tmpl-sensitive": {
				constants.FieldSysEmailTemplate_IsActive:  true,
				constants.FieldSysEmailTemplate_Subject:   "Private {{Record.Name}}",
				constants.FieldSysEmailTemplate_HTMLValue: "<p>{{Variables.Secret}}</p>",
				constants.FieldSysEmailTemplate_Body:      "Plain {{User.Name}}",
			},
		},
	}
	var logBuf bytes.Buffer
	logger := newSendEmailActionTestLogger(&logBuf)
	actionService := &ActionService{
		persistence: persistence,
		emailSender: &failingEmailSender{err: errors.New("smtp rejected sensitive-recipient@example.com subject Private Sensitive Plan body Sensitive Variable")},
		logger:      logger,
	}
	executor := newFlowExecutorWithPlatformActionCalls(nil, actionService, nil, nil, nil, nil, logger)
	executor.interviews = NewFlowInterviewService(nil, nil)

	store := newInMemoryStore()
	writer := NewWriter(store)
	prevWriter := GetAuditWriter()
	SetAuditWriter(writer)
	defer SetAuditWriter(prevWriter)

	version := &models.FlowVersion{
		Id:           "301-sensitive",
		DefinitionId: "300-sensitive",
		ProcessType:  "AutoLaunchedFlow",
		Status:       models.FlowStatusActive,
		Metadata: models.FlowMetadata{
			StartElementReference: "Send_Email",
			ActionCalls: []models.FlowActionCall{{
				Name:       "Send_Email",
				ActionName: "Send_Email",
				ActionType: flowSendEmailActionType,
				InputParameters: []models.FlowActionParameter{
					{Name: flowSendEmailTemplateIDParam, Value: "tmpl-sensitive"},
					{Name: constants.ConfigTo, Value: "sensitive-recipient@example.com"},
					{Name: constants.ConfigVariables, Value: map[string]interface{}{
						"Secret": map[string]interface{}{"elementReference": "Secret"},
					}},
				},
			}},
		},
	}
	result, err := executor.executeFlowVersion(WithTransientFlowInterviews(context.Background()), version, "Sensitive_SendEmail_Flow", map[string]interface{}{
		"$Record": models.SObject{"Name": "Sensitive Plan"},
		"Secret":  "Sensitive Variable",
	}, &models.UserSession{ID: "005-user", Name: "Sensitive User"})
	if err != nil {
		t.Fatalf("executeFlowVersion returned err: %v", err)
	}
	if result == nil || result.IsSuccess {
		t.Fatalf("executeFlowVersion result = %#v; want failed FlowExecutionResult", result)
	}
	if len(result.Errors) == 0 {
		t.Fatalf("executeFlowVersion result errors = %#v; want sanitized error", result)
	}
	if !strings.Contains(result.Errors[0].Message, "failed to send email to 1 recipients") {
		t.Fatalf("flow error message = %q; want sanitized recipient-count failure", result.Errors[0].Message)
	}

	shutdownAuditWriter(t, writer)
	rows := store.Rows()
	if len(rows) == 0 {
		t.Fatal("audit rows = 0; want action error row")
	}
	var actionErrorPayload string
	for _, row := range rows {
		if row.ElementId == "Send_Email" && row.Status == "error" {
			actionErrorPayload = row.ErrorPayload
			break
		}
	}
	if actionErrorPayload == "" {
		t.Fatalf("audit rows = %#v; want Send_Email error payload", rows)
	}

	assertSendEmailSurfacesOmitSensitiveContent(t, result.Errors[0].Message, "flow result error")
	assertSendEmailSurfacesOmitSensitiveContent(t, actionErrorPayload, "flow audit ErrorPayload")
	assertSendEmailSurfacesOmitSensitiveContent(t, logBuf.String(), "flow executor logs")
}

func TestFlowEmailSimpleSuccessLogOmitsRenderedContentAndRecipient(t *testing.T) {
	persistence := &emailTemplatePersistenceStub{
		records: map[string]models.SObject{
			"tmpl-sensitive": {
				constants.FieldSysEmailTemplate_IsActive:  true,
				constants.FieldSysEmailTemplate_Subject:   "Private {{Record.Name}}",
				constants.FieldSysEmailTemplate_HTMLValue: "<p>{{Variables.Secret}}</p>",
				constants.FieldSysEmailTemplate_Body:      "Plain {{User.Name}}",
			},
		},
	}
	var logBuf bytes.Buffer
	logger := newSendEmailActionTestLogger(&logBuf)
	sender := &recordingEmailSender{}
	actionService := &ActionService{
		persistence: persistence,
		emailSender: sender,
		logger:      logger,
	}
	executor := newFlowExecutorWithPlatformActionCalls(nil, actionService, nil, nil, nil, nil, logger)
	interview := &models.FlowInterview{
		Id: "interview-1",
		ContextData: map[string]interface{}{
			"$Record": models.SObject{"Name": "Sensitive Plan"},
			"Secret":  "Sensitive Variable",
		},
	}
	flowMeta := &models.FlowMetadata{Start: &models.FlowStart{Object: "Opportunity"}}
	action := &models.FlowActionCall{
		Name:       "Send_Email_Simple",
		ActionName: "Send_Email_Simple",
		ActionType: "emailSimple",
		InputParameters: []models.FlowActionParameter{
			{Name: constants.ConfigEmailTemplate, Value: "tmpl-sensitive"},
			{Name: constants.ConfigRecipient, Value: "sensitive-recipient@example.com"},
			{Name: constants.ConfigVariables, Value: map[string]interface{}{
				"Secret": map[string]interface{}{"elementReference": "Secret"},
			}},
		},
		Connector: &models.FlowConnector{TargetReference: "done"},
	}

	next, err := executor.executeActionCall(context.Background(), interview, flowMeta, action, &models.UserSession{ID: "005-user", Name: "Sensitive User"})
	if err != nil {
		t.Fatalf("executeActionCall(emailSimple): %v", err)
	}
	if next != "done" {
		t.Fatalf("next = %q; want done", next)
	}
	if len(sender.messages) != 1 {
		t.Fatalf("email sender calls = %d; want 1", len(sender.messages))
	}

	logged := logBuf.String()
	assertSendEmailSurfacesOmitSensitiveContent(t, logged, "emailSimple success logs")
	// emailSimple now shares the canonical send path, so its success log uses the
	// canonical "FLOW SEND EMAIL" marker set emitted by executeCanonicalSendEmail.
	for _, want := range []string{"FLOW SEND EMAIL", "Template=tmpl-sensitive", "ToCount=1", "CcCount=0", "SenderPolicy=default", "FromOverride=false", "SentCount=1", "RecipientResultCount=1"} {
		if !strings.Contains(logged, want) {
			t.Fatalf("emailSimple success logs = %q; want non-sensitive marker %q", logged, want)
		}
	}
}

func TestFlowEmailSimpleProviderFailureRedactsFlowAuditLogsAndResult(t *testing.T) {
	persistence := &emailTemplatePersistenceStub{
		records: map[string]models.SObject{
			"tmpl-sensitive": {
				constants.FieldSysEmailTemplate_IsActive:  true,
				constants.FieldSysEmailTemplate_Subject:   "Private {{Record.Name}}",
				constants.FieldSysEmailTemplate_HTMLValue: "<p>{{Variables.Secret}}</p>",
				constants.FieldSysEmailTemplate_Body:      "Plain {{User.Name}}",
			},
		},
	}
	var logBuf bytes.Buffer
	logger := newSendEmailActionTestLogger(&logBuf)
	actionService := &ActionService{
		persistence: persistence,
		emailSender: &failingEmailSender{err: errors.New("smtp rejected sensitive-recipient@example.com subject Private Sensitive Plan body Sensitive Variable from Sensitive User")},
		logger:      logger,
	}
	executor := newFlowExecutorWithPlatformActionCalls(nil, actionService, nil, nil, nil, nil, logger)
	executor.interviews = NewFlowInterviewService(nil, nil)

	store := newInMemoryStore()
	writer := NewWriter(store)
	prevWriter := GetAuditWriter()
	SetAuditWriter(writer)
	defer SetAuditWriter(prevWriter)

	version := &models.FlowVersion{
		Id:           "301-sensitive-email-simple",
		DefinitionId: "300-sensitive-email-simple",
		ProcessType:  "AutoLaunchedFlow",
		Status:       models.FlowStatusActive,
		Metadata: models.FlowMetadata{
			StartElementReference: "Send_Email_Simple",
			Start:                 &models.FlowStart{Object: "Opportunity"},
			ActionCalls: []models.FlowActionCall{{
				Name:       "Send_Email_Simple",
				ActionName: "Send_Email_Simple",
				ActionType: "emailSimple",
				InputParameters: []models.FlowActionParameter{
					{Name: constants.ConfigEmailTemplate, Value: "tmpl-sensitive"},
					{Name: constants.ConfigRecipient, Value: "sensitive-recipient@example.com"},
					{Name: constants.ConfigVariables, Value: map[string]interface{}{
						"Secret": map[string]interface{}{"elementReference": "Secret"},
					}},
				},
			}},
		},
	}
	result, err := executor.executeFlowVersion(WithTransientFlowInterviews(context.Background()), version, "Sensitive_EmailSimple_Flow", map[string]interface{}{
		"$Record": models.SObject{"Name": "Sensitive Plan"},
		"Secret":  "Sensitive Variable",
	}, &models.UserSession{ID: "005-user", Name: "Sensitive User"})
	if err != nil {
		t.Fatalf("executeFlowVersion returned err: %v", err)
	}
	if result == nil || result.IsSuccess {
		t.Fatalf("executeFlowVersion result = %#v; want failed FlowExecutionResult", result)
	}
	if len(result.Errors) == 0 {
		t.Fatalf("executeFlowVersion result errors = %#v; want sanitized error", result)
	}
	if !strings.Contains(result.Errors[0].Message, "failed to send email to 1 recipients") {
		t.Fatalf("flow error message = %q; want sanitized recipient-count failure", result.Errors[0].Message)
	}

	shutdownAuditWriter(t, writer)
	rows := store.Rows()
	if len(rows) == 0 {
		t.Fatal("audit rows = 0; want action error row")
	}
	var actionErrorPayload string
	for _, row := range rows {
		if row.ElementId == "Send_Email_Simple" && row.Status == "error" {
			actionErrorPayload = row.ErrorPayload
			break
		}
	}
	if actionErrorPayload == "" {
		t.Fatalf("audit rows = %#v; want Send_Email_Simple error payload", rows)
	}

	assertSendEmailSurfacesOmitSensitiveContent(t, result.Errors[0].Message, "flow result error")
	assertSendEmailSurfacesOmitSensitiveContent(t, actionErrorPayload, "flow audit ErrorPayload")
	assertSendEmailSurfacesOmitSensitiveContent(t, logBuf.String(), "flow executor logs")
}

func TestFlowEmailSimpleRejectsUserIDRecipientWithoutRawUserLookup(t *testing.T) {
	persistence := &emailTemplatePersistenceStub{
		records: map[string]models.SObject{
			"tmpl-sensitive": {
				constants.FieldSysEmailTemplate_IsActive: true,
				constants.FieldSysEmailTemplate_Subject:  "Private {{Record.Name}}",
			},
		},
	}
	sender := &recordingEmailSender{}
	logger := logging.New(context.Background())
	actionService := &ActionService{
		persistence: persistence,
		emailSender: sender,
		logger:      logger,
	}
	executor := newFlowExecutorWithPlatformActionCalls(nil, actionService, nil, nil, nil, nil, logger)
	interview := &models.FlowInterview{
		Id: "interview-1",
		ContextData: map[string]interface{}{
			"$Record": models.SObject{"Name": "Sensitive Plan"},
		},
	}
	flowMeta := &models.FlowMetadata{Start: &models.FlowStart{Object: "Opportunity"}}
	action := &models.FlowActionCall{
		Name:       "Send_Email_Simple",
		ActionName: "Send_Email_Simple",
		ActionType: "emailSimple",
		InputParameters: []models.FlowActionParameter{
			{Name: constants.ConfigEmailTemplate, Value: "tmpl-sensitive"},
			{Name: constants.ConfigRecipient, Value: "005-hidden-user"},
		},
	}
	ctx := constants.WithSecurityContext(context.Background(), constants.SystemContext, "record-triggered flow execution")
	ctx = constants.WithStandardActionInternalAccessScopes(ctx, "email-simple-test", []constants.StandardActionInternalAccessScope{{
		StandardActionID: "email-simple-test",
		ObjectAPIName:    constants.TableUser,
		Operation:        constants.PermRead,
	}})

	_, err := executor.executeActionCall(ctx, interview, flowMeta, action, &models.UserSession{ID: "005-requester"})
	if err == nil {
		t.Fatal("executeActionCall(emailSimple) succeeded with User ID recipient; want email-string validation")
	}
	// emailSimple is now a true canonical alias of sendEmail; the recipient param
	// is normalized to constants.ConfigTo, so validation emits the canonical
	// "to must be an email address string" message. Resolved value unchanged
	// (still fails closed, no email sent).
	if !strings.Contains(err.Error(), "to must be an email address string") {
		t.Fatalf("error = %q; want email-string validation", err.Error())
	}
	if len(sender.messages) != 0 {
		t.Fatalf("email sender calls = %d; want 0", len(sender.messages))
	}
	if len(persistence.getRawCalls) != 0 {
		t.Fatalf("GetRaw calls = %#v; want no template or User lookup before recipient validation", persistence.getRawCalls)
	}
	assertNoGetRawCall(t, persistence, constants.TableUser)
	assertNoGetRawCall(t, persistence, constants.TableEmailTemplate)
}

// TestFlowEmailSimpleResolvesTypedValueWrapperRecipientAndTemplate proves the
// emailSimple normalization renames the legacy param keys but does NOT
// pre-unwrap the FE typed-value wrapper: the {stringValue:...} wrapper is
// forwarded verbatim and unwrapped downstream to the literal address/template,
// exactly as the canonical sendEmail path does. Guards against a
// bare-string-only normalization silently breaking real FE flows.
func TestFlowEmailSimpleResolvesTypedValueWrapperRecipientAndTemplate(t *testing.T) {
	persistence := &emailTemplatePersistenceStub{
		records: map[string]models.SObject{
			"tmpl-1": {
				constants.FieldSysEmailTemplate_IsActive: true,
				constants.FieldSysEmailTemplate_Subject:  "Hello",
			},
		},
	}
	sender := &recordingEmailSender{}
	actionService := &ActionService{
		persistence: persistence,
		emailSender: sender,
		logger:      logging.New(context.Background()),
	}
	executor := newFlowExecutorWithPlatformActionCalls(nil, actionService, nil, nil, nil, nil, logging.New(context.Background()))
	action := &models.FlowActionCall{
		Name:       "Send_Email_Simple",
		ActionName: "Send_Email_Simple",
		ActionType: "emailSimple",
		InputParameters: []models.FlowActionParameter{
			{Name: constants.ConfigEmailTemplate, Value: map[string]interface{}{"stringValue": "tmpl-1"}},
			{Name: constants.ConfigRecipient, Value: map[string]interface{}{"stringValue": "person@example.com"}},
		},
	}

	_, err := executor.executeActionCall(context.Background(), &models.FlowInterview{ContextData: map[string]interface{}{}}, &models.FlowMetadata{}, action, &models.UserSession{ID: "005-user"})
	if err != nil {
		t.Fatalf("executeActionCall(emailSimple): %v", err)
	}
	if len(sender.messages) != 1 {
		t.Fatalf("email sender calls = %d; want 1", len(sender.messages))
	}
	if got := sender.messages[0].To; !reflect.DeepEqual(got, []string{"person@example.com"}) {
		t.Fatalf("To = %#v; want literal address unwrapped from {stringValue} wrapper", got)
	}
}

// TestMetadataSendEmailActionRoutesThroughCanonical guards the generic
// metadata-action capability: a _System_Action with Type=SendEmail (reached via
// ExecuteActionDirect, the generic Flow fallback, or a Composite step) must still
// deliver email, now funneled through the single canonical send implementation.
// This is the anti-regression for the deleted legacy executeSendEmail leaf.
func TestMetadataSendEmailActionRoutesThroughCanonical(t *testing.T) {
	persistence := &emailTemplatePersistenceStub{
		records: map[string]models.SObject{
			"tmpl-1": {
				constants.FieldSysEmailTemplate_IsActive: true,
				constants.FieldSysEmailTemplate_Subject:  "Hello",
			},
		},
	}
	sender := &recordingEmailSender{}
	actionService := &ActionService{
		persistence: persistence,
		emailSender: sender,
		formula:     formula.NewEngine(),
		logger:      logging.New(context.Background()),
	}
	action := &models.ActionMetadata{
		ID:            "send-email",
		ObjectAPIName: "Opportunity",
		Type:          constants.ActionTypeSendEmail,
		Config: map[string]interface{}{
			constants.ConfigEmailTemplate: "tmpl-1",
			constants.ConfigRecipient:     "person@example.com",
		},
	}
	if err := actionService.ExecuteActionDirect(context.Background(), action, models.SObject{}, nil); err != nil {
		t.Fatalf("ExecuteActionDirect(Type=SendEmail): %v; metadata-backed SendEmail must not regress", err)
	}
	if len(sender.messages) != 1 {
		t.Fatalf("email sender calls = %d; want 1 (metadata-backed SendEmail must route through canonical send)", len(sender.messages))
	}
	if got := sender.messages[0].To; !reflect.DeepEqual(got, []string{"person@example.com"}) {
		t.Fatalf("To = %#v; want [person@example.com]", got)
	}
}

// TestMetadataSendEmailActionDoesNotSendWhenTemplateRenderingFails preserves the
// render-fail-before-send guarantee for the metadata-action path (was previously
// covered by a direct executeSendEmail unit test), now exercised end-to-end through
// the public ExecuteActionDirect dispatch + canonical send.
func TestMetadataSendEmailActionDoesNotSendWhenTemplateRenderingFails(t *testing.T) {
	persistence := &emailTemplatePersistenceStub{
		records: map[string]models.SObject{
			"tmpl-1": {
				constants.FieldSysEmailTemplate_IsActive: true,
				constants.FieldSysEmailTemplate_Subject:  "Record {{Record.Name}}",
			},
		},
	}
	sender := &recordingEmailSender{}
	service := &ActionService{
		persistence: persistence,
		emailSender: sender,
		formula:     formula.NewEngine(),
		logger:      logging.New(context.Background()),
	}
	action := &models.ActionMetadata{
		ID:            "send-email",
		ObjectAPIName: "Opportunity",
		Type:          constants.ActionTypeSendEmail,
		Config: map[string]interface{}{
			constants.ConfigEmailTemplate: "tmpl-1",
			constants.ConfigRecipient:     "person@example.com",
		},
	}
	if err := service.ExecuteActionDirect(context.Background(), action, models.SObject{}, nil); err == nil {
		t.Fatal("ExecuteActionDirect(Type=SendEmail) succeeded; want render validation error")
	}
	if len(sender.messages) != 0 {
		t.Fatalf("email sender calls = %d; want 0 before failed render", len(sender.messages))
	}
}

// TestFlowEmailSimpleRejectsTypedValueWrapperUserIDRecipient is the wrapper
// analog of the bare-string G1 case: a {stringValue} wrapper unwraps to a
// literal (wasLiteral=true via extractTypedValue); the canonical path does NOT
// raw-read User by ID — same fail-closed outcome as the bare-string case.
func TestFlowEmailSimpleRejectsTypedValueWrapperUserIDRecipient(t *testing.T) {
	persistence := &emailTemplatePersistenceStub{
		records: map[string]models.SObject{
			"tmpl-1": {
				constants.FieldSysEmailTemplate_IsActive: true,
				constants.FieldSysEmailTemplate_Subject:  "Hello",
			},
		},
	}
	sender := &recordingEmailSender{}
	actionService := &ActionService{
		persistence: persistence,
		emailSender: sender,
		logger:      logging.New(context.Background()),
	}
	executor := newFlowExecutorWithPlatformActionCalls(nil, actionService, nil, nil, nil, nil, logging.New(context.Background()))
	action := &models.FlowActionCall{
		Name:       "Send_Email_Simple",
		ActionName: "Send_Email_Simple",
		ActionType: "emailSimple",
		InputParameters: []models.FlowActionParameter{
			{Name: constants.ConfigEmailTemplate, Value: map[string]interface{}{"stringValue": "tmpl-1"}},
			{Name: constants.ConfigRecipient, Value: map[string]interface{}{"stringValue": "005-user"}},
		},
	}

	_, err := executor.executeActionCall(context.Background(), &models.FlowInterview{ContextData: map[string]interface{}{}}, &models.FlowMetadata{}, action, &models.UserSession{ID: "005-user"})
	if err == nil {
		t.Fatal("executeActionCall(emailSimple) succeeded with User ID recipient; want email-string validation")
	}
	if !strings.Contains(err.Error(), "to must be an email address string") {
		t.Fatalf("error = %q; want email-string validation", err.Error())
	}
	if len(sender.messages) != 0 {
		t.Fatalf("email sender calls = %d; want 0", len(sender.messages))
	}
	assertNoGetRawCall(t, persistence, constants.TableUser)
	assertNoGetRawCall(t, persistence, constants.TableEmailTemplate)
}

// TestFlowEmailSimpleRejectsDollarPrefixedVariablesKey proves the canonical
// variables resolver fails closed at build time on a malformed $-prefixed
// variables key (collides with the $-global namespace), rather than silently
// dropping the variable as the legacy path did.
func TestFlowEmailSimpleRejectsDollarPrefixedVariablesKey(t *testing.T) {
	persistence := &emailTemplatePersistenceStub{
		records: map[string]models.SObject{
			"tmpl-1": {
				constants.FieldSysEmailTemplate_IsActive: true,
				constants.FieldSysEmailTemplate_Subject:  "Hello",
			},
		},
	}
	sender := &recordingEmailSender{}
	actionService := &ActionService{
		persistence: persistence,
		emailSender: sender,
		logger:      logging.New(context.Background()),
	}
	executor := newFlowExecutorWithPlatformActionCalls(nil, actionService, nil, nil, nil, nil, logging.New(context.Background()))
	action := &models.FlowActionCall{
		Name:       "Send_Email_Simple",
		ActionName: "Send_Email_Simple",
		ActionType: "emailSimple",
		InputParameters: []models.FlowActionParameter{
			{Name: constants.ConfigEmailTemplate, Value: "tmpl-1"},
			{Name: constants.ConfigRecipient, Value: "person@example.com"},
			{Name: constants.ConfigVariables, Value: map[string]interface{}{
				"$Foo": map[string]interface{}{"stringValue": "x"},
			}},
		},
	}

	_, err := executor.executeActionCall(context.Background(), &models.FlowInterview{ContextData: map[string]interface{}{}}, &models.FlowMetadata{}, action, &models.UserSession{ID: "005-user"})
	if err == nil {
		t.Fatal("executeActionCall(emailSimple) succeeded with $-prefixed variables key; want build-time rejection")
	}
	if !strings.Contains(err.Error(), "variables keys must be non-empty names without a $ prefix") {
		t.Fatalf("error = %q; want $-prefixed variables key rejection", err.Error())
	}
	if len(sender.messages) != 0 {
		t.Fatalf("email sender calls = %d; want 0", len(sender.messages))
	}
}

func TestFlowSendEmailCanonicalRejectsUserIDRecipientWithoutLookup(t *testing.T) {
	persistence := &emailTemplatePersistenceStub{
		records: map[string]models.SObject{
			"tmpl-1": {
				constants.FieldSysEmailTemplate_IsActive: true,
				constants.FieldSysEmailTemplate_Subject:  "Hello",
			},
		},
	}
	sender := &recordingEmailSender{}
	actionService := &ActionService{
		persistence: persistence,
		emailSender: sender,
		logger:      logging.New(context.Background()),
	}
	executor := newFlowExecutorWithPlatformActionCalls(nil, actionService, nil, nil, nil, nil, logging.New(context.Background()))
	action := &models.FlowActionCall{
		Name:       "send_review_email",
		ActionName: "send_review_email",
		ActionType: "sendEmail",
		InputParameters: []models.FlowActionParameter{
			{Name: "templateId", Value: "tmpl-1"},
			{Name: constants.ConfigTo, Value: "005-user"},
		},
	}

	_, err := executor.executeActionCall(context.Background(), &models.FlowInterview{ContextData: map[string]interface{}{}}, &models.FlowMetadata{}, action, &models.UserSession{ID: "005-user"})
	if err == nil {
		t.Fatal("executeActionCall(sendEmail) succeeded; want invalid recipient error")
	}
	if !strings.Contains(err.Error(), "to must be an email address string") {
		t.Fatalf("error = %q; want email-string validation", err.Error())
	}
	if len(sender.messages) != 0 {
		t.Fatalf("email sender calls = %d; want 0", len(sender.messages))
	}
	assertNoGetRawCall(t, persistence, constants.TableUser)
	assertNoGetRawCall(t, persistence, constants.TableEmailTemplate)
	assertNoGetRawCall(t, persistence, constants.TableWorkflowAlert)
}

func TestFlowSendEmailCanonicalDoesNotSendForMissingTemplate(t *testing.T) {
	persistence := &emailTemplatePersistenceStub{records: map[string]models.SObject{}}
	sender := &recordingEmailSender{}
	actionService := &ActionService{
		persistence: persistence,
		emailSender: sender,
		logger:      logging.New(context.Background()),
	}
	executor := newFlowExecutorWithPlatformActionCalls(nil, actionService, nil, nil, nil, nil, logging.New(context.Background()))
	action := &models.FlowActionCall{
		Name:       "send_review_email",
		ActionName: "send_review_email",
		ActionType: "sendEmail",
		InputParameters: []models.FlowActionParameter{
			{Name: "templateId", Value: "missing-template"},
			{Name: constants.ConfigTo, Value: "person@example.com"},
		},
	}

	_, err := executor.executeActionCall(context.Background(), &models.FlowInterview{ContextData: map[string]interface{}{}}, &models.FlowMetadata{}, action, &models.UserSession{ID: "005-user"})
	if err == nil {
		t.Fatal("executeActionCall(sendEmail) succeeded; want missing template error")
	}
	if len(sender.messages) != 0 {
		t.Fatalf("email sender calls = %d; want 0", len(sender.messages))
	}
	assertGetRawCalls(t, persistence, []emailTemplateGetRawCall{{objectName: constants.TableEmailTemplate, id: "missing-template"}})
	assertNoGetRawCall(t, persistence, constants.TableWorkflowAlert)
}

func TestFlowSendEmailCanonicalDoesNotSendForUnresolvedTemplateVariable(t *testing.T) {
	persistence := &emailTemplatePersistenceStub{
		records: map[string]models.SObject{
			"tmpl-1": {
				constants.FieldSysEmailTemplate_IsActive: true,
				constants.FieldSysEmailTemplate_Subject:  "Review {{Variables.ApprovalUrl}}",
			},
		},
	}
	sender := &recordingEmailSender{}
	actionService := &ActionService{
		persistence: persistence,
		emailSender: sender,
		logger:      logging.New(context.Background()),
	}
	executor := newFlowExecutorWithPlatformActionCalls(nil, actionService, nil, nil, nil, nil, logging.New(context.Background()))
	action := &models.FlowActionCall{
		Name:       "send_review_email",
		ActionName: "send_review_email",
		ActionType: "sendEmail",
		InputParameters: []models.FlowActionParameter{
			{Name: "templateId", Value: "tmpl-1"},
			{Name: constants.ConfigTo, Value: "person@example.com"},
		},
	}

	_, err := executor.executeActionCall(context.Background(), &models.FlowInterview{ContextData: map[string]interface{}{}}, &models.FlowMetadata{}, action, &models.UserSession{ID: "005-user"})
	if err == nil {
		t.Fatal("executeActionCall(sendEmail) succeeded; want unresolved template variable error")
	}
	if !strings.Contains(err.Error(), "unresolved merge token {{Variables.ApprovalUrl}}") {
		t.Fatalf("error = %q; want unresolved variable", err.Error())
	}
	if len(sender.messages) != 0 {
		t.Fatalf("email sender calls = %d; want 0", len(sender.messages))
	}
	assertGetRawCalls(t, persistence, []emailTemplateGetRawCall{{objectName: constants.TableEmailTemplate, id: "tmpl-1"}})
	assertNoGetRawCall(t, persistence, constants.TableWorkflowAlert)
}

func assertGetRawCalls(t *testing.T, persistence *emailTemplatePersistenceStub, want []emailTemplateGetRawCall) {
	t.Helper()
	if !reflect.DeepEqual(persistence.getRawCalls, want) {
		t.Fatalf("GetRaw calls = %#v; want %#v", persistence.getRawCalls, want)
	}
}

func assertNoGetRawCall(t *testing.T, persistence *emailTemplatePersistenceStub, objectName string) {
	t.Helper()
	for _, call := range persistence.getRawCalls {
		if call.objectName == objectName {
			t.Fatalf("GetRaw called %s via %#v; want no lookup", objectName, persistence.getRawCalls)
		}
	}
}

func newSendEmailActionTestLogger(buf *bytes.Buffer) *logging.Logger {
	tctx := zerolog.New(buf).WithContext(context.Background())
	return logging.New(tctx)
}

func assertSendEmailSurfacesOmitSensitiveContent(t *testing.T, surface string, name string) {
	t.Helper()
	for _, forbidden := range []string{
		"sensitive-recipient@example.com",
		"sensitive-reviewer@example.com",
		"Private Sensitive Plan",
		"Sensitive Variable",
		"Sensitive User",
	} {
		if strings.Contains(surface, forbidden) {
			t.Fatalf("%s leaked %q in %q", name, forbidden, surface)
		}
	}
}

func shutdownAuditWriter(t *testing.T, writer *Writer) {
	t.Helper()
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := writer.Shutdown(ctx); err != nil {
		t.Fatalf("audit writer shutdown: %v", err)
	}
}

type failingEmailSender struct {
	messages []*ports.EmailMessage
	err      error
}

func (s *failingEmailSender) Send(ctx context.Context, msg *ports.EmailMessage) (*ports.EmailSendResult, error) {
	s.messages = append(s.messages, msg)
	return nil, s.err
}
