package automation

import (
	"context"
	"testing"

	"github.com/nexuscrm/backend/internal/domain/ports"
	"github.com/nexuscrm/backend/internal/features/metadata"
	"github.com/nexuscrm/backend/internal/features/permissions"
	"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"
)

// ---------------------------------------------------------------------------
// Mock TransactionManager
// ---------------------------------------------------------------------------

type MockTxManager struct {
	mock.Mock
}

func (m *MockTxManager) WithTransaction(ctx context.Context, fn func(ctx context.Context) error) error {
	args := m.Called(ctx, fn)
	// By default, just run the function directly (no real tx)
	if args.Error(0) != nil {
		return args.Error(0)
	}
	return fn(ctx)
}

func (m *MockTxManager) WithRetry(ctx context.Context, fn func(ctx context.Context) error, maxRetries int) error {
	return fn(ctx)
}

func (m *MockTxManager) IsTransactionActive(ctx context.Context) bool {
	return false
}

func (m *MockTxManager) ExtractTx(ctx context.Context) interface{} {
	return nil
}

func (m *MockTxManager) DetachTx(ctx context.Context) context.Context { return ctx }

// Ensure MockTxManager satisfies ports.TransactionManager
var _ ports.TransactionManager = (*MockTxManager)(nil)

// ---------------------------------------------------------------------------
// Helpers for building an ActionService with mocks
// ---------------------------------------------------------------------------

// MockEmailSender captures email sends for test assertions.
type MockEmailSender struct {
	mock.Mock
}

func (m *MockEmailSender) Send(ctx context.Context, msg *ports.EmailMessage) error {
	args := m.Called(ctx, msg)
	return args.Error(0)
}

var _ ports.EmailSender = (*MockEmailSender)(nil)

func newTestActionService(
	metaSvc *metadata.MetadataService,
	persist PersistenceProvider,
	perm *permissions.PermissionService,
	txMgr ports.TransactionManager,
) *ActionService {
	logger := logging.New(context.Background())
	noopEmail := &MockEmailSender{}
	noopEmail.On("Send", mock.Anything, mock.Anything).Return(nil)
	return NewActionService(metaSvc, persist, perm, txMgr, noopEmail, logger)
}

// ---------------------------------------------------------------------------
// NewActionService
// ---------------------------------------------------------------------------

func TestNewActionService(t *testing.T) {
	svc := newTestActionService(nil, nil, nil, nil)
	assert.NotNil(t, svc)
	assert.NotNil(t, svc.formula)
	assert.NotNil(t, svc.logger)
}

// ---------------------------------------------------------------------------
// evaluateRef — literal values and formula detection
// ---------------------------------------------------------------------------

func TestEvaluateRef_LiteralNonString(t *testing.T) {
	svc := newTestActionService(nil, nil, nil, nil)
	ctx := context.Background()
	actionCtx := &ActionContext{
		Record:  models.SObject{"Id": "r-1"},
		User:    &models.UserSession{ID: "u-1", Name: "Test"},
		Results: make(map[string]map[string]interface{}),
	}

	// Non-string value is returned as-is
	result, err := svc.evaluateRef(ctx, 42, actionCtx, "Account")
	assert.NoError(t, err)
	assert.Equal(t, 42, result)

	// Bool
	result, err = svc.evaluateRef(ctx, true, actionCtx, "Account")
	assert.NoError(t, err)
	assert.Equal(t, true, result)
}

func TestEvaluateRef_LiteralString(t *testing.T) {
	svc := newTestActionService(nil, nil, nil, nil)
	ctx := context.Background()
	actionCtx := &ActionContext{
		Record:  models.SObject{},
		User:    nil,
		Results: make(map[string]map[string]interface{}),
	}

	// Non-formula string is returned as-is
	result, err := svc.evaluateRef(ctx, "hello world", actionCtx, "Account")
	assert.NoError(t, err)
	assert.Equal(t, "hello world", result)
}

