package catalogreconcile

import (
	"encoding/json"
	"strings"
	"testing"

	"github.com/nexuscrm/kernel/pkg/schemautil"

	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	consumerv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/catalog/consumerv1"
	"github.com/pingcap-inc/nexuscrm-controlplane/catalogapp"
)

func TestBuildEffectiveObjectSearchComponentIsExactAndDeterministic(t *testing.T) {
	keyPrefix := "001"
	validator := "runtime_scoped_lookup"
	validatorConfig := `{"filterMappings":[]}`
	formulaReturnType := constants.FieldTypePercent
	formulaPrecision := 18
	formulaScale := 2
	defaultStatus := "Active"
	childRelationshipName := "Contacts"
	deleteRule := models.DeleteRule(constants.DeleteRuleCascade)
	objects := []*models.ObjectMetadata{{
		APIName:     "Account",
		KeyPrefix:   &keyPrefix,
		Label:       "Account",
		PluralLabel: "Accounts",
		TableType:   constants.TableTypeCustomObject,
		Searchable:  true,
		ListFields:  []string{"status", " Name "},
		Fields: []models.FieldMetadata{
			{
				APIName: "ComputedMargin", Label: "Computed Margin",
				Type: constants.FieldTypeFormula, ReturnType: &formulaReturnType,
				Precision: &formulaPrecision, Scale: &formulaScale,
			},
			{
				APIName: "OwnerId", Label: "Owner",
				Type:        constants.FieldTypeReference,
				ReferenceTo: []string{"User", "Group"},
				Validator:   &validator, ValidatorConfig: &validatorConfig,
				ChildRelationshipName: &childRelationshipName,
				DeleteRule:            &deleteRule, IsMasterDetail: true,
			},
			{
				APIName: "Name", Label: "Account Name",
				Type: constants.FieldTypeString, IsNameField: true, IsSystem: true,
			},
			{
				APIName: "Status", Label: "Status",
				Type: constants.FieldTypePicklist, Required: true,
				Createable: true, Updateable: true, DefaultValue: &defaultStatus,
				Options: models.PicklistOptions{{Value: "Active", Label: "Active", Tags: []string{"open"}}},
			},
		},
	}}
	declarations := []searchAllDefaultFieldsDeclaration{{
		ObjectAPIName: "Account",
		Fields:        []string{"Name"},
	}}
	first, err := buildObjectSearchForTest(
		objects, declarations, consumerv1.AppMetadataV3CapabilityName,
	)
	if err != nil {
		t.Fatal(err)
	}
	second, err := buildObjectSearchForTest(
		objects, declarations, consumerv1.AppMetadataV3CapabilityName,
	)
	if err != nil {
		t.Fatal(err)
	}
	if first.ComponentKind != consumerv1.EffectiveObjectSearchComponentKind ||
		first.BusinessKey !=
			consumerv1.EffectiveObjectSearchComponentBusinessKey ||
		first.ComponentFormatVersion != 6 ||
		first.ContentDigest != second.ContentDigest ||
		string(first.CanonicalBehavior) != string(second.CanonicalBehavior) ||
		consumerv1.ValidateEffectiveMetadataComponent(first) != nil {
		t.Fatalf("unexpected component: %#v", first)
	}
	if strings.Count(string(first.CanonicalBehavior), `"returnType"`) != 1 {
		t.Fatalf("returnType projection is not sparse: %s", first.CanonicalBehavior)
	}
	for _, absent := range []string{
		`"isNameField":false`, `"isSystem":false`, `"referenceTo":[]`,
		`"validator":""`, `"validatorConfig":""`,
	} {
		if strings.Contains(string(first.CanonicalBehavior), absent) {
			t.Fatalf("effective object-search projection contains default field %s: %s", absent, first.CanonicalBehavior)
		}
	}
	var document effectiveObjectSearchDocument
	if err := json.Unmarshal(first.CanonicalBehavior, &document); err != nil {
		t.Fatal(err)
	}
	if len(document.Objects) != 1 ||
		document.Objects[0].PluralLabel != "Accounts" ||
		document.Objects[0].TableType != string(constants.TableTypeCustomObject) ||
		len(document.Objects[0].Fields) != 4 ||
		document.Objects[0].Fields[0].APIName != "ComputedMargin" ||
		document.Objects[0].Fields[0].ReturnType != "percent" ||
		document.Objects[0].Fields[0].Precision == nil ||
		*document.Objects[0].Fields[0].Precision != 18 ||
		document.Objects[0].Fields[0].Scale == nil ||
		*document.Objects[0].Fields[0].Scale != 2 ||
		document.Objects[0].Fields[1].APIName != "Name" ||
		document.Objects[0].Fields[1].ReturnType != "" ||
		!document.Objects[0].Fields[1].IsSystem ||
		document.Objects[0].Fields[2].APIName != "OwnerId" ||
		document.Objects[0].Fields[2].ChildRelationshipName != "Contacts" ||
		document.Objects[0].Fields[2].DeleteRule != "Cascade" ||
		!document.Objects[0].Fields[2].IsMasterDetail ||
		document.Objects[0].Fields[3].APIName != "Status" ||
		!document.Objects[0].Fields[3].Required ||
		!document.Objects[0].Fields[3].Createable ||
		!document.Objects[0].Fields[3].Updateable ||
		document.Objects[0].Fields[3].DefaultValue == nil ||
		*document.Objects[0].Fields[3].DefaultValue != "Active" ||
		len(document.Objects[0].Fields[3].Options) != 1 ||
		document.Objects[0].Fields[3].Options[0].Value != "Active" ||
		strings.Join(document.Objects[0].Fields[3].Options[0].Tags, ",") != "open" ||
		len(document.Objects[0].SearchAllDefaultFields) != 1 ||
		document.Objects[0].SearchAllDefaultFields[0] != "Name" ||
		strings.Join(
			document.Objects[0].Fields[2].ReferenceTo, ",",
		) != "Group,User" {
		t.Fatalf("unexpected effective object-search document: %#v", document)
	}
	var listFieldsDocument struct {
		Objects []struct {
			ListFields []string `json:"listFields"`
		} `json:"objects"`
	}
	if err := json.Unmarshal(first.CanonicalBehavior, &listFieldsDocument); err != nil {
		t.Fatal(err)
	}
	if len(listFieldsDocument.Objects) != 1 ||
		strings.Join(listFieldsDocument.Objects[0].ListFields, ",") != "Status,Name" {
		t.Fatalf("projected list fields = %#v, want Status,Name", listFieldsDocument.Objects)
	}
}

