package metadata

import (
	"context"
	"fmt"
	"strings"
	"testing"

	"github.com/nexuscrm/backend/internal/domain/ports"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// ============================================================================
// Enhanced mock that tracks dashboard and app state for CRUD tests
// ============================================================================

type statefulMockRepo struct {
	mockMetadataRepo
	// Dashboard state
	dashboards map[string]*models.DashboardConfig
	// App state
	apps           map[string]*models.AppConfig
	appMenuCreated []string
	// Error overrides
	getAllDashboardsErr error
	getDashboardErr     error
	createDashboardErr  error
	updateDashboardErr  error
	deleteDashboardErr  error
	getAllAppsErr       error
	getAppErr           error
	createAppErr        error
	createAppMenuErr    error
	updateAppErr        error
	deleteAppErr        error
}

func newStatefulMockRepo() *statefulMockRepo {
	base := newMockMetadataRepo()
	return &statefulMockRepo{
		mockMetadataRepo: *base,
		dashboards:       make(map[string]*models.DashboardConfig),
		apps:             make(map[string]*models.AppConfig),
	}
}

// Dashboard overrides
func (m *statefulMockRepo) GetAllDashboards(_ context.Context) ([]*models.DashboardConfig, error) {
	if m.getAllDashboardsErr != nil {
		return nil, m.getAllDashboardsErr
	}
	result := make([]*models.DashboardConfig, 0, len(m.dashboards))
	for _, d := range m.dashboards {
		result = append(result, d)
	}
	return result, nil
}

func (m *statefulMockRepo) GetDashboard(_ context.Context, id string) (*models.DashboardConfig, error) {
	if m.getDashboardErr != nil {
		return nil, m.getDashboardErr
	}
	d, ok := m.dashboards[id]
	if !ok {
		return nil, fmt.Errorf("not found")
	}
	return d, nil
}

func (m *statefulMockRepo) CreateDashboard(_ context.Context, d *models.DashboardConfig) error {
	if m.createDashboardErr != nil {
		return m.createDashboardErr
	}
	m.dashboards[d.ID] = d
	return nil
}

func (m *statefulMockRepo) UpdateDashboard(_ context.Context, id string, d *models.DashboardConfig) error {
	if m.updateDashboardErr != nil {
		return m.updateDashboardErr
	}
	m.dashboards[id] = d
	return nil
}

func (m *statefulMockRepo) DeleteDashboard(_ context.Context, id string) error {
	if m.deleteDashboardErr != nil {
		return m.deleteDashboardErr
	}
	delete(m.dashboards, id)
	return nil
}

// App overrides
func (m *statefulMockRepo) GetAllApps(_ context.Context) ([]*models.AppConfig, error) {
	if m.getAllAppsErr != nil {
		return nil, m.getAllAppsErr
	}
	result := make([]*models.AppConfig, 0, len(m.apps))
	for _, a := range m.apps {
		result = append(result, a)
	}
	return result, nil
}

func (m *statefulMockRepo) GetApp(_ context.Context, id string) (*models.AppConfig, error) {
	if m.getAppErr != nil {
		return nil, m.getAppErr
	}
	a, ok := m.apps[id]
	if !ok {
		return nil, nil // App methods return nil, nil when not found
	}
	return a, nil
}

func (m *statefulMockRepo) CreateApp(_ context.Context, app *models.AppConfig) error {
	if m.createAppErr != nil {
		return m.createAppErr
	}
	m.apps[app.ID] = app
	return nil
}

func (m *statefulMockRepo) CreateAppMenuItem(_ context.Context, app *models.AppConfig) error {
	if m.createAppMenuErr != nil {
		return m.createAppMenuErr
	}
	m.appMenuCreated = append(m.appMenuCreated, app.ID)
	return nil
}

func (m *statefulMockRepo) UpdateApp(_ context.Context, id string, app *models.AppConfig) error {
	if m.updateAppErr != nil {
		return m.updateAppErr
	}
	m.apps[id] = app
	return nil
}

func (m *statefulMockRepo) DeleteApp(_ context.Context, id string) error {
	if m.deleteAppErr != nil {
		return m.deleteAppErr
	}
	delete(m.apps, id)
	return nil
}

// newTestServiceStateful creates a MetadataService with the stateful mock
func newTestServiceStateful() (*MetadataService, *statefulMockRepo, *mockSchemaRepo, *mockEventPublisher) {
	repo := newStatefulMockRepo()
	schemaRepo := newMockSchemaRepo()
	eventPub := newMockEventPublisher()
	schemaMgr := NewSchemaManager(schemaRepo)
	ms := NewMetadataService(repo, schemaMgr, eventPub)
	return ms, repo, schemaRepo, eventPub
}

// ============================================================================
// Tests: Dashboard CRUD
// ============================================================================

func TestMetadataService_GetDashboards_Empty(t *testing.T) {
	ms, _, _, _ := newTestServiceStateful()
	ctx := context.Background()

	result := ms.GetDashboards(ctx, nil)
	assert.NotNil(t, result)
	assert.Empty(t, result)
}

func TestMetadataService_GetDashboards_WithData(t *testing.T) {
	ms, repo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	repo.dashboards["d1"] = &models.DashboardConfig{ID: "d1", Label: "Sales Dashboard"}
	repo.dashboards["d2"] = &models.DashboardConfig{ID: "d2", Label: "Service Dashboard"}

	result := ms.GetDashboards(ctx, nil)
	assert.Len(t, result, 2)
}

func TestMetadataService_GetDashboards_RepoError(t *testing.T) {
	ms, repo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	repo.getAllDashboardsErr = fmt.Errorf("database error")

	result := ms.GetDashboards(ctx, nil)
	assert.NotNil(t, result)
	assert.Empty(t, result)
}

func TestMetadataService_GetDashboard_Found(t *testing.T) {
	ms, repo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	repo.dashboards["d1"] = &models.DashboardConfig{ID: "d1", Label: "Sales Dashboard"}

	result := ms.GetDashboard(ctx, "d1")
	require.NotNil(t, result)
	assert.Equal(t, "Sales Dashboard", result.Label)
}

func TestMetadataService_GetDashboard_NotFound(t *testing.T) {
	ms, _, _, _ := newTestServiceStateful()
	ctx := context.Background()

	result := ms.GetDashboard(ctx, "nonexistent")
	assert.Nil(t, result)
}

func TestMetadataService_CreateDashboard_HappyPath(t *testing.T) {
	ms, repo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	dashboard := &models.DashboardConfig{
		Label: "New Dashboard",
		Widgets: []models.WidgetConfig{
			{Title: "Widget 1", Type: "chart"},
		},
	}

	err := ms.CreateDashboard(ctx, dashboard)
	require.NoError(t, err)

	// ID should be auto-generated
	assert.NotEmpty(t, dashboard.ID)

	// Layout should default to "two-column"
	assert.Equal(t, "two-column", dashboard.Layout)

	// Widget should have ID assigned
	assert.NotEmpty(t, dashboard.Widgets[0].ID)

	// Should be in repo
	assert.Len(t, repo.dashboards, 1)
}

func TestMetadataService_CreateDashboard_MissingLabel(t *testing.T) {
	ms, _, _, _ := newTestServiceStateful()
	ctx := context.Background()

	dashboard := &models.DashboardConfig{
		Label: "", // Missing
	}

	err := ms.CreateDashboard(ctx, dashboard)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "label")
}

func TestMetadataService_CreateDashboard_DuplicateID(t *testing.T) {
	ms, repo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	repo.dashboards["existing_id"] = &models.DashboardConfig{ID: "existing_id", Label: "Existing"}

	dashboard := &models.DashboardConfig{
		ID:    "existing_id",
		Label: "Duplicate",
	}

	err := ms.CreateDashboard(ctx, dashboard)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "already exists")
}

func TestMetadataService_CreateDashboard_CustomLayout(t *testing.T) {
	ms, _, _, _ := newTestServiceStateful()
	ctx := context.Background()

	dashboard := &models.DashboardConfig{
		Label:  "Custom Layout Dashboard",
		Layout: "three-column",
	}

	err := ms.CreateDashboard(ctx, dashboard)
	require.NoError(t, err)

	// Custom layout should be preserved, not overwritten
	assert.Equal(t, "three-column", dashboard.Layout)
}

func TestMetadataService_CreateDashboard_RepoError(t *testing.T) {
	ms, repo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	repo.createDashboardErr = fmt.Errorf("insert failed")

	dashboard := &models.DashboardConfig{
		Label: "Will Fail",
	}

	err := ms.CreateDashboard(ctx, dashboard)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "failed to insert dashboard")
}

func TestMetadataService_UpdateDashboard_HappyPath(t *testing.T) {
	ms, repo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	desc := "Original description"
	repo.dashboards["d1"] = &models.DashboardConfig{
		ID:          "d1",
		Label:       "Original",
		Layout:      "two-column",
		Description: &desc,
	}

	newDesc := "Updated description"
	updates := &models.DashboardConfig{
		Label:       "Updated Dashboard",
		Layout:      "three-column",
		Description: &newDesc,
		FilterMode:  "date",
		LayoutMode:  "fixed",
	}

	err := ms.UpdateDashboard(ctx, "d1", updates)
	require.NoError(t, err)
}

func TestMetadataService_UpdateDashboard_NotFound(t *testing.T) {
	ms, _, _, _ := newTestServiceStateful()
	ctx := context.Background()

	updates := &models.DashboardConfig{Label: "Update"}

	err := ms.UpdateDashboard(ctx, "nonexistent", updates)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "not found")
}

