package rest_test

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

	"context"

	"github.com/gin-gonic/gin"
	"github.com/nexuscrm/backend/internal/features/automation"
	"github.com/nexuscrm/backend/internal/interfaces/rest"
	"github.com/nexuscrm/backend/pkg/auth"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// ---------------------------------------------------------------------------
// Mock ApprovalService
// ---------------------------------------------------------------------------

type mockApprovalService struct {
	submitFn            func(ctx context.Context, req automation.SubmitRequest, user *models.UserSession) (models.SObject, error)
	approveFn           func(ctx context.Context, workItemID, comments string, user *models.UserSession) (models.SObject, error)
	rejectFn            func(ctx context.Context, workItemID, comments string, user *models.UserSession) (models.SObject, error)
	recallFn            func(ctx context.Context, workItemID, comments string, user *models.UserSession) (models.SObject, error)
	listPendingFn       func(ctx context.Context, user *models.UserSession) ([]automation.PendingApprovalView, error)
	getLatestInstanceFn func(ctx context.Context, recordID string, user *models.UserSession) (*automation.ProcessInstanceSummary, error)
	cloneFn             func(ctx context.Context, processDefID, newDeveloperName, newLabel string, user *models.UserSession) (*models.SObject, error)
	projectChainFn      func(ctx context.Context, processDefID, objectAPIName, recordID string, user *models.UserSession) ([]automation.ProjectedNode, error)
}

func (m *mockApprovalService) Submit(ctx context.Context, req automation.SubmitRequest, user *models.UserSession) (models.SObject, error) {
	if m.submitFn != nil {
		return m.submitFn(ctx, req, user)
	}
	return nil, nil
}

func (m *mockApprovalService) Approve(ctx context.Context, workItemID, comments string, user *models.UserSession) (models.SObject, error) {
	if m.approveFn != nil {
		return m.approveFn(ctx, workItemID, comments, user)
	}
	return nil, nil
}

func (m *mockApprovalService) Reject(ctx context.Context, workItemID, comments string, user *models.UserSession) (models.SObject, error) {
	if m.rejectFn != nil {
		return m.rejectFn(ctx, workItemID, comments, user)
	}
	return nil, nil
}

func (m *mockApprovalService) Recall(ctx context.Context, workItemID, comments string, user *models.UserSession) (models.SObject, error) {
	if m.recallFn != nil {
		return m.recallFn(ctx, workItemID, comments, user)
	}
	return nil, nil
}

func (m *mockApprovalService) Reassign(ctx context.Context, workItemID, newActorID string, user *models.UserSession) error {
	return nil
}

func (m *mockApprovalService) ListPendingApprovals(ctx context.Context, user *models.UserSession) ([]automation.PendingApprovalView, error) {
	if m.listPendingFn != nil {
		return m.listPendingFn(ctx, user)
	}
	return nil, nil
}

func (m *mockApprovalService) GetLatestProcessInstance(ctx context.Context, recordID string, user *models.UserSession) (*automation.ProcessInstanceSummary, error) {
	if m.getLatestInstanceFn != nil {
		return m.getLatestInstanceFn(ctx, recordID, user)
	}
	return nil, nil
}

func (m *mockApprovalService) Clone(ctx context.Context, processDefID, newDeveloperName, newLabel string, user *models.UserSession) (*models.SObject, error) {
	if m.cloneFn != nil {
		return m.cloneFn(ctx, processDefID, newDeveloperName, newLabel, user)
	}
	return &models.SObject{}, nil
}

func (m *mockApprovalService) ProjectChain(ctx context.Context, processDefID, objectAPIName, recordID string, user *models.UserSession) ([]automation.ProjectedNode, error) {
	if m.projectChainFn != nil {
		return m.projectChainFn(ctx, processDefID, objectAPIName, recordID, user)
	}
	return nil, nil
}

// helper: set up gin context with authenticated user
func setupAuthenticatedContext(method, path string, body []byte) (*httptest.ResponseRecorder, *gin.Context) {
	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	if body != nil {
		c.Request = httptest.NewRequest(method, path, bytes.NewReader(body))
	} else {
		c.Request = httptest.NewRequest(method, path, nil)
	}
	c.Request.Header.Set("Content-Type", "application/json")

	authUser := auth.UserSession{
		ID:        "user-001",
		Name:      "Test Admin",
		Email:     "admin@example.com",
		ProfileId: constants.ProfileSystemAdmin,
	}
	c.Set(constants.ContextKeyUser, authUser)
	return w, c
}

