package uiapi

import (
	"context"
	"testing"
	"time"

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

func TestTruthyRecordValue(t *testing.T) {
	tests := []struct {
		name  string
		value interface{}
		want  bool
	}{
		{name: "bool true", value: true, want: true},
		{name: "bool false", value: false, want: false},
		{name: "int one", value: 1, want: true},
		{name: "int zero", value: 0, want: false},
		{name: "int64 one", value: int64(1), want: true},
		{name: "int64 zero", value: int64(0), want: false},
		{name: "float nonzero", value: float64(0.5), want: true},
		{name: "float zero", value: float64(0), want: false},
		{name: "string true", value: "true", want: true},
		{name: "string true spaced uppercase", value: " TRUE ", want: true},
		{name: "string one", value: "1", want: true},
		{name: "string false", value: "false", want: false},
		{name: "string empty", value: "", want: false},
		{name: "string nonboolean", value: "yes", want: false},
		{name: "nil", value: nil, want: false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := truthyRecordValue(tt.value); got != tt.want {
				t.Fatalf("truthyRecordValue(%v) = %v, want %v", tt.value, got, tt.want)
			}
		})
	}
}

func TestConversionLinkLabel(t *testing.T) {
	tests := []struct {
		name     string
		label    string
		fallback string
		want     string
	}{
		{name: "strips converted prefix and id suffix", label: "Converted Account ID", fallback: "Account", want: "Account"},
		{name: "case insensitive strips", label: "converted contact id", fallback: "Contact", want: "contact"},
		{name: "no strip", label: "Account", fallback: "Fallback", want: "Account"},
		{name: "prefix only", label: "Converted Opportunity", fallback: "Opportunity", want: "Opportunity"},
		{name: "suffix only", label: "Opportunity ID", fallback: "Opportunity", want: "Opportunity"},
		{name: "empty label falls back", label: "", fallback: "Account", want: "Account"},
		{name: "short label unchanged", label: "ID", fallback: "Account", want: "ID"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			field := models.FieldMetadata{Label: tt.label}
			if got := conversionLinkLabel(field, tt.fallback); got != tt.want {
				t.Fatalf("conversionLinkLabel(%q, %q) = %q, want %q", tt.label, tt.fallback, got, tt.want)
			}
		})
	}
}

// =============================================================================
// mapRecordFieldsToUI — field formatting and lookup collection
// Uses no service dependencies (only constants.IsReferenceType, a package func)
// =============================================================================

