package metadata

import (
	"context"
	"encoding/json"
	"errors"
	"reflect"
	"strings"
	"testing"
	"time"

	apperrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/kernel/pkg/schema"
	"github.com/nexuscrm/metadata/catalogsnapshot"
	"github.com/nexuscrm/shared/pkg/models"
	layoutauthoringv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/layoutauthoringv1"
)

type layoutAdministrationTxKey struct{}

type layoutAdministrationTxManager struct{}

func (*layoutAdministrationTxManager) WithTransaction(ctx context.Context, fn func(context.Context) error) error {
	return fn(ctx)
}

func (*layoutAdministrationTxManager) WithPessimisticTransaction(ctx context.Context, fn func(context.Context) error) error {
	return fn(ctx)
}

func (*layoutAdministrationTxManager) WithRetry(ctx context.Context, fn func(context.Context) error, _ int) error {
	return fn(ctx)
}

func (*layoutAdministrationTxManager) WithPessimisticRetry(ctx context.Context, fn func(context.Context) error, _ int) error {
	return fn(ctx)
}

func (*layoutAdministrationTxManager) IsTransactionActive(ctx context.Context) bool {
	return ctx.Value(layoutAdministrationTxKey{}) != nil
}

func (*layoutAdministrationTxManager) IsPessimisticTx(ctx context.Context) bool {
	return ctx.Value(layoutAdministrationTxKey{}) != nil
}

func (*layoutAdministrationTxManager) ExtractTx(ctx context.Context) interface{} {
	return ctx.Value(layoutAdministrationTxKey{})
}

func (*layoutAdministrationTxManager) DetachTx(ctx context.Context) context.Context { return ctx }

type layoutAdministrationRepo struct {
	ports.MetadataRepository

	layout           *models.PageLayout
	calls            []string
	listFields       []string
	history          *models.PageLayoutHistoryEntry
	historyJSON      string
	historyFailure   error
	outsideTxAccess  bool
	canonicalObject  string
	listFieldsObject string
	assignments      []*models.LayoutAssignment
	schemas          map[string]*models.ObjectMetadata
	schemaReads      []string
}

func TestLayoutAuthoringAdministrationReadinessRejectsServingReaderAndIncompleteOwner(t *testing.T) {
	service := NewMetadataService(&layoutAdministrationRepo{}, nil, nil)
	if err := service.CheckLayoutAuthoringAdministrationReady(); err == nil || !strings.Contains(err.Error(), "transactional metadata owner") {
		t.Fatalf("repository without transaction owner readiness = %v", err)
	}
	service.SetTransactionManager(&layoutAdministrationTxManager{})
	if err := service.CheckLayoutAuthoringAdministrationReady(); err != nil {
		t.Fatalf("complete authoring owner readiness: %v", err)
	}
	runtimeService, err := NewRuntimeMetadataService(func(context.Context) (*catalogsnapshot.RuntimeMetadataView, bool) {
		return nil, false
	})
	if err != nil {
		t.Fatal(err)
	}
	runtimeService.SetTransactionManager(&layoutAdministrationTxManager{})
	if err := runtimeService.CheckLayoutAuthoringAdministrationReady(); err == nil {
		t.Fatal("request-bound serving reader accepted as Layout authoring owner")
	}
}

func (r *layoutAdministrationRepo) LockLayoutAuthoringRecordTypes(context.Context, []string) ([]*models.RecordType, error) {
	return nil, nil
}

func (r *layoutAdministrationRepo) LockLayoutAuthoringSchemaReferences(context.Context, []schema.IncarnationReference) error {
	return nil
}

func (r *layoutAdministrationRepo) requireTx(ctx context.Context) {
	if ctx.Value(layoutAdministrationTxKey{}) == nil {
		r.outsideTxAccess = true
	}
}

func (r *layoutAdministrationRepo) CreateLayoutForAdministration(ctx context.Context, layout *models.PageLayout) error {
	r.requireTx(ctx)
	r.calls = append(r.calls, "create")
	cloned := *layout
	r.layout = &cloned
	return nil
}

func (r *layoutAdministrationRepo) SaveLayout(ctx context.Context, layout *models.PageLayout) error {
	r.requireTx(ctx)
	r.calls = append(r.calls, "save")
	cloned := *layout
	r.layout = &cloned
	return nil
}

func (r *layoutAdministrationRepo) SaveLayoutForAdministration(ctx context.Context, layout *models.PageLayout) error {
	r.requireTx(ctx)
	r.calls = append(r.calls, "save")
	cloned := *layout
	r.layout = &cloned
	return nil
}