// ---------------------------------------------------------------------------
// HandleProcessApprovals — unauthenticated
// ---------------------------------------------------------------------------

func TestApprovalHandler_HandleProcessApprovals_Unauthenticated(t *testing.T) {
	handler := rest.NewApprovalHandler(&mockApprovalService{})
	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Request = httptest.NewRequest("POST", "/process/approvals", nil)

	assert.Panics(t, func() { handler.HandleProcessApprovals(c) })
}

// ---------------------------------------------------------------------------
// HandleProcessApprovals — invalid JSON
// ---------------------------------------------------------------------------

func TestApprovalHandler_HandleProcessApprovals_InvalidJSON(t *testing.T) {
	handler := rest.NewApprovalHandler(&mockApprovalService{})
	w, c := setupAuthenticatedContext("POST", "/process/approvals", []byte(`{not valid json`))

	handler.HandleProcessApprovals(c)
	assert.Equal(t, http.StatusBadRequest, w.Code)
}

// ---------------------------------------------------------------------------
// HandleProcessApprovals — empty requests array
// ---------------------------------------------------------------------------

func TestApprovalHandler_HandleProcessApprovals_EmptyRequests(t *testing.T) {
	handler := rest.NewApprovalHandler(&mockApprovalService{})
	body, _ := json.Marshal(map[string]interface{}{
		"requests": []interface{}{},
	})
	w, c := setupAuthenticatedContext("POST", "/process/approvals", body)

	handler.HandleProcessApprovals(c)
	assert.Equal(t, http.StatusBadRequest, w.Code)
}

// ---------------------------------------------------------------------------
// HandleProcessApprovals — Submit with SF-standard wrapper format
// ---------------------------------------------------------------------------

func TestApprovalHandler_HandleProcessApprovals_SubmitSuccess(t *testing.T) {
	svc := &mockApprovalService{
		submitFn: func(ctx context.Context, req automation.SubmitRequest, user *models.UserSession) (models.SObject, error) {
			return models.SObject{
				constants.FieldID:   "04i000000000001",
				"ProcessInstanceId": "04g000000000001",
				"TargetObjectId":    "001000000000001",
			}, nil
		},
	}
	handler := rest.NewApprovalHandler(svc)

	body, _ := json.Marshal(map[string]interface{}{
		"requests": []map[string]interface{}{
			{
				"actionType": "Submit",
				"contextId":  "001000000000001",
				"comments":   "Please approve",
			},
		},
	})
	w, c := setupAuthenticatedContext("POST", "/process/approvals", body)

	handler.HandleProcessApprovals(c)
	assert.Equal(t, http.StatusOK, w.Code)

	var results []rest.ProcessResult
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &results))
	require.Len(t, results, 1)
	assert.True(t, results[0].Success)
	assert.Equal(t, constants.ApprovalStatusPending, results[0].InstanceStatus)
	assert.Equal(t, "001000000000001", results[0].EntityId)
}

func TestApprovalHandler_HandleProcessApprovals_SubmitAutoApprovedResult(t *testing.T) {
	svc := &mockApprovalService{
		submitFn: func(ctx context.Context, req automation.SubmitRequest, user *models.UserSession) (models.SObject, error) {
			return models.SObject{
				"ProcessInstanceId": "04g000000000001",
				"TargetObjectId":    "001000000000001",
				"InstanceStatus":    constants.ApprovalStatusApproved,
				"ActorIds":          []string{},
				"NewWorkitemIds":    []string{},
			}, nil
		},
	}
	handler := rest.NewApprovalHandler(svc)

	body, _ := json.Marshal(map[string]interface{}{
		"requests": []map[string]interface{}{
			{
				"actionType": "Submit",
				"contextId":  "001000000000001",
			},
		},
	})
	w, c := setupAuthenticatedContext("POST", "/process/approvals", body)

	handler.HandleProcessApprovals(c)
	assert.Equal(t, http.StatusOK, w.Code)

	var results []rest.ProcessResult
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &results))
	require.Len(t, results, 1)
	assert.True(t, results[0].Success)
	assert.Equal(t, constants.ApprovalStatusApproved, results[0].InstanceStatus)
	assert.Equal(t, "04g000000000001", results[0].InstanceId)
	assert.Equal(t, "001000000000001", results[0].EntityId)
	assert.Empty(t, results[0].ActorIds)
	assert.Empty(t, results[0].NewWorkitemIds)
}

// ---------------------------------------------------------------------------
// HandleProcessApprovals — Submit with missing contextId
// ---------------------------------------------------------------------------

