package tenantoverlay

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

	"github.com/nexuscrm/backend/internal/bootstrap"
	"github.com/nexuscrm/backend/internal/controlplane/packs"
	"github.com/nexuscrm/backend/internal/controlplane/packspec"
	domainschema "github.com/nexuscrm/backend/internal/domain/schema"
	"github.com/nexuscrm/backend/internal/features/metadata"
	"github.com/nexuscrm/backend/internal/infrastructure/persistence"
	"github.com/nexuscrm/shared/pkg/constants"
)

// TestTenantObjectsSpec_OrderTenantFields guards that the PingCAP-branded
// picklist values relocated out of standard_objects_spec.json per issue
// #1609 remain in tenant_objects_spec.json on the Order object.
func TestTenantObjectsSpec_OrderTenantFields(t *testing.T) {
	specs, err := testOverlay(t).parseTenantObjectsSpec()
	if err != nil {
		t.Fatalf("parseTenantObjectsSpec() error = %v", err)
	}

	order := findSpec(specs, "Order")
	if order == nil {
		t.Fatal("Order spec not found in tenant_objects_spec.json")
	}

	fields := indexFields(order.Fields)
	if got := fields["PingCAP_RevRec_Entity__c"]; got.Type != "picklist" {
		t.Fatalf("tenant Order.PingCAP_RevRec_Entity__c type = %q, want picklist", got.Type)
	}
	if got := fields["Product_Family__c"]; got.Type != "picklist" {
		t.Fatalf("tenant Order.Product_Family__c type = %q, want picklist", got.Type)
	}
	if got := fields["Applicable_Law__c"]; got.Type != "picklist" {
		t.Fatalf("tenant Order.Applicable_Law__c type = %q, want picklist", got.Type)
	}
	if got := fields["Owned_DM__c"]; got.Type != "reference" {
		t.Fatalf("tenant Order.Owned_DM__c type = %q, want reference", got.Type)
	}
	for _, apiName := range []string{
		"Payment_Term__c",
		"Payment_Type__c",
		"Tax__c",
		"Net_Fees__c",
		"ACV_Amount_do_not_use__c",
		"Overall_Start_Date__c",
		"Overall_End_Date__c",
		"Order_End_Date__c",
		"Partner_Name__c",
		"Order_is_Terminated__c",
		"Order_to_be_deleted__c",
		"Order_Expiration_Alert__c",
		"Contain_Earlier_Termination_Clause__c",
		"Is_Stripe__c",
		"OP_Paid_via_Marketplace__c",
		"Cash_Collected_exc_tax_Amount__c",
		"Cash_Collected_USD_exc_tax__c",
		"Payable_Amount_USD_exc_tax__c",
	} {
		if _, ok := fields[apiName]; !ok {
			t.Fatalf("%s missing from tenant Order spec", apiName)
		}
	}
}

func TestTenantObjectsSpec_ContractTenantFields(t *testing.T) {
	specs, err := testOverlay(t).parseTenantObjectsSpec()
	if err != nil {
		t.Fatalf("parseTenantObjectsSpec() error = %v", err)
	}

	contract := findSpec(specs, "Contract")
	if contract == nil {
		t.Fatal("Contract spec not found in tenant_objects_spec.json")
	}

	fields := indexFields(contract.Fields)
	if got := fields["PingCAP_Contracting_Entity__c"]; got.Type != "picklist" {
		t.Fatalf("tenant Contract.PingCAP_Contracting_Entity__c type = %q, want picklist", got.Type)
	}
	if got := fields["Applicable_Law__c"]; got.Type != "picklist" {
		t.Fatalf("tenant Contract.Applicable_Law__c type = %q, want picklist", got.Type)
	}
	if got := fields["Owned_DM__c"]; got.Type != "reference" {
		t.Fatalf("tenant Contract.Owned_DM__c type = %q, want reference", got.Type)
	}
}