func (r *layoutAdministrationRepo) DeleteLayoutForAdministration(ctx context.Context, layoutID string) error {
	r.requireTx(ctx)
	r.calls = append(r.calls, "strict-delete")
	if r.layout == nil || r.layout.ID != layoutID {
		return apperrors.NewNotFoundError("PageLayout", layoutID)
	}
	r.layout = nil
	return nil
}

func (r *layoutAdministrationRepo) DeleteAssignmentForAdministration(ctx context.Context, assignmentID string) error {
	r.requireTx(ctx)
	r.calls = append(r.calls, "strict-assignment-delete")
	for index, assignment := range r.assignments {
		if assignment != nil && assignment.ID == assignmentID {
			r.assignments = append(r.assignments[:index], r.assignments[index+1:]...)
			return nil
		}
	}
	return apperrors.NewNotFoundError("LayoutAssignment", assignmentID)
}

func (r *layoutAdministrationRepo) DeleteAssignmentsByLayoutID(ctx context.Context, layoutID string) error {
	r.requireTx(ctx)
	r.calls = append(r.calls, "assignment-cleanup")
	remaining := r.assignments[:0]
	for _, assignment := range r.assignments {
		if assignment == nil || !strings.EqualFold(assignment.LayoutID, layoutID) {
			remaining = append(remaining, assignment)
		}
	}
	r.assignments = remaining
	return nil
}

func (r *layoutAdministrationRepo) GetLayout(ctx context.Context, layoutID string) (*models.PageLayout, error) {
	r.requireTx(ctx)
	r.calls = append(r.calls, "read")
	if r.layout == nil || r.layout.ID != layoutID {
		return nil, nil
	}
	cloned := *r.layout
	return &cloned, nil
}

func (r *layoutAdministrationRepo) GetSchemaByAPIName(_ context.Context, apiName string) (*models.ObjectMetadata, error) {
	r.schemaReads = append(r.schemaReads, apiName)
	return r.schemas[strings.ToLower(apiName)], nil
}

func (r *layoutAdministrationRepo) GetLayouts(ctx context.Context, _ string) ([]*models.PageLayout, error) {
	r.requireTx(ctx)
	r.calls = append(r.calls, "layouts")
	if r.layout == nil {
		return []*models.PageLayout{}, nil
	}
	cloned := *r.layout
	return []*models.PageLayout{&cloned}, nil
}

func (r *layoutAdministrationRepo) GetAssignmentsForObject(ctx context.Context, _ string) ([]*models.LayoutAssignment, error) {
	r.requireTx(ctx)
	r.calls = append(r.calls, "assignments")
	return []*models.LayoutAssignment{}, nil
}

func (r *layoutAdministrationRepo) ReadLayoutAuthoringAggregate(ctx context.Context, objectID string) (*models.LayoutAuthoringAggregate, error) {
	r.requireTx(ctx)
	r.calls = append(r.calls, "read-aggregate")
	layouts := []*models.PageLayout{}
	if r.layout != nil {
		layouts = append(layouts, r.layout)
	}
	canonicalObject := r.canonicalObject
	if canonicalObject == "" {
		canonicalObject = objectID
	}
	return &models.LayoutAuthoringAggregate{
		ObjectAPIName: canonicalObject, OrderedListFields: append([]string(nil), r.listFields...),
		Layouts: layouts, Assignments: append([]*models.LayoutAssignment(nil), r.assignments...),
	}, nil
}

func (r *layoutAdministrationRepo) WriteLayoutHistory(ctx context.Context, entry *models.PageLayoutHistoryEntry) error {
	r.requireTx(ctx)
	r.calls = append(r.calls, "history")
	if r.historyFailure != nil {
		return r.historyFailure
	}
	cloned := *entry
	r.history = &cloned
	return nil
}

func (r *layoutAdministrationRepo) GetLayoutHistoryMaxVersion(ctx context.Context, _ string) (int, error) {
	r.requireTx(ctx)
	r.calls = append(r.calls, "history-max")
	return 1, nil
}

func (r *layoutAdministrationRepo) GetLayoutHistoryConfig(ctx context.Context, _ string, _ int) (string, error) {
	r.requireTx(ctx)
	r.calls = append(r.calls, "history-config")
	return r.historyJSON, nil
}

