package uiapi

import (
	"context"
	"testing"

	"github.com/nexuscrm/backend/pkg/apierr"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// =============================================================================
// GetObjectInfo — object metadata retrieval
// =============================================================================

func TestGetObjectInfo_BasicObject(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{
			APIName:     "Account",
			Label:       "Account",
			PluralLabel: "Accounts",
			IsSystem:    false,
			Fields: []models.FieldMetadata{
				{APIName: "Id", Label: "Record ID", Type: "id", IsSystem: true},
				{APIName: "Name", Label: "Account Name", Type: "string", Required: true, IsNameField: true},
				{APIName: "Industry", Label: "Industry", Type: "picklist"},
			},
		},
	}

	svc, user, cleanup := testService(schemas)
	defer cleanup()

	ctx := context.Background()
	info, err := svc.GetObjectInfo(ctx, "Account", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if info == nil {
		t.Fatal("expected non-nil ObjectInfo")
	}

	// Verify basic properties
	if info.ApiName != "Account" {
		t.Errorf("ApiName = %q, want 'Account'", info.ApiName)
	}
	if info.Label != "Account" {
		t.Errorf("Label = %q, want 'Account'", info.Label)
	}
	if info.LabelPlural != "Accounts" {
		t.Errorf("LabelPlural = %q, want 'Accounts'", info.LabelPlural)
	}

	// Non-system objects should be writable
	if !info.Createable {
		t.Error("non-system object should be createable")
	}
	if !info.Updateable {
		t.Error("non-system object should be updateable")
	}
	if !info.Deletable {
		t.Error("non-system object should be deletable")
	}
	if !info.Queryable {
		t.Error("non-system object should be queryable")
	}

	// Fields
	if len(info.Fields) != 3 {
		t.Errorf("expected 3 fields, got %d", len(info.Fields))
	}

	nameField, ok := info.Fields["Name"]
	if !ok {
		t.Fatal("expected 'Name' field in ObjectInfo")
	}
	if !nameField.NameField {
		t.Error("Name field should be marked as NameField")
	}
	if !nameField.Required {
		t.Error("Name field should be required")
	}
}

func TestGetObjectInfo_BlockedSystemObjectReadOnly(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{
			APIName:                 "_System_Metadata",
			Label:                   "System Metadata",
			PluralLabel:             "System Metadata",
			IsSystem:                true,
			RequiredWritePermission: "BLOCKED",
			Fields:                  []models.FieldMetadata{},
		},
	}

	svc, user, cleanup := testService(schemas)
	defer cleanup()

	info, err := svc.GetObjectInfo(context.Background(), "_System_Metadata", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if info == nil {
		t.Fatal("expected non-nil ObjectInfo")
	}

	// BLOCKED system objects should be read-only
	if info.Createable {
		t.Error("system object should NOT be createable")
	}
	if info.Updateable {
		t.Error("system object should NOT be updateable")
	}
	if info.Deletable {
		t.Error("system object should NOT be deletable")
	}
}

func TestGetObjectInfo_SystemMetadataCanBeWritable(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{
			APIName:                 "Report",
			Label:                   "Report",
			PluralLabel:             "Reports",
			IsSystem:                true,
			TableType:               constants.TableTypeSystemMetadata,
			RequiredWritePermission: "CreateCustomizeReports",
			Fields:                  []models.FieldMetadata{},
		},
	}

	svc, user, cleanup := testService(schemas)
	defer cleanup()

	info, err := svc.GetObjectInfo(context.Background(), "Report", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if info == nil {
		t.Fatal("expected non-nil ObjectInfo")
	}

	if !info.Createable {
		t.Error("system metadata object should be createable when permission service allows it")
	}
	if !info.Updateable {
		t.Error("system metadata object should be updateable when permission service allows it")
	}
	if !info.Deletable {
		t.Error("system metadata object should be deletable when permission service allows it")
	}
}

func TestGetObjectInfo_FieldCreateableUpdateable(t *testing.T) {
	formula := "Name & '-' & Id"
	schemas := []*models.ObjectMetadata{
		{
			APIName:     "Account",
			Label:       "Account",
			PluralLabel: "Accounts",
			Fields: []models.FieldMetadata{
				{APIName: "Id", Type: "id", IsSystem: false, Createable: false, Updateable: false},                 // System audit -> not createable
				{APIName: "CreatedDate", Type: "datetime", IsSystem: true, Createable: false, Updateable: false},   // System field -> not createable
				{APIName: "Name", Type: "string", Createable: true, Updateable: true},                              // Normal -> createable
				{APIName: "FormulaField", Type: "string", Formula: &formula, Createable: false, Updateable: false}, // Formula -> not createable
			},
		},
	}

	svc, user, cleanup := testService(schemas)
	defer cleanup()

	info, err := svc.GetObjectInfo(context.Background(), "Account", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	// Id is a system audit field -> not createable/updateable
	if idField, ok := info.Fields["Id"]; ok {
		if idField.Createable {
			t.Error("Id field should NOT be createable (system audit field)")
		}
		if idField.Updateable {
			t.Error("Id field should NOT be updateable (system audit field)")
		}
	}

	// CreatedDate is system -> not createable
	if cd, ok := info.Fields["CreatedDate"]; ok {
		if cd.Createable {
			t.Error("CreatedDate should NOT be createable (system field)")
		}
	}

	// Name is normal -> createable
	if nameField, ok := info.Fields["Name"]; ok {
		if !nameField.Createable {
			t.Error("Name field should be createable")
		}
		if !nameField.Updateable {
			t.Error("Name field should be updateable")
		}
	}

	// Formula field -> not createable
	if ff, ok := info.Fields["FormulaField"]; ok {
		if ff.Createable {
			t.Error("Formula field should NOT be createable")
		}
		if ff.Updateable {
			t.Error("Formula field should NOT be updateable")
		}
	}
}

func TestGetObjectInfo_NameFieldIsEditableEvenWhenIsSystem(t *testing.T) {
	// In the new model, Createable/Updateable are stored properties.
	// IsSystem no longer affects editability — it only means "standard field" (custom=false).
	// Both Name and OwnerId are standard business fields → createable/updateable.
	schemas := []*models.ObjectMetadata{
		{
			APIName:     "Campaign",
			Label:       "Campaign",
			PluralLabel: "Campaigns",
			Fields: []models.FieldMetadata{
				{APIName: "Name", Type: "string", IsSystem: true, IsNameField: true, Createable: true, Updateable: true},
				{APIName: "OwnerId", Type: "reference", IsSystem: true, Createable: true, Updateable: true}, // Standard business field -> editable
			},
		},
	}

	svc, user, cleanup := testService(schemas)
	defer cleanup()

	info, err := svc.GetObjectInfo(context.Background(), "Campaign", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	// Name field: IsSystem=true but IsNameField=true -> MUST be createable/updateable
	if nameField, ok := info.Fields["Name"]; ok {
		if !nameField.Createable {
			t.Error("Name field (IsNameField=true) should be createable even when IsSystem=true")
		}
		if !nameField.Updateable {
			t.Error("Name field (IsNameField=true) should be updateable even when IsSystem=true")
		}
	} else {
		t.Error("Name field not found in object info")
	}

	// OwnerId: standard business field -> createable/updateable (IsSystem no longer affects editability)
	if ownerField, ok := info.Fields["OwnerId"]; ok {
		if !ownerField.Createable {
			t.Error("OwnerId should be createable (standard business field)")
		}
		if !ownerField.Updateable {
			t.Error("OwnerId should be updateable (standard business field)")
		}
	}
}

func TestGetObjectInfo_ReferenceField(t *testing.T) {
	relName := "Account"
	schemas := []*models.ObjectMetadata{
		{
			APIName:     "Contact",
			Label:       "Contact",
			PluralLabel: "Contacts",
			Fields: []models.FieldMetadata{
				{
					APIName:          "AccountId",
					Label:            "Account",
					Type:             constants.FieldTypeReference,
					ReferenceTo:      []string{"Account"},
					RelationshipName: &relName,
				},
			},
		},
	}

	svc, user, cleanup := testService(schemas)
	defer cleanup()

	info, err := svc.GetObjectInfo(context.Background(), "Contact", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	acctField, ok := info.Fields["AccountId"]
	if !ok {
		t.Fatal("expected 'AccountId' field")
	}
	if !acctField.Reference {
		t.Error("AccountId should be marked as reference")
	}
	if len(acctField.ReferenceToInfos) != 1 {
		t.Fatalf("expected 1 ReferenceToInfo, got %d", len(acctField.ReferenceToInfos))
	}
	if acctField.ReferenceToInfos[0].ApiName != "Account" {
		t.Errorf("ReferenceToInfo.ApiName = %q", acctField.ReferenceToInfos[0].ApiName)
	}
	if acctField.RelationshipName != "Account" {
		t.Errorf("RelationshipName = %q", acctField.RelationshipName)
	}
}

func TestGetObjectInfo_NonExistentObject(t *testing.T) {
	svc, user, cleanup := testService([]*models.ObjectMetadata{})
	defer cleanup()

	info, err := svc.GetObjectInfo(context.Background(), "NonExistent", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if info != nil {
		t.Error("expected nil for non-existent object")
	}
}

func TestGetObjectInfo_Caching(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{
			APIName:     "Account",
			Label:       "Account",
			PluralLabel: "Accounts",
			Fields:      []models.FieldMetadata{},
		},
	}

	svc, user, cleanup := testService(schemas)
	defer cleanup()

	ctx := context.Background()

	// First call populates cache
	info1, err := svc.GetObjectInfo(ctx, "Account", user)
	if err != nil {
		t.Fatalf("first call error: %v", err)
	}

	// Second call should return cached value
	info2, err := svc.GetObjectInfo(ctx, "Account", user)
	if err != nil {
		t.Fatalf("second call error: %v", err)
	}

	// Both should be the same pointer (from cache)
	if info1 != info2 {
		t.Error("expected cached pointer equality on second call")
	}
}

// =============================================================================
// GetLayout — layout generation
// =============================================================================

func TestGetLayout_BasicLayout(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{
			APIName:     "Account",
			Label:       "Account",
			PluralLabel: "Accounts",
			Fields: []models.FieldMetadata{
				{APIName: "Name", Label: "Account Name", Type: "string", Required: true},
				{APIName: "Industry", Label: "Industry", Type: "picklist"},
			},
		},
	}

	layouts := map[string][]*models.PageLayout{
		"Account": {
			{
				ID:            "layout-1",
				ObjectAPIName: "Account",
				LayoutName:    "Default Layout",
				IsDefault:     true,
				LayoutVersion: 99, // prevent Tier 2b version-check from deleting test fixture
				Sections: []models.PageSection{
					{
						ID:      "sec-1",
						Label:   "Details",
						Columns: 2,
						Fields:  []string{"Name", "Industry"},
					},
				},
			},
		},
	}

	svc, user, cleanup := testServiceWithLayouts(schemas, layouts)
	defer cleanup()

	layout, err := svc.GetLayout(context.Background(), "Account", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if layout == nil {
		t.Fatal("expected non-nil layout")
	}

	if layout.Id != "layout-1" {
		t.Errorf("Id = %q, want 'layout-1'", layout.Id)
	}
	if layout.Mode != "View" {
		t.Errorf("Mode = %q, want 'View'", layout.Mode)
	}
	if len(layout.Sections) != 1 {
		t.Fatalf("expected 1 section, got %d", len(layout.Sections))
	}

	sec := layout.Sections[0]
	if sec.Heading != "Details" {
		t.Errorf("Heading = %q, want 'Details'", sec.Heading)
	}
	if sec.Columns != 2 {
		t.Errorf("Columns = %d, want 2", sec.Columns)
	}
	if !sec.UseHeading {
		t.Error("UseHeading should be true")
	}
	if !sec.Collapsible {
		t.Error("Collapsible should be true")
	}

	// With 2 columns and 2 fields, should produce 1 row
	if len(sec.LayoutRows) != 1 {
		t.Fatalf("expected 1 row, got %d", len(sec.LayoutRows))
	}
	if len(sec.LayoutRows[0].LayoutItems) != 2 {
		t.Errorf("expected 2 items in row, got %d", len(sec.LayoutRows[0].LayoutItems))
	}

	// Verify field labels are resolved from schema
	item := sec.LayoutRows[0].LayoutItems[0]
	if item.Label != "Account Name" {
		t.Errorf("item Label = %q, want 'Account Name'", item.Label)
	}
	if !item.Required {
		t.Error("Name field should be required")
	}
}

func TestGetLayout_TabsPassedThrough(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{
			APIName:     "Opportunity",
			Label:       "Opportunity",
			PluralLabel: "Opportunities",
			Fields: []models.FieldMetadata{
				{APIName: "Name", Label: "Opportunity Name", Type: "string", Required: true},
			},
		},
	}

	layouts := map[string][]*models.PageLayout{
		"Opportunity": {
			{
				ID:            "layout-tabs",
				ObjectAPIName: "Opportunity",
				LayoutName:    "Default Layout",
				IsDefault:     true,
				LayoutVersion: 99,
				Tabs:          []string{"Details", "Related", "Activity"},
				Sections: []models.PageSection{
					{
						ID:      "sec-1",
						Label:   "Information",
						Columns: 1,
						Fields:  []string{"Name"},
					},
				},
			},
		},
	}

	svc, user, cleanup := testServiceWithLayouts(schemas, layouts)
	defer cleanup()

	layout, err := svc.GetLayout(context.Background(), "Opportunity", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if layout == nil {
		t.Fatal("expected non-nil layout")
	}

	// Verify tabs are passed through from internal layout to UI API response
	if len(layout.Tabs) != 3 {
		t.Fatalf("expected 3 tabs, got %d", len(layout.Tabs))
	}
	expectedTabs := []string{"Details", "Related", "Activity"}
	for i, tab := range expectedTabs {
		if layout.Tabs[i] != tab {
			t.Errorf("Tabs[%d] = %q, want %q", i, layout.Tabs[i], tab)
		}
	}
}

func TestGetLayout_SingleColumnLayout(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{
			APIName: "Account",
			Label:   "Account",
			Fields: []models.FieldMetadata{
				{APIName: "Name", Label: "Name", Type: "string"},
				{APIName: "Phone", Label: "Phone", Type: "phone"},
				{APIName: "Email", Label: "Email", Type: "email"},
			},
		},
	}

	layouts := map[string][]*models.PageLayout{
		"Account": {
			{
				ID:            "layout-single",
				ObjectAPIName: "Account",
				LayoutName:    "Single Column",
				IsDefault:     true,
				LayoutVersion: 99, // prevent Tier 2b version-check from deleting test fixture
				Sections: []models.PageSection{
					{
						ID:      "sec-1",
						Label:   "Info",
						Columns: 1,
						Fields:  []string{"Name", "Phone", "Email"},
					},
				},
			},
		},
	}

	svc, user, cleanup := testServiceWithLayouts(schemas, layouts)
	defer cleanup()

	layout, err := svc.GetLayout(context.Background(), "Account", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	// With 1 column and 3 fields, should produce 3 rows
	sec := layout.Sections[0]
	if len(sec.LayoutRows) != 3 {
		t.Errorf("expected 3 rows for single column with 3 fields, got %d", len(sec.LayoutRows))
	}
}

func TestGetLayout_NonExistentObject(t *testing.T) {
	svc, user, cleanup := testServiceWithLayouts([]*models.ObjectMetadata{}, nil)
	defer cleanup()

	layout, err := svc.GetLayout(context.Background(), "NonExistent", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if layout != nil {
		t.Error("expected nil layout for non-existent object")
	}
}

func TestGetLayout_SystemAuditFieldsNotEditable(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{
			APIName: "Account",
			Label:   "Account",
			Fields: []models.FieldMetadata{
				{APIName: "Id", Label: "Record ID", Type: "id", Createable: false, Updateable: false},
				{APIName: "CreatedDate", Label: "Created Date", Type: "datetime", Createable: false, Updateable: false},
				{APIName: "Name", Label: "Name", Type: "string", Createable: true, Updateable: true},
			},
		},
	}

	layouts := map[string][]*models.PageLayout{
		"Account": {
			{
				ID:            "layout-1",
				ObjectAPIName: "Account",
				LayoutName:    "Default",
				IsDefault:     true,
				LayoutVersion: 99, // prevent Tier 2b version-check from deleting test fixture
				Sections: []models.PageSection{
					{ID: "sec-1", Label: "Fields", Columns: 1, Fields: []string{"Id", "CreatedDate", "Name"}},
				},
			},
		},
	}

	svc, user, cleanup := testServiceWithLayouts(schemas, layouts)
	defer cleanup()

	layout, err := svc.GetLayout(context.Background(), "Account", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	for _, row := range layout.Sections[0].LayoutRows {
		for _, item := range row.LayoutItems {
			apiName := item.LayoutComponents[0].ApiName
			switch apiName {
			case "Id", "CreatedDate":
				if item.EditableForNew {
					t.Errorf("%s should NOT be editable for new", apiName)
				}
				if item.EditableForUpdate {
					t.Errorf("%s should NOT be editable for update", apiName)
				}
			case "Name":
				if !item.EditableForNew {
					t.Error("Name should be editable for new")
				}
				if !item.EditableForUpdate {
					t.Error("Name should be editable for update")
				}
			}
		}
	}
}

func TestGetLayout_NameFieldNotSystemShouldBeEditable(t *testing.T) {
	// Regression test for issue #300: Campaign.Name was marked IsSystem=true.
	// In the new model, IsSystem no longer affects editability.
	// Createable/Updateable are stored properties that determine editability.
	schemas := []*models.ObjectMetadata{
		{
			APIName: "Campaign",
			Label:   "Campaign",
			Fields: []models.FieldMetadata{
				{APIName: "Id", Label: "Record ID", Type: "id", IsSystem: true, Createable: false, Updateable: false},
				{APIName: "Name", Label: "Campaign Name", Type: "string", Required: true, IsSystem: false, Createable: true, Updateable: true},
				{APIName: "OwnerId", Label: "Campaign Owner", Type: "reference", IsSystem: false, Createable: true, Updateable: true},
				{APIName: "CreatedDate", Label: "Created Date", Type: "datetime", IsSystem: true, Createable: false, Updateable: false},
			},
		},
	}

	layouts := map[string][]*models.PageLayout{
		"Campaign": {
			{
				ID:            "layout-campaign",
				ObjectAPIName: "Campaign",
				LayoutName:    "Default",
				IsDefault:     true,
				LayoutVersion: 99,
				Sections: []models.PageSection{
					{ID: "sec-info", Label: "Information", Columns: 2, Fields: []string{"Name", "OwnerId", "CreatedDate"}},
				},
			},
		},
	}

	svc, user, cleanup := testServiceWithLayouts(schemas, layouts)
	defer cleanup()

	layout, err := svc.GetLayout(context.Background(), "Campaign", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	found := map[string]bool{}
	for _, row := range layout.Sections[0].LayoutRows {
		for _, item := range row.LayoutItems {
			apiName := item.LayoutComponents[0].ApiName
			found[apiName] = true
			switch apiName {
			case "Name":
				if !item.EditableForNew {
					t.Error("Campaign Name (IsSystem=false) should be editable for new records")
				}
				if !item.EditableForUpdate {
					t.Error("Campaign Name (IsSystem=false) should be editable for update")
				}
				if !item.Required {
					t.Error("Campaign Name should be required")
				}
			case "OwnerId":
				if !item.EditableForNew {
					t.Error("Campaign OwnerId (IsSystem=false) should be editable for new records")
				}
			case "CreatedDate":
				if item.EditableForNew {
					t.Error("CreatedDate (IsSystem=true) should NOT be editable for new")
				}
			}
		}
	}

	if !found["Name"] {
		t.Error("Name field must be present in the layout")
	}
}

func TestGetLayout_RelatedLists(t *testing.T) {
	relName := "Account"
	schemas := []*models.ObjectMetadata{
		{
			APIName:     "Account",
			Label:       "Account",
			PluralLabel: "Accounts",
			Fields:      []models.FieldMetadata{},
		},
		{
			APIName:     "Contact",
			Label:       "Contact",
			PluralLabel: "Contacts",
			Fields: []models.FieldMetadata{
				{
					APIName:          "AccountId",
					Label:            "Account",
					Type:             constants.FieldTypeReference,
					ReferenceTo:      []string{"Account"},
					RelationshipName: &relName,
				},
				{APIName: "FullName", Label: "Full Name", Type: "string", IsNameField: true},
			},
		},
	}

	layouts := map[string][]*models.PageLayout{
		"Account": {
			{
				ID:            "layout-1",
				ObjectAPIName: "Account",
				LayoutName:    "Default",
				IsDefault:     true,
				LayoutVersion: 99, // prevent Tier 2b version-check from deleting test fixture
				Sections:      []models.PageSection{},
				RelatedLists: []models.RelatedListConfig{
					{
						ID:             "Contact-AccountId",
						Label:          "Contacts",
						SecondaryLabel: "via Account",
						ObjectAPIName:  "Contact",
						LookupField:    "AccountId",
						Fields:         []string{"FullName", "CreatedDate"},
					},
				},
			},
		},
	}

	svc, user, cleanup := testServiceWithLayouts(schemas, layouts)
	defer cleanup()

	layout, err := svc.GetLayout(context.Background(), "Account", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if len(layout.RelatedLists) == 0 {
		t.Fatal("expected at least 1 related list")
	}

	found := false
	for _, rl := range layout.RelatedLists {
		if rl.ObjectApiName == "Contact" {
			found = true
			if rl.LookupField != "AccountId" {
				t.Errorf("LookupField = %q, want 'AccountId'", rl.LookupField)
			}
			if rl.Label != "Contacts" {
				t.Errorf("Label = %q, want 'Contacts'", rl.Label)
			}
			if rl.SecondaryLabel != "via Account" {
				t.Errorf("SecondaryLabel = %q, want 'via Account'", rl.SecondaryLabel)
			}
			// Name field is custom (FullName)
			if len(rl.Fields) == 0 || rl.Fields[0] != "FullName" {
				t.Errorf("Fields = %v, expected [FullName]", rl.Fields)
			}
		}
	}
	if !found {
		t.Error("expected Contact related list")
	}
}

func TestGetLayout_CampaignHierarchyCuration(t *testing.T) {
	relName := "Campaign"
	schemas := []*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},
			},
		},
	}

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

	svc, user, cleanup := testServiceWithLayouts(schemas, layouts)
	defer cleanup()

	layout, err := svc.GetLayout(context.Background(), "Campaign", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if len(layout.RelatedLists) != 1 {
		t.Fatalf("expected 1 related list, got %d", len(layout.RelatedLists))
	}

	rl := layout.RelatedLists[0]
	if rl.Label != "Campaign Hierarchy" {
		t.Errorf("Label = %q, want 'Campaign Hierarchy'", rl.Label)
	}
	wantFields := []string{"Name", "ActualCost", "AmountAllOpportunities", "AmountWonOpportunities"}
	if len(rl.Fields) != len(wantFields) {
		t.Fatalf("Fields = %v, want %v", rl.Fields, wantFields)
	}
	for i, want := range wantFields {
		if rl.Fields[i] != want {
			t.Fatalf("Fields[%d] = %q, want %q", i, rl.Fields[i], want)
		}
	}
}

// =============================================================================
// GetRecordDefaults — create defaults
// =============================================================================

func TestGetRecordDefaults_BasicDefaults(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{
			APIName:     "Account",
			Label:       "Account",
			PluralLabel: "Accounts",
			Fields: []models.FieldMetadata{
				{APIName: "Name", Label: "Account Name", Type: "string"},
				{APIName: "Active", Label: "Active", Type: "boolean"},
				{APIName: "Revenue", Label: "Revenue", Type: "currency"},
				{APIName: "OwnerId", Label: "Owner", Type: constants.FieldTypeReference},
			},
		},
	}

	svc, user, cleanup := testService(schemas)
	defer cleanup()

	defaults, err := svc.GetRecordDefaults(context.Background(), "Account", "rt-1", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if defaults == nil {
		t.Fatal("expected non-nil defaults")
	}

	if defaults.ObjectApiName != "Account" {
		t.Errorf("ObjectApiName = %q", defaults.ObjectApiName)
	}
	if defaults.RecordTypeId != "rt-1" {
		t.Errorf("RecordTypeId = %q", defaults.RecordTypeId)
	}

	// Boolean without an explicit default should remain nil
	if active, ok := defaults.Record.Fields["Active"]; ok {
		if active.Value != nil {
			t.Errorf("boolean default = %v, want nil", active.Value)
		}
	}

	// Currency without an explicit default should remain nil
	if rev, ok := defaults.Record.Fields["Revenue"]; ok {
		if rev.Value != nil {
			t.Errorf("currency default = %v, want nil", rev.Value)
		}
	}

	// String without an explicit default should remain nil
	if name, ok := defaults.Record.Fields["Name"]; ok {
		if name.Value != nil {
			t.Errorf("string default = %v, want nil", name.Value)
		}
	}

	// OwnerId defaults to current user
	if owner, ok := defaults.Record.Fields["OwnerId"]; ok {
		if owner.Value != user.ID {
			t.Errorf("OwnerId = %v, want %v", owner.Value, user.ID)
		}
		if owner.DisplayValue == nil || *owner.DisplayValue != user.Name {
			t.Errorf("OwnerId DisplayValue = %v, want %q", owner.DisplayValue, user.Name)
		}
	}

	// Should include ObjectInfo
	if defaults.ObjectInfo == nil {
		t.Error("expected ObjectInfo in defaults")
	}
}

func TestGetRecordDefaults_WithDefaultValue(t *testing.T) {
	defaultVal := "High"
	schemas := []*models.ObjectMetadata{
		{
			APIName:     "Case",
			Label:       "Case",
			PluralLabel: "Cases",
			Fields: []models.FieldMetadata{
				{APIName: "Priority", Label: "Priority", Type: "picklist", DefaultValue: &defaultVal},
			},
		},
	}

	svc, user, cleanup := testService(schemas)
	defer cleanup()

	defaults, err := svc.GetRecordDefaults(context.Background(), "Case", "", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	priority, ok := defaults.Record.Fields["Priority"]
	if !ok {
		t.Fatal("expected Priority field in defaults")
	}
	if priority.Value != "High" {
		t.Errorf("Priority default = %v, want 'High'", priority.Value)
	}
}

func TestGetRecordDefaults_WithTypedBooleanAndNumericDefaults(t *testing.T) {
	boolDefault := "false"
	numberDefault := "0"
	doubleDefault := "1.5"
	schemas := []*models.ObjectMetadata{
		{
			APIName:     "Campaign",
			Label:       "Campaign",
			PluralLabel: "Campaigns",
			Fields: []models.FieldMetadata{
				{APIName: "IsActive", Label: "Active", Type: "boolean", DefaultValue: &boolDefault},
				{APIName: "BudgetedCost", Label: "Budgeted Cost", Type: "currency", DefaultValue: &numberDefault},
				{APIName: "Probability", Label: "Probability", Type: "double", DefaultValue: &doubleDefault},
			},
		},
	}

	svc, user, cleanup := testService(schemas)
	defer cleanup()

	defaults, err := svc.GetRecordDefaults(context.Background(), "Campaign", "", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if defaults.Record.Fields["IsActive"].Value != false {
		t.Errorf("IsActive default = %v, want false", defaults.Record.Fields["IsActive"].Value)
	}
	if defaults.Record.Fields["BudgetedCost"].Value != 0.0 {
		t.Errorf("BudgetedCost default = %v, want 0.0", defaults.Record.Fields["BudgetedCost"].Value)
	}
	if defaults.Record.Fields["Probability"].Value != 1.5 {
		t.Errorf("Probability default = %v, want 1.5", defaults.Record.Fields["Probability"].Value)
	}
}

func TestGetRecordDefaults_NonExistentObject(t *testing.T) {
	svc, user, cleanup := testService([]*models.ObjectMetadata{})
	defer cleanup()

	defaults, err := svc.GetRecordDefaults(context.Background(), "NonExistent", "", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if defaults != nil {
		t.Error("expected nil defaults for non-existent object")
	}
}

// =============================================================================
// GetCloneDefaults — clone defaults
// =============================================================================

func TestGetCloneDefaults_WithTypeHint(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{
			APIName:     "Account",
			Label:       "Account",
			PluralLabel: "Accounts",
			Fields: []models.FieldMetadata{
				{APIName: "Id", Label: "Record ID", Type: "id"},
				{APIName: "Name", Label: "Account Name", Type: "string"},
				{APIName: "Industry", Label: "Industry", Type: "picklist"},
				{APIName: "OwnerId", Label: "Owner", Type: constants.FieldTypeReference},
				{APIName: "CreatedDate", Label: "Created Date", Type: "datetime"},
			},
		},
	}

	records := map[string]map[string]models.SObject{
		"Account": {
			"acc-1": {
				"Id":          "acc-1",
				"Name":        "Acme Corp",
				"Industry":    "Technology",
				"OwnerId":     "old-owner",
				"CreatedDate": "2024-01-01T00:00:00Z",
			},
		},
	}

	svc, user, cleanup := testServiceWithRecords(schemas, records)
	defer cleanup()

	defaults, err := svc.GetCloneDefaults(context.Background(), "acc-1", "Account", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if defaults == nil {
		t.Fatal("expected non-nil defaults")
	}

	// Name should be copied from source
	if name, ok := defaults.Record.Fields["Name"]; ok {
		if name.Value != "Acme Corp" {
			t.Errorf("Name = %v, want 'Acme Corp'", name.Value)
		}
	} else {
		t.Error("expected Name field in clone defaults")
	}

	// Industry should be copied
	if ind, ok := defaults.Record.Fields["Industry"]; ok {
		if ind.Value != "Technology" {
			t.Errorf("Industry = %v, want 'Technology'", ind.Value)
		}
	}

	// OwnerId should be overridden with current user
	if owner, ok := defaults.Record.Fields["OwnerId"]; ok {
		if owner.Value != user.ID {
			t.Errorf("OwnerId = %v, want %v (current user)", owner.Value, user.ID)
		}
	}

	// System audit fields should be excluded
	if _, ok := defaults.Record.Fields["Id"]; ok {
		t.Error("Id should be excluded from clone defaults")
	}
	if _, ok := defaults.Record.Fields["CreatedDate"]; ok {
		t.Error("CreatedDate should be excluded from clone defaults")
	}
}

func TestGetCloneDefaults_RecordNotFound(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{
			APIName:     "Account",
			Label:       "Account",
			PluralLabel: "Accounts",
			Fields:      []models.FieldMetadata{},
		},
	}

	svc, user, cleanup := testServiceWithRecords(schemas, nil)
	defer cleanup()

	defaults, err := svc.GetCloneDefaults(context.Background(), "nonexistent", "", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if defaults != nil {
		t.Error("expected nil defaults for non-existent record")
	}
}

func TestGetCloneDefaults_DeniesUnreadableSourceRecord(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{
			APIName:     "Opportunity",
			Label:       "Opportunity",
			PluralLabel: "Opportunities",
			Fields: []models.FieldMetadata{
				{APIName: "Id", Type: "id"},
				{APIName: "Name", Type: "string"},
				{APIName: "OwnerId", Type: constants.FieldTypeReference},
			},
		},
	}

	records := map[string]map[string]models.SObject{
		"Opportunity": {
			"opp-1": {
				"Id":      "opp-1",
				"Name":    "Admin Deal",
				"OwnerId": "admin-user",
			},
		},
	}

	user := &models.UserSession{
		ID:        "sales-user",
		Name:      "Sales User",
		ProfileID: "sales_profile",
	}
	objectPerms := map[string]*models.ObjectPermissions{
		"Opportunity": {
			PermissionsRead:   true,
			PermissionsCreate: true,
		},
	}

	svc, cleanup := testServiceWithRecordsAndUser(schemas, records, objectPerms, user)
	defer cleanup()

	_, err := svc.GetCloneDefaults(context.Background(), "opp-1", "Opportunity", user)
	if err == nil {
		t.Fatal("expected forbidden error for unreadable source record")
	}

	apiErr, ok := err.(*apierr.Error)
	if !ok {
		t.Fatalf("expected apierr.Error, got %T", err)
	}
	if apiErr.Status != 404 {
		t.Fatalf("expected 404 (record hidden by RLS), got %d (%v)", apiErr.Status, err)
	}
}

// =============================================================================
// GetListInfo — list view metadata
// =============================================================================

func TestGetListInfo_Found(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{
			APIName:     "Account",
			Label:       "Account",
			PluralLabel: "Accounts",
			Fields: []models.FieldMetadata{
				{APIName: "Name", Label: "Account Name", Type: "string"},
				{APIName: "Industry", Label: "Industry", Type: "picklist"},
			},
		},
	}

	listViews := map[string][]*models.ListView{
		"Account": {
			{
				ID:            "lv-all",
				ObjectAPIName: "Account",
				Label:         "All Accounts",
				Fields:        []string{"Name", "Industry"},
			},
		},
	}

	svc, user, cleanup := testServiceWithListViews(schemas, listViews)
	defer cleanup()

	info, err := svc.GetListInfo(context.Background(), "Account", "lv-all", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if info == nil {
		t.Fatal("expected non-nil ListInfo")
	}

	if info.Id != "lv-all" {
		t.Errorf("Id = %q, want 'lv-all'", info.Id)
	}
	if info.Label != "All Accounts" {
		t.Errorf("Label = %q, want 'All Accounts'", info.Label)
	}
	if info.ObjectApiName != "Account" {
		t.Errorf("ObjectApiName = %q", info.ObjectApiName)
	}
	if info.Visibility != "Public" {
		t.Errorf("Visibility = %q, want 'Public'", info.Visibility)
	}

	// Display columns should have resolved labels
	if len(info.DisplayColumns) != 2 {
		t.Fatalf("expected 2 columns, got %d", len(info.DisplayColumns))
	}
	if info.DisplayColumns[0].Label != "Account Name" {
		t.Errorf("column 0 Label = %q, want 'Account Name'", info.DisplayColumns[0].Label)
	}
	if !info.DisplayColumns[0].Sortable {
		t.Error("columns should be sortable")
	}
}

func TestGetListInfo_MatchByLabel(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{APIName: "Account", Label: "Account", PluralLabel: "Accounts"},
	}

	listViews := map[string][]*models.ListView{
		"Account": {
			{ID: "lv-1", Label: "My Accounts", Fields: []string{"Name"}},
		},
	}

	svc, user, cleanup := testServiceWithListViews(schemas, listViews)
	defer cleanup()

	// Match by Label
	info, err := svc.GetListInfo(context.Background(), "Account", "My Accounts", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if info == nil {
		t.Fatal("expected to find list view by label")
	}
	if info.Label != "My Accounts" {
		t.Errorf("Label = %q", info.Label)
	}
}

func TestGetListInfo_NotFound(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{APIName: "Account", Label: "Account", PluralLabel: "Accounts"},
	}

	listViews := map[string][]*models.ListView{
		"Account": {
			{ID: "lv-1", Label: "All Accounts", Fields: []string{"Name"}},
		},
	}

	svc, user, cleanup := testServiceWithListViews(schemas, listViews)
	defer cleanup()

	info, err := svc.GetListInfo(context.Background(), "Account", "NonExistent", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if info != nil {
		t.Error("expected nil for non-existent list view")
	}
}

func TestGetListInfo_NoListViews(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{APIName: "Account", Label: "Account", PluralLabel: "Accounts"},
	}

	svc, user, cleanup := testServiceWithListViews(schemas, nil)
	defer cleanup()

	info, err := svc.GetListInfo(context.Background(), "Account", "anything", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if info != nil {
		t.Error("expected nil when no list views exist")
	}
}

// =============================================================================
// processRecordForUI — full record processing
// =============================================================================

func TestProcessRecordForUI_BasicRecord(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{
			APIName:     "Account",
			Label:       "Account",
			PluralLabel: "Accounts",
			Fields: []models.FieldMetadata{
				{APIName: "Name", Label: "Account Name", Type: "string"},
				{APIName: "Revenue", Label: "Revenue", Type: "currency"},
			},
		},
	}

	records := map[string]map[string]models.SObject{
		"Account": {
			"acc-1": {
				"Id":               "acc-1",
				"Name":             "Acme Corp",
				"Revenue":          float64(1000000),
				"LastModifiedDate": "2024-06-15T10:30:00Z",
				"SystemModstamp":   "2024-06-15T10:30:00Z",
			},
		},
	}

	svc, user, cleanup := testServiceWithRecords(schemas, records)
	defer cleanup()

	uiRecord, err := svc.processRecordForUI(context.Background(), "Account", "acc-1", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if uiRecord == nil {
		t.Fatal("expected non-nil record")
	}

	if uiRecord.ApiName != "Account" {
		t.Errorf("ApiName = %q", uiRecord.ApiName)
	}
	if uiRecord.Id != "acc-1" {
		t.Errorf("Id = %q", uiRecord.Id)
	}
	if uiRecord.LastModifiedDate != "2024-06-15T10:30:00Z" {
		t.Errorf("LastModifiedDate = %q", uiRecord.LastModifiedDate)
	}

	// Revenue should have formatted display value
	if rev, ok := uiRecord.Fields["Revenue"]; ok {
		if rev.DisplayValue == nil || *rev.DisplayValue != "$1000000.00" {
			t.Errorf("Revenue DisplayValue = %v", rev.DisplayValue)
		}
	}
}

func TestProcessRecordForUI_RecordAccessUsesValidationRuleLock(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{
			APIName:     "Lead",
			Label:       "Lead",
			PluralLabel: "Leads",
			Fields: []models.FieldMetadata{
				{APIName: "Name", Label: "Name", Type: "string"},
				{APIName: "IsConverted", Label: "Converted", Type: "boolean"},
			},
		},
	}
	records := map[string]map[string]models.SObject{
		"Lead": {
			"lead-1": {
				"Id":          "lead-1",
				"Name":        "Converted Lead",
				"IsConverted": true,
			},
		},
	}
	rules := []*models.ValidationRule{{
		ObjectAPIName: "Lead",
		Active:        true,
		Name:          "ReadOnlyAfterState",
		Condition:     `PRIORVALUE(IsConverted) = TRUE`,
		ErrorMessage:  "record is locked",
	}}

	svc, user, cleanup := testServiceWithRecordsAndValidationRules(schemas, records, rules)
	defer cleanup()

	uiRecord, err := svc.processRecordForUI(context.Background(), "Lead", "lead-1", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if uiRecord.RecordAccess == nil {
		t.Fatal("expected recordAccess")
	}
	if uiRecord.RecordAccess.HasEditAccess {
		t.Fatal("prior-only validation lock must make UI API edit access false")
	}
	if !uiRecord.RecordAccess.HasReadAccess {
		t.Fatal("validation lock must not remove read access")
	}
}

func TestProcessRecordForUI_RecordAccessValidationLockUsesRawRecord(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{
			APIName:      "Lead",
			Label:        "Lead",
			PluralLabel:  "Leads",
			SharingModel: constants.SharingModelPublicReadWrite,
			Fields: []models.FieldMetadata{
				{APIName: "Id", Label: "Record ID", Type: "id", IsSystem: true},
				{APIName: "Name", Label: "Name", Type: "string"},
				{APIName: "Lock__c", Label: "Lock", Type: "boolean", Source: "custom"},
			},
		},
	}
	records := map[string]map[string]models.SObject{
		"Lead": {
			"lead-1": {
				"Id":      "lead-1",
				"Name":    "Locked Lead",
				"Lock__c": true,
			},
		},
	}
	rules := []*models.ValidationRule{{
		ObjectAPIName: "Lead",
		Active:        true,
		Name:          "ReadOnlyAfterLock",
		Condition:     `PRIORVALUE(Lock__c) = TRUE`,
		ErrorMessage:  "record is locked",
	}}
	permRepo := &mockPermRepo{
		objectPerms: map[string]*models.ObjectPermissions{
			"Lead": {
				SobjectType:       "Lead",
				PermissionsRead:   true,
				PermissionsEdit:   true,
				PermissionsDelete: true,
			},
		},
		fieldPerms: map[string]*models.FieldPermissions{
			"Lead.Lock__c": {
				SobjectType:     "Lead",
				Field:           "Lock__c",
				PermissionsRead: false,
				PermissionsEdit: false,
			},
		},
	}
	user := &models.UserSession{ID: "user-1", Name: "Standard User", ProfileID: "profile-1"}

	svc, _, cleanup := testServiceWithRecordsValidationRulesPermsAndUser(schemas, records, rules, permRepo, user)
	defer cleanup()

	uiRecord, err := svc.processRecordForUI(context.Background(), "Lead", "lead-1", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if _, ok := uiRecord.Fields["Lock__c"]; ok {
		t.Fatal("test setup expected Lock__c to be FLS-filtered from the returned UI record")
	}
	if uiRecord.RecordAccess == nil {
		t.Fatal("expected recordAccess")
	}
	if uiRecord.RecordAccess.HasEditAccess {
		t.Fatal("validation lock must evaluate against raw prior image even when the lock field is hidden by FLS")
	}
}

func TestRecordEditBlockedByValidationRulesForID_UsesRawRecordBeforeFallback(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{
			APIName:      "Lead",
			Label:        "Lead",
			PluralLabel:  "Leads",
			SharingModel: constants.SharingModelPublicReadWrite,
			Fields: []models.FieldMetadata{
				{APIName: "Id", Label: "Record ID", Type: "id", IsSystem: true},
				{APIName: "Name", Label: "Name", Type: "string"},
				{APIName: "Lock__c", Label: "Lock", Type: "boolean", Source: "custom"},
			},
		},
	}
	records := map[string]map[string]models.SObject{
		"Lead": {
			"lead-1": {
				"Id":      "lead-1",
				"Name":    "Locked Lead",
				"Lock__c": true,
			},
		},
	}
	rules := []*models.ValidationRule{{
		ObjectAPIName: "Lead",
		Active:        true,
		Name:          "ReadOnlyAfterLock",
		Condition:     `PRIORVALUE(Lock__c) = TRUE`,
		ErrorMessage:  "record is locked",
	}}

	svc, _, cleanup := testServiceWithRecordsAndValidationRules(schemas, records, rules)
	defer cleanup()

	filteredFallback := models.SObject{
		"Id":   "lead-1",
		"Name": "Locked Lead",
	}

	blocked := svc.recordEditBlockedByValidationRulesForID(context.Background(), schemas[0], "lead-1", filteredFallback)
	if !blocked {
		t.Fatal("validation lock must use the raw record when fallback does not contain hidden lock fields")
	}
}

func TestProcessRecordForUI_ConversionStateUsesMetadataConvertedLookups(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{
			APIName:      "Lead",
			Label:        "Lead",
			PluralLabel:  "Leads",
			SharingModel: constants.SharingModelPublicReadWrite,
			Fields: []models.FieldMetadata{
				{APIName: "Id", Label: "Record ID", Type: "id", IsSystem: true},
				{APIName: "Name", Label: "Name", Type: "string"},
				{APIName: "IsConverted", Label: "Converted", Type: constants.FieldTypeBoolean},
				{APIName: "ConvertedDate", Label: "Converted Date", Type: constants.FieldTypeDate},
				{APIName: "ConvertedAccountId", Label: "Converted Account ID", Type: constants.FieldTypeReference, ReferenceTo: []string{"Account"}},
				{APIName: "ConvertedContactId", Label: "Converted Contact ID", Type: constants.FieldTypeReference, ReferenceTo: []string{"Contact"}},
				{APIName: "ConvertedOpportunityId", Label: "Converted Opportunity ID", Type: constants.FieldTypeReference, ReferenceTo: []string{"Opportunity"}},
				{APIName: constants.FieldLastModifiedByID, Label: "Last Modified By", Type: constants.FieldTypeReference, ReferenceTo: []string{constants.TableUser}},
			},
		},
		{
			APIName:      constants.TableUser,
			Label:        "User",
			PluralLabel:  "Users",
			SharingModel: constants.SharingModelPublicReadWrite,
			Fields: []models.FieldMetadata{
				{APIName: "Id", Label: "Record ID", Type: "id", IsSystem: true},
				{APIName: "Name", Label: "Name", Type: "string"},
			},
		},
	}
	records := map[string]map[string]models.SObject{
		"Lead": {
			"lead-1": {
				"Id":                     "lead-1",
				"Name":                   "Converted Lead",
				"IsConverted":            true,
				"ConvertedDate":          "2026-04-29",
				"ConvertedAccountId":     "account-1",
				"ConvertedContactId":     "contact-1",
				"ConvertedOpportunityId": "opportunity-1",
				"LastModifiedById":       "user-1",
			},
		},
		constants.TableUser: {
			"user-1": {
				"Id":   "user-1",
				"Name": "Ada Admin",
			},
		},
	}

	svc, user, cleanup := testServiceWithRecords(schemas, records)
	defer cleanup()

	uiRecord, err := svc.processRecordForUI(context.Background(), "Lead", "lead-1", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if uiRecord.ConversionState == nil {
		t.Fatal("expected conversion state")
	}
	if !uiRecord.ConversionState.IsConverted {
		t.Fatal("expected converted state to be true")
	}
	if uiRecord.ConversionState.ConvertedDate != "2026-04-29" {
		t.Fatalf("converted date = %q, want 2026-04-29", uiRecord.ConversionState.ConvertedDate)
	}
	if uiRecord.ConversionState.ConvertedByID != "user-1" {
		t.Fatalf("converted by id = %q, want user-1", uiRecord.ConversionState.ConvertedByID)
	}
	if uiRecord.ConversionState.ConvertedByName != "Ada Admin" {
		t.Fatalf("converted by name = %q, want Ada Admin", uiRecord.ConversionState.ConvertedByName)
	}
	if len(uiRecord.ConversionState.Links) != 3 {
		t.Fatalf("conversion links count = %d, want 3", len(uiRecord.ConversionState.Links))
	}
	if uiRecord.ConversionState.Links[0].FieldName != "ConvertedAccountId" ||
		uiRecord.ConversionState.Links[0].Label != "Account" ||
		uiRecord.ConversionState.Links[0].TargetObject != "Account" ||
		uiRecord.ConversionState.Links[0].RecordID != "account-1" {
		t.Fatalf("first conversion link = %+v", uiRecord.ConversionState.Links[0])
	}
}

func TestGetBatchRecordAccess_UsesValidationRuleLock(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{
			APIName:     "Lead",
			Label:       "Lead",
			PluralLabel: "Leads",
			Fields: []models.FieldMetadata{
				{APIName: "Name", Label: "Name", Type: "string"},
				{APIName: "IsConverted", Label: "Converted", Type: "boolean"},
			},
		},
	}
	records := map[string]map[string]models.SObject{
		"Lead": {
			"lead-1": {
				"Id":          "lead-1",
				"Name":        "Converted Lead",
				"IsConverted": true,
			},
		},
	}
	rules := []*models.ValidationRule{{
		ObjectAPIName: "Lead",
		Active:        true,
		Name:          "ReadOnlyAfterState",
		Condition:     `PRIORVALUE(IsConverted) = TRUE`,
		ErrorMessage:  "record is locked",
	}}

	svc, user, cleanup := testServiceWithRecordsAndValidationRules(schemas, records, rules)
	defer cleanup()

	accessByID, err := svc.GetBatchRecordAccess(context.Background(), "Lead", []string{"lead-1"}, user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	access := accessByID["lead-1"]
	if access == nil {
		t.Fatal("expected access for lead-1")
	}
	if access.HasEditAccess {
		t.Fatal("prior-only validation lock must make batch UI API edit access false")
	}
	if !access.HasReadAccess {
		t.Fatal("validation lock must not remove read access")
	}
}

func TestGetBatchRecordAccess_ValidationLockUsesRawRecords(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{
			APIName:      "Lead",
			Label:        "Lead",
			PluralLabel:  "Leads",
			SharingModel: constants.SharingModelPublicReadWrite,
			Fields: []models.FieldMetadata{
				{APIName: "Id", Label: "Record ID", Type: "id", IsSystem: true},
				{APIName: "Name", Label: "Name", Type: "string"},
				{APIName: "Lock__c", Label: "Lock", Type: "boolean", Source: "custom"},
			},
		},
	}
	records := map[string]map[string]models.SObject{
		"Lead": {
			"lead-1": {
				"Id":      "lead-1",
				"Name":    "Locked Lead",
				"Lock__c": true,
			},
		},
	}
	rules := []*models.ValidationRule{{
		ObjectAPIName: "Lead",
		Active:        true,
		Name:          "ReadOnlyAfterLock",
		Condition:     `PRIORVALUE(Lock__c) = TRUE`,
		ErrorMessage:  "record is locked",
	}}
	permRepo := &mockPermRepo{
		objectPerms: map[string]*models.ObjectPermissions{
			"Lead": {
				SobjectType:       "Lead",
				PermissionsRead:   true,
				PermissionsEdit:   true,
				PermissionsDelete: true,
			},
		},
		fieldPerms: map[string]*models.FieldPermissions{
			"Lead.Lock__c": {
				SobjectType:     "Lead",
				Field:           "Lock__c",
				PermissionsRead: false,
				PermissionsEdit: false,
			},
		},
	}
	user := &models.UserSession{ID: "user-1", Name: "Standard User", ProfileID: "profile-1"}

	svc, _, cleanup := testServiceWithRecordsValidationRulesPermsAndUser(schemas, records, rules, permRepo, user)
	defer cleanup()

	accessByID, err := svc.GetBatchRecordAccess(context.Background(), "Lead", []string{"lead-1"}, user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	access := accessByID["lead-1"]
	if access == nil {
		t.Fatal("expected access for lead-1")
	}
	if access.HasEditAccess {
		t.Fatal("batch validation lock must evaluate against raw records even when the lock field is hidden by FLS")
	}
	if !access.HasReadAccess {
		t.Fatal("validation lock must not remove read access")
	}
}

func TestRawRecordIDsForVisibleRecords_OnlyReturnsPermissionFilteredIDs(t *testing.T) {
	visible := map[string]models.SObject{
		"visible-1": {"Id": "visible-1"},
		"":          {"Id": ""},
	}

	ids := rawRecordIDsForVisibleRecords(visible)

	if len(ids) != 1 {
		t.Fatalf("ids = %v, want one visible id", ids)
	}
	if ids[0] != "visible-1" {
		t.Fatalf("ids[0] = %q, want visible-1", ids[0])
	}
}

func TestProcessRecordForUI_RecordNotFound(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{APIName: "Account", Label: "Account", PluralLabel: "Accounts", Fields: []models.FieldMetadata{}},
	}

	svc, user, cleanup := testServiceWithRecords(schemas, nil)
	defer cleanup()

	_, err := svc.processRecordForUI(context.Background(), "Account", "nonexistent", user)
	if err == nil {
		t.Fatal("expected error for non-existent record")
	}
}

func TestProcessRecordForUI_SchemaNotFound(t *testing.T) {
	// Schema exists but for a different object
	schemas := []*models.ObjectMetadata{
		{APIName: "Contact", Label: "Contact", PluralLabel: "Contacts", Fields: []models.FieldMetadata{}},
	}

	records := map[string]map[string]models.SObject{
		"Account": {
			"acc-1": {"Id": "acc-1", "Name": "Test"},
		},
	}

	svc, user, cleanup := testServiceWithRecords(schemas, records)
	defer cleanup()

	_, err := svc.processRecordForUI(context.Background(), "Account", "acc-1", user)
	if err == nil {
		t.Fatal("expected error when schema not found")
	}
}

func TestProcessRecordForUI_WithLookupResolution(t *testing.T) {
	relName := "Account"
	schemas := []*models.ObjectMetadata{
		{
			APIName:     "Contact",
			Label:       "Contact",
			PluralLabel: "Contacts",
			Fields: []models.FieldMetadata{
				{APIName: "Name", Label: "Full Name", Type: "string"},
				{
					APIName:          "AccountId",
					Label:            "Account",
					Type:             constants.FieldTypeReference,
					ReferenceTo:      []string{"Account"},
					RelationshipName: &relName,
				},
			},
		},
		{
			APIName:     "Account",
			Label:       "Account",
			PluralLabel: "Accounts",
			Fields:      []models.FieldMetadata{},
		},
	}

	records := map[string]map[string]models.SObject{
		"Contact": {
			"con-1": {
				"Id":        "con-1",
				"Name":      "John Doe",
				"AccountId": "acc-1",
			},
		},
		"Account": {
			"acc-1": {
				"Id":   "acc-1",
				"Name": "Acme Corp",
			},
		},
	}

	svc, user, cleanup := testServiceWithRecords(schemas, records)
	defer cleanup()

	uiRecord, err := svc.processRecordForUI(context.Background(), "Contact", "con-1", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	// The Account relationship should be resolved
	acctRel, ok := uiRecord.Fields["Account"]
	if !ok {
		t.Fatal("expected 'Account' relationship field")
	}
	if acctRel.DisplayValue == nil || *acctRel.DisplayValue != "Acme Corp" {
		t.Errorf("Account DisplayValue = %v, want 'Acme Corp'", acctRel.DisplayValue)
	}

	// The value should be a nested Record
	nestedRecord, ok := acctRel.Value.(Record)
	if !ok {
		t.Fatal("expected Value to be a Record")
	}
	if nestedRecord.ApiName != "Account" {
		t.Errorf("nested ApiName = %q", nestedRecord.ApiName)
	}
	if nestedRecord.Id != "acc-1" {
		t.Errorf("nested Id = %q", nestedRecord.Id)
	}
}

func TestProcessRecordForUI_DeniesForeignPrivateRecord(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{
			APIName:     "Opportunity",
			Label:       "Opportunity",
			PluralLabel: "Opportunities",
			Fields: []models.FieldMetadata{
				{APIName: "Id", Type: "id"},
				{APIName: "Name", Type: "string"},
				{APIName: "OwnerId", Type: constants.FieldTypeReference},
			},
		},
	}

	records := map[string]map[string]models.SObject{
		"Opportunity": {
			"opp-admin": {
				"Id":      "opp-admin",
				"Name":    "Admin Private Opportunity",
				"OwnerId": "admin-user",
			},
		},
	}

	user := &models.UserSession{
		ID:        "sales-user",
		Name:      "Sales User",
		ProfileID: "sales_profile",
	}
	objectPerms := map[string]*models.ObjectPermissions{
		"Opportunity": {
			PermissionsRead: true,
		},
	}

	svc, cleanup := testServiceWithRecordsAndUser(schemas, records, objectPerms, user)
	defer cleanup()

	_, err := svc.processRecordForUI(context.Background(), "Opportunity", "opp-admin", user)
	if err == nil {
		t.Fatal("expected forbidden error for foreign private record")
	}

	apiErr, ok := err.(*apierr.Error)
	if !ok {
		t.Fatalf("expected apierr.Error, got %T", err)
	}
	if apiErr.Status != 404 {
		t.Fatalf("expected 404 (record hidden by RLS), got %d (%v)", apiErr.Status, err)
	}
}

func TestProcessRecordForUI_OmitsUnreadableLookupRecord(t *testing.T) {
	relName := "Account"
	schemas := []*models.ObjectMetadata{
		{
			APIName:     "Contact",
			Label:       "Contact",
			PluralLabel: "Contacts",
			Fields: []models.FieldMetadata{
				{APIName: "Id", Type: "id"},
				{APIName: "Name", Type: "string"},
				{APIName: "OwnerId", Type: constants.FieldTypeReference},
				{
					APIName:          "AccountId",
					Type:             constants.FieldTypeReference,
					ReferenceTo:      []string{"Account"},
					RelationshipName: &relName,
				},
			},
		},
		{
			APIName:     "Account",
			Label:       "Account",
			PluralLabel: "Accounts",
			Fields: []models.FieldMetadata{
				{APIName: "Id", Type: "id"},
				{APIName: "Name", Type: "string"},
				{APIName: "OwnerId", Type: constants.FieldTypeReference},
			},
		},
	}

	records := map[string]map[string]models.SObject{
		"Contact": {
			"con-1": {
				"Id":        "con-1",
				"Name":      "Visible Contact",
				"OwnerId":   "sales-user",
				"AccountId": "acc-admin",
			},
		},
		"Account": {
			"acc-admin": {
				"Id":      "acc-admin",
				"Name":    "Admin Private Account",
				"OwnerId": "admin-user",
			},
		},
	}

	user := &models.UserSession{
		ID:        "sales-user",
		Name:      "Sales User",
		ProfileID: "sales_profile",
	}
	objectPerms := map[string]*models.ObjectPermissions{
		"Contact": {PermissionsRead: true},
		"Account": {PermissionsRead: true},
	}

	svc, cleanup := testServiceWithRecordsAndUser(schemas, records, objectPerms, user)
	defer cleanup()

	uiRecord, err := svc.processRecordForUI(context.Background(), "Contact", "con-1", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if _, ok := uiRecord.Fields["Account"]; ok {
		t.Fatal("expected inaccessible lookup relationship to be omitted")
	}
	if _, ok := uiRecord.Fields["AccountId"]; !ok {
		t.Fatal("expected raw lookup id field to remain present")
	}
}

// =============================================================================
// GetRecordUI — full aggregation
// =============================================================================

func TestGetRecordUI_FullAggregation(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{
			APIName:     "Account",
			Label:       "Account",
			PluralLabel: "Accounts",
			Fields: []models.FieldMetadata{
				{APIName: "Name", Label: "Account Name", Type: "string"},
			},
		},
	}

	records := map[string]map[string]models.SObject{
		"Account": {
			"acc-1": {"Id": "acc-1", "Name": "Acme Corp"},
			"acc-2": {"Id": "acc-2", "Name": "Widget Inc"},
		},
	}

	svc, user, cleanup := testServiceWithRecords(schemas, records)
	defer cleanup()

	result, err := svc.GetRecordUI(context.Background(), []string{"acc-1", "acc-2"}, "Account", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if result == nil {
		t.Fatal("expected non-nil result")
	}

	// Should have 2 records
	if len(result.Records) != 2 {
		t.Errorf("expected 2 records, got %d", len(result.Records))
	}

	// Should have ObjectInfo
	if _, ok := result.ObjectInfos["Account"]; !ok {
		t.Error("expected Account in ObjectInfos")
	}
}

func TestGetRecordUI_OriginalTypeHintPreservesCasing(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{
			APIName:     "Quote",
			Label:       "Quote",
			PluralLabel: "Quotes",
			Fields: []models.FieldMetadata{
				{APIName: "Name", Label: "Quote Name", Type: "string"},
			},
		},
	}

	// Use lowercase key to match the handler's behavior (typeHint is lowercased)
	records := map[string]map[string]models.SObject{
		"quote": {
			"qt-1": {"Id": "qt-1", "Name": "Test Quote"},
		},
	}

	svc, user, cleanup := testServiceWithRecords(schemas, records)
	defer cleanup()

	// Pass lowercase typeHint (as handler does) with original casing
	result, err := svc.GetRecordUI(context.Background(), []string{"qt-1"}, "quote", user, "Quote")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if result == nil {
		t.Fatal("expected non-nil result")
	}

	// Layout and ObjectInfo keys should use PascalCase "Quote", not lowercase "quote"
	if _, ok := result.ObjectInfos["Quote"]; !ok {
		t.Error("expected PascalCase 'Quote' in ObjectInfos, got keys: ")
		for k := range result.ObjectInfos {
			t.Logf("  key: %q", k)
		}
	}
}

func TestGetRecordUI_DeniesForeignPrivateRecord(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{
			APIName:     "Opportunity",
			Label:       "Opportunity",
			PluralLabel: "Opportunities",
			Fields: []models.FieldMetadata{
				{APIName: "Id", Type: "id"},
				{APIName: "Name", Type: "string"},
				{APIName: "OwnerId", Type: constants.FieldTypeReference},
			},
		},
	}

	records := map[string]map[string]models.SObject{
		"Opportunity": {
			"opp-admin": {
				"Id":      "opp-admin",
				"Name":    "Admin Private Opportunity",
				"OwnerId": "admin-user",
			},
		},
	}

	user := &models.UserSession{
		ID:        "sales-user",
		Name:      "Sales User",
		ProfileID: "sales_profile",
	}
	objectPerms := map[string]*models.ObjectPermissions{
		"Opportunity": {PermissionsRead: true},
	}

	svc, cleanup := testServiceWithRecordsAndUser(schemas, records, objectPerms, user)
	defer cleanup()

	_, err := svc.GetRecordUI(context.Background(), []string{"opp-admin"}, "Opportunity", user)
	if err == nil {
		t.Fatal("expected forbidden error for foreign private record")
	}

	apiErr, ok := err.(*apierr.Error)
	if !ok {
		t.Fatalf("expected apierr.Error, got %T", err)
	}
	if apiErr.Status != 404 {
		t.Fatalf("expected 404 (record hidden by RLS), got %d (%v)", apiErr.Status, err)
	}
}

// =============================================================================
// ResolveObjectType — object type resolution
// =============================================================================

func TestResolveObjectType_WithHint(t *testing.T) {
	svc, user, cleanup := testService([]*models.ObjectMetadata{})
	defer cleanup()

	objType, err := svc.ResolveObjectType(context.Background(), "any-id", "Account", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if objType != "Account" {
		t.Errorf("expected 'Account', got %q", objType)
	}
}

func TestResolveObjectType_WithoutHint_FindsRecord(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{APIName: "Account", Label: "Account", PluralLabel: "Accounts", Fields: []models.FieldMetadata{}},
	}

	records := map[string]map[string]models.SObject{
		"Account": {
			"acc-1": {"Id": "acc-1", "Name": "Test"},
		},
	}

	svc, user, cleanup := testServiceWithRecords(schemas, records)
	defer cleanup()

	objType, err := svc.ResolveObjectType(context.Background(), "acc-1", "", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if objType != "Account" {
		t.Errorf("expected 'Account', got %q", objType)
	}
}

func TestResolveObjectType_WithoutHint_NotFound(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{APIName: "Account", Label: "Account", PluralLabel: "Accounts", Fields: []models.FieldMetadata{}},
	}

	svc, user, cleanup := testServiceWithRecords(schemas, nil)
	defer cleanup()

	objType, err := svc.ResolveObjectType(context.Background(), "nonexistent", "", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if objType != "" {
		t.Errorf("expected empty string, got %q", objType)
	}
}

func TestResolveObjectType_SkipsSystemTables(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{APIName: "_System_Metadata", Label: "System", PluralLabel: "System", IsSystem: true, Fields: []models.FieldMetadata{}},
		{APIName: "Account", Label: "Account", PluralLabel: "Accounts", Fields: []models.FieldMetadata{}},
	}

	records := map[string]map[string]models.SObject{
		"Account": {
			"acc-1": {"Id": "acc-1", "Name": "Test"},
		},
	}

	svc, user, cleanup := testServiceWithRecords(schemas, records)
	defer cleanup()

	objType, err := svc.ResolveObjectType(context.Background(), "acc-1", "", user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	// Should find it in Account, not look in system tables
	if objType != "Account" {
		t.Errorf("expected 'Account', got %q", objType)
	}
}
