package dialectsfcrm

import (
	"context"
	"encoding/json"
	"net/http"
	"reflect"
	"slices"
	"testing"

	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/dialect"
	"github.com/nexuscrm/shared/pkg/fieldprovenance"
	"github.com/nexuscrm/shared/pkg/models"
)

// behaviorStub builds a layout whose single section carries the given authored
// behavior, over a schema with one plainly-editable field and one the create
// path genuinely demands.
func behaviorStub(t *testing.T, behaviors map[string]map[string]string) *uiAPITestContracts {
	t.Helper()
	stub := newUIAPITestContracts()
	stub.schema = &models.ObjectMetadata{
		APIName: "Account",
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldName, Label: "Account Name", Type: constants.FieldTypeString, Createable: true, Updateable: true, IsNameField: true},
			{APIName: "Rating__c", Label: "Rating", Type: constants.FieldTypeString, Createable: true, Updateable: true},
			// Required with no default and not defaulted on create: the create
			// path will demand it, so hiding it must be clamped.
			{APIName: "Mandatory__c", Label: "Mandatory", Type: constants.FieldTypeString, Createable: true, Updateable: true, Required: true},
		},
	}
	stub.schemas = map[string]*models.ObjectMetadata{"Account": stub.schema}
	stub.layout = &models.PageLayout{
		ID:            "curated_Account_default",
		ObjectAPIName: "Account",
		LayoutName:    "Account Layout",
		Sections: []models.PageSection{{
			ID:             "s1",
			Label:          "Account Information",
			Columns:        2,
			Fields:         []string{constants.FieldName, "Rating__c", "Mandatory__c"},
			FieldBehaviors: behaviors,
		}},
	}
	return stub
}

// layoutRows returns the sections' rows with item order intact. A field-keyed
// map would be more convenient and would also hide the thing "hidden = absence"
// exists to buy: appendItem re-packs the grid, so a hidden field must leave no
// gap. Order and row membership are the evidence for that.
func layoutRows(t *testing.T, stub *uiAPITestContracts, mode string) [][][]string {
	t.Helper()
	body := layoutBody(t, stub, mode)
	var out [][][]string
	for _, section := range body["sections"].([]any) {
		var rows [][]string
		rawRows, _ := section.(map[string]any)["layoutRows"].([]any)
		for _, row := range rawRows {
			var names []string
			for _, item := range row.(map[string]any)["layoutItems"].([]any) {
				comps, _ := item.(map[string]any)["layoutComponents"].([]any)
				if len(comps) == 0 {
					continue
				}
				name, _ := comps[0].(map[string]any)["apiName"].(string)
				names = append(names, name)
			}
			rows = append(rows, names)
		}
		out = append(out, rows)
	}
	return out
}

// layoutBody issues the request and asserts the requested type actually reached
// the projection. Without that assertion a handler that ignored ?type= would
// leave every mode-scoped test in this file green: they would all be reading one
// mode's response and agreeing with each other about it.
func layoutBody(t *testing.T, stub *uiAPITestContracts, mode string) map[string]any {
	t.Helper()
	req := &dialect.Request{Params: map[string]string{"objectApiName": "Account"}}
	if mode != "" {
		req.Query = map[string][]string{"type": {mode}}
	}
	resp, err := UIAPILayoutNeutralHandler(context.Background(), req, uiAPIDeps(stub, nil))
	if err != nil {
		t.Fatalf("handler error: %v", err)
	}
	if resp.Status != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	body := decodeResponseBody(t, resp.Body)
	// The handler folds "" and "Detail" onto the View mode; Create and Edit
	// pass through. Asserting the fold as well pins the mapping itself.
	want := "View"
	switch mode {
	case "Create", "Edit":
		want = mode
	case "", "Detail":
	default:
		t.Fatalf("test asked for type=%q, which is not a type this endpoint accepts", mode)
	}
	if got, _ := body["mode"].(string); got != want {
		t.Fatalf("requested type=%q but the response reports mode=%q, want %q — "+
			"the query parameter did not reach the projection, so every mode-scoped "+
			"assertion in this file is vacuous", mode, got, want)
	}
	return body
}

func layoutItems(t *testing.T, stub *uiAPITestContracts, mode string) map[string]map[string]any {
	t.Helper()
	body := layoutBody(t, stub, mode)
	out := map[string]map[string]any{}
	for _, section := range body["sections"].([]any) {
		// A section whose every field is hidden emits no rows at all, so this
		// must tolerate their absence rather than panicking on it.
		rawRows, _ := section.(map[string]any)["layoutRows"].([]any)
		for _, row := range rawRows {
			for _, item := range row.(map[string]any)["layoutItems"].([]any) {
				it := item.(map[string]any)
				comps, _ := it["layoutComponents"].([]any)
				if len(comps) == 0 {
					continue
				}
				if name, _ := comps[0].(map[string]any)["apiName"].(string); name != "" {
					out[name] = it
				}
			}
		}
	}
	return out
}

// The whole point is that hiding applies to the authored mode and nothing else.
// Pinning only the Create side would stay green if the mode key were ignored
// and every mode hid the field.
func TestAuthoredHiddenAppliesOnlyToTheAuthoredMode(t *testing.T) {
	behaviors := map[string]map[string]string{"Rating__c": {"create": "hidden"}}

	if items := layoutItems(t, behaviorStub(t, behaviors), "Create"); items["Rating__c"] != nil {
		t.Error("Rating__c is authored hidden on create but still reached the create form")
	}
	for _, mode := range []string{"Edit", "Detail"} {
		if items := layoutItems(t, behaviorStub(t, behaviors), mode); items["Rating__c"] == nil {
			t.Errorf("Rating__c was authored hidden on create only, but is missing in %s", mode)
		}
	}
}

