package sfcrmappservice

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"os"
	"reflect"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	catalogsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/catalog/consumerv1"
	commonsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
	permissionsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/effectivepermissionsv1"
	querysemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/queryv1"
	recordsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordsv1"
	searchsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/searchv1"
)

func searchTestView(
	t *testing.T,
	objects []searchObjectSpec,
) catalogsemantic.AppMetadataViewV3 {
	t.Helper()
	appView := validSearchScopeView(t, 17, 3)
	behavior, err := json.Marshal(effectiveObjectSearchDocument{Objects: objects})
	if err != nil {
		t.Fatal(err)
	}
	component := catalogsemantic.EffectiveMetadataComponent{
		ComponentKind:          catalogsemantic.EffectiveObjectSearchComponentKind,
		ComponentFormatVersion: effectiveObjectSearchComponentFormatV4,
		BusinessKey:            catalogsemantic.EffectiveObjectSearchComponentBusinessKey,
		CanonicalBehavior:      behavior,
	}
	component.ContentDigest =
		catalogsemantic.ComputeEffectiveMetadataComponentDigest(behavior)
	view := catalogsemantic.AppMetadataViewV3{
		OrgID: appView.OrgID, AppInstallationID: appView.AppInstallationID,
		AppID: appView.AppID, AppReleaseDigest: appView.AppReleaseDigest,
		Lineage: appView.Lineage, AppComponents: appView.Components,
		EffectiveComponents: []catalogsemantic.EffectiveMetadataComponent{
			component,
		},
		Receipts: appView.Receipts,
	}
	view.Receipts.CapabilityVersion =
		catalogsemantic.AppMetadataV3CapabilityName
	view.Lineage.ViewDigest =
		catalogsemantic.ComputeAppMetadataViewV3Digest(view)
	if err := catalogsemantic.ValidateAppMetadataViewV3(view); err != nil {
		t.Fatalf("search AppMetadataViewV3 is invalid: %v", err)
	}
	return view
}

func TestProjectedPublishedObjectSpecIsConsumableBySearch(t *testing.T) {
	packBehavior, err := os.ReadFile(
		"../../salesforce-crm-standard/objects/standard_objects_spec.json",
	)
	if err != nil {
		t.Fatal(err)
	}
	var objects []searchObjectSpec
	if err := json.Unmarshal(packBehavior, &objects); err != nil {
		t.Fatal(err)
	}
	behavior, err := json.Marshal(struct {
		Objects []searchObjectSpec `json:"objects"`
	}{Objects: objects})
	if err != nil {
		t.Fatal(err)
	}
	specs, err := searchObjectSpecsFromView(catalogsemantic.AppMetadataViewV3{
		EffectiveComponents: []catalogsemantic.EffectiveMetadataComponent{{
			ComponentKind:          catalogsemantic.EffectiveObjectSearchComponentKind,
			ComponentFormatVersion: effectiveObjectSearchComponentFormatV5,
			BusinessKey:            catalogsemantic.EffectiveObjectSearchComponentBusinessKey,
			CanonicalBehavior:      behavior,
		}},
	})
	if err != nil {
		t.Fatalf("published object spec is not consumable: %v", err)
	}
	account, found := specs["account"]
	if !found || !reflect.DeepEqual(
		scopedSearchFields(account, "ALL"),
		[]string{"Name", "Phone", "Fax"},
	) {
		t.Fatalf("Account ALL search fields = %#v, found=%v", scopedSearchFields(account, "ALL"), found)
	}
}

func TestObjectSearchFormat3DecodesControlplanePathAndSparseUniqueFacts(t *testing.T) {
	behavior := []byte(`{"objects":[{"apiName":"Voucher","label":"Voucher","tableType":"custom_object","searchable":false,"pathField":"Status","fields":[` +
		`{"apiName":"RequiredOnly","label":"Required Only","type":"string","required":true,"createable":false,"updateable":false},` +
		`{"apiName":"ExplicitFalse","label":"Explicit False","type":"string","unique":false,"createable":false,"updateable":false},` +
		`{"apiName":"SourceKey","label":"Source Key","type":"string","unique":true,"createable":false,"updateable":false},` +
		`{"apiName":"Status","label":"Status","type":"picklist","createable":false,"updateable":false}` +
		`]}]}`)
	specs, err := searchObjectSpecsFromView(catalogsemantic.AppMetadataViewV3{
		EffectiveComponents: []catalogsemantic.EffectiveMetadataComponent{{
			ComponentKind:          catalogsemantic.EffectiveObjectSearchComponentKind,
			ComponentFormatVersion: effectiveObjectSearchComponentFormatV3,
			BusinessKey:            catalogsemantic.EffectiveObjectSearchComponentBusinessKey,
			CanonicalBehavior:      behavior,
		}},
	})
	if err != nil {
		t.Fatal(err)
	}
	fields := specs["voucher"].Fields
	if specs["voucher"].PathField != "Status" ||
		len(fields) != 4 || !fields[0].Required || fields[0].Unique ||
		fields[1].Unique || fields[2].Required || !fields[2].Unique {
		t.Fatalf("decoded path/unique facts = spec %#v fields %#v", specs["voucher"], fields)
	}
}

func TestObjectSearchFormat4DecodesControlplaneListFields(t *testing.T) {
	behavior := []byte(`{"objects":[{"apiName":"Month_End_ARR__c","label":"Month End ARR","listFields":["Product_Family__c","Revenue_Month__c","Month_End_ARR_Amount__c","Total_ARR__c","Owner_Name__c"],"fields":[` +
		`{"apiName":"Product_Family__c","label":"Product Family","type":"string","createable":true,"updateable":true},` +
		`{"apiName":"Revenue_Month__c","label":"Revenue Month","type":"date","createable":true,"updateable":true},` +
		`{"apiName":"Month_End_ARR_Amount__c","label":"Month End ARR Amount","type":"currency","createable":true,"updateable":true},` +
		`{"apiName":"Total_ARR__c","label":"Total ARR","type":"currency","createable":true,"updateable":true},` +
		`{"apiName":"Owner_Name__c","label":"Owner","type":"string","createable":true,"updateable":true}` +
		`]}]}`)
	specs, err := searchObjectSpecsFromView(catalogsemantic.AppMetadataViewV3{
		EffectiveComponents: []catalogsemantic.EffectiveMetadataComponent{{
			ComponentKind:          catalogsemantic.EffectiveObjectSearchComponentKind,
			ComponentFormatVersion: effectiveObjectSearchComponentFormatV4,
			BusinessKey:            catalogsemantic.EffectiveObjectSearchComponentBusinessKey,
			CanonicalBehavior:      behavior,
		}},
	})
	if err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(
		specs["month_end_arr__c"].ListFields,
		[]string{
			"Product_Family__c", "Revenue_Month__c", "Month_End_ARR_Amount__c",
			"Total_ARR__c", "Owner_Name__c",
		},
	) {
		t.Fatalf("decoded list fields = %#v", specs["month_end_arr__c"].ListFields)
	}
}

