package records

// persistence_retrieve_many_raw_test.go
//
// Regression coverage for RetrieveManyRaw (persistence_bulk_ops.go:755).
// These tests guard the RLS omit-filter: records the requesting user cannot
// access at the row level must be silently absent from the returned map, not
// returned as {hasReadAccess: false} entries. A removed "continue" or reverted
// call would make RLS-hidden records distinguishable from nonexistent records.

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"strings"
	"testing"

	"github.com/nexuscrm/kernel/pkg/apierr"
	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/kernel/pkg/schemautil"
	"github.com/nexuscrm/metadata"
	"github.com/nexuscrm/permissions"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/models"
)

// ---------------------------------------------------------------------------
// Stub RecordRepository — only FindMany/FindOne/FindOneByField carry
// test-specific behaviour; the embedded interface traps any other call with
// a nil panic, the loud signal that production reached for an unmodeled API.
// ---------------------------------------------------------------------------

type stubRecordRepoRMR struct {
	ports.RecordRepository

	findManyFn       func(ctx context.Context, tableName string, ids []string) (map[string]models.SObject, error)
	findOneFn        func(ctx context.Context, tableName, id string) (models.SObject, error)
	findOneByFieldFn func(ctx context.Context, tableName, fieldName string, fieldValue interface{}) (models.SObject, error)
}

func (r *stubRecordRepoRMR) FindMany(ctx context.Context, tableName string, ids []string) (map[string]models.SObject, error) {
	if r.findManyFn != nil {
		return r.findManyFn(ctx, tableName, ids)
	}
	return map[string]models.SObject{}, nil
}
func (r *stubRecordRepoRMR) FindOne(ctx context.Context, tableName, id string) (models.SObject, error) {
	if r.findOneFn != nil {
		return r.findOneFn(ctx, tableName, id)
	}
	return nil, nil
}
func (r *stubRecordRepoRMR) FindOneByField(ctx context.Context, tableName, fieldName string, fieldValue interface{}) (models.SObject, error) {
	if r.findOneByFieldFn != nil {
		return r.findOneByFieldFn(ctx, tableName, fieldName, fieldValue)
	}
	return nil, nil
}

// ---------------------------------------------------------------------------
// Stub PermissionRepository
// GetAllRoles — called during NewPermissionService init.
// LoadEffectiveObjectPermission — grants Read so the object-level gate passes.
// GetEffectiveSystemPermissions — returns empty (non-admin user has no perms).
// IsUserInGroup — returns false (user not in any group).
// GetManualShareAccessLevels / GetTeamMemberAccessLevel — deny manual/team share.
// The embedded interface traps any other call with a nil panic.
// ---------------------------------------------------------------------------

type stubPermRepoRMR struct {
	ports.PermissionRepository

	grantRead        bool            // when true, LoadEffectiveObjectPermission grants Read
	fieldRead        map[string]bool // optional lower-cased field API name -> explicit FLS Read
	manualLevels     []string
	manualPointCalls int
	manualBatchCalls int
	teamPointCalls   int
	teamBatchCalls   int
	pendingLockCalls int
}

func (r *stubPermRepoRMR) GetAllRoles(ctx context.Context) ([]*models.UserRole, error) {
	return []*models.UserRole{}, nil
}
func (r *stubPermRepoRMR) LoadEffectiveObjectPermission(ctx context.Context, user *models.UserSession, objectAPIName string) (*models.ObjectPermissions, error) {
	return &models.ObjectPermissions{PermissionsRead: r.grantRead}, nil
}
func (r *stubPermRepoRMR) GetEffectiveSystemPermissions(ctx context.Context, userID, profileID string) (map[string]bool, error) {
	return map[string]bool{}, nil
}
func (r *stubPermRepoRMR) GetEffectiveCustomPermissions(ctx context.Context, userID, profileID string) (map[string]bool, error) {
	return map[string]bool{}, nil
}
func (r *stubPermRepoRMR) IsUserInGroup(ctx context.Context, groupID, userID string) (bool, error) {
	return false, nil
}
func (r *stubPermRepoRMR) GetManualShareAccessLevels(ctx context.Context, objectAPIName, recordID, userID string) ([]string, error) {
	r.manualPointCalls++
	return append([]string(nil), r.manualLevels...), nil
}
func (r *stubPermRepoRMR) GetTeamMemberAccessLevel(ctx context.Context, objectAPIName, recordID, userID string) (*string, error) {
	r.teamPointCalls++
	return nil, nil
}

