package sfcrmappservice

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"strconv"
	"strings"
	"sync"
	"testing"
	"time"

	approvalsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/approvalv1"
	commonsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
)

func approvalReceipts(capability string) commonsemantic.Receipts {
	return commonsemantic.Receipts{
		ProtocolVersion: "1.0", CapabilityVersion: capability,
		CatalogRevision: 17, SchemaGeneration: 8, EngineGeneration: 9,
		SecurityEpoch: 3,
	}
}

func approvalTestService(
	t *testing.T,
	execute RuntimeExecuteApprovalFunc,
	read RuntimeReadApprovalFunc,
) *Service {
	t.Helper()
	config := permissionMetadataServiceConfig(nil, nil)
	config.RuntimeExecuteApproval = execute
	config.RuntimeReadApproval = read
	service, err := NewService(config)
	if err != nil {
		t.Fatal(err)
	}
	return service
}

func approvalHTTPRequest(method, target, body string) *http.Request {
	request := httptest.NewRequest(method, target, strings.NewReader(body))
	request.Header.Set("Authorization", "Bearer user-secret")
	return request
}

func approvalExecuteResponse(
	request approvalsemantic.ExecuteRequest,
) approvalsemantic.ExecuteResponse {
	response := approvalsemantic.ExecuteResponse{
		ExecutionID: "execution-" + string(request.Operation),
		InstanceID:  "instance-1", TargetRecordID: "record-1",
		ActingPrincipalID: "user-1",
		Receipts:          approvalReceipts(approvalsemantic.ExecuteCapabilityName),
	}
	if request.Operation == approvalsemantic.OperationSubmit {
		response.TargetRecordID = request.TargetRecordID
	} else {
		response.SourceWorkItemID = request.WorkItemID
	}
	switch request.Operation {
	case approvalsemantic.OperationSubmit:
		response.State = approvalsemantic.InstanceStatePending
		response.PendingWorkItems = []approvalsemantic.WorkItemAssignment{{
			WorkItemID: "work-submit", ActorID: "actor-submit",
		}}
	case approvalsemantic.OperationApprove:
		response.State = approvalsemantic.InstanceStateApproved
	case approvalsemantic.OperationReject:
		response.State = approvalsemantic.InstanceStateRejected
	case approvalsemantic.OperationReassign:
		response.State = approvalsemantic.InstanceStatePending
		response.PendingWorkItems = []approvalsemantic.WorkItemAssignment{{
			WorkItemID: request.WorkItemID, ActorID: request.NextActorID,
		}}
	case approvalsemantic.OperationRecall:
		response.State = approvalsemantic.InstanceStateWithdrawn
		response.ActionFailures = []approvalsemantic.ActionFailure{{
			ActionRef: "Recall_Notice", Kind: approvalsemantic.ActionFailureUnsupported,
			Reason: approvalsemantic.ActionFailureUnsupportedAction,
		}}
	}
	return response
}

func TestApprovalExecuteProjectsExistingLifecycleWireAndStableReplayIdentity(t *testing.T) {
	var (
		mu       sync.Mutex
		requests []approvalsemantic.ExecuteRequest
	)
	execute := func(
		_ context.Context,
		bearer string,
		request approvalsemantic.ExecuteRequest,
	) (approvalsemantic.ExecuteResponse, error) {
		if bearer != "user-secret" {
			t.Fatalf("bearer = %q", bearer)
		}
		mu.Lock()
		requests = append(requests, request)
		mu.Unlock()
		return approvalExecuteResponse(request), nil
	}
	service := approvalTestService(
		t, execute,
		func(context.Context, string, approvalsemantic.ReadRequest) (approvalsemantic.ReadResponse, error) {
			t.Fatal("execute route reached Approval read")
			return approvalsemantic.ReadResponse{}, nil
		},
	)
	body := `{"ObjectApiName":"Account","requests":[` +
		`{"actionType":"Submit","contextId":"record-1","comments":"submit"},` +
		`{"actionType":"Approve","contextId":"work-approve","comments":"approve"},` +
		`{"actionType":"Reject","contextId":"work-reject","comments":"reject"},` +
		`{"actionType":"Reassign","contextId":"work-reassign","nextApproverIds":["actor-next"]},` +
		`{"actionType":"Removed","contextId":"work-recall","comments":"recall"}]}`
	request := approvalHTTPRequest(http.MethodPost, "/services/data/v63.0/process/approvals", body)
	request.Header.Set(crmMutationIdempotencyHeader, "approval-logical-request")
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, request)
	if recorder.Code != http.StatusOK {
		t.Fatalf("status = %d, body=%s", recorder.Code, recorder.Body.String())
	}
	want := `[` +
		`{"actorIds":["actor-submit"],"entityId":"record-1","errors":[],"instanceId":"instance-1","instanceStatus":"Pending","newWorkitemIds":["work-submit"],"success":true},` +
		`{"actorIds":[],"entityId":"record-1","errors":[],"instanceId":"instance-1","instanceStatus":"Approved","newWorkitemIds":[],"success":true},` +
		`{"actorIds":[],"entityId":"record-1","errors":[],"instanceId":"instance-1","instanceStatus":"Rejected","newWorkitemIds":[],"success":true},` +
		`{"actorIds":["actor-next"],"entityId":"","errors":[],"instanceId":"work-reassign","instanceStatus":"Pending","newWorkitemIds":["work-reassign"],"success":true},` +
		`{"actorIds":["user-1"],"entityId":"record-1","errors":[],"instanceId":"instance-1","instanceStatus":"Removed","newWorkitemIds":[],"success":true,"actionFailures":[{"actionName":"Recall_Notice","actionType":"UnsupportedAction","reason":"Unsupported action"}]}` +
		`]`
	if got := strings.TrimSpace(recorder.Body.String()); got != want {
		t.Fatalf("body = %s\nwant = %s", got, want)
	}
	mu.Lock()
	defer mu.Unlock()
	if len(requests) != 5 {
		t.Fatalf("requests = %#v", requests)
	}
	base, err := mutationCommandID([]string{"approval-logical-request"})
	if err != nil {
		t.Fatal(err)
	}
	wantOperations := []approvalsemantic.Operation{
		approvalsemantic.OperationSubmit, approvalsemantic.OperationApprove,
		approvalsemantic.OperationReject, approvalsemantic.OperationReassign,
		approvalsemantic.OperationRecall,
	}
	for index, request := range requests {
		if request.Operation != wantOperations[index] ||
			request.IdempotencyKey != base+"-"+strconv.Itoa(index) {
			t.Fatalf("request[%d] = %#v", index, request)
		}
	}
	if requests[0].ObjectID != "Account" || requests[0].TargetRecordID != "record-1" ||
		requests[3].NextActorID != "actor-next" {
		t.Fatalf("requests = %#v", requests)
	}
}

