package dialectsfcrm

import (
	"archive/zip"
	"bytes"
	"context"
	"errors"
	"strings"
	"testing"
	"time"

	"github.com/nexuscrm/dialect-sf-crm/wire"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/dialect"
	"github.com/nexuscrm/shared/pkg/models"
)

func TestMetadataDeployRequestNeutralHandlerStartsJobAndAppliesApprovalProcessParent(t *testing.T) {
	rawXML := `
<ApprovalProcess>
  <label>Deal Approval</label>
  <active>true</active>
  <description>Approve big deals</description>
  <recordEditability>Locked</recordEditability>
  <finalApprovalRecordLock>true</finalApprovalRecordLock>
  <entryCriteria>
    <criteriaItems>
      <field>Amount</field>
      <operation>greaterThan</operation>
      <value>1000</value>
    </criteriaItems>
  </entryCriteria>
</ApprovalProcess>`
	zipBytes := metadataDeployZip(t, "approvalProcesses/Opportunity.Deal_Approval.approvalProcess", rawXML)
	body, contentType := multipartBody(t, map[string]string{"file": "deploy.zip"}, zipBytes)
	deploy := &fakeMetadataDeploy{jobID: "1234567890abcde", completed: make(chan contract.MetadataDeployResult, 1)}

	resp, err := MetadataDeployRequestNeutralHandler(context.Background(), &dialect.Request{
		Header:     map[string][]string{"Content-Type": []string{contentType}},
		BodyStream: bytes.NewReader(body),
		User:       &models.UserSession{ID: "005000000000001"},
	}, metadataDeployDeps(deploy))
	if err != nil {
		t.Fatalf("MetadataDeployRequestNeutralHandler err = %v", err)
	}
	assertBody(t, "metadata-deploy-start", resp.Body, "",
		`{"done":false,"id":"0Af1234567890abcde"}`)

	select {
	case result := <-deploy.completed:
		if !result.Done || result.Status != "Succeeded" || !result.Success {
			t.Fatalf("completed result = %+v, want succeeded", result)
		}
	case <-time.After(time.Second):
		t.Fatal("metadata deploy did not complete")
	}

	req := deploy.applyReq
	if req.ObjectAPIName != constants.TableProcessDefinition {
		t.Fatalf("ObjectAPIName = %q, want ProcessDefinition", req.ObjectAPIName)
	}
	if req.MatchFields["DeveloperName"] != "Deal_Approval" || req.MatchFields["TableEnumOrId"] != "Opportunity" {
		t.Fatalf("match fields = %#v", req.MatchFields)
	}
	if req.RequiredCreateLifecycleCapability != ApprovalProcessChildSyncCapability ||
		req.RequiredUpdateLifecycleCapability != ApprovalProcessChildSyncCapability {
		t.Fatalf("required lifecycle capabilities = %q/%q", req.RequiredCreateLifecycleCapability, req.RequiredUpdateLifecycleCapability)
	}
	create := req.CreateRecord
	if create["Name"] != "Deal Approval" || create["TableEnumOrId"] != "Opportunity" ||
		create["State"] != "Active" || create["IsActive"] != true ||
		create["AllowRecall"] != true ||
		create["EntryFilter"] != "Amount greaterThan 1000" ||
		create["LockType"] != "Locked" || create["FinalApprovalRecordLock"] != true {
		t.Fatalf("create record = %#v", create)
	}
	for _, forbidden := range []string{"ProcessDefinitionId", "ProcessNodeId", "ActionType", "ActionEvent", "ApproverType", "ApproverUserId"} {
		if _, ok := create[forbidden]; ok {
			t.Fatalf("metadata deploy parent record unexpectedly materialized child field %s: %#v", forbidden, create)
		}
	}
}

