package automation

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"testing"

	apperrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/shared/pkg/api"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/nexuscrm/shared/pkg/queryspec"
)

type approvalFailClosedQueryStub struct {
	users         map[string]models.SObject
	groups        map[string]models.SObject
	roles         map[string]models.SObject
	records       map[string]models.SObject
	processNodes  []models.SObject
	groupMembers  map[string][]string
	queueSobjects map[string]map[string]bool
	queryErrors   map[string]error
}

func (s *approvalFailClosedQueryStub) QueryWithFilter(ctx context.Context, objectName, filter string, user *models.UserSession, sortField, sortDir string, limit int) (*models.QueryResult, error) {
	if err := s.queryErrors[approvalFailClosedQueryErrorKey(objectName, filter)]; err != nil {
		return nil, err
	}
	switch objectName {
	case constants.TableUser:
		if roleID := quotedFilterValue(filter, constants.FieldUserRoleID); roleID != "" {
			var records []models.SObject
			for _, userRecord := range s.users {
				if approvalStringValue(userRecord[constants.FieldUserRoleID]) == roleID {
					records = append(records, userRecord)
				}
			}
			return approvalFailClosedQueryResult(records, limit), nil
		}
		for _, userRecord := range s.users {
			id := approvalStringValue(userRecord[constants.FieldID])
			username := approvalStringValue(userRecord["Username"])
			if filter == fmt.Sprintf("Id == '%s'", id) || filter == fmt.Sprintf("Username == '%s'", username) {
				return approvalFailClosedQueryResult([]models.SObject{userRecord}, limit), nil
			}
		}
		return approvalFailClosedQueryResult(nil, limit), nil
	case constants.TableGroup:
		if groupID := quotedFilterValue(filter, constants.FieldID); groupID != "" {
			if record := s.groups[groupID]; record != nil {
				return approvalFailClosedQueryResult([]models.SObject{record}, limit), nil
			}
			return approvalFailClosedQueryResult(nil, limit), nil
		}
		for _, groupRecord := range s.groups {
			developerName := approvalStringValue(groupRecord[constants.FieldGroup_DeveloperName])
			name := approvalStringValue(groupRecord[constants.FieldGroup_Name])
			if developerName != "" && filter == fmt.Sprintf("%s == '%s'", constants.FieldGroup_DeveloperName, developerName) {
				return approvalFailClosedQueryResult([]models.SObject{groupRecord}, limit), nil
			}
			if name != "" && filter == fmt.Sprintf("%s == '%s'", constants.FieldGroup_Name, name) {
				return approvalFailClosedQueryResult([]models.SObject{groupRecord}, limit), nil
			}
		}
		return approvalFailClosedQueryResult(nil, limit), nil
	case constants.TableQueueSobject:
		queueID := quotedFilterValue(filter, constants.FieldQueueSobject_QueueID)
		objectName := quotedFilterValue(filter, constants.FieldQueueSobject_SobjectType)
		if queueID == "" || objectName == "" || !s.queueSobjects[queueID][objectName] {
			return approvalFailClosedQueryResult(nil, limit), nil
		}
		return approvalFailClosedQueryResult([]models.SObject{{
			constants.FieldID:                       queueID + "/" + objectName,
			constants.FieldQueueSobject_QueueID:     queueID,
			constants.FieldQueueSobject_SobjectType: objectName,
		}}, limit), nil
	case constants.TableGroupMembershipClosure:
		groupID := quotedFilterValue(filter, constants.FieldSysGroupMembershipClosure_GroupID)
		memberType := quotedFilterValue(filter, constants.FieldSysGroupMembershipClosure_MemberType)
		if groupID == "" || memberType != constants.GroupMembershipClosureMemberTypeUser {
			return approvalFailClosedQueryResult(nil, limit), nil
		}
		members := s.groupMembers[groupID]
		records := make([]models.SObject, 0, len(members))
		for _, memberID := range members {
			records = append(records, models.SObject{constants.FieldSysGroupMembershipClosure_MemberID: memberID})
		}
		return approvalFailClosedQueryResult(records, limit), nil
	case constants.TableUserRole:
		if roleID := quotedFilterValue(filter, constants.FieldID); roleID != "" {
			if record := s.roles[roleID]; record != nil {
				return approvalFailClosedQueryResult([]models.SObject{record}, limit), nil
			}
			return approvalFailClosedQueryResult(nil, limit), nil
		}
		for _, roleRecord := range s.roles {
			name := approvalStringValue(roleRecord[constants.FieldUserRole_Name])
			if name != "" && filter == fmt.Sprintf("%s == '%s'", constants.FieldUserRole_Name, name) {
				return approvalFailClosedQueryResult([]models.SObject{roleRecord}, limit), nil
			}
		}
		return approvalFailClosedQueryResult(nil, limit), nil
	case constants.TableProcessNode:
		return approvalFailClosedQueryResult(s.processNodes, limit), nil
	default:
		id := quotedFilterValue(filter, constants.FieldID)
		if id == "" {
			return approvalFailClosedQueryResult(nil, limit), nil
		}
		if record := s.records[objectName+"/"+id]; record != nil {
			return approvalFailClosedQueryResult([]models.SObject{record}, limit), nil
		}
		return approvalFailClosedQueryResult(nil, limit), nil
	}
}

func approvalFailClosedQueryResult(records []models.SObject, limit int) *models.QueryResult {
	totalSize := len(records)
	done := true
	if limit > 0 && len(records) > limit {
		records = records[:limit]
		done = false
	}
	return &models.QueryResult{
		TotalSize: totalSize,
		Done:      done,
		Records:   append([]models.SObject(nil), records...),
	}
}