func TestEvaluateRef_FormulaExecution(t *testing.T) {
	svc := newTestActionService(nil, nil, nil, nil)
	ctx := context.Background()
	actionCtx := &ActionContext{
		Record:  models.SObject{"Name": "Acme"},
		User:    nil,
		Results: make(map[string]map[string]interface{}),
	}

	// The formula engine uses lowercase "record" and flattens record fields to top-level.
	// Simple formula accessing a flattened record field: {!Name}
	result, err := svc.evaluateRef(ctx, "{!Name}", actionCtx, "Account")
	assert.NoError(t, err)
	// The formula engine should resolve Name from the record
	assert.Equal(t, "Acme", result)
}

func TestEvaluateRef_WithUserContext_LiteralPath(t *testing.T) {
	svc := newTestActionService(nil, nil, nil, nil)
	ctx := context.Background()
	email := "test@example.com"
	actionCtx := &ActionContext{
		Record: models.SObject{},
		User: &models.UserSession{
			ID:        "user-1",
			Name:      "John Doe",
			Email:     &email,
			ProfileID: "prof-1",
		},
		Results: make(map[string]map[string]interface{}),
	}

	// Literal strings pass through evaluateRef without formula execution,
	// so no PermissionService needed. This still exercises the User context setup.
	result, err := svc.evaluateRef(ctx, "literal-val", actionCtx, "Account")
	assert.NoError(t, err)
	assert.Equal(t, "literal-val", result)
}

func TestEvaluateRef_UserNilEmail_LiteralPath(t *testing.T) {
	svc := newTestActionService(nil, nil, nil, nil)
	ctx := context.Background()
	actionCtx := &ActionContext{
		Record: models.SObject{},
		User: &models.UserSession{
			ID:    "user-1",
			Name:  "Admin",
			Email: nil,
		},
		Results: make(map[string]map[string]interface{}),
	}

	result, err := svc.evaluateRef(ctx, "some-literal", actionCtx, "Account")
	assert.NoError(t, err)
	assert.Equal(t, "some-literal", result)
}

func TestEvaluateRef_WithNilUser(t *testing.T) {
	svc := newTestActionService(nil, nil, nil, nil)
	ctx := context.Background()
	actionCtx := &ActionContext{
		Record:  models.SObject{},
		User:    nil,
		Results: make(map[string]map[string]interface{}),
	}

	// Even with nil user, should not panic
	result, err := svc.evaluateRef(ctx, "literal", actionCtx, "Account")
	assert.NoError(t, err)
	assert.Equal(t, "literal", result)
}

// ---------------------------------------------------------------------------
// getConfigValue
// ---------------------------------------------------------------------------

func TestGetConfigValue_Present(t *testing.T) {
	svc := newTestActionService(nil, nil, nil, nil)
	ctx := context.Background()
	actionCtx := &ActionContext{
		Record:  models.SObject{},
		User:    nil,
		Results: make(map[string]map[string]interface{}),
	}

	config := map[string]interface{}{
		"to":      "admin@test.com",
		"subject": "Hello",
	}

	result, err := svc.getConfigValue(ctx, config, "to", actionCtx, "Account")
	assert.NoError(t, err)
	assert.Equal(t, "admin@test.com", result)
}

func TestGetConfigValue_Missing(t *testing.T) {
	svc := newTestActionService(nil, nil, nil, nil)
	ctx := context.Background()
	actionCtx := &ActionContext{
		Record:  models.SObject{},
		User:    nil,
		Results: make(map[string]map[string]interface{}),
	}

	config := map[string]interface{}{}

	_, err := svc.getConfigValue(ctx, config, "nonexistent", actionCtx, "Account")
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "nonexistent")
}

// ---------------------------------------------------------------------------
// executeActionFromMetadata — routing by type
// ---------------------------------------------------------------------------