func TestMetadataDeployRequestNeutralHandlerAppliesOwnerlessApprovalProcessDestructiveChanges(t *testing.T) {
	destructiveXML := `<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
  <types>
    <members>Opportunity.Deal_Approval</members>
    <name>ApprovalProcess</name>
  </types>
</Package>`
	zipBytes := metadataDeployZipFiles(t, map[string]string{
		"destructiveChanges.xml": destructiveXML,
		"package.xml":            `<Package xmlns="http://soap.sforce.com/2006/04/metadata"><version>63.0</version></Package>`,
	})
	body, contentType := multipartBody(t, map[string]string{"file": "deploy.zip"}, zipBytes)
	deploy := &fakeMetadataDeploy{jobID: "1234567890abcde", completed: make(chan contract.MetadataDeployResult, 1)}

	resp, err := MetadataDeployRequestNeutralHandler(context.Background(), &dialect.Request{
		Header:     map[string][]string{"Content-Type": []string{contentType}},
		BodyStream: bytes.NewReader(body),
		User:       &models.UserSession{ID: "005000000000001"},
	}, metadataDeployDeps(deploy))
	if err != nil {
		t.Fatalf("MetadataDeployRequestNeutralHandler err = %v", err)
	}
	assertBody(t, "metadata-deploy-destructive-start", resp.Body, "",
		`{"done":false,"id":"0Af1234567890abcde"}`)

	select {
	case result := <-deploy.completed:
		if !result.Done || result.Status != "Succeeded" || !result.Success {
			t.Fatalf("completed result = %+v, want succeeded", result)
		}
	case <-time.After(time.Second):
		t.Fatal("metadata deploy did not complete")
	}

	req := deploy.applyReq
	if deploy.applyCalls != 1 {
		t.Fatalf("apply calls = %d, want 1", deploy.applyCalls)
	}
	if req.Operation != contract.MetadataDeployRecordOperationDelete {
		t.Fatalf("operation = %q, want delete", req.Operation)
	}
	if req.ObjectAPIName != constants.TableProcessDefinition {
		t.Fatalf("ObjectAPIName = %q, want ProcessDefinition", req.ObjectAPIName)
	}
	if len(req.MatchFields) != 3 || req.MatchFields["DeveloperName"] != "Deal_Approval" ||
		req.MatchFields["TableEnumOrId"] != "Opportunity" || req.MatchFields["OwnerPack"] != "" {
		t.Fatalf("match fields = %#v, want exact ownerless approval-process identity", req.MatchFields)
	}
	if req.CreateRecord != nil || req.UpdateRecord != nil || req.RequiredCreateLifecycleCapability != "" || req.RequiredUpdateLifecycleCapability != "" {
		t.Fatalf("delete request unexpectedly carries upsert payload: %+v", req)
	}
}

func TestMetadataDeployRejectsDestructiveManifestsOutsideSingleApprovalProcessContract(t *testing.T) {
	tests := []struct {
		name     string
		manifest string
		problem  string
	}{
		{
			name: "case-equivalent members",
			manifest: `<Package xmlns="http://soap.sforce.com/2006/04/metadata"><types>
<members>Opportunity.Deal_Approval</members><members>opportunity.deal_approval</members><name>ApprovalProcess</name>
</types></Package>`,
			problem: "destructiveChanges.xml must declare exactly one ApprovalProcess member",
		},
		{
			name: "cross-object globally unique developer name",
			manifest: `<Package xmlns="http://soap.sforce.com/2006/04/metadata"><types>
<members>Opportunity.Deal_Approval</members><members>Account.Deal_Approval</members><name>ApprovalProcess</name>
</types></Package>`,
			problem: "destructiveChanges.xml must declare exactly one ApprovalProcess member",
		},
		{
			name:     "unsupported type",
			manifest: `<Package xmlns="http://soap.sforce.com/2006/04/metadata"><types><members>Deal_Flow</members><name>Flow</name></types></Package>`,
			problem:  "destructiveChanges.xml must declare exactly one ApprovalProcess type",
		},
		{
			name: "mixed types",
			manifest: `<Package xmlns="http://soap.sforce.com/2006/04/metadata">
<types><members>Opportunity.Deal_Approval</members><name>ApprovalProcess</name></types>
<types><members>Deal_Flow</members><name>Flow</name></types></Package>`,
			problem: "destructiveChanges.xml must declare exactly one ApprovalProcess type",
		},
		{
			name:     "invalid member",
			manifest: `<Package xmlns="http://soap.sforce.com/2006/04/metadata"><types><members>Opportunity.Invalid.Member</members><name>ApprovalProcess</name></types></Package>`,
			problem:  "Invalid ApprovalProcess member name",
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			deploy := &fakeMetadataDeploy{jobID: "1234567890abcde"}
			result := completeMetadataDeployZip(t, metadataDeployZip(t, "destructiveChanges.xml", test.manifest), deploy)
			if !result.Done || result.Status != "Failed" || result.Success {
				t.Fatalf("completed result = %+v, want failed", result)
			}
			if len(result.ComponentFailures) != 1 ||
				result.ComponentFailures[0].Problem != test.problem ||
				result.ComponentFailures[0].FileName != "destructiveChanges.xml" ||
				result.ComponentFailures[0].ComponentType != metadataDeployComponentType {
				t.Fatalf("component failures = %+v", result.ComponentFailures)
			}
			if deploy.applyCalls != 0 {
				t.Fatalf("apply calls = %d, want no mutation", deploy.applyCalls)
			}
		})
	}
}