func approvalFailClosedQueryErrorKey(objectName, filter string) string {
	return objectName + "|" + filter
}

func (s *approvalFailClosedQueryStub) Query(ctx context.Context, req api.QueryRequest, user *models.UserSession) (*models.QueryResult, error) {
	panic("approvalFailClosedQueryStub.Query: not implemented")
}

func (s *approvalFailClosedQueryStub) QueryWithSpec(context.Context, queryspec.QuerySpec, *models.UserSession) (*models.QueryResult, error) {
	panic("approvalFailClosedQueryStub.QueryWithSpec: not implemented")
}

func (s *approvalFailClosedQueryStub) RunAnalytics(ctx context.Context, analyticsQuery models.AnalyticsQuery, currentUser *models.UserSession) (interface{}, error) {
	panic("approvalFailClosedQueryStub.RunAnalytics: not implemented")
}

func (s *approvalFailClosedQueryStub) SearchSingleObject(ctx context.Context, objectName, term string, searchFields []string, returnFields []string, limit int, offset int, orderBy string, orderDir string, filterExpr string, currentUser *models.UserSession) ([]models.SObject, error) {
	panic("approvalFailClosedQueryStub.SearchSingleObject: not implemented")
}

func (s *approvalFailClosedQueryStub) SearchSuggestions(context.Context, string, string, []string, []string, int, *models.UserSession) ([]models.SObject, error) {
	return nil, nil
}

func (s *approvalFailClosedQueryStub) GlobalSearch(ctx context.Context, term string, currentUser *models.UserSession) ([]models.SearchResult, error) {
	panic("approvalFailClosedQueryStub.GlobalSearch: not implemented")
}

func (s *approvalFailClosedQueryStub) Calculate(ctx context.Context, objectName string, record models.SObject, user *models.UserSession) (models.SObject, error) {
	panic("approvalFailClosedQueryStub.Calculate: not implemented")
}

func (s *approvalFailClosedQueryStub) ExecuteRawSQL(ctx context.Context, sql string, params []interface{}, user *models.UserSession) ([]models.SObject, error) {
	panic("approvalFailClosedQueryStub.ExecuteRawSQL: not implemented")
}

func quotedFilterValue(filter, field string) string {
	prefix := field + " == '"
	start := strings.Index(filter, prefix)
	if start < 0 {
		prefix = field + " = '"
		start = strings.Index(filter, prefix)
	}
	if start < 0 {
		return ""
	}
	rest := filter[start+len(prefix):]
	idx := strings.Index(rest, "'")
	if idx < 0 {
		return ""
	}
	return rest[:idx]
}

func assertApproverResolutionValidationError(t *testing.T, err error, wantFragments ...string) {
	t.Helper()
	if err == nil {
		t.Fatal("error = nil, want fail-closed approver resolution error")
	}
	if !apperrors.IsValidation(err) {
		t.Fatalf("error = %T %v, want validation error", err, err)
	}
	for _, want := range append([]string{"assignedApprover", "no active approver resolved", "fails closed"}, wantFragments...) {
		if !strings.Contains(err.Error(), want) {
			t.Fatalf("error = %v, want fragment %q", err, want)
		}
	}
}

func approvalCollisionUsers() map[string]models.SObject {
	return map[string]models.SObject{
		"005-inactive": {
			constants.FieldID: "005-inactive",
			"Username":        "inactive@example.test",
			"IsActive":        false,
		},
		"005-active-collision": {
			constants.FieldID: "005-active-collision",
			"Username":        "005-inactive",
			"IsActive":        true,
		},
	}
}

func assertApproverIDs(t *testing.T, got []string, want ...string) {
	t.Helper()
	if len(got) != len(want) {
		t.Fatalf("ids = %v, want %v", got, want)
	}
	seen := make(map[string]int)
	for _, id := range got {
		seen[id]++
	}
	for _, id := range want {
		if seen[id] != 1 {
			t.Fatalf("ids = %v, want exactly one %q", got, id)
		}
	}
}

func approvalPagedUsers(prefix string, count int, roleID string) (map[string]models.SObject, []string) {
	users := make(map[string]models.SObject, count)
	ids := make([]string, 0, count)
	for i := 0; i < count; i++ {
		id := fmt.Sprintf("%s-%04d", prefix, i)
		ids = append(ids, id)
		record := models.SObject{
			constants.FieldID: id,
			"IsActive":        true,
		}
		if roleID != "" {
			record[constants.FieldUserRoleID] = roleID
		}
		users[id] = record
	}
	return users, ids
}

func TestResolveApproverIDsForNodeWithContext_FailsClosedForUnresolvedMetadataApprover(t *testing.T) {
	svc := NewApprovalService(nil, &approvalFailClosedQueryStub{}, nil, nil, nil, nil, nil, nil)
	def := models.SObject{
		constants.FieldID: "pd-1",
		"TableEnumOrId":   "Case",
		"Metadata": `{
			"approvalStep": [{
				"name": "Manager_Review",
				"label": "Manager Review",
				"assignedApprover": {
					"approver": { "type": "user", "name": "missing.approver@example.test" }
				}
			}]
		}`,
	}
	node := models.SObject{
		constants.FieldID: "pn-1",
		"DeveloperName":   "Manager_Review",
		"Name":            "Manager Review",
		"ApproverType":    "user",
		"ApproverUserId":  "005-legacy-fallback",
	}

	ids, err := svc.resolveApproverIDsForNodeWithContext(
		context.Background(),
		def,
		node,
		"500-target",
		&models.UserSession{ID: "005-submit"},
		approvalApproverResolutionContext{submitterID: "005-submit"},
	)

	if ids != nil {
		t.Fatalf("ids = %v, want nil on unresolved authored metadata approver", ids)
	}
	assertApproverResolutionValidationError(t, err, "user", "missing.approver@example.test", "Manager_Review")
}