// Hidden is the item's absence rather than editableForNew=false, so the grid
// re-packs. An implementation that set the boolean instead would leave the item
// present and this assertion would catch it.
func TestAuthoredHiddenRemovesTheItemRatherThanFlaggingIt(t *testing.T) {
	items := layoutItems(t, behaviorStub(t, map[string]map[string]string{"Rating__c": {"create": "hidden"}}), "Create")
	if _, present := items["Rating__c"]; present {
		t.Fatal("hidden field is still emitted as an item; the create grid would keep a gap for it")
	}
	if items[constants.FieldName] == nil {
		t.Fatal("hiding one field removed an unrelated one")
	}
}

// FLS is the floor: authoring narrows what survives it and can never widen it.
func TestAuthoredBehaviorCannotWidenFieldLevelSecurity(t *testing.T) {
	stub := behaviorStub(t, map[string]map[string]string{"Rating__c": {"create": "shown"}})
	stub.fieldAccess = map[string]contract.FieldAccess{
		"Rating__c": {Readable: true, Createable: false, Updateable: false},
	}

	items := layoutItems(t, stub, "Create")
	item := items["Rating__c"]
	if item == nil {
		t.Fatal("Rating__c is readable, so it should still appear")
	}
	if item["editableForNew"] != false {
		t.Errorf("editableForNew = %#v, want false — authoring must not widen FLS", item["editableForNew"])
	}
}

// Authored required widens; it must never narrow a field the schema already
// requires.
func TestAuthoredRequiredWidensButNeverNarrows(t *testing.T) {
	items := layoutItems(t, behaviorStub(t, map[string]map[string]string{
		"Rating__c":    {"create": "required"},
		"Mandatory__c": {"create": "shown"},
	}), "Create")

	if items["Rating__c"]["required"] != true {
		t.Errorf("Rating__c authored required but required = %#v", items["Rating__c"]["required"])
	}
	if items["Mandatory__c"]["required"] != true {
		t.Errorf("Mandatory__c is schema-required; authoring \"shown\" must not clear it")
	}
}

// Hiding a field the create path will demand produces a save failure naming a
// field the user cannot see, so the projection refuses to hide it. Production
// is roll-forward only, which makes that error expensive to recover from.
func TestAuthoredHiddenIsClampedForFieldsTheCreatePathDemands(t *testing.T) {
	items := layoutItems(t, behaviorStub(t, map[string]map[string]string{
		"Mandatory__c": {"create": "hidden"},
	}), "Create")

	if items["Mandatory__c"] == nil {
		t.Fatal("a required-on-create field was hidden; saving would fail naming a field the form never showed")
	}
}

// The clamp and the create validator must answer the same question, so a field
// newly exempted from one is exempted from both. Table-driving every skip
// branch is what makes a re-inlined copy of either visible.
func TestCreateRequiredPredicateCoversEverySkipBranch(t *testing.T) {
	schema := &models.ObjectMetadata{APIName: "Account"}
	defaulted := "seeded"

	for _, tc := range []struct {
		name  string
		field models.FieldMetadata
		want  bool
	}{
		{"not required at all", models.FieldMetadata{APIName: "A__c", Type: constants.FieldTypeString}, false},
		{"required with no escape", models.FieldMetadata{APIName: "B__c", Type: constants.FieldTypeString, Required: true}, true},
		{"non-empty default", models.FieldMetadata{APIName: "C__c", Type: constants.FieldTypeString, Required: true, DefaultValue: &defaulted}, false},
		{"defaulted on create", models.FieldMetadata{APIName: "D__c", Type: constants.FieldTypeString, Required: true, DefaultedOnCreate: true}, false},
		{"audit field", models.FieldMetadata{APIName: constants.FieldCreatedDate, Type: constants.FieldTypeString, Required: true}, false},
		{"autonumber", models.FieldMetadata{APIName: "E__c", Type: constants.FieldTypeAutonumber, Required: true}, false},
		{"formula", models.FieldMetadata{APIName: "F__c", Type: constants.FieldTypeFormula, Required: true}, false},
		{"rollup summary", models.FieldMetadata{APIName: "G__c", Type: constants.FieldTypeRollupSummary, Required: true}, false},
	} {
		t.Run(tc.name, func(t *testing.T) {
			field := tc.field
			if got := fieldRequiresUserInputOnCreate(schema, &field); got != tc.want {
				t.Errorf("fieldRequiresUserInputOnCreate(%s) = %v, want %v", tc.field.APIName, got, tc.want)
			}
			// The validator must agree: a field the predicate exempts is never
			// reported missing, and one it demands always is.
			err := validateRequiredForCreate(
				&models.ObjectMetadata{APIName: "Account", Fields: []models.FieldMetadata{field}},
				map[string]interface{}{},
			)
			if tc.want && err == nil {
				t.Errorf("predicate demands %s but the validator accepts an empty payload", tc.field.APIName)
			}
			if !tc.want && err != nil {
				t.Errorf("predicate exempts %s but the validator rejects an empty payload: %v", tc.field.APIName, err)
			}
		})
	}
}

// An unrecognized value, an unrecognized mode key, and an absent map all mean
// "shown" — byte-for-byte today's behavior — so an app layer can introduce
// vocabulary without the platform rejecting it.
func TestUnknownAuthoredVocabularyFallsThroughToTodaysBehavior(t *testing.T) {
	baseline := layoutItems(t, behaviorStub(t, nil), "Create")
	for _, value := range []string{"readonly", "Hidden", "HIDDEN", "required ", "", "shown", "nonsense"} {
		t.Run(value, func(t *testing.T) {
			got := layoutItems(t, behaviorStub(t, map[string]map[string]string{"Rating__c": {"create": value}}), "Create")
			// Whole-item equality, not presence. Presence alone stays true when
			// an unrecognized value is quietly folded into a recognized one —
			// adding "readonly" to the required case, say, keeps the item there
			// and changes what it means.
			if !reflect.DeepEqual(got, baseline) {
				t.Errorf("authoring %q changed the response; unrecognized vocabulary must be inert.\n got: %#v\nwant: %#v",
					value, got["Rating__c"], baseline["Rating__c"])
			}
		})
	}
}