func TestApprovalHandler_HandleProcessApprovals_SubmitMissingContextId(t *testing.T) {
	handler := rest.NewApprovalHandler(&mockApprovalService{})
	body, _ := json.Marshal(map[string]interface{}{
		"requests": []map[string]interface{}{
			{
				"actionType": "Submit",
				"contextId":  "",
			},
		},
	})
	w, c := setupAuthenticatedContext("POST", "/process/approvals", body)

	handler.HandleProcessApprovals(c)
	assert.Equal(t, http.StatusOK, w.Code)

	var results []rest.ProcessResult
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &results))
	require.Len(t, results, 1)
	assert.False(t, results[0].Success)
	assert.Contains(t, results[0].Errors[0], "Missing contextId")
}

// ---------------------------------------------------------------------------
// HandleProcessApprovals — Submit with known error messages
// ---------------------------------------------------------------------------

func TestApprovalHandler_HandleProcessApprovals_SubmitNoActiveProcess(t *testing.T) {
	svc := &mockApprovalService{
		submitFn: func(ctx context.Context, req automation.SubmitRequest, user *models.UserSession) (models.SObject, error) {
			return nil, errors.New("no active approval process found for this object")
		},
	}
	handler := rest.NewApprovalHandler(svc)

	body, _ := json.Marshal(map[string]interface{}{
		"requests": []map[string]interface{}{
			{"actionType": "Submit", "contextId": "001000000000001"},
		},
	})
	w, c := setupAuthenticatedContext("POST", "/process/approvals", body)

	handler.HandleProcessApprovals(c)
	assert.Equal(t, http.StatusOK, w.Code)

	var results []rest.ProcessResult
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &results))
	assert.False(t, results[0].Success)
	assert.Equal(t, "Error", results[0].InstanceStatus)
}

func TestApprovalHandler_HandleProcessApprovals_SubmitPendingApproval(t *testing.T) {
	svc := &mockApprovalService{
		submitFn: func(ctx context.Context, req automation.SubmitRequest, user *models.UserSession) (models.SObject, error) {
			return nil, errors.New("record already has a pending approval")
		},
	}
	handler := rest.NewApprovalHandler(svc)

	body, _ := json.Marshal(map[string]interface{}{
		"requests": []map[string]interface{}{
			{"actionType": "Submit", "contextId": "001000000000001"},
		},
	})
	w, c := setupAuthenticatedContext("POST", "/process/approvals", body)

	handler.HandleProcessApprovals(c)
	assert.Equal(t, http.StatusOK, w.Code)

	var results []rest.ProcessResult
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &results))
	assert.False(t, results[0].Success)
}

func TestApprovalHandler_GetPendingApprovals(t *testing.T) {
	svc := &mockApprovalService{
		listPendingFn: func(ctx context.Context, user *models.UserSession) ([]automation.PendingApprovalView, error) {
			return []automation.PendingApprovalView{
				{
					WorkItemID:      "wi-1",
					TargetObjectID:  "001000000000001",
					ObjectAPIName:   "Account",
					RecordName:      "Acme Corp",
					SubmittedByName: "Submitter One",
				},
			}, nil
		},
	}
	handler := rest.NewApprovalHandler(svc)
	w, c := setupAuthenticatedContext("GET", "/process/approvals/pending", nil)

	handler.GetPendingApprovals(c)
	assert.Equal(t, http.StatusOK, w.Code)

	var payload []automation.PendingApprovalView
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &payload))
	require.Len(t, payload, 1)
	assert.Equal(t, "wi-1", payload[0].WorkItemID)
	assert.Equal(t, "Account", payload[0].ObjectAPIName)
}

func TestApprovalHandler_GetLatestProcessInstance(t *testing.T) {
	svc := &mockApprovalService{
		getLatestInstanceFn: func(ctx context.Context, recordID string, user *models.UserSession) (*automation.ProcessInstanceSummary, error) {
			return &automation.ProcessInstanceSummary{
				ID:                  "pi-1",
				ProcessDefinitionID: "pd-1",
				TargetObjectID:      recordID,
				Status:              constants.ApprovalStatusPending,
				SubmittedByName:     "Submitter One",
				CreatedDate:         "2026-04-23T00:00:00Z",
			}, nil
		},
	}
	handler := rest.NewApprovalHandler(svc)
	w, c := setupAuthenticatedContext("GET", "/process/approvals/records/001000000000001/instance", nil)
	c.Params = gin.Params{{Key: "recordId", Value: "001000000000001"}}

	handler.GetLatestProcessInstance(c)
	assert.Equal(t, http.StatusOK, w.Code)

	var payload automation.ProcessInstanceSummary
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &payload))
	assert.Equal(t, "pi-1", payload.ID)
	assert.Equal(t, "001000000000001", payload.TargetObjectID)
}