func TestResolveApproverIDsForNodeWithContext_FailsClosedForInactiveAndDeletedExplicitUsers(t *testing.T) {
	cases := []struct {
		name  string
		ref   string
		users map[string]models.SObject
	}{
		{
			name: "missing",
			ref:  "005-missing",
		},
		{
			name: "inactive",
			ref:  "005-inactive",
			users: map[string]models.SObject{
				"005-inactive": {
					constants.FieldID: "005-inactive",
					"Username":        "inactive@example.test",
					"IsActive":        false,
				},
			},
		},
		{
			name: "deleted",
			ref:  "005-deleted",
			users: map[string]models.SObject{
				"005-deleted": {
					constants.FieldID: "005-deleted",
					"Username":        "deleted@example.test",
					"IsDeleted":       true,
				},
			},
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			svc := NewApprovalService(nil, &approvalFailClosedQueryStub{users: tc.users}, nil, nil, nil, nil, nil, nil)
			node := models.SObject{
				constants.FieldID: "pn-1",
				"Name":            "Explicit User Review",
				"ApproverType":    "user",
				"ApproverUserId":  tc.ref,
			}

			ids, err := svc.resolveApproverIDsForNodeWithContext(
				context.Background(),
				models.SObject{constants.FieldID: "pd-1", "TableEnumOrId": "Case"},
				node,
				"500-target",
				&models.UserSession{ID: "005-submit"},
				approvalApproverResolutionContext{submitterID: "005-submit"},
			)

			if ids != nil {
				t.Fatalf("ids = %v, want nil", ids)
			}
			assertApproverResolutionValidationError(t, err, "user", tc.ref)
		})
	}
}

func TestResolveApproverIDsForNodeWithContext_FailsClosedForInactiveExactIDBeforeUsernameFallback(t *testing.T) {
	query := &approvalFailClosedQueryStub{users: approvalCollisionUsers()}
	svc := NewApprovalService(nil, query, nil, nil, nil, nil, nil, nil)
	def := models.SObject{
		constants.FieldID: "pd-1",
		"TableEnumOrId":   "Case",
		"Metadata": `{
			"approvalStep": [{
				"name": "Collision_Review",
				"label": "Collision Review",
				"assignedApprover": {
					"approver": { "type": "user", "name": "005-inactive" }
				}
			}]
		}`,
	}
	node := models.SObject{
		constants.FieldID: "pn-collision",
		"DeveloperName":   "Collision_Review",
		"Name":            "Collision Review",
	}

	ids, err := svc.resolveApproverIDsForNodeWithContext(
		context.Background(),
		def,
		node,
		"500-target",
		&models.UserSession{ID: "005-submit"},
		approvalApproverResolutionContext{submitterID: "005-submit"},
	)

	if ids != nil {
		t.Fatalf("ids = %v, want nil instead of active username collision", ids)
	}
	assertApproverResolutionValidationError(t, err, "user", "005-inactive", "Collision_Review")
}

func TestResolveApproverIDsFromNode_FailsClosedForInactiveExactIDBeforeUsernameFallback(t *testing.T) {
	svc := NewApprovalService(nil, &approvalFailClosedQueryStub{users: approvalCollisionUsers()}, nil, nil, nil, nil, nil, nil)
	node := models.SObject{
		constants.FieldID: "pn-collision",
		"Name":            "Collision Review",
		"ApproverType":    "user",
		"ApproverUserId":  "005-inactive",
	}

	ids, err := svc.resolveApproverIDsFromNode(
		context.Background(),
		node,
		"500-target",
		"Case",
		&models.UserSession{ID: "005-submit"},
	)

	if ids != nil {
		t.Fatalf("ids = %v, want nil instead of active username collision", ids)
	}
	assertApproverResolutionValidationError(t, err, "user", "005-inactive", "Collision Review")
}

func TestResolveApproverFromNode_DoesNotRouteInactiveExactIDToUsernameFallback(t *testing.T) {
	svc := NewApprovalService(nil, &approvalFailClosedQueryStub{users: approvalCollisionUsers()}, nil, nil, nil, nil, nil, nil)
	node := models.SObject{
		constants.FieldID: "pn-collision",
		"Name":            "Collision Review",
		"ApproverType":    "user",
		"ApproverUserId":  "005-inactive",
	}

	id, err := svc.resolveApproverFromNode(
		context.Background(),
		node,
		"500-target",
		"Case",
		&models.UserSession{ID: "005-submit"},
	)

	if err != nil {
		t.Fatalf("resolveApproverFromNode() error = %v", err)
	}
	if id != "" {
		t.Fatalf("id = %q, want empty instead of active username collision", id)
	}
}

func TestResolveApproverIDsForNodeWithContext_PropagatesUserLookupError(t *testing.T) {
	lookupErr := errors.New("user lookup unavailable")
	query := &approvalFailClosedQueryStub{
		queryErrors: map[string]error{
			approvalFailClosedQueryErrorKey(constants.TableUser, "Id == '005-transient'"): lookupErr,
		},
	}
	svc := NewApprovalService(nil, query, nil, nil, nil, nil, nil, nil)
	node := models.SObject{
		constants.FieldID: "pn-transient",
		"Name":            "Transient Review",
		"ApproverType":    "user",
		"ApproverUserId":  "005-transient",
	}

	ids, err := svc.resolveApproverIDsForNodeWithContext(
		context.Background(),
		models.SObject{constants.FieldID: "pd-1", "TableEnumOrId": "Case"},
		node,
		"500-target",
		&models.UserSession{ID: "005-submit"},
		approvalApproverResolutionContext{submitterID: "005-submit"},
	)

	if ids != nil {
		t.Fatalf("ids = %v, want nil on lookup error", ids)
	}
	if !errors.Is(err, lookupErr) {
		t.Fatalf("error = %v, want lookup error %v", err, lookupErr)
	}
	if apperrors.IsValidation(err) {
		t.Fatalf("error = %v, want infrastructure error instead of validation", err)
	}
}