func (r *layoutAdministrationRepo) LockLayoutHistoryConfig(ctx context.Context, _ string, _ int) (string, error) {
	r.requireTx(ctx)
	r.calls = append(r.calls, "history-config")
	return r.historyJSON, nil
}

func (r *layoutAdministrationRepo) SetLayoutAuthoringListFields(ctx context.Context, objectID string, fields []string) error {
	r.requireTx(ctx)
	r.calls = append(r.calls, "list-fields")
	r.listFieldsObject = objectID
	r.listFields = append([]string(nil), fields...)
	if r.listFields == nil {
		r.listFields = []string{}
	}
	return nil
}

func (r *layoutAdministrationRepo) LockLayoutAuthoringAggregate(ctx context.Context, objectID string) (*models.LayoutAuthoringAggregate, error) {
	r.requireTx(ctx)
	r.calls = append(r.calls, "lock")
	layouts := []*models.PageLayout{}
	if r.layout != nil {
		cloned := *r.layout
		layouts = append(layouts, &cloned)
	}
	canonicalObject := r.canonicalObject
	if canonicalObject == "" {
		canonicalObject = objectID
	}
	return &models.LayoutAuthoringAggregate{
		ObjectAPIName: canonicalObject, OrderedListFields: append([]string(nil), r.listFields...),
		Layouts: layouts, Assignments: append([]*models.LayoutAssignment(nil), r.assignments...),
	}, nil
}

func TestCreateLayoutForAdministrationUsesTheLockedCanonicalObjectIdentity(t *testing.T) {
	repo := &layoutAdministrationRepo{canonicalObject: "WorkItem"}
	service := NewMetadataService(repo, nil, nil)
	service.SetTransactionManager(&layoutAdministrationTxManager{})
	ctx := context.WithValue(t.Context(), layoutAdministrationTxKey{}, struct{}{})
	listFields := []string{"Subject"}
	layout := &models.PageLayout{ID: "layout-canonical", ObjectAPIName: "workitem", LayoutName: "Canonical"}
	if err := service.CreateLayoutForAdministration(ctx, layout, &listFields, "admin_create", nil); err != nil {
		t.Fatal(err)
	}
	if repo.layout == nil || repo.layout.ObjectAPIName != "WorkItem" || repo.listFieldsObject != "WorkItem" {
		t.Fatalf("canonical object identity was not used for all writes: layout=%#v listFieldsObject=%q", repo.layout, repo.listFieldsObject)
	}
}

func TestLayoutReferenceValidationLoadsOnlyReachableSchemas(t *testing.T) {
	account := &models.ObjectMetadata{ID: "object-account", APIName: "Account", Fields: []models.FieldMetadata{
		{ID: "field-account-owner", APIName: "Owner", ReferenceTo: []string{"User"}},
		{ID: "field-account-name", APIName: "Name"},
	}}
	user := &models.ObjectMetadata{ID: "object-user", APIName: "User", Fields: []models.FieldMetadata{
		{ID: "field-user-name", APIName: "Name"},
	}}
	contact := &models.ObjectMetadata{ID: "object-contact", APIName: "Contact", Fields: []models.FieldMetadata{
		{ID: "field-contact-account", APIName: "AccountId", ReferenceTo: []string{"Account"}},
		{ID: "field-contact-name", APIName: "Name"},
	}}
	repo := &layoutAdministrationRepo{schemas: map[string]*models.ObjectMetadata{
		"account": account, "user": user, "contact": contact,
	}}
	service := NewMetadataService(repo, nil, nil)
	document := layoutauthoringv1.LayoutDocument{
		LayoutID: "layout-account", ObjectID: "Account", Name: "Account", Kind: "Detail",
		CompactFields: []string{"Name"}, Tabs: []string{},
		Sections:     []layoutauthoringv1.LayoutSection{{SectionID: "main", Fields: []string{"Owner.Name"}, ComponentConfiguration: []layoutauthoringv1.NamedValue{}, FieldLabels: []layoutauthoringv1.FieldLabel{}, FieldBehaviors: []layoutauthoringv1.FieldBehaviorGroup{}}},
		RelatedLists: []layoutauthoringv1.RelatedList{{RelatedListID: "contacts", ObjectID: "Contact", LookupFieldID: "AccountId", FieldIDs: []string{"Name"}, PrefillMappings: []layoutauthoringv1.PrefillMapping{}}},
		RecordPanels: []layoutauthoringv1.RecordPanel{}, HeaderActions: layoutauthoringv1.ActionPlacement{Actions: []layoutauthoringv1.LayoutAction{}},
		QuickActions: layoutauthoringv1.ActionPlacement{Actions: []layoutauthoringv1.LayoutAction{}}, RecordStates: []layoutauthoringv1.RecordStateRule{}, PrimaryHeaderActions: []string{},
	}
	objects, err := service.loadLayoutAuthoringReferencedSchemas(t.Context(), document)
	if err != nil {
		t.Fatal(err)
	}
	if len(objects) != 3 {
		t.Fatalf("loaded objects = %d, want 3", len(objects))
	}
	if !reflect.DeepEqual(repo.schemaReads, []string{"Account", "User", "Contact"}) {
		t.Fatalf("schema reads = %v", repo.schemaReads)
	}
}

