package metadata

import (
	"context"
	"errors"
	"strings"
	"testing"

	apperrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/ports"
	domainSchema "github.com/nexuscrm/kernel/pkg/schema"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/formulastorage"
	"github.com/nexuscrm/shared/pkg/models"
)

func TestCreateFieldAddsActiveFormulaToLayouts(t *testing.T) {
	repo := newFormulaLayoutMetadataRepo()
	schemaRepo := &formulaLayoutSchemaRepo{}
	service := NewMetadataService(repo, NewSchemaManager(schemaRepo), nil)

	formula := "Amount__c * 2"
	returnType := constants.FieldTypeCurrency
	field := &models.FieldMetadata{
		APIName:    "DoubleAmount__c",
		Label:      "Double Amount",
		Type:       constants.FieldTypeFormula,
		Formula:    &formula,
		ReturnType: &returnType,
	}

	if err := service.CreateField(context.Background(), "Issue", field); err != nil {
		t.Fatalf("CreateField: %v", err)
	}
	if len(schemaRepo.registeredOnly) != 1 {
		t.Fatalf("registeredOnly = %d, want 1 metadata-only formula registration", len(schemaRepo.registeredOnly))
	}
	if !layoutHasField(repo.layouts[0], "DoubleAmount__c") {
		t.Fatalf("active formula was not added to layout: %+v", repo.layouts[0].Sections)
	}
	if len(repo.savedLayouts) != 1 {
		t.Fatalf("savedLayouts = %d, want 1", len(repo.savedLayouts))
	}
}

func TestBatchCreateFieldsAddsActiveFormulaToLayouts(t *testing.T) {
	repo := newFormulaLayoutMetadataRepo()
	schemaRepo := &formulaLayoutSchemaRepo{}
	service := NewMetadataService(repo, NewSchemaManager(schemaRepo), nil)

	formula := "Amount__c * 2"
	returnType := constants.FieldTypeCurrency
	fields := []models.FieldMetadata{{
		APIName:    "DoubleAmount__c",
		Label:      "Double Amount",
		Type:       constants.FieldTypeFormula,
		Formula:    &formula,
		ReturnType: &returnType,
	}}

	created, err := service.BatchCreateFields(context.Background(), "Issue", fields)
	if err != nil {
		t.Fatalf("BatchCreateFields: %v", err)
	}
	if len(created) != 1 {
		t.Fatalf("created = %d, want 1", len(created))
	}
	if len(schemaRepo.batchFields) != 1 {
		t.Fatalf("batchFields = %d, want 1", len(schemaRepo.batchFields))
	}
	if schemaRepo.batchFields[0].FormulaMode == nil || *schemaRepo.batchFields[0].FormulaMode != "active" {
		t.Fatalf("batch formula mode = %v, want active", schemaRepo.batchFields[0].FormulaMode)
	}
	if !layoutHasField(repo.layouts[0], "DoubleAmount__c") {
		t.Fatalf("active formula was not added to layout: %+v", repo.layouts[0].Sections)
	}
	if len(repo.savedLayouts) != 1 {
		t.Fatalf("savedLayouts = %d, want 1", len(repo.savedLayouts))
	}
}

func TestBatchCreateFieldsRejectsFormulaExpressionOnNonFormulaType(t *testing.T) {
	repo := newFormulaLayoutMetadataRepo()
	schemaRepo := &formulaLayoutSchemaRepo{}
	service := NewMetadataService(repo, NewSchemaManager(schemaRepo), nil)

	formula := "Name"
	mode := "active"
	fields := []models.FieldMetadata{{
		APIName:     "BadFormulaAsText__c",
		Label:       "Bad Formula As Text",
		Type:        constants.FieldTypeString,
		Formula:     &formula,
		FormulaMode: &mode,
	}}

	created, err := service.BatchCreateFields(context.Background(), "Issue", fields)
	if err == nil {
		t.Fatal("BatchCreateFields = nil, want non-formula type rejection")
	}
	if len(created) != 0 {
		t.Fatalf("created = %d, want 0", len(created))
	}
	if len(schemaRepo.batchFields) != 0 {
		t.Fatalf("batchFields = %d, want 0 because validation must fail before DDL", len(schemaRepo.batchFields))
	}
	if len(repo.savedLayouts) != 0 {
		t.Fatalf("savedLayouts = %d, want 0 because validation must fail before layout work", len(repo.savedLayouts))
	}
	for _, want := range []string{
		"field.formula.expression_on_non_formula_type",
		`object "Issue"`,
		`field "BadFormulaAsText__c"`,
		`type "string"`,
		`expected type "formula"`,
	} {
		if !strings.Contains(err.Error(), want) {
			t.Fatalf("BatchCreateFields error = %v, want %q", err, want)
		}
	}
}

func TestCreateFieldRejectsAggregateFormulaAuthoring(t *testing.T) {
	repo := newFormulaLayoutMetadataRepo()
	schemaRepo := &formulaLayoutSchemaRepo{}
	service := NewMetadataService(repo, NewSchemaManager(schemaRepo), nil)

	formula := "SUM(WorkLog__c.Hours__c)"
	returnType := constants.FieldTypeCurrency
	SetAggregateFormulaDetector(func(expr string) (bool, error) {
		return expr == formula, nil
	})
	t.Cleanup(func() { SetAggregateFormulaDetector(nil) })
	field := &models.FieldMetadata{
		APIName:    "LoggedHours__c",
		Label:      "Logged Hours",
		Type:       constants.FieldTypeFormula,
		Formula:    &formula,
		ReturnType: &returnType,
	}

	err := service.CreateField(context.Background(), "Issue", field)
	if err == nil {
		t.Fatal("CreateField = nil, want aggregate authoring rejection")
	}
	if len(schemaRepo.registeredOnly) != 0 {
		t.Fatalf("registeredOnly = %d, want 0 because aggregate authoring must fail before metadata registration", len(schemaRepo.registeredOnly))
	}
	if len(repo.savedLayouts) != 0 {
		t.Fatalf("savedLayouts = %d, want 0 because aggregate authoring must fail before layout work", len(repo.savedLayouts))
	}
	if !strings.Contains(err.Error(), formulaAggregateAuthoringRequiresRollupReason) {
		t.Fatalf("CreateField error = %q, want aggregate authoring rejection", err.Error())
	}
}