func TestResolveApproverIDsForNodeWithContext_ResolvesMetadataQueueApprover(t *testing.T) {
	query := &approvalFailClosedQueryStub{
		users: map[string]models.SObject{
			"005-active": {
				constants.FieldID: "005-active",
				"Username":        "active@example.test",
				"IsActive":        true,
			},
			"005-inactive": {
				constants.FieldID: "005-inactive",
				"Username":        "inactive@example.test",
				"IsActive":        false,
			},
			"005-deleted": {
				constants.FieldID: "005-deleted",
				"Username":        "deleted@example.test",
				"IsDeleted":       true,
			},
		},
		groups: map[string]models.SObject{
			"00G-support": {
				constants.FieldID:                  "00G-support",
				constants.FieldGroup_DeveloperName: "Support_Queue",
				constants.FieldGroup_Name:          "Support Queue",
				constants.FieldGroup_Type:          constants.GroupTypeQueue,
			},
		},
		queueSobjects: map[string]map[string]bool{
			"00G-support": {"Case": true},
		},
		groupMembers: map[string][]string{
			"00G-support": {"005-active", "005-inactive", "005-deleted", "005-missing"},
		},
	}
	svc := NewApprovalService(nil, query, nil, nil, nil, nil, nil, nil)
	def := models.SObject{
		constants.FieldID: "pd-queue",
		"TableEnumOrId":   "Case",
		"Metadata": `{
			"approvalStep": [{
				"name": "Queue_Review",
				"label": "Queue Review",
				"assignedApprover": {
					"approver": { "type": "queue", "name": "Support_Queue" }
				}
			}]
		}`,
	}
	node := models.SObject{
		constants.FieldID: "pn-queue",
		"DeveloperName":   "Queue_Review",
		"Name":            "Queue Review",
	}

	ids, err := svc.resolveApproverIDsForNodeWithContext(
		context.Background(),
		def,
		node,
		"500-target",
		&models.UserSession{ID: "005-submit"},
		approvalApproverResolutionContext{submitterID: "005-submit"},
	)

	if err != nil {
		t.Fatalf("resolveApproverIDsForNodeWithContext() error = %v", err)
	}
	assertApproverIDs(t, ids, "005-active")
}

func TestResolveApproverIDsForNodeWithContext_QueueReadsBeyondFormerMemberCap(t *testing.T) {
	users, memberIDs := approvalPagedUsers("005-queue-member", 1001, "")
	query := &approvalFailClosedQueryStub{
		users: users,
		groups: map[string]models.SObject{
			"00G-large": {
				constants.FieldID:                  "00G-large",
				constants.FieldGroup_DeveloperName: "Large_Queue",
				constants.FieldGroup_Name:          "Large Queue",
				constants.FieldGroup_Type:          constants.GroupTypeQueue,
			},
		},
		queueSobjects: map[string]map[string]bool{
			"00G-large": {"Case": true},
		},
		groupMembers: map[string][]string{
			"00G-large": memberIDs,
		},
	}
	svc := NewApprovalService(nil, query, nil, nil, nil, nil, nil, nil)
	def := models.SObject{
		constants.FieldID: "pd-queue-large",
		"TableEnumOrId":   "Case",
		"Metadata": `{
			"approvalStep": [{
				"name": "Large_Queue_Review",
				"assignedApprover": {
					"approver": { "type": "queue", "name": "Large_Queue" }
				}
			}]
		}`,
	}
	node := models.SObject{constants.FieldID: "pn-queue-large", "DeveloperName": "Large_Queue_Review"}

	ids, err := svc.resolveApproverIDsForNodeWithContext(
		context.Background(),
		def,
		node,
		"500-target",
		&models.UserSession{ID: "005-submit"},
		approvalApproverResolutionContext{submitterID: "005-submit"},
	)

	if err != nil {
		t.Fatalf("resolveApproverIDsForNodeWithContext() error = %v", err)
	}
	assertApproverIDs(t, ids, memberIDs...)
}

func TestResolveApproverIDsForNodeWithContext_FailsClosedForDeletedQueueApprover(t *testing.T) {
	query := &approvalFailClosedQueryStub{
		users: map[string]models.SObject{
			"005-active": {constants.FieldID: "005-active", "IsActive": true},
		},
		groups: map[string]models.SObject{
			"00G-support": {
				constants.FieldID:                  "00G-support",
				constants.FieldGroup_DeveloperName: "Support_Queue",
				constants.FieldGroup_Type:          constants.GroupTypeQueue,
				constants.FieldGroup_IsDeleted:     true,
			},
		},
		queueSobjects: map[string]map[string]bool{
			"00G-support": {"Case": true},
		},
		groupMembers: map[string][]string{
			"00G-support": {"005-active"},
		},
	}
	svc := NewApprovalService(nil, query, nil, nil, nil, nil, nil, nil)
	def := models.SObject{
		constants.FieldID: "pd-queue",
		"TableEnumOrId":   "Case",
		"Metadata": `{
			"approvalStep": [{
				"name": "Queue_Review",
				"assignedApprover": {
					"approver": { "type": "queue", "name": "Support_Queue" }
				}
			}]
		}`,
	}
	node := models.SObject{constants.FieldID: "pn-queue", "DeveloperName": "Queue_Review"}

	ids, err := svc.resolveApproverIDsForNodeWithContext(
		context.Background(),
		def,
		node,
		"500-target",
		&models.UserSession{ID: "005-submit"},
		approvalApproverResolutionContext{submitterID: "005-submit"},
	)

	if ids != nil {
		t.Fatalf("ids = %v, want nil for deleted queue", ids)
	}
	assertApproverResolutionValidationError(t, err, "queue", "Support_Queue", "Queue_Review")
}