func TestCreateLayoutForAdministrationOwnsOneTransactionAndDefersCacheInvalidation(t *testing.T) {
	repo := &layoutAdministrationRepo{}
	service := NewMetadataService(repo, nil, nil)
	service.SetTransactionManager(&layoutAdministrationTxManager{})
	service.cache.Store(&metadataSnapshot{})

	outer := context.WithValue(t.Context(), layoutAdministrationTxKey{}, struct{}{})
	outer = ports.WithAfterCommitHooks(outer)
	listFields := []string{}
	layout := &models.PageLayout{
		ID: "layout-a", ObjectAPIName: "WorkItem", LayoutName: "Work Item",
		Sections: []models.PageSection{{ID: "main", Fields: []string{"Subject"}}},
	}
	if err := service.CreateLayoutForAdministration(outer, layout, &listFields, "admin_create", nil); err != nil {
		t.Fatal(err)
	}
	if repo.outsideTxAccess {
		t.Fatal("Layout administration accessed the repository outside the caller transaction")
	}
	wantCalls := []string{"lock", "create", "read", "history", "history-max", "list-fields"}
	if !reflect.DeepEqual(repo.calls, wantCalls) {
		t.Fatalf("calls = %#v, want %#v", repo.calls, wantCalls)
	}
	if repo.listFields == nil || len(repo.listFields) != 0 {
		t.Fatalf("authored ListFields clear = %#v, want non-nil empty", repo.listFields)
	}
	var stored models.PageLayout
	if repo.history == nil || json.Unmarshal([]byte(repo.history.Config), &stored) != nil ||
		!reflect.DeepEqual(stored.Sections, layout.Sections) {
		t.Fatalf("history did not capture the committed authoring document: %#v", repo.history)
	}
	if service.cache.Load() == nil {
		t.Fatal("cache was invalidated before the outer transaction committed")
	}
	ports.RunAfterCommitHooks(outer)
	if service.cache.Load() != nil {
		t.Fatal("cache was not invalidated after commit")
	}
}

func TestSaveLayoutListFieldsForAdministrationDoesNotWriteLayoutOrHistory(t *testing.T) {
	repo := &layoutAdministrationRepo{}
	service := NewMetadataService(repo, nil, nil)
	service.SetTransactionManager(&layoutAdministrationTxManager{})
	service.cache.Store(&metadataSnapshot{})

	outer := context.WithValue(t.Context(), layoutAdministrationTxKey{}, struct{}{})
	outer = ports.WithAfterCommitHooks(outer)
	if err := service.SaveLayoutListFieldsForAdministration(outer, "Account", []string{"Name"}); err != nil {
		t.Fatal(err)
	}
	if repo.outsideTxAccess {
		t.Fatal("ListFields administration accessed the repository outside the caller transaction")
	}
	if !reflect.DeepEqual(repo.calls, []string{"list-fields"}) ||
		repo.listFieldsObject != "Account" || !reflect.DeepEqual(repo.listFields, []string{"Name"}) {
		t.Fatalf("ListFields-only write = calls %#v, object %q, fields %#v", repo.calls, repo.listFieldsObject, repo.listFields)
	}
	if service.cache.Load() == nil {
		t.Fatal("cache was invalidated before the outer transaction committed")
	}
	ports.RunAfterCommitHooks(outer)
	if service.cache.Load() != nil {
		t.Fatal("cache was not invalidated after the outer transaction committed")
	}
}

