package packs

import (
	"strings"
	"testing"

	"github.com/nexuscrm/shared/pkg/models"
)

// TestValidateObjectSpecFieldShapes_ReferenceShape covers R1–R11 from the issue
// #489 acceptance matrix: nil, empty, all-blank, mixed-blank, padded, and
// golden ReferenceTo slices.
func TestValidateObjectSpecFieldShapes_ReferenceShape(t *testing.T) {
	cases := []struct {
		name        string
		referenceTo []string
		wantCode    string // empty string means expect no diagnostic
	}{
		// R1 — nil slice
		{name: "R1_nil", referenceTo: nil, wantCode: "field.reference.no_target"},
		// R2 — empty slice
		{name: "R2_empty", referenceTo: []string{}, wantCode: "field.reference.no_target"},
		// R3 — single blank entry
		{name: "R3_single_blank", referenceTo: []string{""}, wantCode: "field.reference.no_target"},
		// R4 — multiple blank entries
		{name: "R4_multi_blank", referenceTo: []string{"", "  "}, wantCode: "field.reference.no_target"},
		// R5 — mixed: canonical + blank
		{name: "R5_mixed_blank", referenceTo: []string{"Account", ""}, wantCode: "field.reference.blank_target"},
		// R6 — mixed: canonical + blank (trailing)
		{name: "R6_trailing_blank", referenceTo: []string{"Account", "Contact", ""}, wantCode: "field.reference.blank_target"},
		// R7 — single padded entry
		{name: "R7_single_padded", referenceTo: []string{" Account "}, wantCode: "field.reference.padded_target"},
		// R8 — mixed: canonical + padded
		{name: "R8_mixed_padded", referenceTo: []string{"Account", " Contact "}, wantCode: "field.reference.padded_target"},
		// R9 — golden: single canonical target
		{name: "R9_golden_single", referenceTo: []string{"Account"}, wantCode: ""},
		// R10 — golden: polymorphic (two canonical targets)
		{name: "R10_golden_polymorphic", referenceTo: []string{"Account", "Contact"}, wantCode: ""},
		// R11 — blank wins over padded: canonical + blank (not padded_target)
		{name: "R11_blank_wins_over_padded", referenceTo: []string{"Account", " "}, wantCode: "field.reference.blank_target"},
		// R12 (optional) — trailing-only padding
		{name: "R12_trailing_padding", referenceTo: []string{"Account "}, wantCode: "field.reference.padded_target"},
		// R13 (optional) — leading-only padding
		{name: "R13_leading_padding", referenceTo: []string{" Account"}, wantCode: "field.reference.padded_target"},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			specs := []ObjectSpec{{
				APIName: "TestObj",
				Fields: []FieldSpec{{
					APIName:     "Ref",
					Type:        "reference",
					ReferenceTo: tc.referenceTo,
				}},
			}}
			diags := validateObjectSpecFieldShapes(specs)
			if tc.wantCode == "" {
				if len(diags) != 0 {
					t.Errorf("want no diagnostics, got %d: %v", len(diags), diagCodes(diags))
				}
				return
			}
			if !hasDiagCode(diags, tc.wantCode) {
				t.Errorf("want diagnostic %q, got %v", tc.wantCode, diagCodes(diags))
			}
			// Exactly one diagnostic expected for a single-field spec.
			if len(diags) != 1 {
				t.Errorf("want exactly 1 diagnostic, got %d: %v", len(diags), diagCodes(diags))
			}
		})
	}
}

func TestValidateObjectSpecFieldShapes_WritableReferenceTo(t *testing.T) {
	cases := []struct {
		name                string
		fieldType           string
		referenceTo         []string
		writableReferenceTo []string
		wantCode            string
	}{
		{
			name:                "valid_subset",
			fieldType:           "reference",
			referenceTo:         []string{"User", "Group"},
			writableReferenceTo: []string{"User"},
		},
		{
			name:                "target_not_declared",
			fieldType:           "reference",
			referenceTo:         []string{"User"},
			writableReferenceTo: []string{"Group"},
			wantCode:            "field.reference.writable_target_not_declared",
		},
		{
			name:                "blank_target",
			fieldType:           "reference",
			referenceTo:         []string{"User"},
			writableReferenceTo: []string{""},
			wantCode:            "field.reference.no_target",
		},
		{
			name:                "padded_target",
			fieldType:           "reference",
			referenceTo:         []string{"User"},
			writableReferenceTo: []string{" User "},
			wantCode:            "field.reference.padded_target",
		},
		{
			name:                "non_reference_field",
			fieldType:           "text",
			writableReferenceTo: []string{"User"},
			wantCode:            "field.reference.writable_target_on_non_reference",
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			specs := []ObjectSpec{{
				APIName: "TestObj",
				Fields: []FieldSpec{{
					APIName:             "Ref",
					Type:                tc.fieldType,
					ReferenceTo:         tc.referenceTo,
					WritableReferenceTo: tc.writableReferenceTo,
				}},
			}}
			diags := validateObjectSpecFieldShapes(specs)
			if tc.wantCode == "" {
				if len(diags) != 0 {
					t.Errorf("want no diagnostics, got %d: %v", len(diags), diagCodes(diags))
				}
				return
			}
			if !hasDiagCode(diags, tc.wantCode) {
				t.Errorf("want diagnostic %q, got %v", tc.wantCode, diagCodes(diags))
			}
		})
	}
}

// TestValidateObjectSpecFieldShapes_FormulaReturnType covers F1–F3: blank,
// padded, and canonical formula returnType values.
func TestValidateObjectSpecFieldShapes_FormulaReturnType(t *testing.T) {
	cases := []struct {
		name       string
		returnType string
		wantCode   string
	}{
		// F1 — blank returnType (empty string)
		{name: "F1_blank", returnType: "", wantCode: "field.formula.no_return_type"},
		// F2 — whitespace-only returnType
		{name: "F2_whitespace_only", returnType: "   ", wantCode: "field.formula.no_return_type"},
		// F3 — padded returnType (leading+trailing space)
		{name: "F3_padded", returnType: " string ", wantCode: "field.formula.padded_return_type"},
		// F4 — leading-only padding
		{name: "F4_leading_padded", returnType: " number", wantCode: "field.formula.padded_return_type"},
		// F5 — trailing-only padding
		{name: "F5_trailing_padded", returnType: "boolean ", wantCode: "field.formula.padded_return_type"},
		// F6 — golden: canonical returnType
		{name: "F6_golden_string", returnType: "string", wantCode: ""},
		// F7 — golden: canonical number
		{name: "F7_golden_number", returnType: "number", wantCode: ""},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			specs := []ObjectSpec{{
				APIName: "TestObj",
				Fields: []FieldSpec{{
					APIName:    "Calc",
					Type:       "formula",
					ReturnType: tc.returnType,
				}},
			}}
			diags := validateObjectSpecFieldShapes(specs)
			if tc.wantCode == "" {
				if len(diags) != 0 {
					t.Errorf("want no diagnostics, got %d: %v", len(diags), diagCodes(diags))
				}
				return
			}
			if !hasDiagCode(diags, tc.wantCode) {
				t.Errorf("want diagnostic %q, got %v", tc.wantCode, diagCodes(diags))
			}
			if len(diags) != 1 {
				t.Errorf("want exactly 1 diagnostic, got %d: %v", len(diags), diagCodes(diags))
			}
		})
	}
}