func TestMetadataService_UpdateDashboard_RepoError(t *testing.T) {
	ms, repo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	repo.dashboards["d1"] = &models.DashboardConfig{ID: "d1", Label: "Existing"}
	repo.updateDashboardErr = fmt.Errorf("update failed")

	err := ms.UpdateDashboard(ctx, "d1", &models.DashboardConfig{Label: "New"})
	require.Error(t, err)
	assert.Contains(t, err.Error(), "failed to update dashboard")
}

func TestMetadataService_UpdateDashboard_PartialUpdate(t *testing.T) {
	ms, repo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	startDate := "2024-01-01"
	endDate := "2024-12-31"
	repo.dashboards["d1"] = &models.DashboardConfig{
		ID:        "d1",
		Label:     "Original",
		Layout:    "two-column",
		StartDate: &startDate,
		EndDate:   &endDate,
	}

	// Only update date range type, keep everything else
	updates := &models.DashboardConfig{
		DateRangeType: "custom",
	}

	err := ms.UpdateDashboard(ctx, "d1", updates)
	require.NoError(t, err)

	// Verify the original data was preserved and new field merged
	saved := repo.dashboards["d1"]
	assert.Equal(t, "Original", saved.Label)
	assert.Equal(t, "custom", saved.DateRangeType)
}

func TestMetadataService_DeleteDashboard_HappyPath(t *testing.T) {
	ms, repo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	repo.dashboards["d1"] = &models.DashboardConfig{ID: "d1", Label: "To Delete"}

	err := ms.DeleteDashboard(ctx, "d1")
	require.NoError(t, err)
	assert.Empty(t, repo.dashboards)
}

func TestMetadataService_DeleteDashboard_NotFound(t *testing.T) {
	ms, _, _, _ := newTestServiceStateful()
	ctx := context.Background()

	err := ms.DeleteDashboard(ctx, "nonexistent")
	require.Error(t, err)
	assert.Contains(t, err.Error(), "not found")
}

// ============================================================================
// Tests: normalizeWidgets helper
// ============================================================================

func TestNormalizeWidgets_AssignsIDs(t *testing.T) {
	widgets := []models.WidgetConfig{
		{Title: "Widget 1", Type: "chart", ID: ""},
		{Title: "Widget 2", Type: "metric", ID: "existing_id"},
		{Title: "Widget 3", Type: "table", ID: ""},
	}

	result := normalizeWidgets(widgets)
	assert.Len(t, result, 3)

	// Widget without ID should get one assigned
	assert.NotEmpty(t, result[0].ID)
	// Widget with existing ID should keep it
	assert.Equal(t, "existing_id", result[1].ID)
	// Another widget without ID should get one assigned
	assert.NotEmpty(t, result[2].ID)
}

func TestNormalizeWidgets_EmptySlice(t *testing.T) {
	result := normalizeWidgets([]models.WidgetConfig{})
	assert.Empty(t, result)
}

func TestNormalizeWidgets_NilSlice(t *testing.T) {
	result := normalizeWidgets(nil)
	assert.Nil(t, result)
}

// ============================================================================
// Tests: App CRUD (unit tests, no DB)
// ============================================================================

func TestMetadataService_GetApps_Empty(t *testing.T) {
	ms, _, _, _ := newTestServiceStateful()
	ctx := context.Background()

	result := ms.GetApps(ctx)
	assert.NotNil(t, result)
	assert.Empty(t, result)
}

func TestMetadataService_GetApps_WithData(t *testing.T) {
	ms, repo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	repo.apps["app1"] = &models.AppConfig{ID: "app1", Label: "Sales"}
	repo.apps["app2"] = &models.AppConfig{ID: "app2", Label: "Service"}

	result := ms.GetApps(ctx)
	assert.Len(t, result, 2)
}

func TestMetadataService_GetApps_RepoError(t *testing.T) {
	ms, repo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	repo.getAllAppsErr = fmt.Errorf("database error")

	result := ms.GetApps(ctx)
	assert.NotNil(t, result)
	assert.Empty(t, result)
}

func TestMetadataService_GetApp_Found(t *testing.T) {
	ms, repo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	repo.apps["app1"] = &models.AppConfig{ID: "app1", Label: "Sales"}

	result := ms.GetApp(ctx, "app1")
	require.NotNil(t, result)
	assert.Equal(t, "Sales", result.Label)
}

func TestMetadataService_GetApp_NotFound(t *testing.T) {
	ms, _, _, _ := newTestServiceStateful()
	ctx := context.Background()

	result := ms.GetApp(ctx, "nonexistent")
	assert.Nil(t, result)
}

func TestMetadataService_CreateApp_HappyPath(t *testing.T) {
	ms, repo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	app := &models.AppConfig{
		ID:    "sales_app",
		Label: "Sales App",
		Icon:  "standard:opportunity",
	}

	err := ms.CreateApp(ctx, app)
	require.NoError(t, err)

	// Timestamps should be set
	assert.False(t, app.CreatedDate.IsZero())
	assert.False(t, app.LastModifiedDate.IsZero())

	// Should be in repo
	assert.Contains(t, repo.apps, "sales_app")

	// AppMenuItem should be created
	assert.Contains(t, repo.appMenuCreated, "sales_app")
}

func TestMetadataService_CreateApp_MissingID(t *testing.T) {
	ms, _, _, _ := newTestServiceStateful()
	ctx := context.Background()

	app := &models.AppConfig{
		ID:    "", // Missing
		Label: "Test App",
	}

	err := ms.CreateApp(ctx, app)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "app ID is required")
}

func TestMetadataService_CreateApp_MissingLabel(t *testing.T) {
	ms, _, _, _ := newTestServiceStateful()
	ctx := context.Background()

	app := &models.AppConfig{
		ID:    "test_app",
		Label: "", // Missing
	}

	err := ms.CreateApp(ctx, app)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "app label is required")
}

func TestMetadataService_CreateApp_DuplicateID(t *testing.T) {
	ms, repo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	repo.apps["existing_app"] = &models.AppConfig{ID: "existing_app", Label: "Existing"}

	app := &models.AppConfig{
		ID:    "existing_app",
		Label: "Duplicate",
	}

	err := ms.CreateApp(ctx, app)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "already exists")
}

func TestMetadataService_CreateApp_RepoError(t *testing.T) {
	ms, repo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	repo.createAppErr = fmt.Errorf("insert failed")

	app := &models.AppConfig{
		ID:    "test_app",
		Label: "Test",
	}

	err := ms.CreateApp(ctx, app)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "failed to insert app")
}

func TestMetadataService_CreateApp_GetAppError(t *testing.T) {
	ms, repo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	repo.getAppErr = fmt.Errorf("check failed")

	app := &models.AppConfig{
		ID:    "test_app",
		Label: "Test",
	}

	err := ms.CreateApp(ctx, app)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "failed to check app existence")
}

func TestMetadataService_CreateApp_AppMenuItemError(t *testing.T) {
	ms, repo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	repo.createAppMenuErr = fmt.Errorf("menu insert failed")

	app := &models.AppConfig{
		ID:    "test_app",
		Label: "Test",
	}

	// Should succeed even if AppMenuItem fails (non-fatal)
	err := ms.CreateApp(ctx, app)
	require.NoError(t, err)
	assert.Contains(t, repo.apps, "test_app")
}

func TestMetadataService_UpdateApp_HappyPath(t *testing.T) {
	ms, repo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	repo.apps["app1"] = &models.AppConfig{
		ID:          "app1",
		Label:       "Original",
		Description: "Old desc",
		Icon:        "old_icon",
		Color:       "red",
		IsDefault:   false,
	}

	updates := &models.AppConfig{
		Label:       "Updated App",
		Description: "New desc",
		Icon:        "new_icon",
		Color:       "blue",
		IsDefault:   true,
		NavigationItems: []models.NavigationItem{
			{Label: "Home"},
		},
	}

	err := ms.UpdateApp(ctx, "app1", updates)
	require.NoError(t, err)

	// Verify merged fields
	saved := repo.apps["app1"]
	assert.Equal(t, "Updated App", saved.Label)
	assert.Equal(t, "New desc", saved.Description)
	assert.Equal(t, "new_icon", saved.Icon)
	assert.Equal(t, "blue", saved.Color)
	assert.True(t, saved.IsDefault)
	assert.Len(t, saved.NavigationItems, 1)
	assert.False(t, saved.LastModifiedDate.IsZero())
}

func TestMetadataService_UpdateApp_NotFound(t *testing.T) {
	ms, _, _, _ := newTestServiceStateful()
	ctx := context.Background()

	err := ms.UpdateApp(ctx, "nonexistent", &models.AppConfig{Label: "Update"})
	require.Error(t, err)
	assert.Contains(t, err.Error(), "not found")
}

func TestMetadataService_UpdateApp_PartialUpdate(t *testing.T) {
	ms, repo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	repo.apps["app1"] = &models.AppConfig{
		ID:              "app1",
		Label:           "Original",
		Description:     "Keep this",
		Icon:            "keep_icon",
		Color:           "keep_color",
		NavigationItems: []models.NavigationItem{{Label: "Home"}},
		UtilityItems:    []models.UtilityItem{{Label: "Chat"}},
	}

	// Only update label
	updates := &models.AppConfig{
		Label: "New Label",
	}

	err := ms.UpdateApp(ctx, "app1", updates)
	require.NoError(t, err)

	saved := repo.apps["app1"]
	assert.Equal(t, "New Label", saved.Label)
	assert.Equal(t, "Keep this", saved.Description)
	assert.Equal(t, "keep_icon", saved.Icon)
	assert.Equal(t, "keep_color", saved.Color)
	// NavigationItems and UtilityItems should be preserved (nil in updates)
	assert.Len(t, saved.NavigationItems, 1)
	assert.Len(t, saved.UtilityItems, 1)
}

