package dialectsfcrm

import (
	"errors"
	"reflect"
	"strings"
	"testing"

	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/approvalir"
)

func newTestApprovalProcessDefinitionDecoder() approvalir.DefinitionDecoder {
	return NewApprovalProcessDefinitionDecoderWithFormulaValidator(approvalir.FormulaValidatorFunc(func(string) error { return nil }))
}

func TestDecodeApprovalProcessMetadataXMLComprehensive(t *testing.T) {
	metadata, err := DecodeApprovalProcessMetadata(`
<ApprovalProcess>
  <label> Deal Approval </label>
  <active>true</active>
  <description> Approve large deals </description>
	<allowRecall>false</allowRecall>
  <recordEditability>Locked</recordEditability>
  <finalApprovalRecordLock> TRUE </finalApprovalRecordLock>
  <finalRejectionRecordLock> false </finalRejectionRecordLock>
  <entryCriteria>
    <booleanFilter>1 AND 2</booleanFilter>
    <criteriaItems><field>Amount</field><operation>greaterThan</operation><value>1000</value></criteriaItems>
    <criteriaItems><field>StageName</field><operation>equals</operation><value>Review</value></criteriaItems>
  </entryCriteria>
  <allowedSubmitters><type>RecordCreator</type><submitter>ignored</submitter></allowedSubmitters>
  <allowedSubmitters><type>User</type><submitter>submitter@example.com</submitter></allowedSubmitters>
  <nextAutomatedApprover>
    <userHierarchyField>ManagerId</userHierarchyField>
    <useApproverFieldOfRecordOwner>true</useApproverFieldOfRecordOwner>
  </nextAutomatedApprover>
  <approvalPageFields><field>Name</field><field>Amount</field></approvalPageFields>
  <enableMobileDeviceAccess>true</enableMobileDeviceAccess>
  <processOrder>7</processOrder>
  <approvalStep>
    <name>Manager_Review</name>
    <label>Manager Review</label>
    <description>Manager decides</description>
    <allowComments>false</allowComments>
    <requireComments>true</requireComments>
    <allowDelegate>true</allowDelegate>
    <rejectBehavior><type>BackToPrevious</type></rejectBehavior>
    <assignedApprover>
      <approver><type>User</type><name>first@example.com</name></approver>
      <approver><type>RelatedUser</type><name>Owner.ManagerId</name></approver>
      <whenMultipleApprovers>Unanimous</whenMultipleApprovers>
    </assignedApprover>
    <stepCriteria><formula>Amount &gt; 500</formula></stepCriteria>
    <entryCriteria>
      <criteriaItems><field>Priority</field><operation>equals</operation><value>High</value></criteriaItems>
    </entryCriteria>
    <ifCriteriaNotMet>gotonextstep</ifCriteriaNotMet>
    <stepApprovalActions>
      <actions>
        <name>Mark Approved</name><type>fieldUpdate</type>
        <fieldApiName>Status</fieldApiName><fieldValue>Literal: Approved</fieldValue>
      </actions>
    </stepApprovalActions>
    <stepRejectionActions>
      <actions><name>Reject Flow</name><type>Flow</type><apiName>Reject_Notification</apiName></actions>
    </stepRejectionActions>
  </approvalStep>
  <initialSubmissionActions>
    <actions><name>Submit Flow</name><type>Flow</type><apiName>Submit_Notification</apiName></actions>
  </initialSubmissionActions>
  <finalApprovalActions>
	<action><name>Legacy Singular Flow</name><type>Flow</type><apiName>Legacy_Notification</apiName></action>
    <actions><name>Final Flow</name><type>Flow</type><apiName>Final_Notification</apiName></actions>
  </finalApprovalActions>
</ApprovalProcess>`)
	if err != nil {
		t.Fatalf("DecodeApprovalProcessMetadata returned error: %v", err)
	}
	if metadata == nil {
		t.Fatal("DecodeApprovalProcessMetadata returned nil metadata")
	}
	if metadata.Label != "Deal Approval" || !metadata.Active || metadata.Description != "Approve large deals" {
		t.Fatalf("parent metadata = %+v", metadata)
	}
	if metadata.AllowRecall == nil || *metadata.AllowRecall || metadata.EffectiveAllowRecall() {
		t.Fatalf("allowRecall = explicit:%v effective:%v, want explicit false", metadata.AllowRecall, metadata.EffectiveAllowRecall())
	}
	if metadata.RecordEditability != "Locked" || !metadata.FinalApprovalRecordLock || metadata.FinalRejectionRecordLock {
		t.Fatalf("lock metadata = (%q,%v,%v)", metadata.RecordEditability, metadata.FinalApprovalRecordLock, metadata.FinalRejectionRecordLock)
	}
	if metadata.EntryCriteria == nil || metadata.EntryCriteria.BooleanFilter != "1 AND 2" || len(metadata.EntryCriteria.CriteriaItems) != 2 {
		t.Fatalf("entry criteria = %+v", metadata.EntryCriteria)
	}
	if !metadata.HasExplicitAllowedSubmitters || len(metadata.AllowedSubmitters) != 2 {
		t.Fatalf("allowed submitters = explicit:%v values:%+v", metadata.HasExplicitAllowedSubmitters, metadata.AllowedSubmitters)
	}
	if metadata.AllowedSubmitters[0].Type != constants.ApprovalSubmitterTypeRecordCreator || metadata.AllowedSubmitters[0].Submitter != "" {
		t.Fatalf("record creator submitter = %+v", metadata.AllowedSubmitters[0])
	}
	if metadata.AllowedSubmitters[1].Type != constants.ApprovalSubmitterTypeUser || metadata.AllowedSubmitters[1].Submitter != "submitter@example.com" {
		t.Fatalf("user submitter = %+v", metadata.AllowedSubmitters[1])
	}
	if metadata.NextAutomatedApprover == nil || metadata.NextAutomatedApprover.UserHierarchyField != "ManagerId" || !metadata.NextAutomatedApprover.UseApproverFieldOfRecordOwner {
		t.Fatalf("next automated approver = %+v", metadata.NextAutomatedApprover)
	}
	if !reflect.DeepEqual(metadata.ApprovalPageFields, []string{"Name", "Amount"}) || !metadata.EnableMobileDeviceAccess || metadata.ProcessOrder == nil || *metadata.ProcessOrder != 7 {
		t.Fatalf("page/mobile/order = (%v,%v,%v)", metadata.ApprovalPageFields, metadata.EnableMobileDeviceAccess, metadata.ProcessOrder)
	}
	if len(metadata.Steps) != 1 {
		t.Fatalf("step count = %d, want 1", len(metadata.Steps))
	}
	step := metadata.Steps[0]
	if step.Name != "Manager_Review" || step.Label != "Manager Review" || step.Description != "Manager decides" {
		t.Fatalf("step identity = %+v", step)
	}
	if step.AllowComments == nil || *step.AllowComments || step.RequireComments == nil || !*step.RequireComments || !step.AllowDelegate {
		t.Fatalf("step flags = allowComments:%v requireComments:%v allowDelegate:%v", step.AllowComments, step.RequireComments, step.AllowDelegate)
	}
	if step.RejectBehavior != constants.ApprovalRejectBehaviorBackToPrevious || step.WhenMultipleApprovers != constants.ApprovalModeUnanimous {
		t.Fatalf("step behavior/mode = (%q,%q)", step.RejectBehavior, step.WhenMultipleApprovers)
	}
	if len(step.Approvers) != 2 || step.Approvers[0].Type != constants.ApprovalApproverTypeUser || step.Approvers[1].Type != constants.ApprovalApproverTypeRelatedUserField {
		t.Fatalf("step approvers = %+v", step.Approvers)
	}
	if step.StepCriteria == nil || len(step.StepCriteria.CriteriaItems) != 1 || step.StepCriteria.CriteriaItems[0].Field != "Priority" {
		t.Fatalf("step criteria = %+v, want entryCriteria-preferred Priority", step.StepCriteria)
	}
	if step.IfCriteriaNotMet != "GotoNextStep" {
		t.Fatalf("ifCriteriaNotMet = %q", step.IfCriteriaNotMet)
	}
	if len(step.StepApprovalActions) != 1 || step.StepApprovalActions[0].Type != constants.ProcessActionTypeFieldUpdate || step.StepApprovalActions[0].FieldValue != "Approved" {
		t.Fatalf("step approval actions = %+v", step.StepApprovalActions)
	}
	if len(step.StepRejectionActions) != 1 || step.StepRejectionActions[0].APIName != "Reject_Notification" {
		t.Fatalf("step rejection actions = %+v", step.StepRejectionActions)
	}
	if len(metadata.InitialSubmissionActions) != 1 || metadata.InitialSubmissionActions[0].referenceForm || metadata.InitialSubmissionActions[0].APIName != "Submit_Notification" {
		t.Fatalf("initial submission actions = %+v", metadata.InitialSubmissionActions)
	}
	if len(metadata.FinalApprovalActions) != 2 || !metadata.FinalApprovalActions[0].referenceForm || metadata.FinalApprovalActions[0].APIName != "Legacy_Notification" || metadata.FinalApprovalActions[1].referenceForm || metadata.FinalApprovalActions[1].APIName != "Final_Notification" {
		t.Fatalf("final approval actions = %+v", metadata.FinalApprovalActions)
	}
}