func TestEmptyModeKeyIsInertInEveryMode(t *testing.T) {
	for _, behavior := range []string{"hidden", "required"} {
		for _, mode := range []string{"", "Detail", "Create", "Edit"} {
			items := layoutItems(t, behaviorStub(t, map[string]map[string]string{
				"Rating__c": {"": behavior},
			}), mode)
			item := items["Rating__c"]
			if item == nil {
				t.Errorf("behavior %q under the empty mode key removed Rating__c in mode %q", behavior, mode)
				continue
			}
			if item["required"] != false {
				t.Errorf("behavior %q under the empty mode key set required in mode %q", behavior, mode)
			}
		}
	}
}

// The clamp must ask the SHARED predicate, not a bare Required check. This
// fixture is the discriminating case: DefaultedOnCreate__c is Required AND
// DefaultedOnCreate, so the create path will NOT demand it — the shared
// predicate exempts it and hiding is allowed. A clamp written as
// `fMeta.Required` would refuse to hide it, and no other test can tell the two
// implementations apart.
func TestClampUsesTheSharedPredicateNotABareRequiredCheck(t *testing.T) {
	stub := behaviorStub(t, map[string]map[string]string{"Defaulted__c": {"create": "hidden"}})
	stub.schema.Fields = append(stub.schema.Fields, models.FieldMetadata{
		APIName: "Defaulted__c", Label: "Defaulted", Type: constants.FieldTypeString,
		Createable: true, Updateable: true, Required: true, DefaultedOnCreate: true,
	})
	stub.layout.Sections[0].Fields = append(stub.layout.Sections[0].Fields, "Defaulted__c")

	if items := layoutItems(t, stub, "Create"); items["Defaulted__c"] != nil {
		t.Error("Defaulted__c is Required but defaulted on create, so the create path never demands it — " +
			"the clamp must allow hiding it; a bare Required check would not")
	}
}

// A compound parent is exempt from the create demand, so hiding it is allowed
// even though it is Required — its components carry the value. This is the half
// of the split that lives in the shared predicate; the other half
// (fieldSatisfiedByNameComponents) stays in the validator because it asks what a
// particular request supplied.
//
// The classifier has to be installed explicitly. fieldprovenance's package-level
// default is nil until bootstrap builds one from the L1 manifests, and nothing
// in this test binary does that, so ClassifyCompound answers "not compound" for
// every field and the branch is unreachable by default. An earlier version of
// this test read that answer as data about Account and skipped itself — green,
// and testing nothing.
// Do NOT add t.Parallel() here. This swaps a package-level global that four
// production sites read; Go only resumes parallel tests after the sequential
// ones, which is the sole reason the swap window is safe today.
func TestCompoundParentIsExemptFromTheCreateDemand(t *testing.T) {
	previous := fieldprovenance.Default()
	t.Cleanup(func() { fieldprovenance.SetDefault(previous) })
	fieldprovenance.SetDefault(fieldprovenance.BuildClassifier(nil, []fieldprovenance.ObjectDeclaration{{
		APIName: "Account",
		Fields: []fieldprovenance.FieldDeclaration{
			{APIName: "BillingAddress", IsCompound: true, ComposedFrom: []string{"BillingStreet", "BillingCity"}},
			{APIName: "BillingStreet", CompoundFieldName: "BillingAddress", CompoundComponentRole: "street"},
			{APIName: "BillingCity", CompoundFieldName: "BillingAddress", CompoundComponentRole: "city"},
		},
	}}, nil, nil))

	if isCompound, _ := fieldprovenance.ClassifyCompound("Account", "BillingAddress"); !isCompound {
		t.Fatal("the installed classifier does not report BillingAddress compound; the rest of this test would be vacuous")
	}

	compound := models.FieldMetadata{
		APIName: "BillingAddress", Label: "Billing Address", Type: constants.FieldTypeString,
		Createable: true, Updateable: true, Required: true,
	}
	if fieldRequiresUserInputOnCreate(&models.ObjectMetadata{APIName: "Account"}, &compound) {
		t.Error("a compound parent must be exempt from the predicate: its components carry the value")
	}

	// And the projection must honor that exemption, not re-derive the demand
	// from Required.
	stub := behaviorStub(t, map[string]map[string]string{"BillingAddress": {"create": "hidden"}})
	stub.schema.Fields = append(stub.schema.Fields, compound)
	stub.layout.Sections[0].Fields = append(stub.layout.Sections[0].Fields, "BillingAddress")
	if items := layoutItems(t, stub, "Create"); items["BillingAddress"] != nil {
		t.Error("BillingAddress is Required but compound, so the create path never demands it directly — " +
			"the clamp must allow hiding it")
	}
}

// Hidden must leave NO gap. With two columns and three fields the untouched
// grid is [A B][C]; hiding B must re-pack to [A C], not [A][C] and not [A _][C].
func TestHiddenFieldLeavesNoGapInTheGrid(t *testing.T) {
	before := layoutRows(t, behaviorStub(t, nil), "Create")
	if len(before) == 0 || len(before[0]) != 2 || len(before[0][0]) != 2 {
		t.Fatalf("baseline grid = %#v, want two rows with the first full", before)
	}

	after := layoutRows(t, behaviorStub(t, map[string]map[string]string{"Rating__c": {"create": "hidden"}}), "Create")
	if len(after) == 0 {
		t.Fatal("no sections after hiding")
	}
	if len(after[0]) != 1 {
		t.Fatalf("grid = %#v, want the two remaining fields packed into one row", after[0])
	}
	if len(after[0][0]) != 2 {
		t.Fatalf("first row = %v, want both remaining fields; a gap means hidden was flagged rather than removed", after[0][0])
	}
}