func TestApprovalHandler_HandleProcessApprovals_SubmitPermissionDenied(t *testing.T) {
	svc := &mockApprovalService{
		submitFn: func(ctx context.Context, req automation.SubmitRequest, user *models.UserSession) (models.SObject, error) {
			return nil, errors.New("you don't have permission to submit this record for approval")
		},
	}
	handler := rest.NewApprovalHandler(svc)

	body, _ := json.Marshal(map[string]interface{}{
		"requests": []map[string]interface{}{
			{"actionType": "Submit", "contextId": "001000000000001"},
		},
	})
	w, c := setupAuthenticatedContext("POST", "/process/approvals", body)

	handler.HandleProcessApprovals(c)
	assert.Equal(t, http.StatusOK, w.Code)

	var results []rest.ProcessResult
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &results))
	assert.False(t, results[0].Success)
}

// ---------------------------------------------------------------------------
// HandleProcessApprovals — Approve action
// ---------------------------------------------------------------------------

func TestApprovalHandler_HandleProcessApprovals_ApproveSuccess(t *testing.T) {
	svc := &mockApprovalService{
		approveFn: func(ctx context.Context, workItemID, comments string, user *models.UserSession) (models.SObject, error) {
			return models.SObject{
				constants.FieldID: "04i000000000002",
			}, nil
		},
	}
	handler := rest.NewApprovalHandler(svc)

	body, _ := json.Marshal(map[string]interface{}{
		"requests": []map[string]interface{}{
			{"actionType": "Approve", "contextId": "04i000000000001", "comments": "Looks good"},
		},
	})
	w, c := setupAuthenticatedContext("POST", "/process/approvals", body)

	handler.HandleProcessApprovals(c)
	assert.Equal(t, http.StatusOK, w.Code)

	var results []rest.ProcessResult
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &results))
	assert.True(t, results[0].Success)
	assert.Equal(t, constants.ApprovalStatusApproved, results[0].InstanceStatus)
}

func TestApprovalHandler_HandleProcessApprovals_ApproveMissingContextId(t *testing.T) {
	handler := rest.NewApprovalHandler(&mockApprovalService{})
	body, _ := json.Marshal(map[string]interface{}{
		"requests": []map[string]interface{}{
			{"actionType": "Approve", "contextId": ""},
		},
	})
	w, c := setupAuthenticatedContext("POST", "/process/approvals", body)

	handler.HandleProcessApprovals(c)
	assert.Equal(t, http.StatusOK, w.Code)

	var results []rest.ProcessResult
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &results))
	assert.False(t, results[0].Success)
	assert.Contains(t, results[0].Errors[0], "Missing contextId")
}

func TestApprovalHandler_HandleProcessApprovals_ApproveError(t *testing.T) {
	svc := &mockApprovalService{
		approveFn: func(ctx context.Context, workItemID, comments string, user *models.UserSession) (models.SObject, error) {
			return nil, errors.New("work item not found")
		},
	}
	handler := rest.NewApprovalHandler(svc)

	body, _ := json.Marshal(map[string]interface{}{
		"requests": []map[string]interface{}{
			{"actionType": "Approve", "contextId": "04i000000000001"},
		},
	})
	w, c := setupAuthenticatedContext("POST", "/process/approvals", body)

	handler.HandleProcessApprovals(c)
	assert.Equal(t, http.StatusOK, w.Code)

	var results []rest.ProcessResult
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &results))
	assert.False(t, results[0].Success)
	assert.Equal(t, "Error", results[0].InstanceStatus)
}

// ---------------------------------------------------------------------------
// HandleProcessApprovals — Reject action
// ---------------------------------------------------------------------------

func TestApprovalHandler_HandleProcessApprovals_RejectSuccess(t *testing.T) {
	svc := &mockApprovalService{
		rejectFn: func(ctx context.Context, workItemID, comments string, user *models.UserSession) (models.SObject, error) {
			return models.SObject{
				constants.FieldID: "04i000000000003",
			}, nil
		},
	}
	handler := rest.NewApprovalHandler(svc)

	body, _ := json.Marshal(map[string]interface{}{
		"requests": []map[string]interface{}{
			{"actionType": "Reject", "contextId": "04i000000000001", "comments": "Needs revision"},
		},
	})
	w, c := setupAuthenticatedContext("POST", "/process/approvals", body)

	handler.HandleProcessApprovals(c)
	assert.Equal(t, http.StatusOK, w.Code)

	var results []rest.ProcessResult
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &results))
	assert.True(t, results[0].Success)
	assert.Equal(t, constants.ApprovalStatusRejected, results[0].InstanceStatus)
}