func TestObjectSearchFormat5PreservesRelationshipDisplayNamePath(t *testing.T) {
	behavior := []byte(`{"objects":[` +
		`{"apiName":"Account","label":"Account","fields":[{"apiName":"ExternalId__c","label":"External ID","type":"string","createable":false,"updateable":false}]},` +
		`{"apiName":"Widget","label":"Widget","displayNameField":"Account.ExternalId__c","fields":[{"apiName":"AccountId","label":"Account","type":"reference","referenceTo":["Account"],"relationshipName":"Account","createable":true,"updateable":true}]}` +
		`]}`)
	specs, err := searchObjectSpecsFromView(catalogsemantic.AppMetadataViewV3{
		EffectiveComponents: []catalogsemantic.EffectiveMetadataComponent{{
			ComponentKind:          catalogsemantic.EffectiveObjectSearchComponentKind,
			ComponentFormatVersion: effectiveObjectSearchComponentFormatV5,
			BusinessKey:            catalogsemantic.EffectiveObjectSearchComponentBusinessKey,
			CanonicalBehavior:      behavior,
		}},
	})
	if err != nil {
		t.Fatal(err)
	}
	if got := specs["widget"].DisplayNameField; got != "Account.ExternalId__c" {
		t.Fatalf("Widget displayNameField = %q", got)
	}
}

func TestObjectSearchFormat4RejectsInvalidListFields(t *testing.T) {
	for _, test := range []struct {
		name       string
		listFields string
	}{
		{name: "null", listFields: "null"},
		{name: "missing", listFields: "[\"Missing\"]"},
		{name: "duplicate", listFields: "[\"Name\",\"Name\"]"},
		{name: "non-canonical case", listFields: "[\"name\"]"},
		{name: "whitespace", listFields: "[\" Name \"]"},
	} {
		t.Run(test.name, func(t *testing.T) {
			behavior := []byte(`{"objects":[{"apiName":"Example","label":"Example","listFields":` +
				test.listFields +
				`,"fields":[{"apiName":"Name","label":"Name","type":"string","createable":true,"updateable":true}]}]}`)
			_, err := searchObjectSpecsFromView(catalogsemantic.AppMetadataViewV3{
				EffectiveComponents: []catalogsemantic.EffectiveMetadataComponent{{
					ComponentKind:          catalogsemantic.EffectiveObjectSearchComponentKind,
					ComponentFormatVersion: effectiveObjectSearchComponentFormatV4,
					BusinessKey:            catalogsemantic.EffectiveObjectSearchComponentBusinessKey,
					CanonicalBehavior:      behavior,
				}},
			})
			if err == nil {
				t.Fatalf("format-4 object-search accepted listFields %s", test.listFields)
			}
		})
	}
}

func TestObjectSearchFormat2RemainsReadableWithoutPathField(t *testing.T) {
	behavior := []byte(`{"objects":[{"apiName":"Voucher","label":"Voucher","fields":[` +
		`{"apiName":"SourceKey","label":"Source Key","type":"string","unique":true,"createable":false,"updateable":false}` +
		`]}]}`)
	specs, err := searchObjectSpecsFromView(catalogsemantic.AppMetadataViewV3{
		EffectiveComponents: []catalogsemantic.EffectiveMetadataComponent{{
			ComponentKind:          catalogsemantic.EffectiveObjectSearchComponentKind,
			ComponentFormatVersion: effectiveObjectSearchComponentFormatV2,
			BusinessKey:            catalogsemantic.EffectiveObjectSearchComponentBusinessKey,
			CanonicalBehavior:      behavior,
		}},
	})
	if err != nil {
		t.Fatal(err)
	}
	if spec := specs["voucher"]; spec.PathField != "" ||
		len(spec.Fields) != 1 || !spec.Fields[0].Unique {
		t.Fatalf("decoded format-2 object = %#v", spec)
	}
}