func TestMetadataDeployRejectsMixedApprovalProcessSourceAndDestructiveManifestWithoutMutation(t *testing.T) {
	zipBytes := metadataDeployZipFiles(t, map[string]string{
		"approvalProcesses/Opportunity.Deal_Approval.approvalProcess": `<ApprovalProcess><label>Deal Approval</label></ApprovalProcess>`,
		"destructiveChanges.xml": `<Package xmlns="http://soap.sforce.com/2006/04/metadata"><types>
<members>Opportunity.Deal_Approval</members><name>ApprovalProcess</name></types></Package>`,
	})
	deploy := &fakeMetadataDeploy{jobID: "1234567890abcde"}
	result := completeMetadataDeployZip(t, zipBytes, deploy)
	if !result.Done || result.Status != "Failed" || result.Success || len(result.ComponentFailures) != 1 {
		t.Fatalf("completed result = %+v, want one failure", result)
	}
	failure := result.ComponentFailures[0]
	if failure.Problem != "ApprovalProcess source and destructive members cannot be deployed together" ||
		failure.FileName != "destructiveChanges.xml" || failure.ComponentType != metadataDeployComponentType {
		t.Fatalf("component failure = %+v", failure)
	}
	if deploy.applyCalls != 0 {
		t.Fatalf("apply calls = %d, want no mutation", deploy.applyCalls)
	}
}

func TestMetadataDeployRejectsDuplicateDestructiveManifestWithoutMutation(t *testing.T) {
	manifest := `<Package xmlns="http://soap.sforce.com/2006/04/metadata"><types>
<members>Opportunity.Deal_Approval</members><name>ApprovalProcess</name></types></Package>`
	zipBytes := metadataDeployZipEntries(t, []metadataDeployZipEntry{
		{name: "destructiveChanges.xml", content: manifest},
		{name: "destructiveChanges.xml", content: manifest},
	})
	deploy := &fakeMetadataDeploy{jobID: "1234567890abcde"}
	result := completeMetadataDeployZip(t, zipBytes, deploy)
	if !result.Done || result.Status != "Failed" || result.Success || len(result.ComponentFailures) != 1 {
		t.Fatalf("completed result = %+v, want one failure", result)
	}
	failure := result.ComponentFailures[0]
	if failure.Problem != "Duplicate destructiveChanges.xml" ||
		failure.FileName != "destructiveChanges.xml" || failure.ComponentType != metadataDeployComponentType {
		t.Fatalf("component failure = %+v", failure)
	}
	if deploy.applyCalls != 0 {
		t.Fatalf("apply calls = %d, want no mutation", deploy.applyCalls)
	}
}

func TestMetadataDeployStatusNeutralHandlerProjectsV63Envelope(t *testing.T) {
	deploy := &fakeMetadataDeploy{
		result: &contract.MetadataDeployResult{
			Done:    true,
			Status:  "Failed",
			Success: false,
			ComponentFailures: []contract.MetadataDeployComponentFailure{{
				Problem:       "Invalid XML format",
				FileName:      "approvalProcesses/Opportunity.Bad.approvalProcess",
				ComponentType: "ApprovalProcess",
			}},
		},
		found: true,
	}
	resp, err := MetadataDeployStatusNeutralHandler(context.Background(), &dialect.Request{
		Params: map[string]string{"id": "0Afdeploy-1"},
	}, metadataDeployDeps(deploy))
	if err != nil {
		t.Fatalf("MetadataDeployStatusNeutralHandler err = %v", err)
	}
	if deploy.statusID != "deploy-1" {
		t.Fatalf("status id = %q, want trimmed deploy id", deploy.statusID)
	}
	assertBody(t, "metadata-deploy-status", resp.Body, "",
		`{"deployResult":{"done":true,"status":"Failed","details":{"componentFailures":[{"problem":"Invalid XML format","fileName":"approvalProcesses/Opportunity.Bad.approvalProcess","componentType":"ApprovalProcess"}]}}}`)
}