func TestResolveApproverIDsForNodeWithContext_FailsClosedForQueueWithoutActiveMembers(t *testing.T) {
	query := &approvalFailClosedQueryStub{
		users: map[string]models.SObject{
			"005-inactive": {constants.FieldID: "005-inactive", "IsActive": false},
			"005-deleted":  {constants.FieldID: "005-deleted", "IsDeleted": true},
		},
		groups: map[string]models.SObject{
			"00G-support": {
				constants.FieldID:                  "00G-support",
				constants.FieldGroup_DeveloperName: "Support_Queue",
				constants.FieldGroup_Type:          constants.GroupTypeQueue,
			},
		},
		queueSobjects: map[string]map[string]bool{
			"00G-support": {"Case": true},
		},
		groupMembers: map[string][]string{
			"00G-support": {"005-inactive", "005-deleted", "005-missing"},
		},
	}
	svc := NewApprovalService(nil, query, nil, nil, nil, nil, nil, nil)
	def := models.SObject{
		constants.FieldID: "pd-queue",
		"TableEnumOrId":   "Case",
		"Metadata": `{
			"approvalStep": [{
				"name": "Queue_Review",
				"assignedApprover": {
					"approver": { "type": "queue", "name": "Support_Queue" }
				}
			}]
		}`,
	}
	node := models.SObject{constants.FieldID: "pn-queue", "DeveloperName": "Queue_Review"}

	ids, err := svc.resolveApproverIDsForNodeWithContext(
		context.Background(),
		def,
		node,
		"500-target",
		&models.UserSession{ID: "005-submit"},
		approvalApproverResolutionContext{submitterID: "005-submit"},
	)

	if ids != nil {
		t.Fatalf("ids = %v, want nil for queue without active members", ids)
	}
	assertApproverResolutionValidationError(t, err, "queue", "Support_Queue", "Queue_Review")
}

func TestResolveApproverIDsForNodeWithContext_ResolvesMetadataRoleApprover(t *testing.T) {
	query := &approvalFailClosedQueryStub{
		users: map[string]models.SObject{
			"005-active": {
				constants.FieldID:         "005-active",
				constants.FieldUserRoleID: "00E-reviewers",
				"IsActive":                true,
			},
			"005-inactive": {
				constants.FieldID:         "005-inactive",
				constants.FieldUserRoleID: "00E-reviewers",
				"IsActive":                false,
			},
			"005-other": {
				constants.FieldID:         "005-other",
				constants.FieldUserRoleID: "00E-other",
				"IsActive":                true,
			},
		},
		roles: map[string]models.SObject{
			"00E-reviewers": {
				constants.FieldID:            "00E-reviewers",
				constants.FieldUserRole_Name: "Regional Reviewers",
			},
		},
	}
	svc := NewApprovalService(nil, query, nil, nil, nil, nil, nil, nil)
	def := models.SObject{
		constants.FieldID: "pd-role",
		"TableEnumOrId":   "Case",
		"Metadata": `{
			"approvalStep": [{
				"name": "Role_Review",
				"label": "Role Review",
				"assignedApprover": {
					"approver": { "type": "role", "name": "Regional Reviewers" }
				}
			}]
		}`,
	}
	node := models.SObject{
		constants.FieldID: "pn-role",
		"DeveloperName":   "Role_Review",
		"Name":            "Role Review",
	}

	ids, err := svc.resolveApproverIDsForNodeWithContext(
		context.Background(),
		def,
		node,
		"500-target",
		&models.UserSession{ID: "005-submit"},
		approvalApproverResolutionContext{submitterID: "005-submit"},
	)

	if err != nil {
		t.Fatalf("resolveApproverIDsForNodeWithContext() error = %v", err)
	}
	assertApproverIDs(t, ids, "005-active")
}

func TestResolveApproverIDsForNodeWithContext_RoleReadsBeyondFormerMemberCap(t *testing.T) {
	users, userIDs := approvalPagedUsers("005-role-member", 1001, "00E-large")
	query := &approvalFailClosedQueryStub{
		users: users,
		roles: map[string]models.SObject{
			"00E-large": {
				constants.FieldID:            "00E-large",
				constants.FieldUserRole_Name: "Large Reviewers",
			},
		},
	}
	svc := NewApprovalService(nil, query, nil, nil, nil, nil, nil, nil)
	def := models.SObject{
		constants.FieldID: "pd-role-large",
		"TableEnumOrId":   "Case",
		"Metadata": `{
			"approvalStep": [{
				"name": "Large_Role_Review",
				"assignedApprover": {
					"approver": { "type": "role", "name": "Large Reviewers" }
				}
			}]
		}`,
	}
	node := models.SObject{constants.FieldID: "pn-role-large", "DeveloperName": "Large_Role_Review"}

	ids, err := svc.resolveApproverIDsForNodeWithContext(
		context.Background(),
		def,
		node,
		"500-target",
		&models.UserSession{ID: "005-submit"},
		approvalApproverResolutionContext{submitterID: "005-submit"},
	)

	if err != nil {
		t.Fatalf("resolveApproverIDsForNodeWithContext() error = %v", err)
	}
	assertApproverIDs(t, ids, userIDs...)
}