// GetManualShareAccessLevelsByRecordIDs and GetTeamMemberAccessLevelsByRecordIDs
// are not part of ports.PermissionRepository; PermissionService.PrimeRecordAccessInputCache
// reaches them through its manualShareLevelBatchLoader / teamMemberLevelBatchLoader
// type assertions, so they stay explicit here.

func (r *stubPermRepoRMR) GetManualShareAccessLevelsByRecordIDs(_ context.Context, _, _ string, recordIDs []string) (map[string][]string, error) {
	r.manualBatchCalls++
	result := make(map[string][]string, len(recordIDs))
	for _, recordID := range recordIDs {
		result[recordID] = append([]string(nil), r.manualLevels...)
	}
	return result, nil
}
func (r *stubPermRepoRMR) GetTeamMemberAccessLevelsByRecordIDs(_ context.Context, _, _ string, _ []string) (map[string]string, error) {
	r.teamBatchCalls++
	return map[string]string{}, nil
}
func (r *stubPermRepoRMR) LoadEffectiveFieldPermission(ctx context.Context, user *models.UserSession, objectAPIName, fieldAPIName string) (*models.FieldPermissions, error) {
	if readable, ok := r.fieldRead[strings.ToLower(strings.TrimSpace(fieldAPIName))]; ok {
		return &models.FieldPermissions{PermissionsRead: readable}, nil
	}
	return nil, nil
}
func (r *stubPermRepoRMR) ListEffectiveFieldPermissionsForUserObject(ctx context.Context, userID, profileID, objectAPIName string) ([]models.FieldPermissions, error) {
	return nil, nil
}
func (r *stubPermRepoRMR) GetPendingApprovalLockInfo(ctx context.Context, targetObjectID string) (string, string, error) {
	r.pendingLockCalls++
	return "", "", nil
}

// ---------------------------------------------------------------------------
// Stub MetadataRepository — only the cache-init reads carry behaviour; the
// embedded interface traps any other call with a nil panic.
// ---------------------------------------------------------------------------

type stubMetaRepoRMR struct {
	ports.MetadataRepository

	schema *models.ObjectMetadata
}

func (r *stubMetaRepoRMR) GetAllSchemas(ctx context.Context) ([]*models.ObjectMetadata, error) {
	if r.schema == nil {
		return nil, nil
	}
	return []*models.ObjectMetadata{r.schema}, nil
}
func (r *stubMetaRepoRMR) GetSchemaByAPIName(ctx context.Context, apiName string) (*models.ObjectMetadata, error) {
	if r.schema == nil || !strings.EqualFold(r.schema.APIName, apiName) {
		return nil, nil
	}
	return r.schema, nil
}
func (r *stubMetaRepoRMR) GetAllRelationships(ctx context.Context) ([]*models.Relationship, error) {
	return []*models.Relationship{}, nil
}
func (r *stubMetaRepoRMR) GetAllFlowDefinitions(ctx context.Context) ([]*models.FlowDefinition, error) {
	return nil, nil
}
func (r *stubMetaRepoRMR) GetAllValidationRules(ctx context.Context) ([]*models.ValidationRule, error) {
	return nil, nil
}
func (r *stubMetaRepoRMR) GetSharingRules(ctx context.Context, objectAPIName string) ([]*models.SharingRule, error) {
	return nil, nil
}

// ---------------------------------------------------------------------------
// Helper: build a PersistenceService wired for RLS testing.
// ---------------------------------------------------------------------------

func newRLSTestPersistenceService(
	t *testing.T,
	findManyFn func(ctx context.Context, tableName string, ids []string) (map[string]models.SObject, error),
	testSchema *models.ObjectMetadata,
) *PersistenceService {
	t.Helper()

	permRepo := &stubPermRepoRMR{grantRead: true}
	permSvc := permissions.NewPermissionService(permRepo, nil, nil)

	metaRepo := &stubMetaRepoRMR{schema: testSchema}
	metaSvc := metadata.NewMetadataService(metaRepo, nil, nil)

	recRepo := &stubRecordRepoRMR{findManyFn: findManyFn}

	return &PersistenceService{
		repo:        recRepo,
		permissions: permSvc,
		metadata:    metaSvc,
	}
}