func TestObjectSearchRejectsUnsupportedFormatsAndCrossVersionFacts(t *testing.T) {
	validBehavior := []byte(`{"objects":[{"apiName":"Example","label":"Example","fields":[{"apiName":"Name","label":"Name","type":"string","createable":true,"updateable":true}]}]}`)
	for _, version := range []uint32{1, 6} {
		t.Run(fmt.Sprintf("format-%d", version), func(t *testing.T) {
			_, err := searchObjectSpecsFromView(catalogsemantic.AppMetadataViewV3{
				EffectiveComponents: []catalogsemantic.EffectiveMetadataComponent{{
					ComponentKind:          catalogsemantic.EffectiveObjectSearchComponentKind,
					ComponentFormatVersion: version,
					BusinessKey:            catalogsemantic.EffectiveObjectSearchComponentBusinessKey,
					CanonicalBehavior:      validBehavior,
				}},
			})
			if err == nil {
				t.Fatalf("object-search format %d was accepted", version)
			}
		})
	}

	unknownBehavior := []byte(`{"objects":[{"apiName":"Example","label":"Example","fields":[{"apiName":"Name","label":"Name","type":"string","createable":true,"updateable":true,"futureFact":true}]}]}`)
	_, err := searchObjectSpecsFromView(catalogsemantic.AppMetadataViewV3{
		EffectiveComponents: []catalogsemantic.EffectiveMetadataComponent{{
			ComponentKind:          catalogsemantic.EffectiveObjectSearchComponentKind,
			ComponentFormatVersion: effectiveObjectSearchComponentFormatV3,
			BusinessKey:            catalogsemantic.EffectiveObjectSearchComponentBusinessKey,
			CanonicalBehavior:      unknownBehavior,
		}},
	})
	if err == nil {
		t.Fatal("format-3 object-search accepted an unknown field fact")
	}

	formulaBehavior := []byte(`{"objects":[{"apiName":"Example","label":"Example","fields":[{"apiName":"Derived__c","label":"Derived","type":"formula","createable":false,"updateable":false,"formula":"TEXT(Name)"},{"apiName":"Name","label":"Name","type":"picklist","createable":true,"updateable":true}]}]}`)
	_, err = searchObjectSpecsFromView(catalogsemantic.AppMetadataViewV3{
		EffectiveComponents: []catalogsemantic.EffectiveMetadataComponent{{
			ComponentKind:          catalogsemantic.EffectiveObjectSearchComponentKind,
			ComponentFormatVersion: effectiveObjectSearchComponentFormatV3,
			BusinessKey:            catalogsemantic.EffectiveObjectSearchComponentBusinessKey,
			CanonicalBehavior:      formulaBehavior,
		}},
	})
	if err == nil {
		t.Fatal("format-3 object-search accepted a format-4 formula fact")
	}

	listFieldsBehavior := []byte(`{"objects":[{"apiName":"Example","label":"Example","listFields":["Name"],"fields":[{"apiName":"Name","label":"Name","type":"string","createable":true,"updateable":true}]}]}`)
	_, err = searchObjectSpecsFromView(catalogsemantic.AppMetadataViewV3{
		EffectiveComponents: []catalogsemantic.EffectiveMetadataComponent{{
			ComponentKind:          catalogsemantic.EffectiveObjectSearchComponentKind,
			ComponentFormatVersion: effectiveObjectSearchComponentFormatV3,
			BusinessKey:            catalogsemantic.EffectiveObjectSearchComponentBusinessKey,
			CanonicalBehavior:      listFieldsBehavior,
		}},
	})
	if err == nil {
		t.Fatal("format-3 object-search accepted the format-4 listFields fact")
	}

	presentationBehavior := []byte(`{"objects":[{"apiName":"Example","label":"Example","fields":[{"apiName":"OwnerId","label":"Owner","type":"reference","referenceTo":["User"],"relationshipName":"Owner","createable":true,"updateable":true}]}]}`)
	_, err = searchObjectSpecsFromView(catalogsemantic.AppMetadataViewV3{
		EffectiveComponents: []catalogsemantic.EffectiveMetadataComponent{{
			ComponentKind:          catalogsemantic.EffectiveObjectSearchComponentKind,
			ComponentFormatVersion: effectiveObjectSearchComponentFormatV4,
			BusinessKey:            catalogsemantic.EffectiveObjectSearchComponentBusinessKey,
			CanonicalBehavior:      presentationBehavior,
		}},
	})
	if err == nil {
		t.Fatal("format-4 object-search accepted a format-5 presentation fact")
	}

	for _, test := range []struct {
		name  string
		value string
	}{
		{name: "value", value: `"StageName"`},
		{name: "empty", value: `""`},
		{name: "null", value: `null`},
	} {
		t.Run("format-2-pathField-"+test.name, func(t *testing.T) {
			pathBehavior := []byte(`{"objects":[{"apiName":"Opportunity","label":"Opportunity","pathField":` + test.value + `,"fields":[{"apiName":"StageName","label":"Stage","type":"picklist","createable":true,"updateable":true}]}]}`)
			_, err := searchObjectSpecsFromView(catalogsemantic.AppMetadataViewV3{
				EffectiveComponents: []catalogsemantic.EffectiveMetadataComponent{{
					ComponentKind:          catalogsemantic.EffectiveObjectSearchComponentKind,
					ComponentFormatVersion: effectiveObjectSearchComponentFormatV2,
					BusinessKey:            catalogsemantic.EffectiveObjectSearchComponentBusinessKey,
					CanonicalBehavior:      pathBehavior,
				}},
			})
			if err == nil {
				t.Fatalf("format-2 object-search accepted pathField %s", test.value)
			}
		})
	}
	for _, test := range []struct {
		name  string
		value string
	}{
		{name: "empty", value: `""`},
		{name: "null", value: `null`},
	} {
		t.Run("format-3-invalid-pathField-"+test.name, func(t *testing.T) {
			pathBehavior := []byte(`{"objects":[{"apiName":"Opportunity","label":"Opportunity","pathField":` + test.value + `,"fields":[{"apiName":"StageName","label":"Stage","type":"picklist","createable":true,"updateable":true}]}]}`)
			_, err := searchObjectSpecsFromView(catalogsemantic.AppMetadataViewV3{
				EffectiveComponents: []catalogsemantic.EffectiveMetadataComponent{{
					ComponentKind:          catalogsemantic.EffectiveObjectSearchComponentKind,
					ComponentFormatVersion: effectiveObjectSearchComponentFormatV3,
					BusinessKey:            catalogsemantic.EffectiveObjectSearchComponentBusinessKey,
					CanonicalBehavior:      pathBehavior,
				}},
			})
			if err == nil {
				t.Fatalf("format-3 object-search accepted invalid pathField %s", test.value)
			}
		})
	}
}

func TestObjectSearchFormat4RejectsMalformedPicklistValueSources(t *testing.T) {
	for _, test := range []struct {
		name        string
		source      string
		sourceTy    string
		sourceExtra string
	}{
		{name: "missing object", source: `{"object":"Missing","field":"Region__c"}`, sourceTy: "picklist"},
		{name: "missing field", source: `{"object":"Account","field":"Missing__c"}`, sourceTy: "picklist"},
		{name: "non-picklist field", source: `{"object":"Account","field":"Region__c"}`, sourceTy: "string"},
		{name: "formula returning picklist", source: `{"object":"Account","field":"Region__c"}`, sourceTy: "formula", sourceExtra: `,"returnType":"picklist"`},
		{name: "non-canonical object", source: `{"object":"account","field":"Region__c"}`, sourceTy: "picklist"},
	} {
		t.Run(test.name, func(t *testing.T) {
			behavior := []byte(`{"objects":[` +
				`{"apiName":"Account","label":"Account","fields":[` +
				`{"apiName":"Region__c","label":"Region","type":"` + test.sourceTy + `"` + test.sourceExtra + `,"createable":false,"updateable":false}]},` +
				`{"apiName":"Opportunity","label":"Opportunity","fields":[` +
				`{"apiName":"Region__c","label":"Region","type":"formula","createable":false,"updateable":false,` +
				`"formula":"TEXT(Account.Region__c)","picklistValueSources":[` + test.source + `]}]}]}`)
			_, err := searchObjectSpecsFromView(catalogsemantic.AppMetadataViewV3{
				EffectiveComponents: []catalogsemantic.EffectiveMetadataComponent{{
					ComponentKind:          catalogsemantic.EffectiveObjectSearchComponentKind,
					ComponentFormatVersion: effectiveObjectSearchComponentFormatV4,
					BusinessKey:            catalogsemantic.EffectiveObjectSearchComponentBusinessKey,
					CanonicalBehavior:      behavior,
				}},
			})
			if err == nil {
				t.Fatal("format-4 object-search accepted malformed picklist source")
			}
		})
	}
}

func searchTestService(
	t *testing.T,
	view catalogsemantic.AppMetadataViewV3,
	search RuntimeSearchFunc,
	query RuntimeQueryFunc,
) *Service {
	t.Helper()
	config := permissionMetadataServiceConfig(
		func(
			context.Context,
			string,
			permissionsemantic.Request,
		) (permissionsemantic.Response, error) {
			return validPermissionResponse(17, 3), nil
		},
		func(context.Context, catalogsemantic.GetRequest) (
			catalogsemantic.AppMetadataView,
			error,
		) {
			return validSearchScopeView(
				t,
				view.Lineage.CatalogRevision,
				view.Receipts.SecurityEpoch,
			), nil
		},
	)
	config.RuntimeSearch = search
	config.RuntimeQuery = query
	config.RuntimeGetAppMetadataViewV3 = func(
		context.Context,
		catalogsemantic.GetRequest,
	) (catalogsemantic.AppMetadataViewV3, error) {
		return view, nil
	}
	service, err := NewService(config)
	if err != nil {
		t.Fatal(err)
	}
	return service
}

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