func TestDecodeApprovalProcessMetadataLegacyJSONComprehensive(t *testing.T) {
	metadata, err := DecodeApprovalProcessMetadata(`{
  "label": "Legacy Approval",
  "active": "true",
  "description": "Legacy JSON record",
	"allowRecall": "false",
  "recordEditability": "AdminOnly",
  "finalApprovalRecordLock": true,
  "finalRejectionRecordLock": false,
  "entryCriteria": {"formula": "Amount > 0"},
  "allowedSubmitters": {"type": "owner", "submitter": "ignored"},
  "nextAutomatedApprover": {
    "userHierarchyField": "ManagerId",
    "useApproverFieldOfRecordOwner": "true"
  },
  "approvalPageFields": {"field": "Name"},
  "enableMobileDeviceAccess": "true",
  "processOrder": "9",
  "approvalStep": {
    "name": "Legacy_Step",
    "label": "Legacy Step",
    "description": "JSON step",
    "allowComments": false,
    "requireComments": "true",
    "allowDelegate": "true",
    "rejectBehavior": "RejectRequest",
    "assignedApprover": {
      "approver": {"type": "relatedUserField", "name": "Owner.ManagerId"},
      "whenMultipleApprovers": "unanimous"
    },
    "stepCriteria": {"formula": "Ignored__c = true"},
    "entryCriteria": {
      "booleanFilter": "1",
      "criteriaItems": {"field": "Priority", "operation": "equals", "value": null}
    },
    "ifCriteriaNotMet": "ApproveRecord",
    "stepApprovalActions": {
      "actions": {"name": "Set Status", "type": "FieldUpdate", "fieldApiName": "Status", "fieldValue": "Literal: Done"}
    }
  },
  "initialSubmissionActions": {
	"action": {"name": "Legacy Notify", "type": "Flow", "apiName": "Legacy_Notify_Flow"},
	"actions": {"name": "Notify", "type": "Flow", "apiName": "Notify_Flow"}
  }
}`)
	if err != nil {
		t.Fatalf("DecodeApprovalProcessMetadata returned error: %v", err)
	}
	if metadata == nil || metadata.Label != "Legacy Approval" || !metadata.Active || metadata.ProcessOrder == nil || *metadata.ProcessOrder != 9 {
		t.Fatalf("legacy parent metadata = %+v", metadata)
	}
	if metadata.AllowRecall == nil || *metadata.AllowRecall || metadata.EffectiveAllowRecall() {
		t.Fatalf("legacy allowRecall = explicit:%v effective:%v, want explicit false", metadata.AllowRecall, metadata.EffectiveAllowRecall())
	}
	if !metadata.FinalApprovalRecordLock || metadata.FinalRejectionRecordLock {
		t.Fatalf("legacy lock metadata = (%v,%v)", metadata.FinalApprovalRecordLock, metadata.FinalRejectionRecordLock)
	}
	if !metadata.HasExplicitAllowedSubmitters || len(metadata.AllowedSubmitters) != 1 || metadata.AllowedSubmitters[0].Type != constants.ApprovalSubmitterTypeRecordOwner || metadata.AllowedSubmitters[0].Submitter != "" {
		t.Fatalf("legacy allowed submitters = %+v", metadata.AllowedSubmitters)
	}
	if metadata.NextAutomatedApprover == nil || !metadata.NextAutomatedApprover.UseApproverFieldOfRecordOwner {
		t.Fatalf("legacy next automated approver = %+v", metadata.NextAutomatedApprover)
	}
	if !reflect.DeepEqual(metadata.ApprovalPageFields, []string{"Name"}) || !metadata.EnableMobileDeviceAccess {
		t.Fatalf("legacy page/mobile = (%v,%v)", metadata.ApprovalPageFields, metadata.EnableMobileDeviceAccess)
	}
	if len(metadata.Steps) != 1 {
		t.Fatalf("legacy step count = %d, want 1", len(metadata.Steps))
	}
	step := metadata.Steps[0]
	if step.AllowComments == nil || *step.AllowComments || step.RequireComments == nil || !*step.RequireComments || !step.AllowDelegate {
		t.Fatalf("legacy step flags = allowComments:%v requireComments:%v allowDelegate:%v", step.AllowComments, step.RequireComments, step.AllowDelegate)
	}
	if step.RejectBehavior != constants.ApprovalRejectBehaviorRejectRequest || step.WhenMultipleApprovers != constants.ApprovalModeUnanimous {
		t.Fatalf("legacy step behavior/mode = (%q,%q)", step.RejectBehavior, step.WhenMultipleApprovers)
	}
	if len(step.Approvers) != 1 || step.Approvers[0].Type != constants.ApprovalApproverTypeRelatedUserField || step.Approvers[0].Name != "Owner.ManagerId" {
		t.Fatalf("legacy approvers = %+v", step.Approvers)
	}
	if step.StepCriteria == nil || len(step.StepCriteria.CriteriaItems) != 1 || step.StepCriteria.CriteriaItems[0].Field != "Priority" || step.StepCriteria.CriteriaItems[0].Value != "" {
		t.Fatalf("legacy step criteria = %+v", step.StepCriteria)
	}
	if len(step.StepApprovalActions) != 1 || step.StepApprovalActions[0].FieldValue != "Done" {
		t.Fatalf("legacy step actions = %+v", step.StepApprovalActions)
	}
	if len(metadata.InitialSubmissionActions) != 2 || !metadata.InitialSubmissionActions[0].referenceForm || metadata.InitialSubmissionActions[0].APIName != "Legacy_Notify_Flow" || metadata.InitialSubmissionActions[1].referenceForm || metadata.InitialSubmissionActions[1].APIName != "Notify_Flow" {
		t.Fatalf("legacy process actions = %+v", metadata.InitialSubmissionActions)
	}
}