func TestResolveCurrentPageLayoutsReadsWorkspaceOnceAndPreservesRoleDistance(t *testing.T) {
	profileID, ownRole, parentRole := "profile-admin", "role-user", "role-parent"
	repo := &layoutAdministrationRepo{
		canonicalObject: "Account",
		schemas: map[string]*models.ObjectMetadata{
			"account": {ID: "object-account", APIName: "Account"},
		},
		assignments: []*models.LayoutAssignment{
			{ID: "assignment-default", ObjectAPIName: "Account", LayoutID: "layout-default"},
			{ID: "assignment-parent", ObjectAPIName: "Account", LayoutID: "layout-parent", RoleID: &parentRole, Specificity: 1, CreatedDate: time.Unix(1, 0)},
			{ID: "assignment-user", ObjectAPIName: "Account", LayoutID: "layout-user", RoleID: &ownRole, Specificity: 1, CreatedDate: time.Unix(2, 0)},
			{ID: "assignment-enterprise", ObjectAPIName: "Account", LayoutID: "layout-enterprise", ProfileID: &profileID, RecordTypeID: stringPointerForLayoutTest("rt-enterprise"), Specificity: 2},
		},
	}
	repo.layout = &models.PageLayout{ID: "layout-unused", ObjectAPIName: "Account"}
	// The aggregate reader used by this test exposes one layout field for legacy
	// administration tests, so use a dedicated repository for the full batch.
	batch := &currentPageLayoutBatchRepo{
		layoutAdministrationRepo: *repo,
		layouts: []*models.PageLayout{
			{ID: "layout-default", ObjectAPIName: "Account"},
			{ID: "layout-parent", ObjectAPIName: "Account"},
			{ID: "layout-user", ObjectAPIName: "Account"},
			{ID: "layout-enterprise", ObjectAPIName: "Account"},
		},
	}
	service := NewMetadataService(batch, nil, nil)
	service.SetTransactionManager(&layoutAdministrationTxManager{})

	resolved, err := service.ResolveCurrentPageLayouts(
		t.Context(), "Account", []string{ownRole, parentRole}, profileID,
		[]string{"rt-enterprise", "rt-other"},
	)
	if err != nil {
		t.Fatal(err)
	}
	if resolved["rt-enterprise"].ID != "layout-enterprise" || resolved["rt-other"].ID != "layout-user" {
		t.Fatalf("batch resolution = %#v", resolved)
	}
	if batch.autoNumberReads != 1 {
		t.Fatalf("autonumber reads = %d, want 1", batch.autoNumberReads)
	}
	if batch.aggregateReads != 1 {
		t.Fatalf("aggregate reads = %d, want 1", batch.aggregateReads)
	}
}

type currentPageLayoutBatchRepo struct {
	layoutAdministrationRepo
	layouts         []*models.PageLayout
	aggregateReads  int
	autoNumberReads int
}

func (repo *currentPageLayoutBatchRepo) ReadLayoutAuthoringAggregate(ctx context.Context, objectID string) (*models.LayoutAuthoringAggregate, error) {
	repo.requireTx(ctx)
	repo.aggregateReads++
	return &models.LayoutAuthoringAggregate{
		ObjectAPIName: objectID,
		Layouts:       append([]*models.PageLayout(nil), repo.layouts...),
		Assignments:   append([]*models.LayoutAssignment(nil), repo.assignments...),
	}, nil
}

func stringPointerForLayoutTest(value string) *string { return &value }

func TestSaveLayoutForAdministrationStopsBeforeListFieldsWhenHistoryFails(t *testing.T) {
	wantErr := errors.New("history unavailable")
	stored := &models.PageLayout{
		ID: "layout-a", ObjectAPIName: "WorkItem", LayoutName: "Work Item",
		Source: "admin_created",
	}
	repo := &layoutAdministrationRepo{layout: stored, historyFailure: wantErr}
	service := NewMetadataService(repo, nil, nil)
	service.SetTransactionManager(&layoutAdministrationTxManager{})
	ctx := context.WithValue(t.Context(), layoutAdministrationTxKey{}, struct{}{})
	ctx = ports.WithAfterCommitHooks(ctx)
	listFields := []string{"Subject"}
	snapshot, err := service.LockLayoutAuthoringAggregate(ctx, "WorkItem", LayoutAuthoringVersionPreconditions{})
	if err != nil {
		t.Fatal(err)
	}
	expected := snapshot.Versions.LayoutAggregateVersions[stored.ID]
	repo.calls = nil

	err = service.SaveLayoutForAdministration(ctx, &models.PageLayout{
		ID: "layout-a", ObjectAPIName: "WorkItem", LayoutName: "Work Item",
	}, expected, &listFields, "admin_update", nil)
	if !errors.Is(err, wantErr) {
		t.Fatalf("error = %v, want %v", err, wantErr)
	}
	if !reflect.DeepEqual(repo.calls, []string{"lock", "save", "read", "history"}) {
		t.Fatalf("calls after history failure = %#v", repo.calls)
	}
}