func TestExecuteActionFromMetadata_UnsupportedType(t *testing.T) {
	svc := newTestActionService(nil, nil, nil, nil)
	ctx := context.Background()
	actionCtx := &ActionContext{
		Record:  models.SObject{},
		User:    nil,
		Results: make(map[string]map[string]interface{}),
	}

	action := &models.ActionMetadata{
		ID:   "act-1",
		Type: "UnsupportedType",
	}

	err := svc.executeActionFromMetadata(ctx, action, actionCtx)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "unsupported action type")
}

func TestExecuteActionFromMetadata_CreateRecord(t *testing.T) {
	mockPersist := new(MockPersistenceForAction)
	svc := newTestActionService(nil, mockPersist, nil, nil)
	ctx := context.Background()
	user := &models.UserSession{ID: "u-1", Name: "Test"}
	actionCtx := &ActionContext{
		Record:  models.SObject{},
		User:    user,
		Results: make(map[string]map[string]interface{}),
	}

	action := &models.ActionMetadata{
		ID:   "act-1",
		Type: constants.ActionTypeCreateRecord,
		Config: map[string]interface{}{
			constants.ConfigTargetObject: "Contact",
			constants.ConfigFieldMappings: map[string]interface{}{
				"FirstName": "John",
				"LastName":  "Doe",
			},
		},
	}

	expectedResult := models.SObject{constants.FieldID: "con-001", "FirstName": "John", "LastName": "Doe"}
	mockPersist.On("Insert", ctx, "Contact", mock.MatchedBy(func(r models.SObject) bool {
		return r["FirstName"] == "John" && r["LastName"] == "Doe"
	}), user).Return(expectedResult, nil)

	err := svc.executeActionFromMetadata(ctx, action, actionCtx)
	assert.NoError(t, err)
	// Check result was stored
	assert.NotNil(t, actionCtx.Results["act-1"])
	assert.Equal(t, "con-001", actionCtx.Results["act-1"][constants.FieldID])
	mockPersist.AssertExpectations(t)
}

func TestExecuteActionFromMetadata_CreateRecord_MissingFieldMappings(t *testing.T) {
	svc := newTestActionService(nil, nil, nil, nil)
	ctx := context.Background()
	actionCtx := &ActionContext{
		Record:  models.SObject{},
		User:    nil,
		Results: make(map[string]map[string]interface{}),
	}

	action := &models.ActionMetadata{
		ID:   "act-1",
		Type: constants.ActionTypeCreateRecord,
		Config: map[string]interface{}{
			constants.ConfigTargetObject: "Contact",
			// no field_mappings
		},
	}

	err := svc.executeActionFromMetadata(ctx, action, actionCtx)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "field_mappings")
}

func TestExecuteActionFromMetadata_CreateRecord_MissingTargetObject(t *testing.T) {
	svc := newTestActionService(nil, nil, nil, nil)
	ctx := context.Background()
	actionCtx := &ActionContext{
		Record:  models.SObject{},
		User:    nil,
		Results: make(map[string]map[string]interface{}),
	}

	action := &models.ActionMetadata{
		ID:     "act-1",
		Type:   constants.ActionTypeCreateRecord,
		Config: map[string]interface{}{},
	}

	err := svc.executeActionFromMetadata(ctx, action, actionCtx)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "target_object")
}

func TestExecuteActionFromMetadata_UpdateRecord(t *testing.T) {
	mockPersist := new(MockPersistenceForAction)
	svc := newTestActionService(nil, mockPersist, nil, nil)
	ctx := context.Background()
	user := &models.UserSession{ID: "u-1", Name: "Test"}
	actionCtx := &ActionContext{
		Record:  models.SObject{},
		User:    user,
		Results: make(map[string]map[string]interface{}),
	}

	action := &models.ActionMetadata{
		ID:   "act-2",
		Type: constants.ActionTypeUpdateRecord,
		Config: map[string]interface{}{
			constants.ConfigTargetObject: "Account",
			constants.ConfigRecordID:     "acc-123",
			constants.ConfigFieldMappings: map[string]interface{}{
				"Status": "Active",
			},
		},
	}

	mockPersist.On("Update", ctx, "Account", "acc-123", mock.MatchedBy(func(r models.SObject) bool {
		return r["Status"] == "Active"
	}), user).Return(nil)

	err := svc.executeActionFromMetadata(ctx, action, actionCtx)
	assert.NoError(t, err)
	mockPersist.AssertExpectations(t)
}