func TestEffectiveObjectSearchCreateInputContextPreservesPresenceAndHistoricalBytes(t *testing.T) {
	defaultValue := "Assigned"
	object := &models.ObjectMetadata{APIName: "WorkItem", Label: "Work item", Fields: []models.FieldMetadata{
		{APIName: "Title", Label: "Title", Type: constants.FieldTypeString, Required: true, Createable: true},
		{APIName: "Compound", Label: "Compound", Type: constants.FieldTypeString, Required: true, Createable: true, IsCompound: true, ComposedFrom: []string{"Title"}},
		{APIName: "WithDefault", Label: "With default", Type: constants.FieldTypeString, Required: true, Createable: true, DefaultValue: &defaultValue},
		{APIName: "ServerAssigned", Label: "Server assigned", Type: constants.FieldTypeString, Required: true, Createable: true, DefaultedOnCreate: true},
		{APIName: "ReadOnly", Label: "Read only", Type: constants.FieldTypeString, Required: true},
		{APIName: "Computed", Label: "Computed", Type: constants.FieldTypeFormula, Required: true, Createable: true},
	}}
	component, err := buildObjectSearchForTest([]*models.ObjectMetadata{object}, nil, consumerv1.AppMetadataV3CapabilityName)
	if err != nil {
		t.Fatal(err)
	}
	var document effectiveObjectSearchDocument
	if err := json.Unmarshal(component.CanonicalBehavior, &document); err != nil {
		t.Fatal(err)
	}
	fields := document.Objects[0].CreateRequiredFields
	if component.ComponentFormatVersion != 6 || fields == nil || strings.Join(*fields, ",") != "Compound,Title" {
		t.Fatalf("create input context = %s", component.CanonicalBehavior)
	}
	// This is a read projection: caller requirements do not rewrite Required.
	for _, field := range document.Objects[0].Fields {
		if !field.Required {
			t.Fatalf("schema required flag changed for %s", field.APIName)
		}
	}
	empty := []*models.ObjectMetadata{{APIName: "Empty", Label: "Empty"}}
	for _, format := range []uint32{2, 3, 4, 5, 6} {
		component, err := buildObjectSearchForFormatForTest(empty, nil, format)
		if err != nil {
			t.Fatal(err)
		}
		want := `{"objects":[{"apiName":"Empty","label":"Empty","tableType":"","searchable":false,"fields":[]}]}`
		if format == 6 {
			want = `{"objects":[{"apiName":"Empty","label":"Empty","tableType":"","searchable":false,"createRequiredFields":[],"fields":[]}]}`
		}
		if string(component.CanonicalBehavior) != want {
			t.Fatalf("format %d bytes = %s, want %s", format, component.CanonicalBehavior, want)
		}
	}
}