func TestBatchCreateFieldsRejectsAggregateFormulaAuthoring(t *testing.T) {
	repo := newFormulaLayoutMetadataRepo()
	schemaRepo := &formulaLayoutSchemaRepo{}
	service := NewMetadataService(repo, NewSchemaManager(schemaRepo), nil)

	formula := "SUM(WorkLog__c.Hours__c)"
	returnType := constants.FieldTypeCurrency
	SetAggregateFormulaDetector(func(expr string) (bool, error) {
		return expr == formula, nil
	})
	t.Cleanup(func() { SetAggregateFormulaDetector(nil) })
	fields := []models.FieldMetadata{{
		APIName:    "LoggedHours__c",
		Label:      "Logged Hours",
		Type:       constants.FieldTypeFormula,
		Formula:    &formula,
		ReturnType: &returnType,
	}}

	created, err := service.BatchCreateFields(context.Background(), "Issue", fields)
	if err == nil {
		t.Fatal("BatchCreateFields = nil, want aggregate authoring rejection")
	}
	if len(created) != 0 {
		t.Fatalf("created = %d, want 0", len(created))
	}
	if len(schemaRepo.batchFields) != 0 {
		t.Fatalf("batchFields = %d, want 0 because aggregate authoring must fail before DDL", len(schemaRepo.batchFields))
	}
	if len(repo.savedLayouts) != 0 {
		t.Fatalf("savedLayouts = %d, want 0 because aggregate authoring must fail before layout work", len(repo.savedLayouts))
	}
	if !strings.Contains(err.Error(), formulaAggregateAuthoringRequiresRollupReason) {
		t.Fatalf("BatchCreateFields error = %q, want aggregate authoring rejection", err.Error())
	}
}

func TestCreateFieldRejectsMalformedWriteMaterializedFormula(t *testing.T) {
	repo := newFormulaLayoutMetadataRepo()
	schemaRepo := &formulaLayoutSchemaRepo{}
	service := NewMetadataService(repo, NewSchemaManager(schemaRepo), nil)

	formulaExpr := "TEXT("
	formulaMode := "active"
	storageMode := formulastorage.ModeWriteMaterialized
	returnType := constants.FieldTypeString
	field := &models.FieldMetadata{
		APIName:            "NameStored__c",
		Label:              "Name Stored",
		Type:               constants.FieldTypeFormula,
		Formula:            &formulaExpr,
		FormulaMode:        &formulaMode,
		FormulaStorageMode: &storageMode,
		ReturnType:         &returnType,
	}

	err := service.CreateField(context.Background(), "Issue", field)
	if err == nil {
		t.Fatal("CreateField = nil, want write-materialized formula validation rejection")
	}
	if len(schemaRepo.registeredOnly) != 0 {
		t.Fatalf("registeredOnly = %d, want 0 because malformed write-materialized formula must fail before metadata registration", len(schemaRepo.registeredOnly))
	}
	if len(repo.savedLayouts) != 0 {
		t.Fatalf("savedLayouts = %d, want 0 because malformed write-materialized formula must fail before layout work", len(repo.savedLayouts))
	}
	for _, want := range []string{"write-materialized formula validation failed", "field \"NameStored__c\""} {
		if !strings.Contains(err.Error(), want) {
			t.Fatalf("CreateField error = %q, want %q", err.Error(), want)
		}
	}
}

func TestBatchCreateFieldsRejectsMalformedWriteMaterializedFormula(t *testing.T) {
	repo := newFormulaLayoutMetadataRepo()
	schemaRepo := &formulaLayoutSchemaRepo{}
	service := NewMetadataService(repo, NewSchemaManager(schemaRepo), nil)

	formulaExpr := "TEXT("
	formulaMode := "active"
	storageMode := formulastorage.ModeWriteMaterialized
	returnType := constants.FieldTypeString
	fields := []models.FieldMetadata{{
		APIName:            "NameStored__c",
		Label:              "Name Stored",
		Type:               constants.FieldTypeFormula,
		Formula:            &formulaExpr,
		FormulaMode:        &formulaMode,
		FormulaStorageMode: &storageMode,
		ReturnType:         &returnType,
	}}

	created, err := service.BatchCreateFields(context.Background(), "Issue", fields)
	if err == nil {
		t.Fatal("BatchCreateFields = nil, want write-materialized formula validation rejection")
	}
	if len(created) != 0 {
		t.Fatalf("created = %d, want 0", len(created))
	}
	if len(schemaRepo.batchFields) != 0 {
		t.Fatalf("batchFields = %d, want 0 because malformed write-materialized formula must fail before DDL", len(schemaRepo.batchFields))
	}
	if len(repo.savedLayouts) != 0 {
		t.Fatalf("savedLayouts = %d, want 0 because malformed write-materialized formula must fail before layout work", len(repo.savedLayouts))
	}
	for _, want := range []string{"write-materialized formula validation failed", "field \"NameStored__c\""} {
		if !strings.Contains(err.Error(), want) {
			t.Fatalf("BatchCreateFields error = %q, want %q", err.Error(), want)
		}
	}
}

type formulaLayoutMetadataRepo struct {
	ports.MetadataRepository

	schema          *models.ObjectMetadata
	schemas         map[string]*models.ObjectMetadata
	layouts         []*models.PageLayout
	layoutsByObject map[string][]*models.PageLayout
	savedLayouts    []*models.PageLayout
	saveErr         error
	saveErrs        []error
	restoreErrs     []error
	restoreCalls    int
}

func newFormulaLayoutMetadataRepo() *formulaLayoutMetadataRepo {
	return &formulaLayoutMetadataRepo{
		schema: &models.ObjectMetadata{
			APIName: "Issue",
			Fields: []models.FieldMetadata{
				{APIName: "Name", Label: "Name", Type: constants.FieldTypeString},
			},
		},
		layouts: []*models.PageLayout{{
			ID:            "layout_issue",
			ObjectAPIName: "Issue",
			Sections: []models.PageSection{{
				ID:      "section_details",
				Label:   "Details",
				Columns: 2,
				Fields:  []string{"Name"},
			}},
		}},
	}
}

func (r *formulaLayoutMetadataRepo) GetSchemaByAPIName(ctx context.Context, apiName string) (*models.ObjectMetadata, error) {
	if len(r.schemas) > 0 {
		return r.schemas[strings.ToLower(apiName)], nil
	}
	return r.schema, nil
}

func (r *formulaLayoutMetadataRepo) GetAllSchemas(ctx context.Context) ([]*models.ObjectMetadata, error) {
	if len(r.schemas) > 0 {
		schemas := make([]*models.ObjectMetadata, 0, len(r.schemas))
		for _, schema := range r.schemas {
			schemas = append(schemas, schema)
		}
		return schemas, nil
	}
	return []*models.ObjectMetadata{r.schema}, nil
}