func TestTenantObjectsSpec_QuoteProductFamily(t *testing.T) {
	specs, err := testOverlay(t).parseTenantObjectsSpec()
	if err != nil {
		t.Fatalf("parseTenantObjectsSpec() error = %v", err)
	}

	quote := findSpec(specs, "Quote")
	if quote == nil {
		t.Fatal("Quote spec not found in tenant_objects_spec.json")
	}

	fields := indexFields(quote.Fields)
	if got := fields["Product_Family__c"]; got.Type != "picklist" {
		t.Fatalf("tenant Quote.Product_Family__c type = %q, want picklist", got.Type)
	}
	if got := fields["Payment_Term__c"]; got.Type != "picklist" {
		t.Fatalf("tenant Quote.Payment_Term__c type = %q, want picklist", got.Type)
	}
	if got := fields["Payment_Type__c"]; got.Type != "picklist" {
		t.Fatalf("tenant Quote.Payment_Type__c type = %q, want picklist", got.Type)
	}
	if got := fields["Service_Start_Date__c"]; got.Type != "date" {
		t.Fatalf("tenant Quote.Service_Start_Date__c type = %q, want date", got.Type)
	}
	if got := fields["Service_End_Date__c"]; got.Type != "date" {
		t.Fatalf("tenant Quote.Service_End_Date__c type = %q, want date", got.Type)
	}
	if got := fields["Overall_Start_Date__c"]; got.Type != "date" {
		t.Fatalf("tenant Quote.Overall_Start_Date__c type = %q, want date", got.Type)
	}
	if got := fields["Overall_End_Date__c"]; got.Type != "date" {
		t.Fatalf("tenant Quote.Overall_End_Date__c type = %q, want date", got.Type)
	}
	for _, apiName := range []string{
		"Tax__c",
		"NetFees__c",
		"ACV_Amount_do_not_use__c",
		"Partner_Name__c",
	} {
		if _, ok := fields[apiName]; !ok {
			t.Fatalf("%s missing from tenant Quote spec", apiName)
		}
	}
}

func TestTenantObjectsSpec_OpportunityCreateQuoteSourceFields(t *testing.T) {
	specs, err := testOverlay(t).parseTenantObjectsSpec()
	if err != nil {
		t.Fatalf("parseTenantObjectsSpec() error = %v", err)
	}

	opportunity := findSpec(specs, "Opportunity")
	if opportunity == nil {
		t.Fatal("Opportunity spec not found in tenant_objects_spec.json")
	}

	fields := indexFields(opportunity.Fields)
	if got := fields["Product_Family__c"]; got.Type != "picklist" {
		t.Fatalf("tenant Opportunity.Product_Family__c type = %q, want picklist", got.Type)
	}
	if got := fields["Overall_Start_Date__c"]; got.Type != "date" {
		t.Fatalf("tenant Opportunity.Overall_Start_Date__c type = %q, want date", got.Type)
	}
	if _, ok := fields["NetFees__c"]; ok {
		t.Fatal("tenant Opportunity.NetFees__c must not be reintroduced into tenant_objects_spec.json")
	}
}

func TestTenantObjectsSpec_QuoteProductCustomObject(t *testing.T) {
	specs, err := testOverlay(t).parseTenantObjectsSpec()
	if err != nil {
		t.Fatalf("parseTenantObjectsSpec() error = %v", err)
	}

	quoteProduct := findSpec(specs, "quote_product__c")
	if quoteProduct == nil {
		t.Fatal("quote_product__c missing from tenant_objects_spec.json")
	}
	if quoteProduct.TableType != string(constants.TableTypeCustomObject) {
		t.Fatalf("quote_product__c tableType = %q, want custom_object", quoteProduct.TableType)
	}
	if quoteProduct.SharingModel != string(constants.SharingModelControlledByParent) {
		t.Fatalf("quote_product__c sharingModel = %q, want ControlledByParent", quoteProduct.SharingModel)
	}

	fields := indexFields(quoteProduct.Fields)
	for _, apiName := range []string{"Name", "Quote__c", "Product__c", "Quantity__c", "UnitPrice__c", "TotalPrice__c"} {
		if _, ok := fields[apiName]; !ok {
			t.Fatalf("quote_product__c.%s missing from tenant spec", apiName)
		}
	}
	quoteRef := fields["Quote__c"]
	if quoteRef.Type != "reference" || len(quoteRef.ReferenceTo) != 1 || quoteRef.ReferenceTo[0] != "Quote" {
		t.Fatalf("quote_product__c.Quote__c = {Type:%q ReferenceTo:%v}, want Quote reference", quoteRef.Type, quoteRef.ReferenceTo)
	}

	if len(quoteProduct.ManagedRelationships) != 1 {
		t.Fatalf("quote_product__c managedRelationships count = %d, want 1", len(quoteProduct.ManagedRelationships))
	}
	rel := quoteProduct.ManagedRelationships[0]
	if rel.FieldAPIName != "Quote__c" || rel.ParentObject != "Quote" {
		t.Fatalf("quote_product__c relationship = {%s %s}, want Quote__c -> Quote", rel.FieldAPIName, rel.ParentObject)
	}
	if rel.IsSharingParent == nil || !*rel.IsSharingParent {
		t.Fatalf("quote_product__c relationship IsSharingParent = %v, want true", rel.IsSharingParent)
	}
	if rel.CascadeDelete == nil || *rel.CascadeDelete {
		t.Fatalf("quote_product__c relationship CascadeDelete = %v, want false", rel.CascadeDelete)
	}
}