// Behavior is looked up per section. A single-section fixture cannot tell a
// correct implementation from one that reads section 0's map for every section.
func TestBehaviorIsLookedUpPerSectionNotFromTheFirst(t *testing.T) {
	stub := behaviorStub(t, nil)
	stub.layout.Sections = append(stub.layout.Sections, models.PageSection{
		ID: "s2", Label: "Second", Columns: 2,
		Fields:         []string{"Second__c", "SecondKept__c"},
		FieldBehaviors: map[string]map[string]string{"Second__c": {"create": "hidden"}},
	})
	stub.schema.Fields = append(stub.schema.Fields,
		models.FieldMetadata{APIName: "Second__c", Label: "Second", Type: constants.FieldTypeString, Createable: true, Updateable: true},
		models.FieldMetadata{APIName: "SecondKept__c", Label: "Second Kept", Type: constants.FieldTypeString, Createable: true, Updateable: true},
	)

	items := layoutItems(t, stub, "Create")
	if items["Second__c"] != nil {
		t.Error("Second__c is hidden by its OWN section's map but still reached the form")
	}
	if items["Rating__c"] == nil {
		t.Error("section 1 has no behaviors, but section 2's map removed one of its fields")
	}
	if items["SecondKept__c"] == nil {
		t.Error("SecondKept__c has no authored behavior, but hiding its section-mate removed it too")
	}
}

// Hiding every field of a section leaves the section with no rows rather than
// an item-shaped placeholder. Worth pinning: the form collapses a section only
// when every item in it is gone, so a placeholder would render an empty box.
func TestSectionWithEveryFieldHiddenEmitsNoRows(t *testing.T) {
	stub := behaviorStub(t, map[string]map[string]string{
		constants.FieldName: {"create": "hidden"},
		"Rating__c":         {"create": "hidden"},
	})
	// Taking Mandatory__c off the section no longer empties the create form:
	// the projection supplies a field the create path demands whether or not
	// the layout carries it. This test previously asserted the opposite, which
	// was the defect — a form that renders nothing while the save still insists
	// on a value.
	stub.layout.Sections[0].Fields = []string{constants.FieldName, "Rating__c"}

	rows := layoutRows(t, stub, "Create")
	if len(rows) == 0 {
		t.Fatal("no sections in the response")
	}
	var got []string
	for _, row := range rows[0] {
		got = append(got, row...)
	}
	if !slices.Equal(got, []string{"Mandatory__c"}) {
		t.Fatalf("create form = %v, want exactly the demanded field it was missing", got)
	}
}

func TestSectionWithNothingLeftToShowIsNotEmittedAtAll(t *testing.T) {
	// This test used to assert the opposite -- that the section shipped with
	// zero rows -- which is what the renderer turns into a heading bar over an
	// empty box. "Hidden is the item's ABSENCE" was already the rule for every
	// field-level exit; the section was where it stopped being applied.
	//
	// Reachable only when the schema demands nothing of the create path. With
	// a demanded field present the projection supplies it, the section has a
	// row, and it ships -- which the test above pins.
	stub := behaviorStub(t, map[string]map[string]string{
		constants.FieldName: {"create": "hidden"},
		"Rating__c":         {"create": "hidden"},
	})
	stub.schema.Fields = stub.schema.Fields[:2]
	stub.schemas = map[string]*models.ObjectMetadata{"Account": stub.schema}
	stub.layout.Sections[0].Fields = []string{constants.FieldName, "Rating__c"}

	// Stated rather than left to layoutRows, which happens to panic on a null
	// sections key. A helper that later tolerates null would silently un-pin
	// the wire shape; this says what the shape has to be.
	if sections, ok := layoutBody(t, stub, "Create")["sections"].([]any); !ok || len(sections) != 0 {
		t.Fatalf("sections = %#v, want an empty list (not null, not populated)",
			layoutBody(t, stub, "Create")["sections"])
	}
	// The same section is untouched on the forms it was not hidden for --
	// absence is per mode, so this must not become a way to delete a section.
	if got := len(layoutRows(t, stub, "Detail")); got != 1 {
		t.Fatalf("detail page emitted %d sections, want the one it still shows fields for", got)
	}
}

func TestSectionEmptiedByFieldLevelSecurityIsNotEmittedEither(t *testing.T) {
	// The population that needs no authoring at all, and the reason this is a
	// live defect rather than one the mode lens introduced: a user whose FLS
	// leaves nothing readable in a section takes the same absence exit, and
	// has been shown an empty box on the detail page all along.
	stub := behaviorStub(t, nil)
	stub.layout.Sections[0].Fields = []string{"Rating__c"}
	// Stated, not omitted: this stub fills in every schema field as readable
	// and treats fieldAccess as an override map, so leaving a field out makes
	// it VISIBLE here even though the projection's own lookup treats a missing
	// entry as unreadable. A test that relied on omission would be asserting
	// against a double more generous than the thing it stands in for.
	stub.fieldAccess = map[string]contract.FieldAccess{
		"Rating__c": {Readable: false},
	}

	if got := len(layoutRows(t, stub, "Detail")); got != 0 {
		t.Fatalf("detail page emitted %d sections for a caller who can read none of it, want none", got)
	}
}

func TestComponentSectionSurvivesHavingNoRows(t *testing.T) {
	// A component section carries no fields by design, so "no rows" is its
	// normal state rather than evidence that everything went absent. Dropping
	// it would delete the admin's arrangement rather than tidy it.
	//
	// It still renders as an empty box today, because this path never sends
	// componentName -- a separate defect, deliberately left alone here.
	// Given FIELDS, and all of them hidden -- otherwise this passes for the
	// wrong reason. A component section with no fields is already kept by the
	// "had fields" condition, so a nil-field fixture would leave the component
	// exemption itself unpinned.
	stub := behaviorStub(t, map[string]map[string]string{
		constants.FieldName: {"detail": "hidden"},
		"Rating__c":         {"detail": "hidden"},
	})
	component := "Component"
	stub.layout.Sections[0].Type = &component
	stub.layout.Sections[0].Fields = []string{constants.FieldName, "Rating__c"}

	if got := len(layoutRows(t, stub, "Detail")); got != 1 {
		t.Fatalf("component section count = %d, want it kept -- its content is the component, not the fields", got)
	}
}