func TestExecuteActionFromMetadata_UpdateRecord_MissingRecordID(t *testing.T) {
	svc := newTestActionService(nil, nil, nil, nil)
	ctx := context.Background()
	actionCtx := &ActionContext{
		Record:  models.SObject{},
		User:    nil,
		Results: make(map[string]map[string]interface{}),
	}

	action := &models.ActionMetadata{
		ID:   "act-2",
		Type: constants.ActionTypeUpdateRecord,
		Config: map[string]interface{}{
			constants.ConfigTargetObject: "Account",
			// missing record_id
			constants.ConfigFieldMappings: map[string]interface{}{
				"Status": "Active",
			},
		},
	}

	err := svc.executeActionFromMetadata(ctx, action, actionCtx)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "record_id")
}

func TestExecuteActionFromMetadata_SendEmail(t *testing.T) {
	mockPersist := new(MockPersistenceForAction)
	mockEmail := new(MockEmailSender)
	logger := logging.New(context.Background())
	svc := NewActionService(nil, mockPersist, nil, nil, mockEmail, logger)

	ctx := context.Background()
	email := "admin@test.com"
	user := &models.UserSession{ID: "u-1", Name: "Admin", Email: &email}
	actionCtx := &ActionContext{
		Record:  models.SObject{"Name": "Test Record"},
		User:    user,
		Results: make(map[string]map[string]interface{}),
	}

	// Mock template lookup
	mockPersist.On("GetRaw", ctx, constants.TableEmailTemplate, "tmpl-1").Return(models.SObject{
		"Id":        "tmpl-1",
		"Subject":   "Welcome to NexusCRM",
		"Body":      "Dear user, thank you for joining.",
		"HtmlValue": "<p>Dear user, thank you for joining.</p>",
		"IsActive":  int64(1),
	}, nil)

	mockEmail.On("Send", ctx, mock.MatchedBy(func(msg *ports.EmailMessage) bool {
		return msg.To[0] == "recipient@test.com" &&
			msg.Subject == "Welcome to NexusCRM" &&
			msg.HTML == "<p>Dear user, thank you for joining.</p>"
	})).Return(nil)

	action := &models.ActionMetadata{
		ID:   "act-3",
		Type: constants.ActionTypeSendEmail,
		Config: map[string]interface{}{
			constants.ConfigEmailTemplate: "tmpl-1",
			constants.ConfigRecipient:     "recipient@test.com",
		},
	}

	err := svc.executeActionFromMetadata(ctx, action, actionCtx)
	assert.NoError(t, err)
	mockEmail.AssertExpectations(t)
	mockPersist.AssertExpectations(t)
}

func TestExecuteActionFromMetadata_SendEmail_MissingTemplate(t *testing.T) {
	svc := newTestActionService(nil, nil, nil, nil)
	ctx := context.Background()
	actionCtx := &ActionContext{
		Record:  models.SObject{},
		User:    nil,
		Results: make(map[string]map[string]interface{}),
	}

	action := &models.ActionMetadata{
		ID:   "act-3",
		Type: constants.ActionTypeSendEmail,
		Config: map[string]interface{}{
			// missing emailTemplate
			constants.ConfigRecipient: "test@test.com",
		},
	}

	err := svc.executeActionFromMetadata(ctx, action, actionCtx)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "emailTemplate")
}