func TestTenantQuoteProductBuildersUseCustomObjectMetadata(t *testing.T) {
	specs, err := testOverlay(t).parseTenantObjectsSpec()
	if err != nil {
		t.Fatalf("parseTenantObjectsSpec() error = %v", err)
	}
	quoteProduct := findSpec(specs, "quote_product__c")
	if quoteProduct == nil {
		t.Fatal("quote_product__c missing from tenant_objects_spec.json")
	}

	schemaMgr := metadata.NewSchemaManager(persistence.NewSchemaRepository(nil, nil))
	defs := buildTenantColumnDefs([]packs.ObjectSpec{*quoteProduct}, schemaMgr)
	if len(defs) != 1 {
		t.Fatalf("buildTenantColumnDefs returned %d defs, want 1", len(defs))
	}
	def := defs[0]
	if def.TableType != string(constants.TableTypeCustomObject) {
		t.Fatalf("quote_product__c def TableType = %q, want custom_object", def.TableType)
	}
	cols := indexColumns(def.Columns)
	for _, apiName := range []string{constants.FieldID, constants.FieldOwnerID, "Name", "Quote__c"} {
		if _, ok := cols[apiName]; !ok {
			t.Fatalf("quote_product__c column %s missing from generated def", apiName)
		}
	}
	if quoteCol := cols["Quote__c"]; len(quoteCol.ReferenceTo) != 1 || quoteCol.ReferenceTo[0] != "Quote" || quoteCol.RelationshipName != "Quote__r" {
		t.Fatalf("Quote__c column = {ReferenceTo:%v RelationshipName:%q}, want Quote / Quote__r", quoteCol.ReferenceTo, quoteCol.RelationshipName)
	}

	objects := buildTenantObjectMetadata([]packs.ObjectSpec{*quoteProduct})
	if len(objects) != 1 {
		t.Fatalf("buildTenantObjectMetadata returned %d objects, want 1", len(objects))
	}
	obj := objects[0]
	if !obj.IsCustom || obj.TableType != constants.TableTypeCustomObject {
		t.Fatalf("quote_product__c metadata custom shape = {IsCustom:%v TableType:%q}, want custom_object", obj.IsCustom, obj.TableType)
	}
	if obj.SharingModel != "ControlledByParent" {
		t.Fatalf("quote_product__c metadata SharingModel = %q, want ControlledByParent", obj.SharingModel)
	}
	if len(obj.ListFields) == 0 || obj.ListFields[0] != "Name" {
		t.Fatalf("quote_product__c metadata ListFields = %v, want tenant-authored list fields", obj.ListFields)
	}

	rels := tenantManagedRelationshipSemantics([]packs.ObjectSpec{*quoteProduct})
	if len(rels) != 1 {
		t.Fatalf("tenantManagedRelationshipSemantics returned %d rels, want 1", len(rels))
	}
	if rels[0].ChildObjectAPIName != "quote_product__c" || rels[0].ParentObjectAPIName != "Quote" || rels[0].FieldAPIName != "Quote__c" {
		t.Fatalf("relationship semantics = %+v, want quote_product__c -> Quote via Quote__c", rels[0])
	}
	if rels[0].IsSharingParent == nil || !*rels[0].IsSharingParent {
		t.Fatalf("relationship semantics IsSharingParent = %v, want true", rels[0].IsSharingParent)
	}
	if rels[0].CascadeDelete == nil || *rels[0].CascadeDelete {
		t.Fatalf("relationship semantics CascadeDelete = %v, want false", rels[0].CascadeDelete)
	}
}