// TestValidateObjectSpecFieldShapes_PicklistOptionValues covers O1–O5 from the
// issue #489 acceptance matrix: blank and padded option values for picklist
// and multipicklist fields.
func TestValidateObjectSpecFieldShapes_PicklistOptionValues(t *testing.T) {
	cases := []struct {
		name      string
		fieldType string
		options   models.PicklistOptions
		wantCode  string
	}{
		// O1 — picklist with one blank value
		{
			name:      "O1_picklist_blank_value",
			fieldType: "picklist",
			options:   models.PicklistOptions{{Value: "hot"}, {Value: ""}},
			wantCode:  "field.picklist.blank_option_value",
		},
		// O2 — picklist with padded value
		{
			name:      "O2_picklist_padded_value",
			fieldType: "picklist",
			options:   models.PicklistOptions{{Value: " hot "}},
			wantCode:  "field.picklist.blank_option_value",
		},
		// O3 — picklist golden
		{
			name:      "O3_picklist_golden",
			fieldType: "picklist",
			options:   models.PicklistOptions{{Value: "a"}, {Value: "b"}},
			wantCode:  "",
		},
		// O4 — multipicklist with blank value
		{
			name:      "O4_multipicklist_blank_value",
			fieldType: "multipicklist",
			options:   models.PicklistOptions{{Value: ""}},
			wantCode:  "field.multipicklist.blank_option_value",
		},
		// O5 — multipicklist golden
		{
			name:      "O5_multipicklist_golden",
			fieldType: "multipicklist",
			options:   models.PicklistOptions{{Value: "a"}},
			wantCode:  "",
		},
		// O6 (optional) — multipicklist padded value
		{
			name:      "O6_multipicklist_padded_value",
			fieldType: "multipicklist",
			options:   models.PicklistOptions{{Value: " tag "}},
			wantCode:  "field.multipicklist.blank_option_value",
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			specs := []ObjectSpec{{
				APIName: "TestObj",
				Fields: []FieldSpec{{
					APIName: "Status",
					Type:    tc.fieldType,
					Options: tc.options,
				}},
			}}
			diags := validateObjectSpecFieldShapes(specs)
			if tc.wantCode == "" {
				if len(diags) != 0 {
					t.Errorf("want no diagnostics, got %d: %v", len(diags), diagCodes(diags))
				}
				return
			}
			if !hasDiagCode(diags, tc.wantCode) {
				t.Errorf("want diagnostic %q, got %v", tc.wantCode, diagCodes(diags))
			}
			if len(diags) != 1 {
				t.Errorf("want exactly 1 diagnostic, got %d: %v", len(diags), diagCodes(diags))
			}
		})
	}
}

// TestValidateObjectSpecFieldShapes_MasterDetailReferenceShape verifies that
// masterdetail fields are subject to the same referenceTo canonicalization as
// reference fields. IsReferenceType covers both types; this test ensures the
// switch branch is not accidentally limited to "reference" alone.
func TestValidateObjectSpecFieldShapes_MasterDetailReferenceShape(t *testing.T) {
	cases := []struct {
		name        string
		referenceTo []string
		wantCode    string
	}{
		// MD1 — nil referenceTo
		{name: "MD1_nil", referenceTo: nil, wantCode: "field.reference.no_target"},
		// MD2 — empty referenceTo slice
		{name: "MD2_empty", referenceTo: []string{}, wantCode: "field.reference.no_target"},
		// MD3 — single blank entry
		{name: "MD3_single_blank", referenceTo: []string{""}, wantCode: "field.reference.no_target"},
		// MD4 — padded target entry
		{name: "MD4_padded", referenceTo: []string{" Account "}, wantCode: "field.reference.padded_target"},
		// MD5 — golden: single canonical parent
		{name: "MD5_golden", referenceTo: []string{"Account"}, wantCode: ""},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			specs := []ObjectSpec{{
				APIName: "TestObj",
				Fields: []FieldSpec{{
					APIName:     "ParentId",
					Type:        "masterdetail",
					ReferenceTo: tc.referenceTo,
				}},
			}}
			diags := validateObjectSpecFieldShapes(specs)
			if tc.wantCode == "" {
				if len(diags) != 0 {
					t.Errorf("want no diagnostics, got %d: %v", len(diags), diagCodes(diags))
				}
				return
			}
			if !hasDiagCode(diags, tc.wantCode) {
				t.Errorf("want diagnostic %q, got %v", tc.wantCode, diagCodes(diags))
			}
			if len(diags) != 1 {
				t.Errorf("want exactly 1 diagnostic, got %d: %v", len(diags), diagCodes(diags))
			}
		})
	}
}

// TestValidateObjectSpecFieldShapes_Aggregation covers A1–A2: the validator
// must aggregate diagnostics across fields (not fail-fast after the first bad
// field).
func TestValidateObjectSpecFieldShapes_Aggregation(t *testing.T) {
	t.Run("A1_three_fields_three_diagnostics", func(t *testing.T) {
		// reference["Account",""] + picklist[{Value:""}] + formula{returnType:""}
		// → three distinct diagnostics in one call
		specs := []ObjectSpec{{
			APIName: "TestObj",
			Fields: []FieldSpec{
				{APIName: "LookupId", Type: "reference", ReferenceTo: []string{"Account", ""}},
				{APIName: "Status", Type: "picklist", Options: models.PicklistOptions{{Value: ""}}},
				{APIName: "Calc", Type: "formula", ReturnType: ""},
			},
		}}
		diags := validateObjectSpecFieldShapes(specs)
		if len(diags) != 3 {
			t.Errorf("want 3 diagnostics (one per bad field), got %d: %v", len(diags), diagCodes(diags))
		}
		for _, code := range []string{
			"field.reference.blank_target",
			"field.picklist.blank_option_value",
			"field.formula.no_return_type",
		} {
			if !hasDiagCode(diags, code) {
				t.Errorf("missing diagnostic %q in %v", code, diagCodes(diags))
			}
		}
	})

	t.Run("A2_two_reference_fields_two_diagnostics", func(t *testing.T) {
		// Two reference fields, both with mixed-blank slices → two diagnostics
		specs := []ObjectSpec{{
			APIName: "TestObj",
			Fields: []FieldSpec{
				{APIName: "PrimaryId", Type: "reference", ReferenceTo: []string{"Account", ""}},
				{APIName: "SecondaryId", Type: "reference", ReferenceTo: []string{"Contact", ""}},
			},
		}}
		diags := validateObjectSpecFieldShapes(specs)
		if len(diags) != 2 {
			t.Errorf("want 2 diagnostics (one per field), got %d: %v", len(diags), diagCodes(diags))
		}
		for _, d := range diags {
			if d.Code != "field.reference.blank_target" {
				t.Errorf("unexpected diagnostic code %q; want field.reference.blank_target", d.Code)
			}
		}
	})
}