func TestApprovalSubmitRejectionsAreBusinessResultsAcrossEnvelopes(t *testing.T) {
	rejections := []struct {
		name       string
		rejection  approvalsemantic.SubmitRejection
		wantErrors []string
	}{
		{
			name: "no active process",
			rejection: approvalsemantic.SubmitRejection{
				Reason: approvalsemantic.SubmitRejectionNoActiveProcess,
			},
			wantErrors: []string{"No applicable approval process was found."},
		},
		{
			name: "entry criteria miss",
			rejection: approvalsemantic.SubmitRejection{
				Reason: approvalsemantic.SubmitRejectionEntryCriteriaMiss,
			},
			wantErrors: []string{"No applicable approval process was found."},
		},
		{
			name: "record validation failed",
			rejection: approvalsemantic.SubmitRejection{
				Reason: approvalsemantic.SubmitRejectionRecordValidationFailed,
				ValidationIssues: []approvalsemantic.ValidationIssue{
					{
						RuleName: "Quote_Submit_Requires_Complete_Lines",
						Message:  "Cannot submit: the quote needs at least one complete line item.",
					},
					{
						RuleName: "Quote_Submit_Requires_Service_Dates",
						Field:    "EndDate",
						Message:  "Cannot submit: complete the quote service dates.",
					},
				},
			},
			wantErrors: []string{
				"Cannot submit: the quote needs at least one complete line item.",
				"Cannot submit: complete the quote service dates.",
			},
		},
	}
	envelopes := []struct {
		name string
		body string
	}{
		{
			name: "legacy single",
			body: `{"ObjectApiName":"Account","RecordId":"record-1"}`,
		},
		{
			name: "batch wrapper",
			body: `{"ObjectApiName":"Account","requests":[{"actionType":"Submit","contextId":"record-1"}]}`,
		},
	}
	for _, rejection := range rejections {
		for _, envelope := range envelopes {
			t.Run(rejection.name+"/"+envelope.name, func(t *testing.T) {
				calls := 0
				service := approvalTestService(
					t,
					func(
						_ context.Context,
						_ string,
						request approvalsemantic.ExecuteRequest,
					) (approvalsemantic.ExecuteResponse, error) {
						calls++
						if request.Operation != approvalsemantic.OperationSubmit || request.TargetRecordID != "record-1" {
							t.Fatalf("request = %#v", request)
						}
						return approvalsemantic.ExecuteResponse{
							TargetRecordID:  request.TargetRecordID,
							SubmitRejection: &rejection.rejection,
							Receipts:        approvalReceipts(approvalsemantic.ExecuteCapabilityName),
						}, nil
					},
					func(context.Context, string, approvalsemantic.ReadRequest) (approvalsemantic.ReadResponse, error) {
						t.Fatal("submit rejection issued a diagnostic read")
						return approvalsemantic.ReadResponse{}, nil
					},
				)
				recorder := httptest.NewRecorder()
				service.ServeHTTP(recorder, approvalHTTPRequest(
					http.MethodPost, "/services/data/v63.0/process/approvals", envelope.body,
				))
				var results []approvalProcessResult
				if recorder.Code != http.StatusOK || calls != 1 ||
					json.Unmarshal(recorder.Body.Bytes(), &results) != nil || len(results) != 1 {
					t.Fatalf("status=%d calls=%d body=%s", recorder.Code, calls, recorder.Body.String())
				}
				result := results[0]
				if result.Success || result.EntityID != "record-1" || result.InstanceID != "" ||
					result.InstanceStatus != "Error" ||
					strings.Join(result.Errors, "\x00") != strings.Join(rejection.wantErrors, "\x00") ||
					len(result.ActorIDs) != 0 || len(result.NewWorkitemIDs) != 0 {
					t.Fatalf("result = %#v", result)
				}
			})
		}
	}
}

func TestApprovalSubmitRejectionErrorsFailClosedForUnreviewedShapes(t *testing.T) {
	tests := []struct {
		name      string
		rejection *approvalsemantic.SubmitRejection
	}{
		{name: "nil rejection"},
		{
			name: "unknown reason",
			rejection: &approvalsemantic.SubmitRejection{
				Reason: approvalsemantic.SubmitRejectionReason("UNREVIEWED_REASON"),
			},
		},
		{
			name: "validation without issues",
			rejection: &approvalsemantic.SubmitRejection{
				Reason: approvalsemantic.SubmitRejectionRecordValidationFailed,
			},
		},
		{
			name: "validation with empty message",
			rejection: &approvalsemantic.SubmitRejection{
				Reason: approvalsemantic.SubmitRejectionRecordValidationFailed,
				ValidationIssues: []approvalsemantic.ValidationIssue{
					{RuleName: "Rule_One"},
				},
			},
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			got := approvalSubmitRejectionErrors(test.rejection)
			if len(got) != 1 || got[0] != approvalUnexpectedErrorMessage {
				t.Fatalf("errors = %#v, want generic fallback", got)
			}
		})
	}
}