func TestBuildEffectiveObjectSearchComponentProjectsSiblingMaxInFormatV4(t *testing.T) {
	parentField := "ParentId"
	object := &models.ObjectMetadata{
		APIName: "RankedChild", Label: "Ranked Child",
		Fields: []models.FieldMetadata{
			{
				APIName: "ParentId", Label: "Parent", Type: constants.FieldTypeReference,
				ReferenceTo: []string{"Parent"}, Createable: true, Updateable: true,
			},
			{
				APIName: "Sequence", Label: "Sequence", Type: constants.FieldTypeInt,
				Createable: true, Updateable: true, SiblingMaxParentField: &parentField,
			},
		},
	}
	component, err := buildObjectSearchForTest(
		[]*models.ObjectMetadata{object}, nil, consumerv1.AppMetadataV3CapabilityName,
	)
	if err != nil {
		t.Fatal(err)
	}
	if component.ComponentFormatVersion != effectiveObjectSearchComponentFormatV6 ||
		!strings.Contains(string(component.CanonicalBehavior), `"siblingMaxParentField":"ParentId"`) {
		t.Fatalf("format=%d behavior=%s", component.ComponentFormatVersion, component.CanonicalBehavior)
	}
	historical, err := buildObjectSearchForFormatForTest(
		[]*models.ObjectMetadata{object}, nil, effectiveObjectSearchComponentFormatV3,
	)
	if err != nil {
		t.Fatal(err)
	}
	if strings.Contains(string(historical.CanonicalBehavior), `"siblingMaxParentField"`) {
		t.Fatalf("format-3 compatibility projection exposed format-4 fact: %s", historical.CanonicalBehavior)
	}
}

func TestBuildEffectiveObjectSearchComponentPreservesRelationshipDisplayNamePath(t *testing.T) {
	displayNameField := "Account.ExternalId__c"
	relationshipName := "Account"
	objects := []*models.ObjectMetadata{{
		APIName: "Widget", Label: "Widget", DisplayNameField: &displayNameField,
		Fields: []models.FieldMetadata{{
			APIName: "AccountId", Label: "Account", Type: constants.FieldTypeReference,
			ReferenceTo: []string{"Account"}, RelationshipName: &relationshipName,
		}},
	}, {
		APIName: "Account", Label: "Account",
		Fields: []models.FieldMetadata{{
			APIName: "ExternalId__c", Label: "External ID", Type: constants.FieldTypeString,
		}},
	}}
	component, err := buildObjectSearchForTest(
		objects, nil, consumerv1.AppMetadataV3CapabilityName,
	)
	if err != nil {
		t.Fatal(err)
	}
	if component.ComponentFormatVersion != effectiveObjectSearchComponentFormatV6 ||
		!strings.Contains(string(component.CanonicalBehavior), `"displayNameField":"Account.ExternalId__c"`) {
		t.Fatalf("format=%d behavior=%s", component.ComponentFormatVersion, component.CanonicalBehavior)
	}
	historical, err := buildObjectSearchForFormatForTest(
		objects, nil, effectiveObjectSearchComponentFormatV4,
	)
	if err != nil {
		t.Fatal(err)
	}
	if strings.Contains(string(historical.CanonicalBehavior), `"displayNameField"`) {
		t.Fatalf("format-4 compatibility projection exposed format-5 fact: %s", historical.CanonicalBehavior)
	}
}

func TestEffectiveDisplayNameFieldPrefersExactAPIName(t *testing.T) {
	displayNameField := "Label"
	relationshipName := "Label"
	object := &models.ObjectMetadata{APIName: "Widget", DisplayNameField: &displayNameField, Fields: []models.FieldMetadata{
		{
			APIName: "ParentId", Type: constants.FieldTypeReference,
			RelationshipName: &relationshipName,
		},
		{APIName: "Label", Type: constants.FieldTypeString},
	}}
	if _, err := buildObjectSearchForTest(
		[]*models.ObjectMetadata{object}, nil, consumerv1.AppMetadataV3CapabilityName,
	); err != nil {
		t.Fatalf("exact scalar API name was shadowed by an earlier relationship name: %v", err)
	}
}

func TestBuildEffectiveObjectSearchComponentRejectsInvalidRelationshipDisplayNamePath(t *testing.T) {
	displayNameField := "Account.ExternalId__c"
	relationshipName := "Account"
	_, err := buildObjectSearchForTest(
		[]*models.ObjectMetadata{{
			APIName: "Widget", DisplayNameField: &displayNameField,
			Fields: []models.FieldMetadata{{
				APIName: "AccountId", Type: constants.FieldTypeReference,
				ReferenceTo: []string{"Account"}, RelationshipName: &relationshipName,
			}},
		}},
		nil, consumerv1.AppMetadataV3CapabilityName,
	)
	if err == nil || !strings.Contains(err.Error(), "target object") {
		t.Fatalf("missing relationship target schema error = %v", err)
	}
}