func TestExecuteActionFromMetadata_SendEmail_TemplateNotFound(t *testing.T) {
	mockPersist := new(MockPersistenceForAction)
	svc := newTestActionService(nil, mockPersist, nil, nil)
	ctx := context.Background()
	actionCtx := &ActionContext{
		Record:  models.SObject{},
		User:    nil,
		Results: make(map[string]map[string]interface{}),
	}

	mockPersist.On("GetRaw", ctx, constants.TableEmailTemplate, "bad-id").Return(models.SObject(nil), nil)

	action := &models.ActionMetadata{
		ID:   "act-3",
		Type: constants.ActionTypeSendEmail,
		Config: map[string]interface{}{
			constants.ConfigEmailTemplate: "bad-id",
			constants.ConfigRecipient:     "test@test.com",
		},
	}

	err := svc.executeActionFromMetadata(ctx, action, actionCtx)
	assert.Error(t, err)
}

func TestExecuteActionFromMetadata_SendEmail_InactiveTemplate(t *testing.T) {
	mockPersist := new(MockPersistenceForAction)
	svc := newTestActionService(nil, mockPersist, nil, nil)
	ctx := context.Background()
	actionCtx := &ActionContext{
		Record:  models.SObject{},
		User:    nil,
		Results: make(map[string]map[string]interface{}),
	}

	mockPersist.On("GetRaw", ctx, constants.TableEmailTemplate, "tmpl-inactive").Return(models.SObject{
		"Id":       "tmpl-inactive",
		"Subject":  "Test",
		"Body":     "Test body",
		"IsActive": int64(0),
	}, nil)

	action := &models.ActionMetadata{
		ID:   "act-3",
		Type: constants.ActionTypeSendEmail,
		Config: map[string]interface{}{
			constants.ConfigEmailTemplate: "tmpl-inactive",
			constants.ConfigRecipient:     "test@test.com",
		},
	}

	err := svc.executeActionFromMetadata(ctx, action, actionCtx)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "inactive")
}

func TestExecuteActionFromMetadata_SendEmail_RecipientUserID(t *testing.T) {
	mockPersist := new(MockPersistenceForAction)
	mockEmail := new(MockEmailSender)
	logger := logging.New(context.Background())
	svc := NewActionService(nil, mockPersist, nil, nil, mockEmail, logger)

	ctx := context.Background()
	actionCtx := &ActionContext{
		Record:  models.SObject{},
		User:    nil,
		Results: make(map[string]map[string]interface{}),
	}

	// Mock template lookup
	mockPersist.On("GetRaw", ctx, constants.TableEmailTemplate, "tmpl-1").Return(models.SObject{
		"Id":       "tmpl-1",
		"Subject":  "Test",
		"Body":     "Body",
		"IsActive": int64(1),
	}, nil)

	// Mock user lookup (recipient is a User ID, not an email)
	mockPersist.On("GetRaw", ctx, constants.TableUser, "user-123").Return(models.SObject{
		"Id":    "user-123",
		"Email": "resolved@test.com",
	}, nil)

	mockEmail.On("Send", ctx, mock.MatchedBy(func(msg *ports.EmailMessage) bool {
		return msg.To[0] == "resolved@test.com"
	})).Return(nil)

	action := &models.ActionMetadata{
		ID:   "act-3",
		Type: constants.ActionTypeSendEmail,
		Config: map[string]interface{}{
			constants.ConfigEmailTemplate: "tmpl-1",
			constants.ConfigRecipient:     "user-123",
		},
	}

	err := svc.executeActionFromMetadata(ctx, action, actionCtx)
	assert.NoError(t, err)
	mockEmail.AssertExpectations(t)
}