func TestApprovalExecuteRequestFieldMatrixMapsOrRejectsEveryKnownSelector(t *testing.T) {
	tests := []struct {
		name        string
		request     approvalProcessRequest
		want        approvalsemantic.ExecuteRequest
		wantFailure string
	}{
		{
			name: "submit supported fields",
			request: approvalProcessRequest{
				ActionType: "Submit", ContextID: "record-1", Comments: "review",
				SkipEntryCriteria: true,
			},
			want: approvalsemantic.ExecuteRequest{
				Operation: approvalsemantic.OperationSubmit, IdempotencyKey: "key",
				ObjectID: "Account", TargetRecordID: "record-1", Comments: "review",
				SkipEntryCriteria: true,
			},
		},
		{
			name:    "approve supported fields",
			request: approvalProcessRequest{ActionType: "Approve", ContextID: "work-1", Comments: "yes"},
			want: approvalsemantic.ExecuteRequest{
				Operation: approvalsemantic.OperationApprove, IdempotencyKey: "key",
				WorkItemID: "work-1", Comments: "yes",
			},
		},
		{
			name:    "reject supported fields",
			request: approvalProcessRequest{ActionType: "Reject", ContextID: "work-1", Comments: "no"},
			want: approvalsemantic.ExecuteRequest{
				Operation: approvalsemantic.OperationReject, IdempotencyKey: "key",
				WorkItemID: "work-1", Comments: "no",
			},
		},
		{
			name:    "reassign supported fields",
			request: approvalProcessRequest{ActionType: "Reassign", ContextID: "work-1", NextApproverIDs: []string{"actor-1"}},
			want: approvalsemantic.ExecuteRequest{
				Operation: approvalsemantic.OperationReassign, IdempotencyKey: "key",
				WorkItemID: "work-1", NextActorID: "actor-1",
			},
		},
		{
			name:    "recall supported fields",
			request: approvalProcessRequest{ActionType: "Removed", ContextID: "work-1", Comments: "withdraw"},
			want: approvalsemantic.ExecuteRequest{
				Operation: approvalsemantic.OperationRecall, IdempotencyKey: "key",
				WorkItemID: "work-1", Comments: "withdraw",
			},
		},
		{
			name:        "submit actor override",
			request:     approvalProcessRequest{ActionType: "Submit", ContextID: "record-1", ContextActorID: "actor-1"},
			wantFailure: "contextActorId is not supported for Submit action",
		},
		{
			name:        "submit definition override",
			request:     approvalProcessRequest{ActionType: "Submit", ContextID: "record-1", ProcessDefinitionNameOrID: "definition-1"},
			wantFailure: "processDefinitionNameOrId is not supported for Submit action",
		},
		{
			name:        "submit next approver override",
			request:     approvalProcessRequest{ActionType: "Submit", ContextID: "record-1", NextApproverIDs: []string{"actor-1"}},
			wantFailure: "nextApproverIds is only supported for Reassign action",
		},
		{
			name:        "approve skip entry criteria",
			request:     approvalProcessRequest{ActionType: "Approve", ContextID: "work-1", SkipEntryCriteria: true},
			wantFailure: "skipEntryCriteria is only supported for Submit action",
		},
		{
			name:        "reject actor override",
			request:     approvalProcessRequest{ActionType: "Reject", ContextID: "work-1", ContextActorID: "actor-1"},
			wantFailure: "contextActorId is not supported for Reject action",
		},
		{
			name:        "recall definition override",
			request:     approvalProcessRequest{ActionType: "Removed", ContextID: "work-1", ProcessDefinitionNameOrID: "definition-1"},
			wantFailure: "processDefinitionNameOrId is not supported for Removed action",
		},
		{
			name:        "reassign comments",
			request:     approvalProcessRequest{ActionType: "Reassign", ContextID: "work-1", NextApproverIDs: []string{"actor-1"}, Comments: "move"},
			wantFailure: "comments are not supported for Reassign action",
		},
		{
			name:        "reassign multiple actors",
			request:     approvalProcessRequest{ActionType: "Reassign", ContextID: "work-1", NextApproverIDs: []string{"actor-1", "actor-2"}},
			wantFailure: "Exactly one non-empty nextApproverId is required for Reassign action",
		},
		{
			name:        "reassign empty actor",
			request:     approvalProcessRequest{ActionType: "Reassign", ContextID: "work-1", NextApproverIDs: []string{""}},
			wantFailure: "Exactly one non-empty nextApproverId is required for Reassign action",
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			got, failure := approvalExecuteRequest(test.request, "Account", "key")
			if failure != test.wantFailure {
				t.Fatalf("failure = %q, want %q", failure, test.wantFailure)
			}
			if test.wantFailure == "" && got != test.want {
				t.Fatalf("request = %#v, want %#v", got, test.want)
			}
		})
	}
}