func TestMetadataService_UpdateApp_AssignedProfiles(t *testing.T) {
	ms, repo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	repo.apps["app1"] = &models.AppConfig{
		ID:               "app1",
		Label:            "App",
		AssignedProfiles: []string{"admin"},
	}

	// Update with empty slice = visible to all
	updates := &models.AppConfig{
		AssignedProfiles: []string{},
	}

	err := ms.UpdateApp(ctx, "app1", updates)
	require.NoError(t, err)

	saved := repo.apps["app1"]
	assert.NotNil(t, saved.AssignedProfiles)
	assert.Empty(t, saved.AssignedProfiles)
}

func TestMetadataService_UpdateApp_GetAppRepoError(t *testing.T) {
	ms, repo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	repo.getAppErr = fmt.Errorf("database error")

	err := ms.UpdateApp(ctx, "app1", &models.AppConfig{Label: "Update"})
	require.Error(t, err)
	assert.Contains(t, err.Error(), "failed to check app existence")
}

func TestMetadataService_DeleteApp_HappyPath(t *testing.T) {
	ms, repo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	repo.apps["app1"] = &models.AppConfig{ID: "app1", Label: "To Delete"}

	err := ms.DeleteApp(ctx, "app1")
	require.NoError(t, err)
	assert.Empty(t, repo.apps)
}

func TestMetadataService_UpdateAppTx_HappyPath(t *testing.T) {
	ms, _, _, _ := newTestServiceStateful()
	ctx := context.Background()

	updates := &models.AppConfig{
		ID:    "app1",
		Label: "Updated in Tx",
	}

	// UpdateAppTx delegates to repo.UpdateAppWithTx which is a no-op in the base mock
	err := ms.UpdateAppTx(ctx, nil, "app1", updates)
	require.NoError(t, err)

	// ID should be set to appID
	assert.Equal(t, "app1", updates.ID)
	// LastModifiedDate should be set
	assert.False(t, updates.LastModifiedDate.IsZero())
}

// ============================================================================
// Tests: Layout - GetLayout with profile
// ============================================================================

func TestMetadataService_GetLayout_WithProfile(t *testing.T) {
	ms, metaRepo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	profileID := "profile_admin"
	layoutID := "layout_custom"

	layout := &models.PageLayout{
		ID:            layoutID,
		ObjectAPIName: "Account",
		LayoutName:    "Custom Layout",
		LayoutVersion: currentLayoutVersion,
		Sections:      []models.PageSection{{Label: "Custom", Fields: []string{"Name"}}},
	}
	metaRepo.layouts["account"] = []*models.PageLayout{layout}
	metaRepo.layoutByID[layoutID] = layout

	result := ms.GetLayout(ctx, "Account", &profileID)
	require.NotNil(t, result)
	assert.Equal(t, layoutID, result.ID)
	assert.Equal(t, "Custom Layout", result.LayoutName)
}

func TestMetadataService_GetLayout_WithRelatedLists(t *testing.T) {
	ms, metaRepo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	// Layout with pre-populated related lists (filled by bootstrap backfill, not runtime)
	metaRepo.layouts["account"] = []*models.PageLayout{
		{
			ID:            "layout_1",
			ObjectAPIName: "Account",
			LayoutName:    "Default",
			LayoutVersion: currentLayoutVersion,
			Sections:      []models.PageSection{{Label: "Info", Fields: []string{"Name"}}},
			RelatedLists: []models.RelatedListConfig{
				{
					ID:             "Contact-AccountId",
					Label:          "Contacts",
					SecondaryLabel: "via Account",
					ObjectAPIName:  "Contact",
					LookupField:    "AccountId",
					Fields:         []string{"Name", "Status", "Amount"},
				},
				{
					ID:             "Opportunity-AccountId",
					Label:          "Opportunities",
					SecondaryLabel: "via Account",
					ObjectAPIName:  "Opportunity",
					LookupField:    "AccountId",
					Fields:         []string{constants.FieldName, constants.FieldCreatedDate},
				},
			},
		},
	}

	result := ms.GetLayout(ctx, "Account", nil)
	require.NotNil(t, result)

	// Related lists come from stored layout — no runtime augmentation
	assert.Len(t, result.RelatedLists, 2)

	foundContact := false
	foundOpp := false
	for _, rl := range result.RelatedLists {
		if rl.ObjectAPIName == "Contact" {
			foundContact = true
			assert.Equal(t, "Contacts", rl.Label)
			assert.Equal(t, "AccountId", rl.LookupField)
			assert.Equal(t, []string{"Name", "Status", "Amount"}, rl.Fields)
		}
		if rl.ObjectAPIName == "Opportunity" {
			foundOpp = true
			assert.Equal(t, "Opportunities", rl.Label)
			assert.Contains(t, rl.Fields, constants.FieldName)
			assert.Contains(t, rl.Fields, constants.FieldCreatedDate)
		}
	}
	assert.True(t, foundContact)
	assert.True(t, foundOpp)
}

func TestMetadataService_GetLayout_RepairsExistingRelatedListWithEmptyFields(t *testing.T) {
	ms, metaRepo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{
		APIName:     "Account",
		Label:       "Account",
		PluralLabel: "Accounts",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Label: "Account Name", Type: constants.FieldTypeString, IsNameField: true},
		},
	}
	metaRepo.schemas["contact"] = &models.ObjectMetadata{
		APIName:     "Contact",
		Label:       "Contact",
		PluralLabel: "Contacts",
		Fields: []models.FieldMetadata{
			{APIName: "FullName", Label: "Full Name", Type: constants.FieldTypeString, IsNameField: true},
		},
	}

	metaRepo.layouts["account"] = []*models.PageLayout{
		{
			ID:            "layout_1",
			ObjectAPIName: "Account",
			LayoutName:    "Default",
			LayoutVersion: currentLayoutVersion,
			Sections:      []models.PageSection{{Label: "Info", Fields: []string{"Name"}}},
			RelatedLists: []models.RelatedListConfig{
				{
					ID:            "contact-AccountId",
					Label:         "Contacts",
					ObjectAPIName: "Contact",
					LookupField:   "AccountId",
					Fields:        []string{},
				},
			},
		},
	}

	customFields := `["FullName","Email"]`
	metaRepo.relatedListCfgs["account"] = []ports.RelatedListConfig{
		{
			ChildObjectAPI:    "Contact",
			LookupFieldAPI:    "AccountId",
			ChildPluralLabel:  "Contacts",
			RelatedListFields: &customFields,
		},
	}

	result := ms.GetLayout(ctx, "Account", nil)
	require.NotNil(t, result)
	require.Len(t, result.RelatedLists, 1)
	assert.Equal(t, []string{"FullName", "Email"}, result.RelatedLists[0].Fields)
	assert.Equal(t, "Contacts", result.RelatedLists[0].Label)
}

func TestMetadataService_GetLayout_CuratesCampaignHierarchy(t *testing.T) {
	ms, metaRepo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	relName := "Campaign"
	metaRepo.schemas["campaign"] = &models.ObjectMetadata{
		APIName:     "Campaign",
		Label:       "Campaign",
		PluralLabel: "Campaigns",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Label: "Campaign Name", Type: constants.FieldTypeString, IsNameField: true},
			{APIName: "ParentId", Label: "Parent Campaign", Type: constants.FieldTypeReference, ReferenceTo: []string{"Campaign"}, RelationshipName: &relName},
			{APIName: "ActualCost", Label: "Actual Cost in Campaign", Type: constants.FieldTypeCurrency},
			{APIName: "AmountAllOpportunities", Label: "Value Opportunities in Campaign", Type: constants.FieldTypeCurrency},
			{APIName: "AmountWonOpportunities", Label: "Value Won Opportunities in Campaign", Type: constants.FieldTypeCurrency},
		},
	}

	metaRepo.layouts["campaign"] = []*models.PageLayout{
		{
			ID:            "default_campaign",
			ObjectAPIName: "Campaign",
			LayoutName:    "Campaign Layout",
			LayoutVersion: currentLayoutVersion,
			Sections:      []models.PageSection{{Label: "Campaign Information", Fields: []string{"Name"}}},
			RelatedLists: []models.RelatedListConfig{
				{
					ID:            "campaign-ParentId",
					Label:         "Campaigns",
					ObjectAPIName: "campaign",
					LookupField:   "ParentId",
					Fields:        []string{},
				},
			},
		},
	}

	metaRepo.relatedListCfgs["campaign"] = []ports.RelatedListConfig{
		{
			ChildObjectAPI:   "campaign",
			LookupFieldAPI:   "ParentId",
			ChildPluralLabel: "Campaigns",
		},
	}

	result := ms.GetLayout(ctx, "Campaign", nil)
	require.NotNil(t, result)
	require.Len(t, result.RelatedLists, 1)
	assert.Equal(t, campaignHierarchyLabel, result.RelatedLists[0].Label)
	assert.Equal(t, campaignHierarchyFields, result.RelatedLists[0].Fields)
}

func TestMetadataService_GetLayout_EmptyRelatedListsStaysEmpty(t *testing.T) {
	ms, metaRepo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	metaRepo.layouts["account"] = []*models.PageLayout{
		{
			ID:            "layout_1",
			ObjectAPIName: "Account",
			LayoutName:    "Default",
			LayoutVersion: currentLayoutVersion,
			Sections:      []models.PageSection{{Label: "Info", Fields: []string{"Name"}}},
			RelatedLists:  []models.RelatedListConfig{},
		},
	}

	metaRepo.relatedListCfgs["account"] = []ports.RelatedListConfig{
		{ChildObjectAPI: "Contact", LookupFieldAPI: "AccountId", ChildPluralLabel: "Contacts"},
		{ChildObjectAPI: "Opportunity", LookupFieldAPI: "AccountId", ChildPluralLabel: "Opportunities"},
	}

	result := ms.GetLayout(ctx, "Account", nil)
	require.NotNil(t, result)
	assert.Empty(t, result.RelatedLists, "Runtime must not auto-discover new related lists into an empty layout")
}