// TestValidateObjectSpecFieldShapes_FormulaExpressionOnNonFormulaType covers
// F8–F17: formula expression on non-formula field types must emit a diagnostic
// when no compute path is wired (FormulaMode blank or "frozen"), but MUST NOT
// fire when FormulaMode=="active" — that shape is the supported view-compute
// substrate (formula_view_service.go:1080-1083; live in pr3_substrate
// _guardrails_test.go:364-375 as CampaignMember.Title and friends).
func TestValidateObjectSpecFieldShapes_FormulaExpressionOnNonFormulaType(t *testing.T) {
	cases := []struct {
		name        string
		fieldType   string
		formula     string
		formulaMode string
		returnType  string
		wantCode    string
	}{
		// F8 — string field with formula expression, blank formulaMode (dead column)
		{name: "F8_string_with_formula", fieldType: "string", formula: "Amount * 2", wantCode: "field.formula.expression_on_non_formula_type"},
		// F9 — number field with formula expression, blank formulaMode (dead column)
		{name: "F9_number_with_formula", fieldType: "number", formula: "1 + 1", wantCode: "field.formula.expression_on_non_formula_type"},
		// F10 — date field with formula expression, blank formulaMode (dead column)
		{name: "F10_date_with_formula", fieldType: "date", formula: "TODAY()", wantCode: "field.formula.expression_on_non_formula_type"},
		// F11 — formula field with expression, returnType, and explicit
		// formulaMode (golden — no diagnostic). The explicit mode is required
		// by the Codex round-9 gate; blank mode + non-empty formula on a
		// formula-typed field is now REJECTED (see
		// TestValidateObjectSpecFieldShapes_FormulaTypeBlankModeRequiresExplicitMode).
		{name: "F11_formula_golden", fieldType: "formula", formula: "Amount * 2", formulaMode: "active", returnType: "number", wantCode: ""},
		// F12 — string field with blank formula (golden — no diagnostic)
		{name: "F12_string_blank_formula", fieldType: "string", formula: "", wantCode: ""},
		// F13 — string field with whitespace-only formula (golden — no diagnostic)
		{name: "F13_string_whitespace_formula", fieldType: "string", formula: "   ", wantCode: ""},
		// F14 — string field with formula and FormulaMode=active: ALLOWED
		// (this is the live CampaignMember.Title / OpportunityLineItem
		// .TotalPrice substrate — FormulaViewService projects scalar+active
		// fields via v_<obj>).
		{name: "F14_string_with_formula_active_mode", fieldType: "string", formula: "Amount * 2", formulaMode: "active", returnType: "string", wantCode: ""},
		// F15 — string field with formula and FormulaMode=frozen: REJECTED
		// (frozen save-time compute in formula_compute.go:162,282 requires
		// Type==formula; without it the column never receives a value).
		{name: "F15_string_with_formula_frozen_mode", fieldType: "string", formula: "Amount * 2", formulaMode: "frozen", wantCode: "field.formula.expression_on_non_formula_type"},
		// F16 — currency field with formula and FormulaMode=active: ALLOWED
		// (mirrors OpportunityLineItem.TotalPrice).
		{name: "F16_currency_with_formula_active_mode", fieldType: "currency", formula: "Quantity * UnitPrice", formulaMode: "active", returnType: "currency", wantCode: ""},
		// F17 — uppercase "ACTIVE" must be REJECTED via the new
		// unknown_formula_mode diagnostic. Downstream pack-install readers
		// (source_pack_ops.go:1186, schema_pack_owned_field.go:597) match the
		// canonical lowercase form exactly; a case-folded acceptance here
		// would let the pack install with no backing storage or derived view
		// (Codex round-3 BLOCK on #964).
		{name: "F17_string_with_formula_ACTIVE_uppercase_rejected", fieldType: "string", formula: "Amount * 2", formulaMode: "ACTIVE", returnType: "string", wantCode: "field.formula.unknown_formula_mode"},
		// F18 — mixed-case "Active" rejected for the same reason.
		{name: "F18_string_with_formula_Active_titlecase_rejected", fieldType: "string", formula: "Amount * 2", formulaMode: "Active", returnType: "string", wantCode: "field.formula.unknown_formula_mode"},
		// F19 — whitespace-padded " active " rejected (downstream readers
		// don't TrimSpace before comparing).
		{name: "F19_string_with_formula_padded_active_rejected", fieldType: "string", formula: "Amount * 2", formulaMode: " active ", returnType: "string", wantCode: "field.formula.unknown_formula_mode"},
		// F20 — arbitrary non-canonical value rejected.
		{name: "F20_string_with_formula_random_mode_rejected", fieldType: "string", formula: "Amount * 2", formulaMode: "random_mode", returnType: "string", wantCode: "field.formula.unknown_formula_mode"},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			specs := []ObjectSpec{{
				APIName: "TestObj",
				Fields: []FieldSpec{{
					APIName:     "Test",
					Type:        tc.fieldType,
					Formula:     tc.formula,
					FormulaMode: tc.formulaMode,
					ReturnType:  tc.returnType,
				}},
			}}
			diags := validateObjectSpecFieldShapes(specs)
			if tc.wantCode == "" {
				if len(diags) != 0 {
					t.Errorf("want no diagnostics, got %d: %v", len(diags), diagCodes(diags))
				}
				return
			}
			if !hasDiagCode(diags, tc.wantCode) {
				t.Errorf("want diagnostic %q, got %v", tc.wantCode, diagCodes(diags))
			}
			if len(diags) != 1 {
				t.Errorf("want exactly 1 diagnostic, got %d: %v", len(diags), diagCodes(diags))
			}
		})
	}
}

func TestValidateObjectSpecFieldShapes_FormulaStorageMode(t *testing.T) {
	cases := []struct {
		name               string
		formulaStorageMode string
		wantCode           string
	}{
		{name: "blank_allowed", formulaStorageMode: "", wantCode: ""},
		{name: "view_only_allowed", formulaStorageMode: "view_only", wantCode: ""},
		{name: "write_materialized_allowed", formulaStorageMode: "write_materialized", wantCode: ""},
		{name: "uppercase_rejected", formulaStorageMode: "WRITE_MATERIALIZED", wantCode: "field.formula.unknown_formula_storage_mode"},
		{name: "padded_rejected", formulaStorageMode: " write_materialized ", wantCode: "field.formula.unknown_formula_storage_mode"},
		{name: "random_rejected", formulaStorageMode: "materialize_me", wantCode: "field.formula.unknown_formula_storage_mode"},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			specs := []ObjectSpec{{
				APIName: "TestObj",
				Fields: []FieldSpec{{
					APIName:            "ComputedValue",
					Type:               "formula",
					Formula:            "Amount * 2",
					FormulaMode:        "active",
					FormulaStorageMode: tc.formulaStorageMode,
					ReturnType:         "number",
				}},
			}}
			diags := validateObjectSpecFieldShapes(specs)
			if tc.wantCode == "" {
				if len(diags) != 0 {
					t.Errorf("want no diagnostics, got %d: %v", len(diags), diagCodes(diags))
				}
				return
			}
			if !hasDiagCode(diags, tc.wantCode) {
				t.Errorf("want diagnostic %q, got %v", tc.wantCode, diagCodes(diags))
			}
			if len(diags) != 1 {
				t.Errorf("want exactly 1 diagnostic, got %d: %v", len(diags), diagCodes(diags))
			}
		})
	}
}