func TestApprovalHandler_HandleProcessApprovals_RejectMissingContextId(t *testing.T) {
	handler := rest.NewApprovalHandler(&mockApprovalService{})
	body, _ := json.Marshal(map[string]interface{}{
		"requests": []map[string]interface{}{
			{"actionType": "Reject", "contextId": ""},
		},
	})
	w, c := setupAuthenticatedContext("POST", "/process/approvals", body)

	handler.HandleProcessApprovals(c)
	assert.Equal(t, http.StatusOK, w.Code)

	var results []rest.ProcessResult
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &results))
	assert.False(t, results[0].Success)
	assert.Contains(t, results[0].Errors[0], "Missing contextId")
}

func TestApprovalHandler_HandleProcessApprovals_RejectError(t *testing.T) {
	svc := &mockApprovalService{
		rejectFn: func(ctx context.Context, workItemID, comments string, user *models.UserSession) (models.SObject, error) {
			return nil, errors.New("reject failed")
		},
	}
	handler := rest.NewApprovalHandler(svc)

	body, _ := json.Marshal(map[string]interface{}{
		"requests": []map[string]interface{}{
			{"actionType": "Reject", "contextId": "04i000000000001"},
		},
	})
	w, c := setupAuthenticatedContext("POST", "/process/approvals", body)

	handler.HandleProcessApprovals(c)
	assert.Equal(t, http.StatusOK, w.Code)

	var results []rest.ProcessResult
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &results))
	assert.False(t, results[0].Success)
}

// ---------------------------------------------------------------------------
// HandleProcessApprovals — unsupported actionType
// ---------------------------------------------------------------------------

func TestApprovalHandler_HandleProcessApprovals_UnsupportedAction(t *testing.T) {
	handler := rest.NewApprovalHandler(&mockApprovalService{})
	body, _ := json.Marshal(map[string]interface{}{
		"requests": []map[string]interface{}{
			{"actionType": "Unknown", "contextId": "001000000000001"},
		},
	})
	w, c := setupAuthenticatedContext("POST", "/process/approvals", body)

	handler.HandleProcessApprovals(c)
	assert.Equal(t, http.StatusOK, w.Code)

	var results []rest.ProcessResult
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &results))
	assert.False(t, results[0].Success)
	assert.Contains(t, results[0].Errors[0], "Unsupported actionType")
}

// ---------------------------------------------------------------------------
// HandleProcessApprovals — default actionType to Submit
// ---------------------------------------------------------------------------

func TestApprovalHandler_HandleProcessApprovals_DefaultActionTypeSubmit(t *testing.T) {
	submitCalled := false
	svc := &mockApprovalService{
		submitFn: func(ctx context.Context, req automation.SubmitRequest, user *models.UserSession) (models.SObject, error) {
			submitCalled = true
			return models.SObject{constants.FieldID: "04i000000000001"}, nil
		},
	}
	handler := rest.NewApprovalHandler(svc)

	// No actionType specified — should default to Submit
	body, _ := json.Marshal(map[string]interface{}{
		"requests": []map[string]interface{}{
			{"contextId": "001000000000001"},
		},
	})
	w, c := setupAuthenticatedContext("POST", "/process/approvals", body)

	handler.HandleProcessApprovals(c)
	assert.Equal(t, http.StatusOK, w.Code)
	assert.True(t, submitCalled)
}

// ---------------------------------------------------------------------------
// HandleProcessApprovals — legacy flat format
// ---------------------------------------------------------------------------

func TestApprovalHandler_HandleProcessApprovals_LegacyFormat(t *testing.T) {
	submitCalled := false
	svc := &mockApprovalService{
		submitFn: func(ctx context.Context, req automation.SubmitRequest, user *models.UserSession) (models.SObject, error) {
			submitCalled = true
			assert.Equal(t, "001000000000001", req.RecordID)
			return models.SObject{constants.FieldID: "04i000000000001"}, nil
		},
	}
	handler := rest.NewApprovalHandler(svc)

	// Legacy flat format (no "requests" key)
	body, _ := json.Marshal(map[string]interface{}{
		"actionType": "Submit",
		"contextId":  "001000000000001",
		"comments":   "legacy format",
	})
	w, c := setupAuthenticatedContext("POST", "/process/approvals", body)

	handler.HandleProcessApprovals(c)
	assert.Equal(t, http.StatusOK, w.Code)
	assert.True(t, submitCalled)
}