func TestMetadataDeployStatusNeutralHandlerUsesGenericInternalErrorOnStorageFailure(t *testing.T) {
	deploy := &fakeMetadataDeploy{
		statusErr: errors.New("db password leaked in driver error"),
	}
	resp, err := MetadataDeployStatusNeutralHandler(context.Background(), &dialect.Request{
		Params: map[string]string{"id": "0Afdeploy-1"},
	}, metadataDeployDeps(deploy))
	if err != nil {
		t.Fatalf("MetadataDeployStatusNeutralHandler err = %v", err)
	}
	if deploy.statusID != "deploy-1" {
		t.Fatalf("status id = %q, want trimmed deploy id", deploy.statusID)
	}
	if resp.Status != 500 {
		t.Fatalf("status = %d, want 500; body=%#v", resp.Status, resp.Body)
	}
	assertBody(t, "metadata-deploy-status-storage-error", resp.Body,
		mustJSON(t, wire.SFGenericInternalErrorResponse()),
		`[{"errorCode":"INTERNAL_ERROR","message":"An internal server error occurred. Contact your system administrator."}]`)
	if strings.Contains(mustJSON(t, resp.Body), "db password") {
		t.Fatalf("internal error body leaked storage cause: %s", mustJSON(t, resp.Body))
	}
}

func TestMetadataDeployStatusNeutralHandlerUsesGenericInternalErrorOnCorruptStoredResult(t *testing.T) {
	deploy := &fakeMetadataDeploy{found: true}
	resp, err := MetadataDeployStatusNeutralHandler(context.Background(), &dialect.Request{
		Params: map[string]string{"id": "0Afdeploy-1"},
	}, metadataDeployDeps(deploy))
	if err != nil {
		t.Fatalf("MetadataDeployStatusNeutralHandler err = %v", err)
	}
	if resp.Status != 500 {
		t.Fatalf("status = %d, want 500; body=%#v", resp.Status, resp.Body)
	}
	assertBody(t, "metadata-deploy-status-corrupt-result", resp.Body,
		mustJSON(t, wire.SFGenericInternalErrorResponse()),
		`[{"errorCode":"INTERNAL_ERROR","message":"An internal server error occurred. Contact your system administrator."}]`)
	if got := mustJSON(t, resp.Body); strings.Contains(got, "Failed to parse deploy result") || strings.Contains(got, "UNKNOWN_EXCEPTION") {
		t.Fatalf("internal error body used legacy deploy error shape: %s", got)
	}
}

func TestMetadataDeployRequestNeutralHandlerUsesGenericInternalErrorWhenContractMissing(t *testing.T) {
	resp, err := MetadataDeployRequestNeutralHandler(context.Background(), &dialect.Request{}, dialect.Deps{})
	if err != nil {
		t.Fatalf("MetadataDeployRequestNeutralHandler err = %v", err)
	}
	if resp.Status != 500 {
		t.Fatalf("status = %d, want 500; body=%#v", resp.Status, resp.Body)
	}
	assertBody(t, "metadata-deploy-request-missing-contract", resp.Body,
		mustJSON(t, wire.SFGenericInternalErrorResponse()),
		`[{"errorCode":"INTERNAL_ERROR","message":"An internal server error occurred. Contact your system administrator."}]`)
	if got := mustJSON(t, resp.Body); strings.Contains(got, "metadata deploy service unavailable") {
		t.Fatalf("internal error body used route-local deploy error shape: %s", got)
	}
}