func TestMetadataService_GetLayout_RelatedListDedup(t *testing.T) {
	ms, metaRepo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	// Layout already has a related list
	metaRepo.layouts["account"] = []*models.PageLayout{
		{
			ID:            "layout_1",
			ObjectAPIName: "Account",
			LayoutVersion: currentLayoutVersion,
			Sections:      []models.PageSection{{Label: "Info", Fields: []string{"Name"}}},
			RelatedLists: []models.RelatedListConfig{
				{ObjectAPIName: "Contact", LookupField: "AccountId", Label: "Contacts"},
			},
		},
	}

	// Same related list returned from repo
	metaRepo.relatedListCfgs["account"] = []ports.RelatedListConfig{
		{
			ChildObjectAPI:   "Contact",
			LookupFieldAPI:   "AccountId",
			ChildPluralLabel: "Contacts",
		},
	}

	result := ms.GetLayout(ctx, "Account", nil)
	require.NotNil(t, result)

	// Should NOT duplicate the related list
	contactCount := 0
	for _, rl := range result.RelatedLists {
		if rl.ObjectAPIName == "Contact" {
			contactCount++
		}
	}
	assert.Equal(t, 1, contactCount)
}

func TestMetadataService_GetRelatedListCandidates_UsesRelationshipLevelConfig(t *testing.T) {
	ms, metaRepo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	metaRepo.schemas["contact"] = &models.ObjectMetadata{
		APIName: "Contact",
		Fields: []models.FieldMetadata{
			{APIName: "FullName", IsNameField: true},
		},
	}

	label := "Key Contacts"
	fields := `["FullName","Email"]`
	metaRepo.relatedListCfgs["account"] = []ports.RelatedListConfig{
		{
			ChildObjectAPI:    "Contact",
			ChildPluralLabel:  "Contacts",
			LookupFieldAPI:    "AccountId",
			LookupFieldLabel:  "Account",
			RelatedListLabel:  &label,
			RelatedListFields: &fields,
		},
	}

	candidates, err := ms.GetRelatedListCandidates(ctx, "Account")
	require.NoError(t, err)
	require.Len(t, candidates, 1)

	// Label uses ChildPluralLabel, not RelatedListLabel (which contains relationship names)
	assert.Equal(t, "Contacts", candidates[0].Label)
	assert.Equal(t, "via Account", candidates[0].SecondaryLabel)
	assert.Equal(t, []string{"FullName", "Email"}, candidates[0].Fields)
}

func TestMetadataService_GetRelatedListCandidates_QuoteIncludesSyntheticFiles(t *testing.T) {
	ms, metaRepo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	metaRepo.schemas[constants.TableFile] = &models.ObjectMetadata{
		APIName:     constants.TableFile,
		Label:       "File",
		PluralLabel: "Files",
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldName, IsNameField: true},
			{APIName: constants.FieldCreatedDate, IsSystem: true},
		},
	}

	candidates, err := ms.GetRelatedListCandidates(ctx, "Quote")
	require.NoError(t, err)

	var filesCandidate *RelatedListCandidate
	for i := range candidates {
		if strings.EqualFold(candidates[i].ObjectAPIName, constants.TableFile) &&
			strings.EqualFold(candidates[i].LookupField, constants.FieldSysFile_ParentID) {
			filesCandidate = &candidates[i]
			break
		}
	}

	require.NotNil(t, filesCandidate, "expected Quote related list candidates to include Files")
	assert.Equal(t, "Files", filesCandidate.Label)
	assert.True(t, filesCandidate.IsRecommended)
	assert.Equal(t, string(BucketRecommended), filesCandidate.Bucket)
	assert.Equal(t, []string{constants.FieldName, constants.FieldCreatedDate}, filesCandidate.Fields)
}

func TestMetadataService_GetRelatedListCandidates_OrderIncludesTenantInvoiceCollections(t *testing.T) {
	ms, metaRepo, _, _ := newTestServiceStateful()
	ctx := context.Background()
	ResetTenantRelatedListRulesForTest()
	defer ResetTenantRelatedListRulesForTest()
	require.NoError(t, RegisterTenantRelatedListRules([]RelatedListRule{
		{
			Key:              "order-invoice-collections",
			ParentObjectAPI:  "Order",
			ChildObjectAPI:   "invoice_collection__c",
			LookupField:      "Order__c",
			Bucket:           BucketRecommended,
			SortOrder:        40,
			AppendIfMissing:  true,
			Label:            "Invoice Collections",
			SecondaryLabel:   "via Order",
			LookupFieldLabel: "Order",
			Fields:           []string{constants.FieldName, constants.FieldCreatedDate},
		},
	}))

	metaRepo.schemas["invoice_collection__c"] = &models.ObjectMetadata{
		APIName:     "invoice_collection__c",
		Label:       "Invoice Collection",
		PluralLabel: "Invoice Collections",
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldName, IsNameField: true},
			{APIName: constants.FieldCreatedDate, IsSystem: true},
		},
	}
	metaRepo.relatedListCfgs["order"] = []ports.RelatedListConfig{
		{ChildObjectAPI: "OrderItem", LookupFieldAPI: "OrderId", ChildPluralLabel: "Order Items"},
		{ChildObjectAPI: "Contract", LookupFieldAPI: "OrderId", ChildPluralLabel: "Contracts"},
	}

	candidates, err := ms.GetRelatedListCandidates(ctx, "Order")
	require.NoError(t, err)

	var invoiceCollections *RelatedListCandidate
	for i := range candidates {
		if strings.EqualFold(candidates[i].ObjectAPIName, "invoice_collection__c") &&
			strings.EqualFold(candidates[i].LookupField, "Order__c") {
			invoiceCollections = &candidates[i]
			break
		}
	}

	require.NotNil(t, invoiceCollections, "expected Order related list candidates to include Invoice Collections")
	assert.Equal(t, "Invoice Collections", invoiceCollections.Label)
	assert.Equal(t, "via Order", invoiceCollections.SecondaryLabel)
	assert.Equal(t, string(BucketRecommended), invoiceCollections.Bucket)
	assert.Equal(t, []string{constants.FieldName, constants.FieldCreatedDate}, invoiceCollections.Fields)
}

func TestMetadataService_GetRelatedListCandidates_QuoteUsesTenantPresentationDefaults(t *testing.T) {
	ms, metaRepo, _, _ := newTestServiceStateful()
	ctx := context.Background()
	ResetTenantRelatedListRulesForTest()
	defer ResetTenantRelatedListRulesForTest()
	require.NoError(t, RegisterTenantRelatedListRules([]RelatedListRule{
		{
			Key:              "quote-products",
			ParentObjectAPI:  "Quote",
			ChildObjectAPI:   "quote_product__c",
			LookupField:      "Quote__c",
			Bucket:           BucketRecommended,
			SortOrder:        20,
			AppendIfMissing:  true,
			Label:            "Quote Line Items",
			SecondaryLabel:   "via Quote",
			LookupFieldLabel: "Quote",
			Fields:           []string{"Name", "Quote__c", "Product__c", "Quantity__c", "UnitPrice__c", "TotalPrice__c"},
		},
	}))

	metaRepo.schemas["quote_product__c"] = &models.ObjectMetadata{
		APIName:     "quote_product__c",
		Label:       "Quote Product",
		PluralLabel: "Quote Products",
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldName, IsNameField: true},
			{APIName: "Quote__c"},
			{APIName: "Product__c"},
			{APIName: "Quantity__c"},
			{APIName: "UnitPrice__c"},
			{APIName: "TotalPrice__c"},
		},
	}
	metaRepo.relatedListCfgs["quote"] = []ports.RelatedListConfig{
		{
			ChildObjectAPI:   "quote_product__c",
			LookupFieldAPI:   "Quote__c",
			LookupFieldLabel: "Legacy Quote",
			ChildPluralLabel: "Quote Products",
		},
	}

	candidates, err := ms.GetRelatedListCandidates(ctx, "Quote")
	require.NoError(t, err)

	var quoteProducts *RelatedListCandidate
	for i := range candidates {
		if strings.EqualFold(candidates[i].ObjectAPIName, "quote_product__c") &&
			strings.EqualFold(candidates[i].LookupField, "Quote__c") {
			quoteProducts = &candidates[i]
			break
		}
	}

	require.NotNil(t, quoteProducts, "expected Quote related list candidates to include tenant quote products")
	assert.Equal(t, "Quote Line Items", quoteProducts.Label)
	assert.Equal(t, "via Quote", quoteProducts.SecondaryLabel)
	assert.Equal(t, string(BucketRecommended), quoteProducts.Bucket)
	assert.Equal(t, []string{"Name", "Quote__c", "Product__c", "Quantity__c", "UnitPrice__c", "TotalPrice__c"}, quoteProducts.Fields)
}

func TestMetadataService_SeedRelatedLists_UsesRelationshipLevelFields(t *testing.T) {
	ms, metaRepo, _, _ := newTestServiceStateful()

	metaRepo.schemas["contact"] = &models.ObjectMetadata{
		APIName: "Contact",
		Fields: []models.FieldMetadata{
			{APIName: "FullName", IsNameField: true},
		},
	}

	label := "Key Contacts"
	fields := `["FullName","Email"]`
	metaRepo.relatedListCfgs["account"] = []ports.RelatedListConfig{
		{
			ChildObjectAPI:    "Contact",
			ChildPluralLabel:  "Contacts",
			LookupFieldAPI:    "AccountId",
			LookupFieldLabel:  "Account",
			RelatedListLabel:  &label,
			RelatedListFields: &fields,
		},
	}

	seeded := ms.seedRelatedLists("Account")
	require.Len(t, seeded, 1)

	// Label uses ChildPluralLabel, not RelatedListLabel
	assert.Equal(t, "Contacts", seeded[0].Label)
	assert.Equal(t, "via Account", seeded[0].SecondaryLabel)
	assert.Equal(t, []string{"FullName", "Email"}, seeded[0].Fields)
}