func TestMapRecordFieldsToUI_DateFormatting(t *testing.T) {
	svc := &Service{}

	tests := []struct {
		name           string
		fieldType      constants.SchemaFieldType
		value          interface{}
		wantDisplay    *string
		wantDisplayNil bool
		valueCoerced   bool
	}{
		{
			name:        "date field truncates to 10 chars",
			fieldType:   "date",
			value:       "2024-01-15T12:00:00Z",
			wantDisplay: strPtr("2024-01-15"),
		},
		{
			name:        "date field exact 10 chars",
			fieldType:   "date",
			value:       "2024-01-15",
			wantDisplay: strPtr("2024-01-15"),
		},
		{
			name:           "date field too short (< 10 chars)",
			fieldType:      "date",
			value:          "2024-01",
			wantDisplayNil: true,
		},
		{
			name:           "date field non-string value",
			fieldType:      "date",
			value:          12345,
			wantDisplayNil: true,
		},
		{
			name:        "datetime field truncates to 19 chars",
			fieldType:   "datetime",
			value:       "2024-01-15T12:30:45.000Z",
			wantDisplay: strPtr("2024-01-15T12:30:45"),
		},
		{
			name:        "datetime field exact 19 chars",
			fieldType:   "datetime",
			value:       "2024-01-15T12:30:45",
			wantDisplay: strPtr("2024-01-15T12:30:45"),
		},
		{
			name:           "datetime field too short (< 19 chars)",
			fieldType:      "datetime",
			value:          "2024-01-15",
			wantDisplayNil: true,
		},
		{
			name:        "currency field formats with dollar sign",
			fieldType:   "currency",
			value:       float64(1234.56),
			wantDisplay: strPtr("$1234.56"),
		},
		{
			name:        "currency field zero",
			fieldType:   "currency",
			value:       float64(0),
			wantDisplay: strPtr("$0.00"),
		},
		{
			name:         "currency field string-coerced",
			fieldType:    "currency",
			value:        "1234.56",
			wantDisplay:  strPtr("$1234.56"),
			valueCoerced: true,
		},
		{
			name:        "percent field formats with percent sign",
			fieldType:   "percent",
			value:       float64(75),
			wantDisplay: strPtr("75%"),
		},
		{
			name:        "percent field with decimal (rounds down)",
			fieldType:   "percent",
			value:       float64(33.33),
			wantDisplay: strPtr("33%"),
		},
		{
			name:        "percent field zero",
			fieldType:   "percent",
			value:       float64(0),
			wantDisplay: strPtr("0%"),
		},
		{
			name:           "string type has nil displayValue",
			fieldType:      "string",
			value:          "hello",
			wantDisplayNil: true,
		},
		{
			name:           "boolean type has nil displayValue",
			fieldType:      "boolean",
			value:          true,
			wantDisplayNil: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			fieldName := "TestField"
			rec := models.SObject{fieldName: tt.value}
			fieldIndex := map[string]*models.FieldMetadata{
				fieldName: {APIName: fieldName, Type: tt.fieldType},
			}
			uiRecord := &Record{Fields: make(map[string]FieldValue)}

			svc.mapRecordFieldsToUI(rec, fieldIndex, uiRecord)

			fv, ok := uiRecord.Fields[fieldName]
			if !ok {
				t.Fatalf("expected field %q in uiRecord.Fields", fieldName)
			}

			if tt.wantDisplayNil {
				if fv.DisplayValue != nil {
					t.Errorf("expected nil DisplayValue, got %q", *fv.DisplayValue)
				}
			} else if tt.wantDisplay != nil {
				if fv.DisplayValue == nil {
					t.Errorf("expected DisplayValue=%q, got nil", *tt.wantDisplay)
				} else if *fv.DisplayValue != *tt.wantDisplay {
					t.Errorf("DisplayValue = %q, want %q", *fv.DisplayValue, *tt.wantDisplay)
				}
			}

			if !tt.valueCoerced {
				if fv.Value != tt.value {
					t.Errorf("Value = %v, want %v", fv.Value, tt.value)
				}
			}
		})
	}
}

func TestMapRecordFieldsToUI_FieldWithNoMetadata(t *testing.T) {
	svc := &Service{}
	rec := models.SObject{"UnknownField": "some value"}
	fieldIndex := map[string]*models.FieldMetadata{} // Empty - no metadata for this field
	uiRecord := &Record{Fields: make(map[string]FieldValue)}

	svc.mapRecordFieldsToUI(rec, fieldIndex, uiRecord)

	fv, ok := uiRecord.Fields["UnknownField"]
	if !ok {
		t.Fatal("expected UnknownField in uiRecord.Fields")
	}
	if fv.DisplayValue != nil {
		t.Errorf("expected nil DisplayValue for field with no metadata, got %q", *fv.DisplayValue)
	}
	if fv.Value != "some value" {
		t.Errorf("Value = %v, want 'some value'", fv.Value)
	}
}