func TestSectionKeepingOneVisibleFieldStillShips(t *testing.T) {
	stub := behaviorStub(t, map[string]map[string]string{
		constants.FieldName: {"create": "hidden"},
	})
	stub.layout.Sections[0].Fields = []string{constants.FieldName, "Rating__c"}

	rows := layoutRows(t, stub, "Create")
	if len(rows) != 1 {
		t.Fatalf("section count = %d, want the one that still shows a field", len(rows))
	}
}

// A lookup expansion takes a separate arm of the projection, which is exactly
// how it came to ignore authored behavior entirely. Hiding must reach it;
// "required" must not, because the expansion is not a writable field of this
// record and the save path would never enforce it.
func TestAuthoredBehaviorOnALookupExpansion(t *testing.T) {
	withExpansion := func(behaviors map[string]map[string]string) *uiAPITestContracts {
		stub := behaviorStub(t, behaviors)
		stub.schema.Fields = append(stub.schema.Fields, models.FieldMetadata{
			APIName: "OwnerId", Label: "Owner", Type: constants.FieldTypeReference,
			Createable: true, Updateable: true, ReferenceTo: []string{"User"},
		})
		// The expansion is only readable if the relationship resolves and the
		// target field passes the target object's own FLS, so the target
		// schema has to exist.
		stub.schemas["User"] = &models.ObjectMetadata{
			APIName: "User",
			Fields: []models.FieldMetadata{
				{APIName: constants.FieldName, Label: "Full Name", Type: constants.FieldTypeString, IsNameField: true},
			},
		}
		stub.layout.Sections[0].Fields = append(stub.layout.Sections[0].Fields, "OwnerId.Name")
		return stub
	}

	if items := layoutItems(t, withExpansion(nil), "Create"); items["OwnerId.Name"] == nil {
		t.Fatal("baseline: the expansion is missing without any authored behavior, so this test proves nothing")
	}

	hidden := map[string]map[string]string{"OwnerId.Name": {"create": "hidden"}}
	hiddenStub := withExpansion(hidden)
	hiddenStub.layout.Sections[0].FieldLabels = map[string]string{"OwnerId.Name": "Account Owner"}
	if items := layoutItems(t, hiddenStub, "Create"); items["OwnerId.Name"] != nil {
		t.Error("authored hidden did not reach the lookup-expansion arm")
	}
	// The expansion arm has its own label lookup, so "hidden = absence" has to
	// be made total there separately.
	labels, _ := layoutBody(t, hiddenStub, "Create")["sections"].([]any)[0].(map[string]any)["fieldLabels"].(map[string]any)
	if _, ok := labels["OwnerId.Name"]; ok {
		t.Errorf("the hidden expansion kept its custom label: %#v", labels)
	}
	// Scoped to the authored mode there too.
	if items := layoutItems(t, withExpansion(hidden), "Edit"); items["OwnerId.Name"] == nil {
		t.Error("create-hidden removed the expansion from Edit as well")
	}

	// The record page too. That arm was written before detail existed, and
	// leaving it out would give admins "I can hide Owner Name on the new form
	// but not on the record page" — a difference with no reason behind it.
	detailHidden := map[string]map[string]string{"OwnerId.Name": {"detail": "hidden"}}
	if items := layoutItems(t, withExpansion(detailHidden), "Detail"); items["OwnerId.Name"] != nil {
		t.Error("detail-hidden did not reach the lookup-expansion arm")
	}
	if items := layoutItems(t, withExpansion(detailHidden), "Create"); items["OwnerId.Name"] == nil {
		t.Error("detail-hidden leaked into the create form")
	}

	req := map[string]map[string]string{"OwnerId.Name": {"create": "required"}}
	items := layoutItems(t, withExpansion(req), "Create")
	if items["OwnerId.Name"] == nil {
		t.Fatal("authored required removed the expansion; it should be inert, not hiding")
	}
	if got, _ := items["OwnerId.Name"]["required"].(bool); got {
		t.Error("required must stay inert on an expansion: the save path does not enforce it, " +
			"so the form would demand something it can never be given")
	}
}

// The "detail" key is honored now, and only for the modes that render a record
// page. It was deliberately inert until app-packs#622 pointed the Setup editor
// at the authoring endpoint: the editor used to read this very response and
// write its reconstruction back, so an absent item was persisted as a deleted
// field — the live data-loss defect behind #3506. That precondition is not just
// believed, it is demonstrated by the round-trip fixpoint suite (#641).
func TestDetailHiddenAppliesOnlyToTheRecordPageModes(t *testing.T) {
	stub := behaviorStub(t, map[string]map[string]string{"Rating__c": {"detail": "hidden"}})

	// "" and "Detail" both resolve to the View mode: the record page and the
	// record-ui composite, which asks without naming a type, are one context.
	for _, mode := range []string{"", "Detail"} {
		if items := layoutItems(t, stub, mode); items["Rating__c"] != nil {
			t.Errorf("type=%q: detail-hidden did not reach the record page", mode)
		}
	}
	// And it stays out of the forms.
	for _, mode := range []string{"Create", "Edit"} {
		if items := layoutItems(t, stub, mode); items["Rating__c"] == nil {
			t.Errorf("type=%q: detail-hidden leaked into a form", mode)
		}
	}
}