func newRLSTestPersistenceServiceWithFinders(
	t *testing.T,
	findManyFn func(ctx context.Context, tableName string, ids []string) (map[string]models.SObject, error),
	findOneFn func(ctx context.Context, tableName string, id string) (models.SObject, error),
	findOneByFieldFn func(ctx context.Context, tableName, fieldName string, fieldValue interface{}) (models.SObject, error),
	testSchema *models.ObjectMetadata,
) *PersistenceService {
	t.Helper()

	metaRepo := &stubMetaRepoRMR{schema: testSchema}
	metaSvc := metadata.NewMetadataService(metaRepo, nil, nil)

	permRepo := &stubPermRepoRMR{grantRead: true}
	permSvc := permissions.NewPermissionService(permRepo, metaSvc, nil)

	recRepo := &stubRecordRepoRMR{
		findManyFn:       findManyFn,
		findOneFn:        findOneFn,
		findOneByFieldFn: findOneByFieldFn,
	}

	return &PersistenceService{
		repo:        recRepo,
		permissions: permSvc,
		metadata:    metaSvc,
	}
}

func newReadProtectionSchema(apiName string) *models.ObjectMetadata {
	schema := &models.ObjectMetadata{
		APIName:      apiName,
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, Createable: false, Updateable: false},
			{
				APIName:              "SensitiveField",
				Type:                 constants.FieldTypeString,
				Createable:           true,
				Updateable:           true,
				ReadProtectionPolicy: constants.FieldReadProtectionBlocked,
			},
		},
	}
	schema.FieldsMap = map[string]*models.FieldMetadata{
		strings.ToLower(constants.FieldID):        &schema.Fields[0],
		strings.ToLower(schema.Fields[1].APIName): &schema.Fields[1],
	}
	return schema
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

// TestRetrieveManyRaw_RLSBlockedRecordsOmitted verifies that records whose
// OwnerId does not match the requesting user are silently absent from the
// returned map, not present as {hasReadAccess: false} entries.
// This guards the "continue" at persistence_bulk_ops.go:794.
func TestRetrieveManyRaw_RLSBlockedRecordsOmitted(t *testing.T) {
	const (
		objectName    = "TestObject"
		requestingUID = "user-abc"
		otherUID      = "user-xyz"
		ownedRecID    = "rec-owned"
		blockedRecID  = "rec-blocked"
	)

	testSchema := &models.ObjectMetadata{
		APIName:      objectName,
		SharingModel: constants.SharingModelPrivate,
	}

	rawRecords := map[string]models.SObject{
		ownedRecID: {
			constants.FieldID:      ownedRecID,
			constants.FieldOwnerID: requestingUID, // user owns this → RLS passes
		},
		blockedRecID: {
			constants.FieldID:      blockedRecID,
			constants.FieldOwnerID: otherUID, // different owner → RLS denied
		},
	}

	ps := newRLSTestPersistenceService(t,
		func(_ context.Context, _ string, _ []string) (map[string]models.SObject, error) {
			return rawRecords, nil
		},
		testSchema,
	)

	user := &models.UserSession{
		ID:        requestingUID,
		ProfileID: constants.ProfileStandardUser,
	}

	result, err := ps.RetrieveManyRaw(context.Background(), objectName, []string{ownedRecID, blockedRecID}, user)
	if err != nil {
		t.Fatalf("RetrieveManyRaw returned unexpected error: %v", err)
	}

	// The blocked record must be absent — not just {hasReadAccess:false}.
	if _, present := result[blockedRecID]; present {
		t.Errorf("RLS-blocked record %q is present in result; it must be silently omitted", blockedRecID)
	}
	// The owned record must be present.
	if _, present := result[ownedRecID]; !present {
		t.Errorf("owned record %q is absent from result; it must be included", ownedRecID)
	}
}