func TestDecodeApprovalProcessMetadataLegacyJSONActionShapes(t *testing.T) {
	tests := []struct {
		name          string
		actionJSON    string
		wantNames     []string
		wantReference []bool
	}{
		{
			name:          "direct object",
			actionJSON:    `{"name":"Direct","type":"Flow","apiName":"Direct_Flow"}`,
			wantNames:     []string{"Direct"},
			wantReference: []bool{false},
		},
		{
			name:          "direct array",
			actionJSON:    `[{"name":"First","type":"Flow","apiName":"First_Flow"},{"name":"Second","type":"Flow","apiName":"Second_Flow"}]`,
			wantNames:     []string{"First", "Second"},
			wantReference: []bool{false, false},
		},
		{
			name:          "wrapped singular",
			actionJSON:    `{"action":{"name":"Singular","type":"Flow","apiName":"Singular_Flow"}}`,
			wantNames:     []string{"Singular"},
			wantReference: []bool{true},
		},
		{
			name:          "wrapped plural",
			actionJSON:    `{"actions":[{"name":"Plural First","type":"Flow","apiName":"Plural_First_Flow"},{"name":"Plural Second","type":"Flow","apiName":"Plural_Second_Flow"}]}`,
			wantNames:     []string{"Plural First", "Plural Second"},
			wantReference: []bool{false, false},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			metadata, err := DecodeApprovalProcessMetadata(`{"initialSubmissionActions":` + tt.actionJSON + `}`)
			if err != nil {
				t.Fatalf("DecodeApprovalProcessMetadata returned error: %v", err)
			}
			gotNames := make([]string, 0, len(metadata.InitialSubmissionActions))
			gotReference := make([]bool, 0, len(metadata.InitialSubmissionActions))
			for _, action := range metadata.InitialSubmissionActions {
				gotNames = append(gotNames, action.Name)
				gotReference = append(gotReference, action.referenceForm)
			}
			if !reflect.DeepEqual(gotNames, tt.wantNames) {
				t.Fatalf("decoded action names = %v, want %v", gotNames, tt.wantNames)
			}
			if !reflect.DeepEqual(gotReference, tt.wantReference) {
				t.Fatalf("decoded action reference provenance = %v, want %v", gotReference, tt.wantReference)
			}
		})
	}
}

func TestDecodeApprovalProcessMetadataPreservesOmittedDefaultsAndInvalidOrder(t *testing.T) {
	metadata, err := DecodeApprovalProcessMetadata(`<ApprovalProcess><processOrder>not-a-number</processOrder></ApprovalProcess>`)
	if err != nil {
		t.Fatalf("DecodeApprovalProcessMetadata returned error: %v", err)
	}
	if metadata.AllowRecall != nil || !metadata.EffectiveAllowRecall() {
		t.Fatalf("allowRecall = explicit:%v effective:%v, want omitted/default true", metadata.AllowRecall, metadata.EffectiveAllowRecall())
	}
	if metadata.ProcessOrder != nil {
		t.Fatalf("ProcessOrder = %v, want nil for invalid optional order", metadata.ProcessOrder)
	}
}