func TestTenantObjectsSpec_TaskSubjectPicklist(t *testing.T) {
	specs, err := testOverlay(t).parseTenantObjectsSpec()
	if err != nil {
		t.Fatalf("parseTenantObjectsSpec() error = %v", err)
	}

	task := findSpec(specs, "Task")
	if task == nil {
		t.Fatal("Task spec not found in tenant_objects_spec.json")
	}

	fields := indexFields(task.Fields)
	subject, ok := fields["Subject"]
	if !ok {
		t.Fatal("Subject field missing from tenant Task spec")
	}
	if subject.Type != "picklist" {
		t.Fatalf("tenant Task.Subject type = %q, want picklist", subject.Type)
	}
	wantOptions := map[string]bool{
		"Initial Engagement":   true,
		"Technical Engagement": true,
		"Business Engagement":  true,
		"Mixed Engagement":     true,
		"IQM Scheduled":        true,
		"IQM Completed":        true,
	}
	for _, opt := range subject.Options {
		delete(wantOptions, opt.Value)
	}
	if len(wantOptions) > 0 {
		t.Fatalf("tenant Task.Subject missing options: %v", wantOptions)
	}
}

// TestRecordTypeCodeSeeds_MatchesExpectedSet validates that the 10
// PingCAP-branded Contract RecordTypes each have a short code that
// Contract.Name's compound autonumber format can dereference via
// {RecordType.RecordTypeCode}.
func TestRecordTypeCodeSeeds_MatchesExpectedSet(t *testing.T) {
	want := map[string]string{
		"TiDB Cloud Services Agreement":          "CSA",
		"TiDB Cloud Services Agreement Online":   "TCSA",
		"Customer Reference Agreement":           "CRA",
		"PingCAP Non-disclosure Agreement":       "NDA",
		"PingCAP Partner Network Agreement":      "PNA",
		"PingCAP Reseller Distribution Addendum": "RDA",
		"Payment Agreement":                      "PA",
		"TiDB Cloud PoC Agreement":               "TCPA",
		"TiDB Enterprise PoC Agreement":          "TEPA",
		"PingCAP Master Agreement":               "MA",
	}
	seeds := readAutoNumberOverlayForTest(t).RecordTypeCodeSeeds
	got := make(map[string]string, len(seeds))
	for _, s := range seeds {
		got[s.Name] = s.Code
	}
	for name, code := range want {
		if got[name] != code {
			t.Errorf("tenant RecordType %q code = %q, want %q", name, got[name], code)
		}
	}
	if len(got) != len(want) {
		t.Errorf("tenant RT seed count = %d, want %d (got: %v)", len(got), len(want), got)
	}
}

// TestAutoNumberFormatSeeds_NoLeakIntoPlatformConfig guards the invariant
// that tenant autonumber seeds live ONLY in this package — none of them
// may leak into packs.AutoNumberFormatSeeds (issue #1609).
func TestAutoNumberFormatSeeds_NoLeakIntoPlatformConfig(t *testing.T) {
	tenantKeys := map[string]struct{}{}
	for _, s := range readAutoNumberOverlayForTest(t).AutoNumberFormatSeeds {
		tenantKeys[s.ObjectName+"."+s.FieldApiName] = struct{}{}
	}
	for _, s := range packs.AutoNumberFormatSeeds {
		key := s.ObjectName + "." + s.FieldApiName
		if _, isTenant := tenantKeys[key]; isTenant {
			t.Errorf("tenant autonumber seed %q leaked into packs.AutoNumberFormatSeeds (issue #1609)", key)
		}
	}
}

// TestRecordTypesJSON_ContractTypes validates the 10 Contract RTs carried
// by record_types.json in this package.
func TestRecordTypesJSON_ContractTypes(t *testing.T) {
	var data struct {
		RecordTypes []struct {
			ObjectApiName string `json:"objectApiName"`
			Types         []struct {
				Name string `json:"name"`
			} `json:"types"`
		} `json:"recordTypes"`
	}
	if err := json.Unmarshal(readTestOverlayFile(t, RecordTypesFile), &data); err != nil {
		t.Fatalf("parse record_types.json: %v", err)
	}

	var contract *struct {
		ObjectApiName string `json:"objectApiName"`
		Types         []struct {
			Name string `json:"name"`
		} `json:"types"`
	}
	for i := range data.RecordTypes {
		if data.RecordTypes[i].ObjectApiName == "Contract" {
			contract = &data.RecordTypes[i]
			break
		}
	}
	if contract == nil {
		t.Fatal("Contract object missing from record_types.json")
	}

	wantNames := []string{
		"PingCAP Master Agreement",
		"Customer Reference Agreement",
		"Payment Agreement",
		"PingCAP Non-disclosure Agreement",
		"PingCAP Partner Network Agreement",
		"PingCAP Reseller Distribution Addendum",
		"TiDB Cloud PoC Agreement",
		"TiDB Cloud Services Agreement",
		"TiDB Cloud Services Agreement Online",
		"TiDB Enterprise PoC Agreement",
	}
	got := make(map[string]bool, len(contract.Types))
	for _, rt := range contract.Types {
		got[rt.Name] = true
	}
	for _, name := range wantNames {
		if !got[name] {
			t.Errorf("tenant Contract RT %q missing from record_types.json", name)
		}
	}
	if len(contract.Types) != len(wantNames) {
		t.Errorf("tenant Contract RT count = %d, want %d", len(contract.Types), len(wantNames))
	}
}