func TestBatchRecordReadAccessDoesNotExpandIntoApprovalOrMutationAuthority(t *testing.T) {
	const (
		objectName = "Contact"
		recordID   = "003-readable"
		userID     = "005-reader"
	)
	schema := &models.ObjectMetadata{
		APIName:      objectName,
		SharingModel: constants.SharingModelPrivate,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString, IsSystem: true},
			{APIName: constants.FieldOwnerID, Type: constants.FieldTypeReference, IsSystem: true},
		},
	}
	permRepo := &stubPermRepoRMR{grantRead: true}
	metaSvc := metadata.NewMetadataService(&stubMetaRepoRMR{schema: schema}, nil, nil)
	persistence := &PersistenceService{
		repo: &stubRecordRepoRMR{findManyFn: func(_ context.Context, _ string, _ []string) (map[string]models.SObject, error) {
			return map[string]models.SObject{recordID: {
				constants.FieldID: recordID, constants.FieldOwnerID: userID,
			}}, nil
		}},
		permissions: permissions.NewPermissionService(permRepo, metaSvc, nil),
		metadata:    metaSvc,
	}
	got, err := NewContractService(persistence).BatchRecordReadAccess(
		permissions.WithAccessInputCache(context.Background()), objectName, []string{recordID},
		&models.UserSession{ID: userID, ProfileID: "profile-reader"},
	)
	if err != nil || got[recordID] != (contract.RecordAccess{Read: true}) {
		t.Fatalf("BatchRecordReadAccess = %#v, %v", got, err)
	}
	if permRepo.pendingLockCalls != 0 {
		t.Fatalf("approval lock probes = %d, want zero for read-only authority", permRepo.pendingLockCalls)
	}
}

func TestRetrieve_MultipleReadBypassPathsFilterReadProtectedFields(t *testing.T) {
	const (
		objectName = "Issue"
		recordID   = "issue-001"
	)
	schema := newReadProtectionSchema(objectName)
	record := models.SObject{
		constants.FieldID:          recordID,
		"Name":                     "public",
		"Probability":              float64(42.5),
		"SensitiveField":           "sensitive",
		constants.FieldOwnerID:     "owner",
		constants.FieldCreatedDate: "2026-01-01T00:00:00.000Z",
	}
	record[models.ObjectTypeMetadataKey] = objectName
	schemautil.SetExactDecimalForField(record, "Probability", schemautil.ExactDecimal("42.500000000000000001"))
	schemautil.SetExactDecimalForField(record, "SensitiveField", schemautil.ExactDecimal("9.5000"))

	tests := []struct {
		name     string
		retrieve func(*PersistenceService, context.Context, *models.UserSession) (models.SObject, error)
	}{
		{
			name: "Retrieve",
			retrieve: func(ps *PersistenceService, ctx context.Context, user *models.UserSession) (models.SObject, error) {
				return ps.Retrieve(ctx, objectName, recordID, user)
			},
		},
		{
			name: "RetrieveWithAccess",
			retrieve: func(ps *PersistenceService, ctx context.Context, user *models.UserSession) (models.SObject, error) {
				rec, _, err := ps.RetrieveWithAccess(ctx, objectName, recordID, user)
				return rec, err
			},
		},
		{
			name: "RetrieveByField",
			retrieve: func(ps *PersistenceService, ctx context.Context, user *models.UserSession) (models.SObject, error) {
				return ps.RetrieveByField(ctx, objectName, constants.FieldID, recordID, user)
			},
		},
	}

	ctx := constants.WithSecurityContext(context.Background(), constants.SystemContext, "sensitive-read contract")
	user := recordEntrypointUser()

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			ps := newRLSTestPersistenceServiceWithFinders(
				t,
				nil,
				func(_ context.Context, _ string, _ string) (models.SObject, error) {
					return record, nil
				},
				func(_ context.Context, _ string, _ string, _ interface{}) (models.SObject, error) {
					return record, nil
				},
				schema,
			)

			got, err := tc.retrieve(ps, ctx, user)
			if err != nil {
				t.Fatalf("read path %q returned unexpected error: %v", tc.name, err)
			}
			if _, ok := got["SensitiveField"]; ok {
				t.Errorf("read path %q leaked blocked field SensitiveField under bypassed FLS", tc.name)
			}
			if _, ok := got[models.ObjectTypeMetadataKey]; ok {
				t.Errorf("read path %q leaked object type metadata under bypassed FLS", tc.name)
			}
			if decimal, ok := schemautil.ExactDecimalForField(got, "Probability"); !ok || decimal.String() != "42.500000000000000001" {
				t.Errorf("read path %q exact decimal = %#v, %v; want authorized carrier", tc.name, decimal, ok)
			}
			if _, ok := schemautil.ExactDecimalForField(got, "SensitiveField"); ok {
				t.Errorf("read path %q retained exact decimal for blocked field", tc.name)
			}
			if got["Name"] != "public" {
				t.Fatalf("read path %q expected Name to be preserved, got %#v", tc.name, got["Name"])
			}
		})
	}
}