func TestMapRecordFieldsToUI_CollectsLookups(t *testing.T) {
	svc := &Service{}
	relName := "Account"
	rec := models.SObject{
		"AccountId": "001-abc-123",
	}
	fieldIndex := map[string]*models.FieldMetadata{
		"AccountId": {
			APIName:          "AccountId",
			Type:             constants.FieldTypeReference,
			ReferenceTo:      []string{"Account"},
			RelationshipName: &relName,
		},
	}
	uiRecord := &Record{Fields: make(map[string]FieldValue)}

	lookups := svc.mapRecordFieldsToUI(rec, fieldIndex, uiRecord)

	if len(lookups) != 1 {
		t.Fatalf("expected 1 lookup target, got %d", len(lookups))
	}
	lt := lookups[0]
	if lt.fieldName != "AccountId" {
		t.Errorf("lookup fieldName = %q, want 'AccountId'", lt.fieldName)
	}
	if lt.relName != "Account" {
		t.Errorf("lookup relName = %q, want 'Account'", lt.relName)
	}
	if lt.targetObj != "Account" {
		t.Errorf("lookup targetObj = %q, want 'Account'", lt.targetObj)
	}
	if lt.targetID != "001-abc-123" {
		t.Errorf("lookup targetID = %q, want '001-abc-123'", lt.targetID)
	}
}

func TestMapRecordFieldsToUI_MasterDetailLookup(t *testing.T) {
	svc := &Service{}
	relName := "Parent"
	rec := models.SObject{
		"ParentId": "parent-id-123",
	}
	fieldIndex := map[string]*models.FieldMetadata{
		"ParentId": {
			APIName:          "ParentId",
			Type:             constants.FieldTypeMasterDetail,
			ReferenceTo:      []string{"ParentObject"},
			RelationshipName: &relName,
		},
	}
	uiRecord := &Record{Fields: make(map[string]FieldValue)}

	lookups := svc.mapRecordFieldsToUI(rec, fieldIndex, uiRecord)

	if len(lookups) != 1 {
		t.Fatalf("expected 1 lookup for master-detail, got %d", len(lookups))
	}
	if lookups[0].targetObj != "ParentObject" {
		t.Errorf("targetObj = %q, want 'ParentObject'", lookups[0].targetObj)
	}
}

func TestMapRecordFieldsToUI_InferStandardLookupRelationshipNameWhenMetadataMissing(t *testing.T) {
	svc := &Service{}
	rec := models.SObject{
		"AccountId": "001-abc-123",
	}
	fieldIndex := map[string]*models.FieldMetadata{
		"AccountId": {
			APIName:     "AccountId",
			Type:        constants.FieldTypeReference,
			ReferenceTo: []string{"Account"},
		},
	}
	uiRecord := &Record{Fields: make(map[string]FieldValue)}

	lookups := svc.mapRecordFieldsToUI(rec, fieldIndex, uiRecord)

	if len(lookups) != 1 {
		t.Fatalf("expected 1 lookup when RelationshipName is inferred, got %d", len(lookups))
	}
	if lookups[0].relName != "Account" {
		t.Errorf("lookup relName = %q, want 'Account'", lookups[0].relName)
	}
}

func TestMapRecordFieldsToUI_InferCustomLookupRelationshipNameWhenMetadataMissing(t *testing.T) {
	svc := &Service{}
	rec := models.SObject{
		"Quote__c": "quote-123",
	}
	fieldIndex := map[string]*models.FieldMetadata{
		"Quote__c": {
			APIName:     "Quote__c",
			Type:        constants.FieldTypeReference,
			ReferenceTo: []string{"Quote"},
		},
	}
	uiRecord := &Record{Fields: make(map[string]FieldValue)}

	lookups := svc.mapRecordFieldsToUI(rec, fieldIndex, uiRecord)

	if len(lookups) != 1 {
		t.Fatalf("expected 1 lookup when custom RelationshipName is inferred, got %d", len(lookups))
	}
	if lookups[0].relName != "Quote__r" {
		t.Errorf("lookup relName = %q, want 'Quote__r'", lookups[0].relName)
	}
}