func TestResolveApproverIDsForNodeWithContext_FailsClosedForDeletedRoleApprover(t *testing.T) {
	query := &approvalFailClosedQueryStub{
		users: map[string]models.SObject{
			"005-active": {constants.FieldID: "005-active", constants.FieldUserRoleID: "00E-reviewers", "IsActive": true},
		},
		roles: map[string]models.SObject{
			"00E-reviewers": {
				constants.FieldID:                 "00E-reviewers",
				constants.FieldUserRole_Name:      "Regional Reviewers",
				constants.FieldUserRole_IsDeleted: true,
			},
		},
	}
	svc := NewApprovalService(nil, query, nil, nil, nil, nil, nil, nil)
	def := models.SObject{
		constants.FieldID: "pd-role",
		"TableEnumOrId":   "Case",
		"Metadata": `{
			"approvalStep": [{
				"name": "Role_Review",
				"assignedApprover": {
					"approver": { "type": "role", "name": "Regional Reviewers" }
				}
			}]
		}`,
	}
	node := models.SObject{constants.FieldID: "pn-role", "DeveloperName": "Role_Review"}

	ids, err := svc.resolveApproverIDsForNodeWithContext(
		context.Background(),
		def,
		node,
		"500-target",
		&models.UserSession{ID: "005-submit"},
		approvalApproverResolutionContext{submitterID: "005-submit"},
	)

	if ids != nil {
		t.Fatalf("ids = %v, want nil for deleted role", ids)
	}
	assertApproverResolutionValidationError(t, err, "role", "Regional Reviewers", "Role_Review")
}

func TestResolveApproverIDsForNodeWithContext_FailsClosedForRoleWithoutActiveUsers(t *testing.T) {
	query := &approvalFailClosedQueryStub{
		users: map[string]models.SObject{
			"005-inactive": {constants.FieldID: "005-inactive", constants.FieldUserRoleID: "00E-reviewers", "IsActive": false},
			"005-deleted":  {constants.FieldID: "005-deleted", constants.FieldUserRoleID: "00E-reviewers", "IsDeleted": true},
		},
		roles: map[string]models.SObject{
			"00E-reviewers": {
				constants.FieldID:            "00E-reviewers",
				constants.FieldUserRole_Name: "Regional Reviewers",
			},
		},
	}
	svc := NewApprovalService(nil, query, nil, nil, nil, nil, nil, nil)
	def := models.SObject{
		constants.FieldID: "pd-role",
		"TableEnumOrId":   "Case",
		"Metadata": `{
			"approvalStep": [{
				"name": "Role_Review",
				"assignedApprover": {
					"approver": { "type": "role", "name": "Regional Reviewers" }
				}
			}]
		}`,
	}
	node := models.SObject{constants.FieldID: "pn-role", "DeveloperName": "Role_Review"}

	ids, err := svc.resolveApproverIDsForNodeWithContext(
		context.Background(),
		def,
		node,
		"500-target",
		&models.UserSession{ID: "005-submit"},
		approvalApproverResolutionContext{submitterID: "005-submit"},
	)

	if ids != nil {
		t.Fatalf("ids = %v, want nil for role without active users", ids)
	}
	assertApproverResolutionValidationError(t, err, "role", "Regional Reviewers", "Role_Review")
}

func TestResolveApproverIDsForNodeWithContext_FailsClosedForMixedMetadataApproverList(t *testing.T) {
	query := &approvalFailClosedQueryStub{
		users: map[string]models.SObject{
			"005-active": {
				constants.FieldID: "005-active",
				"Username":        "active@example.test",
				"IsActive":        true,
			},
			"005-inactive": {
				constants.FieldID: "005-inactive",
				"Username":        "inactive@example.test",
				"IsActive":        false,
			},
		},
	}
	svc := NewApprovalService(nil, query, nil, nil, nil, nil, nil, nil)
	def := models.SObject{
		constants.FieldID: "pd-1",
		"TableEnumOrId":   "Case",
		"Metadata": `{
			"approvalStep": [{
				"name": "Unanimous_Review",
				"label": "Unanimous Review",
				"assignedApprover": {
					"whenMultipleApprovers": "Unanimous",
					"approver": [
						{ "type": "user", "name": "005-active" },
						{ "type": "user", "name": "005-inactive" }
					]
				}
			}]
		}`,
	}
	node := models.SObject{
		constants.FieldID: "pn-unanimous",
		"DeveloperName":   "Unanimous_Review",
		"Name":            "Unanimous Review",
	}

	ids, err := svc.resolveApproverIDsForNodeWithContext(
		context.Background(),
		def,
		node,
		"500-target",
		&models.UserSession{ID: "005-submit"},
		approvalApproverResolutionContext{submitterID: "005-submit"},
	)

	if ids != nil {
		t.Fatalf("ids = %v, want nil instead of partial active-user list", ids)
	}
	assertApproverResolutionValidationError(t, err, "user", "005-inactive", "Unanimous_Review")
}