func TestDecodeApprovalProcessMetadataFormatsNormalizeExecutionFactsEqually(t *testing.T) {
	xmlMetadata, err := DecodeApprovalProcessMetadata(`<ApprovalProcess>
  <allowedSubmitters><type>User</type><submitter>submitter@example.com</submitter></allowedSubmitters>
  <approvalStep>
    <name>Review</name><label>Review Label</label><allowDelegate>true</allowDelegate>
    <assignedApprover><approver><type>User</type><name>approver@example.com</name></approver><whenMultipleApprovers>Unanimous</whenMultipleApprovers></assignedApprover>
    <stepCriteria><formula>Amount &gt; 10</formula></stepCriteria><ifCriteriaNotMet>GotoNextStep</ifCriteriaNotMet>
  </approvalStep>
</ApprovalProcess>`)
	if err != nil {
		t.Fatalf("decode XML: %v", err)
	}
	jsonMetadata, err := DecodeApprovalProcessMetadata(`{
  "allowedSubmitters": {"type":"User","submitter":"submitter@example.com"},
  "approvalStep": {
    "name":"Review","label":"Review Label","allowDelegate":true,
    "assignedApprover":{"approver":{"type":"User","name":"approver@example.com"},"whenMultipleApprovers":"Unanimous"},
    "stepCriteria":{"formula":"Amount > 10"},"ifCriteriaNotMet":"GotoNextStep"
  }
}`)
	if err != nil {
		t.Fatalf("decode JSON: %v", err)
	}
	if !reflect.DeepEqual(xmlMetadata.AllowedSubmitters, jsonMetadata.AllowedSubmitters) || xmlMetadata.HasExplicitAllowedSubmitters != jsonMetadata.HasExplicitAllowedSubmitters {
		t.Fatalf("allowed submitter parity mismatch: XML=%+v JSON=%+v", xmlMetadata.AllowedSubmitters, jsonMetadata.AllowedSubmitters)
	}
	if !reflect.DeepEqual(xmlMetadata.Steps, jsonMetadata.Steps) {
		t.Fatalf("step parity mismatch:\nXML=%+v\nJSON=%+v", xmlMetadata.Steps, jsonMetadata.Steps)
	}
}

func TestDecodeApprovalProcessMetadataEmptyAndUnsupported(t *testing.T) {
	metadata, err := DecodeApprovalProcessMetadata("  \n\t")
	if err != nil || metadata != nil {
		t.Fatalf("empty metadata = (%+v,%v), want (nil,nil)", metadata, err)
	}
	_, err = DecodeApprovalProcessMetadata("not metadata")
	if err == nil || !strings.Contains(err.Error(), "unsupported approval metadata format") {
		t.Fatalf("unsupported metadata error = %v", err)
	}
}

func TestApprovalProcessDefinitionDecoderValidatesCanonicalSemantics(t *testing.T) {
	decoder := NewApprovalProcessDefinitionDecoder()
	definition, err := decoder.DecodeApprovalDefinition(`{"label":"Expense Approval","active":true,"recordEditability":"Admin","entryCriteria":{"criteriaItems":{"field":"Amount","operation":"greaterThan","value":"10"}},"allowedSubmitters":{"type":"recordCreator"},"approvalStep":{"name":"Manager","label":"Manager","assignedApprover":{"approver":[{"type":"user","name":"one@example.com"},{"type":"user","name":"two@example.com"}],"whenMultipleApprovers":"Unanimous"},"rejectBehavior":{"type":"RejectRequest"},"stepCriteria":{"criteriaItems":{"field":"Status","operation":"equals","value":"Open"}},"ifCriteriaNotMet":"GotoNextStep","stepApprovalActions":{"action":{"name":"Set Status","type":"FieldUpdate","fieldApiName":"Status","fieldValue":"Literal: Approved"}}},"initialSubmissionActions":{"action":{"name":"Notify","type":"Flow","apiName":"Notify_Approver"}}}`)
	if err != nil {
		t.Fatalf("DecodeApprovalDefinition() error = %v", err)
	}
	if definition.Policy.RecordEditability != "admin_only" || definition.Policy.AllowedSubmitters[0].Kind != "record_creator" || definition.Steps[0].Approvers[0].Kind != "user" {
		t.Fatalf("DecodeApprovalDefinition() = %#v", definition)
	}
	inlineAction := definition.InitialSubmissionActions[0]
	if inlineAction.Reference != nil || inlineAction.Flow == nil || inlineAction.Flow.APIName != "Notify_Approver" {
		t.Fatalf("singular action with an inline body = %#v, want inline flow payload", inlineAction)
	}
	for _, raw := range []string{"", `{"label":"Mixed criteria","recordEditability":"Admin","entryCriteria":{"formula":"Amount > 10","criteriaItems":{"field":"Amount","operation":"greaterThan","value":"10"}}}`} {
		if got, err := decoder.DecodeApprovalDefinition(raw); got != nil || err == nil || !strings.Contains(err.Error(), "approval definition") {
			t.Fatalf("DecodeApprovalDefinition(%q) = (%#v, %v), want fail closed", raw, got, err)
		}
	}
}

func TestApprovalProcessDefinitionDecoderAllowsMissingRootLabelWithGatedRouting(t *testing.T) {
	definition, err := NewApprovalProcessDefinitionDecoder().DecodeApprovalDefinition(`{
  "approvalStep": [
    {
      "name": "Initial_Review",
      "label": "Initial Review",
      "assignedApprover": {"approver": {"name": "Initial_Reviewer__c", "type": "relatedUserField"}},
      "rejectBehavior": {"type": "RejectRequest"}
    },
    {
      "name": "Risk_Review",
      "label": "Risk Review",
      "assignedApprover": {"approver": {"name": "Risk_Reviewer__c", "type": "relatedUserField"}},
      "stepCriteria": {
        "booleanFilter": "1 AND 2",
        "criteriaItems": [
          {"field": "Requested_Amount__c", "operation": "greaterThan", "value": "1000"},
          {"field": "Requires_Risk_Review__c", "operation": "equals", "value": "true"}
        ]
      },
      "ifCriteriaNotMet": "GotoNextStep",
      "rejectBehavior": {"type": "RejectRequest"}
    },
    {
      "name": "Executive_Review",
      "label": "Executive Review",
      "assignedApprover": {"approver": {"name": "Executive_Reviewer__c", "type": "relatedUserField"}},
      "stepCriteria": {"criteriaItems": {"field": "Requested_Amount__c", "operation": "greaterThan", "value": "3000"}},
      "ifCriteriaNotMet": "ApproveRecord",
      "rejectBehavior": {"type": "RejectRequest"}
    }
  ]
}`)
	if err != nil {
		t.Fatalf("DecodeApprovalDefinition() error = %v", err)
	}
	if definition.Label != "" {
		t.Fatalf("Label = %q, want empty root label", definition.Label)
	}
	if definition.Active {
		t.Fatal("Active = true, want omitted metadata activation to remain false")
	}
	wants := []struct {
		name, approver string
		transition     approvalir.IfCriteriaNotMet
		criteria       []approvalir.CriteriaItem
	}{
		{name: "Initial_Review", approver: "Initial_Reviewer__c"},
		{name: "Risk_Review", approver: "Risk_Reviewer__c", transition: approvalir.IfCriteriaNotMetGotoNextStep, criteria: []approvalir.CriteriaItem{
			{Field: "Requested_Amount__c", Operation: approvalir.CriteriaOperatorGreaterThan, Value: "1000"},
			{Field: "Requires_Risk_Review__c", Operation: approvalir.CriteriaOperatorEquals, Value: "true"},
		}},
		{name: "Executive_Review", approver: "Executive_Reviewer__c", transition: approvalir.IfCriteriaNotMetApprove, criteria: []approvalir.CriteriaItem{
			{Field: "Requested_Amount__c", Operation: approvalir.CriteriaOperatorGreaterThan, Value: "3000"},
		}},
	}
	if len(definition.Steps) != len(wants) {
		t.Fatalf("step count = %d, want %d", len(definition.Steps), len(wants))
	}
	for index, want := range wants {
		step := definition.Steps[index]
		if step.Name != want.name || step.RejectBehavior != approvalir.RejectBehaviorRejectRequest || len(step.Approvers) != 1 ||
			step.Approvers[0].Kind != approvalir.ApproverKindRelatedUserField || step.Approvers[0].Reference != want.approver ||
			step.IfCriteriaNotMet != want.transition {
			t.Fatalf("step %d routing = %#v, want name %q approver %q transition %q", index, step, want.name, want.approver, want.transition)
		}
		if len(want.criteria) == 0 {
			if step.Criteria != nil {
				t.Fatalf("step %d criteria = %#v, want nil", index, step.Criteria)
			}
			continue
		}
		if step.Criteria == nil || step.Criteria.Mode != approvalir.CriteriaModeItems || !reflect.DeepEqual(step.Criteria.Items, want.criteria) {
			t.Fatalf("step %d criteria = %#v, want %#v", index, step.Criteria, want.criteria)
		}
	}
	if got := definition.Steps[1].Criteria.BooleanFilter; got != "1 AND 2" {
		t.Fatalf("risk boolean filter = %q, want 1 AND 2", got)
	}
}