func TestMapRecordFieldsToUI_SkipsLookupWithoutInferrableRelationshipName(t *testing.T) {
	svc := &Service{}
	rec := models.SObject{
		"ExternalRef": "ext-123",
	}
	fieldIndex := map[string]*models.FieldMetadata{
		"ExternalRef": {
			APIName:     "ExternalRef",
			Type:        constants.FieldTypeReference,
			ReferenceTo: []string{"ExternalObject"},
		},
	}
	uiRecord := &Record{Fields: make(map[string]FieldValue)}

	lookups := svc.mapRecordFieldsToUI(rec, fieldIndex, uiRecord)

	if len(lookups) != 0 {
		t.Errorf("expected 0 lookups when relationship name cannot be inferred, got %d", len(lookups))
	}
}

func TestMapRecordFieldsToUI_SkipsLookupWithEmptyID(t *testing.T) {
	svc := &Service{}
	relName := "Account"
	rec := models.SObject{
		"AccountId": "", // Empty ID
	}
	fieldIndex := map[string]*models.FieldMetadata{
		"AccountId": {
			APIName:          "AccountId",
			Type:             constants.FieldTypeReference,
			ReferenceTo:      []string{"Account"},
			RelationshipName: &relName,
		},
	}
	uiRecord := &Record{Fields: make(map[string]FieldValue)}

	lookups := svc.mapRecordFieldsToUI(rec, fieldIndex, uiRecord)

	if len(lookups) != 0 {
		t.Errorf("expected 0 lookups when ID is empty, got %d", len(lookups))
	}
}

func TestMapRecordFieldsToUI_SkipsLookupWithNoReferenceTo(t *testing.T) {
	svc := &Service{}
	relName := "Account"
	rec := models.SObject{
		"AccountId": "001-abc-123",
	}
	fieldIndex := map[string]*models.FieldMetadata{
		"AccountId": {
			APIName:          "AccountId",
			Type:             constants.FieldTypeReference,
			ReferenceTo:      []string{}, // Empty
			RelationshipName: &relName,
		},
	}
	uiRecord := &Record{Fields: make(map[string]FieldValue)}

	lookups := svc.mapRecordFieldsToUI(rec, fieldIndex, uiRecord)

	if len(lookups) != 0 {
		t.Errorf("expected 0 lookups when ReferenceTo is empty, got %d", len(lookups))
	}
}

func TestMapRecordFieldsToUI_PolymorphicOwnerIdUsesUser(t *testing.T) {
	svc := &Service{}
	relName := "Owner"
	rec := models.SObject{
		"OwnerId": "user-id-123",
	}
	fieldIndex := map[string]*models.FieldMetadata{
		"OwnerId": {
			APIName:          "OwnerId",
			Type:             constants.FieldTypeReference,
			ReferenceTo:      []string{"User", "Group"},
			IsPolymorphic:    true,
			RelationshipName: &relName,
		},
	}
	uiRecord := &Record{Fields: make(map[string]FieldValue)}

	lookups := svc.mapRecordFieldsToUI(rec, fieldIndex, uiRecord)

	if len(lookups) != 1 {
		t.Fatalf("expected 1 lookup, got %d", len(lookups))
	}
	// Polymorphic OwnerId should always resolve to User
	if lookups[0].targetObj != constants.TableUser {
		t.Errorf("targetObj = %q, want %q (User)", lookups[0].targetObj, constants.TableUser)
	}
}

func TestMapRecordFieldsToUI_NonStringLookupValueSkipped(t *testing.T) {
	svc := &Service{}
	relName := "Account"
	rec := models.SObject{
		"AccountId": 12345, // Non-string value
	}
	fieldIndex := map[string]*models.FieldMetadata{
		"AccountId": {
			APIName:          "AccountId",
			Type:             constants.FieldTypeReference,
			ReferenceTo:      []string{"Account"},
			RelationshipName: &relName,
		},
	}
	uiRecord := &Record{Fields: make(map[string]FieldValue)}

	lookups := svc.mapRecordFieldsToUI(rec, fieldIndex, uiRecord)

	if len(lookups) != 0 {
		t.Errorf("expected 0 lookups for non-string lookup value, got %d", len(lookups))
	}
}