// detail's vocabulary is shown|hidden. "required" is not in it, and the switch
// that applies behavior is shared across all three modes, so honoring detail
// without a per-mode vocabulary check would have made "required" start working
// on the record page — a marker with nothing behind it. An authored required is
// enforced only by the surface that renders it, and the record page's inline
// editor never refuses a save for being empty; the save path's own check reads
// the field's schema Required and never consults a layout. See
// authoredBehaviorFor.
func TestDetailRequiredIsInert(t *testing.T) {
	stub := behaviorStub(t, map[string]map[string]string{"Rating__c": {"detail": "required"}})

	// Every mode, not just the record page. The test this replaced covered
	// required against all four, and a detail-scoped key reaching a FORM is the
	// worse half: the create form would demand a field on the authority of a
	// key that names the record page.
	for _, mode := range []string{"", "Detail", "Create", "Edit"} {
		item := layoutItems(t, stub, mode)["Rating__c"]
		if item == nil {
			t.Fatalf("type=%q: an out-of-vocabulary value removed the item; it must resolve to shown", mode)
		}
		if required, _ := item["required"].(bool); required {
			t.Errorf("type=%q: a detail-scoped required reached this mode", mode)
		}
	}
}

// An unmapped mode resolves to nothing, and specifically does NOT fall back to
// the empty key.
//
// Every mode the routes can produce is mapped today — "" and "Detail" fold to
// View, and Create and Edit pass through — so this branch is currently
// unreachable through the handler and no rendered-item test can reach it. It is
// kept and tested here because the map lookup is by string: without it a future
// mode would index behaviors[field][""], and an entry stored under "" would
// then match it. Nothing on the save path validates the vocabulary, so such an
// entry can exist.
func TestAnUnmappedModeResolvesToNothing(t *testing.T) {
	behaviors := map[string]map[string]string{"Rating__c": {"": "hidden", "detail": "hidden"}}

	if got := authoredBehaviorFor(behaviors, "Rating__c", "SomeFutureMode"); got != "" {
		t.Errorf("unmapped mode resolved %q; it must not reach the empty key", got)
	}
	// The mapped modes are unaffected by that protection.
	if got := authoredBehaviorFor(behaviors, "Rating__c", uiAPILayoutModeView); got != authoredBehaviorHidden {
		t.Errorf("View resolved %q, want hidden", got)
	}
}

// The vocabulary check itself, not just its outcome.
//
// Removing it is unobservable through the projection today, because the
// "required" arm happens to handle only Create and Edit and lets View fall
// through untouched. That is a coincidence of two independent decisions, and
// the check exists so the outcome does not depend on it: with the check, adding
// a View case to that arm cannot make the record page demand a field. Testing
// only the rendered item would leave the rule unpinned, so it is tested where
// it is decided.
func TestDetailVocabularyRejectsRequired(t *testing.T) {
	behaviors := map[string]map[string]string{"Rating__c": {
		"create": "required", "edit": "required", "detail": "required",
	}}

	if got := authoredBehaviorFor(behaviors, "Rating__c", uiAPILayoutModeView); got != "" {
		t.Errorf("detail resolved %q; its vocabulary is shown|hidden, so required must fall through to shown", got)
	}
	// And the check is scoped to detail — the other two modes still accept it.
	if got := authoredBehaviorFor(behaviors, "Rating__c", uiAPILayoutModeCreate); got != authoredBehaviorRequired {
		t.Errorf("create resolved %q, want required", got)
	}
	if got := authoredBehaviorFor(behaviors, "Rating__c", uiAPILayoutModeEdit); got != authoredBehaviorRequired {
		t.Errorf("edit resolved %q, want required", got)
	}
	// hidden is in every mode's vocabulary.
	hidden := map[string]map[string]string{"Rating__c": {"detail": "hidden"}}
	if got := authoredBehaviorFor(hidden, "Rating__c", uiAPILayoutModeView); got != authoredBehaviorHidden {
		t.Errorf("detail resolved %q, want hidden", got)
	}
}

// A field the create path demands is clamped back into the create form, and
// that clamp is scoped to create. Hiding it on the record page has no save to
// break, so it must be allowed — otherwise a required field is un-hideable on
// the one screen where requiring it means nothing.
func TestDetailHidingIsNotClamped(t *testing.T) {
	stub := behaviorStub(t, map[string]map[string]string{"Mandatory__c": {"detail": "hidden"}})

	if items := layoutItems(t, stub, "Detail"); items["Mandatory__c"] != nil {
		t.Error("the create-path clamp leaked into the record page")
	}
	// Checked on a field the clamp does NOT rescue. Asserting this on
	// Mandatory__c looked like a leak check but could never fail from one: the
	// create-path clamp puts that field back whichever mode key hid it.
	leak := behaviorStub(t, map[string]map[string]string{"Rating__c": {"detail": "hidden"}})
	if items := layoutItems(t, leak, "Create"); items["Rating__c"] == nil {
		t.Error("hiding on the record page removed the field from the create form too")
	}
}

// Edit is half the shipped vocabulary and was certified by nothing: every
// Edit-mode assertion elsewhere in this file is a non-effect assertion
// (create-scoped behavior must not leak into Edit), and those all stay true if
// Edit support is deleted outright.
func TestAuthoredBehaviorAppliesToEditMode(t *testing.T) {
	hidden := behaviorStub(t, map[string]map[string]string{"Rating__c": {"edit": "hidden"}})
	if items := layoutItems(t, hidden, "Edit"); items["Rating__c"] != nil {
		t.Error("edit-hidden did not reach the Edit response")
	}
	if items := layoutItems(t, hidden, "Create"); items["Rating__c"] == nil {
		t.Error("edit-hidden leaked into Create")
	}

	required := behaviorStub(t, map[string]map[string]string{"Rating__c": {"edit": "required"}})
	items := layoutItems(t, required, "Edit")
	if items["Rating__c"] == nil {
		t.Fatal("edit-required removed the item; it must widen, not hide")
	}
	if got, _ := items["Rating__c"]["required"].(bool); !got {
		t.Error("edit-required did not set required in the Edit response")
	}
	if got, _ := layoutItems(t, required, "Create")["Rating__c"]["required"].(bool); got {
		t.Error("edit-required leaked into Create")
	}
}