func TestApprovalProcessDefinitionDecoderPreservesSingularSalesforceActionReferences(t *testing.T) {
	tests := []struct {
		name string
		raw  string
	}{
		{
			name: "XML action wrapper",
			raw: `<ApprovalProcess>
  <label>Reference actions</label>
  <initialSubmissionActions>
    <action><name>Set_Submitted</name><type>FieldUpdate</type></action>
    <action><name>Notify_Approver</name><type>Flow</type></action>
    <action><name>Submission_Alert</name><type>Alert</type></action>
  </initialSubmissionActions>
</ApprovalProcess>`,
		},
		{
			name: "legacy JSON action wrapper",
			raw:  `{"label":"Reference actions","initialSubmissionActions":{"action":[{"name":"Set_Submitted","type":"FieldUpdate"},{"name":"Notify_Approver","type":"Flow"},{"name":"Submission_Alert","type":"Alert"}]}}`,
		},
	}
	wants := []struct {
		name string
		kind approvalir.ActionKind
	}{
		{name: "Set_Submitted", kind: approvalir.ActionKindFieldUpdate},
		{name: "Notify_Approver", kind: approvalir.ActionKindFlow},
		{name: "Submission_Alert", kind: approvalir.ActionKindAlert},
	}
	decoder := NewApprovalProcessDefinitionDecoder()
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			definition, err := decoder.DecodeApprovalDefinition(tt.raw)
			if err != nil {
				t.Fatalf("DecodeApprovalDefinition() error = %v", err)
			}
			if len(definition.InitialSubmissionActions) != len(wants) {
				t.Fatalf("action count = %d, want %d", len(definition.InitialSubmissionActions), len(wants))
			}
			for index, want := range wants {
				action := definition.InitialSubmissionActions[index]
				if action.Name != want.name || action.Kind != want.kind || action.Reference == nil || action.Reference.APIName != want.name {
					t.Fatalf("action %d = %#v, want %q reference of kind %q", index, action, want.name, want.kind)
				}
				if action.FieldUpdate != nil || action.Flow != nil || action.Alert != nil {
					t.Fatalf("action %d = %#v, want reference without inline payload", index, action)
				}
			}
		})
	}
}

func TestApprovalProcessDefinitionDecoderKeepsXMLPluralActionsInline(t *testing.T) {
	definition, err := NewApprovalProcessDefinitionDecoder().DecodeApprovalDefinition(`<ApprovalProcess>
  <label>Inline actions</label>
  <initialSubmissionActions>
    <actions><name>Set Status</name><type>FieldUpdate</type><fieldApiName>Status</fieldApiName><fieldValue>Submitted</fieldValue></actions>
    <actions><name>Notify Approver</name><type>Flow</type><apiName>Notify_Approver</apiName></actions>
    <actions><name>Email Approver</name><type>Alert</type><emailTemplateId>Approval_Assigned</emailTemplateId></actions>
  </initialSubmissionActions>
</ApprovalProcess>`)
	if err != nil {
		t.Fatalf("DecodeApprovalDefinition() error = %v", err)
	}
	if len(definition.InitialSubmissionActions) != 3 {
		t.Fatalf("action count = %d, want 3", len(definition.InitialSubmissionActions))
	}
	fieldUpdate := definition.InitialSubmissionActions[0]
	if fieldUpdate.Reference != nil || fieldUpdate.FieldUpdate == nil || fieldUpdate.FieldUpdate.FieldAPIName != "Status" {
		t.Fatalf("plural field update = %#v, want inline payload", fieldUpdate)
	}
	flow := definition.InitialSubmissionActions[1]
	if flow.Reference != nil || flow.Flow == nil || flow.Flow.APIName != "Notify_Approver" {
		t.Fatalf("plural flow = %#v, want inline payload", flow)
	}
	alert := definition.InitialSubmissionActions[2]
	if alert.Reference != nil || alert.Alert == nil || alert.Alert.EmailTemplateID != "Approval_Assigned" {
		t.Fatalf("plural alert = %#v, want inline payload", alert)
	}
}