func TestMapRecordFieldsToUI_MultipleFieldsMixed(t *testing.T) {
	svc := &Service{}
	relName := "Account"
	rec := models.SObject{
		"Name":      "Test Record",
		"Amount":    float64(100.50),
		"CloseDate": "2024-06-15T00:00:00Z",
		"AccountId": "001-abc",
	}
	fieldIndex := map[string]*models.FieldMetadata{
		"Name":      {APIName: "Name", Type: "string"},
		"Amount":    {APIName: "Amount", Type: "currency"},
		"CloseDate": {APIName: "CloseDate", Type: "date"},
		"AccountId": {
			APIName:          "AccountId",
			Type:             constants.FieldTypeReference,
			ReferenceTo:      []string{"Account"},
			RelationshipName: &relName,
		},
	}
	uiRecord := &Record{Fields: make(map[string]FieldValue)}

	lookups := svc.mapRecordFieldsToUI(rec, fieldIndex, uiRecord)

	// Verify all fields present
	if len(uiRecord.Fields) != 4 {
		t.Errorf("expected 4 fields, got %d", len(uiRecord.Fields))
	}

	// Currency formatted
	if amt := uiRecord.Fields["Amount"]; amt.DisplayValue == nil || *amt.DisplayValue != "$100.50" {
		t.Errorf("Amount DisplayValue wrong: %v", amt.DisplayValue)
	}

	// Date formatted
	if cd := uiRecord.Fields["CloseDate"]; cd.DisplayValue == nil || *cd.DisplayValue != "2024-06-15" {
		t.Errorf("CloseDate DisplayValue wrong: %v", cd.DisplayValue)
	}

	// String no formatting
	if nm := uiRecord.Fields["Name"]; nm.DisplayValue != nil {
		t.Errorf("Name should have nil DisplayValue, got %q", *nm.DisplayValue)
	}

	// One lookup collected
	if len(lookups) != 1 {
		t.Errorf("expected 1 lookup, got %d", len(lookups))
	}
}

// =============================================================================
// resolveRecordLookups — relationship resolution
// Requires recordSvc.Get, so we test with nil recordSvc for empty lookups path,
// and verify the output structure for known scenarios.
// =============================================================================