func (r *formulaLayoutMetadataRepo) GetLayouts(ctx context.Context, objectAPIName string) ([]*models.PageLayout, error) {
	if len(r.layoutsByObject) > 0 {
		return r.layoutsByObject[strings.ToLower(objectAPIName)], nil
	}
	return r.layouts, nil
}

func (r *formulaLayoutMetadataRepo) SaveLayout(ctx context.Context, layout *models.PageLayout) error {
	if len(r.saveErrs) > 0 {
		err := r.saveErrs[0]
		r.saveErrs = r.saveErrs[1:]
		if err != nil {
			return err
		}
	}
	if r.saveErr != nil {
		return r.saveErr
	}
	r.savedLayouts = append(r.savedLayouts, cloneTestPageLayout(layout))
	return nil
}

func (r *formulaLayoutMetadataRepo) SnapshotLayoutForFieldAdmission(ctx context.Context, layoutID string) (func(context.Context, *models.PageLayout) error, error) {
	original := cloneTestPageLayout(r.findLayoutByID(layoutID))
	if original == nil {
		return nil, errors.New("layout not found")
	}
	return func(context.Context, *models.PageLayout) error {
		r.restoreCalls++
		if len(r.restoreErrs) > 0 {
			restoreErr := r.restoreErrs[0]
			r.restoreErrs = r.restoreErrs[1:]
			if restoreErr != nil {
				return restoreErr
			}
		}
		return r.replaceLayoutByID(layoutID, cloneTestPageLayout(original))
	}, nil
}

func (r *formulaLayoutMetadataRepo) SaveLayoutForFieldAdmission(ctx context.Context, original, updated *models.PageLayout) (func(context.Context, *models.PageLayout) error, error) {
	restore, err := r.SnapshotLayoutForFieldAdmission(ctx, original.ID)
	if err != nil {
		return nil, err
	}
	if err := r.SaveLayout(ctx, updated); err != nil {
		return nil, err
	}
	return restore, nil
}

func newRelationshipLayoutMetadataRepo() *formulaLayoutMetadataRepo {
	child := &models.ObjectMetadata{
		APIName:     "Line__c",
		Label:       "Line",
		PluralLabel: "Lines",
		IsCustom:    true,
		Fields: []models.FieldMetadata{
			{APIName: "Name", Label: "Name", Type: constants.FieldTypeString, IsNameField: true},
			{APIName: "Status__c", Label: "Status", Type: constants.FieldTypeString},
		},
		ListFields: []string{"Name", "Status__c"},
	}
	parent := &models.ObjectMetadata{
		APIName:     "Project__c",
		Label:       "Project",
		PluralLabel: "Projects",
		IsCustom:    true,
		Fields: []models.FieldMetadata{
			{APIName: "Name", Label: "Name", Type: constants.FieldTypeString, IsNameField: true},
		},
	}

	return &formulaLayoutMetadataRepo{
		schemas: map[string]*models.ObjectMetadata{
			"line__c":    child,
			"project__c": parent,
		},
		layoutsByObject: map[string][]*models.PageLayout{
			"line__c": {{
				ID:            "layout_line",
				ObjectAPIName: "Line__c",
				LayoutName:    "Line__c-default-layout",
				Source:        "admin_created",
				IsCustomized:  true,
				Sections: []models.PageSection{{
					ID:      "section_line",
					Label:   "Details",
					Columns: 2,
					Fields:  []string{"Name"},
				}},
			}},
			"project__c": {{
				ID:            "layout_project",
				ObjectAPIName: "Project__c",
				LayoutName:    "Project__c-default-layout",
				Source:        "admin_created",
				IsCustomized:  true,
				Sections: []models.PageSection{{
					ID:      "section_project",
					Label:   "Details",
					Columns: 2,
					Fields:  []string{"Name"},
				}},
			}},
		},
	}
}

func relatedListByIdentity(layout *models.PageLayout, childAPI string, lookupField string) (models.RelatedListConfig, bool) {
	if layout == nil {
		return models.RelatedListConfig{}, false
	}
	key := relatedListKey(childAPI, lookupField)
	for _, rl := range layout.RelatedLists {
		if relatedListKey(rl.ObjectAPIName, rl.LookupField) == key {
			return rl, true
		}
	}
	return models.RelatedListConfig{}, false
}

func (r *formulaLayoutMetadataRepo) findLayoutByID(layoutID string) *models.PageLayout {
	for _, layout := range r.layouts {
		if layout != nil && layout.ID == layoutID {
			return layout
		}
	}
	for _, layouts := range r.layoutsByObject {
		for _, layout := range layouts {
			if layout != nil && layout.ID == layoutID {
				return layout
			}
		}
	}
	return nil
}

func (r *formulaLayoutMetadataRepo) replaceLayoutByID(layoutID string, replacement *models.PageLayout) error {
	for i := range r.layouts {
		if r.layouts[i] != nil && r.layouts[i].ID == layoutID {
			r.layouts[i] = replacement
			return nil
		}
	}
	for objectAPIName, layouts := range r.layoutsByObject {
		for i := range layouts {
			if layouts[i] != nil && layouts[i].ID == layoutID {
				r.layoutsByObject[objectAPIName][i] = replacement
				return nil
			}
		}
	}
	return errors.New("layout not found")
}

func TestCreateFieldAddsRelationshipRelatedListToParentLayout(t *testing.T) {
	repo := newRelationshipLayoutMetadataRepo()
	schemaRepo := &formulaLayoutSchemaRepo{}
	service := NewMetadataService(repo, NewSchemaManager(schemaRepo), nil)

	err := service.CreateField(context.Background(), "Line__c", &models.FieldMetadata{
		APIName:     "Project__c",
		Label:       "Project",
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"Project__c"},
	})
	if err != nil {
		t.Fatalf("CreateField: %v", err)
	}

	childLayout := repo.layoutsByObject["line__c"][0]
	if !layoutHasField(childLayout, "Project__c") {
		t.Fatalf("child layout missing lookup field: %+v", childLayout.Sections)
	}

	parentLayout := repo.layoutsByObject["project__c"][0]
	relatedList, ok := relatedListByIdentity(parentLayout, "Line__c", "Project__c")
	if !ok {
		t.Fatalf("parent layout missing related list: %+v", parentLayout.RelatedLists)
	}
	if relatedList.ID != "Line__c-Project__c" {
		t.Fatalf("relatedList.ID = %q, want Line__c-Project__c", relatedList.ID)
	}
	if relatedList.Label != "Lines" {
		t.Fatalf("relatedList.Label = %q, want Lines", relatedList.Label)
	}
	if relatedList.SecondaryLabel != "via Project" {
		t.Fatalf("relatedList.SecondaryLabel = %q, want via Project", relatedList.SecondaryLabel)
	}
	if strings.Join(relatedList.Fields, ",") != "Name,Status__c" {
		t.Fatalf("relatedList.Fields = %v, want [Name Status__c]", relatedList.Fields)
	}
	if len(schemaRepo.addedColumns) != 1 {
		t.Fatalf("AddColumn attempts = %d, want 1", len(schemaRepo.addedColumns))
	}
	if len(repo.savedLayouts) != 2 {
		t.Fatalf("savedLayouts = %d, want child field admission + parent related-list admission", len(repo.savedLayouts))
	}
}