func TestExecuteActionFromMetadata_SendEmail_DoubleBraceMergeFields(t *testing.T) {
	mockPersist := new(MockPersistenceForAction)
	mockEmail := new(MockEmailSender)
	logger := logging.New(context.Background())
	svc := NewActionService(nil, mockPersist, nil, nil, mockEmail, logger)

	ctx := context.Background()
	email := "admin@test.com"
	user := &models.UserSession{ID: "u-1", Name: "Admin", Email: &email}
	actionCtx := &ActionContext{
		Record:  models.SObject{"Name": "Acme Corp"},
		User:    user,
		Results: make(map[string]map[string]interface{}),
	}

	mockPersist.On("GetRaw", ctx, constants.TableEmailTemplate, "tmpl-braces").Return(models.SObject{
		"Id":        "tmpl-braces",
		"Subject":   "Hello {{Account.Name}}",
		"Body":      "Plain {{Account.Name}} from {{User.Email}}",
		"HtmlValue": "<p>{{Account.Name}}</p><p>{{User.Email}}</p>",
		"IsActive":  int64(1),
	}, nil)
	mockEmail.On("Send", ctx, mock.MatchedBy(func(msg *ports.EmailMessage) bool {
		return len(msg.To) == 1 &&
			msg.To[0] == "recipient@test.com" &&
			msg.Subject == "Hello Acme Corp" &&
			msg.Body == "Plain Acme Corp from admin@test.com" &&
			msg.HTML == "<p>Acme Corp</p><p>admin@test.com</p>"
	})).Return(nil)

	action := &models.ActionMetadata{
		ID:            "act-braces",
		ObjectAPIName: "Account",
		Type:          constants.ActionTypeSendEmail,
		Config: map[string]interface{}{
			constants.ConfigEmailTemplate: "tmpl-braces",
			constants.ConfigRecipient:     "recipient@test.com",
		},
	}

	err := svc.executeActionFromMetadata(ctx, action, actionCtx)
	assert.NoError(t, err)
	mockPersist.AssertExpectations(t)
	mockEmail.AssertExpectations(t)
}

// ---------------------------------------------------------------------------
// executeComposite + executeSteps
// ---------------------------------------------------------------------------

func TestExecuteActionFromMetadata_Composite(t *testing.T) {
	mockPersist := new(MockPersistenceForAction)
	mockTx := new(MockTxManager)
	svc := newTestActionService(nil, mockPersist, nil, mockTx)
	ctx := context.Background()
	user := &models.UserSession{ID: "u-1", Name: "Test"}
	actionCtx := &ActionContext{
		Record:  models.SObject{},
		User:    user,
		Results: make(map[string]map[string]interface{}),
	}

	// Mock template for the send email step inside composite
	mockPersist.On("GetRaw", ctx, constants.TableEmailTemplate, "tmpl-comp").Return(models.SObject{
		"Id":       "tmpl-comp",
		"Subject":  "Composite Subject",
		"Body":     "Composite Body",
		"IsActive": int64(1),
	}, nil)

	// Build composite action with 1 step: send email
	action := &models.ActionMetadata{
		ID:            "act-comp",
		Type:          constants.ActionTypeComposite,
		ObjectAPIName: "Account",
		Config: map[string]interface{}{
			constants.ConfigKeySteps: []interface{}{
				map[string]interface{}{
					constants.FieldID:             "step-1",
					constants.FieldSysAction_Type: constants.ActionTypeSendEmail,
					constants.ConfigEmailTemplate: "tmpl-comp",
					constants.ConfigRecipient:     "test@test.com",
				},
			},
		},
	}

	mockTx.On("WithTransaction", ctx, mock.Anything).Return(nil)

	err := svc.executeActionFromMetadata(ctx, action, actionCtx)
	assert.NoError(t, err)
	mockTx.AssertExpectations(t)
}

func TestExecuteActionFromMetadata_Composite_MissingSteps(t *testing.T) {
	svc := newTestActionService(nil, nil, nil, nil)
	ctx := context.Background()
	actionCtx := &ActionContext{
		Record:  models.SObject{},
		User:    nil,
		Results: make(map[string]map[string]interface{}),
	}

	action := &models.ActionMetadata{
		ID:     "act-comp",
		Type:   constants.ActionTypeComposite,
		Config: map[string]interface{}{
			// no steps
		},
	}

	err := svc.executeActionFromMetadata(ctx, action, actionCtx)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "steps")
}