func TestRetrieveWithAccessUsesOneCompleteRecordAccessDecision(t *testing.T) {
	schema := &models.ObjectMetadata{
		APIName:      "Account",
		SharingModel: constants.SharingModelPrivate,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString},
			{APIName: constants.FieldOwnerID, Type: constants.FieldTypeReference},
			{APIName: "Name", Type: constants.FieldTypeString},
		},
	}
	schema.FieldsMap = map[string]*models.FieldMetadata{}
	for i := range schema.Fields {
		schema.FieldsMap[strings.ToLower(schema.Fields[i].APIName)] = &schema.Fields[i]
	}

	permRepo := &stubPermRepoRMR{grantRead: true, manualLevels: []string{constants.PermRead}}
	metaSvc := metadata.NewMetadataService(&stubMetaRepoRMR{schema: schema}, nil, nil)
	service := &PersistenceService{
		repo: &stubRecordRepoRMR{findOneFn: func(context.Context, string, string) (models.SObject, error) {
			return models.SObject{
				constants.FieldID:      "account-1",
				constants.FieldOwnerID: "someone-else",
				"Name":                 "Visible through a manual read share",
			}, nil
		}},
		permissions: permissions.NewPermissionService(permRepo, metaSvc, nil),
		metadata:    metaSvc,
	}
	user := &models.UserSession{ID: "reader", ProfileID: constants.ProfileStandardUser}

	row, access, err := service.RetrieveWithAccess(permissions.WithAccessInputCache(context.Background()), "Account", "account-1", user)
	if err != nil {
		t.Fatalf("RetrieveWithAccess: %v", err)
	}
	if row == nil || access == nil || !access.HasReadAccess || access.HasEditAccess || access.HasDeleteAccess {
		t.Fatalf("row/access = %#v, %#v; want read-only shared row", row, access)
	}
	if permRepo.manualBatchCalls != 1 || permRepo.teamBatchCalls != 1 {
		t.Fatalf("batch loads manual=%d team=%d, want 1/1", permRepo.manualBatchCalls, permRepo.teamBatchCalls)
	}
	if permRepo.manualPointCalls != 0 || permRepo.teamPointCalls != 0 {
		t.Fatalf("point loads manual=%d team=%d, want 0/0", permRepo.manualPointCalls, permRepo.teamPointCalls)
	}

	// The collapsed decision must preserve existence concealment: removing the
	// only Read grant returns the same 404 as the former standalone Read gate.
	permRepo.manualLevels = nil
	_, _, err = service.RetrieveWithAccess(permissions.WithAccessInputCache(context.Background()), "Account", "account-1", user)
	var status interface{ HTTPStatus() int }
	if !errors.As(err, &status) || status.HTTPStatus() != http.StatusNotFound {
		t.Fatalf("denied RetrieveWithAccess error = %v, want concealed 404", err)
	}
}