func TestCreateFieldRollsBackRelationshipRelatedListAdmissionWhenPhysicalDDLFailure(t *testing.T) {
	repo := newRelationshipLayoutMetadataRepo()
	schemaRepo := &formulaLayoutSchemaRepo{
		addColumnErr: errors.New("physical DDL exploded"),
	}
	service := NewMetadataService(repo, NewSchemaManager(schemaRepo), nil)

	err := service.CreateField(context.Background(), "Line__c", &models.FieldMetadata{
		APIName:     "Project__c",
		Label:       "Project",
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"Project__c"},
	})
	if err == nil {
		t.Fatal("CreateField error = nil, want physical DDL failure")
	}
	if !strings.Contains(err.Error(), "physical DDL exploded") {
		t.Fatalf("CreateField error = %v, want DDL failure detail", err)
	}

	childLayout := repo.layoutsByObject["line__c"][0]
	if layoutHasField(childLayout, "Project__c") {
		t.Fatalf("failed DDL left lookup field in child layout: %+v", childLayout.Sections)
	}

	parentLayout := repo.layoutsByObject["project__c"][0]
	if _, ok := relatedListByIdentity(parentLayout, "Line__c", "Project__c"); ok {
		t.Fatalf("failed DDL left related list in parent layout: %+v", parentLayout.RelatedLists)
	}
	if repo.restoreCalls != 2 {
		t.Fatalf("layout rollback attempts = %d, want child field + parent related-list rollbacks", repo.restoreCalls)
	}
}

func TestBatchCreateFieldsAddsRelationshipRelatedListToParentLayout(t *testing.T) {
	repo := newRelationshipLayoutMetadataRepo()
	schemaRepo := &formulaLayoutSchemaRepo{}
	service := NewMetadataService(repo, NewSchemaManager(schemaRepo), nil)

	created, err := service.BatchCreateFields(context.Background(), "Line__c", []models.FieldMetadata{{
		APIName:     "Project__c",
		Label:       "Project",
		Type:        constants.FieldTypeReference,
		ReferenceTo: []string{"Project__c"},
	}})
	if err != nil {
		t.Fatalf("BatchCreateFields: %v", err)
	}
	if len(created) != 1 {
		t.Fatalf("created = %d, want 1", len(created))
	}
	if len(schemaRepo.batchFields) != 1 {
		t.Fatalf("batchFields = %d, want 1", len(schemaRepo.batchFields))
	}

	childLayout := repo.layoutsByObject["line__c"][0]
	if !layoutHasField(childLayout, "Project__c") {
		t.Fatalf("child layout missing lookup field: %+v", childLayout.Sections)
	}

	parentLayout := repo.layoutsByObject["project__c"][0]
	if _, ok := relatedListByIdentity(parentLayout, "Line__c", "Project__c"); !ok {
		t.Fatalf("parent layout missing related list after batch create: %+v", parentLayout.RelatedLists)
	}
}

func TestCreateFieldReturnsLayoutAdmissionError(t *testing.T) {
	repo := newFormulaLayoutMetadataRepo()
	repo.saveErr = errors.New("page layout admin admission failed")
	schemaRepo := &formulaLayoutSchemaRepo{}
	service := NewMetadataService(repo, NewSchemaManager(schemaRepo), nil)

	formula := "Amount__c * 2"
	returnType := constants.FieldTypeCurrency
	err := service.CreateField(context.Background(), "Issue", &models.FieldMetadata{
		APIName:    "DoubleAmount__c",
		Label:      "Double Amount",
		Type:       constants.FieldTypeFormula,
		Formula:    &formula,
		ReturnType: &returnType,
	})
	if err == nil {
		t.Fatal("CreateField error = nil, want layout admission error")
	}
	if !strings.Contains(err.Error(), "page layout admin admission failed") {
		t.Fatalf("CreateField error = %v, want layout admission detail", err)
	}
	if len(schemaRepo.registeredOnly) != 0 {
		t.Fatalf("registeredOnly = %d, want 0 because layout admission must fail before field metadata registration", len(schemaRepo.registeredOnly))
	}
}

func TestCreateFieldReturnsCombinedErrorWhenLayoutRollbackFailsAfterRegistrationFailure(t *testing.T) {
	repo := newFormulaLayoutMetadataRepo()
	repo.restoreErrs = []error{apperrors.NewConflictError("PageLayout", "owner", "layout_issue")}
	schemaRepo := &formulaLayoutSchemaRepo{
		registerOnlyErr: errors.New("metadata registration exploded"),
	}
	service := NewMetadataService(repo, NewSchemaManager(schemaRepo), nil)

	formula := "Amount__c * 2"
	returnType := constants.FieldTypeCurrency
	err := service.CreateField(context.Background(), "Issue", &models.FieldMetadata{
		APIName:    "DoubleAmount__c",
		Label:      "Double Amount",
		Type:       constants.FieldTypeFormula,
		Formula:    &formula,
		ReturnType: &returnType,
	})
	if err == nil {
		t.Fatal("CreateField error = nil, want field registration plus rollback failure")
	}
	for _, want := range []string{
		"failed to register formula field metadata",
		"metadata registration exploded",
		"page layout admission rollback failed",
		"PageLayout already exists with owner='layout_issue'",
	} {
		if !strings.Contains(err.Error(), want) {
			t.Fatalf("CreateField error = %v, want %q", err, want)
		}
	}
	var internalErr *apperrors.InternalError
	if !errors.As(err, &internalErr) {
		t.Fatalf("CreateField error type = %T, want InternalError wrapper", err)
	}
	var conflictErr *apperrors.ConflictError
	if !errors.As(err, &conflictErr) {
		t.Fatalf("CreateField error = %v, want rollback conflict to remain discoverable", err)
	}
	if repo.restoreCalls != 1 {
		t.Fatalf("layout rollback attempts = %d, want 1", repo.restoreCalls)
	}
}