func TestBuildEffectiveObjectSearchComponentProjectsFormulaPicklistSourceInFormatV4(t *testing.T) {
	relationshipName := "Account"
	formulaBody := "TEXT(Account.Region__c)"
	formulaReturnType := constants.FieldTypeString
	component, err := buildObjectSearchForTest(
		[]*models.ObjectMetadata{
			{
				APIName: "Account", Label: "Account",
				Fields: []models.FieldMetadata{{
					APIName: "Region__c", Label: "Region",
					Type: constants.FieldTypePicklist,
					Options: models.PicklistOptions{{
						Value: "APAC", Label: "Asia Pacific", Tags: []string{"regional"},
					}},
				}},
			},
			{
				APIName: "Opportunity", Label: "Opportunity",
				Fields: []models.FieldMetadata{
					{
						APIName: "AccountId", Label: "Account",
						Type:             constants.FieldTypeReference,
						ReferenceTo:      []string{"Account"},
						RelationshipName: &relationshipName,
					},
					{
						APIName: "Region__c", Label: "Region",
						Type:    constants.FieldTypeFormula,
						Formula: &formulaBody, ReturnType: &formulaReturnType,
					},
				},
			},
		},
		nil,
		consumerv1.AppMetadataV3CapabilityName,
	)
	if err != nil {
		t.Fatal(err)
	}
	if component.ComponentFormatVersion != 6 {
		t.Fatalf(
			"object-search component format = %d, want 5 for formula read facts",
			component.ComponentFormatVersion,
		)
	}
	var document struct {
		Objects []struct {
			APIName string `json:"apiName"`
			Fields  []struct {
				APIName              string  `json:"apiName"`
				Formula              *string `json:"formula"`
				PicklistValueSources []struct {
					Object string `json:"object"`
					Field  string `json:"field"`
				} `json:"picklistValueSources"`
			} `json:"fields"`
		} `json:"objects"`
	}
	if err := json.Unmarshal(component.CanonicalBehavior, &document); err != nil {
		t.Fatal(err)
	}
	var formulaField *struct {
		APIName              string  `json:"apiName"`
		Formula              *string `json:"formula"`
		PicklistValueSources []struct {
			Object string `json:"object"`
			Field  string `json:"field"`
		} `json:"picklistValueSources"`
	}
	for objectIndex := range document.Objects {
		if document.Objects[objectIndex].APIName != "Opportunity" {
			continue
		}
		for fieldIndex := range document.Objects[objectIndex].Fields {
			if document.Objects[objectIndex].Fields[fieldIndex].APIName == "Region__c" {
				formulaField = &document.Objects[objectIndex].Fields[fieldIndex]
			}
		}
	}
	if formulaField == nil || formulaField.Formula == nil ||
		*formulaField.Formula != formulaBody || len(formulaField.PicklistValueSources) != 1 ||
		formulaField.PicklistValueSources[0].Object != "Account" ||
		formulaField.PicklistValueSources[0].Field != "Region__c" {
		t.Fatalf("formula read projection = %#v", formulaField)
	}
	historical, err := buildObjectSearchForFormatForTest(
		[]*models.ObjectMetadata{{
			APIName: "Opportunity", Label: "Opportunity",
			Fields: []models.FieldMetadata{{
				APIName: "Region__c", Label: "Region", Type: constants.FieldTypeFormula,
				Formula: &formulaBody, ReturnType: &formulaReturnType,
			}},
		}}, nil, effectiveObjectSearchComponentFormatV3,
	)
	if err != nil {
		t.Fatal(err)
	}
	if strings.Contains(string(historical.CanonicalBehavior), `"formula":`) ||
		strings.Contains(string(historical.CanonicalBehavior), `"picklistValueSources"`) {
		t.Fatalf("format-3 compatibility projection exposed format-4 facts: %s", historical.CanonicalBehavior)
	}
}

func TestBuildEffectiveObjectSearchComponentProjectsActiveScalarFormulaFactsInFormatV4(t *testing.T) {
	relationshipName := "Account"
	formulaBody := "TEXT($Record.Account.Region__c)"
	formulaMode := "active"
	component, err := buildObjectSearchForTest(
		[]*models.ObjectMetadata{
			{
				APIName: "Account", Label: "Account",
				Fields: []models.FieldMetadata{{
					APIName: "Region__c", Label: "Region", Type: constants.FieldTypePicklist,
				}},
			},
			{
				APIName: "Opportunity", Label: "Opportunity",
				Fields: []models.FieldMetadata{
					{
						APIName: "AccountId", Label: "Account",
						Type: constants.FieldTypeMasterDetail, ReferenceTo: []string{"Account"},
						RelationshipName: &relationshipName,
					},
					{
						APIName: "Region__c", Label: "Region",
						Type: constants.FieldTypeString, Formula: &formulaBody,
						FormulaMode: &formulaMode,
					},
				},
			},
		},
		nil,
		consumerv1.AppMetadataV3CapabilityName,
	)
	if err != nil {
		t.Fatal(err)
	}
	var document effectiveObjectSearchDocument
	if err := json.Unmarshal(component.CanonicalBehavior, &document); err != nil {
		t.Fatal(err)
	}
	var projected *effectiveObjectSearchField
	for objectIndex := range document.Objects {
		if document.Objects[objectIndex].APIName != "Opportunity" {
			continue
		}
		for fieldIndex := range document.Objects[objectIndex].Fields {
			if document.Objects[objectIndex].Fields[fieldIndex].APIName == "Region__c" {
				projected = &document.Objects[objectIndex].Fields[fieldIndex]
			}
		}
	}
	if projected == nil || projected.Formula == nil ||
		*projected.Formula != formulaBody || len(projected.PicklistValueSources) != 1 ||
		projected.PicklistValueSources[0].Object != "Account" ||
		projected.PicklistValueSources[0].Field != "Region__c" {
		t.Fatalf("active scalar formula read projection = %#v", projected)
	}
}