func searchReceipts(revision uint64) commonsemantic.Receipts {
	return commonsemantic.Receipts{
		ProtocolVersion:   "1.0",
		CapabilityVersion: searchsemantic.CapabilityName,
		CatalogRevision:   revision,
		SchemaGeneration:  8,
		EngineGeneration:  9,
		SecurityEpoch:     3,
	}
}

func queryReceipts(revision uint64) commonsemantic.Receipts {
	return commonsemantic.Receipts{
		ProtocolVersion:   "1.0",
		CapabilityVersion: querysemantic.CapabilityName,
		CatalogRevision:   revision,
		SchemaGeneration:  8,
		EngineGeneration:  9,
		SecurityEpoch:     3,
	}
}

func TestSearchRoutesRemainAbsentWithoutRuntimeSearchPort(t *testing.T) {
	runtime := &fakeRuntime{}
	runtime.ready.Store(true)
	recorder := httptest.NewRecorder()
	newTestService(t, runtime).ServeHTTP(
		recorder,
		searchTestRequest(
			http.MethodPost,
			"/services/data/v63.0/parameterizedSearch",
			`{"q":"ac","sobjects":[{"name":"Account","fields":["Id","Name"]}]}`,
		),
	)
	if recorder.Code != http.StatusNotFound {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
}

func TestSearchLayoutUsesImmutableObjectMetadata(t *testing.T) {
	view := searchTestView(t, []searchObjectSpec{
		{
			APIName: "Account", KeyPrefix: "001", Label: "Account",
			SearchAllDefaultFields: []string{"Name"},
			Fields: []searchFieldSpec{
				{APIName: "Name", Label: "Account Name", Type: "string", IsNameField: true},
				{APIName: "Phone", Label: "Phone", Type: "phone"},
				{APIName: "Email", Label: "Email", Type: "email"},
				{APIName: "OwnerId", Label: "Account Owner", Type: "reference", ReferenceTo: []string{"User"}},
				{APIName: "Description", Label: "Description", Type: "string"},
			},
		},
	})
	var metadataCalls atomic.Uint64
	service := searchTestService(
		t,
		view,
		func(
			context.Context,
			string,
			searchsemantic.Request,
		) (searchsemantic.Response, error) {
			t.Fatal("search layout called Runtime Search")
			return searchsemantic.Response{}, nil
		},
		func(
			context.Context,
			string,
			querysemantic.Request,
		) (querysemantic.Response, error) {
			t.Fatal("search layout called Runtime Query")
			return querysemantic.Response{}, nil
		},
	)
	service.search.appMetadata = func(
		context.Context,
		catalogsemantic.GetRequest,
	) (catalogsemantic.AppMetadataViewV3, error) {
		metadataCalls.Add(1)
		return view, nil
	}
	recorder := httptest.NewRecorder()
	service.ServeHTTP(
		recorder,
		searchTestRequest(
			http.MethodGet,
			"/services/data/v63.0/search/layout?q=Account",
			"",
		),
	)
	if recorder.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	if metadataCalls.Load() != 1 {
		t.Fatalf("metadata calls=%d", metadataCalls.Load())
	}
	const want = `[{"label":"Search Results","limitRows":25,"objectType":"Account",` +
		`"searchColumns":[` +
		`{"field":"Account.Name","format":null,"label":"Account Name","name":"Name"},` +
		`{"field":"Account.Phone","format":null,"label":"Phone","name":"Phone"},` +
		`{"field":"Account.Email","format":null,"label":"Email","name":"Email"},` +
		`{"field":"User.Alias","format":null,"label":"Account Owner Alias","name":"Owner.Alias"},` +
		`{"field":"Account.Description","format":null,"label":"Description","name":"Description"}` +
		`]}]` + "\n"
	if recorder.Body.String() != want {
		t.Fatalf("body=%s want=%s", recorder.Body.String(), want)
	}
}

func TestSearchLayoutRejectsMissingObjectWithoutRuntimeFallback(t *testing.T) {
	view := searchTestView(t, []searchObjectSpec{{
		APIName: "Account", KeyPrefix: "001", Label: "Account",
		SearchAllDefaultFields: []string{"Name"},
		Fields: []searchFieldSpec{{
			APIName: "Name", Label: "Account Name", Type: "string", IsNameField: true,
		}},
	}})
	service := searchTestService(
		t,
		view,
		func(
			context.Context,
			string,
			searchsemantic.Request,
		) (searchsemantic.Response, error) {
			t.Fatal("missing search layout object called Runtime Search")
			return searchsemantic.Response{}, nil
		},
		func(
			context.Context,
			string,
			querysemantic.Request,
		) (querysemantic.Response, error) {
			t.Fatal("missing search layout object called Runtime Query")
			return querysemantic.Response{}, nil
		},
	)
	recorder := httptest.NewRecorder()
	service.ServeHTTP(
		recorder,
		searchTestRequest(
			http.MethodGet,
			"/services/data/v63.0/search/layout?q=Ghost",
			"",
		),
	)
	if recorder.Code != http.StatusNotFound ||
		!strings.Contains(recorder.Body.String(), `"errorCode":"NOT_FOUND"`) {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
}

func TestParameterizedSearchUsesOneImmutableViewAndRuntimeSearch(t *testing.T) {
	view := searchTestView(t, []searchObjectSpec{
		{
			APIName: "Account", KeyPrefix: "001", Label: "Account",
			SearchAllDefaultFields: []string{"Name"},
			Fields: []searchFieldSpec{
				{APIName: "Name", Label: "Account Name", Type: "string", IsNameField: true},
				{APIName: "Phone", Label: "Phone", Type: "phone"},
			},
		},
		{
			APIName: "Contact", KeyPrefix: "003", Label: "Contact",
			SearchAllDefaultFields: []string{"Name"},
			Fields: []searchFieldSpec{
				{APIName: "Name", Label: "Full Name", Type: "string", IsNameField: true},
				{APIName: "Email", Label: "Email", Type: "email"},
			},
		},
	})
	var metadataCalls atomic.Uint64
	var requests []searchsemantic.Request
	config := permissionMetadataServiceConfig(
		func(
			context.Context,
			string,
			permissionsemantic.Request,
		) (permissionsemantic.Response, error) {
			return validPermissionResponse(17, 3), nil
		},
		func(context.Context, catalogsemantic.GetRequest) (
			catalogsemantic.AppMetadataView,
			error,
		) {
			return validSearchScopeView(t, 17, 3), nil
		},
	)
	config.RuntimeGetAppMetadataViewV3 = func(
		context.Context,
		catalogsemantic.GetRequest,
	) (catalogsemantic.AppMetadataViewV3, error) {
		metadataCalls.Add(1)
		return view, nil
	}
	config.RuntimeSearch = func(
		_ context.Context,
		bearer string,
		request searchsemantic.Request,
	) (searchsemantic.Response, error) {
		if bearer != "user-secret" {
			t.Fatalf("bearer=%q", bearer)
		}
		requests = append(requests, request)
		records := make([]searchsemantic.Record, 0, len(request.ObjectPlans))
		for _, plan := range request.ObjectPlans {
			name := "Acme"
			recordID := "001-acme"
			if plan.ObjectID == "Contact" {
				name = "Ada"
				recordID = "003-ada"
			}
			records = append(records, searchsemantic.Record{
				ObjectID: plan.ObjectID,
				RecordID: recordID,
				Fields: []recordsemantic.Field{{
					Name:   "Name",
					Kind:   recordsemantic.ValueKindString,
					String: &name,
				}},
			})
		}
		return searchsemantic.Response{
			Records:  records,
			Receipts: searchReceipts(17),
		}, nil
	}
	service, err := NewService(config)
	if err != nil {
		t.Fatal(err)
	}
	recorder := httptest.NewRecorder()
	service.ServeHTTP(
		recorder,
		searchTestRequest(
			http.MethodPost,
			"/services/data/v63.0/parameterizedSearch",
			`{"q":"ac","overallLimit":2,"sobjects":[`+
				`{"name":"Account","fields":["Id","Name"]},`+
				`{"name":"Contact","fields":["Id","Name"]}]}`,
		),
	)
	if recorder.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	if metadataCalls.Load() != 1 || len(requests) != 1 {
		t.Fatalf("metadata calls=%d search requests=%d", metadataCalls.Load(), len(requests))
	}
	request := requests[0]
	if request.MinCatalogRevision == nil ||
		*request.MinCatalogRevision != 17 || request.Term != "ac" ||
		len(request.ObjectPlans) != 2 ||
		request.ObjectPlans[0].ObjectID != "Account" ||
		request.ObjectPlans[1].ObjectID != "Contact" ||
		!reflect.DeepEqual(request.ObjectPlans[0].SelectedFields, []string{"Name"}) ||
		!reflect.DeepEqual(request.ObjectPlans[1].SelectedFields, []string{"Name"}) {
		t.Fatalf("request=%#v", request)
	}
	var response struct {
		SearchRecords []map[string]any `json:"searchRecords"`
	}
	if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
		t.Fatal(err)
	}
	if len(response.SearchRecords) != 2 ||
		response.SearchRecords[0]["Id"] != "001-acme" ||
		response.SearchRecords[1]["Id"] != "003-ada" {
		t.Fatalf("response=%#v", response)
	}
}

func TestParameterizedSearchComposesCompoundNameFromAuthorizedMetadata(
	t *testing.T,
) {
	view := searchTestView(t, []searchObjectSpec{{
		APIName: "Contact", KeyPrefix: "003", Label: "Contact",
		Fields: []searchFieldSpec{
			{
				APIName: "Salutation", Label: "Salutation", Type: "string",
				CompoundFieldName: "Name",
			},
			{
				APIName: "FirstName", Label: "First Name", Type: "string",
				CompoundFieldName: "Name",
			},
			{
				APIName: "LastName", Label: "Last Name", Type: "string",
				IsNameField: true, CompoundFieldName: "Name",
			},
		},
	}})
	var got searchsemantic.Request
	service := searchTestService(
		t,
		view,
		func(
			_ context.Context,
			_ string,
			request searchsemantic.Request,
		) (searchsemantic.Response, error) {
			got = request
			salutation, firstName, lastName := "Dr.", "Ada", "Lovelace"
			return searchsemantic.Response{
				Records: []searchsemantic.Record{{
					ObjectID: "Contact",
					RecordID: "003-ada",
					Fields: []recordsemantic.Field{
						{
							Name: "FirstName", Kind: recordsemantic.ValueKindString,
							String: &firstName,
						},
						{
							Name: "LastName", Kind: recordsemantic.ValueKindString,
							String: &lastName,
						},
						{
							Name: "Salutation", Kind: recordsemantic.ValueKindString,
							String: &salutation,
						},
					},
				}},
				Receipts: searchReceipts(17),
			}, nil
		},
		func(
			context.Context,
			string,
			querysemantic.Request,
		) (querysemantic.Response, error) {
			t.Fatal("parameterized search used Query")
			return querysemantic.Response{}, nil
		},
	)
	recorder := httptest.NewRecorder()
	service.ServeHTTP(
		recorder,
		searchTestRequest(
			http.MethodGet,
			"/services/data/v63.0/parameterizedSearch?"+
				"q=ada&sobject=Contact&fields=Id%2CName",
			"",
		),
	)
	if recorder.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	wantNameFields := []string{"Salutation", "FirstName", "LastName"}
	if len(got.ObjectPlans) != 1 ||
		!reflect.DeepEqual(got.ObjectPlans[0].SearchFields, wantNameFields) ||
		!reflect.DeepEqual(got.ObjectPlans[0].SelectedFields, wantNameFields) {
		t.Fatalf("request=%#v", got)
	}
	var response struct {
		SearchRecords []map[string]any `json:"searchRecords"`
	}
	if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
		t.Fatal(err)
	}
	if len(response.SearchRecords) != 1 ||
		response.SearchRecords[0]["Name"] != "Dr. Ada Lovelace" {
		t.Fatalf("response=%#v", response)
	}
}