func TestCreateFieldRollsBackLayoutAdmissionWhenPhysicalDDLFailure(t *testing.T) {
	repo := newFormulaLayoutMetadataRepo()
	schemaRepo := &formulaLayoutSchemaRepo{
		addColumnErr: errors.New("physical DDL exploded"),
	}
	service := NewMetadataService(repo, NewSchemaManager(schemaRepo), nil)

	err := service.CreateField(context.Background(), "Issue", &models.FieldMetadata{
		APIName: "PriorityNote__c",
		Label:   "Priority Note",
		Type:    constants.FieldTypeString,
	})
	if err == nil {
		t.Fatal("CreateField error = nil, want physical DDL failure after layout admission")
	}
	for _, want := range []string{
		"failed to add column to schema",
		"physical DDL exploded",
	} {
		if !strings.Contains(err.Error(), want) {
			t.Fatalf("CreateField error = %v, want %q", err, want)
		}
	}
	var internalErr *apperrors.InternalError
	if !errors.As(err, &internalErr) {
		t.Fatalf("CreateField error type = %T, want InternalError wrapper", err)
	}
	if len(schemaRepo.addedColumns) != 1 {
		t.Fatalf("AddColumn attempts = %d, want 1", len(schemaRepo.addedColumns))
	}
	if schemaRepo.addedColumns[0].Name != "PriorityNote__c" {
		t.Fatalf("AddColumn column = %q, want PriorityNote__c", schemaRepo.addedColumns[0].Name)
	}
	if len(repo.savedLayouts) != 1 {
		t.Fatalf("savedLayouts = %d, want one admitted layout before DDL failure", len(repo.savedLayouts))
	}
	if repo.restoreCalls != 1 {
		t.Fatalf("layout rollback attempts = %d, want 1 after DDL failure", repo.restoreCalls)
	}
	if layoutHasField(repo.layouts[0], "PriorityNote__c") {
		t.Fatalf("failed DDL left admitted field in layout: %+v", repo.layouts[0].Sections)
	}
}

func TestBatchCreateFieldsReturnsLayoutAdmissionError(t *testing.T) {
	repo := newFormulaLayoutMetadataRepo()
	repo.saveErr = errors.New("page layout admin admission failed")
	schemaRepo := &formulaLayoutSchemaRepo{}
	service := NewMetadataService(repo, NewSchemaManager(schemaRepo), nil)

	formula := "Amount__c * 2"
	returnType := constants.FieldTypeCurrency
	created, err := service.BatchCreateFields(context.Background(), "Issue", []models.FieldMetadata{{
		APIName:    "DoubleAmount__c",
		Label:      "Double Amount",
		Type:       constants.FieldTypeFormula,
		Formula:    &formula,
		ReturnType: &returnType,
	}})
	if err == nil {
		t.Fatal("BatchCreateFields error = nil, want layout admission error")
	}
	if len(created) != 0 {
		t.Fatalf("created = %d, want 0 on layout admission error", len(created))
	}
	if !strings.Contains(err.Error(), "page layout admin admission failed") {
		t.Fatalf("BatchCreateFields error = %v, want layout admission detail", err)
	}
	if len(schemaRepo.batchFields) != 0 {
		t.Fatalf("batchFields = %d, want 0 because layout admission must fail before field metadata registration", len(schemaRepo.batchFields))
	}
}

func TestBatchCreateFieldsRollsBackPriorLayoutAdmissionsWhenLaterFieldAdmissionFails(t *testing.T) {
	repo := newFormulaLayoutMetadataRepo()
	repo.saveErrs = []error{nil, errors.New("second layout admission failed")}
	schemaRepo := &formulaLayoutSchemaRepo{}
	service := NewMetadataService(repo, NewSchemaManager(schemaRepo), nil)

	formula := "Amount__c * 2"
	returnType := constants.FieldTypeCurrency
	created, err := service.BatchCreateFields(context.Background(), "Issue", []models.FieldMetadata{
		{
			APIName:    "DoubleAmount__c",
			Label:      "Double Amount",
			Type:       constants.FieldTypeFormula,
			Formula:    &formula,
			ReturnType: &returnType,
		},
		{
			APIName:    "TripleAmount__c",
			Label:      "Triple Amount",
			Type:       constants.FieldTypeFormula,
			Formula:    &formula,
			ReturnType: &returnType,
		},
	})
	if err == nil {
		t.Fatal("BatchCreateFields error = nil, want second layout admission failure")
	}
	if len(created) != 0 {
		t.Fatalf("created = %d, want 0 on layout admission failure", len(created))
	}
	if !strings.Contains(err.Error(), "second layout admission failed") {
		t.Fatalf("BatchCreateFields error = %v, want second layout admission detail", err)
	}
	if len(schemaRepo.batchFields) != 0 {
		t.Fatalf("batchFields = %d, want 0 because later layout admission failed before batch DDL", len(schemaRepo.batchFields))
	}
	if repo.restoreCalls != 1 {
		t.Fatalf("layout rollback attempts = %d, want 1 for prior admitted field", repo.restoreCalls)
	}
	if layoutHasField(repo.layouts[0], "DoubleAmount__c") {
		t.Fatalf("prior admitted field still present after rollback: %+v", repo.layouts[0].Sections)
	}
	if layoutHasField(repo.layouts[0], "TripleAmount__c") {
		t.Fatalf("failed later field should not remain in layout: %+v", repo.layouts[0].Sections)
	}
	if len(repo.savedLayouts) != 1 {
		t.Fatalf("savedLayouts = %d, want only the first layout admission persisted before failure", len(repo.savedLayouts))
	}
}

func TestBatchCreateFieldsAggregatesLayoutRollbackErrorsAfterBatchFailure(t *testing.T) {
	repo := newFormulaLayoutMetadataRepo()
	repo.restoreErrs = []error{
		errors.New("rollback failed for second field"),
		apperrors.NewConflictError("PageLayout", "owner", "layout_issue"),
	}
	schemaRepo := &formulaLayoutSchemaRepo{
		batchAddColumnsErr: errors.New("batch DDL exploded"),
	}
	service := NewMetadataService(repo, NewSchemaManager(schemaRepo), nil)

	formula := "Amount__c * 2"
	returnType := constants.FieldTypeCurrency
	created, err := service.BatchCreateFields(context.Background(), "Issue", []models.FieldMetadata{
		{
			APIName:    "DoubleAmount__c",
			Label:      "Double Amount",
			Type:       constants.FieldTypeFormula,
			Formula:    &formula,
			ReturnType: &returnType,
		},
		{
			APIName:    "TripleAmount__c",
			Label:      "Triple Amount",
			Type:       constants.FieldTypeFormula,
			Formula:    &formula,
			ReturnType: &returnType,
		},
	})
	if err == nil {
		t.Fatal("BatchCreateFields error = nil, want batch failure plus rollback failures")
	}
	if len(created) != 0 {
		t.Fatalf("created = %d, want 0 on batch failure", len(created))
	}
	for _, want := range []string{
		"batch creation failed",
		"batch DDL exploded",
		"rollback failed for second field",
		"PageLayout already exists with owner='layout_issue'",
	} {
		if !strings.Contains(err.Error(), want) {
			t.Fatalf("BatchCreateFields error = %v, want %q", err, want)
		}
	}
	var internalErr *apperrors.InternalError
	if !errors.As(err, &internalErr) {
		t.Fatalf("BatchCreateFields error type = %T, want InternalError wrapper", err)
	}
	var conflictErr *apperrors.ConflictError
	if !errors.As(err, &conflictErr) {
		t.Fatalf("BatchCreateFields error = %v, want rollback conflict to remain discoverable", err)
	}
	if repo.restoreCalls != 2 {
		t.Fatalf("layout rollback attempts = %d, want 2", repo.restoreCalls)
	}
}