// FLS is the floor: authoring may narrow what survives it and may never widen.
// The previous version of this guard authored "shown", which is not a case in
// the switch, so it exercised the unrecognized-vocabulary fall-through where
// none of the new code runs — it stayed green even with the whole authored
// block deleted.
func TestAuthoredRequiredCannotWidenFieldLevelSecurity(t *testing.T) {
	stub := behaviorStub(t, map[string]map[string]string{"Rating__c": {"create": "required", "edit": "required"}})
	stub.fieldAccess = map[string]contract.FieldAccess{
		"Rating__c": {Readable: true, Createable: false, Updateable: false},
	}
	for _, mode := range []string{"Create", "Edit"} {
		item := layoutItems(t, stub, mode)["Rating__c"]
		if item == nil {
			t.Fatalf("type=%s: the item vanished; required must widen, not hide", mode)
		}
		if got, _ := item["editableForNew"].(bool); got {
			t.Errorf("type=%s: authored required made an FLS-uncreateable field editable for new", mode)
		}
		if got, _ := item["editableForUpdate"].(bool); got {
			t.Errorf("type=%s: authored required made an FLS-unupdateable field editable for update", mode)
		}
		if got, _ := item["required"].(bool); got {
			t.Errorf("type=%s: authored required marked a field the caller may not write, "+
				"which is an obligation they cannot discharge", mode)
		}
	}
}

// The clamp exists because the CREATE path demands the field. There is no
// update-side required-validator, so hiding the same field on Edit cannot
// produce the invisible-field save failure the clamp prevents — and must
// therefore be allowed, or admins cannot hide it anywhere.
func TestTheClampIsScopedToCreateOnly(t *testing.T) {
	stub := behaviorStub(t, map[string]map[string]string{"Mandatory__c": {"create": "hidden", "edit": "hidden"}})
	if items := layoutItems(t, stub, "Create"); items["Mandatory__c"] == nil {
		t.Error("the create clamp did not rescue a field the create path demands")
	}
	if items := layoutItems(t, stub, "Edit"); items["Mandatory__c"] != nil {
		t.Error("the create-path clamp leaked into Edit, making a required field un-hideable there")
	}
}

// The split left fieldSatisfiedByNameComponents in the validator because it
// asks what a particular request supplied. Nothing in the package covered it:
// deleting the call left everything green.
//
// Reaching it takes a specific shape, which is why the obvious fixture misses.
// The predicate exempts a compound parent by asking the CLASSIFIER, while
// compoundMemberNames also accepts a parent the SCHEMA declares compound
// (search.go:1240 before it consults the classifier at :1243). So the branch is
// live exactly when the schema declares the compound and the classifier does
// not — the state of any process where bootstrap installed no declaration for
// that object. With the classifier agreeing, the predicate exempts Name first
// and this code never runs.
//
// Ordering is unchanged by the extraction: the original ran the compound
// exemption before this check too, so moving it inside the predicate preserved
// the sequence.
func TestNameComponentsSatisfyRequiredNameInTheValidator(t *testing.T) {
	previous := fieldprovenance.Default()
	t.Cleanup(func() { fieldprovenance.SetDefault(previous) })
	// Deliberately empty: the classifier must NOT know Contact, or the
	// predicate exempts Name and this branch is unreachable.
	fieldprovenance.SetDefault(fieldprovenance.BuildClassifier(nil, nil, nil, nil))

	schema := &models.ObjectMetadata{
		APIName: "Contact",
		Fields: []models.FieldMetadata{
			{
				APIName: constants.FieldName, Type: constants.FieldTypeString, Required: true,
				IsNameField: true, IsCompound: true, ComposedFrom: []string{"FirstName", "LastName"},
			},
			{APIName: "FirstName", Type: constants.FieldTypeString},
			{APIName: "LastName", Type: constants.FieldTypeString},
		},
	}
	name := schema.Fields[0]
	if !fieldRequiresUserInputOnCreate(schema, &name) {
		t.Fatal("the predicate already exempts Name, so the validator never reaches the components check " +
			"and the rest of this test would be vacuous")
	}
	if names := compoundMemberNames(schema, constants.FieldName); len(names) == 0 {
		t.Fatal("Name has no components under this schema; the rest of this test would be vacuous")
	}

	if err := validateRequiredForCreate(schema, map[string]interface{}{"LastName": "Rivera"}); err != nil {
		t.Errorf("a component satisfies the required compound name, but the validator rejected it: %v", err)
	}
	if err := validateRequiredForCreate(schema, map[string]interface{}{"FirstName": "Ana"}); err != nil {
		t.Errorf("any one component should satisfy it, not just the last: %v", err)
	}
	if err := validateRequiredForCreate(schema, map[string]interface{}{"LastName": "   "}); err == nil {
		t.Error("a whitespace-only component satisfied the required name")
	}
	if err := validateRequiredForCreate(schema, map[string]interface{}{}); err == nil {
		t.Error("an empty payload satisfied the required name")
	}
}