func TestApprovalEnvelopesDoNotDiscardKnownUnsupportedSelectors(t *testing.T) {
	service := approvalTestService(
		t,
		func(context.Context, string, approvalsemantic.ExecuteRequest) (approvalsemantic.ExecuteResponse, error) {
			t.Fatal("unsupported legacy selector reached Runtime")
			return approvalsemantic.ExecuteResponse{}, nil
		},
		func(context.Context, string, approvalsemantic.ReadRequest) (approvalsemantic.ReadResponse, error) {
			t.Fatal("execute route reached Approval read")
			return approvalsemantic.ReadResponse{}, nil
		},
	)
	tests := []struct {
		name   string
		body   string
		want   string
		status int
	}{
		{
			name:   "legacy next approver",
			body:   `{"ObjectApiName":"Lead","RecordId":"lead-1","nextApproverIds":["actor-1"]}`,
			want:   "nextApproverIds is only supported for Reassign action",
			status: http.StatusBadRequest,
		},
		{
			name:   "legacy definition override",
			body:   `{"ObjectApiName":"Lead","RecordId":"lead-1","processDefinitionNameOrId":"definition-1"}`,
			want:   "processDefinitionNameOrId is not supported for Submit action",
			status: http.StatusBadRequest,
		},
		{
			name:   "legacy context actor override",
			body:   `{"ObjectApiName":"Lead","RecordId":"lead-1","contextActorId":"actor-1"}`,
			want:   "contextActorId is not supported for Submit action",
			status: http.StatusBadRequest,
		},
		{
			name:   "wrapper next approver",
			body:   `{"ObjectApiName":"Lead","requests":[{"actionType":"Submit","contextId":"lead-1","nextApproverIds":["actor-1"]}]}`,
			want:   "nextApproverIds is only supported for Reassign action",
			status: http.StatusOK,
		},
		{
			name:   "wrapper definition override",
			body:   `{"ObjectApiName":"Lead","requests":[{"actionType":"Submit","contextId":"lead-1","processDefinitionNameOrId":"definition-1"}]}`,
			want:   "processDefinitionNameOrId is not supported for Submit action",
			status: http.StatusOK,
		},
		{
			name:   "wrapper context actor override",
			body:   `{"ObjectApiName":"Lead","requests":[{"actionType":"Submit","contextId":"lead-1","contextActorId":"actor-1"}]}`,
			want:   "contextActorId is not supported for Submit action",
			status: http.StatusOK,
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			recorder := httptest.NewRecorder()
			service.ServeHTTP(recorder, approvalHTTPRequest(
				http.MethodPost, "/services/data/v63.0/process/approvals", test.body,
			))
			if recorder.Code != test.status || !strings.Contains(recorder.Body.String(), test.want) {
				t.Fatalf("status=%d body=%s, want %q", recorder.Code, recorder.Body.String(), test.want)
			}
		})
	}
}

func TestApprovalEnvelopeIdentifiersAreOperationScoped(t *testing.T) {
	var calls int
	service := approvalTestService(
		t,
		func(_ context.Context, _ string, request approvalsemantic.ExecuteRequest) (approvalsemantic.ExecuteResponse, error) {
			calls++
			return approvalExecuteResponse(request), nil
		},
		func(context.Context, string, approvalsemantic.ReadRequest) (approvalsemantic.ReadResponse, error) {
			t.Fatal("execute route reached Approval read")
			return approvalsemantic.ReadResponse{}, nil
		},
	)
	tests := []struct {
		name      string
		body      string
		status    int
		want      string
		wantCalls int
	}{
		{
			name:      "legacy submit RecordId alias",
			body:      `{"ObjectApiName":"Lead","actionType":"Submit","RecordId":"record-1"}`,
			status:    http.StatusOK,
			want:      `"entityId":"record-1"`,
			wantCalls: 1,
		},
		{
			name:      "legacy submit matching aliases",
			body:      `{"ObjectApiName":"Lead","actionType":"Submit","contextId":"record-1","RecordId":"record-1"}`,
			status:    http.StatusOK,
			want:      `"entityId":"record-1"`,
			wantCalls: 1,
		},
		{
			name:   "legacy submit conflicting aliases",
			body:   `{"ObjectApiName":"Lead","actionType":"Submit","contextId":"record-1","RecordId":"record-2"}`,
			status: http.StatusBadRequest,
			want:   "contextId and RecordId must identify the same record",
		},
		{
			name:   "legacy approve cannot reinterpret RecordId",
			body:   `{"ObjectApiName":"Lead","actionType":"Approve","RecordId":"record-1"}`,
			status: http.StatusOK,
			want:   "RecordId is only supported for Submit action",
		},
		{
			name:   "legacy reject cannot reinterpret RecordId",
			body:   `{"ObjectApiName":"Lead","actionType":"Reject","RecordId":"record-1"}`,
			status: http.StatusOK,
			want:   "RecordId is only supported for Submit action",
		},
		{
			name:   "legacy reassign cannot reinterpret RecordId",
			body:   `{"ObjectApiName":"Lead","actionType":"Reassign","RecordId":"record-1","nextApproverIds":["actor-1"]}`,
			status: http.StatusOK,
			want:   "RecordId is only supported for Submit action",
		},
		{
			name:   "legacy recall cannot reinterpret RecordId",
			body:   `{"ObjectApiName":"Lead","actionType":"Removed","RecordId":"record-1"}`,
			status: http.StatusOK,
			want:   "RecordId is only supported for Submit action",
		},
		{
			name:   "legacy work item rejects conflicting RecordId",
			body:   `{"ObjectApiName":"Lead","actionType":"Approve","contextId":"work-1","RecordId":"record-1"}`,
			status: http.StatusOK,
			want:   "RecordId is only supported for Submit action",
		},
		{
			name:   "wrapper does not accept legacy RecordId alias",
			body:   `{"ObjectApiName":"Lead","requests":[{"actionType":"Submit","RecordId":"record-1"}]}`,
			status: http.StatusOK,
			want:   "RecordId is only supported by legacy single Submit requests",
		},
		{
			name:   "wrapper work item rejects RecordId",
			body:   `{"ObjectApiName":"Lead","requests":[{"actionType":"Approve","contextId":"work-1","RecordId":"record-1"}]}`,
			status: http.StatusOK,
			want:   "RecordId is only supported by legacy single Submit requests",
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			before := calls
			recorder := httptest.NewRecorder()
			service.ServeHTTP(recorder, approvalHTTPRequest(
				http.MethodPost, "/services/data/v63.0/process/approvals", test.body,
			))
			if recorder.Code != test.status || calls-before != test.wantCalls ||
				!strings.Contains(recorder.Body.String(), test.want) {
				t.Fatalf(
					"status=%d calls=%d body=%s, want status=%d calls=%d containing %q",
					recorder.Code, calls-before, recorder.Body.String(), test.status, test.wantCalls, test.want,
				)
			}
		})
	}
}