func TestValidateObjectSpecFieldShapes_RejectsAggregateFormulaAuthoring(t *testing.T) {
	specs := []ObjectSpec{{
		APIName: "Parent__c",
		Fields: []FieldSpec{{
			APIName:     "Total__c",
			Type:        "formula",
			Formula:     "SUM(Child__c.Amount__c)",
			FormulaMode: "active",
			ReturnType:  "number",
		}},
	}}

	diags := validateObjectSpecFieldShapes(specs)
	if !hasDiagCode(diags, "field.formula.aggregate_authoring_requires_rollupsummary") {
		t.Fatalf("want aggregate authoring diagnostic, got %v", diagCodes(diags))
	}
}

// TestValidateObjectSpecFieldShapes_FormulaTypeWithNonCanonicalFormulaMode
// covers Codex round-4 BLOCK on #964. The round-3 fix added the canonical-
// formulaMode check only inside the non-formula branch, so a formula-typed
// field could still declare formulaMode "ACTIVE" / " active " / "FROZEN" and
// pass validation. Downstream pack apply/verify
// (source_backed_pack.go:367, source_pack_ops.go:1186) match the canonical
// lowercase form raw, so non-canonical spellings let pack-owned convergence
// skip physical DDL — yielding metadata with no backing column and no
// rebuilt derived view. The canonical gate now fires regardless of Type.
func TestValidateObjectSpecFieldShapes_FormulaTypeWithNonCanonicalFormulaMode(t *testing.T) {
	cases := []struct {
		name        string
		formulaMode string
		// returnType is always set so the formula branch's no_return_type /
		// padded_return_type guards don't fire and pollute the diagnostics.
		returnType string
		wantCode   string
	}{
		// Canonical-mode regressions (must still pass through cleanly).
		// NOTE: the blank-mode case has been REJECTED as of Codex round-9
		// BLOCK on #964 — see formula_type_blank_mode_with_formula_rejected
		// below. The legitimate "blank-mode" survivor is the formula-typed
		// field with NO formula expression yet (placeholder authoring), which
		// is covered by formula_type_blank_mode_blank_formula_allowed in
		// TestValidateObjectSpecFieldShapes_FormulaTypeBlankModeRequiresExplicitMode.
		{name: "formula_type_frozen_mode_allowed", formulaMode: "frozen", returnType: "number", wantCode: ""},
		{name: "formula_type_active_mode_allowed", formulaMode: "active", returnType: "number", wantCode: ""},
		// Non-canonical mode on a formula-typed field — the round-4 BLOCK.
		{name: "formula_type_ACTIVE_uppercase_rejected", formulaMode: "ACTIVE", returnType: "number", wantCode: "field.formula.unknown_formula_mode"},
		{name: "formula_type_padded_active_rejected", formulaMode: " active ", returnType: "number", wantCode: "field.formula.unknown_formula_mode"},
		{name: "formula_type_FROZEN_uppercase_rejected", formulaMode: "FROZEN", returnType: "number", wantCode: "field.formula.unknown_formula_mode"},
		{name: "formula_type_padded_frozen_rejected", formulaMode: " frozen ", returnType: "number", wantCode: "field.formula.unknown_formula_mode"},
		{name: "formula_type_random_mode_rejected", formulaMode: "random_mode", returnType: "number", wantCode: "field.formula.unknown_formula_mode"},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			specs := []ObjectSpec{{
				APIName: "TestObj",
				Fields: []FieldSpec{{
					APIName:     "Calc",
					Type:        "formula",
					Formula:     "Amount * 2",
					FormulaMode: tc.formulaMode,
					ReturnType:  tc.returnType,
				}},
			}}
			diags := validateObjectSpecFieldShapes(specs)
			if tc.wantCode == "" {
				if len(diags) != 0 {
					t.Errorf("want no diagnostics, got %d: %v", len(diags), diagCodes(diags))
				}
				return
			}
			if !hasDiagCode(diags, tc.wantCode) {
				t.Errorf("want diagnostic %q, got %v", tc.wantCode, diagCodes(diags))
			}
			if len(diags) != 1 {
				t.Errorf("want exactly 1 diagnostic, got %d: %v", len(diags), diagCodes(diags))
			}
		})
	}
}

// TestValidateObjectSpecFieldShapes_FormulaTypeBlankModeRequiresExplicitMode
// covers Codex round-9 BLOCK on #964. Before the fix, a source-backed pack
// could declare `type: "formula"` with a real `formula` expression and a
// blank `formulaMode` and pass validation. Downstream the active-mode path
// (formula_view_service.go:1115, source_backed_pack.go:367) requires
// formulaMode=="active" exactly and the frozen save-time path
// (records/formula_compute.go:162, 282; persistence_bulk_insert.go:624)
// requires formulaMode=="frozen" exactly — blank hits neither, yet pack-owned
// convergence (schema_pack_owned_field.go:597) treats it as "not active" and
// still emits a plain physical column. The fix requires an explicit mode
// when a formula expression is present, while keeping the formula-typed +
// blank-formula authoring case intact (the formula switch arm continues to
// reject it independently via the no_return_type / padded_return_type gates
// when returnType is also missing/wrong).
func TestValidateObjectSpecFieldShapes_FormulaTypeBlankModeRequiresExplicitMode(t *testing.T) {
	cases := []struct {
		name        string
		formula     string
		formulaMode string
		returnType  string
		wantCode    string
	}{
		// Pack-A from the round-9 directive: formula expression present, blank
		// mode → REJECTED with unknown_formula_mode.
		{name: "formula_type_blank_mode_with_formula_rejected", formula: "Amount * 2", formulaMode: "", returnType: "number", wantCode: "field.formula.unknown_formula_mode"},
		// Whitespace-only formula counts as no formula (TrimSpace), so the
		// new gate stays silent; the formula switch arm still allows it (the
		// no_return_type guard already covers returnType-missing).
		{name: "formula_type_blank_mode_whitespace_formula_allowed", formula: "   ", formulaMode: "", returnType: "number", wantCode: ""},
		// Pack-A regression: explicit modes still allowed.
		{name: "formula_type_active_mode_with_formula_allowed", formula: "Amount * 2", formulaMode: "active", returnType: "number", wantCode: ""},
		{name: "formula_type_frozen_mode_with_formula_allowed", formula: "Amount * 2", formulaMode: "frozen", returnType: "number", wantCode: ""},
		// Pack-A regression: formula-typed authoring placeholder (no formula
		// expression yet, blank mode) is ALLOWED at the formulaMode gate.
		// returnType is intentionally provided so the formula switch arm
		// does not emit no_return_type and pollute the diagnostic check.
		{name: "formula_type_blank_mode_blank_formula_allowed", formula: "", formulaMode: "", returnType: "number", wantCode: ""},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			specs := []ObjectSpec{{
				APIName: "TestObj",
				Fields: []FieldSpec{{
					APIName:     "Calc",
					Type:        "formula",
					Formula:     tc.formula,
					FormulaMode: tc.formulaMode,
					ReturnType:  tc.returnType,
				}},
			}}
			diags := validateObjectSpecFieldShapes(specs)
			if tc.wantCode == "" {
				if len(diags) != 0 {
					t.Errorf("want no diagnostics, got %d: %v", len(diags), diagCodes(diags))
				}
				return
			}
			if !hasDiagCode(diags, tc.wantCode) {
				t.Errorf("want diagnostic %q, got %v", tc.wantCode, diagCodes(diags))
			}
			if len(diags) != 1 {
				t.Errorf("want exactly 1 diagnostic, got %d: %v", len(diags), diagCodes(diags))
			}
		})
	}
}