func TestEffectiveFieldHasReadFormulaRequiresCanonicalActiveMode(t *testing.T) {
	formulaBody := "TEXT(RegionSource__c)"
	for _, formulaMode := range []string{"ACTIVE", " active ", "Active"} {
		field := models.FieldMetadata{
			APIName: "Region__c", Type: constants.FieldTypeString,
			Formula: &formulaBody, FormulaMode: &formulaMode,
		}
		if effectiveFieldHasReadFormula(field) {
			t.Fatalf("non-canonical formula mode %q was treated as active", formulaMode)
		}
	}
	formulaMode := "active"
	field := models.FieldMetadata{
		APIName: "Region__c", Type: constants.FieldTypeString,
		Formula: &formulaBody, FormulaMode: &formulaMode,
	}
	if !effectiveFieldHasReadFormula(field) {
		t.Fatal("canonical active formula mode was not projected")
	}
}

func TestEffectiveFormulaPicklistValueSourcesOnlyProjectsExactPassThroughs(t *testing.T) {
	relationshipName := "Account"
	newObjects := func(formulaBody string) ([]*models.ObjectMetadata, *models.ObjectMetadata, models.FieldMetadata) {
		account := &models.ObjectMetadata{
			APIName: "Account", Label: "Account",
			Fields: []models.FieldMetadata{{
				APIName: "Region__c", Label: "Region", Type: constants.FieldTypePicklist,
			}},
		}
		opportunity := &models.ObjectMetadata{
			APIName: "Opportunity", Label: "Opportunity",
			Fields: []models.FieldMetadata{
				{
					APIName: "RegionSource__c", Label: "Region Source",
					Type: constants.FieldTypePicklist,
				},
				{
					APIName: "AccountId", Label: "Account", Type: constants.FieldTypeReference,
					ReferenceTo: []string{"Account"}, RelationshipName: &relationshipName,
				},
			},
		}
		formulaField := models.FieldMetadata{
			APIName: "Region__c", Label: "Region", Type: constants.FieldTypeFormula,
			Formula: &formulaBody,
		}
		return []*models.ObjectMetadata{account, opportunity}, opportunity, formulaField
	}

	t.Run("current object", func(t *testing.T) {
		objects, current, field := newObjects("TEXT(RegionSource__c)")
		sources := effectiveFormulaPicklistValueSources(objects, current, field)
		if len(sources) != 1 || sources[0].Object != "Opportunity" ||
			sources[0].Field != "RegionSource__c" {
			t.Fatalf("current-object source = %#v", sources)
		}
	})

	for _, test := range []struct {
		name         string
		body         string
		masterDetail bool
	}{
		{name: "one-hop reference", body: "TEXT(Account.Region__c)"},
		{name: "record-context one-hop reference", body: "TEXT($Record.Account.Region__c)"},
		{name: "one-hop master-detail", body: "TEXT(Account.Region__c)", masterDetail: true},
	} {
		t.Run(test.name, func(t *testing.T) {
			objects, current, field := newObjects(test.body)
			if test.masterDetail {
				current.Fields[1].Type = constants.FieldTypeMasterDetail
			}
			sources := effectiveFormulaPicklistValueSources(objects, current, field)
			if len(sources) != 1 || sources[0].Object != "Account" ||
				sources[0].Field != "Region__c" {
				t.Fatalf("one-hop source = %#v", sources)
			}
		})
	}

	for _, test := range []struct {
		name   string
		body   string
		mutate func([]*models.ObjectMetadata, *models.ObjectMetadata)
	}{
		{name: "transformed value", body: `TEXT(Account.Region__c) & "-suffix"`},
		{
			name: "ambiguous relationship", body: "TEXT(Account.Region__c)",
			mutate: func(_ []*models.ObjectMetadata, current *models.ObjectMetadata) {
				current.Fields = append(current.Fields, models.FieldMetadata{
					APIName: "OtherAccountId", Label: "Other Account",
					Type: constants.FieldTypeReference, ReferenceTo: []string{"Account"},
					RelationshipName: &relationshipName,
				})
			},
		},
		{
			name: "polymorphic relationship", body: "TEXT(Account.Region__c)",
			mutate: func(_ []*models.ObjectMetadata, current *models.ObjectMetadata) {
				current.Fields[1].IsPolymorphic = true
				current.Fields[1].ReferenceTo = []string{"Account", "Contact"}
			},
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			objects, current, field := newObjects(test.body)
			if test.mutate != nil {
				test.mutate(objects, current)
			}
			if sources := effectiveFormulaPicklistValueSources(objects, current, field); len(sources) != 0 {
				t.Fatalf("non-pass-through formula projected sources = %#v", sources)
			}
		})
	}
}