func TestSaveLayoutForAdministrationRejectsStaleAggregateWhenTimestampDoesNotChange(t *testing.T) {
	unchangedTimestamp := time.Date(2026, time.August, 30, 16, 50, 20, 0, time.UTC)
	repo := &layoutAdministrationRepo{layout: &models.PageLayout{
		ID: "layout-a", ObjectAPIName: "WorkItem", LayoutName: "Work Item",
		Source: "admin_created", LastModifiedDate: unchangedTimestamp,
		Sections: []models.PageSection{{ID: "main", Fields: []string{"Subject"}}},
	}}
	service := NewMetadataService(repo, nil, nil)
	service.SetTransactionManager(&layoutAdministrationTxManager{})
	ctx := context.WithValue(t.Context(), layoutAdministrationTxKey{}, struct{}{})

	snapshot, err := service.LockLayoutAuthoringAggregate(ctx, "WorkItem", LayoutAuthoringVersionPreconditions{})
	if err != nil {
		t.Fatal(err)
	}
	staleVersion := snapshot.Versions.LayoutAggregateVersions[repo.layout.ID]

	// Simulate another committed writer whose content changed inside the same
	// database timestamp tick. The audit timestamp is deliberately unchanged;
	// only the Catalog-owned aggregate digest can detect this stale request.
	repo.layout.Sections = []models.PageSection{{ID: "main", Fields: []string{"Subject", "Status"}}}
	repo.calls = nil
	proposed := &models.PageLayout{
		ID: "layout-a", ObjectAPIName: "WorkItem", LayoutName: "Work Item",
		Source: "admin_created", LastModifiedDate: unchangedTimestamp,
		Sections: []models.PageSection{{ID: "main", Fields: []string{"Subject", "OwnerId"}}},
	}
	err = service.SaveLayoutForAdministration(ctx, proposed, staleVersion, nil, "admin_update", nil)
	if err == nil || !apperrors.IsConflict(err) {
		t.Fatalf("same-timestamp stale update = %v, want conflict", err)
	}
	if err.Error() != "The layout configuration has changed. Reload the latest layout before saving." {
		t.Fatalf("stale update lost its remediation: %v", err)
	}
	if !reflect.DeepEqual(repo.calls, []string{"lock"}) {
		t.Fatalf("calls after stale aggregate = %#v, want lock only", repo.calls)
	}
	if got := repo.layout.Sections[0].Fields; !reflect.DeepEqual(got, []string{"Subject", "Status"}) {
		t.Fatalf("stale update replaced current fields: %#v", got)
	}
}

func TestSaveLayoutForAdministrationRequiresExpectedAggregateVersion(t *testing.T) {
	repo := &layoutAdministrationRepo{}
	service := NewMetadataService(repo, nil, nil)
	service.SetTransactionManager(&layoutAdministrationTxManager{})
	ctx := context.WithValue(t.Context(), layoutAdministrationTxKey{}, struct{}{})

	err := service.SaveLayoutForAdministration(ctx, &models.PageLayout{
		ID: "layout-a", ObjectAPIName: "WorkItem", LayoutName: "Work Item",
	}, "", nil, "admin_update", nil)
	if err == nil || !apperrors.IsValidation(err) {
		t.Fatalf("missing expected aggregate version = %v, want validation error", err)
	}
	if len(repo.calls) != 0 {
		t.Fatalf("missing precondition reached repository: %#v", repo.calls)
	}
}