func TestMetadataService_GetLayout_QuoteAppendsSyntheticFilesRelatedList(t *testing.T) {
	ms, metaRepo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	metaRepo.layouts["quote"] = []*models.PageLayout{
		{
			ID:            "layout_quote",
			ObjectAPIName: "Quote",
			LayoutName:    "Default",
			LayoutVersion: currentLayoutVersion,
			Sections:      []models.PageSection{{Label: "Info", Fields: []string{"Name"}}},
			RelatedLists: []models.RelatedListConfig{
				{ObjectAPIName: "QuoteLineItem", LookupField: "QuoteId", Label: "Quote Line Items"},
			},
		},
	}

	result := ms.GetLayout(ctx, "Quote", nil)
	require.NotNil(t, result)

	var foundFiles bool
	filesIndex := -1
	quoteLineItemIndex := -1
	for i, rl := range result.RelatedLists {
		if strings.EqualFold(rl.ObjectAPIName, constants.TableFile) &&
			strings.EqualFold(rl.LookupField, constants.FieldSysFile_ParentID) {
			foundFiles = true
			filesIndex = i
			assert.Equal(t, "Files", rl.Label)
			assert.Equal(t, []string{constants.FieldName, constants.FieldCreatedDate}, rl.Fields)
		}
		if strings.EqualFold(rl.ObjectAPIName, "QuoteLineItem") &&
			strings.EqualFold(rl.LookupField, "QuoteId") {
			quoteLineItemIndex = i
		}
	}

	assert.True(t, foundFiles, "expected Quote layout to include Files related list")
	assert.NotEqual(t, -1, quoteLineItemIndex, "expected Quote layout to include Quote Line Items related list")
	assert.Less(t, filesIndex, quoteLineItemIndex, "expected Quote Files related list to render ahead of Quote Line Items so Upload Files is visible without scrolling")
}

func TestMetadataService_ResolveLayout_RegeneratesOrderLayoutWhenRecommendedRelatedListsChange(t *testing.T) {
	ms, metaRepo, _, _ := newTestServiceStateful()
	ctx := context.Background()
	ResetTenantRelatedListRulesForTest()
	defer ResetTenantRelatedListRulesForTest()
	require.NoError(t, RegisterTenantRelatedListRules([]RelatedListRule{
		{
			Key:              "order-invoice-collections",
			ParentObjectAPI:  "Order",
			ChildObjectAPI:   "invoice_collection__c",
			LookupField:      "Order__c",
			Bucket:           BucketRecommended,
			SortOrder:        40,
			AppendIfMissing:  true,
			Label:            "Invoice Collections",
			SecondaryLabel:   "via Order",
			LookupFieldLabel: "Order",
			Fields:           []string{constants.FieldName, constants.FieldCreatedDate},
		},
	}))

	metaRepo.disableImplicitLayoutAssignment = true
	metaRepo.schemas["order"] = &models.ObjectMetadata{
		APIName:     "Order",
		Label:       "Order",
		PluralLabel: "Orders",
		Fields: []models.FieldMetadata{
			{APIName: "Name", Label: "Order Name", Type: constants.FieldTypeString, IsNameField: true},
		},
	}

	existing := &models.PageLayout{
		ID:                "layout_order_default",
		ObjectAPIName:     "Order",
		LayoutName:        "Default",
		Source:            "auto_generated",
		GenerationVersion: currentGenerationVersion - 1,
		LayoutVersion:     currentGenerationVersion - 1,
		Sections:          []models.PageSection{{Label: "Info", Fields: []string{"Name"}}},
		RelatedLists: []models.RelatedListConfig{
			{ObjectAPIName: "orderitem", LookupField: "OrderId", Label: "Order Items"},
			{ObjectAPIName: "contract", LookupField: "OrderId", Label: "Contracts"},
		},
	}
	metaRepo.layouts["order"] = []*models.PageLayout{existing}
	metaRepo.layoutByID[existing.ID] = existing
	metaRepo.relatedListCfgs["order"] = []ports.RelatedListConfig{
		{ChildObjectAPI: "OrderItem", LookupFieldAPI: "OrderId", ChildPluralLabel: "Order Items"},
		{ChildObjectAPI: "Contract", LookupFieldAPI: "OrderId", ChildPluralLabel: "Contracts"},
		{ChildObjectAPI: "invoice_collection__c", LookupFieldAPI: "Order__c", ChildPluralLabel: "Invoice Collections"},
	}

	result := ms.ResolveLayout(ctx, "Order", nil, "", "")
	require.NotNil(t, result)
	assert.Equal(t, existing.ID, result.ID)

	var foundInvoiceCollection bool
	for _, rl := range result.RelatedLists {
		if strings.EqualFold(rl.ObjectAPIName, "invoice_collection__c") &&
			strings.EqualFold(rl.LookupField, "Order__c") {
			foundInvoiceCollection = true
			assert.Equal(t, "Invoice Collections", rl.Label)
		}
	}
	assert.True(t, foundInvoiceCollection, "expected regenerated Order layout to include Invoice Collections")
	assert.Equal(t, currentGenerationVersion, result.GenerationVersion)
	assert.Equal(t, currentGenerationVersion, metaRepo.layoutByID[existing.ID].GenerationVersion)
}

func TestMetadataService_GetLayout_DedupesDuplicateOrderInvoiceCollectionsRelatedList(t *testing.T) {
	ms, metaRepo, _, _ := newTestServiceStateful()
	ctx := context.Background()
	ResetTenantRelatedListRulesForTest()
	defer ResetTenantRelatedListRulesForTest()
	require.NoError(t, RegisterTenantRelatedListRules([]RelatedListRule{
		{
			Key:              "order-invoice-collections",
			ParentObjectAPI:  "Order",
			ChildObjectAPI:   "invoice_collection__c",
			LookupField:      "Order__c",
			Bucket:           BucketRecommended,
			SortOrder:        40,
			AppendIfMissing:  true,
			Label:            "Invoice Collections",
			SecondaryLabel:   "via Order",
			LookupFieldLabel: "Order",
			Fields:           []string{constants.FieldName, constants.FieldCreatedDate},
		},
	}))

	metaRepo.layouts["order"] = []*models.PageLayout{
		{
			ID:            "layout_order",
			ObjectAPIName: "Order",
			LayoutName:    "Default",
			LayoutVersion: currentLayoutVersion,
			Sections:      []models.PageSection{{Label: "Info", Fields: []string{"Name"}}},
			RelatedLists: []models.RelatedListConfig{
				{ID: "orderitem-OrderId", ObjectAPIName: "OrderItem", LookupField: "OrderId", Label: "Order Line Items"},
				{ID: "invoice_collection__c-Order__c", ObjectAPIName: "invoice_collection__c", LookupField: "Order__c", Label: "Invoice Collections"},
				{ID: "invoice_collection__c-Order__c", ObjectAPIName: "invoice_collection__c", LookupField: "Order__c", Label: "Invoice Collections"},
			},
		},
	}

	metaRepo.relatedListCfgs["order"] = []ports.RelatedListConfig{
		{ChildObjectAPI: "OrderItem", LookupFieldAPI: "OrderId", ChildPluralLabel: "Order Line Items"},
		{ChildObjectAPI: "invoice_collection__c", LookupFieldAPI: "Order__c", ChildPluralLabel: "Invoice Collections"},
	}

	result := ms.GetLayout(ctx, "Order", nil)
	require.NotNil(t, result)

	invoiceCollectionsCount := 0
	for _, rl := range result.RelatedLists {
		if strings.EqualFold(rl.ObjectAPIName, "invoice_collection__c") &&
			strings.EqualFold(rl.LookupField, "Order__c") {
			invoiceCollectionsCount++
		}
	}

	assert.Equal(t, 1, invoiceCollectionsCount, "expected duplicate Invoice Collections related lists to be collapsed")
}

// ============================================================================
// Tests: addFieldToLayout
// ============================================================================

func TestMetadataService_AddFieldToLayout_HappyPath(t *testing.T) {
	ms, metaRepo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	existingLayout := &models.PageLayout{
		ID:            "layout_1",
		ObjectAPIName: "Account",
		Sections: []models.PageSection{
			{Label: "Info", Fields: []string{"Name"}},
		},
	}
	metaRepo.layouts["account"] = []*models.PageLayout{existingLayout}
	metaRepo.layoutByID["layout_1"] = existingLayout

	// Lock is expected to be held by caller; in test we call directly
	ms.mu.Lock()
	err := ms.addFieldToLayout(ctx, "Account", "Industry")
	ms.mu.Unlock()

	require.NoError(t, err)

	// Verify the field was added
	saved := metaRepo.layoutByID["layout_1"]
	assert.Contains(t, saved.Sections[0].Fields, "Industry")
}

func TestMetadataService_AddFieldToLayout_FieldAlreadyExists(t *testing.T) {
	ms, metaRepo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	existingLayout := &models.PageLayout{
		ID:            "layout_1",
		ObjectAPIName: "Account",
		Sections: []models.PageSection{
			{Label: "Info", Fields: []string{"Name", "Industry"}},
		},
	}
	metaRepo.layouts["account"] = []*models.PageLayout{existingLayout}

	ms.mu.Lock()
	err := ms.addFieldToLayout(ctx, "Account", "Industry")
	ms.mu.Unlock()

	require.NoError(t, err)

	// Field should not be duplicated
	count := 0
	for _, f := range existingLayout.Sections[0].Fields {
		if f == "Industry" {
			count++
		}
	}
	assert.Equal(t, 1, count)
}