func TestResolveRecordLookups_EmptyLookups(t *testing.T) {
	svc := &Service{} // recordSvc is nil, but empty lookups won't hit it
	uiRecord := &Record{Fields: make(map[string]FieldValue)}

	err := svc.resolveRecordLookups(context.Background(), nil, uiRecord, nil)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestResolveRecordLookups_EmptySliceLookups(t *testing.T) {
	svc := &Service{}
	uiRecord := &Record{Fields: make(map[string]FieldValue)}

	err := svc.resolveRecordLookups(context.Background(), []lookupTarget{}, uiRecord, nil)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestProcessRecordForUI_UserLookupUsesCompoundNameWhenNameMissing(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{
			APIName: "Campaign",
			Fields: []models.FieldMetadata{
				{APIName: "Id", Type: "id"},
				{APIName: "OwnerId", Type: constants.FieldTypeReference, ReferenceTo: []string{constants.TableUser}},
			},
		},
		{
			APIName: constants.TableUser,
			Fields: []models.FieldMetadata{
				{APIName: "Id", Type: "id"},
				{APIName: "Name", Type: "string", IsNameField: true},
				{APIName: "FirstName", Type: "string"},
				{APIName: "LastName", Type: "string"},
				{APIName: "Username", Type: "string"},
			},
		},
	}

	recordData := map[string]map[string]models.SObject{
		"Campaign": {
			"camp-1": {
				"Id":      "camp-1",
				"OwnerId": "user-1",
			},
		},
		constants.TableUser: {
			"user-1": {
				"Id":        "user-1",
				"FirstName": "Selva",
				"LastName":  "Ganesh Gopal",
				"Username":  "selva.ganesh@pingcap.com",
			},
		},
	}

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

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

	owner, ok := uiRecord.Fields["Owner"]
	if !ok {
		t.Fatal("expected Owner relationship field to be present")
	}
	if owner.DisplayValue == nil || *owner.DisplayValue != "Selva Ganesh Gopal" {
		t.Fatalf("Owner displayValue = %v, want Selva Ganesh Gopal", owner.DisplayValue)
	}

	nested, ok := owner.Value.(Record)
	if !ok {
		t.Fatalf("Owner value type = %T, want uiapi.Record", owner.Value)
	}
	if got := nested.Fields["Name"].Value; got != "Selva Ganesh Gopal" {
		t.Fatalf("nested Owner Name = %v, want Selva Ganesh Gopal", got)
	}
}

func TestProcessRecordForUI_UserLookupFallsBackToUsername(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{
			APIName: "Campaign",
			Fields: []models.FieldMetadata{
				{APIName: "Id", Type: "id"},
				{APIName: "OwnerId", Type: constants.FieldTypeReference, ReferenceTo: []string{constants.TableUser}},
			},
		},
		{
			APIName: constants.TableUser,
			Fields: []models.FieldMetadata{
				{APIName: "Id", Type: "id"},
				{APIName: "Name", Type: "string", IsNameField: true},
				{APIName: "FirstName", Type: "string"},
				{APIName: "LastName", Type: "string"},
				{APIName: "Username", Type: "string"},
			},
		},
	}

	recordData := map[string]map[string]models.SObject{
		"Campaign": {
			"camp-2": {
				"Id":      "camp-2",
				"OwnerId": "user-2",
			},
		},
		constants.TableUser: {
			"user-2": {
				"Id":       "user-2",
				"Username": "service.account@example.com",
			},
		},
	}

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

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

	owner, ok := uiRecord.Fields["Owner"]
	if !ok {
		t.Fatal("expected Owner relationship field to be present")
	}
	if owner.DisplayValue == nil || *owner.DisplayValue != "service.account@example.com" {
		t.Fatalf("Owner displayValue = %v, want service.account@example.com", owner.DisplayValue)
	}

	nested, ok := owner.Value.(Record)
	if !ok {
		t.Fatalf("Owner value type = %T, want uiapi.Record", owner.Value)
	}
	if got := nested.Fields["Name"].Value; got != "service.account@example.com" {
		t.Fatalf("nested Owner Name = %v, want service.account@example.com", got)
	}
}

func TestProcessRecordForUI_CustomLookupWithoutRelationshipMetadataStillResolves(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{
			APIName: "QuoteLineItem",
			Fields: []models.FieldMetadata{
				{APIName: "Id", Type: "id"},
				{APIName: "Quote__c", Type: constants.FieldTypeReference, ReferenceTo: []string{"Quote"}},
			},
		},
		{
			APIName: "Quote",
			Fields: []models.FieldMetadata{
				{APIName: "Id", Type: "id"},
				{APIName: "Name", Type: "string", IsNameField: true},
			},
		},
	}

	recordData := map[string]map[string]models.SObject{
		"QuoteLineItem": {
			"qli-1": {
				"Id":       "qli-1",
				"Quote__c": "quote-1",
			},
		},
		"Quote": {
			"quote-1": {
				"Id":   "quote-1",
				"Name": "Q-000123",
			},
		},
	}

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

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

	quote, ok := uiRecord.Fields["Quote__r"]
	if !ok {
		t.Fatal("expected Quote__r relationship field to be present")
	}
	if quote.DisplayValue == nil || *quote.DisplayValue != "Q-000123" {
		t.Fatalf("Quote__r displayValue = %v, want Q-000123", quote.DisplayValue)
	}

	nested, ok := quote.Value.(Record)
	if !ok {
		t.Fatalf("Quote__r value type = %T, want uiapi.Record", quote.Value)
	}
	if got := nested.Fields["Name"].Value; got != "Q-000123" {
		t.Fatalf("nested Quote__r Name = %v, want Q-000123", got)
	}
}