func TestApprovalHandler_HandleProcessApprovals_LegacyFormatUsesRecordID(t *testing.T) {
	svc := &mockApprovalService{
		submitFn: func(ctx context.Context, req automation.SubmitRequest, user *models.UserSession) (models.SObject, error) {
			assert.Equal(t, "001RECORD000001", req.RecordID)
			return models.SObject{constants.FieldID: "04i000000000001"}, nil
		},
	}
	handler := rest.NewApprovalHandler(svc)

	// Legacy: contextId is empty, but RecordId is provided
	body, _ := json.Marshal(map[string]interface{}{
		"actionType": "Submit",
		"RecordId":   "001RECORD000001",
	})
	w, c := setupAuthenticatedContext("POST", "/process/approvals", body)

	handler.HandleProcessApprovals(c)
	assert.Equal(t, http.StatusOK, w.Code)
}

// ---------------------------------------------------------------------------
// HandleProcessApprovals — multiple requests (batch)
// ---------------------------------------------------------------------------

func TestApprovalHandler_HandleProcessApprovals_MultipleBatch(t *testing.T) {
	svc := &mockApprovalService{
		submitFn: func(ctx context.Context, req automation.SubmitRequest, user *models.UserSession) (models.SObject, error) {
			return models.SObject{constants.FieldID: "04i000000000001"}, nil
		},
		approveFn: func(ctx context.Context, workItemID, comments string, user *models.UserSession) (models.SObject, error) {
			return models.SObject{constants.FieldID: "04i000000000002"}, nil
		},
	}
	handler := rest.NewApprovalHandler(svc)

	body, _ := json.Marshal(map[string]interface{}{
		"requests": []map[string]interface{}{
			{"actionType": "Submit", "contextId": "001000000000001"},
			{"actionType": "Approve", "contextId": "04i000000000099"},
		},
	})
	w, c := setupAuthenticatedContext("POST", "/process/approvals", body)

	handler.HandleProcessApprovals(c)
	assert.Equal(t, http.StatusOK, w.Code)

	var results []rest.ProcessResult
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &results))
	require.Len(t, results, 2)
	assert.True(t, results[0].Success)
	assert.True(t, results[1].Success)
}

// ---------------------------------------------------------------------------
// GetWorkflowRules — authenticated
// ---------------------------------------------------------------------------

func TestApprovalHandler_GetWorkflowRules_Success(t *testing.T) {
	handler := rest.NewApprovalHandler(&mockApprovalService{})
	w, c := setupAuthenticatedContext("GET", "/process/rules", nil)

	handler.GetWorkflowRules(c)
	assert.Equal(t, http.StatusOK, w.Code)

	var result map[string]interface{}
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &result))
	assert.Contains(t, result, "rules")
}

func TestApprovalHandler_GetWorkflowRules_Unauthenticated(t *testing.T) {
	handler := rest.NewApprovalHandler(&mockApprovalService{})
	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Request = httptest.NewRequest("GET", "/process/rules", nil)

	assert.Panics(t, func() { handler.GetWorkflowRules(c) })
}

// ---------------------------------------------------------------------------
// GetWorkflowRulesForObject — authenticated
// ---------------------------------------------------------------------------

func TestApprovalHandler_GetWorkflowRulesForObject_Success(t *testing.T) {
	handler := rest.NewApprovalHandler(&mockApprovalService{})
	w, c := setupAuthenticatedContext("GET", "/process/rules/Account", nil)
	c.Params = gin.Params{{Key: "sObject", Value: "Account"}}

	handler.GetWorkflowRulesForObject(c)
	assert.Equal(t, http.StatusOK, w.Code)

	var result map[string]interface{}
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &result))
	assert.Contains(t, result, "rules")
}

func TestApprovalHandler_GetWorkflowRulesForObject_Unauthenticated(t *testing.T) {
	handler := rest.NewApprovalHandler(&mockApprovalService{})
	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Request = httptest.NewRequest("GET", "/process/rules/Account", nil)
	c.Params = gin.Params{{Key: "sObject", Value: "Account"}}

	assert.Panics(t, func() { handler.GetWorkflowRulesForObject(c) })
}

func TestApprovalHandler_GetWorkflowRulesForObject_EmptySObject(t *testing.T) {
	handler := rest.NewApprovalHandler(&mockApprovalService{})
	w, c := setupAuthenticatedContext("GET", "/process/rules/", nil)
	c.Params = gin.Params{{Key: "sObject", Value: ""}}

	handler.GetWorkflowRulesForObject(c)
	assert.Equal(t, http.StatusBadRequest, w.Code)
}