type formulaLayoutSchemaRepo struct {
	ports.SchemaRepository

	registeredOnly     []domainSchema.ColumnDefinition
	addedColumns       []domainSchema.ColumnDefinition
	indexedColumns     []string
	batchFields        []models.FieldMetadata
	registerOnlyErr    error
	addColumnErr       error
	batchAddColumnsErr error
}

func (r *formulaLayoutSchemaRepo) MapFieldTypeToSQL(fieldType string) string {
	return "VARCHAR(255)"
}

func (r *formulaLayoutSchemaRepo) RegisterFieldOnly(tableName string, col domainSchema.ColumnDefinition) error {
	if r.registerOnlyErr != nil {
		return r.registerOnlyErr
	}
	r.registeredOnly = append(r.registeredOnly, col)
	return nil
}

func (r *formulaLayoutSchemaRepo) AddColumn(tableName string, col domainSchema.ColumnDefinition) error {
	r.addedColumns = append(r.addedColumns, col)
	if r.addColumnErr != nil {
		return r.addColumnErr
	}
	return nil
}

func (r *formulaLayoutSchemaRepo) EnsureColumnIndex(tableName string, columnName string) error {
	r.indexedColumns = append(r.indexedColumns, tableName+"."+columnName)
	return nil
}

func (r *formulaLayoutSchemaRepo) BatchAddColumns(tableName string, fields []models.FieldMetadata) error {
	if r.batchAddColumnsErr != nil {
		return r.batchAddColumnsErr
	}
	r.batchFields = append([]models.FieldMetadata(nil), fields...)
	return nil
}

func layoutHasField(layout *models.PageLayout, fieldAPIName string) bool {
	if layout == nil {
		return false
	}
	for _, section := range layout.Sections {
		for _, field := range section.Fields {
			if field == fieldAPIName {
				return true
			}
		}
	}
	return false
}

func cloneTestPageLayout(layout *models.PageLayout) *models.PageLayout {
	return clonePageLayoutForFieldAdmission(layout)
}

func TestStructuralLayoutProjectionExcludesAutonumberAndDoesNotClassifyNames(t *testing.T) {
	obj := &models.ObjectMetadata{
		APIName: "TestObject",
		Fields: []models.FieldMetadata{
			{APIName: "Id", Label: "ID", Type: constants.FieldTypeString},
			// v1 autonumber name field — IS the name field, so it will be picked for CompactLayout
			{APIName: "OrderNumber", Label: "Order Number", Type: constants.FieldTypeAutonumber, IsNameField: true},
			// v1 autonumber non-name field
			{APIName: "Sequence__c", Label: "Sequence", Type: constants.FieldTypeAutonumber},
			// v2 source-backed autonumber (Type=string but in resolver set)
			{APIName: "QuoteNumber", Label: "Quote Number", Type: constants.FieldTypeString, IsNameField: false},
			// Address-like names must not create an Address section.
			{APIName: "Street", Label: "Street", Type: constants.FieldTypeString},
			{APIName: "City", Label: "City", Type: constants.FieldTypeString},
			{APIName: "PostalCode", Label: "Postal Code", Type: constants.FieldTypeString},
			// Custom prefixes must not create ARR sections.
			{APIName: "ARR_Amount__c", Label: "ARR Amount", Type: constants.FieldTypeString},
			{APIName: "ARR_Type__c", Label: "ARR Type", Type: constants.FieldTypeString},
			{APIName: "ARR_Date__c", Label: "ARR Date", Type: constants.FieldTypeString},
			// normal field
			{APIName: "Status", Label: "Status", Type: constants.FieldTypeString},
		},
	}
	repo := &structuralProjectionLayoutRepo{schema: obj}
	service := NewMetadataService(repo, nil, nil)
	service.SetAutoNumberFieldResolver(func(objectName string) map[string]struct{} {
		return map[string]struct{}{"QuoteNumber": {}}
	})

	layout := service.BuildStructuralLayoutProjection(context.Background(), obj)
	if len(layout.RelatedLists) != 0 || repo.relatedListReads != 0 {
		t.Fatal("structural projection consulted or injected application related-list recommendations")
	}
	if layout.ID != "TestObject-default-layout" || layout.LayoutName != "TestObject-default-layout" {
		t.Fatalf("projection identity = %s/%s, want TestObject-default-layout", layout.ID, layout.LayoutName)
	}
	if layout.Source != structuralProjectionSource {
		t.Fatalf("projection Source = %q, want %q", layout.Source, structuralProjectionSource)
	}

	// Collect all fields from all sections
	var sectionFields []string
	for _, section := range layout.Sections {
		sectionFields = append(sectionFields, section.Fields...)
		if section.Label == constants.SectionLabelAddress || section.Label == "ARR" {
			t.Fatalf("unexpected classifier-created section %q: %+v", section.Label, layout.Sections)
		}
	}

	// Assert autonumber fields are absent from ALL sections (set-non-membership)
	for _, an := range []string{"OrderNumber", "Sequence__c", "QuoteNumber"} {
		for _, f := range sectionFields {
			if f == an {
				t.Fatalf("autonumber field %q found in editable section: %+v", an, layout.Sections)
			}
		}
	}

	// Normal field should be present
	hasStatus := false
	for _, f := range sectionFields {
		if f == "Status" {
			hasStatus = true
			break
		}
	}
	if !hasStatus {
		t.Fatal("normal field Status should be in a section")
	}
	for _, structuralName := range []string{"Street", "City", "PostalCode", "ARR_Amount__c", "ARR_Type__c", "ARR_Date__c"} {
		if !layoutHasField(layout, structuralName) {
			t.Fatalf("structural field %q should remain in the generic Information section: %+v", structuralName, layout.Sections)
		}
	}

	// The compact scaffold may retain the autonumber name field as the record identifier.
	hasOrderNumber := false
	for _, f := range layout.CompactLayout {
		if f == "OrderNumber" {
			hasOrderNumber = true
			break
		}
	}
	if !hasOrderNumber {
		t.Fatalf("autonumber name field OrderNumber should be in CompactLayout: %v", layout.CompactLayout)
	}
}