func TestApprovalLegacySubmitUsesObjectRecordAndReplaysAcrossServiceRestart(t *testing.T) {
	var requests []approvalsemantic.ExecuteRequest
	execute := func(
		_ context.Context,
		_ string,
		request approvalsemantic.ExecuteRequest,
	) (approvalsemantic.ExecuteResponse, error) {
		requests = append(requests, request)
		return approvalExecuteResponse(request), nil
	}
	read := func(context.Context, string, approvalsemantic.ReadRequest) (approvalsemantic.ReadResponse, error) {
		return approvalsemantic.ReadResponse{}, errors.New("unexpected read")
	}
	for range 2 {
		service := approvalTestService(t, execute, read)
		request := approvalHTTPRequest(
			http.MethodPost, "/services/data/v62.0/process/approvals",
			`{"ObjectApiName":"Lead","RecordId":"lead-1","Comments":"review","skipEntryCriteria":true}`,
		)
		request.Header.Set(crmMutationIdempotencyHeader, "stable-approval")
		recorder := httptest.NewRecorder()
		service.ServeHTTP(recorder, request)
		if recorder.Code != http.StatusOK {
			t.Fatalf("status = %d, body=%s", recorder.Code, recorder.Body.String())
		}
	}
	if len(requests) != 2 || requests[0] != requests[1] ||
		requests[0].ObjectID != "Lead" || requests[0].TargetRecordID != "lead-1" ||
		requests[0].Comments != "review" || !requests[0].SkipEntryCriteria {
		t.Fatalf("requests = %#v", requests)
	}
}

func TestApprovalReadsProjectExistingPendingLatestAndStepWire(t *testing.T) {
	var seen []approvalsemantic.ReadRequest
	read := func(
		_ context.Context,
		bearer string,
		request approvalsemantic.ReadRequest,
	) (approvalsemantic.ReadResponse, error) {
		if bearer != "user-secret" {
			t.Fatalf("bearer = %q", bearer)
		}
		seen = append(seen, request)
		response := approvalsemantic.ReadResponse{
			Receipts: approvalReceipts(approvalsemantic.ReadCapabilityName),
		}
		switch request.Kind {
		case approvalsemantic.ReadPendingForCaller:
			response.Pending = []approvalsemantic.PendingItem{{
				WorkItemID: "work-1", InstanceID: "instance-1", DefinitionID: "definition-1",
				TargetRecordID: "record-1", ObjectID: "Account", SubmittedByID: "submitter-1",
				SubmittedByName: "Submitter", SubmittedAt: "2026-06-12T10:00:00Z",
				ActorID: "actor-1", OriginalActorID: "actor-original",
				Comments: "please review", RecordName: "Acme",
			}}
		case approvalsemantic.ReadLatestForRecord:
			response.Latest = &approvalsemantic.InstanceSummary{
				InstanceID: "instance-1", DefinitionID: "definition-1",
				TargetRecordID: request.TargetRecordID, State: approvalsemantic.InstanceStatePending,
				SubmittedByID: "submitter-1", SubmittedByName: "Submitter",
				CreatedAt: "2026-06-12T10:00:00Z",
				PendingWorkItems: []approvalsemantic.PendingWorkItem{{
					WorkItemID: "work-1", ActorID: "actor-1", ActorName: "Approver",
					OriginalActorID: "actor-original", OriginalActorName: "Original",
				}},
			}
		case approvalsemantic.ReadStepDisplayForRecord:
			response.Steps = []approvalsemantic.StepDisplay{{
				StepID: "node-1", StepOrder: 2, StepName: "Manager_Approval",
				StepLabel: "Manager Approval", HasCriteria: true,
				NoMatchBehavior: approvalsemantic.StepNoMatchNext,
			}}
		}
		return response, nil
	}
	service := approvalTestService(
		t,
		func(context.Context, string, approvalsemantic.ExecuteRequest) (approvalsemantic.ExecuteResponse, error) {
			t.Fatal("read route reached Approval execute")
			return approvalsemantic.ExecuteResponse{}, nil
		},
		read,
	)
	tests := []struct {
		path string
		want string
	}{
		{
			path: "/services/data/v63.0/process/approvals/pending",
			want: `[{"workItemId":"work-1","processInstanceId":"instance-1","processDefinitionId":"definition-1","targetObjectId":"record-1","objectApiName":"Account","submittedById":"submitter-1","submittedByName":"Submitter","submittedDate":"2026-06-12T10:00:00Z","actorId":"actor-1","originalActorId":"actor-original","comments":"please review","recordName":"Acme"}]`,
		},
		{
			path: "/services/data/v62.0/process/approvals/records/record-1/instance",
			want: `{"id":"instance-1","processDefinitionId":"definition-1","targetObjectId":"record-1","status":"Pending","submittedById":"submitter-1","submittedByName":"Submitter","createdDate":"2026-06-12T10:00:00Z","pendingWorkItems":[{"id":"work-1","actorId":"actor-1","actorName":"Approver","originalActorId":"actor-original","originalActorName":"Original"}]}`,
		},
		{
			path: "/services/data/v61.0/process/approvals/records/record-1/step-display",
			want: `[{"processNodeId":"node-1","stepOrder":2,"stepName":"Manager_Approval","stepLabel":"Manager Approval","hasEntryCriteria":true,"ifCriteriaNotMet":"GotoNextStep"}]`,
		},
	}
	for _, test := range tests {
		recorder := httptest.NewRecorder()
		service.ServeHTTP(recorder, approvalHTTPRequest(http.MethodGet, test.path, ""))
		if recorder.Code != http.StatusOK || strings.TrimSpace(recorder.Body.String()) != test.want {
			t.Fatalf("path=%s status=%d body=%s", test.path, recorder.Code, recorder.Body.String())
		}
	}
	if len(seen) != 3 || seen[0].Kind != approvalsemantic.ReadPendingForCaller ||
		seen[1].Kind != approvalsemantic.ReadLatestForRecord ||
		seen[2].Kind != approvalsemantic.ReadStepDisplayForRecord ||
		seen[1].TargetRecordID != "record-1" || seen[2].TargetRecordID != "record-1" {
		t.Fatalf("reads = %#v", seen)
	}
}