// TestValidateObjectSpecFieldShapes_ExpressionOnNonFormulaTypeHoisted covers
// Codex round-6 BLOCK on #964. Before this PR, the
// expression_on_non_formula_type check lived inside the final switch arm, so
// reference / masterdetail / picklist / multipicklist short-circuited into
// their own earlier arms and never reached the dead-column check. A source-
// backed pack could therefore declare e.g. `type: "reference"` with a non-
// empty formula and blank or "frozen" formulaMode and pass
// validateObjectSpecFieldShapes — exactly the install-time admission of the
// dead-column shape this PR is meant to block. The fix hoists the check above
// the switch so it fires on every non-formula Type carrying a non-empty
// Formula, regardless of which subtype-specific arm would have matched.
//
// Each case is a single-field spec to keep the assertion simple: the hoisted
// check must always emit exactly the expression_on_non_formula_type diagnostic
// (and possibly the subtype's own diagnostic if the spec also fails the
// subtype-specific guard — verified in HoistedAggregation below).
func TestValidateObjectSpecFieldShapes_ExpressionOnNonFormulaTypeHoisted(t *testing.T) {
	cases := []struct {
		name        string
		fieldType   string
		formula     string
		formulaMode string
		referenceTo []string // populated for reference / masterdetail to avoid no_target noise
		options     models.PicklistOptions
		wantHoisted bool // true ⇒ expect expression_on_non_formula_type diagnostic
	}{
		// Reference + non-empty formula + blank/"frozen" formulaMode: must be
		// REJECTED. Before the hoist, the reference branch short-circuited and
		// the dead-column check never ran. Provide a canonical referenceTo so
		// the reference-shape validator does not pile on extra diagnostics.
		{name: "reference_with_formula_blank_mode", fieldType: "reference", formula: "Amount * 2", referenceTo: []string{"Account"}, wantHoisted: true},
		{name: "reference_with_formula_frozen_mode", fieldType: "reference", formula: "Amount * 2", formulaMode: "frozen", referenceTo: []string{"Account"}, wantHoisted: true},
		// Reference + formula + ACTIVE: still allowed (view-compute substrate).
		{name: "reference_with_formula_active_mode_allowed", fieldType: "reference", formula: "Amount * 2", formulaMode: "active", referenceTo: []string{"Account"}, wantHoisted: false},
		// MasterDetail with formula and blank/"frozen" — same regression class.
		{name: "masterdetail_with_formula_blank_mode", fieldType: "masterdetail", formula: "Amount * 2", referenceTo: []string{"Account"}, wantHoisted: true},
		{name: "masterdetail_with_formula_frozen_mode", fieldType: "masterdetail", formula: "Amount * 2", formulaMode: "frozen", referenceTo: []string{"Account"}, wantHoisted: true},
		// Picklist + formula + blank/"frozen": REJECTED. Earlier picklist arm
		// short-circuited and skipped the dead-column check.
		{name: "picklist_with_formula_blank_mode", fieldType: "picklist", formula: "Amount * 2", options: models.PicklistOptions{{Value: "hot"}}, wantHoisted: true},
		{name: "picklist_with_formula_frozen_mode", fieldType: "picklist", formula: "Amount * 2", formulaMode: "frozen", options: models.PicklistOptions{{Value: "hot"}}, wantHoisted: true},
		// Multipicklist + formula — same regression class.
		{name: "multipicklist_with_formula_blank_mode", fieldType: "multipicklist", formula: "Amount * 2", options: models.PicklistOptions{{Value: "hot"}}, wantHoisted: true},
		{name: "multipicklist_with_formula_frozen_mode", fieldType: "multipicklist", formula: "Amount * 2", formulaMode: "frozen", options: models.PicklistOptions{{Value: "hot"}}, wantHoisted: true},
		// Boolean isn't covered by any switch arm at all — was completely
		// skipped before the hoist because the old `case` clause required
		// `Type != reference && Type != picklist && Type != multipicklist
		// && Type != formula` AND was guarded by `formulaModeCanonical && Formula != ""`.
		// Now the hoisted check runs unconditionally.
		{name: "boolean_with_formula_blank_mode", fieldType: "boolean", formula: "IsActive", wantHoisted: true},
		{name: "boolean_with_formula_frozen_mode", fieldType: "boolean", formula: "IsActive", formulaMode: "frozen", wantHoisted: true},
		{name: "boolean_with_formula_active_mode_allowed", fieldType: "boolean", formula: "IsActive", formulaMode: "active", wantHoisted: false},
		// Formula-typed field still passes (the hoisted check skips Type=="formula").
		{name: "formula_type_with_expression_allowed", fieldType: "formula", formula: "Amount * 2", wantHoisted: false},
		// Non-canonical formulaMode on a reference field: the dedicated
		// unknown_formula_mode diagnostic fires; the hoisted dead-column check
		// is suppressed to avoid piling a second diagnostic on the same root
		// cause (the field's runtime behavior is undefined once formulaMode is
		// non-canonical, mirroring the formula-arm exemption logic).
		{name: "reference_with_formula_uppercase_mode_only_unknown_mode", fieldType: "reference", formula: "Amount * 2", formulaMode: "ACTIVE", referenceTo: []string{"Account"}, wantHoisted: false},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			specs := []ObjectSpec{{
				APIName: "TestObj",
				Fields: []FieldSpec{{
					APIName:     "Test",
					Type:        tc.fieldType,
					Formula:     tc.formula,
					FormulaMode: tc.formulaMode,
					ReferenceTo: tc.referenceTo,
					Options:     tc.options,
					// Provide a canonical returnType for the formula-typed case so
					// the formula-arm's no_return_type guard doesn't fire and
					// pollute the assertion.
					ReturnType: func() string {
						if tc.fieldType == "formula" {
							return "number"
						}
						return ""
					}(),
				}},
			}}
			diags := validateObjectSpecFieldShapes(specs)
			gotHoisted := hasDiagCode(diags, "field.formula.expression_on_non_formula_type")
			if tc.wantHoisted && !gotHoisted {
				t.Errorf("want field.formula.expression_on_non_formula_type, got %v", diagCodes(diags))
			}
			if !tc.wantHoisted && gotHoisted {
				t.Errorf("did not want field.formula.expression_on_non_formula_type, got %v", diagCodes(diags))
			}
		})
	}
}

// TestValidateObjectSpecFieldShapes_HoistedChecksRunWithSubtypeChecks confirms
// that hoisting the dead-column check above the switch does not REPLACE the
// subtype-specific check. A reference field with a bad referenceTo AND a
// formula expression should now emit BOTH diagnostics (it previously emitted
// only the reference one because the dead-column check was unreachable for
// reference fields).
func TestValidateObjectSpecFieldShapes_HoistedChecksRunWithSubtypeChecks(t *testing.T) {
	specs := []ObjectSpec{{
		APIName: "TestObj",
		Fields: []FieldSpec{{
			APIName:     "Lookup",
			Type:        "reference",
			Formula:     "Amount * 2",
			ReferenceTo: []string{"Account", " "}, // mixed-blank → blank_target
		}},
	}}
	diags := validateObjectSpecFieldShapes(specs)
	if !hasDiagCode(diags, "field.formula.expression_on_non_formula_type") {
		t.Errorf("want field.formula.expression_on_non_formula_type from hoisted check, got %v", diagCodes(diags))
	}
	if !hasDiagCode(diags, "field.reference.blank_target") {
		t.Errorf("want field.reference.blank_target from reference-shape check, got %v", diagCodes(diags))
	}
}