func TestApprovalProcessDefinitionDecoderRejectsPartialInlineAction(t *testing.T) {
	tests := []struct {
		name, raw, wantErr string
	}{
		{
			name:    "XML blank field API name",
			raw:     `<ApprovalProcess><label>Partial</label><initialSubmissionActions><action><name>Set_Status</name><type>FieldUpdate</type><fieldApiName></fieldApiName></action></initialSubmissionActions></ApprovalProcess>`,
			wantErr: "field API name is required",
		},
		{
			name:    "JSON blank field API name",
			raw:     `{"label":"Partial","initialSubmissionActions":{"action":{"name":"Set_Status","type":"FieldUpdate","fieldApiName":""}}}`,
			wantErr: "field API name is required",
		},
		{
			name:    "XML blank field value",
			raw:     `<ApprovalProcess><label>Partial</label><initialSubmissionActions><action><name>Set_Status</name><type>FieldUpdate</type><fieldValue></fieldValue></action></initialSubmissionActions></ApprovalProcess>`,
			wantErr: "field API name is required",
		},
		{
			name:    "JSON blank field value",
			raw:     `{"label":"Partial","initialSubmissionActions":{"action":{"name":"Set_Status","type":"FieldUpdate","fieldValue":""}}}`,
			wantErr: "field API name is required",
		},
		{
			name:    "XML blank flow API name",
			raw:     `<ApprovalProcess><label>Partial</label><initialSubmissionActions><action><name>Notify</name><type>Flow</type><apiName></apiName></action></initialSubmissionActions></ApprovalProcess>`,
			wantErr: "flow payload requires an API name",
		},
		{
			name:    "JSON blank flow API name",
			raw:     `{"label":"Partial","initialSubmissionActions":{"action":{"name":"Notify","type":"Flow","apiName":""}}}`,
			wantErr: "flow payload requires an API name",
		},
		{
			name:    "JSON null flow API name",
			raw:     `{"label":"Partial","initialSubmissionActions":{"action":{"name":"Notify","type":"Flow","apiName":null}}}`,
			wantErr: "flow payload requires an API name",
		},
		{
			name:    "XML blank email template ID",
			raw:     `<ApprovalProcess><label>Partial</label><initialSubmissionActions><action><name>Notify</name><type>Alert</type><emailTemplateId></emailTemplateId></action></initialSubmissionActions></ApprovalProcess>`,
			wantErr: "alert payload requires an email-template reference",
		},
		{
			name:    "JSON blank email template ID",
			raw:     `{"label":"Partial","initialSubmissionActions":{"action":{"name":"Notify","type":"Alert","emailTemplateId":""}}}`,
			wantErr: "alert payload requires an email-template reference",
		},
	}
	decoder := NewApprovalProcessDefinitionDecoder()
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			definition, err := decoder.DecodeApprovalDefinition(tt.raw)
			if definition != nil || err == nil || !strings.Contains(err.Error(), tt.wantErr) {
				t.Fatalf("DecodeApprovalDefinition() = (%#v, %v), want error containing %q", definition, err, tt.wantErr)
			}
		})
	}
}

func TestApprovalProcessDefinitionDecoderRejectsNameTypeOnlyNonReferenceActionForms(t *testing.T) {
	tests := []struct {
		name, raw string
	}{
		{
			name: "XML actions wrapper",
			raw:  `<ApprovalProcess><label>Malformed action</label><initialSubmissionActions><actions><name>Set_Status</name><type>FieldUpdate</type></actions></initialSubmissionActions></ApprovalProcess>`,
		},
		{
			name: "legacy JSON actions wrapper",
			raw:  `{"label":"Malformed action","initialSubmissionActions":{"actions":{"name":"Set_Status","type":"FieldUpdate"}}}`,
		},
		{
			name: "legacy JSON direct object",
			raw:  `{"label":"Malformed action","initialSubmissionActions":{"name":"Set_Status","type":"FieldUpdate"}}`,
		},
		{
			name: "legacy JSON direct array",
			raw:  `{"label":"Malformed action","initialSubmissionActions":[{"name":"Set_Status","type":"FieldUpdate"}]}`,
		},
	}
	decoder := NewApprovalProcessDefinitionDecoder()
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			definition, err := decoder.DecodeApprovalDefinition(tt.raw)
			if definition != nil || err == nil || !strings.Contains(err.Error(), "field API name is required") {
				t.Fatalf("DecodeApprovalDefinition() = (%#v, %v), want non-reference action form to fail inline validation", definition, err)
			}
		})
	}
}

func TestApprovalProcessDefinitionDecoderRejectsUnknownActionBody(t *testing.T) {
	tests := []struct {
		name, raw string
	}{
		{
			name: "XML singular unknown only",
			raw:  `<ApprovalProcess><label>Unknown action</label><initialSubmissionActions><action><name>Notify</name><type>Flow</type><unsupportedPayload>value</unsupportedPayload></action></initialSubmissionActions></ApprovalProcess>`,
		},
		{
			name: "JSON singular unknown only",
			raw:  `{"label":"Unknown action","initialSubmissionActions":{"action":{"name":"Notify","type":"Flow","unsupportedPayload":"value"}}}`,
		},
		{
			name: "XML singular unknown with complete inline payload",
			raw:  `<ApprovalProcess><label>Unknown action</label><initialSubmissionActions><action><name>Notify</name><type>Flow</type><apiName>Notify_Flow</apiName><unsupportedPayload>value</unsupportedPayload></action></initialSubmissionActions></ApprovalProcess>`,
		},
		{
			name: "JSON singular unknown with complete inline payload",
			raw:  `{"label":"Unknown action","initialSubmissionActions":{"action":{"name":"Notify","type":"Flow","apiName":"Notify_Flow","unsupportedPayload":"value"}}}`,
		},
		{
			name: "XML plural unknown with complete inline payload",
			raw:  `<ApprovalProcess><label>Unknown action</label><initialSubmissionActions><actions><name>Notify</name><type>Flow</type><apiName>Notify_Flow</apiName><unsupportedPayload>value</unsupportedPayload></actions></initialSubmissionActions></ApprovalProcess>`,
		},
		{
			name: "JSON direct unknown with complete inline payload",
			raw:  `{"label":"Unknown action","initialSubmissionActions":{"name":"Notify","type":"Flow","apiName":"Notify_Flow","unsupportedPayload":"value"}}`,
		},
	}
	decoder := NewApprovalProcessDefinitionDecoder()
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			definition, err := decoder.DecodeApprovalDefinition(tt.raw)
			if definition != nil || err == nil || !strings.Contains(err.Error(), "exactly one action payload") {
				t.Fatalf("DecodeApprovalDefinition() = (%#v, %v), want unknown body to fail closed", definition, err)
			}
		})
	}
}