func TestProcessRecordForUI_RecordTypeLookupResolvesFromGenericMetadata(t *testing.T) {
	schemas := []*models.ObjectMetadata{
		{
			APIName: "Quote",
			Fields: []models.FieldMetadata{
				{APIName: "Id", Type: "id"},
				{APIName: "RecordTypeId", Type: constants.FieldTypeReference, ReferenceTo: []string{"RecordType"}},
			},
		},
		{
			APIName: "RecordType",
			Fields: []models.FieldMetadata{
				{APIName: "Id", Type: "id"},
				{APIName: "Name", Type: "string", IsNameField: true},
			},
		},
	}

	recordData := map[string]map[string]models.SObject{
		"Quote": {
			"quote-rt-1": {
				"Id":           "quote-rt-1",
				"RecordTypeId": "rt-quote-new",
			},
		},
		"RecordType": {
			"rt-quote-new": {
				"Id":   "rt-quote-new",
				"Name": "New",
			},
		},
	}

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

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

	recordType, ok := uiRecord.Fields["RecordType"]
	if !ok {
		t.Fatal("expected RecordType relationship field to be present")
	}
	if recordType.DisplayValue == nil || *recordType.DisplayValue != "New" {
		t.Fatalf("RecordType displayValue = %v, want New", recordType.DisplayValue)
	}

	nested, ok := recordType.Value.(Record)
	if !ok {
		t.Fatalf("RecordType value type = %T, want uiapi.Record", recordType.Value)
	}
	if got := nested.Fields["Name"].Value; got != "New" {
		t.Fatalf("nested RecordType Name = %v, want New", got)
	}
}

// =============================================================================
// GetRecordUI — top-level aggregator
// =============================================================================

func TestGetRecordUI_EmptyRecordIDs(t *testing.T) {
	svc := &Service{
		objCache:    newCache(time.Minute),
		layoutCache: newCache(time.Minute),
	}
	defer svc.Close()

	result, err := svc.GetRecordUI(context.Background(), []string{}, "Account", nil)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if result == nil {
		t.Fatal("expected non-nil result")
	}
	if len(result.Records) != 0 {
		t.Errorf("expected 0 records, got %d", len(result.Records))
	}
	if len(result.Layouts) != 0 {
		t.Errorf("expected 0 layouts, got %d", len(result.Layouts))
	}
	if len(result.ObjectInfos) != 0 {
		t.Errorf("expected 0 objectInfos, got %d", len(result.ObjectInfos))
	}
}

func TestGetRecordUI_EmptyTypeHint(t *testing.T) {
	svc := &Service{
		objCache:    newCache(time.Minute),
		layoutCache: newCache(time.Minute),
	}
	defer svc.Close()

	_, err := svc.GetRecordUI(context.Background(), []string{"record-1"}, "", nil)
	if err == nil {
		t.Fatal("expected validation error for empty typeHint")
	}
}

// =============================================================================
// lookupTarget struct
// =============================================================================

func TestLookupTarget_StructFields(t *testing.T) {
	lt := lookupTarget{
		fieldName: "AccountId",
		relName:   "Account",
		targetObj: "Account",
		targetID:  "001-abc",
	}
	if lt.fieldName != "AccountId" {
		t.Errorf("fieldName = %q", lt.fieldName)
	}
	if lt.relName != "Account" {
		t.Errorf("relName = %q", lt.relName)
	}
	if lt.targetObj != "Account" {
		t.Errorf("targetObj = %q", lt.targetObj)
	}
	if lt.targetID != "001-abc" {
		t.Errorf("targetID = %q", lt.targetID)
	}
}

// =============================================================================
// helpers
// =============================================================================

func strPtr(s string) *string {
	return &s
}