func TestBuildEffectiveObjectSearchComponentProjectsPathFieldInNewFormat(t *testing.T) {
	pathField := "stagename"
	wantPathField := "StageName"
	component, err := buildObjectSearchForTest(
		[]*models.ObjectMetadata{{
			APIName: "Opportunity", Label: "Opportunity",
			PathField: &pathField,
			Fields: []models.FieldMetadata{{
				APIName: "StageName", Label: "Stage", Type: constants.FieldTypePicklist,
			}},
		}},
		nil,
		consumerv1.AppMetadataV3CapabilityName,
	)
	if err != nil {
		t.Fatal(err)
	}
	if component.ComponentFormatVersion != 6 {
		t.Fatalf(
			"object-search component format = %d, want 5 for current V3 grammar",
			component.ComponentFormatVersion,
		)
	}
	var document struct {
		Objects []struct {
			APIName   string `json:"apiName"`
			PathField string `json:"pathField"`
		} `json:"objects"`
	}
	if err := json.Unmarshal(component.CanonicalBehavior, &document); err != nil {
		t.Fatal(err)
	}
	if len(document.Objects) != 1 ||
		document.Objects[0].APIName != "Opportunity" ||
		document.Objects[0].PathField != wantPathField {
		t.Fatalf("projected path field = %#v, want Opportunity.StageName", document.Objects)
	}
	objects := []*models.ObjectMetadata{{
		APIName: "Opportunity", Label: "Opportunity", PathField: &pathField,
		ListFields: []string{"StageName"},
		Fields: []models.FieldMetadata{{
			APIName: "StageName", Label: "Stage", Type: constants.FieldTypePicklist,
		}},
	}}
	historical, err := buildObjectSearchForFormatForTest(
		objects, nil, effectiveObjectSearchComponentFormatV3,
	)
	if err != nil {
		t.Fatal(err)
	}
	if historical.ComponentFormatVersion != 3 ||
		!strings.Contains(string(historical.CanonicalBehavior), "\"pathField\":\"StageName\"") ||
		strings.Contains(string(historical.CanonicalBehavior), "\"listFields\"") {
		t.Fatalf("format-3 compatibility projection has the wrong grammar: %#v", historical)
	}
	legacy, err := buildObjectSearchForTest(
		objects,
		nil,
		consumerv1.AppMetadataV2CapabilityName,
	)
	if err != nil {
		t.Fatal(err)
	}
	if legacy.ComponentFormatVersion != 2 ||
		strings.Contains(string(legacy.CanonicalBehavior), `"pathField"`) ||
		strings.Contains(string(legacy.CanonicalBehavior), `"listFields"`) {
		t.Fatalf("V2 compatibility component exposed newer V3 grammar: %#v", legacy)
	}
}

func TestBuildEffectiveObjectSearchComponentRejectsInvalidListFields(t *testing.T) {
	for _, test := range []struct {
		name       string
		listFields []string
		wantDetail string
	}{
		{name: "missing", listFields: []string{"Missing"}, wantDetail: "list field[0] \"Missing\" is absent"},
		{name: "blank", listFields: []string{" "}, wantDetail: "list field[0] \"\" is absent"},
		{name: "duplicate", listFields: []string{"Name", "name"}, wantDetail: "duplicate list field \"Name\""},
	} {
		t.Run(test.name, func(t *testing.T) {
			_, err := buildObjectSearchForTest(
				[]*models.ObjectMetadata{{
					APIName: "Account", Label: "Account", ListFields: test.listFields,
					Fields: []models.FieldMetadata{{
						APIName: "Name", Label: "Name", Type: constants.FieldTypeString,
					}},
				}},
				nil,
				consumerv1.AppMetadataV3CapabilityName,
			)
			if err == nil || !strings.Contains(err.Error(), test.wantDetail) {
				t.Fatalf("invalid list fields error = %v, want %q", err, test.wantDetail)
			}
		})
	}
}

func TestBuildEffectiveObjectSearchComponentPreservesExplicitEmptyListFields(t *testing.T) {
	component, err := buildObjectSearchForTest(
		[]*models.ObjectMetadata{
			{
				APIName: "ExplicitEmpty", Label: "Explicit Empty",
				ListFields: []string{},
			},
			{
				APIName: "Unconfigured", Label: "Unconfigured",
			},
		},
		nil,
		consumerv1.AppMetadataV3CapabilityName,
	)
	if err != nil {
		t.Fatal(err)
	}
	var document struct {
		Objects []map[string]json.RawMessage `json:"objects"`
	}
	if err := json.Unmarshal(component.CanonicalBehavior, &document); err != nil {
		t.Fatal(err)
	}
	if len(document.Objects) != 2 {
		t.Fatalf("projected objects = %d, want 2", len(document.Objects))
	}
	if got, present := document.Objects[0]["listFields"]; !present || string(got) != "[]" {
		t.Fatalf("explicit empty listFields = %s, present=%v; want []", got, present)
	}
	if got, present := document.Objects[1]["listFields"]; present {
		t.Fatalf("unconfigured listFields unexpectedly projected as %s", got)
	}
}