func TestMetadataDeployStatusNeutralHandlerUsesGenericInternalErrorWhenContractMissing(t *testing.T) {
	resp, err := MetadataDeployStatusNeutralHandler(context.Background(), &dialect.Request{
		Params: map[string]string{"id": "0Afdeploy-1"},
	}, dialect.Deps{})
	if err != nil {
		t.Fatalf("MetadataDeployStatusNeutralHandler err = %v", err)
	}
	if resp.Status != 500 {
		t.Fatalf("status = %d, want 500; body=%#v", resp.Status, resp.Body)
	}
	assertBody(t, "metadata-deploy-status-missing-contract", resp.Body,
		mustJSON(t, wire.SFGenericInternalErrorResponse()),
		`[{"errorCode":"INTERNAL_ERROR","message":"An internal server error occurred. Contact your system administrator."}]`)
	if got := mustJSON(t, resp.Body); strings.Contains(got, "metadata deploy service unavailable") {
		t.Fatalf("internal error body used route-local deploy error shape: %s", got)
	}
}

func TestMetadataDeployRequestNeutralHandlerUsesGenericInternalErrorOnStartFailure(t *testing.T) {
	rawXML := `<ApprovalProcess><label>Deal Approval</label><active>true</active></ApprovalProcess>`
	zipBytes := metadataDeployZip(t, "approvalProcesses/Opportunity.Deal_Approval.approvalProcess", rawXML)
	body, contentType := multipartBody(t, map[string]string{"file": "deploy.zip"}, zipBytes)
	deploy := &fakeMetadataDeploy{startErr: errors.New("db password leaked in driver error")}

	resp, err := MetadataDeployRequestNeutralHandler(context.Background(), &dialect.Request{
		Header:     map[string][]string{"Content-Type": []string{contentType}},
		BodyStream: bytes.NewReader(body),
		User:       &models.UserSession{ID: "005000000000001"},
	}, metadataDeployDeps(deploy))
	if err != nil {
		t.Fatalf("MetadataDeployRequestNeutralHandler err = %v", err)
	}
	if resp.Status != 500 {
		t.Fatalf("status = %d, want 500; body=%#v", resp.Status, resp.Body)
	}
	assertBody(t, "metadata-deploy-start-error", resp.Body,
		mustJSON(t, wire.SFGenericInternalErrorResponse()),
		`[{"errorCode":"INTERNAL_ERROR","message":"An internal server error occurred. Contact your system administrator."}]`)
	if got := mustJSON(t, resp.Body); strings.Contains(got, "db password") || strings.Contains(got, "metadata deploy service unavailable") {
		t.Fatalf("internal error body leaked start failure detail: %s", got)
	}
}

func TestMetadataDeployRequestPreservesDiagnosticsWhenApplyFails(t *testing.T) {
	rawXML := `<ApprovalProcess><label>Deal Approval</label><active>true</active></ApprovalProcess>`
	zipBytes := metadataDeployZip(t, "approvalProcesses/Opportunity.Deal_Approval.approvalProcess", rawXML)
	body, contentType := multipartBody(t, map[string]string{"file": "deploy.zip"}, zipBytes)
	deploy := &fakeMetadataDeploy{
		jobID:     "1234567890abcde",
		completed: make(chan contract.MetadataDeployResult, 1),
		applyResult: &contract.MetadataDeployRecordResult{Diagnostics: []contract.MetadataDeployDiagnostic{{
			Problem:       "missing approval step child",
			ComponentType: "ApprovalProcess",
		}}},
		applyErr: errors.New("failed to insert ProcessDefinition"),
	}

	resp, err := MetadataDeployRequestNeutralHandler(context.Background(), &dialect.Request{
		Header:     map[string][]string{"Content-Type": []string{contentType}},
		BodyStream: bytes.NewReader(body),
		User:       &models.UserSession{ID: "005000000000001"},
	}, metadataDeployDeps(deploy))
	if err != nil {
		t.Fatalf("MetadataDeployRequestNeutralHandler err = %v", err)
	}
	assertBody(t, "metadata-deploy-start-with-failure", resp.Body, "",
		`{"done":false,"id":"0Af1234567890abcde"}`)

	select {
	case result := <-deploy.completed:
		if !result.Done || result.Status != "Failed" || result.Success {
			t.Fatalf("completed result = %+v, want failed deploy", result)
		}
		if len(result.ComponentFailures) != 2 {
			t.Fatalf("component failures = %+v, want diagnostic plus apply error", result.ComponentFailures)
		}
		if result.ComponentFailures[0].Problem != "missing approval step child" ||
			result.ComponentFailures[0].FileName != "approvalProcesses/Opportunity.Deal_Approval.approvalProcess" ||
			result.ComponentFailures[0].ComponentType != "ApprovalProcess" {
			t.Fatalf("diagnostic failure = %+v", result.ComponentFailures[0])
		}
		if result.ComponentFailures[1].Problem != "failed to insert ProcessDefinition" ||
			result.ComponentFailures[1].FileName != "approvalProcesses/Opportunity.Deal_Approval.approvalProcess" {
			t.Fatalf("apply failure = %+v", result.ComponentFailures[1])
		}
	case <-time.After(time.Second):
		t.Fatal("metadata deploy did not complete")
	}
}