func TestRestoreLayoutForAdministrationRestoresAuthoredName(t *testing.T) {
	historical := models.PageLayout{
		ID: "layout-a", ObjectAPIName: "WorkItem", LayoutName: "Historical Name",
		Sections: []models.PageSection{{ID: "main", Fields: []string{"Subject"}}},
	}
	historyJSON, err := json.Marshal(historical)
	if err != nil {
		t.Fatal(err)
	}
	repo := &layoutAdministrationRepo{
		layout: &models.PageLayout{
			ID: "layout-a", ObjectAPIName: "WorkItem", LayoutName: "Current Name",
			Source: "admin_created",
		},
		historyJSON: string(historyJSON),
	}
	service := NewMetadataService(repo, nil, nil)
	service.SetTransactionManager(&layoutAdministrationTxManager{})
	ctx := context.WithValue(t.Context(), layoutAdministrationTxKey{}, struct{}{})
	ctx = ports.WithAfterCommitHooks(ctx)

	actorID := "user-layout-admin"
	if err := service.RestoreLayoutForAdministration(ctx, "layout-a", 1, actorID); err != nil {
		t.Fatal(err)
	}
	if repo.layout == nil || repo.layout.LayoutName != "Historical Name" {
		t.Fatalf("restored LayoutName = %#v, want Historical Name", repo.layout)
	}
	if repo.history == nil || repo.history.ChangedByID == nil || *repo.history.ChangedByID != actorID {
		t.Fatalf("restore history actor = %#v, want %q", repo.history, actorID)
	}
	wantCalls := []string{"read", "lock", "history-config", "save", "read", "history", "history-max"}
	if !reflect.DeepEqual(repo.calls, wantCalls) {
		t.Fatalf("calls = %#v, want %#v", repo.calls, wantCalls)
	}
}

func TestRestoreLayoutForAdministrationRequiresActorBeforeRepositoryAccess(t *testing.T) {
	repo := &layoutAdministrationRepo{
		layout: &models.PageLayout{ID: "layout-a", ObjectAPIName: "WorkItem", LayoutName: "Current"},
	}
	service := NewMetadataService(repo, nil, nil)
	service.SetTransactionManager(&layoutAdministrationTxManager{})
	ctx := context.WithValue(t.Context(), layoutAdministrationTxKey{}, struct{}{})

	if err := service.RestoreLayoutForAdministration(ctx, "layout-a", 1, "  "); err == nil {
		t.Fatal("restore accepted a blank actor")
	}
	if len(repo.calls) != 0 {
		t.Fatalf("blank actor reached repository: %#v", repo.calls)
	}
}

func TestRestoreLayoutForAdministrationRejectsNonDocumentHistory(t *testing.T) {
	repo := &layoutAdministrationRepo{
		layout:      &models.PageLayout{ID: "layout-a", ObjectAPIName: "WorkItem", LayoutName: "Current", Source: "admin_created"},
		historyJSON: `{"id":"layout-a","objectApiName":"WorkItem","layoutName":"Historical"}{}`,
	}
	service := NewMetadataService(repo, nil, nil)
	service.SetTransactionManager(&layoutAdministrationTxManager{})
	ctx := context.WithValue(t.Context(), layoutAdministrationTxKey{}, struct{}{})

	if err := service.RestoreLayoutForAdministration(ctx, "layout-a", 1, "user-layout-admin"); err == nil {
		t.Fatal("restore accepted a history prefix followed by another JSON value")
	}
	if !reflect.DeepEqual(repo.calls, []string{"read", "lock", "history-config"}) {
		t.Fatalf("calls after invalid history = %#v", repo.calls)
	}
}

func TestDeleteLayoutForAdministrationUsesStrictDeleteRoute(t *testing.T) {
	repo := &layoutAdministrationRepo{
		layout: &models.PageLayout{
			ID: "layout-a", ObjectAPIName: "WorkItem", LayoutName: "Current", Source: "admin_created",
		},
	}
	service := NewMetadataService(repo, nil, nil)
	service.SetTransactionManager(&layoutAdministrationTxManager{})
	ctx := context.WithValue(t.Context(), layoutAdministrationTxKey{}, struct{}{})

	if err := service.DeleteLayoutForAdministration(ctx, "layout-a"); err != nil {
		t.Fatal(err)
	}
	wantCalls := []string{"read", "lock", "strict-delete", "assignment-cleanup"}
	if !reflect.DeepEqual(repo.calls, wantCalls) {
		t.Fatalf("calls = %#v, want %#v", repo.calls, wantCalls)
	}
}

func TestDeleteAssignmentForAdministrationUsesStrictDeleteRoute(t *testing.T) {
	profileID := "profile-a"
	repo := &layoutAdministrationRepo{assignments: []*models.LayoutAssignment{{
		ID: "assignment-a", ObjectAPIName: "WorkItem", LayoutID: "layout-a", ProfileID: &profileID,
	}}}
	service := NewMetadataService(repo, nil, nil)
	service.SetTransactionManager(&layoutAdministrationTxManager{})
	ctx := context.WithValue(t.Context(), layoutAdministrationTxKey{}, struct{}{})

	if err := service.DeleteAssignmentForAdministration(ctx, "assignment-a"); err != nil {
		t.Fatal(err)
	}
	if len(repo.assignments) != 0 || !reflect.DeepEqual(repo.calls, []string{"strict-assignment-delete"}) {
		t.Fatalf("assignments=%#v calls=%#v", repo.assignments, repo.calls)
	}
}