// A clone POSTs as a create, so its seeded layout must be the Create
// projection: the clamp is what keeps a create-demanded field from being
// authored out of a form whose save will then demand it.
func TestCloneDefaultsProjectsCreateNotEdit(t *testing.T) {
	stub := behaviorStub(t, map[string]map[string]string{
		"Mandatory__c": {"edit": "hidden"},
		"Rating__c":    {"create": "hidden"},
	})
	stub.records = map[string]contract.Record{
		"001000000000001": {"Id": "001000000000001", "Name": "Acme", "Rating__c": "Hot"},
	}
	req := &dialect.Request{
		Params: map[string]string{"recordId": "001000000000001"},
		Query:  map[string][]string{"type": {"Account"}},
	}
	resp, err := UIAPIRecordDefaultsCloneNeutralHandler(context.Background(), req, uiAPIDeps(stub, nil))
	if err != nil {
		t.Fatalf("handler error: %v", err)
	}
	if resp.Status != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}
	body := decodeResponseBody(t, resp.Body)
	layout, _ := body["layout"].(map[string]any)
	if layout == nil {
		t.Fatalf("no layout in the clone defaults: %#v", body)
	}
	if got, _ := layout["mode"].(string); got != "Create" {
		t.Errorf("clone defaults projected mode=%q, want Create — a clone's save is a create, "+
			"so an Edit projection skips the create-path clamp", got)
	}

	names := map[string]bool{}
	sections, _ := layout["sections"].([]any)
	for _, section := range sections {
		rows, _ := section.(map[string]any)["layoutRows"].([]any)
		for _, row := range rows {
			items, _ := row.(map[string]any)["layoutItems"].([]any)
			for _, item := range items {
				comps, _ := item.(map[string]any)["layoutComponents"].([]any)
				for _, c := range comps {
					if n, _ := c.(map[string]any)["apiName"].(string); n != "" {
						names[n] = true
					}
				}
			}
		}
	}
	if !names["Mandatory__c"] {
		t.Error("a field the create path demands is missing from the clone form; its save would fail naming it")
	}
	if names["Rating__c"] {
		t.Error("create-authored hiding was ignored, so this is still the Edit projection")
	}
}

// "hidden = absence" has to be total: a custom label left behind for an item
// that is not in the response is a dangling half. The FLS path already drops
// labels for the fields it removes.
func TestHiddenFieldLeavesNoLabelBehind(t *testing.T) {
	stub := behaviorStub(t, map[string]map[string]string{"Rating__c": {"create": "hidden"}})
	stub.layout.Sections[0].FieldLabels = map[string]string{
		"Rating__c":         "Customer Tier",
		constants.FieldName: "Company",
	}
	body := layoutBody(t, stub, "Create")
	section := body["sections"].([]any)[0].(map[string]any)
	labels, _ := section["fieldLabels"].(map[string]any)
	if _, ok := labels["Rating__c"]; ok {
		t.Errorf("the hidden field kept its custom label: %#v", labels)
	}
	if got, _ := labels[constants.FieldName].(string); got != "Company" {
		t.Errorf("pruning the hidden label removed a surviving field's label too: %#v", labels)
	}
	// And the stored layout must not have been mutated in the process.
	if stub.layout.Sections[0].FieldLabels["Rating__c"] != "Customer Tier" {
		t.Error("pruning deleted from the stored layout's own map, not a projection of it")
	}
}

// The record page and the record-ui composite are one rendering context.
//
// record-ui asks for a layout without naming a type, and the layout route folds
// both "" and "Detail" onto the View mode, so they land on the same projection.
// That is the commit's headline claim and every other test here drives the
// layout route only — leaving the claim true by code reading and held by
// nothing. Without this, changing what record-ui requests produces a field that
// is hidden or not depending on which fetch loaded the page, with the suite
// green.
func TestRecordUIHonorsDetailHidingLikeTheLayoutRoute(t *testing.T) {
	stub := behaviorStub(t, map[string]map[string]string{"Rating__c": {"detail": "hidden"}})
	stub.records = map[string]contract.Record{
		"001000000000001": {"Id": "001000000000001", "Name": "Acme", "Rating__c": "Hot"},
	}

	resp, err := UIAPIRecordUINeutralHandler(context.Background(), &dialect.Request{
		Version: "v63.0",
		Params:  map[string]string{"recordIds": "001000000000001"},
		Query:   map[string][]string{"objectApiName": {"Account"}},
	}, uiAPIDeps(stub, nil))
	if err != nil {
		t.Fatalf("handler error: %v", err)
	}
	if resp.Status != http.StatusOK {
		t.Fatalf("status = %d, want 200; body=%#v", resp.Status, resp.Body)
	}

	names := recordUILayoutFieldNames(t, decodeResponseBody(t, resp.Body))
	if len(names) == 0 {
		t.Fatal("record-ui returned no layout items, so this proves nothing about hiding")
	}
	if names["Rating__c"] {
		t.Error("record-ui rendered a field the record page hides; the two fetches disagree")
	}
	if !names[constants.FieldName] {
		t.Error("record-ui dropped a field nothing hides")
	}
}

func recordUILayoutFieldNames(t *testing.T, body map[string]any) map[string]bool {
	t.Helper()
	out := map[string]bool{}
	layouts, _ := body["layouts"].(map[string]any)
	for _, byObject := range layouts {
		for _, layout := range byObject.(map[string]any) {
			sections, _ := layout.(map[string]any)["sections"].([]any)
			for _, section := range sections {
				rows, _ := section.(map[string]any)["layoutRows"].([]any)
				for _, row := range rows {
					items, _ := row.(map[string]any)["layoutItems"].([]any)
					for _, item := range items {
						comps, _ := item.(map[string]any)["layoutComponents"].([]any)
						for _, c := range comps {
							if n, _ := c.(map[string]any)["apiName"].(string); n != "" {
								out[n] = true
							}
						}
					}
				}
			}
		}
	}
	return out
}

func decodeResponseBody(t *testing.T, body any) map[string]any {
	t.Helper()
	encoded, err := json.Marshal(body)
	if err != nil {
		t.Fatalf("marshal response body: %v", err)
	}
	var out map[string]any
	if err := json.Unmarshal(encoded, &out); err != nil {
		t.Fatalf("unmarshal response body: %v", err)
	}
	return out
}