func TestMetadataService_AddFieldToLayout_NoLayouts(t *testing.T) {
	ms, _, _, _ := newTestServiceStateful()
	ctx := context.Background()

	ms.mu.Lock()
	err := ms.addFieldToLayout(ctx, "Account", "Industry")
	ms.mu.Unlock()

	// Should return nil when no layouts exist (nothing to add to)
	require.NoError(t, err)
}

// ============================================================================
// Tests: UI Component & Setup Page Methods
// ============================================================================

func TestMetadataService_UpsertUIComponent(t *testing.T) {
	ms, _, _, _ := newTestServiceStateful()
	ctx := context.Background()

	component := &models.UIComponent{
		ID:   "comp_1",
		Name: "TestComponent",
	}

	err := ms.UpsertUIComponent(ctx, component)
	require.NoError(t, err)
}

func TestMetadataService_UpsertSetupPage(t *testing.T) {
	ms, _, _, _ := newTestServiceStateful()
	ctx := context.Background()

	page := &models.SetupPage{
		ID:    "page_1",
		Label: "Test Setup Page",
	}

	err := ms.UpsertSetupPage(ctx, page)
	require.NoError(t, err)
}

func TestMetadataService_GetSetupPages(t *testing.T) {
	ms, _, _, _ := newTestServiceStateful()
	ctx := context.Background()

	pages, err := ms.GetSetupPages(ctx)
	require.NoError(t, err)
	// Default mock returns nil, nil
	assert.Nil(t, pages)
}

// ============================================================================
// Tests: Helpers
// ============================================================================

func TestGenerateObjectID(t *testing.T) {
	id := GenerateObjectID("Account")
	assert.NotEmpty(t, id)
	// IDs are case-normalized to lowercase so all write paths converge on
	// the same canonical value regardless of the casing the caller passes
	// (see schema.GenerateObjectID for the rationale).
	assert.Contains(t, id, "account")
	// Case-normalization is part of the contract: "Account" and "account"
	// must produce the same ID.
	assert.Equal(t, id, GenerateObjectID("account"))
}

func TestGenerateFieldID(t *testing.T) {
	id := GenerateFieldID("Account", "Name")
	assert.NotEmpty(t, id)
	// Both names are lowercase-normalized — see TestGenerateObjectID.
	assert.Contains(t, id, "account")
	assert.Contains(t, id, "name")
	assert.Equal(t, id, GenerateFieldID("account", "name"))
}

func TestGenerateAutoNumberID(t *testing.T) {
	id := GenerateAutoNumberID("Account", "AccountNumber")
	assert.NotEmpty(t, id)
}

func TestGetPolymorphicTypeColumnName(t *testing.T) {
	col := GetPolymorphicTypeColumnName("WhatId")
	assert.NotEmpty(t, col)
	assert.Contains(t, strings.ToLower(col), strings.ToLower("WhatId"))
}

func TestGenerateID_Unique(t *testing.T) {
	id1 := GenerateID()
	id2 := GenerateID()
	assert.NotEmpty(t, id1)
	assert.NotEmpty(t, id2)
	assert.NotEqual(t, id1, id2)
}

// ============================================================================
// Tests: Uncovered metadata_objects.go getters
// ============================================================================

func TestMetadataService_GetRecordTypes(t *testing.T) {
	ms, metaRepo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	metaRepo.recordTypes["account"] = []*models.RecordType{
		{ID: "rt_1", Name: "Standard"},
	}

	result := ms.GetRecordTypes(ctx, "Account")
	assert.Len(t, result, 1)
	assert.Equal(t, "Standard", result[0].Name)
}

func TestMetadataService_GetRecordTypes_Empty(t *testing.T) {
	ms, _, _, _ := newTestServiceStateful()
	ctx := context.Background()

	result := ms.GetRecordTypes(ctx, "NonExistent")
	assert.Empty(t, result)
}

func TestMetadataService_GetRelationships_WithData(t *testing.T) {
	ms, metaRepo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	relName := "Account"
	metaRepo.schemas["contact"] = &models.ObjectMetadata{
		ID:      "obj_contact",
		APIName: "Contact",
		Fields: []models.FieldMetadata{
			{APIName: "AccountId", Type: constants.FieldTypeReference, ReferenceTo: []string{"Account"}, RelationshipName: &relName},
		},
	}
	metaRepo.relationships["contact"] = []*models.Relationship{
		{ID: "rel_1", ChildObjectAPIName: "Contact", ParentObjectAPIName: "Account", FieldAPIName: "AccountId", IsSharingParent: true},
	}

	result := ms.GetRelationships(ctx, "Contact")
	assert.Len(t, result, 1)
	assert.Equal(t, "AccountId", result[0].FieldAPIName)
	assert.True(t, result[0].IsSharingParent)
}

func TestMetadataService_GetRelationships_UsesExplicitDeleteSemantics(t *testing.T) {
	ms, metaRepo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	relName := "Dashboard"
	metaRepo.schemas["dashboardcomponent"] = &models.ObjectMetadata{
		ID:      "obj_dashboard_component",
		APIName: "DashboardComponent",
		Fields: []models.FieldMetadata{
			{APIName: "DashboardId", Type: constants.FieldTypeReference, ReferenceTo: []string{"Dashboard"}, RelationshipName: &relName},
		},
	}
	metaRepo.relationships["dashboardcomponent"] = []*models.Relationship{
		{
			ID:                  "rel_1",
			ChildObjectAPIName:  "DashboardComponent",
			ParentObjectAPIName: "Dashboard",
			FieldAPIName:        "DashboardId",
			RelationshipName:    "Dashboard",
			IsSharingParent:     true,
			CascadeDelete:       true,
			RestrictedDelete:    false,
		},
	}

	result := ms.GetRelationships(ctx, "DashboardComponent")
	require.Len(t, result, 1)
	assert.True(t, result[0].IsSharingParent)
	assert.True(t, result[0].CascadeDelete)
	assert.False(t, result[0].RestrictedDelete)
}

func TestMetadataService_GetRelationships_Empty(t *testing.T) {
	ms, _, _, _ := newTestServiceStateful()
	ctx := context.Background()

	result := ms.GetRelationships(ctx, "NonExistent")
	assert.Empty(t, result)
}

func TestMetadataService_GetFieldDependencies_WithData(t *testing.T) {
	ms, metaRepo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	metaRepo.fieldDependencies["account"] = []*models.FieldDependency{
		{ControllingField: "Type", DependentField: "SubType"},
	}

	result := ms.GetFieldDependencies(ctx, "Account")
	assert.Len(t, result, 1)
}

func TestMetadataService_GetFieldDependencies_Empty(t *testing.T) {
	ms, _, _, _ := newTestServiceStateful()
	ctx := context.Background()

	result := ms.GetFieldDependencies(ctx, "NonExistent")
	assert.Empty(t, result)
}

func TestMetadataService_GetChildRelationships_WithData(t *testing.T) {
	ms, metaRepo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	contactRel := "Account"
	opportunityRel := "Account"
	metaRepo.schemas["contact"] = &models.ObjectMetadata{
		ID:      "obj_contact",
		APIName: "Contact",
		Label:   "Contact",
		Fields: []models.FieldMetadata{
			{APIName: "AccountId", Type: constants.FieldTypeReference, ReferenceTo: []string{"Account"}, RelationshipName: &contactRel},
		},
	}
	metaRepo.schemas["opportunity"] = &models.ObjectMetadata{
		ID:      "obj_opportunity",
		APIName: "Opportunity",
		Label:   "Opportunity",
		Fields: []models.FieldMetadata{
			{APIName: "AccountId", Type: constants.FieldTypeReference, ReferenceTo: []string{"Account"}, RelationshipName: &opportunityRel},
		},
	}

	result := ms.GetChildRelationships(ctx, "Account")
	assert.Len(t, result, 2)
}

func TestMetadataService_GetRelationships_DerivesDeleteSemanticsFromFieldMetadata(t *testing.T) {
	ms, metaRepo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	restrict := constants.DeleteRuleRestrict
	relName := "Parent"
	metaRepo.schemas["child"] = &models.ObjectMetadata{
		ID:      "obj_child",
		APIName: "Child",
		Fields: []models.FieldMetadata{
			{APIName: "ParentId", Type: constants.FieldTypeReference, ReferenceTo: []string{"Parent"}, RelationshipName: &relName, DeleteRule: &restrict},
		},
	}
	metaRepo.relationships["child"] = []*models.Relationship{
		{ID: "rel_1", ChildObjectAPIName: "Child", ParentObjectAPIName: "Parent", FieldAPIName: "ParentId"},
	}

	result := ms.GetRelationships(ctx, "Child")
	assert.Len(t, result, 1)
	assert.False(t, result[0].CascadeDelete)
	assert.True(t, result[0].RestrictedDelete)
}

func TestMetadataService_GetRelationships_AllowsExplicitPositiveDeleteSemantics(t *testing.T) {
	ms, metaRepo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	relName := "Parent"
	metaRepo.schemas["child"] = &models.ObjectMetadata{
		ID:      "obj_child",
		APIName: "Child",
		Fields: []models.FieldMetadata{
			{APIName: "ParentId", Type: constants.FieldTypeReference, ReferenceTo: []string{"Parent"}, RelationshipName: &relName},
		},
	}
	metaRepo.relationships["child"] = []*models.Relationship{
		{ID: "rel_1", ChildObjectAPIName: "Child", ParentObjectAPIName: "Parent", FieldAPIName: "ParentId", RestrictedDelete: true},
	}

	result := ms.GetRelationships(ctx, "Child")
	assert.Len(t, result, 1)
	assert.False(t, result[0].CascadeDelete)
	assert.True(t, result[0].RestrictedDelete)
}