// TestActionsJSON_FullActionsAndOverlays validates the two classes of
// tenant action content: fullActions (new rows) and overlays (patches to
// existing standard rows).
func TestActionsJSON_FullActionsAndOverlays(t *testing.T) {
	var tenant tenantActionsData
	if err := json.Unmarshal(readTestOverlayFile(t, ActionsFile), &tenant); err != nil {
		t.Fatalf("parse actions.json: %v", err)
	}

	wantFull := map[string]string{
		"EditProducts":           "Quote",
		"UploadArchivedOF":       "Order",
		"UploadArchivedContract": "Contract",
		// ApplyForPoC targets the tenant-owned PoC__c object and was
		// relocated here after PR #1794 stripped the ApplyForPoC entry
		// from the platform-generic standard_actions.json. See issue
		// #1609 follow-up: the headerActions reference in the curated
		// Opportunity layout is re-added at apply-tenant time via the
		// tenant layout overlay's headerActionsAppend block.
		"ApplyForPoC": "Opportunity",
	}
	gotFull := make(map[string]string, len(tenant.FullActions))
	for _, a := range tenant.FullActions {
		gotFull[a.Name] = a.ObjectAPIName
	}
	for name, obj := range wantFull {
		if gotFull[name] != obj {
			t.Errorf("tenant fullAction %q objectApiName = %q, want %q", name, gotFull[name], obj)
		}
	}
	if len(gotFull) != len(wantFull) {
		t.Errorf("tenant fullActions count = %d, want %d (got keys: %v)",
			len(gotFull), len(wantFull), gotFull)
	}

	wantOverlay := map[string]string{
		"CreateQuote":    "Opportunity",
		"CreateContract": "Account",
		"CreateOrder":    "Quote",
	}
	gotOverlay := make(map[string]string, len(tenant.Overlays))
	for _, o := range tenant.Overlays {
		gotOverlay[o.Name] = o.ObjectAPIName
	}
	for name, obj := range wantOverlay {
		if gotOverlay[name] != obj {
			t.Errorf("tenant overlay %q objectApiName = %q, want %q", name, gotOverlay[name], obj)
		}
	}
	if len(gotOverlay) != len(wantOverlay) {
		t.Errorf("tenant overlays count = %d, want %d", len(gotOverlay), len(wantOverlay))
	}

	var ocrRoleDefault *tenantStandardActionFieldDefault
	for i := range tenant.StandardActionFieldDefaults {
		row := tenant.StandardActionFieldDefaults[i]
		if row.StandardActionName == "convertLead" &&
			row.TargetObject == "OpportunityContactRole" &&
			row.FieldAPIName == "Role" {
			ocrRoleDefault = &row
			break
		}
	}
	if ocrRoleDefault == nil {
		t.Fatal("tenant convertLead OpportunityContactRole.Role ActionFieldDefault missing from actions.json")
	}
	if ocrRoleDefault.ValueFormula != "'Decision_Maker'" {
		t.Fatalf("tenant OCR Role default valueFormula = %q, want quoted Decision_Maker", ocrRoleDefault.ValueFormula)
	}
}