// TestValidateObjectSpecFieldShapes_ProjectionRequiresActiveFormulaMode covers
// Codex round-10 BLOCK on #964. Before this PR, the formulaMode gate only
// closed the hole for Formula-bearing shapes — a pack field with
// ProjectionConfig != nil and blank or "frozen" formulaMode passed validation
// because the dead-column checks were scoped to field.Formula. Downstream
// source-backed-pack provisioning only treats projection fields as active
// when formulaMode == "active" exactly (source_backed_pack.go:365-374
// objectSpecRequiresDerivedView; formula_view_service.go:1115
// isActiveComputedField; schema_pack_owned_field.go:597 pack-owned
// convergence emits a plain physical column for the inactive case). Result:
// greenfield `setup apply` could admit a projection-backed field as a dead
// physical column with no derived view. The new gate rejects any projection
// field whose formulaMode is not the exact canonical "active".
func TestValidateObjectSpecFieldShapes_ProjectionRequiresActiveFormulaMode(t *testing.T) {
	projection := &models.FieldProjectionConfig{
		Kind:         "lookupValue",
		TargetObject: "Product2",
		SelectField:  "Name",
		Matches: []models.FieldProjectionMatch{
			{TargetField: "Id", SourceField: "Product2Id"},
		},
		ExcludeDeleted: true,
	}
	cases := []struct {
		name             string
		fieldType        string
		projectionConfig *models.FieldProjectionConfig
		formula          string
		formulaMode      string
		returnType       string
		wantProjDiag     bool
	}{
		// Golden — projection + active formulaMode mirrors PricebookEntry.Name
		// and CampaignMember.HasResponded in salesforce-crm-standard.
		{name: "projection_active_mode_allowed", fieldType: "string", projectionConfig: projection, formulaMode: "active", returnType: "string", wantProjDiag: false},
		{name: "projection_active_mode_boolean_allowed", fieldType: "boolean", projectionConfig: projection, formulaMode: "active", returnType: "boolean", wantProjDiag: false},
		// REJECT: projection + frozen → no compute path wired.
		{name: "projection_frozen_mode_rejected", fieldType: "string", projectionConfig: projection, formulaMode: "frozen", returnType: "string", wantProjDiag: true},
		// REJECT: projection + blank → no compute path wired (the issue Codex flagged).
		{name: "projection_blank_mode_rejected", fieldType: "string", projectionConfig: projection, formulaMode: "", returnType: "string", wantProjDiag: true},
		{name: "projection_blank_mode_boolean_rejected", fieldType: "boolean", projectionConfig: projection, formulaMode: "", returnType: "boolean", wantProjDiag: true},
		// Projection on a number type with blank mode is the same regression
		// class — verify it fires on a third type to confirm the gate is
		// type-agnostic (matches the PricebookEntry.Name pattern Codex cited).
		{name: "projection_blank_mode_number_rejected", fieldType: "number", projectionConfig: projection, formulaMode: "", returnType: "number", wantProjDiag: true},
		// Existing behavior unchanged: no projection + any formulaMode does
		// not emit the new projection diagnostic.
		{name: "no_projection_blank_mode_unchanged", fieldType: "string", projectionConfig: nil, formulaMode: "", wantProjDiag: false},
		{name: "no_projection_frozen_mode_unchanged", fieldType: "formula", projectionConfig: nil, formula: "Amount*2", formulaMode: "frozen", returnType: "number", wantProjDiag: false},
		{name: "no_projection_active_mode_unchanged", fieldType: "formula", projectionConfig: nil, formula: "Amount*2", formulaMode: "active", returnType: "number", wantProjDiag: false},
		// Suppression: when formulaMode is non-canonical, the dedicated
		// unknown_formula_mode diagnostic already names the field, so the
		// projection diagnostic is suppressed to avoid piling on the same
		// root cause (mirrors the expression_on_non_formula_type suppression).
		{name: "projection_uppercase_mode_only_unknown_mode", fieldType: "string", projectionConfig: projection, formulaMode: "ACTIVE", returnType: "string", wantProjDiag: false},
		{name: "projection_padded_active_mode_only_unknown_mode", fieldType: "string", projectionConfig: projection, formulaMode: " active ", returnType: "string", wantProjDiag: false},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			specs := []ObjectSpec{{
				APIName: "TestObj",
				Fields: []FieldSpec{{
					APIName:          "Proj",
					Type:             tc.fieldType,
					Formula:          tc.formula,
					FormulaMode:      tc.formulaMode,
					ReturnType:       tc.returnType,
					ProjectionConfig: tc.projectionConfig,
				}},
			}}
			diags := validateObjectSpecFieldShapes(specs)
			gotProj := hasDiagCode(diags, "field.projection.requires_active_formula_mode")
			if tc.wantProjDiag && !gotProj {
				t.Errorf("want field.projection.requires_active_formula_mode, got %v", diagCodes(diags))
			}
			if !tc.wantProjDiag && gotProj {
				t.Errorf("did not want field.projection.requires_active_formula_mode, got %v", diagCodes(diags))
			}
		})
	}
}