func TestGlobalParameterizedSearchSkipsMetadataObjectsWithoutSearchFields(
	t *testing.T,
) {
	view := searchTestView(t, []searchObjectSpec{
		{
			APIName: "CaseComment", KeyPrefix: "00a", Label: "Case Comment",
			Fields: []searchFieldSpec{{
				APIName: "CommentBody", Label: "Comment", Type: "textarea",
			}},
		},
		{
			APIName: "Contact", KeyPrefix: "003", Label: "Contact",
			SearchAllDefaultFields: []string{"Name"},
			Fields: []searchFieldSpec{{
				APIName: "Name", Label: "Full Name", Type: "string",
				IsNameField: true,
			}},
		},
	})
	var requests []searchsemantic.Request
	service := searchTestService(
		t,
		view,
		func(
			_ context.Context,
			_ string,
			request searchsemantic.Request,
		) (searchsemantic.Response, error) {
			requests = append(requests, request)
			return searchsemantic.Response{
				Records:  []searchsemantic.Record{},
				Receipts: searchReceipts(17),
			}, nil
		},
		func(
			context.Context,
			string,
			querysemantic.Request,
		) (querysemantic.Response, error) {
			t.Fatal("global parameterized search used Query")
			return querysemantic.Response{}, nil
		},
	)
	recorder := httptest.NewRecorder()
	service.ServeHTTP(
		recorder,
		searchTestRequest(
			http.MethodPost,
			"/services/data/v63.0/parameterizedSearch",
			`{"q":"ad"}`,
		),
	)
	if recorder.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	if len(requests) != 1 || len(requests[0].ObjectPlans) != 1 ||
		requests[0].ObjectPlans[0].ObjectID != "Contact" {
		t.Fatalf("requests=%#v", requests)
	}
}