func TestBuildEffectiveObjectSearchComponentRejectsInvalidPathField(t *testing.T) {
	for _, test := range []struct {
		name       string
		pathField  string
		fieldType  models.FieldType
		wantDetail string
	}{
		{
			name: "missing", pathField: "MissingStage",
			fieldType: constants.FieldTypePicklist, wantDetail: `path field "MissingStage" is absent`,
		},
		{
			name: "not-picklist", pathField: "StageName",
			fieldType: constants.FieldTypeString, wantDetail: `has type "string", want picklist`,
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			_, err := buildObjectSearchForTest(
				[]*models.ObjectMetadata{{
					APIName: "Opportunity", Label: "Opportunity", PathField: &test.pathField,
					Fields: []models.FieldMetadata{{
						APIName: "StageName", Label: "Stage", Type: test.fieldType,
					}},
				}},
				nil,
				consumerv1.AppMetadataV3CapabilityName,
			)
			if err == nil || !strings.Contains(err.Error(), test.wantDetail) {
				t.Fatalf("invalid path field error = %v, want %q", err, test.wantDetail)
			}
		})
	}
}

func TestBuildEffectiveObjectSearchComponentProjectsUniqueSparselyAndChangesDigest(
	t *testing.T,
) {
	build := func(unique bool) catalogapp.EffectiveMetadataComponent {
		t.Helper()
		component, err := buildObjectSearchForTest(
			[]*models.ObjectMetadata{{
				APIName: "Voucher", Label: "Voucher",
				TableType: constants.TableTypeCustomObject,
				Fields: []models.FieldMetadata{
					{
						APIName: "SourceKey", Label: "Source Key",
						Type: constants.FieldTypeString, IsUnique: unique,
					},
					{
						APIName: "RequiredOnly", Label: "Required Only",
						Type: constants.FieldTypeString, Required: true,
					},
				},
			}},
			nil,
			consumerv1.AppMetadataV3CapabilityName,
		)
		if err != nil {
			t.Fatal(err)
		}
		return component
	}

	withoutUnique := build(false)
	withUnique := build(true)
	wantWithoutUnique := `{"objects":[{"apiName":"Voucher","label":"Voucher","tableType":"custom_object","searchable":false,"createRequiredFields":[],"fields":[{"apiName":"RequiredOnly","label":"Required Only","type":"string","required":true,"createable":false,"updateable":false},{"apiName":"SourceKey","label":"Source Key","type":"string","createable":false,"updateable":false}]}]}`
	wantWithUnique := `{"objects":[{"apiName":"Voucher","label":"Voucher","tableType":"custom_object","searchable":false,"createRequiredFields":[],"fields":[{"apiName":"RequiredOnly","label":"Required Only","type":"string","required":true,"createable":false,"updateable":false},{"apiName":"SourceKey","label":"Source Key","type":"string","unique":true,"createable":false,"updateable":false}]}]}`

	if got := string(withoutUnique.CanonicalBehavior); got != wantWithoutUnique {
		t.Fatalf("non-unique canonical behavior:\n got %s\nwant %s", got, wantWithoutUnique)
	}
	if got := string(withUnique.CanonicalBehavior); got != wantWithUnique {
		t.Fatalf("unique canonical behavior:\n got %s\nwant %s", got, wantWithUnique)
	}
	if strings.Contains(string(withoutUnique.CanonicalBehavior), `"unique"`) ||
		strings.Contains(string(withUnique.CanonicalBehavior), `"unique":false`) ||
		strings.Count(string(withUnique.CanonicalBehavior), `"unique":true`) != 1 {
		t.Fatal("projection does not enforce sparse unique encoding")
	}
	if strings.Contains(string(withUnique.CanonicalBehavior), `"apiName":"SourceKey","label":"Source Key","type":"string","required"`) {
		t.Fatal("projection conflates uniqueness with required")
	}
	if withoutUnique.ComponentFormatVersion != 6 ||
		withUnique.ComponentFormatVersion != 6 {
		t.Fatalf(
			"object-search format versions = %d, %d; want 6",
			withoutUnique.ComponentFormatVersion,
			withUnique.ComponentFormatVersion,
		)
	}
	if withoutUnique.ContentDigest !=
		"sha256:c954857e91233a848d4e7729e85ac524905188f24839b8d738097fadaf7f9f05" ||
		withUnique.ContentDigest !=
			"sha256:fe4dae42073d7f2da67c9b324bf7d871276b17ba0397ff2aead893f260a6cedd" ||
		withoutUnique.ContentDigest == withUnique.ContentDigest ||
		consumerv1.ValidateEffectiveMetadataComponent(withoutUnique) != nil ||
		consumerv1.ValidateEffectiveMetadataComponent(withUnique) != nil {
		t.Fatalf(
			"unique drift was not bound into valid deterministic digests: without=%q with=%q",
			withoutUnique.ContentDigest,
			withUnique.ContentDigest,
		)
	}
}