func TestRetrieve_PointReadPathsStripInternalRecordMetadata(t *testing.T) {
	const (
		objectName = "Issue"
		recordID   = "issue-001"
	)
	schema := newReadProtectionSchema(objectName)
	record := models.SObject{
		constants.FieldID:      recordID,
		"Name":                 "public",
		constants.FieldOwnerID: "owner",
	}
	record[models.ObjectTypeMetadataKey] = objectName
	schemautil.SetExactDecimalForField(record, "Probability", schemautil.ExactDecimal("42.500000000000000001"))

	tests := []struct {
		name     string
		retrieve func(*PersistenceService, context.Context, *models.UserSession) (models.SObject, error)
	}{
		{
			name: "Retrieve",
			retrieve: func(ps *PersistenceService, ctx context.Context, user *models.UserSession) (models.SObject, error) {
				return ps.Retrieve(ctx, objectName, recordID, user)
			},
		},
		{
			name: "RetrieveByField",
			retrieve: func(ps *PersistenceService, ctx context.Context, user *models.UserSession) (models.SObject, error) {
				return ps.RetrieveByField(ctx, objectName, constants.FieldID, recordID, user)
			},
		},
	}

	user := recordEntrypointUser()
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			ps := newRLSTestPersistenceServiceWithFinders(
				t,
				nil,
				func(_ context.Context, _ string, _ string) (models.SObject, error) {
					return record, nil
				},
				func(_ context.Context, _ string, _ string, _ interface{}) (models.SObject, error) {
					return record, nil
				},
				schema,
			)

			got, err := tc.retrieve(ps, context.Background(), user)
			if err != nil {
				t.Fatalf("read path %q returned unexpected error: %v", tc.name, err)
			}
			if _, ok := got[models.ObjectTypeMetadataKey]; ok {
				t.Errorf("read path %q leaked object type metadata: %#v", tc.name, got)
			}
			if _, ok := got[models.ExactDecimalMetadataKey]; ok {
				t.Errorf("read path %q leaked exact decimal metadata: %#v", tc.name, got)
			}
			if got["Name"] != "public" {
				t.Fatalf("read path %q expected Name to be preserved, got %#v", tc.name, got["Name"])
			}
		})
	}
}

func TestRetrieve_ExactDecimalCarrierFollowsFLSProjection(t *testing.T) {
	const (
		objectName = "Issue"
		recordID   = "issue-decimal"
	)
	schema := &models.ObjectMetadata{
		APIName:      objectName,
		SharingModel: constants.SharingModelPublicReadWrite,
		Fields: []models.FieldMetadata{
			{APIName: constants.FieldID, Type: constants.FieldTypeString},
			{APIName: "VisibleEstimate", Type: constants.FieldTypeDouble},
			{APIName: "HiddenEstimate", Type: constants.FieldTypeDouble},
		},
	}
	record := models.SObject{
		constants.FieldID: recordID,
		"VisibleEstimate": float64(12.5),
		"HiddenEstimate":  float64(99.5),
	}
	schemautil.SetExactDecimalForField(record, "VisibleEstimate", schemautil.ExactDecimal("12.5000"))
	schemautil.SetExactDecimalForField(record, "HiddenEstimate", schemautil.ExactDecimal("99.5000"))
	metaSvc := metadata.NewMetadataService(&stubMetaRepoRMR{schema: schema}, nil, nil)
	permSvc := permissions.NewPermissionService(&stubPermRepoRMR{
		grantRead: true,
		fieldRead: map[string]bool{
			strings.ToLower(constants.FieldID): true,
			"visibleestimate":                  true,
			"hiddenestimate":                   false,
		},
	}, metaSvc, nil)
	ps := &PersistenceService{
		repo: &stubRecordRepoRMR{findOneFn: func(context.Context, string, string) (models.SObject, error) {
			return record, nil
		}},
		permissions: permSvc,
		metadata:    metaSvc,
	}

	got, err := ps.Retrieve(context.Background(), objectName, recordID, recordEntrypointUser())
	if err != nil {
		t.Fatalf("Retrieve: %v", err)
	}
	if _, ok := got["HiddenEstimate"]; ok {
		t.Fatal("FLS-hidden decimal public field was returned")
	}
	if decimal, ok := schemautil.ExactDecimalForField(got, "VisibleEstimate"); !ok || decimal.String() != "12.5000" {
		t.Fatalf("visible exact decimal = %#v, %v", decimal, ok)
	}
	if _, ok := schemautil.ExactDecimalForField(got, "HiddenEstimate"); ok {
		t.Fatal("FLS-hidden decimal carrier was returned")
	}
}