func TestParameterizedPOSTRejectsSOSLBeforeMetadataOrRuntime(t *testing.T) {
	var searchCalls atomic.Uint64
	var queryCalls atomic.Uint64
	var metadataCalls atomic.Uint64
	client, err := NewRuntimeSearchClient(
		func(
			context.Context,
			string,
			searchsemantic.Request,
		) (searchsemantic.Response, error) {
			searchCalls.Add(1)
			return searchsemantic.Response{}, nil
		},
		func(
			context.Context,
			string,
			querysemantic.Request,
		) (querysemantic.Response, error) {
			queryCalls.Add(1)
			return querysemantic.Response{}, nil
		},
		func(
			context.Context,
			catalogsemantic.GetRequest,
		) (catalogsemantic.AppMetadataViewV3, error) {
			metadataCalls.Add(1)
			return catalogsemantic.AppMetadataViewV3{}, nil
		},
	)
	if err != nil {
		t.Fatal(err)
	}
	_, err = client.executeParameterized(
		context.Background(),
		"Bearer user-secret",
		"63.0",
		strings.NewReader(`{"q":"FIND {ac}"}`),
	)
	if err == nil || !strings.Contains(err.Error(), "SOSL") {
		t.Fatalf("executeParameterized error = %v, want SOSL rejection", err)
	}
	if searchCalls.Load() != 0 || queryCalls.Load() != 0 ||
		metadataCalls.Load() != 0 {
		t.Fatalf(
			"search=%d query=%d metadata=%d; want zero calls",
			searchCalls.Load(), queryCalls.Load(), metadataCalls.Load(),
		)
	}
}

func TestSearchFanoutIsBoundedBeforeRuntimeCalls(t *testing.T) {
	var searchCalls atomic.Uint64
	var queryCalls atomic.Uint64
	var metadataCalls atomic.Uint64
	client, err := NewRuntimeSearchClient(
		func(
			context.Context,
			string,
			searchsemantic.Request,
		) (searchsemantic.Response, error) {
			searchCalls.Add(1)
			return searchsemantic.Response{}, nil
		},
		func(
			context.Context,
			string,
			querysemantic.Request,
		) (querysemantic.Response, error) {
			queryCalls.Add(1)
			return querysemantic.Response{}, nil
		},
		func(
			context.Context,
			catalogsemantic.GetRequest,
		) (catalogsemantic.AppMetadataViewV3, error) {
			metadataCalls.Add(1)
			return catalogsemantic.AppMetadataViewV3{}, nil
		},
	)
	if err != nil {
		t.Fatal(err)
	}

	objects := make([]searchObjectRequest, searchsemantic.MaxObjects+1)
	names := make([]string, len(objects))
	specs := make(map[string]searchObjectSpec, len(objects))
	for index := range objects {
		name := fmt.Sprintf("Object%d", index)
		names[index] = name
		objects[index] = searchObjectRequest{Name: name, Implicit: true}
		specs[strings.ToLower(name)] = searchObjectSpec{
			APIName:                name,
			SearchAllDefaultFields: []string{"Name"},
			Fields: []searchFieldSpec{{
				APIName: "Name", Type: "string", IsNameField: true,
			}},
		}
	}
	if _, err := client.executeObjects(
		t.Context(),
		"user-secret",
		"v63.0",
		catalogsemantic.AppMetadataViewV3{},
		specs,
		"launch",
		"ALL",
		objects,
		defaultSearchLimit,
	); err == nil {
		t.Fatal("global Search accepted excessive object fanout")
	}
	if _, err := client.executeSuggestions(
		t.Context(),
		"user-secret",
		"v63.0",
		map[string][]string{
			"q":       {"launch"},
			"sobject": {strings.Join(names, ",")},
		},
	); err == nil {
		t.Fatal("suggestions accepted excessive object fanout")
	}
	if searchCalls.Load() != 0 || queryCalls.Load() != 0 ||
		metadataCalls.Load() != 0 {
		t.Fatalf(
			"fanout rejection called Runtime: search=%d query=%d metadata=%d",
			searchCalls.Load(),
			queryCalls.Load(),
			metadataCalls.Load(),
		)
	}
}

func TestSOSLSupportsGlobalAndMultipleReturningObjects(t *testing.T) {
	global, err := parseCurrentSOSL("FIND {launch}")
	if err != nil {
		t.Fatal(err)
	}
	if global.Scope != "ALL" || len(global.Objects) != 0 {
		t.Fatalf("global=%#v", global)
	}
	multiple, err := parseCurrentSOSL(
		"FIND {launch} RETURNING Account(Id,Name), Contact(Id,Email)",
	)
	if err != nil {
		t.Fatal(err)
	}
	if multiple.Scope != "ALL" || !reflect.DeepEqual(
		multiple.Objects,
		[]searchObjectRequest{
			{Name: "Account", Fields: []string{"Id", "Name"}},
			{Name: "Contact", Fields: []string{"Id", "Email"}},
		},
	) {
		t.Fatalf("multiple=%#v", multiple)
	}
	scoped, err := parseCurrentSOSL(
		"FIND {launch} IN EMAIL FIELDS RETURNING Contact(Id,Email)",
	)
	if err != nil || scoped.Scope != "EMAIL" || len(scoped.Objects) != 1 {
		t.Fatalf("scoped=%#v err=%v", scoped, err)
	}
	limited, err := parseCurrentSOSL(
		"FIND {launch} IN ALL FIELDS RETURNING Lead(Id,Name,Email LIMIT 5)",
	)
	if err != nil || !reflect.DeepEqual(limited.Objects, []searchObjectRequest{{
		Name: "Lead", Fields: []string{"Id", "Name", "Email"}, Limit: 5,
	}}) {
		t.Fatalf("limited=%#v err=%v", limited, err)
	}
	if _, err := parseCurrentSOSL(
		"FIND {launch} RETURNING Lead(Id LIMIT 1000000)",
	); err == nil {
		t.Fatal("SOSL RETURNING accepted a limit above the Runtime contract")
	}
	trailing, err := parseCurrentSOSL(
		"FIND {launch} IN NAME FIELDS RETURNING Account(Id,Name) LIMIT 20",
	)
	if err != nil || trailing.Limit != 20 || !reflect.DeepEqual(
		trailing.Objects,
		[]searchObjectRequest{{Name: "Account", Fields: []string{"Id", "Name"}}},
	) {
		t.Fatalf("trailing=%#v err=%v", trailing, err)
	}
	combined, err := parseCurrentSOSL(
		"FIND {launch} RETURNING Account(Id LIMIT 5) LIMIT 20",
	)
	if err != nil || combined.Limit != 20 || combined.Objects[0].Limit != 5 {
		t.Fatalf("combined=%#v err=%v", combined, err)
	}
	if _, err := parseCurrentSOSL(
		"FIND {launch} RETURNING Account(Id) LIMIT 1000000",
	); err == nil {
		t.Fatal("SOSL accepted a trailing limit above the Runtime contract")
	}
}