func TestApprovalBoundaryRejectsLocallyAndMasksRuntimeFailures(t *testing.T) {
	var calls int
	execute := func(
		_ context.Context,
		_ string,
		request approvalsemantic.ExecuteRequest,
	) (approvalsemantic.ExecuteResponse, error) {
		calls++
		if request.TargetRecordID == "internal" {
			return approvalsemantic.ExecuteResponse{}, errors.New("raw sql detail")
		}
		return approvalExecuteResponse(request), nil
	}
	service := approvalTestService(
		t, execute,
		func(context.Context, string, approvalsemantic.ReadRequest) (approvalsemantic.ReadResponse, error) {
			return approvalsemantic.ReadResponse{}, &commonsemantic.Failure{Category: commonsemantic.FailureForbidden}
		},
	)
	tests := []struct {
		name   string
		method string
		path   string
		body   string
		mutate func(*http.Request)
		status int
		calls  int
		text   string
	}{
		{name: "invalid JSON", method: http.MethodPost, path: "/services/data/v63.0/process/approvals", body: `{`, status: 400, calls: 0, text: "JSON_PARSER_ERROR"},
		{name: "empty wrapper", method: http.MethodPost, path: "/services/data/v63.0/process/approvals", body: `{"requests":[]}`, status: 400, calls: 0, text: "At least one"},
		{name: "single submit delegates object resolution", method: http.MethodPost, path: "/services/data/v63.0/process/approvals", body: `{"RecordId":"record-1"}`, status: 200, calls: 1, text: `"success":true`},
		{name: "duplicate identity", method: http.MethodPost, path: "/services/data/v63.0/process/approvals", body: `{"ObjectApiName":"Account","RecordId":"record-1"}`, mutate: func(request *http.Request) { request.Header.Add(crmMutationIdempotencyHeader, "second") }, status: 400, calls: 0, text: "Idempotency-Key"},
		{name: "wrong method", method: http.MethodGet, path: "/services/data/v63.0/process/approvals", status: 405, calls: 0, text: "Method not allowed"},
		{name: "batch internal masked", method: http.MethodPost, path: "/services/data/v63.0/process/approvals", body: `{"ObjectApiName":"Account","requests":[{"actionType":"Submit","contextId":"internal"}]}`, status: 200, calls: 1, text: approvalUnexpectedErrorMessage},
		{name: "read forbidden", method: http.MethodGet, path: "/services/data/v63.0/process/approvals/pending", status: 403, calls: 0, text: "Insufficient access"},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			before := calls
			request := approvalHTTPRequest(test.method, test.path, test.body)
			request.Header.Set(crmMutationIdempotencyHeader, "one")
			if test.mutate != nil {
				test.mutate(request)
			}
			recorder := httptest.NewRecorder()
			service.ServeHTTP(recorder, request)
			if recorder.Code != test.status || calls-before != test.calls ||
				!strings.Contains(recorder.Body.String(), test.text) ||
				strings.Contains(recorder.Body.String(), "raw sql detail") {
				t.Fatalf("status=%d calls=%d body=%s", recorder.Code, calls-before, recorder.Body.String())
			}
		})
	}
}

func TestApprovalSingleSubmitPreservesOnlyOperationConflictAsHTTP409(t *testing.T) {
	tests := []struct {
		name       string
		failure    *commonsemantic.Failure
		wantStatus int
		wantCode   string
	}{
		{
			name: "already-pending operation conflict",
			failure: &commonsemantic.Failure{
				Category:         commonsemantic.FailureInvalidRequest,
				InvalidComponent: commonsemantic.InvalidComponentOperation,
			},
			wantStatus: http.StatusConflict,
			wantCode:   `"errorCode":"CONFLICT"`,
		},
		{
			name: "ordinary invalid command",
			failure: &commonsemantic.Failure{
				Category:         commonsemantic.FailureInvalidRequest,
				InvalidComponent: commonsemantic.InvalidComponentCommandData,
			},
			wantStatus: http.StatusBadRequest,
			wantCode:   `"errorCode":"INVALID_REQUEST"`,
		},
		{
			name: "missing record",
			failure: &commonsemantic.Failure{
				Category: commonsemantic.FailureMissing,
			},
			wantStatus: http.StatusNotFound,
			wantCode:   `"errorCode":"NOT_FOUND"`,
		},
		{
			name: "catalog stale",
			failure: &commonsemantic.Failure{
				Category:  commonsemantic.FailureStaleState,
				StaleAxis: commonsemantic.StaleAxisCatalog,
			},
			wantStatus: http.StatusServiceUnavailable,
			wantCode:   `"errorCode":"UNKNOWN_EXCEPTION"`,
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			calls := 0
			service := approvalTestService(
				t,
				func(context.Context, string, approvalsemantic.ExecuteRequest) (approvalsemantic.ExecuteResponse, error) {
					calls++
					return approvalsemantic.ExecuteResponse{}, test.failure
				},
				func(context.Context, string, approvalsemantic.ReadRequest) (approvalsemantic.ReadResponse, error) {
					t.Fatal("submit conflict issued a diagnostic read")
					return approvalsemantic.ReadResponse{}, nil
				},
			)
			request := approvalHTTPRequest(
				http.MethodPost, "/services/data/v63.0/process/approvals",
				`{"ObjectApiName":"Account","RecordId":"record-1"}`,
			)
			request.Header.Set(crmMutationIdempotencyHeader, "approval-conflict")
			recorder := httptest.NewRecorder()
			service.ServeHTTP(recorder, request)
			if recorder.Code != test.wantStatus || calls != 1 ||
				!strings.Contains(recorder.Body.String(), test.wantCode) {
				t.Fatalf("status=%d calls=%d body=%s", recorder.Code, calls, recorder.Body.String())
			}
			if test.wantStatus == http.StatusConflict &&
				!strings.Contains(recorder.Body.String(), approvalAlreadyPendingMessage) {
				t.Fatalf("conflict body=%s", recorder.Body.String())
			}
		})
	}
}