func TestApprovalProcessDefinitionDecoderRejectsMixedKnownActionBodies(t *testing.T) {
	tests := []struct {
		name, raw string
	}{
		{
			name: "XML flow with field-update field",
			raw:  `<ApprovalProcess><label>Mixed action</label><initialSubmissionActions><action><name>Notify</name><type>Flow</type><apiName>Notify_Flow</apiName><fieldApiName>Status</fieldApiName></action></initialSubmissionActions></ApprovalProcess>`,
		},
		{
			name: "JSON flow with field-update field",
			raw:  `{"label":"Mixed action","initialSubmissionActions":{"action":{"name":"Notify","type":"Flow","apiName":"Notify_Flow","fieldApiName":"Status"}}}`,
		},
		{
			name: "XML field update with flow field",
			raw:  `<ApprovalProcess><label>Mixed action</label><initialSubmissionActions><actions><name>Set_Status</name><type>FieldUpdate</type><fieldApiName>Status</fieldApiName><fieldValue>Submitted</fieldValue><apiName>Unexpected_Flow</apiName></actions></initialSubmissionActions></ApprovalProcess>`,
		},
		{
			name: "JSON field update with flow field",
			raw:  `{"label":"Mixed action","initialSubmissionActions":{"name":"Set_Status","type":"FieldUpdate","fieldApiName":"Status","fieldValue":"Submitted","apiName":"Unexpected_Flow"}}`,
		},
		{
			name: "XML alert with field-update value",
			raw:  `<ApprovalProcess><label>Mixed action</label><initialSubmissionActions><action><name>Notify</name><type>Alert</type><emailTemplateId>Approval_Alert</emailTemplateId><fieldValue></fieldValue></action></initialSubmissionActions></ApprovalProcess>`,
		},
		{
			name: "JSON alert with null field-update value",
			raw:  `{"label":"Mixed action","initialSubmissionActions":{"action":{"name":"Notify","type":"Alert","emailTemplateId":"Approval_Alert","fieldValue":null}}}`,
		},
	}
	decoder := NewApprovalProcessDefinitionDecoder()
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			definition, err := decoder.DecodeApprovalDefinition(tt.raw)
			if definition != nil || err == nil || !strings.Contains(err.Error(), "exactly one action payload") {
				t.Fatalf("DecodeApprovalDefinition() = (%#v, %v), want mixed known body fields to fail closed", definition, err)
			}
		})
	}
}

func TestApprovalProcessDefinitionDecoderWithoutValidatorRejectsFormula(t *testing.T) {
	definition, err := NewApprovalProcessDefinitionDecoder().DecodeApprovalDefinition(`{"label":"Formula approval","entryCriteria":{"formula":"Amount > 10"}}`)
	if definition != nil || err == nil || !strings.Contains(err.Error(), "formula criteria require a formula validator") {
		t.Fatalf("DecodeApprovalDefinition() = (%#v, %v), want formula criteria to fail closed", definition, err)
	}
}

func TestApprovalProcessDefinitionDecoderPreservesFormulaCriteria(t *testing.T) {
	tests := []struct {
		name string
		raw  string
	}{
		{
			name: "XML",
			raw: `<ApprovalProcess>
  <label>Formula approval</label>
  <entryCriteria><formula> Amount &gt; 10 </formula></entryCriteria>
  <approvalStep>
    <name>Review</name><label>Review</label>
    <assignedApprover><approver><type>user</type><name>approver@example.com</name></approver></assignedApprover>
    <stepCriteria><formula> AND(Amount &gt; 10, NOT(IsClosed)) </formula></stepCriteria>
  </approvalStep>
</ApprovalProcess>`,
		},
		{
			name: "legacy JSON",
			raw: `{
  "label":"Formula approval",
  "entryCriteria":{"formula":" Amount > 10 "},
  "approvalStep":{
    "name":"Review","label":"Review",
    "assignedApprover":{"approver":{"type":"user","name":"approver@example.com"}},
    "stepCriteria":{"formula":" AND(Amount > 10, NOT(IsClosed)) "}
  }
}`,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			definition, err := newTestApprovalProcessDefinitionDecoder().DecodeApprovalDefinition(tt.raw)
			if err != nil {
				t.Fatalf("DecodeApprovalDefinition() error = %v", err)
			}
			assertFormulaCriteria := func(name string, criteria *approvalir.Criteria, want string) {
				t.Helper()
				if criteria == nil || criteria.Formula != want || criteria.Mode != "" || criteria.BooleanFilter != "" || len(criteria.Items) != 0 {
					t.Fatalf("%s criteria = %#v, want formula %q only", name, criteria, want)
				}
			}
			assertFormulaCriteria("entry", definition.EntryCriteria, "Amount > 10")
			if len(definition.Steps) != 1 {
				t.Fatalf("step count = %d, want 1", len(definition.Steps))
			}
			assertFormulaCriteria("step", definition.Steps[0].Criteria, "AND(Amount > 10, NOT(IsClosed))")
		})
	}
}

func TestApprovalProcessDefinitionDecoderDelegatesFormulaValidation(t *testing.T) {
	wantErr := errors.New("formula engine rejected source")
	var gotSource string
	decoder := NewApprovalProcessDefinitionDecoderWithFormulaValidator(approvalir.FormulaValidatorFunc(func(source string) error {
		gotSource = source
		return wantErr
	}))
	definition, err := decoder.DecodeApprovalDefinition(`{"label":"Formula approval","entryCriteria":{"formula":"Amount >"}}`)
	if definition != nil || !errors.Is(err, wantErr) {
		t.Fatalf("DecodeApprovalDefinition() = (%#v, %v), want validator error", definition, err)
	}
	if gotSource != "Amount >" {
		t.Fatalf("validated formula = %q, want exact normalized source", gotSource)
	}
}