func TestMetadataService_GetChildRelationships_Empty(t *testing.T) {
	ms, _, _, _ := newTestServiceStateful()
	ctx := context.Background()

	result := ms.GetChildRelationships(ctx, "NonExistent")
	assert.Empty(t, result)
}

func TestMetadataService_GetAllActions_Empty(t *testing.T) {
	ms, _, _, _ := newTestServiceStateful()
	ctx := context.Background()

	result := ms.GetAllActions(ctx)
	assert.NotNil(t, result)
}

func TestMetadataService_GetFlowDefinitionById_Alias(t *testing.T) {
	ms, metaRepo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{APIName: "Account"}
	metaRepo.flowDefinitions["flow_1"] = &models.FlowDefinition{
		Id:            "flow_1",
		DeveloperName: "TestFlow",
	}

	// Prime cache
	_ = ms.GetSchema(ctx, "Account")

	result := ms.GetFlowDefinitionById(ctx, "flow_1")
	require.NotNil(t, result)
	assert.Equal(t, "TestFlow", result.DeveloperName)
}

// ============================================================================
// Tests: Validation Rule edge cases
// ============================================================================

func TestMetadataService_UpdateValidationRule_NameConflict(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	// Set up existing rules
	metaRepo.validationRuleByID["rule_1"] = &models.ValidationRule{
		ID:            "rule_1",
		ObjectAPIName: "Account",
		Name:          "RequirePhone",
		Condition:     "Phone == null",
		ErrorMessage:  "Phone is required",
		Active:        true,
	}
	metaRepo.validationRuleByID["rule_2"] = &models.ValidationRule{
		ID:            "rule_2",
		ObjectAPIName: "Account",
		Name:          "RequireEmail",
		Condition:     "Email == null",
		ErrorMessage:  "Email is required",
		Active:        true,
	}

	// Try to rename rule_1 to the same name as rule_2
	updates := &models.ValidationRule{
		Name: "RequireEmail", // Conflict with rule_2
	}

	err := ms.UpdateValidationRule(ctx, "rule_1", updates)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "already exists")
}

func TestMetadataService_UpdateValidationRule_UpdateConditionAndMessage(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.validationRuleByID["rule_1"] = &models.ValidationRule{
		ID:            "rule_1",
		ObjectAPIName: "Account",
		Name:          "RequirePhone",
		Condition:     "Phone == null",
		ErrorMessage:  "Phone is required",
		Active:        true,
	}

	updates := &models.ValidationRule{
		Condition:    "Phone == null && Type == 'Customer'",
		ErrorMessage: "Phone is required for customers",
		Active:       false,
	}

	err := ms.UpdateValidationRule(ctx, "rule_1", updates)
	require.NoError(t, err)

	// Verify the updates were applied
	saved := metaRepo.validationRuleByID["rule_1"]
	assert.Equal(t, "Phone == null && Type == 'Customer'", saved.Condition)
	assert.Equal(t, "Phone is required for customers", saved.ErrorMessage)
	assert.False(t, saved.Active)
}

func TestMetadataService_CreateValidationRule_CheckExistsError(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.checkValidationRuleExistsErr = fmt.Errorf("db error")

	rule := &models.ValidationRule{
		ObjectAPIName: "Account",
		Name:          "TestRule",
		Condition:     "Phone == null",
		ErrorMessage:  "Phone required",
	}

	err := ms.CreateValidationRule(ctx, rule)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "failed to check for existing validation rule")
}

func TestMetadataService_UpdateValidationRule_CheckExistsError(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.validationRuleByID["rule_1"] = &models.ValidationRule{
		ID:            "rule_1",
		ObjectAPIName: "Account",
		Name:          "OldName",
		Condition:     "x",
		ErrorMessage:  "y",
	}
	metaRepo.checkValidationRuleExistsErr = fmt.Errorf("db error")

	updates := &models.ValidationRule{
		Name: "NewName", // Triggers conflict check which will fail
	}

	err := ms.UpdateValidationRule(ctx, "rule_1", updates)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "failed to check for existing validation rule")
}

// ============================================================================
// Tests: Schema getters with cache error paths
// ============================================================================

func TestMetadataService_GetSchemas_CacheError(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.getAllSchemasErr = fmt.Errorf("db unreachable")

	result := ms.GetSchemas(ctx)
	assert.NotNil(t, result)
	assert.Empty(t, result)
}

func TestMetadataService_GetSearchableObjects_CacheError(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.getAllSchemasErr = fmt.Errorf("db unreachable")

	result := ms.GetSearchableObjects(ctx)
	assert.NotNil(t, result)
	assert.Empty(t, result)
}

func TestMetadataService_GetSchema_CacheError(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.getAllSchemasErr = fmt.Errorf("db unreachable")

	result := ms.GetSchema(ctx, "Account")
	assert.Nil(t, result)
}

// ============================================================================
// Tests: PrepareTableDefinition edge cases
// ============================================================================

func TestMetadataService_PrepareTableDefinition_PolymorphicField(t *testing.T) {
	ms, _, _, _ := newTestService()

	schema := &models.ObjectMetadata{
		APIName:     "Task",
		Label:       "Task",
		PluralLabel: "Tasks",
		Fields: []models.FieldMetadata{
			{
				APIName:       "WhatId",
				Label:         "Related To",
				Type:          constants.FieldTypeReference,
				IsPolymorphic: true,
				ReferenceTo:   []string{"Account", "Opportunity"},
			},
		},
	}

	def, batchFields, err := ms.PrepareTableDefinition(schema)
	require.NoError(t, err)

	// Should have the polymorphic type column added
	hasTypeCol := false
	for _, col := range def.Columns {
		if col.Name == GetPolymorphicTypeColumnName("WhatId") {
			hasTypeCol = true
			assert.Equal(t, "VARCHAR(255)", col.Type)
		}
	}
	assert.True(t, hasTypeCol, "Should have polymorphic type column")
	assert.NotEmpty(t, batchFields)
}

func TestMetadataService_PrepareTableDefinition_DefaultValue(t *testing.T) {
	ms, _, _, _ := newTestService()

	defaultVal := "Active"
	schema := &models.ObjectMetadata{
		APIName:     "TestObj",
		Label:       "Test",
		PluralLabel: "Tests",
		Fields: []models.FieldMetadata{
			{
				APIName:      "Status",
				Label:        "Status",
				Type:         constants.FieldTypeString,
				DefaultValue: &defaultVal,
			},
		},
	}

	def, _, err := ms.PrepareTableDefinition(schema)
	require.NoError(t, err)

	// Find the Status column and verify default
	for _, col := range def.Columns {
		if col.Name == "Status" {
			assert.Equal(t, "'Active'", col.Default)
		}
	}
}

func TestMetadataService_PrepareTableDefinition_BooleanDefaults(t *testing.T) {
	ms, _, _, _ := newTestService()

	tests := []struct {
		name        string
		apiSuffix   string
		defValue    string
		expectedSQL string
	}{
		{"false string", "BoolFalse", "false", "0"},
		{"true string", "BoolTrue", "true", "1"},
		{"zero string", "BoolZero", "0", "0"},
		{"one string", "BoolOne", "1", "1"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			defVal := tt.defValue
			schema := &models.ObjectMetadata{
				APIName:     "TestObj" + tt.apiSuffix,
				Label:       "Test",
				PluralLabel: "Tests",
				Fields: []models.FieldMetadata{
					{
						APIName:      "IsActive",
						Label:        "Active",
						Type:         constants.FieldTypeBoolean,
						DefaultValue: &defVal,
					},
				},
			}

			def, _, err := ms.PrepareTableDefinition(schema)
			require.NoError(t, err)

			for _, col := range def.Columns {
				if col.Name == "IsActive" {
					assert.Equal(t, tt.expectedSQL, col.Default)
				}
			}
		})
	}
}

func TestMetadataService_PrepareTableDefinition_UniqueField(t *testing.T) {
	ms, _, _, _ := newTestService()

	schema := &models.ObjectMetadata{
		APIName:     "TestObj",
		Label:       "Test",
		PluralLabel: "Tests",
		Fields: []models.FieldMetadata{
			{
				APIName:  "Email",
				Label:    "Email",
				Type:     constants.FieldTypeEmail,
				IsUnique: true,
			},
		},
	}

	def, _, err := ms.PrepareTableDefinition(schema)
	require.NoError(t, err)

	for _, col := range def.Columns {
		if col.Name == "Email" {
			assert.True(t, col.Unique)
		}
	}
}

func TestMetadataService_PrepareTableDefinition_RequiredField(t *testing.T) {
	ms, _, _, _ := newTestService()

	schema := &models.ObjectMetadata{
		APIName:     "TestObj",
		Label:       "Test",
		PluralLabel: "Tests",
		Fields: []models.FieldMetadata{
			{
				APIName:  "RequiredField",
				Label:    "Required",
				Type:     constants.FieldTypeString,
				Required: true,
			},
		},
	}

	def, _, err := ms.PrepareTableDefinition(schema)
	require.NoError(t, err)

	for _, col := range def.Columns {
		if col.Name == "RequiredField" {
			assert.False(t, col.Nullable, "Required field should not be nullable")
		}
	}
}