func TestResolveApproverIDsForNodeWithContext_FailsClosedForMixedProcessNodeApproverList(t *testing.T) {
	query := &approvalFailClosedQueryStub{
		users: map[string]models.SObject{
			"005-active": {
				constants.FieldID: "005-active",
				"Username":        "active@example.test",
				"IsActive":        true,
			},
			"005-inactive": {
				constants.FieldID: "005-inactive",
				"Username":        "inactive@example.test",
				"IsActive":        false,
			},
		},
	}
	svc := NewApprovalService(nil, query, nil, nil, nil, nil, nil, nil)
	node := models.SObject{
		constants.FieldID: "pn-node-list",
		"Name":            "Node List Review",
		"ApproverType":    "user",
		"ApproverUserId":  "005-active,005-inactive",
	}

	ids, err := svc.resolveApproverIDsForNodeWithContext(
		context.Background(),
		models.SObject{constants.FieldID: "pd-1", "TableEnumOrId": "Case"},
		node,
		"500-target",
		&models.UserSession{ID: "005-submit"},
		approvalApproverResolutionContext{submitterID: "005-submit"},
	)

	if ids != nil {
		t.Fatalf("ids = %v, want nil instead of partial active-user list", ids)
	}
	assertApproverResolutionValidationError(t, err, "user", "005-inactive", "Node List Review")
}

func TestResolveApproverIDsForNodeWithContext_FailsClosedForEmptyRelatedUserField(t *testing.T) {
	query := &approvalFailClosedQueryStub{
		records: map[string]models.SObject{
			"Case/500-target": {
				constants.FieldID: "500-target",
				"ManagerId":       "",
			},
		},
	}
	svc := NewApprovalService(nil, query, nil, nil, nil, nil, nil, nil)
	node := models.SObject{
		constants.FieldID:      "pn-related",
		"Name":                 "Related User Review",
		"ApproverType":         "relatedUserField",
		"ApproverRelatedField": "ManagerId",
	}

	ids, err := svc.resolveApproverIDsForNodeWithContext(
		context.Background(),
		models.SObject{constants.FieldID: "pd-1", "TableEnumOrId": "Case"},
		node,
		"500-target",
		&models.UserSession{ID: "005-submit"},
		approvalApproverResolutionContext{submitterID: "005-submit"},
	)

	if ids != nil {
		t.Fatalf("ids = %v, want nil", ids)
	}
	assertApproverResolutionValidationError(t, err, "relatedUserField", "ManagerId")
}

func TestSelectInitialApprovalStep_ResolvesProcessNodeApproverRelatedField(t *testing.T) {
	query := &approvalFailClosedQueryStub{
		records: map[string]models.SObject{
			"Case/500-target": {
				constants.FieldID: "500-target",
				"ManagerId":       "005-manager",
			},
		},
	}
	svc := NewApprovalService(nil, query, nil, nil, nil, nil, nil, nil)
	node := models.SObject{
		constants.FieldID:      "pn-related",
		"Name":                 "Related User Review",
		"ApproverType":         "relatedUserField",
		"ApproverRelatedField": "ManagerId",
	}

	selected, actorIDs, initialStatus, err := svc.selectInitialApprovalStep(
		context.Background(),
		models.SObject{constants.FieldID: "pd-1", "TableEnumOrId": "Case"},
		[]models.SObject{node},
		"Case",
		"500-target",
		"005-submit",
		&models.UserSession{ID: "005-submit"},
	)

	if err != nil {
		t.Fatalf("selectInitialApprovalStep() error = %v", err)
	}
	if selected == nil || initialStatus != constants.ApprovalStatusPending {
		t.Fatalf("selected=%v initialStatus=%v, want related-user step to start pending", selected, initialStatus)
	}
	if len(actorIDs) != 1 || actorIDs[0] != "005-manager" {
		t.Fatalf("actorIDs = %v, want [005-manager]", actorIDs)
	}
}

func TestSelectInitialApprovalStep_FailsClosedInsteadOfSelfRoutingForUnsupportedQueue(t *testing.T) {
	svc := NewApprovalService(nil, &approvalFailClosedQueryStub{}, nil, nil, nil, nil, nil, nil)
	node := models.SObject{
		constants.FieldID: "pn-queue",
		"Name":            "Queue Review",
		"ApproverType":    "queue",
	}

	selected, actorIDs, initialStatus, err := svc.selectInitialApprovalStep(
		context.Background(),
		models.SObject{constants.FieldID: "pd-1", "TableEnumOrId": "Case"},
		[]models.SObject{node},
		"Case",
		"500-target",
		"005-submit",
		&models.UserSession{ID: "005-submit"},
	)

	if selected != nil || actorIDs != nil || initialStatus != "" {
		t.Fatalf("selected=%v actorIDs=%v initialStatus=%v, want no self-routed approval on fail-closed error", selected, actorIDs, initialStatus)
	}
	assertApproverResolutionValidationError(t, err, "queue", "Queue Review")
}

func TestSelectInitialApprovalStep_FailsClosedForMatchedStepWithoutApprover(t *testing.T) {
	svc := NewApprovalService(nil, &approvalFailClosedQueryStub{}, nil, nil, nil, nil, nil, nil)
	node := models.SObject{
		constants.FieldID: "pn-empty",
		"Name":            "Empty Review",
	}

	selected, actorIDs, initialStatus, err := svc.selectInitialApprovalStep(
		context.Background(),
		models.SObject{constants.FieldID: "pd-1", "TableEnumOrId": "Case"},
		[]models.SObject{node},
		"Case",
		"500-target",
		"005-submit",
		&models.UserSession{ID: "005-submit"},
	)

	if selected != nil || actorIDs != nil || initialStatus != "" {
		t.Fatalf("selected=%v actorIDs=%v initialStatus=%v, want no zero-approver approval", selected, actorIDs, initialStatus)
	}
	assertApproverResolutionValidationError(t, err, "Empty Review")
}