// ---------------------------------------------------------------------------
// HandleProcessApprovals — Submit with internal error
// ---------------------------------------------------------------------------

func TestApprovalHandler_HandleProcessApprovals_SubmitInternalError(t *testing.T) {
	svc := &mockApprovalService{
		submitFn: func(ctx context.Context, req automation.SubmitRequest, user *models.UserSession) (models.SObject, error) {
			return nil, errors.New("database connection failed")
		},
	}
	handler := rest.NewApprovalHandler(svc)

	body, _ := json.Marshal(map[string]interface{}{
		"requests": []map[string]interface{}{
			{"actionType": "Submit", "contextId": "001000000000001"},
		},
	})
	w, c := setupAuthenticatedContext("POST", "/process/approvals", body)

	handler.HandleProcessApprovals(c)
	// Internal errors should result in an error response (500 via RespondAppError)
	assert.True(t, w.Code == http.StatusOK || w.Code == http.StatusInternalServerError)
}

func TestApprovalHandler_CloneApprovalProcess_Success(t *testing.T) {
	svc := &mockApprovalService{
		cloneFn: func(ctx context.Context, processDefID, newDeveloperName, newLabel string, user *models.UserSession) (*models.SObject, error) {
			assert.Equal(t, "proc-001", processDefID)
			assert.Equal(t, "Lead_Approval_Clone", newDeveloperName)
			assert.Equal(t, "Lead Approval Clone", newLabel)
			assert.Equal(t, "user-001", user.ID)
			record := models.SObject{constants.FieldID: "proc-002", "Name": "Lead Approval Clone"}
			return &record, nil
		},
	}
	handler := rest.NewApprovalHandler(svc)

	body, _ := json.Marshal(map[string]string{
		"processDefinitionId": "proc-001",
		"newDeveloperName":    "Lead_Approval_Clone",
		"newLabel":            "Lead Approval Clone",
	})
	w, c := setupAuthenticatedContext("POST", "/process/approvals/clone", body)

	handler.CloneApprovalProcess(c)

	assert.Equal(t, http.StatusOK, w.Code)
	var resp map[string]interface{}
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
	assert.Equal(t, "proc-002", resp["Id"])
	assert.Equal(t, "Lead Approval Clone", resp["Name"])
}

func TestApprovalHandler_CloneApprovalProcess_MissingFields(t *testing.T) {
	handler := rest.NewApprovalHandler(&mockApprovalService{})

	body, _ := json.Marshal(map[string]string{
		"processDefinitionId": "proc-001",
		"newDeveloperName":    "",
		"newLabel":            "Lead Approval Clone",
	})
	w, c := setupAuthenticatedContext("POST", "/process/approvals/clone", body)

	handler.CloneApprovalProcess(c)

	assert.Equal(t, http.StatusBadRequest, w.Code)
	assert.Contains(t, w.Body.String(), "NewDeveloperName is required")
}

func TestApprovalHandler_CloneApprovalProcess_ServiceErrorAppendsContextError(t *testing.T) {
	svc := &mockApprovalService{
		cloneFn: func(ctx context.Context, processDefID, newDeveloperName, newLabel string, user *models.UserSession) (*models.SObject, error) {
			return nil, errors.New("clone failed")
		},
	}
	handler := rest.NewApprovalHandler(svc)

	body, _ := json.Marshal(map[string]string{
		"processDefinitionId": "proc-001",
		"newDeveloperName":    "Lead_Approval_Clone",
		"newLabel":            "Lead Approval Clone",
	})
	w, c := setupAuthenticatedContext("POST", "/process/approvals/clone", body)

	handler.CloneApprovalProcess(c)

	assert.Equal(t, http.StatusOK, w.Code)
	require.Len(t, c.Errors, 1)
	assert.Contains(t, c.Errors.Last().Err.Error(), "clone failed")
}

// ---------------------------------------------------------------------------
// GetProjectedChain — issue #1946 pre-evaluation endpoint
// ---------------------------------------------------------------------------

func TestApprovalHandler_GetProjectedChain_Unauthenticated(t *testing.T) {
	handler := rest.NewApprovalHandler(&mockApprovalService{})
	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Request = httptest.NewRequest("GET", "/process/projections?processDefId=pd-1&recordId=rec-1", nil)

	assert.Panics(t, func() { handler.GetProjectedChain(c) })
}