func TestApprovalProcessDefinitionDecoderLowersHierarchyRouting(t *testing.T) {
	tests := []struct {
		name, raw, wantField string
		wantSeed             approvalir.HierarchyInitialPrincipal
	}{
		{"hierarchy defaults to submitter manager",
			`{"label":"Hierarchy","approvalStep":{"name":"Review","label":"Review","assignedApprover":{"approver":{"type":"hierarchy"}}}}`,
			"ManagerId", approvalir.HierarchyInitialPrincipalSubmitter},
		{"user hierarchy field uses neutral hierarchy default",
			`{"label":"Hierarchy","approvalStep":{"name":"Review","label":"Review","assignedApprover":{"approver":{"type":"userHierarchyField"}}}}`,
			"ManagerId", approvalir.HierarchyInitialPrincipalSubmitter},
		{"explicit record owner routing is preserved",
			`{"label":"Hierarchy","nextAutomatedApprover":{"userHierarchyField":"CustomManagerId","useApproverFieldOfRecordOwner":true},"approvalStep":{"name":"Review","label":"Review","assignedApprover":{"approver":{"type":"userHierarchyField"}}}}`,
			"CustomManagerId", approvalir.HierarchyInitialPrincipalRecordOwner},
	}
	decoder := newTestApprovalProcessDefinitionDecoder()
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			definition, err := decoder.DecodeApprovalDefinition(tt.raw)
			if err != nil {
				t.Fatalf("DecodeApprovalDefinition() error = %v", err)
			}
			if definition.HierarchyRouting == nil || definition.HierarchyRouting.UserHierarchyField != tt.wantField || definition.HierarchyRouting.InitialPrincipal != tt.wantSeed {
				t.Fatalf("HierarchyRouting = %#v, want field %q seed %q", definition.HierarchyRouting, tt.wantField, tt.wantSeed)
			}
			approver := definition.Steps[0].Approvers[0]
			if approver.Kind != approvalir.ApproverKindHierarchy || approver.Reference != "" {
				t.Fatalf("approver = %#v, want neutral hierarchy without reference", approver)
			}
		})
	}
}

func TestApprovalProcessDefinitionDecoderDefaultsCriteriaTransition(t *testing.T) {
	tests := []struct {
		name       string
		transition string
		want       approvalir.IfCriteriaNotMet
	}{
		{name: "omitted defaults to approve", want: approvalir.IfCriteriaNotMetApprove},
		{name: "reject remains reject", transition: `,"ifCriteriaNotMet":"RejectRecord"`, want: approvalir.IfCriteriaNotMetReject},
	}
	decoder := newTestApprovalProcessDefinitionDecoder()
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			raw := `{"label":"Criteria","approvalStep":{"name":"Review","label":"Review","assignedApprover":{"approver":{"type":"user","name":"approver@example.com"}},"stepCriteria":{"criteriaItems":{"field":"Status","operation":"equals","value":"Open"}}` + tt.transition + `}}`
			definition, err := decoder.DecodeApprovalDefinition(raw)
			if err != nil {
				t.Fatalf("DecodeApprovalDefinition() error = %v", err)
			}
			if got := definition.Steps[0].IfCriteriaNotMet; got != tt.want {
				t.Fatalf("IfCriteriaNotMet = %q, want %q", got, tt.want)
			}
		})
	}
}

func TestApprovalProcessDefinitionDecoderDefaultsStepLabelToName(t *testing.T) {
	tests := []struct {
		name, raw, want string
	}{
		{"XML", "<ApprovalProcess><label>XML approval</label>" +
			"<approvalStep><name> XML_Review </name><assignedApprover><approver><type>user</type><name>approver@example.com</name></approver></assignedApprover></approvalStep>" +
			"</ApprovalProcess>", "XML_Review"},
		{"legacy JSON",
			`{"label":"JSON approval","approvalStep":{"name":" JSON_Review ","label":"  ","assignedApprover":{"approver":{"type":"user","name":"approver@example.com"}}}}`, "JSON_Review"},
	}
	decoder := newTestApprovalProcessDefinitionDecoder()
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			definition, err := decoder.DecodeApprovalDefinition(tt.raw)
			if err != nil {
				t.Fatalf("DecodeApprovalDefinition() error = %v", err)
			}
			if got := definition.Steps[0]; got.Name != tt.want || got.Label != tt.want {
				t.Fatalf("step identity = (%q, %q), want (%q, %q)", got.Name, got.Label, tt.want, tt.want)
			}
		})
	}
}

func TestApprovalProcessDefinitionDecoderPreservesFieldUpdateValuePresence(t *testing.T) {
	definition, err := newTestApprovalProcessDefinitionDecoder().DecodeApprovalDefinition(`{
  "label":"Field updates",
  "initialSubmissionActions":[
    {"name":"Clear","type":"FieldUpdate","fieldApiName":"Note"},
    {"name":"JSON null","type":"FieldUpdate","fieldApiName":"NullNote","fieldValue":null},
    {"name":"Explicit empty","type":"FieldUpdate","fieldApiName":"EmptyNote","fieldValue":""},
    {"name":"Literal empty","type":"FieldUpdate","fieldApiName":"LiteralEmptyNote","fieldValue":"Literal: "},
    {"name":"Set status","type":"FieldUpdate","fieldApiName":"Status","fieldValue":"Literal: Approved"}
  ]
}`)
	if err != nil {
		t.Fatalf("DecodeApprovalDefinition() error = %v", err)
	}
	actions := definition.InitialSubmissionActions
	if len(actions) != 5 {
		t.Fatalf("action count = %d, want 5", len(actions))
	}
	for index := 0; index < 2; index++ {
		clear := actions[index].FieldUpdate.Value
		if !clear.Null || clear.SourceText != nil {
			t.Fatalf("action %d absent/null field value = %#v, want explicit null", index, clear)
		}
	}
	for index, want := range []string{"", "", "Approved"} {
		value := actions[index+2].FieldUpdate.Value
		if value.Null || value.SourceText == nil || *value.SourceText != want {
			t.Fatalf("action %d value = %#v, want source text %q", index+2, value, want)
		}
	}
}

func TestApprovalProcessDefinitionDecoderNumbersBlankActionsPerGroup(t *testing.T) {
	definition, err := newTestApprovalProcessDefinitionDecoder().DecodeApprovalDefinition(`{
  "label":"Action names",
  "initialSubmissionActions":[{"type":"Flow","apiName":"First"},{"type":"Flow","apiName":"Second"}],
  "finalApprovalActions":{"action":{"type":"Flow","apiName":"Final"}}
}`)
	if err != nil {
		t.Fatalf("DecodeApprovalDefinition() error = %v", err)
	}
	if got := []string{definition.InitialSubmissionActions[0].Name, definition.InitialSubmissionActions[1].Name}; !reflect.DeepEqual(got, []string{"#1", "#2"}) {
		t.Fatalf("initial action names = %v, want [#1 #2]", got)
	}
	if got := definition.FinalApprovalActions[0].Name; got != "#1" {
		t.Fatalf("final action name = %q, want #1", got)
	}
}