func TestApprovalProcessDeployUsesCanonicalDecoderWithoutMaterializingChildren(t *testing.T) {
	raw := `
<ApprovalProcess>
  <label>Deal Approval</label>
  <active>true</active>
	<description>Approve big deals</description>
	<allowRecall>false</allowRecall>
  <recordEditability>Locked</recordEditability>
  <finalApprovalRecordLock>true</finalApprovalRecordLock>
  <entryCriteria>
    <criteriaItems>
      <field>Amount</field>
      <operation>greaterThan</operation>
      <value>1000</value>
    </criteriaItems>
  </entryCriteria>
  <approvalStep>
    <name>Manager</name>
    <assignedApprover><approver><type>User</type><name>boss@example.com</name></approver></assignedApprover>
    <stepApprovalActions>
      <actions><name>Notify</name><type>Flow</type><apiName>Notify_Manager</apiName></actions>
    </stepApprovalActions>
  </approvalStep>
</ApprovalProcess>`

	metadata, err := decodeApprovalProcessDeployMetadata(raw)
	if err != nil {
		t.Fatalf("decodeApprovalProcessDeployMetadata returned error: %v", err)
	}
	if len(metadata.Steps) != 1 || len(metadata.Steps[0].StepApprovalActions) != 1 {
		t.Fatalf("canonical metadata children = %+v, want one step with one action", metadata.Steps)
	}
	rec := buildProcessDefinitionInsertRecord(metadata, []byte(raw), "Deal_Approval", "Opportunity", approvalProcessState(metadata.Active))

	if rec["Name"] != "Deal Approval" {
		t.Fatalf("Name = %v, want Deal Approval", rec["Name"])
	}
	if rec["TableEnumOrId"] != "Opportunity" {
		t.Fatalf("TableEnumOrId = %v, want Opportunity", rec["TableEnumOrId"])
	}
	if rec["State"] != "Active" || rec["IsActive"] != true {
		t.Fatalf("state/isActive = (%v,%v), want (Active,true)", rec["State"], rec["IsActive"])
	}
	if rec["AllowRecall"] != false {
		t.Fatalf("AllowRecall = %v, want explicit false", rec["AllowRecall"])
	}
	if rec["EntryFilter"] != "Amount greaterThan 1000" {
		t.Fatalf("EntryFilter = %v, want criteria expression", rec["EntryFilter"])
	}
	if rec["LockType"] != "Locked" || rec["FinalApprovalRecordLock"] != true {
		t.Fatalf("lock fields = (%v,%v), want (Locked,true)", rec["LockType"], rec["FinalApprovalRecordLock"])
	}

	for _, forbidden := range []string{
		"Steps",
		"InitialSubmissionActions",
		"StepApprovalActions",
		"FinalApprovalActions",
		"FinalRejectionActions",
		"RecallActions",
		"ReassignActions",
	} {
		if _, ok := rec[forbidden]; ok {
			t.Fatalf("metadata deploy parent record unexpectedly contains child-owned field %s", forbidden)
		}
	}
}

func TestApprovalProcessDeployRequestIsOwnerlessOnly(t *testing.T) {
	metadata := &ApprovalProcessMetadata{Label: "Deal Approval"}
	req := approvalProcessDeployRequest(metadata, []byte(`<ApprovalProcess/>`), "Deal_Approval", "Opportunity")

	if len(req.MatchFields) != 3 || req.MatchFields["DeveloperName"] != "Deal_Approval" ||
		req.MatchFields["TableEnumOrId"] != "Opportunity" || req.MatchFields["OwnerPack"] != "" {
		t.Fatalf("match fields = %#v, want ownerless approval-process identity", req.MatchFields)
	}
	if owner, ok := req.CreateRecord["OwnerPack"]; !ok || owner != "" {
		t.Fatalf("create OwnerPack = %#v (present %v), want explicit ownerless value", owner, ok)
	}
	if owner, ok := req.UpdateRecord["OwnerPack"]; ok {
		t.Fatalf("update OwnerPack = %#v, want ownership preserved", owner)
	}
}