func TestApprovalHandler_GetProjectedChain_MissingProcessDefId(t *testing.T) {
	handler := rest.NewApprovalHandler(&mockApprovalService{})
	w, c := setupAuthenticatedContext("GET", "/process/projections?recordId=rec-1", nil)

	handler.GetProjectedChain(c)
	assert.Equal(t, http.StatusBadRequest, w.Code)
}

func TestApprovalHandler_GetProjectedChain_MissingRecordId(t *testing.T) {
	handler := rest.NewApprovalHandler(&mockApprovalService{})
	w, c := setupAuthenticatedContext("GET", "/process/projections?processDefId=pd-1", nil)

	handler.GetProjectedChain(c)
	assert.Equal(t, http.StatusBadRequest, w.Code)
}

func TestApprovalHandler_GetProjectedChain_Success(t *testing.T) {
	// Service returns a 3-step projection — handler must JSON-encode it under
	// the "projections" key in StepOrder so the frontend Map preserves the
	// caller's expected node order.
	svc := &mockApprovalService{
		projectChainFn: func(ctx context.Context, processDefID, objectAPIName, recordID string, user *models.UserSession) ([]automation.ProjectedNode, error) {
			assert.Equal(t, "pd-1", processDefID)
			assert.Equal(t, "Lead", objectAPIName)
			assert.Equal(t, "rec-1", recordID)
			assert.Equal(t, "user-001", user.ID)
			return []automation.ProjectedNode{
				{ProcessNodeID: "n1", Outcome: automation.ProjectedOutcomeWillRun},
				{ProcessNodeID: "n2", Outcome: automation.ProjectedOutcomeWillSkip},
				{ProcessNodeID: "n3", Outcome: automation.ProjectedOutcomeWillRun},
			}, nil
		},
	}
	handler := rest.NewApprovalHandler(svc)
	w, c := setupAuthenticatedContext("GET", "/process/projections?processDefId=pd-1&recordId=rec-1&objectApiName=Lead", nil)

	handler.GetProjectedChain(c)
	assert.Equal(t, http.StatusOK, w.Code)

	var body struct {
		Projections []automation.ProjectedNode `json:"projections"`
	}
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
	require.Len(t, body.Projections, 3)
	assert.Equal(t, "n1", body.Projections[0].ProcessNodeID)
	assert.Equal(t, automation.ProjectedOutcomeWillRun, body.Projections[0].Outcome)
	assert.Equal(t, "n2", body.Projections[1].ProcessNodeID)
	assert.Equal(t, automation.ProjectedOutcomeWillSkip, body.Projections[1].Outcome)
	assert.Equal(t, "n3", body.Projections[2].ProcessNodeID)
	assert.Equal(t, automation.ProjectedOutcomeWillRun, body.Projections[2].Outcome)
}

func TestApprovalHandler_GetProjectedChain_OptionalObjectApiName(t *testing.T) {
	// objectApiName is optional — the service falls back to ProcessDefinition.
	// Handler must pass empty string through, not reject the request.
	svc := &mockApprovalService{
		projectChainFn: func(ctx context.Context, processDefID, objectAPIName, recordID string, user *models.UserSession) ([]automation.ProjectedNode, error) {
			assert.Equal(t, "", objectAPIName, "missing query param should arrive as empty string")
			return []automation.ProjectedNode{}, nil
		},
	}
	handler := rest.NewApprovalHandler(svc)
	w, c := setupAuthenticatedContext("GET", "/process/projections?processDefId=pd-1&recordId=rec-1", nil)

	handler.GetProjectedChain(c)
	assert.Equal(t, http.StatusOK, w.Code)
}

func TestApprovalHandler_GetProjectedChain_ServiceError(t *testing.T) {
	// Service errors must surface via RespondAppError — non-2xx response, error
	// captured on the gin context for the framework's logging middleware.
	svc := &mockApprovalService{
		projectChainFn: func(ctx context.Context, processDefID, objectAPIName, recordID string, user *models.UserSession) ([]automation.ProjectedNode, error) {
			return nil, errors.New("metadata load failed")
		},
	}
	handler := rest.NewApprovalHandler(svc)
	_, c := setupAuthenticatedContext("GET", "/process/projections?processDefId=pd-1&recordId=rec-1", nil)

	handler.GetProjectedChain(c)
	// RespondAppError captures the error on the gin context for logging
	// middleware rather than mutating the response code; matches the
	// CloneApprovalProcess error-path pattern.
	require.Len(t, c.Errors, 1)
	assert.Contains(t, c.Errors.Last().Err.Error(), "metadata load failed")
}