func TestDetermineNextApprovalStep_FailsClosedInsteadOfActorFallbackForMissingUser(t *testing.T) {
	currentNode := models.SObject{
		constants.FieldID: "pn-current",
		"Name":            "Current Review",
		"StepOrder":       1,
		"ApproverType":    "user",
		"ApproverUserId":  "005-current",
	}
	nextNode := models.SObject{
		constants.FieldID: "pn-next",
		"Name":            "Next Review",
		"StepOrder":       2,
		"ApproverType":    "user",
		"ApproverUserId":  "005-missing-next",
	}
	query := &approvalFailClosedQueryStub{
		processNodes: []models.SObject{currentNode, nextNode},
	}
	svc := NewApprovalService(nil, query, nil, nil, nil, nil, nil, nil)

	transition, err := svc.determineNextApprovalStep(
		context.Background(),
		models.SObject{constants.FieldID: "pd-1", "TableEnumOrId": "Case"},
		currentNode,
		"500-target",
		"005-submit",
		&models.UserSession{ID: "005-acting-approver"},
	)

	if transition.advanced || len(transition.actorIDs) > 0 {
		t.Fatalf("transition = %#v, want no actor fallback on fail-closed error", transition)
	}
	assertApproverResolutionValidationError(t, err, "user", "005-missing-next", "Next Review")
}

func TestDetermineNextApprovalStep_ResolvesProcessNodeApproverRelatedField(t *testing.T) {
	currentNode := models.SObject{
		constants.FieldID: "pn-current",
		"Name":            "Current Review",
		"StepOrder":       1,
		"ApproverType":    "user",
		"ApproverUserId":  "005-current",
	}
	nextNode := models.SObject{
		constants.FieldID:      "pn-next",
		"Name":                 "Related Next Review",
		"StepOrder":            2,
		"ApproverType":         "relatedUserField",
		"ApproverRelatedField": "ManagerId",
	}
	query := &approvalFailClosedQueryStub{
		records: map[string]models.SObject{
			"Case/500-target": {
				constants.FieldID: "500-target",
				"ManagerId":       "005-manager",
			},
		},
		processNodes: []models.SObject{currentNode, nextNode},
	}
	svc := NewApprovalService(nil, query, nil, nil, nil, nil, nil, nil)

	transition, err := svc.determineNextApprovalStep(
		context.Background(),
		models.SObject{constants.FieldID: "pd-1", "TableEnumOrId": "Case"},
		currentNode,
		"500-target",
		"005-submit",
		&models.UserSession{ID: "005-acting-approver"},
	)

	if err != nil {
		t.Fatalf("determineNextApprovalStep() error = %v", err)
	}
	if !transition.advanced {
		t.Fatalf("transition = %#v, want advanced related-user next step", transition)
	}
	if len(transition.actorIDs) != 1 || transition.actorIDs[0] != "005-manager" {
		t.Fatalf("transition.actorIDs = %v, want [005-manager]", transition.actorIDs)
	}
}

func TestDetermineRejectBackToPrevious_FailsClosedInsteadOfActorFallbackForMissingUser(t *testing.T) {
	prevNode := models.SObject{
		constants.FieldID: "pn-prev",
		"Name":            "Previous Review",
		"StepOrder":       1,
		"ApproverType":    "user",
		"ApproverUserId":  "005-missing-prev",
	}
	currentNode := models.SObject{
		constants.FieldID: "pn-current",
		"Name":            "Current Review",
		"StepOrder":       2,
		"RejectBehavior":  constants.ApprovalRejectBehaviorBackToPrevious,
	}
	query := &approvalFailClosedQueryStub{
		processNodes: []models.SObject{prevNode, currentNode},
	}
	svc := NewApprovalService(nil, query, nil, nil, nil, nil, nil, nil)

	transition, err := svc.determineRejectBackToPrevious(
		context.Background(),
		models.SObject{constants.FieldID: "pd-1", "TableEnumOrId": "Case"},
		"pi-1",
		currentNode,
		"500-target",
		"005-submit",
		&models.UserSession{ID: "005-acting-approver"},
	)

	if transition.advanced || len(transition.actorIDs) > 0 {
		t.Fatalf("transition = %#v, want no actor fallback on fail-closed error", transition)
	}
	assertApproverResolutionValidationError(t, err, "user", "005-missing-prev", "Previous Review")
}

func TestDetermineRejectBackToPrevious_ResolvesProcessNodeApproverRelatedField(t *testing.T) {
	prevNode := models.SObject{
		constants.FieldID:      "pn-prev",
		"Name":                 "Related Previous Review",
		"StepOrder":            1,
		"ApproverType":         "relatedUserField",
		"ApproverRelatedField": "ManagerId",
	}
	currentNode := models.SObject{
		constants.FieldID: "pn-current",
		"Name":            "Current Review",
		"StepOrder":       2,
		"RejectBehavior":  constants.ApprovalRejectBehaviorBackToPrevious,
	}
	query := &approvalFailClosedQueryStub{
		records: map[string]models.SObject{
			"Case/500-target": {
				constants.FieldID: "500-target",
				"ManagerId":       "005-manager",
			},
		},
		processNodes: []models.SObject{prevNode, currentNode},
	}
	svc := NewApprovalService(nil, query, nil, nil, nil, nil, nil, nil)

	transition, err := svc.determineRejectBackToPrevious(
		context.Background(),
		models.SObject{constants.FieldID: "pd-1", "TableEnumOrId": "Case"},
		"pi-1",
		currentNode,
		"500-target",
		"005-submit",
		&models.UserSession{ID: "005-acting-approver"},
	)

	if err != nil {
		t.Fatalf("determineRejectBackToPrevious() error = %v", err)
	}
	if !transition.advanced {
		t.Fatalf("transition = %#v, want advanced related-user previous step", transition)
	}
	if len(transition.actorIDs) != 1 || transition.actorIDs[0] != "005-manager" {
		t.Fatalf("transition.actorIDs = %v, want [005-manager]", transition.actorIDs)
	}
}