func TestRetrieveMany_FiltersReadProtectedFieldInBypassPath(t *testing.T) {
	const (
		objectName = "Issue"
		recordA    = "issue-a"
		recordB    = "issue-b"
	)
	schema := newReadProtectionSchema(objectName)
	recordMap := map[string]models.SObject{
		recordA: {
			constants.FieldID: recordA,
			"Name":            "public-a",
			"SensitiveField":  "secret-a",
		},
		recordB: {
			constants.FieldID: recordB,
			"Name":            "public-b",
			"SensitiveField":  "secret-b",
		},
	}

	ctx := constants.WithSecurityContext(context.Background(), constants.SystemContext, "sensitive-read contract many")
	user := recordEntrypointUser()
	ps := newRLSTestPersistenceServiceWithFinders(
		t,
		func(_ context.Context, _ string, _ []string) (map[string]models.SObject, error) {
			return recordMap, nil
		},
		nil,
		nil,
		schema,
	)

	result, err := ps.RetrieveMany(ctx, objectName, []string{recordA, recordB}, user)
	if err != nil {
		t.Fatalf("RetrieveMany returned unexpected error: %v", err)
	}
	for id, rec := range result {
		if _, ok := rec["SensitiveField"]; ok {
			t.Fatalf("RetrieveMany leaked blocked field for %q under bypassed FLS", id)
		}
		if rec["Name"] == "" {
			t.Fatalf("RetrieveMany should keep visible fields for %q", id)
		}
	}
}

// TestRetrieveManyRaw_AllOwnedRecordsReturned verifies the happy path: records
// owned by the requesting user are all returned unmodified.
func TestRetrieveManyRaw_AllOwnedRecordsReturned(t *testing.T) {
	const (
		objectName = "TestObject"
		userID     = "user-abc"
		recA       = "rec-a"
		recB       = "rec-b"
	)

	testSchema := &models.ObjectMetadata{
		APIName:      objectName,
		SharingModel: constants.SharingModelPrivate,
	}

	rawRecords := map[string]models.SObject{
		recA: {constants.FieldID: recA, constants.FieldOwnerID: userID, "Name": "Alpha"},
		recB: {constants.FieldID: recB, constants.FieldOwnerID: userID, "Name": "Beta"},
	}

	ps := newRLSTestPersistenceService(t,
		func(_ context.Context, _ string, _ []string) (map[string]models.SObject, error) {
			return rawRecords, nil
		},
		testSchema,
	)

	user := &models.UserSession{ID: userID, ProfileID: constants.ProfileStandardUser}

	result, err := ps.RetrieveManyRaw(context.Background(), objectName, []string{recA, recB}, user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	for _, id := range []string{recA, recB} {
		if _, ok := result[id]; !ok {
			t.Errorf("owned record %q missing from result", id)
		}
	}
}

// TestRetrieveManyRaw_EmptyIDsReturnsEmpty verifies that an empty (or nil)
// ID slice returns an empty map without hitting the repository.
func TestRetrieveManyRaw_EmptyIDsReturnsEmpty(t *testing.T) {
	testSchema := &models.ObjectMetadata{APIName: "TestObject", SharingModel: constants.SharingModelPrivate}
	called := false
	ps := newRLSTestPersistenceService(t,
		func(_ context.Context, _ string, _ []string) (map[string]models.SObject, error) {
			called = true
			return nil, fmt.Errorf("should not be called")
		},
		testSchema,
	)

	user := &models.UserSession{ID: "u1", ProfileID: constants.ProfileStandardUser}

	result, err := ps.RetrieveManyRaw(context.Background(), "TestObject", []string{}, user)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(result) != 0 {
		t.Errorf("expected empty map, got %d entries", len(result))
	}
	if called {
		t.Error("repository FindMany was called for empty ID slice")
	}
}

// TestRetrieveManyRaw_NilUserReturnsForbidden verifies that a nil user produces
// an apierr.Forbidden error before any repository call is made.
func TestRetrieveManyRaw_NilUserReturnsForbidden(t *testing.T) {
	testSchema := &models.ObjectMetadata{APIName: "TestObject", SharingModel: constants.SharingModelPrivate}
	ps := newRLSTestPersistenceService(t, nil, testSchema)

	_, err := ps.RetrieveManyRaw(context.Background(), "TestObject", []string{"rec-1"}, nil)
	if err == nil {
		t.Fatal("expected error for nil user, got nil")
	}
	apiErr, ok := err.(*apierr.Error)
	if !ok || apiErr.Status != 403 {
		t.Errorf("expected HTTP 403 Forbidden, got: %v (type %T)", err, err)
	}
}

// Compile-time interface assertions.
var _ ports.RecordRepository = (*stubRecordRepoRMR)(nil)
var _ ports.PermissionRepository = (*stubPermRepoRMR)(nil)
var _ ports.MetadataRepository = (*stubMetaRepoRMR)(nil)