type structuralProjectionLayoutRepo struct {
	ports.MetadataRepository
	schema           *models.ObjectMetadata
	relatedListReads int
}

func (r *structuralProjectionLayoutRepo) GetSchemaByAPIName(context.Context, string) (*models.ObjectMetadata, error) {
	return r.schema, nil
}

func (r *structuralProjectionLayoutRepo) GetAllSchemas(context.Context) ([]*models.ObjectMetadata, error) {
	if r.schema == nil {
		return nil, nil
	}
	return []*models.ObjectMetadata{r.schema}, nil
}

func (r *structuralProjectionLayoutRepo) GetAllRelationships(context.Context) ([]*models.Relationship, error) {
	return nil, nil
}

func (r *structuralProjectionLayoutRepo) GetRelatedListConfigs(context.Context, string) ([]ports.RelatedListConfig, error) {
	r.relatedListReads++
	return nil, nil
}

type autonumberSkipLayoutRepo struct {
	ports.MetadataRepository

	layouts      []*models.PageLayout
	savedLayouts []*models.PageLayout
	saveErr      error
}

func (r *autonumberSkipLayoutRepo) GetLayouts(ctx context.Context, objectAPIName string) ([]*models.PageLayout, error) {
	return append([]*models.PageLayout(nil), r.layouts...), nil
}

func (r *autonumberSkipLayoutRepo) SaveLayout(ctx context.Context, layout *models.PageLayout) error {
	if r.saveErr != nil {
		return r.saveErr
	}
	r.savedLayouts = append(r.savedLayouts, cloneTestPageLayout(layout))
	return nil
}

func (r *autonumberSkipLayoutRepo) SaveLayoutForFieldAdmission(ctx context.Context, original, updated *models.PageLayout) (func(context.Context, *models.PageLayout) error, error) {
	snapshot := cloneTestPageLayout(original)
	if err := r.SaveLayout(ctx, updated); err != nil {
		return nil, err
	}
	return func(context.Context, *models.PageLayout) error {
		for i := range r.layouts {
			if r.layouts[i].ID == snapshot.ID {
				r.layouts[i] = cloneTestPageLayout(snapshot)
				return nil
			}
		}
		return nil
	}, nil
}

func TestAddFieldToLayoutSkipsAutonumberField(t *testing.T) {
	repo := &autonumberSkipLayoutRepo{
		layouts: []*models.PageLayout{{
			ID:            "layout_TestObject",
			ObjectAPIName: "TestObject",
			Sections: []models.PageSection{{
				ID:      "section_details",
				Label:   "Details",
				Columns: 2,
				Fields:  []string{"Name"},
			}},
		}},
	}

	svc := NewMetadataService(repo, nil, nil)
	// Wire a resolver that knows about QuoteNumber as an autonumber field
	svc.SetAutoNumberFieldResolver(func(objectName string) map[string]struct{} {
		return map[string]struct{}{"QuoteNumber": {}}
	})

	ctx := context.Background()

	// V1: autonumber field (Type == FieldTypeAutonumber)
	anField := &models.FieldMetadata{
		APIName: "OrderNumber",
		Label:   "Order Number",
		Type:    constants.FieldTypeAutonumber,
	}
	if err := svc.addFieldToLayout(ctx, "TestObject", anField); err != nil {
		t.Fatalf("addFieldToLayout(v1 autonumber): %v", err)
	}
	if len(repo.savedLayouts) != 0 {
		t.Fatalf("v1 autonumber field should not have triggered SaveLayout; savedLayouts=%d", len(repo.savedLayouts))
	}

	// Verify original layout is untouched
	if len(repo.layouts[0].Sections[0].Fields) != 1 || repo.layouts[0].Sections[0].Fields[0] != "Name" {
		t.Fatalf("original layout mutated: %+v", repo.layouts[0].Sections)
	}

	// V2: Type=string but in resolver set
	v2Field := &models.FieldMetadata{
		APIName: "QuoteNumber",
		Label:   "Quote Number",
		Type:    constants.FieldTypeString,
	}
	if err := svc.addFieldToLayout(ctx, "TestObject", v2Field); err != nil {
		t.Fatalf("addFieldToLayout(v2 autonumber): %v", err)
	}
	if len(repo.savedLayouts) != 0 {
		t.Fatalf("v2 autonumber field should not have triggered SaveLayout; savedLayouts=%d", len(repo.savedLayouts))
	}

	// Non-autonumber field should still be added
	normalField := &models.FieldMetadata{
		APIName: "Description",
		Label:   "Description",
		Type:    constants.FieldTypeString,
	}
	if err := svc.addFieldToLayout(ctx, "TestObject", normalField); err != nil {
		t.Fatalf("addFieldToLayout(normal field): %v", err)
	}
	if len(repo.savedLayouts) != 1 {
		t.Fatalf("normal field should have triggered SaveLayout once; got %d", len(repo.savedLayouts))
	}
	if len(repo.savedLayouts[0].Sections[0].Fields) != 2 {
		t.Fatalf("layout should now have 2 fields; got %d: %+v", len(repo.savedLayouts[0].Sections[0].Fields), repo.savedLayouts[0].Sections)
	}
}

func TestAddFieldToLayoutReturnsSaveLayoutAdmissionError(t *testing.T) {
	repo := &autonumberSkipLayoutRepo{
		layouts: []*models.PageLayout{{
			ID:            "layout_TestObject",
			ObjectAPIName: "TestObject",
			Sections: []models.PageSection{{
				ID:      "section_details",
				Label:   "Details",
				Columns: 2,
				Fields:  []string{"Name"},
			}},
		}},
		saveErr: errors.New("page layout admin admission failed"),
	}

	svc := NewMetadataService(repo, nil, nil)
	err := svc.addFieldToLayout(context.Background(), "TestObject", &models.FieldMetadata{
		APIName: "Description",
		Label:   "Description",
		Type:    constants.FieldTypeString,
	})
	if err == nil {
		t.Fatal("addFieldToLayout error = nil, want SaveLayout admission error")
	}
	if !strings.Contains(err.Error(), "page layout admin admission failed") {
		t.Fatalf("addFieldToLayout error = %v, want SaveLayout admission detail", err)
	}
}