func TestExecuteActionFromMetadata_Composite_StepsWrongType(t *testing.T) {
	svc := newTestActionService(nil, nil, nil, nil)
	ctx := context.Background()
	actionCtx := &ActionContext{
		Record:  models.SObject{},
		User:    nil,
		Results: make(map[string]map[string]interface{}),
	}

	action := &models.ActionMetadata{
		ID:   "act-comp",
		Type: constants.ActionTypeComposite,
		Config: map[string]interface{}{
			constants.ConfigKeySteps: "not-an-array",
		},
	}

	err := svc.executeActionFromMetadata(ctx, action, actionCtx)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "steps must be an array")
}

func TestExecuteSteps_InvalidStepConfig(t *testing.T) {
	svc := newTestActionService(nil, nil, nil, nil)
	ctx := context.Background()
	actionCtx := &ActionContext{
		Record:  models.SObject{},
		User:    nil,
		Results: make(map[string]map[string]interface{}),
	}

	steps := []interface{}{
		"not-a-map",
	}

	err := svc.executeSteps(ctx, steps, actionCtx, "Account")
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "invalid step config")
}

func TestExecuteSteps_MissingStepIDOrType(t *testing.T) {
	svc := newTestActionService(nil, nil, nil, nil)
	ctx := context.Background()
	actionCtx := &ActionContext{
		Record:  models.SObject{},
		User:    nil,
		Results: make(map[string]map[string]interface{}),
	}

	steps := []interface{}{
		map[string]interface{}{
			// missing id and type
		},
	}

	err := svc.executeSteps(ctx, steps, actionCtx, "Account")
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "step must have id and type")
}

// ---------------------------------------------------------------------------
// ExecuteActionDirect
// ---------------------------------------------------------------------------

func TestExecuteActionDirect_SendEmail(t *testing.T) {
	mockPersist := new(MockPersistenceForAction)
	mockEmail := new(MockEmailSender)
	logger := logging.New(context.Background())
	svc := NewActionService(nil, mockPersist, nil, nil, mockEmail, logger)

	ctx := context.Background()
	email := "admin@test.com"
	user := &models.UserSession{ID: "u-1", Name: "Admin", Email: &email}
	record := models.SObject{"Name": "Test Record"}

	mockPersist.On("GetRaw", ctx, constants.TableEmailTemplate, "tmpl-direct").Return(models.SObject{
		"Id":       "tmpl-direct",
		"Subject":  "Direct Action",
		"Body":     "Direct Body",
		"IsActive": int64(1),
	}, nil)

	mockEmail.On("Send", ctx, mock.Anything).Return(nil)

	action := &models.ActionMetadata{
		ID:   "act-direct",
		Type: constants.ActionTypeSendEmail,
		Config: map[string]interface{}{
			constants.ConfigEmailTemplate: "tmpl-direct",
			constants.ConfigRecipient:     "recipient@example.com",
		},
	}

	err := svc.ExecuteActionDirect(ctx, action, record, user)
	assert.NoError(t, err)
	mockEmail.AssertExpectations(t)
}

func TestExecuteActionDirect_Unsupported(t *testing.T) {
	svc := newTestActionService(nil, nil, nil, nil)
	ctx := context.Background()

	action := &models.ActionMetadata{
		ID:   "act-bad",
		Type: "BadType",
	}

	err := svc.ExecuteActionDirect(ctx, action, models.SObject{}, nil)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "unsupported action type")
}

// ---------------------------------------------------------------------------
// Lookup field constants used in FieldSysAction_Type
// ---------------------------------------------------------------------------

func TestFieldConstants(t *testing.T) {
	// Ensure the field constant is valid so action_service.go step parsing works
	assert.Equal(t, "Type", constants.FieldSysAction_Type)
}