func TestApprovalLocalBatchFailuresDoNotClaimEntityOwnership(t *testing.T) {
	calls := 0
	service := approvalTestService(
		t,
		func(_ context.Context, _ string, request approvalsemantic.ExecuteRequest) (approvalsemantic.ExecuteResponse, error) {
			calls++
			if request.Operation != approvalsemantic.OperationSubmit || request.ObjectID != "" || request.TargetRecordID != "record-1" {
				t.Fatalf("Runtime request = %#v", request)
			}
			return approvalExecuteResponse(request), nil
		},
		func(context.Context, string, approvalsemantic.ReadRequest) (approvalsemantic.ReadResponse, error) {
			t.Fatal("execute route reached Approval read")
			return approvalsemantic.ReadResponse{}, nil
		},
	)
	request := approvalHTTPRequest(
		http.MethodPost, "/services/data/v63.0/process/approvals",
		`{"requests":[`+
			`{"actionType":"Submit","contextId":"record-1"},`+
			`{"actionType":"Unknown","contextId":"record-2"}`+
			`]}`,
	)
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, request)
	var results []approvalProcessResult
	if recorder.Code != http.StatusOK || calls != 1 || json.Unmarshal(recorder.Body.Bytes(), &results) != nil ||
		len(results) != 2 || !results[0].Success || results[0].EntityID != "record-1" ||
		results[1].Success || results[1].EntityID != "" || len(results[1].Errors) != 1 ||
		results[1].Errors[0] != "Unsupported actionType: Unknown" {
		t.Fatalf("status=%d calls=%d body=%s", recorder.Code, calls, recorder.Body.String())
	}
}

func TestApprovalCancellationAndInvalidRuntimeResponseFailClosed(t *testing.T) {
	var calls int
	service := approvalTestService(
		t,
		func(
			_ context.Context,
			_ string,
			request approvalsemantic.ExecuteRequest,
		) (approvalsemantic.ExecuteResponse, error) {
			calls++
			response := approvalExecuteResponse(request)
			response.Receipts.CapabilityVersion = "runtime.wrong.v1"
			return response, nil
		},
		func(context.Context, string, approvalsemantic.ReadRequest) (approvalsemantic.ReadResponse, error) {
			return approvalsemantic.ReadResponse{}, nil
		},
	)
	invalid := approvalHTTPRequest(
		http.MethodPost, "/services/data/v63.0/process/approvals",
		`{"ObjectApiName":"Account","RecordId":"record-1"}`,
	)
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, invalid)
	if recorder.Code != http.StatusInternalServerError || calls != 1 ||
		!strings.Contains(recorder.Body.String(), crmwireInternalErrorCodeForTest()) {
		t.Fatalf("invalid response status=%d calls=%d body=%s", recorder.Code, calls, recorder.Body.String())
	}

	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	canceled := approvalHTTPRequest(
		http.MethodPost, "/services/data/v63.0/process/approvals",
		`{"ObjectApiName":"Account","RecordId":"record-1"}`,
	).WithContext(ctx)
	recorder = httptest.NewRecorder()
	service.ServeHTTP(recorder, canceled)
	if recorder.Code != 499 || calls != 1 || !strings.Contains(recorder.Body.String(), "Request canceled") {
		t.Fatalf("canceled status=%d calls=%d body=%s", recorder.Code, calls, recorder.Body.String())
	}
}

func TestApprovalRawContextFailuresKeepTheirHTTPClassification(t *testing.T) {
	tests := []struct {
		name       string
		runtimeErr error
		wantStatus int
		wantBody   string
	}{
		{name: "canceled", runtimeErr: context.Canceled, wantStatus: 499, wantBody: "Request canceled"},
		{name: "deadline", runtimeErr: context.DeadlineExceeded, wantStatus: http.StatusGatewayTimeout, wantBody: "Request deadline exceeded"},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			service := approvalTestService(
				t,
				func(
					context.Context,
					string,
					approvalsemantic.ExecuteRequest,
				) (approvalsemantic.ExecuteResponse, error) {
					return approvalsemantic.ExecuteResponse{}, test.runtimeErr
				},
				func(
					context.Context,
					string,
					approvalsemantic.ReadRequest,
				) (approvalsemantic.ReadResponse, error) {
					return approvalsemantic.ReadResponse{}, test.runtimeErr
				},
			)
			for _, request := range []*http.Request{
				approvalHTTPRequest(
					http.MethodPost, "/services/data/v63.0/process/approvals",
					`{"ObjectApiName":"Account","RecordId":"record-1"}`,
				),
				approvalHTTPRequest(
					http.MethodGet, "/services/data/v63.0/process/approvals/pending", "",
				),
			} {
				recorder := httptest.NewRecorder()
				service.ServeHTTP(recorder, request)
				if recorder.Code != test.wantStatus ||
					!strings.Contains(recorder.Body.String(), test.wantBody) {
					t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
				}
			}
		})
	}
}