type ownerPreservingRollbackRepo struct {
	ports.MetadataRepository

	layouts        []*models.PageLayout
	ownerPacks     map[string]string
	failSaveForID  string
	restores       int
	restoreErr     error
	restoreFailure error
}

func (r *ownerPreservingRollbackRepo) GetLayouts(ctx context.Context, objectAPIName string) ([]*models.PageLayout, error) {
	return r.layouts, nil
}

func (r *ownerPreservingRollbackRepo) SaveLayout(ctx context.Context, layout *models.PageLayout) error {
	if layout.ID == r.failSaveForID {
		return errors.New("page layout admin admission failed")
	}
	saved := cloneTestPageLayout(layout)
	saved.Source = "admin_created"
	saved.IsCustomized = true
	layout.Source = saved.Source
	layout.IsCustomized = saved.IsCustomized
	r.ownerPacks[layout.ID] = ""
	for i := range r.layouts {
		if r.layouts[i].ID == layout.ID {
			r.layouts[i] = saved
			return nil
		}
	}
	return nil
}

func (r *ownerPreservingRollbackRepo) SnapshotLayoutForFieldAdmission(ctx context.Context, layoutID string) (func(context.Context, *models.PageLayout) error, error) {
	var original *models.PageLayout
	for _, layout := range r.layouts {
		if layout.ID == layoutID {
			original = cloneTestPageLayout(layout)
			break
		}
	}
	if original == nil {
		return nil, errors.New("layout not found")
	}
	ownerPack := r.ownerPacks[layoutID]
	return func(_ context.Context, admitted *models.PageLayout) error {
		if admitted == nil || !layoutHasField(admitted, "Description") {
			r.restoreErr = errors.New("restore was not fenced with the admitted layout")
			return r.restoreErr
		}
		if r.restoreFailure != nil {
			return r.restoreFailure
		}
		r.restores++
		r.ownerPacks[layoutID] = ownerPack
		for i := range r.layouts {
			if r.layouts[i].ID == layoutID {
				r.layouts[i] = cloneTestPageLayout(original)
				return nil
			}
		}
		return errors.New("layout not found")
	}, nil
}

func (r *ownerPreservingRollbackRepo) SaveLayoutForFieldAdmission(ctx context.Context, original, updated *models.PageLayout) (func(context.Context, *models.PageLayout) error, error) {
	restore, err := r.SnapshotLayoutForFieldAdmission(ctx, original.ID)
	if err != nil {
		return nil, err
	}
	if err := r.SaveLayout(ctx, updated); err != nil {
		return nil, err
	}
	return restore, nil
}

func TestAddFieldToLayoutRollbackPreservesOriginalOwnerTuple(t *testing.T) {
	repo := &ownerPreservingRollbackRepo{
		layouts: []*models.PageLayout{
			{
				ID:            "layout_one",
				ObjectAPIName: "TestObject",
				LayoutName:    "One",
				Source:        "curated",
				IsCustomized:  false,
				Sections: []models.PageSection{{
					ID:     "section_one",
					Label:  "Details",
					Fields: []string{"Name"},
				}},
			},
			{
				ID:            "layout_two",
				ObjectAPIName: "TestObject",
				LayoutName:    "Two",
				Source:        "curated",
				IsCustomized:  false,
				Sections: []models.PageSection{{
					ID:     "section_two",
					Label:  "Details",
					Fields: []string{"Name"},
				}},
			},
		},
		ownerPacks: map[string]string{
			"layout_one": "base-pack",
			"layout_two": "base-pack",
		},
		failSaveForID: "layout_two",
	}
	svc := NewMetadataService(repo, nil, nil)

	err := svc.addFieldToLayout(context.Background(), "TestObject", &models.FieldMetadata{
		APIName: "Description",
		Label:   "Description",
		Type:    constants.FieldTypeString,
	})
	if err == nil {
		t.Fatal("addFieldToLayout error = nil, want second layout admission error")
	}
	if !strings.Contains(err.Error(), "page layout admin admission failed") {
		t.Fatalf("addFieldToLayout error = %v, want admission detail", err)
	}
	if repo.restores != 1 {
		t.Fatalf("owner-preserving restores = %d, want 1", repo.restores)
	}
	if repo.restoreErr != nil {
		t.Fatalf("restore was not armed with admitted layout: %v", repo.restoreErr)
	}
	restored := repo.layouts[0]
	if layoutHasField(restored, "Description") {
		t.Fatalf("rolled-back layout still contains field: %+v", restored.Sections)
	}
	if restored.Source != "curated" || restored.IsCustomized || repo.ownerPacks["layout_one"] != "base-pack" {
		t.Fatalf("owner tuple after rollback = Source=%q IsCustomized=%v OwnerPack=%q, want curated/false/base-pack", restored.Source, restored.IsCustomized, repo.ownerPacks["layout_one"])
	}
}

func TestAddFieldToLayoutReturnsRollbackFailureFromAdmissionStage(t *testing.T) {
	repo := &ownerPreservingRollbackRepo{
		layouts: []*models.PageLayout{
			{
				ID:            "layout_one",
				ObjectAPIName: "TestObject",
				LayoutName:    "One",
				Source:        "curated",
				IsCustomized:  false,
				Sections: []models.PageSection{{
					ID:     "section_one",
					Label:  "Details",
					Fields: []string{"Name"},
				}},
			},
			{
				ID:            "layout_two",
				ObjectAPIName: "TestObject",
				LayoutName:    "Two",
				Source:        "curated",
				IsCustomized:  false,
				Sections: []models.PageSection{{
					ID:     "section_two",
					Label:  "Details",
					Fields: []string{"Name"},
				}},
			},
		},
		ownerPacks: map[string]string{
			"layout_one": "base-pack",
			"layout_two": "base-pack",
		},
		failSaveForID:  "layout_two",
		restoreFailure: errors.New("layout was changed by another writer"),
	}
	svc := NewMetadataService(repo, nil, nil)

	err := svc.addFieldToLayout(context.Background(), "TestObject", &models.FieldMetadata{
		APIName: "Description",
		Label:   "Description",
		Type:    constants.FieldTypeString,
	})
	if err == nil {
		t.Fatal("addFieldToLayout error = nil, want admission and rollback details")
	}
	if !strings.Contains(err.Error(), "page layout admin admission failed") {
		t.Fatalf("addFieldToLayout error = %v, want admission detail", err)
	}
	if !strings.Contains(err.Error(), "layout was changed by another writer") {
		t.Fatalf("addFieldToLayout error = %v, want rollback failure detail", err)
	}
}