func metadataDeployZip(t *testing.T, name string, content string) []byte {
	t.Helper()
	return metadataDeployZipEntries(t, []metadataDeployZipEntry{{name: name, content: content}})
}

func metadataDeployZipFiles(t *testing.T, files map[string]string) []byte {
	t.Helper()
	entries := make([]metadataDeployZipEntry, 0, len(files))
	for name, content := range files {
		entries = append(entries, metadataDeployZipEntry{name: name, content: content})
	}
	return metadataDeployZipEntries(t, entries)
}

type metadataDeployZipEntry struct {
	name    string
	content string
}

func metadataDeployZipEntries(t *testing.T, entries []metadataDeployZipEntry) []byte {
	t.Helper()
	var buf bytes.Buffer
	zw := zip.NewWriter(&buf)
	for _, entry := range entries {
		w, err := zw.Create(entry.name)
		if err != nil {
			t.Fatalf("zip create: %v", err)
		}
		if _, err := w.Write([]byte(entry.content)); err != nil {
			t.Fatalf("zip write: %v", err)
		}
	}
	if err := zw.Close(); err != nil {
		t.Fatalf("zip close: %v", err)
	}
	return buf.Bytes()
}

func completeMetadataDeployZip(t *testing.T, zipBytes []byte, deploy *fakeMetadataDeploy) contract.MetadataDeployResult {
	t.Helper()
	deploy.completed = make(chan contract.MetadataDeployResult, 1)
	body, contentType := multipartBody(t, map[string]string{"file": "deploy.zip"}, zipBytes)
	_, err := MetadataDeployRequestNeutralHandler(context.Background(), &dialect.Request{
		Header:     map[string][]string{"Content-Type": []string{contentType}},
		BodyStream: bytes.NewReader(body),
		User:       &models.UserSession{ID: "005000000000001"},
	}, metadataDeployDeps(deploy))
	if err != nil {
		t.Fatalf("MetadataDeployRequestNeutralHandler err = %v", err)
	}
	select {
	case result := <-deploy.completed:
		return result
	case <-time.After(time.Second):
		t.Fatal("metadata deploy did not complete")
		return contract.MetadataDeployResult{}
	}
}

func metadataDeployDeps(deploy contract.MetadataDeploy) dialect.Deps {
	return dialect.Deps{Contracts: contract.Deps{MetadataDeploy: deploy}}
}

type fakeMetadataDeploy struct {
	jobID       string
	startErr    error
	applyReq    contract.MetadataDeployRecordRequest
	applyCalls  int
	applyResult *contract.MetadataDeployRecordResult
	applyErr    error
	result      *contract.MetadataDeployResult
	found       bool
	statusErr   error
	statusID    string
	completed   chan contract.MetadataDeployResult
}

func (f *fakeMetadataDeploy) StartMetadataDeploy(context.Context) (string, error) {
	if f.startErr != nil {
		return "", f.startErr
	}
	if f.jobID == "" {
		return "deploy-1", nil
	}
	return f.jobID, nil
}

func (f *fakeMetadataDeploy) CompleteMetadataDeploy(_ context.Context, _ string, result contract.MetadataDeployResult) error {
	if f.completed != nil {
		f.completed <- result
	}
	return nil
}

func (f *fakeMetadataDeploy) GetMetadataDeployResult(_ context.Context, jobID string) (*contract.MetadataDeployResult, bool, error) {
	f.statusID = jobID
	if f.statusErr != nil {
		return nil, false, f.statusErr
	}
	return f.result, f.found, nil
}

func (f *fakeMetadataDeploy) ApplyMetadataDeployRecord(_ context.Context, req contract.MetadataDeployRecordRequest, _ *models.UserSession) (*contract.MetadataDeployRecordResult, error) {
	f.applyReq = req
	f.applyCalls++
	if f.applyResult != nil || f.applyErr != nil {
		return f.applyResult, f.applyErr
	}
	return &contract.MetadataDeployRecordResult{}, nil
}