func TestBuildEffectiveObjectSearchComponentPreservesClassificationWithoutSuffix(t *testing.T) {
	component, err := buildObjectSearchForTest([]*models.ObjectMetadata{{
		APIName: "IdentitySyncSetting", Label: "Identity Sync Setting",
		TableType: constants.TableTypeCustomObject,
		Fields: []models.FieldMetadata{
			{APIName: "Id", Label: "ID", Type: constants.FieldTypeString, IsSystem: true},
			{APIName: "PolicyName", Label: "Policy Name", Type: constants.FieldTypeString},
		},
	}}, nil, consumerv1.AppMetadataV3CapabilityName)
	if err != nil {
		t.Fatal(err)
	}
	var document effectiveObjectSearchDocument
	if err := json.Unmarshal(component.CanonicalBehavior, &document); err != nil {
		t.Fatal(err)
	}
	if len(document.Objects) != 1 ||
		document.Objects[0].TableType != string(constants.TableTypeCustomObject) ||
		len(document.Objects[0].Fields) != 2 ||
		!document.Objects[0].Fields[0].IsSystem ||
		document.Objects[0].Fields[1].IsSystem {
		t.Fatalf("classification projection = %#v", document)
	}
}

func TestBuildEffectiveObjectSearchComponentFailsClosedOnAuthorityDrift(
	t *testing.T,
) {
	objects := []*models.ObjectMetadata{{
		APIName: "Account", Searchable: true,
		Fields: []models.FieldMetadata{
			{APIName: "Name", Type: constants.FieldTypeString},
			{
				APIName: "SystemStamp", Type: constants.FieldTypeString,
				IsSystem: true,
			},
			{
				APIName: "Computed", Type: constants.FieldTypeFormula,
			},
			{
				APIName: "ChildCount", Type: constants.FieldTypeRollupSummary,
			},
		},
	}}
	for _, test := range []struct {
		name         string
		declarations []searchAllDefaultFieldsDeclaration
	}{
		{
			name: "missing field",
			declarations: []searchAllDefaultFieldsDeclaration{{
				ObjectAPIName: "Account", Fields: []string{"Absent"},
			}},
		},
		{
			name: "missing object",
			declarations: []searchAllDefaultFieldsDeclaration{{
				ObjectAPIName: "Contact", Fields: []string{"Name"},
			}},
		},
		{
			name: "duplicate owner",
			declarations: []searchAllDefaultFieldsDeclaration{
				{ObjectAPIName: "Account", Fields: []string{"Name"}},
				{ObjectAPIName: "account", Fields: []string{"Name"}},
			},
		},
		{
			name: "system field",
			declarations: []searchAllDefaultFieldsDeclaration{{
				ObjectAPIName: "Account", Fields: []string{"SystemStamp"},
			}},
		},
		{
			name: "formula field",
			declarations: []searchAllDefaultFieldsDeclaration{{
				ObjectAPIName: "Account", Fields: []string{"Computed"},
			}},
		},
		{
			name: "rollup field",
			declarations: []searchAllDefaultFieldsDeclaration{{
				ObjectAPIName: "Account", Fields: []string{"ChildCount"},
			}},
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			if _, err := buildObjectSearchForTest(
				objects, test.declarations,
				consumerv1.AppMetadataV3CapabilityName,
			); err == nil {
				t.Fatal("authority drift was accepted")
			}
		})
	}
}

func TestBuildEffectiveObjectSearchComponentCanonicalizesDeclaredFieldName(
	t *testing.T,
) {
	component, err := buildObjectSearchForTest(
		[]*models.ObjectMetadata{{
			APIName: "Account", Searchable: true,
			Fields: []models.FieldMetadata{{
				APIName: "DisplayName", Type: constants.FieldTypeString,
			}},
		}},
		[]searchAllDefaultFieldsDeclaration{{
			ObjectAPIName: "account", Fields: []string{"displayname"},
		}},
		consumerv1.AppMetadataV3CapabilityName,
	)
	if err != nil {
		t.Fatal(err)
	}
	var document effectiveObjectSearchDocument
	if err := json.Unmarshal(component.CanonicalBehavior, &document); err != nil {
		t.Fatal(err)
	}
	if len(document.Objects) != 1 ||
		strings.Join(
			document.Objects[0].SearchAllDefaultFields,
			",",
		) != "DisplayName" {
		t.Fatalf("declared field was not schema-canonicalized: %#v", document)
	}
}

// Unit fixtures already provide their effective field flags. Production must
// instead obtain this map from the compiled EffectiveMetadata read API.
func fixtureCreateRequiredFields(objects []*models.ObjectMetadata) map[string][]string {
	result := make(map[string][]string, len(objects))
	for _, object := range objects {
		if object != nil {
			result[object.APIName] = schemautil.RequiredCreateInputFields(object)
		}
	}
	return result
}

func buildObjectSearchForTest(objects []*models.ObjectMetadata, declarations []searchAllDefaultFieldsDeclaration, capability string, roles ...effectiveCompoundMemberRole) (catalogapp.EffectiveMetadataComponent, error) {
	return buildEffectiveObjectSearchComponent(objects, declarations, capability, fixtureCreateRequiredFields(objects), roles...)
}

func buildObjectSearchForFormatForTest(objects []*models.ObjectMetadata, declarations []searchAllDefaultFieldsDeclaration, format uint32, roles ...effectiveCompoundMemberRole) (catalogapp.EffectiveMetadataComponent, error) {
	return buildEffectiveObjectSearchComponentForFormat(objects, declarations, format, fixtureCreateRequiredFields(objects), roles...)
}