// TestQuoteInsertInheritanceSeed_DropsUnsupportedOpportunityFields locks the
// CI fix for PR #1853: the tenant insertInheritFromLookup seed must not
// reference Opportunity source fields that current shipped metadata does not
// declare. Those legacy entries now hard-fail apply-tenant under the new
// validator, so reintroducing them would break stable-scenes CI again.
func TestQuoteInsertInheritanceSeed_DropsUnsupportedOpportunityFields(t *testing.T) {
	var groups []bootstrap.FieldMappingGroup
	if err := json.Unmarshal(readTestOverlayFile(t, ObjectFieldMappingsFile), &groups); err != nil {
		t.Fatalf("parse object_field_mappings.json: %v", err)
	}

	disallowed := map[string]bool{
		"Discount":              true,
		"Service_Start_Date__c": true,
		"Service_End_Date__c":   true,
		"Overall_End_Date__c":   true,
		"NetFees__c":            true,
	}
	for _, g := range groups {
		if g.SourceObject != "Opportunity" || g.TargetObject != "Quote" || g.ActionType != "insertInheritFromLookup" {
			continue
		}
		for _, m := range g.Mappings {
			if disallowed[m.SourceField] {
				t.Fatalf("unsupported Opportunity→Quote sourceField %q reintroduced into object_field_mappings.json", m.SourceField)
			}
		}
	}
}

// TestCreateQuoteOverlay_DropsUnsupportedOpportunityFields keeps the tenant
// CreateQuote overlay aligned with the insertInheritFromLookup seed cleanup:
// these legacy Opportunity source fields were proven absent in current
// metadata by stable-scenes CI, so the overlay must not carry them either.
func TestCreateQuoteOverlay_DropsUnsupportedOpportunityFields(t *testing.T) {
	var tenant tenantActionsData
	if err := json.Unmarshal(readTestOverlayFile(t, ActionsFile), &tenant); err != nil {
		t.Fatalf("parse actions.json: %v", err)
	}

	var createQuote *actionOverlay
	for i := range tenant.Overlays {
		if tenant.Overlays[i].Name == "CreateQuote" && strings.EqualFold(tenant.Overlays[i].ObjectAPIName, "Opportunity") {
			createQuote = &tenant.Overlays[i]
			break
		}
	}
	if createQuote == nil {
		t.Fatal("CreateQuote overlay missing from tenant actions.json")
	}

	workflow, ok := createQuote.ConfigOverlay["workflow"].(map[string]interface{})
	if !ok {
		t.Fatal("CreateQuote overlay missing workflow block")
	}
	mappings, ok := workflow["fieldMappings"].(map[string]interface{})
	if !ok {
		t.Fatal("CreateQuote overlay missing workflow.fieldMappings")
	}
	for _, bad := range []string{"Service_Start_Date__c", "Service_End_Date__c", "Overall_End_Date__c", "NetFees__c"} {
		if _, exists := mappings[bad]; exists {
			t.Fatalf("unsupported CreateQuote overlay mapping %q still present", bad)
		}
	}
}

// TestTenantFieldManifestRegistered verifies explicit overlay registration
// populates the packspec manifest from the selected overlay directory. There
// must be no init-time tenant package side effect.
func TestTenantFieldManifestRegisteredExplicitly(t *testing.T) {
	if err := testOverlay(t).RegisterFieldManifest(); err != nil {
		t.Fatalf("RegisterFieldManifest: %v", err)
	}
	manifest := packspec.TenantFieldManifestJSON()
	if len(manifest) == 0 {
		t.Fatal("packspec.TenantFieldManifestJSON() returned empty")
	}
	var entries []struct {
		APIName string `json:"apiName"`
		Fields  []struct {
			APIName string `json:"apiName"`
		} `json:"fields"`
	}
	if err := json.Unmarshal(manifest, &entries); err != nil {
		t.Fatalf("manifest is not valid JSON: %v", err)
	}
	if len(entries) == 0 {
		t.Fatal("manifest has zero objects")
	}
}

func readAutoNumberOverlayForTest(t *testing.T) autoNumberOverlay {
	t.Helper()
	var overlay autoNumberOverlay
	if err := json.Unmarshal(readTestOverlayFile(t, AutoNumberFormatsFile), &overlay); err != nil {
		t.Fatalf("parse autonumber formats: %v", err)
	}
	return overlay
}

func findSpec(specs []packs.ObjectSpec, apiName string) *packs.ObjectSpec {
	for i := range specs {
		if specs[i].APIName == apiName {
			return &specs[i]
		}
	}
	return nil
}

func indexFields(fields []packs.FieldSpec) map[string]packs.FieldSpec {
	m := make(map[string]packs.FieldSpec, len(fields))
	for _, f := range fields {
		m[f.APIName] = f
	}
	return m
}

func indexColumns(cols []domainschema.ColumnDefinition) map[string]domainschema.ColumnDefinition {
	m := make(map[string]domainschema.ColumnDefinition, len(cols))
	for _, c := range cols {
		m[c.Name] = c
	}
	return m
}