// TestValidateObjectSpecDisplayNameField covers issue #1110 / PR #1133 Ian P1
// acceptance matrix: blank, padded-whitespace, unresolved name, wrong-type,
// invalid relationshipName on relationship-backed fields, valid string name
// field, valid reference field.
func TestValidateObjectSpecDisplayNameField(t *testing.T) {
	// Build a base field set the spec can reference.
	stringName := FieldSpec{APIName: "Name", Type: "string", IsNameField: true}
	refField := FieldSpec{APIName: "AccountId", Type: "reference", ReferenceTo: []string{"Account"}, RelationshipName: "Account"}
	mdField := FieldSpec{APIName: "ParentId", Type: "masterdetail", ReferenceTo: []string{"Account"}, RelationshipName: "Parent"}
	textareaField := FieldSpec{APIName: "Notes", Type: "textarea"}
	autonumberField := FieldSpec{APIName: "Number__c", Type: "autonumber"}
	numField := FieldSpec{APIName: "Amount", Type: "double"}
	picklistField := FieldSpec{APIName: "Stage", Type: "picklist"}
	boolField := FieldSpec{APIName: "Active", Type: "boolean"}
	richField := FieldSpec{APIName: "Body", Type: "richtextarea"}
	refMissingRelationship := FieldSpec{APIName: "AccountId", Type: "reference", ReferenceTo: []string{"Account"}}
	refBlankRelationship := FieldSpec{APIName: "AccountId", Type: "reference", ReferenceTo: []string{"Account"}, RelationshipName: "   "}
	refPaddedRelationship := FieldSpec{APIName: "AccountId", Type: "reference", ReferenceTo: []string{"Account"}, RelationshipName: " Account"}
	refMultiTarget := FieldSpec{APIName: "WhatId", Type: "reference", ReferenceTo: []string{"Account", "Opportunity"}, RelationshipName: "What"}
	refPolymorphicFlag := FieldSpec{APIName: "OwnerId", Type: "reference", ReferenceTo: []string{"User"}, IsPolymorphic: true, RelationshipName: "Owner"}
	mdMultiTarget := FieldSpec{APIName: "ParentId", Type: "masterdetail", ReferenceTo: []string{"Account", "Contact"}, RelationshipName: "Parent"}
	mdMissingRelationship := FieldSpec{APIName: "ParentId", Type: "masterdetail", ReferenceTo: []string{"Account"}}
	missingTargetRef := FieldSpec{APIName: "MissingId", Type: "reference", ReferenceTo: []string{"Missing__c"}, RelationshipName: "Missing"}
	accountSpec := ObjectSpec{
		APIName: "Account",
		Fields: []FieldSpec{
			{APIName: "Name", Type: "string"},
			{APIName: "ExternalId__c", Type: "string"},
			{APIName: "IsActive", Type: "boolean"},
		},
	}

	cases := []struct {
		name        string
		display     string
		fields      []FieldSpec
		wantCode    string // "" = expect no diagnostic
		wantOnlyOne bool
	}{
		{
			name:     "empty_optional_ok",
			display:  "",
			fields:   []FieldSpec{stringName, refField},
			wantCode: "",
		},
		{
			name:        "blank_whitespace_only_rejected",
			display:     "   ",
			fields:      []FieldSpec{stringName, refField},
			wantCode:    "object.display_name_field.blank",
			wantOnlyOne: true,
		},
		{
			name:        "padded_leading_rejected",
			display:     " AccountId",
			fields:      []FieldSpec{stringName, refField},
			wantCode:    "object.display_name_field.padded",
			wantOnlyOne: true,
		},
		{
			name:        "padded_trailing_rejected",
			display:     "AccountId ",
			fields:      []FieldSpec{stringName, refField},
			wantCode:    "object.display_name_field.padded",
			wantOnlyOne: true,
		},
		{
			name:        "unresolved_name_rejected",
			display:     "NoSuchField",
			fields:      []FieldSpec{stringName, refField},
			wantCode:    "object.display_name_field.unresolved",
			wantOnlyOne: true,
		},
		{
			name:        "case_sensitive_unresolved",
			display:     "accountid",
			fields:      []FieldSpec{stringName, refField},
			wantCode:    "object.display_name_field.unresolved",
			wantOnlyOne: true,
		},
		{
			name:        "wrong_type_number_rejected",
			display:     "Amount",
			fields:      []FieldSpec{stringName, numField},
			wantCode:    "object.display_name_field.wrong_type",
			wantOnlyOne: true,
		},
		{
			name:        "wrong_type_picklist_rejected",
			display:     "Stage",
			fields:      []FieldSpec{stringName, picklistField},
			wantCode:    "object.display_name_field.wrong_type",
			wantOnlyOne: true,
		},
		{
			name:        "wrong_type_boolean_rejected",
			display:     "Active",
			fields:      []FieldSpec{stringName, boolField},
			wantCode:    "object.display_name_field.wrong_type",
			wantOnlyOne: true,
		},
		{
			name:        "wrong_type_richtextarea_rejected",
			display:     "Body",
			fields:      []FieldSpec{stringName, richField},
			wantCode:    "object.display_name_field.wrong_type",
			wantOnlyOne: true,
		},
		{
			name:        "reference_missing_relationship_name_rejected",
			display:     "AccountId",
			fields:      []FieldSpec{stringName, refMissingRelationship},
			wantCode:    "object.display_name_field.relationship_name_missing",
			wantOnlyOne: true,
		},
		{
			name:        "reference_blank_relationship_name_rejected",
			display:     "AccountId",
			fields:      []FieldSpec{stringName, refBlankRelationship},
			wantCode:    "object.display_name_field.relationship_name_missing",
			wantOnlyOne: true,
		},
		{
			name:        "reference_padded_relationship_name_rejected",
			display:     "AccountId",
			fields:      []FieldSpec{stringName, refPaddedRelationship},
			wantCode:    "object.display_name_field.relationship_name_padded",
			wantOnlyOne: true,
		},
		{
			name:        "reference_multi_target_rejected",
			display:     "WhatId",
			fields:      []FieldSpec{stringName, refMultiTarget},
			wantCode:    "object.display_name_field.reference_not_single_target",
			wantOnlyOne: true,
		},
		{
			name:        "reference_polymorphic_flag_rejected",
			display:     "OwnerId",
			fields:      []FieldSpec{stringName, refPolymorphicFlag},
			wantCode:    "object.display_name_field.reference_not_single_target",
			wantOnlyOne: true,
		},
		{
			name:        "masterdetail_multi_target_rejected",
			display:     "ParentId",
			fields:      []FieldSpec{stringName, mdMultiTarget},
			wantCode:    "object.display_name_field.reference_not_single_target",
			wantOnlyOne: true,
		},
		{
			name:        "masterdetail_missing_relationship_name_rejected",
			display:     "ParentId",
			fields:      []FieldSpec{stringName, mdMissingRelationship},
			wantCode:    "object.display_name_field.relationship_name_missing",
			wantOnlyOne: true,
		},
		// Golden cases: every accepted type passes.
		{
			name:     "valid_reference",
			display:  "AccountId",
			fields:   []FieldSpec{stringName, refField},
			wantCode: "",
		},
		{
			name:     "valid_relationship_name",
			display:  "Account",
			fields:   []FieldSpec{stringName, refField},
			wantCode: "",
		},
		{
			name:     "valid_relationship_dot_path",
			display:  "Account.ExternalId__c",
			fields:   []FieldSpec{stringName, refField},
			wantCode: "",
		},
		{
			name:        "relationship_dot_path_invalid_child_api_name_rejected",
			display:     "Account.Bad Field",
			fields:      []FieldSpec{stringName, refField},
			wantCode:    "object.display_name_field.child_invalid_api_name",
			wantOnlyOne: true,
		},
		{
			name:        "relationship_dot_path_padded_parent_rejected",
			display:     "Account .ExternalId__c",
			fields:      []FieldSpec{stringName, refField},
			wantCode:    "object.display_name_field.segment_padded",
			wantOnlyOne: true,
		},
		{
			name:        "relationship_dot_path_padded_child_rejected",
			display:     "Account. ExternalId__c",
			fields:      []FieldSpec{stringName, refField},
			wantCode:    "object.display_name_field.segment_padded",
			wantOnlyOne: true,
		},
		{
			name:        "relationship_dot_path_missing_child_rejected",
			display:     "Account.DoesNotExist__c",
			fields:      []FieldSpec{stringName, refField},
			wantCode:    "object.display_name_field.child_unresolved",
			wantOnlyOne: true,
		},
		{
			name:        "relationship_dot_path_wrong_child_type_rejected",
			display:     "Account.IsActive",
			fields:      []FieldSpec{stringName, refField},
			wantCode:    "object.display_name_field.child_wrong_type",
			wantOnlyOne: true,
		},
		{
			name:        "relationship_dot_path_missing_target_spec_rejected",
			display:     "Missing.ExternalId__c",
			fields:      []FieldSpec{stringName, missingTargetRef},
			wantCode:    "object.display_name_field.child_target_unresolved",
			wantOnlyOne: true,
		},
		{
			name:     "valid_lookup_field_dot_path",
			display:  "AccountId.ExternalId__c",
			fields:   []FieldSpec{stringName, refField},
			wantCode: "",
		},
		{
			name:     "valid_masterdetail",
			display:  "ParentId",
			fields:   []FieldSpec{stringName, mdField},
			wantCode: "",
		},
		{
			name:     "valid_string_name_field",
			display:  "Name",
			fields:   []FieldSpec{stringName, refField},
			wantCode: "",
		},
		{
			name:     "valid_textarea",
			display:  "Notes",
			fields:   []FieldSpec{stringName, textareaField},
			wantCode: "",
		},
		{
			name:     "valid_autonumber",
			display:  "Number__c",
			fields:   []FieldSpec{stringName, autonumberField},
			wantCode: "",
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			spec := ObjectSpec{
				APIName:          "TestObj__c",
				DisplayNameField: tc.display,
				Fields:           tc.fields,
			}
			diags := validateObjectSpecDisplayNameField(spec, newObjectSpecFieldResolver([]ObjectSpec{spec, accountSpec}))
			if tc.wantCode == "" {
				if len(diags) != 0 {
					t.Errorf("want no diagnostics, got %d: %v", len(diags), diagCodes(diags))
				}
				return
			}
			if !hasDiagCode(diags, tc.wantCode) {
				t.Errorf("want diagnostic %q, got %v", tc.wantCode, diagCodes(diags))
			}
			if tc.wantOnlyOne && len(diags) != 1 {
				t.Errorf("want exactly 1 diagnostic, got %d: %v", len(diags), diagCodes(diags))
			}
		})
	}
}