func TestApprovalRecallFailsClosedWithoutTrustedActingPrincipal(t *testing.T) {
	client, err := NewRuntimeApprovalsClient(
		func(
			_ context.Context,
			_ string,
			request approvalsemantic.ExecuteRequest,
		) (approvalsemantic.ExecuteResponse, error) {
			response := approvalExecuteResponse(request)
			response.ActingPrincipalID = ""
			return response, nil
		},
		func(context.Context, string, approvalsemantic.ReadRequest) (approvalsemantic.ReadResponse, error) {
			return approvalsemantic.ReadResponse{}, nil
		},
	)
	if err != nil {
		t.Fatal(err)
	}
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	_, err = client.Execute(ctx, "user-secret", approvalsemantic.ExecuteRequest{
		Operation: approvalsemantic.OperationRecall, IdempotencyKey: "recall-1", WorkItemID: "work-1",
	})
	var failure *commonsemantic.Failure
	if !errors.As(err, &failure) || failure.Category != commonsemantic.FailureInternal {
		t.Fatalf("missing trusted acting principal error = %v, want INTERNAL", err)
	}
}

func crmwireInternalErrorCodeForTest() string { return "INTERNAL_ERROR" }

func TestApprovalPortCompositionIsAllOrNothingAndOptional(t *testing.T) {
	base := permissionMetadataServiceConfig(nil, nil)
	base.RuntimeExecuteApproval = func(
		context.Context,
		string,
		approvalsemantic.ExecuteRequest,
	) (approvalsemantic.ExecuteResponse, error) {
		return approvalsemantic.ExecuteResponse{}, nil
	}
	if service, err := NewService(base); err == nil || service != nil {
		t.Fatalf("partial Approval composition = %#v, %v", service, err)
	}
	base.RuntimeExecuteApproval = nil
	service, err := NewService(base)
	if err != nil {
		t.Fatal(err)
	}
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, approvalHTTPRequest(
		http.MethodGet, "/services/data/v63.0/process/approvals/pending", "",
	))
	if recorder.Code != http.StatusNotFound {
		t.Fatalf("uninstalled status=%d body=%s", recorder.Code, recorder.Body.String())
	}
}

func TestApprovalReadLatestWithoutInstanceReturnsJSONNull(t *testing.T) {
	service := approvalTestService(
		t,
		func(context.Context, string, approvalsemantic.ExecuteRequest) (approvalsemantic.ExecuteResponse, error) {
			return approvalsemantic.ExecuteResponse{}, nil
		},
		func(
			_ context.Context,
			_ string,
			request approvalsemantic.ReadRequest,
		) (approvalsemantic.ReadResponse, error) {
			if request.TargetRecordID != "record none" {
				t.Fatalf("request = %#v", request)
			}
			return approvalsemantic.ReadResponse{
				Receipts: approvalReceipts(approvalsemantic.ReadCapabilityName),
			}, nil
		},
	)
	recorder := httptest.NewRecorder()
	service.ServeHTTP(recorder, approvalHTTPRequest(
		http.MethodGet,
		"/services/data/v63.0/process/approvals/records/record%20none/instance",
		"",
	))
	if recorder.Code != http.StatusOK || strings.TrimSpace(recorder.Body.String()) != "null" {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
}

func TestApprovalDTOsRemainJSONSerializable(t *testing.T) {
	// Guard accidental introduction of product-side opaque engine values into
	// the public projection. Every DTO remains ordinary L1 JSON.
	if _, err := json.Marshal(approvalProcessResult{ActorIDs: []string{}, Errors: []string{}, NewWorkitemIDs: []string{}}); err != nil {
		t.Fatal(err)
	}
	if _, err := json.Marshal(approvalPendingViewDTO{}); err != nil {
		t.Fatal(err)
	}
}

func TestApprovalClientRejectsContextWithoutDeadline(t *testing.T) {
	client, err := NewRuntimeApprovalsClient(
		func(context.Context, string, approvalsemantic.ExecuteRequest) (approvalsemantic.ExecuteResponse, error) {
			t.Fatal("invalid request reached execute")
			return approvalsemantic.ExecuteResponse{}, nil
		},
		func(context.Context, string, approvalsemantic.ReadRequest) (approvalsemantic.ReadResponse, error) {
			t.Fatal("invalid request reached read")
			return approvalsemantic.ReadResponse{}, nil
		},
	)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := client.Execute(context.Background(), "bearer", approvalsemantic.ExecuteRequest{}); err == nil {
		t.Fatal("deadline-free execute was accepted")
	}
	if _, err := client.Read(context.Background(), "bearer", approvalsemantic.ReadRequest{}); err == nil {
		t.Fatal("deadline-free read was accepted")
	}
}

func TestApprovalBatchDeadlineStopsFurtherRuntimeCalls(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	var calls int
	client, err := NewRuntimeApprovalsClient(
		func(context.Context, string, approvalsemantic.ExecuteRequest) (approvalsemantic.ExecuteResponse, error) {
			calls++
			return approvalsemantic.ExecuteResponse{}, &commonsemantic.Failure{
				Category: commonsemantic.FailureDeadlineExceeded,
			}
		},
		func(context.Context, string, approvalsemantic.ReadRequest) (approvalsemantic.ReadResponse, error) {
			return approvalsemantic.ReadResponse{}, nil
		},
	)
	if err != nil {
		t.Fatal(err)
	}
	request := approvalsemantic.ExecuteRequest{
		Operation: approvalsemantic.OperationSubmit, IdempotencyKey: "key",
		ObjectID: "Account", TargetRecordID: "record-1",
	}
	if _, err := client.Execute(ctx, "bearer", request); err == nil || calls != 1 {
		t.Fatalf("Execute = calls %d, err %v", calls, err)
	}
}