func TestDeleteLayoutForAdministrationExplainsConflictWithoutDeleting(t *testing.T) {
	profileID := "profile-a"
	for _, tc := range []struct {
		name        string
		isDefault   bool
		assignments []*models.LayoutAssignment
		message     string
	}{
		{name: "default flag", isDefault: true, message: "Choose another default layout before deleting this layout."},
		{name: "wildcard default authority", assignments: []*models.LayoutAssignment{{ID: "wildcard", ObjectAPIName: "WorkItem", LayoutID: "LAYOUT-A"}}, message: "Choose another default layout before deleting this layout."},
		{name: "profile assignment", assignments: []*models.LayoutAssignment{{ID: "profile", ObjectAPIName: "WorkItem", LayoutID: "LAYOUT-A", ProfileID: &profileID}}, message: "This layout is still assigned. Remove its assignments before deleting it."},
	} {
		t.Run(tc.name, func(t *testing.T) {
			repo := &layoutAdministrationRepo{
				layout:      &models.PageLayout{ID: "layout-a", ObjectAPIName: "WorkItem", LayoutName: "Current", IsDefault: tc.isDefault},
				assignments: tc.assignments,
			}
			service := NewMetadataService(repo, nil, nil)
			service.SetTransactionManager(&layoutAdministrationTxManager{})
			ctx := context.WithValue(t.Context(), layoutAdministrationTxKey{}, struct{}{})
			err := service.DeleteLayoutForAdministration(ctx, "layout-a")
			if err == nil || !apperrors.IsConflict(err) || err.Error() != tc.message {
				t.Fatalf("delete = %v, want conflict with remediation %q", err, tc.message)
			}
			if !reflect.DeepEqual(repo.calls, []string{"read", "lock"}) {
				t.Fatalf("rejected deletion reached mutation: %#v", repo.calls)
			}
		})
	}
}

func TestLockLayoutAuthoringAggregateReturnsTheFactsUsedByTheCatalogDigestOwner(t *testing.T) {
	repo := &layoutAdministrationRepo{listFields: []string{"Subject"}}
	service := NewMetadataService(repo, nil, nil)
	service.SetTransactionManager(&layoutAdministrationTxManager{})
	ctx := context.WithValue(t.Context(), layoutAdministrationTxKey{}, struct{}{})

	snapshot, err := service.LockLayoutAuthoringAggregate(ctx, "WorkItem", LayoutAuthoringVersionPreconditions{})
	if err != nil {
		t.Fatal(err)
	}
	actual := snapshot.Versions
	if actual.WorkspaceVersion == "" || snapshot.ObjectAPIName != "WorkItem" || !reflect.DeepEqual(repo.calls, []string{"lock"}) {
		t.Fatalf("locked aggregate snapshot = %#v, calls = %#v", snapshot, repo.calls)
	}
	repo.calls = nil
	if _, err := service.LockLayoutAuthoringAggregate(ctx, "WorkItem", LayoutAuthoringVersionPreconditions{
		WorkspaceVersion: &actual.WorkspaceVersion,
	}); err != nil {
		t.Fatalf("matching workspace version: %v", err)
	}
	stale := "sha256:stale"
	if _, err := service.LockLayoutAuthoringAggregate(ctx, "WorkItem", LayoutAuthoringVersionPreconditions{
		WorkspaceVersion: &stale,
	}); err == nil || !apperrors.IsConflict(err) {
		t.Fatalf("stale workspace version = %v, want conflict", err)
	}
	if _, err := service.LockLayoutAuthoringAggregate(ctx, "WorkItem", LayoutAuthoringVersionPreconditions{
		LayoutResourceVersions: map[string]string{"missing-layout": "sha256:expected"},
	}); err == nil || !apperrors.IsNotFound(err) {
		t.Fatalf("missing expected Layout = %v, want not found", err)
	}
}

func (repo *currentPageLayoutBatchRepo) ReadAutoNumberFieldNames(ctx context.Context, _ string) (map[string]struct{}, error) {
	repo.requireTx(ctx)
	repo.autoNumberReads++
	return nil, nil
}