// TestValidateObjectSpecFieldShapes_DisplayNameField_AggregatedThroughTopLevel
// confirms validateObjectSpecFieldShapes (the public install-time entry point)
// surfaces display-name diagnostics alongside field-level ones, so the
// validate-pack / install-pack call site at pack_source.go:857 picks them up
// without an extra wiring pass.
func TestValidateObjectSpecFieldShapes_DisplayNameField_AggregatedThroughTopLevel(t *testing.T) {
	specs := []ObjectSpec{{
		APIName:          "BadObj__c",
		DisplayNameField: "missing",
		Fields: []FieldSpec{
			{APIName: "Name", Type: "string"},
		},
	}}
	diags := validateObjectSpecFieldShapes(specs)
	if !hasDiagCode(diags, "object.display_name_field.unresolved") {
		t.Errorf("want object.display_name_field.unresolved, got %v", diagCodes(diags))
	}
}

func TestValidateObjectSpecFieldShapes_DisplayNameFieldDotPathUsesProvidedResolver(t *testing.T) {
	accountSpec := ObjectSpec{
		APIName: "Account",
		Fields: []FieldSpec{
			{APIName: "Name", Type: "string"},
			{APIName: "ExternalId__c", Type: "string"},
		},
	}
	widgetSpec := ObjectSpec{
		APIName:          "Widget__c",
		DisplayNameField: "Account.ExternalId__c",
		Fields: []FieldSpec{
			{APIName: "Name", Type: "string"},
			{APIName: "AccountId", Type: "reference", ReferenceTo: []string{"Account"}, RelationshipName: "Account"},
		},
	}
	resolver := newObjectSpecFieldResolver([]ObjectSpec{accountSpec, widgetSpec})

	diags := validateObjectSpecFieldShapesWithResolver([]ObjectSpec{widgetSpec}, resolver, false, false)
	if len(diags) != 0 {
		t.Fatalf("want no diagnostics when target object is resolver-visible, got %v", diagCodes(diags))
	}

	if err := validateSpecDisplayNameFieldsWithResolver([]ObjectSpec{widgetSpec}, resolver); err != nil {
		t.Fatalf("validateSpecDisplayNameFieldsWithResolver: %v", err)
	}
}

// TestValidateSpecDisplayNameFields_ErrorWrapper confirms the apply/verify
// admit-path wrapper returns nil for clean specs and a populated error for
// any non-canonical display-name configuration. This is the function wired
// into applyObjectSpecsWithResolverAndValidation (platform_core.go, between
// validateSpecReadSources and convertSpecToTableDefinitionsWithResolver) and
// into sourceBackedObjectSpecComponent.VerifyComponent
// (source_backed_pack.go:1134).
func TestValidateSpecDisplayNameFields_ErrorWrapper(t *testing.T) {
	t.Run("clean_returns_nil", func(t *testing.T) {
		specs := []ObjectSpec{{
			APIName:          "Good__c",
			DisplayNameField: "AccountId",
			Fields: []FieldSpec{
				{APIName: "Name", Type: "string"},
				{APIName: "AccountId", Type: "reference", ReferenceTo: []string{"Account"}, RelationshipName: "Account"},
			},
		}}
		if err := validateSpecDisplayNameFields(specs); err != nil {
			t.Errorf("want nil for clean specs, got %v", err)
		}
	})
	t.Run("optional_unset_returns_nil", func(t *testing.T) {
		specs := []ObjectSpec{{
			APIName:          "NoOverride__c",
			DisplayNameField: "",
			Fields: []FieldSpec{
				{APIName: "Name", Type: "string"},
			},
		}}
		if err := validateSpecDisplayNameFields(specs); err != nil {
			t.Errorf("want nil for unset DisplayNameField, got %v", err)
		}
	})
	t.Run("padded_returns_error", func(t *testing.T) {
		specs := []ObjectSpec{{
			APIName:          "Padded__c",
			DisplayNameField: "AccountId ",
			Fields: []FieldSpec{
				{APIName: "AccountId", Type: "reference", ReferenceTo: []string{"Account"}},
			},
		}}
		err := validateSpecDisplayNameFields(specs)
		if err == nil {
			t.Fatal("want error for padded DisplayNameField, got nil")
		}
		if !strings.Contains(err.Error(), "object.display_name_field.padded") {
			t.Errorf("want padded code in error, got %q", err.Error())
		}
	})
	t.Run("aggregates_across_objects", func(t *testing.T) {
		specs := []ObjectSpec{
			{
				APIName:          "A__c",
				DisplayNameField: "Missing",
				Fields:           []FieldSpec{{APIName: "Name", Type: "string"}},
			},
			{
				APIName:          "B__c",
				DisplayNameField: "Amount",
				Fields: []FieldSpec{
					{APIName: "Amount", Type: "double"},
				},
			},
		}
		err := validateSpecDisplayNameFields(specs)
		if err == nil {
			t.Fatal("want error for two-bad-objects spec set, got nil")
		}
		// Both diagnostics should be surfaced in the joined error.
		if !strings.Contains(err.Error(), "A__c.displayNameField") {
			t.Errorf("want A__c diagnostic in error, got %q", err.Error())
		}
		if !strings.Contains(err.Error(), "B__c.displayNameField") {
			t.Errorf("want B__c diagnostic in error, got %q", err.Error())
		}
	})
}

// hasDiagCode returns true if any diagnostic in diags has the given code.
func hasDiagCode(diags []PackDiagnostic, code string) bool {
	for _, d := range diags {
		if d.Code == code {
			return true
		}
	}
	return false
}

// diagCodes extracts diagnostic codes for test error messages.
func diagCodes(diags []PackDiagnostic) []string {
	codes := make([]string, len(diags))
	for i, d := range diags {
		codes[i] = d.Code
	}
	return codes
}