func TestMetadataService_PrepareTableDefinition_EmptyDescription(t *testing.T) {
	ms, _, _, _ := newTestService()

	schema := &models.ObjectMetadata{
		APIName:     "TestObj",
		Label:       "Test Object",
		PluralLabel: "Test Objects",
		// Description is nil => should default to Label
	}

	_, _, err := ms.PrepareTableDefinition(schema)
	require.NoError(t, err)
	assert.NotNil(t, schema.Description)
	assert.Equal(t, "Test Object", *schema.Description)
}

func TestMetadataService_PrepareTableDefinition_PreservesExistingDescription(t *testing.T) {
	ms, _, _, _ := newTestService()

	desc := "Custom description"
	schema := &models.ObjectMetadata{
		APIName:     "TestObj",
		Label:       "Test Object",
		PluralLabel: "Test Objects",
		Description: &desc,
	}

	_, _, err := ms.PrepareTableDefinition(schema)
	require.NoError(t, err)
	assert.Equal(t, "Custom description", *schema.Description)
}

// ============================================================================
// Tests: GetLayout generated from schema in cache
// ============================================================================

func TestMetadataService_GetLayout_GeneratedFromCachedSchema(t *testing.T) {
	ms, metaRepo, _, _ := newTestServiceStateful()
	ctx := context.Background()

	// Pre-populate schema in mock repo (will be loaded into cache)
	metaRepo.schemas["customobj"] = &models.ObjectMetadata{
		APIName: "CustomObj",
		Label:   "Custom Object",
		Fields: []models.FieldMetadata{
			{APIName: "Id", Type: constants.FieldTypeString, IsSystem: true},
			{APIName: "Name", Type: constants.FieldTypeString, IsNameField: true},
			{APIName: "Status", Type: constants.FieldTypePicklist},
		},
	}

	// No layouts in repo => will fall through to generated default
	result := ms.GetLayout(ctx, "CustomObj", nil)
	require.NotNil(t, result)

	// Verify it's a generated layout
	assert.Equal(t, "default_CustomObj", result.ID)
	assert.Equal(t, constants.LayoutNameDefault, result.LayoutName)

	// System fields should not appear
	for _, f := range result.Sections[0].Fields {
		assert.NotEqual(t, "Id", f)
	}

	// Business fields should appear
	assert.Contains(t, result.Sections[0].Fields, "Name")
	assert.Contains(t, result.Sections[0].Fields, "Status")
}

// ============================================================================
// Tests: Flow Version edge cases
// ============================================================================

func TestMetadataService_GetFlowVersions_EmptyDefinitionId(t *testing.T) {
	ms, _, _, _ := newTestService()
	ctx := context.Background()

	// Empty definitionId should return empty slice
	versions, err := ms.GetFlowVersions(ctx, "")
	require.NoError(t, err)
	assert.Empty(t, versions)
}

func TestMetadataService_UpdateFlowVersion(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.flowDefinitions["flow_1"] = &models.FlowDefinition{
		Id: "flow_1",
	}
	metaRepo.flowVersions["v1"] = &models.FlowVersion{
		Id:            "v1",
		DefinitionId:  "flow_1",
		VersionNumber: 1,
	}

	updated := &models.FlowVersion{
		Id:            "v1",
		DefinitionId:  "flow_1",
		VersionNumber: 2,
		Metadata: models.FlowMetadata{
			Start: &models.FlowStart{
				TriggerType: "RecordBeforeSave",
				Object:      "Lead",
			},
		},
	}

	err := ms.UpdateFlowVersion(ctx, "v1", updated)
	require.NoError(t, err)
	require.Len(t, metaRepo.triggerInfoUpdates, 1)
	assert.Equal(t, "flow_1", metaRepo.triggerInfoUpdates[0].definitionId)
	assert.Equal(t, "RecordBeforeSave", metaRepo.flowDefinitions["flow_1"].TriggerType)
	assert.Equal(t, "Lead", metaRepo.flowDefinitions["flow_1"].TriggerObjectOrEvent)
}

func TestMetadataService_DeleteFlowVersion(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.flowVersions["v1"] = &models.FlowVersion{Id: "v1"}

	err := ms.DeleteFlowVersion(ctx, "v1")
	require.NoError(t, err)
	assert.Empty(t, metaRepo.flowVersions)
}

// ============================================================================
// Tests: SchemaManager methods
// ============================================================================

func TestSchemaManager_ValidateSchema(t *testing.T) {
	schemaRepo := newMockSchemaRepo()
	sm := NewSchemaManager(schemaRepo)

	err := sm.ValidateSchema("test_table")
	assert.NoError(t, err)
}

func TestSchemaManager_ValidateSchemaRegistry(t *testing.T) {
	schemaRepo := newMockSchemaRepo()
	sm := NewSchemaManager(schemaRepo)

	health, err := sm.ValidateSchemaRegistry()
	require.NoError(t, err)
	assert.NotNil(t, health)
}

func TestSchemaManager_MapFieldTypeToSQL(t *testing.T) {
	schemaRepo := newMockSchemaRepo()
	sm := NewSchemaManager(schemaRepo)

	tests := []struct {
		fieldType string
		expected  string
	}{
		{"string", "VARCHAR(255)"},
		{"double", "DOUBLE"},
		{"boolean", "TINYINT(1)"},
		{"date", "DATE"},
		{"datetime", "DATETIME"},
		{"reference", "VARCHAR(36)"},
		{"textarea", "TEXT"},
	}

	for _, tt := range tests {
		t.Run(tt.fieldType, func(t *testing.T) {
			result := sm.MapFieldTypeToSQL(tt.fieldType)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestSchemaManager_GetStandardFieldMetadata(t *testing.T) {
	schemaRepo := newMockSchemaRepo()
	sm := NewSchemaManager(schemaRepo)

	fields := sm.GetStandardFieldMetadata()
	assert.NotEmpty(t, fields)

	// Should contain Id, CreatedDate, LastModifiedDate, OwnerId
	fieldNames := make([]string, len(fields))
	for i, f := range fields {
		fieldNames[i] = f.APIName
	}
	assert.Contains(t, fieldNames, "Id")
	assert.Contains(t, fieldNames, "CreatedDate")
}

// ============================================================================
// Tests: generateStandardSObjectID
// ============================================================================

func TestGenerateStandardSObjectID_FlowDefinitionPrefix(t *testing.T) {
	id := generateStandardSObjectID("FlowDefinition")
	assert.NotEmpty(t, id)
	assert.True(t, strings.HasPrefix(id, "300"),
		"FlowDefinition ID must use Salesforce key prefix 300, got %q", id)
}

func TestGenerateStandardSObjectID_FlowVersionPrefix(t *testing.T) {
	id := generateStandardSObjectID("Flow")
	assert.NotEmpty(t, id)
	assert.True(t, strings.HasPrefix(id, "301"),
		"Flow (version) ID must use Salesforce key prefix 301, got %q", id)
}

func TestGenerateStandardSObjectID_UnknownObjectPanics(t *testing.T) {
	assert.Panics(t, func() { generateStandardSObjectID("UnknownObject__c") })
}

// ============================================================================
// Tests: isSystemTableForCaching additional cases
// ============================================================================

func TestIsSystemTableForCaching_EdgeCases(t *testing.T) {
	tests := []struct {
		name     string
		apiName  string
		expected bool
	}{
		{"system prefix", "_System_Object", true},
		{"system field table", "_System_Field", true},
		{"regular object", "Account", false},
		{"underscore prefix no system", "_Custom_Table", false},
		{"empty string", "", false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.Equal(t, tt.expected, isSystemTableForCaching(tt.apiName))
		})
	}
}

// ============================================================================
// Tests: Cache concurrent initialization (double-checked locking)
// ============================================================================

func TestMetadataService_EnsureCacheInitialized_DoubleChecked(t *testing.T) {
	ms, metaRepo, _, _ := newTestService()
	ctx := context.Background()

	metaRepo.schemas["account"] = &models.ObjectMetadata{APIName: "Account"}

	// First call initializes cache
	err := ms.ensureCacheInitialized(ctx)
	require.NoError(t, err)
	assert.NotNil(t, ms.loadCache())

	// Second call should be a no-op (fast path)
	err = ms.ensureCacheInitialized(ctx)
	require.NoError(t, err)
	assert.NotNil(t, ms.loadCache())
}

// --- Layout Assignment interface stubs (WP2) ---
// FindBestAssignment is intentionally NOT overridden here — the embedded
// mockMetadataRepo.FindBestAssignment provides a working implementation
// that looks up m.layouts by object name, which is what the layout tests need.
func (m *statefulMockRepo) CreateAssignment(_ context.Context, _ *models.LayoutAssignment) error {
	return nil
}
func (m *statefulMockRepo) DeleteAssignment(_ context.Context, _ string) error { return nil }
func (m *statefulMockRepo) GetAssignmentsForObject(_ context.Context, _ string) ([]*models.LayoutAssignment, error) {
	return nil, nil
}
func (m *statefulMockRepo) DeleteAssignmentsByLayoutID(_ context.Context, _ string) error { return nil }
func (m *statefulMockRepo) WriteLayoutHistory(_ context.Context, _ *models.PageLayoutHistoryEntry) error {
	return nil
}
func (m *statefulMockRepo) GetLayoutHistoryMaxVersion(_ context.Context, _ string) (int, error) {
	return 0, nil
}
func (m *statefulMockRepo) GetMigrationVersion(_ context.Context, _ string) (int, error) {
	return 0, nil
}
func (m *statefulMockRepo) SetMigrationVersion(_ context.Context, _ string, _ int, _ string) error {
	return nil
}

func (m *statefulMockRepo) GetLayoutHistoryConfig(_ context.Context, _ string, _ int) (string, error) {
	return "", nil
}

func (m *statefulMockRepo) PruneLayoutHistory(_ context.Context, _ string, _ int) error { return nil }