func TestSearchFailsClosedOnMixedCatalogAuthority(t *testing.T) {
	view := searchTestView(t, []searchObjectSpec{{
		APIName: "Account", KeyPrefix: "001", Label: "Account",
		SearchAllDefaultFields: []string{"Name"},
		Fields: []searchFieldSpec{{
			APIName: "Name", Label: "Account Name", Type: "string", IsNameField: true,
		}},
	}})
	service := searchTestService(
		t,
		view,
		func(
			context.Context,
			string,
			searchsemantic.Request,
		) (searchsemantic.Response, error) {
			return searchsemantic.Response{
				Records:  []searchsemantic.Record{},
				Receipts: searchReceipts(18),
			}, nil
		},
		func(
			context.Context,
			string,
			querysemantic.Request,
		) (querysemantic.Response, error) {
			t.Fatal("parameterized search used Query")
			return querysemantic.Response{}, nil
		},
	)
	recorder := httptest.NewRecorder()
	service.ServeHTTP(
		recorder,
		searchTestRequest(
			http.MethodPost,
			"/services/data/v63.0/parameterizedSearch",
			`{"q":"ac","sobjects":[{"name":"Account","fields":["Id","Name"]}]}`,
		),
	)
	if recorder.Code != http.StatusServiceUnavailable ||
		!strings.Contains(recorder.Body.String(), "Service temporarily unavailable") {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
}

func TestParameterizedGETUsesImmutableDefaultSearchFields(t *testing.T) {
	view := searchTestView(t, []searchObjectSpec{{
		APIName: "Account", KeyPrefix: "001", Label: "Account",
		Fields: []searchFieldSpec{
			{
				APIName: "Name", Label: "Account Name", Type: "string",
				IsNameField: true,
			},
			{APIName: "Phone", Label: "Phone", Type: "phone"},
		},
	}})
	var got searchsemantic.Request
	service := searchTestService(
		t,
		view,
		func(
			_ context.Context,
			_ string,
			request searchsemantic.Request,
		) (searchsemantic.Response, error) {
			got = request
			return searchsemantic.Response{
				Records:  []searchsemantic.Record{},
				Receipts: searchReceipts(17),
			}, nil
		},
		func(
			context.Context,
			string,
			querysemantic.Request,
		) (querysemantic.Response, error) {
			t.Fatal("parameterized search used Query")
			return querysemantic.Response{}, nil
		},
	)
	recorder := httptest.NewRecorder()
	service.ServeHTTP(
		recorder,
		searchTestRequest(
			http.MethodGet,
			"/services/data/v63.0/parameterizedSearch?"+
				"q=ac&sobject=Account&fields=Id%2CName&limit=2",
			"",
		),
	)
	if recorder.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	if len(got.ObjectPlans) != 1 ||
		!reflect.DeepEqual(got.ObjectPlans[0].SearchFields, []string{"Name", "Phone"}) ||
		!reflect.DeepEqual(got.ObjectPlans[0].SelectedFields, []string{"Name"}) ||
		got.ObjectPlans[0].Limit != 2 || got.Limit != 2 {
		t.Fatalf("request=%#v", got)
	}
}

func TestSearchSuggestionsUseStructuredPrefixQueryAndPropagateCancellation(t *testing.T) {
	view := searchTestView(t, []searchObjectSpec{{
		APIName: "Contact", KeyPrefix: "003", Label: "Contact",
		Fields: []searchFieldSpec{
			{APIName: "Name", Label: "Full Name", Type: "string", IsNameField: true},
			{APIName: "Email", Label: "Email", Type: "email"},
		},
	}})
	var got querysemantic.Request
	queryStarted := make(chan struct{})
	queryDone := make(chan error, 1)
	service := searchTestService(
		t,
		view,
		func(
			context.Context,
			string,
			searchsemantic.Request,
		) (searchsemantic.Response, error) {
			t.Fatal("suggestions used substring Search")
			return searchsemantic.Response{}, nil
		},
		func(
			ctx context.Context,
			bearer string,
			request querysemantic.Request,
		) (querysemantic.Response, error) {
			if bearer != "user-secret" {
				t.Fatalf("bearer=%q", bearer)
			}
			got = request
			close(queryStarted)
			<-ctx.Done()
			queryDone <- ctx.Err()
			return querysemantic.Response{}, &commonsemantic.Failure{
				Category: commonsemantic.FailureCanceled,
			}
		},
	)
	ctx, cancel := context.WithCancel(context.Background())
	request := searchTestRequest(
		http.MethodGet,
		"/services/data/v63.0/search/suggestions?q=ad&sobject=Contact&searchFields=EMAIL",
		"",
	).WithContext(ctx)
	finished := make(chan *httptest.ResponseRecorder, 1)
	go func() {
		recorder := httptest.NewRecorder()
		service.ServeHTTP(recorder, request)
		finished <- recorder
	}()
	<-queryStarted
	cancel()
	select {
	case err := <-queryDone:
		if !errors.Is(err, context.Canceled) {
			t.Fatalf("query cancellation=%v", err)
		}
	case <-time.After(time.Second):
		t.Fatal("Query did not observe caller cancellation")
	}
	recorder := <-finished
	if recorder.Code != 499 {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	if got.ObjectID != "Contact" ||
		got.MinCatalogRevision == nil ||
		*got.MinCatalogRevision != 17 ||
		got.Filter.Kind != querysemantic.PredicateOr ||
		len(got.Filter.Children) != 2 {
		t.Fatalf("query=%#v", got)
	}
	for _, predicate := range got.Filter.Children {
		if predicate.Operator != querysemantic.ComparisonStartsWith {
			t.Fatalf("predicate=%#v", predicate)
		}
	}
}

func TestSuggestionProjectionOnlyReturnsMatchingEmail(t *testing.T) {
	spec := searchObjectSpec{
		APIName: "Contact",
		Fields: []searchFieldSpec{
			{APIName: "Name", Type: "string", IsNameField: true},
			{APIName: "Email", Type: "email"},
		},
	}
	recordID := "003-contact"
	name := "Ada Lovelace"
	email := "unrelated@example.com"
	record := querysemantic.Record{
		ObjectID: "Contact",
		Fields: []querysemantic.Field{
			{
				Name: "Id",
				Value: querysemantic.Value{
					Kind: querysemantic.ValueReference, Reference: &recordID,
				},
			},
			{
				Name: "Name",
				Value: querysemantic.Value{
					Kind: querysemantic.ValueString, String: &name,
				},
			},
			{
				Name: "Email",
				Value: querysemantic.Value{
					Kind: querysemantic.ValueString, String: &email,
				},
			},
		},
	}
	projected, err := suggestionProjection("v63.0", spec, record, true, "ad")
	if err != nil {
		t.Fatal(err)
	}
	if _, found := projected["Email"]; found {
		t.Fatalf("name-only prefix match exposed unrelated email: %#v", projected)
	}
	projected, err = suggestionProjection("v63.0", spec, record, true, "UNREL")
	if err != nil {
		t.Fatal(err)
	}
	if projected["Email"] != email {
		t.Fatalf("email prefix match projection = %#v", projected)
	}
}

func TestScopedLookupFailsClosedWithoutImmutableScopeMetadata(t *testing.T) {
	view := searchTestView(t, []searchObjectSpec{
		{
			APIName: "OpportunityLineItem", Label: "Opportunity Product",
			Fields: []searchFieldSpec{
				{
					APIName: "PricebookEntryId", Label: "Price Book Entry",
					Type: "reference", ReferenceTo: []string{"PricebookEntry"},
				},
				{
					APIName: "OpportunityId", Label: "Opportunity",
					Type: "reference", ReferenceTo: []string{"Opportunity"},
				},
			},
		},
		{
			APIName: "PricebookEntry", Label: "Price Book Entry",
			SearchAllDefaultFields: []string{"Name"},
			Fields: []searchFieldSpec{{
				APIName: "Name", Label: "Name", Type: "string", IsNameField: true,
			}},
		},
	})
	var searchCalls atomic.Uint64
	var queryCalls atomic.Uint64
	service := searchTestService(
		t,
		view,
		func(
			context.Context,
			string,
			searchsemantic.Request,
		) (searchsemantic.Response, error) {
			searchCalls.Add(1)
			return searchsemantic.Response{}, nil
		},
		func(
			context.Context,
			string,
			querysemantic.Request,
		) (querysemantic.Response, error) {
			queryCalls.Add(1)
			return querysemantic.Response{}, nil
		},
	)
	recorder := httptest.NewRecorder()
	service.ServeHTTP(
		recorder,
		searchTestRequest(
			http.MethodGet,
			"/services/data/v63.0/search?q="+
				"FIND+%7Bpr%7D+IN+ALL+FIELDS+RETURNING+PricebookEntry%28Id%2CName%29"+
				"&lookupSourceObject=OpportunityLineItem"+
				"&lookupSourceField=PricebookEntryId"+
				"&lookupContext=%7B%22OpportunityId%22%3A%22006-launch%22%7D",
			"",
		),
	)
	if recorder.Code != http.StatusServiceUnavailable ||
		searchCalls.Load() != 0 || queryCalls.Load() != 0 {
		t.Fatalf(
			"status=%d search=%d query=%d body=%s",
			recorder.Code, searchCalls.Load(), queryCalls.Load(),
			recorder.Body.String(),
		)
	}
}

func TestScopedLookupUsesOneRevisionForParentAndTargetQueries(t *testing.T) {
	validatorConfig := `{"filterMappings":[{"recordField":"OpportunityId",` +
		`"sourceField":"Pricebook2Id","targetField":"Pricebook2Id"}]}`
	view := searchTestView(t, []searchObjectSpec{
		{
			APIName: "OpportunityLineItem", Label: "Opportunity Product",
			Fields: []searchFieldSpec{
				{
					APIName: "PricebookEntryId", Label: "Price Book Entry",
					Type: "reference", ReferenceTo: []string{"PricebookEntry"},
					Validator: runtimeScopedLookupRule, ValidatorConfig: validatorConfig,
				},
				{
					APIName: "OpportunityId", Label: "Opportunity",
					Type: "reference", ReferenceTo: []string{"Opportunity"},
				},
			},
		},
		{
			APIName: "Opportunity", Label: "Opportunity",
			Fields: []searchFieldSpec{
				{APIName: "Name", Label: "Name", Type: "string", IsNameField: true},
				{APIName: "Pricebook2Id", Label: "Price Book", Type: "reference", ReferenceTo: []string{"Pricebook2"}},
			},
		},
		{
			APIName: "PricebookEntry", Label: "Price Book Entry",
			SearchAllDefaultFields: []string{"Name"},
			Fields: []searchFieldSpec{
				{APIName: "Name", Label: "Name", Type: "string", IsNameField: true},
				{APIName: "Pricebook2Id", Label: "Price Book", Type: "reference", ReferenceTo: []string{"Pricebook2"}},
			},
		},
	})
	var requests []querysemantic.Request
	service := searchTestService(
		t,
		view,
		func(
			context.Context,
			string,
			searchsemantic.Request,
		) (searchsemantic.Response, error) {
			t.Fatal("scoped lookup degraded to unfiltered Search")
			return searchsemantic.Response{}, nil
		},
		func(
			_ context.Context,
			bearer string,
			request querysemantic.Request,
		) (querysemantic.Response, error) {
			if bearer != "user-secret" {
				t.Fatalf("bearer=%q", bearer)
			}
			requests = append(requests, request)
			if request.ObjectID == "Opportunity" {
				pricebookID := "01s-launch"
				return querysemantic.Response{
					TotalSize: 1, Done: true,
					Records: []querysemantic.Record{{
						ObjectID: "Opportunity",
						Fields: []querysemantic.Field{{
							Name: "Pricebook2Id",
							Value: querysemantic.Value{
								Kind:      querysemantic.ValueReference,
								Reference: &pricebookID,
							},
						}},
					}},
					Receipts: queryReceipts(17),
				}, nil
			}
			recordID := "01u-launch"
			name := "Launch product"
			return querysemantic.Response{
				TotalSize: 1, Done: true,
				Records: []querysemantic.Record{{
					ObjectID: "PricebookEntry",
					Fields: []querysemantic.Field{
						{
							Name: "Id",
							Value: querysemantic.Value{
								Kind:      querysemantic.ValueReference,
								Reference: &recordID,
							},
						},
						{
							Name: "Name",
							Value: querysemantic.Value{
								Kind:   querysemantic.ValueString,
								String: &name,
							},
						},
					},
				}},
				Receipts: queryReceipts(17),
			}, nil
		},
	)
	recorder := httptest.NewRecorder()
	service.ServeHTTP(
		recorder,
		searchTestRequest(
			http.MethodGet,
			"/services/data/v63.0/search?q="+
				"FIND+%7Bpr%7D+IN+ALL+FIELDS+RETURNING+PricebookEntry%28Id%2CName%29"+
				"&lookupSourceObject=OpportunityLineItem"+
				"&lookupSourceField=PricebookEntryId"+
				"&lookupContext=%7B%22OpportunityId%22%3A%22006-launch%22%7D",
			"",
		),
	)
	if recorder.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	if len(requests) != 2 {
		t.Fatalf("query requests=%d", len(requests))
	}
	for _, request := range requests {
		if request.MinCatalogRevision == nil ||
			*request.MinCatalogRevision != 17 {
			t.Fatalf("request=%#v", request)
		}
	}
	target := requests[1]
	if target.ObjectID != "PricebookEntry" ||
		target.Filter.Kind != querysemantic.PredicateAnd ||
		len(target.Filter.Children) != 2 ||
		target.Filter.Children[0].Operator != querysemantic.ComparisonContains ||
		target.Filter.Children[1].Operator != querysemantic.ComparisonEqual {
		t.Fatalf("target request=%#v", target)
	}
	if !strings.Contains(recorder.Body.String(), `"Id":"01u-launch"`) ||
		!strings.Contains(recorder.Body.String(), `"Name":"Launch product"`) {
		t.Fatalf("body=%s", recorder.Body.String())
	}
}
