package mru

import (
	"context"
	"errors"
	"testing"

	"github.com/nexuscrm/backend/internal/domain/ports"
	"github.com/nexuscrm/shared/pkg/models"
)

// mockMRURepository is a test double implementing ports.MRURepository.
// Each method field can be set per test to control return values.
type mockMRURepository struct {
	trackObjectInteractionFn func(ctx context.Context, userID, objectAPIName string) error
	getScopeOrderFn          func(ctx context.Context, userID string) ([]ports.MRUObjectInteraction, error)
	setObjectPinnedFn        func(ctx context.Context, userID, objectAPIName string, pinned bool) error
	trackRecentItemFn        func(ctx context.Context, userID, objectAPIName, recordID string) error
	getRelevantItemsFn       func(ctx context.Context, userID string, sobjects []string, lastUpdatedId int64) ([]ports.MRURecentItem, int64, error)
	getObjectMetaFn          func(ctx context.Context, objectAPIName string) (*models.ObjectMetadata, error)
	getObjectMetaBatchFn     func(ctx context.Context, objectAPINames []string) (map[string]*models.ObjectMetadata, error)
	getRecordNamesBatchFn    func(ctx context.Context, byObject map[string][]string) (map[string]string, error)
}

func (m *mockMRURepository) TrackObjectInteraction(ctx context.Context, userID, objectAPIName string) error {
	if m.trackObjectInteractionFn != nil {
		return m.trackObjectInteractionFn(ctx, userID, objectAPIName)
	}
	return nil
}

func (m *mockMRURepository) GetScopeOrder(ctx context.Context, userID string) ([]ports.MRUObjectInteraction, error) {
	if m.getScopeOrderFn != nil {
		return m.getScopeOrderFn(ctx, userID)
	}
	return nil, nil
}

func (m *mockMRURepository) SetObjectPinned(ctx context.Context, userID, objectAPIName string, pinned bool) error {
	if m.setObjectPinnedFn != nil {
		return m.setObjectPinnedFn(ctx, userID, objectAPIName, pinned)
	}
	return nil
}

func (m *mockMRURepository) TrackRecentItem(ctx context.Context, userID, objectAPIName, recordID string) error {
	if m.trackRecentItemFn != nil {
		return m.trackRecentItemFn(ctx, userID, objectAPIName, recordID)
	}
	return nil
}

func (m *mockMRURepository) GetRelevantItems(ctx context.Context, userID string, sobjects []string, lastUpdatedId int64) ([]ports.MRURecentItem, int64, error) {
	if m.getRelevantItemsFn != nil {
		return m.getRelevantItemsFn(ctx, userID, sobjects, lastUpdatedId)
	}
	return nil, 0, nil
}

func (m *mockMRURepository) GetObjectMeta(ctx context.Context, objectAPIName string) (*models.ObjectMetadata, error) {
	if m.getObjectMetaFn != nil {
		return m.getObjectMetaFn(ctx, objectAPIName)
	}
	return nil, nil
}

func (m *mockMRURepository) GetObjectMetaBatch(ctx context.Context, objectAPINames []string) (map[string]*models.ObjectMetadata, error) {
	if m.getObjectMetaBatchFn != nil {
		return m.getObjectMetaBatchFn(ctx, objectAPINames)
	}
	return make(map[string]*models.ObjectMetadata), nil
}

func (m *mockMRURepository) GetRecordNamesBatch(ctx context.Context, byObject map[string][]string) (map[string]string, error) {
	if m.getRecordNamesBatchFn != nil {
		return m.getRecordNamesBatchFn(ctx, byObject)
	}
	return make(map[string]string), nil
}

// ---------------------------------------------------------------------------
// NewService
// ---------------------------------------------------------------------------

func TestNewService(t *testing.T) {
	repo := &mockMRURepository{}
	svc := NewService(repo)
	if svc == nil {
		t.Fatal("NewService returned nil")
	}
	if svc.repo != repo {
		t.Error("NewService did not store the provided repository")
	}
}

// ---------------------------------------------------------------------------
// TrackObjectInteraction
// ---------------------------------------------------------------------------

func TestTrackObjectInteraction_EmptyParams(t *testing.T) {
	called := false
	repo := &mockMRURepository{
		trackObjectInteractionFn: func(_ context.Context, _, _ string) error {
			called = true
			return nil
		},
	}
	svc := NewService(repo)
	ctx := context.Background()

	// Empty userID — should short-circuit
	svc.TrackObjectInteraction(ctx, "", "Account")
	if called {
		t.Error("TrackObjectInteraction should not call repo when userID is empty")
	}

	// Empty objectAPIName — should short-circuit
	called = false
	svc.TrackObjectInteraction(ctx, "user1", "")
	if called {
		t.Error("TrackObjectInteraction should not call repo when objectAPIName is empty")
	}

	// Both empty
	called = false
	svc.TrackObjectInteraction(ctx, "", "")
	if called {
		t.Error("TrackObjectInteraction should not call repo when both params are empty")
	}
}

func TestTrackObjectInteraction_Success(t *testing.T) {
	var gotUserID, gotObject string
	repo := &mockMRURepository{
		trackObjectInteractionFn: func(_ context.Context, userID, objectAPIName string) error {
			gotUserID = userID
			gotObject = objectAPIName
			return nil
		},
	}
	svc := NewService(repo)
	svc.TrackObjectInteraction(context.Background(), "user1", "Account")

	if gotUserID != "user1" || gotObject != "Account" {
		t.Errorf("expected (user1, Account), got (%s, %s)", gotUserID, gotObject)
	}
}

func TestTrackObjectInteraction_RepoError(t *testing.T) {
	repo := &mockMRURepository{
		trackObjectInteractionFn: func(_ context.Context, _, _ string) error {
			return errors.New("db error")
		},
	}
	svc := NewService(repo)
	// Should not panic; error is logged, not returned
	svc.TrackObjectInteraction(context.Background(), "user1", "Account")
}

// ---------------------------------------------------------------------------
// TrackRecordAccess
// ---------------------------------------------------------------------------

func TestTrackRecordAccess_EmptyParams(t *testing.T) {
	trackRecentCalled := false
	trackInteractionCalled := false
	repo := &mockMRURepository{
		trackRecentItemFn: func(_ context.Context, _, _, _ string) error {
			trackRecentCalled = true
			return nil
		},
		trackObjectInteractionFn: func(_ context.Context, _, _ string) error {
			trackInteractionCalled = true
			return nil
		},
	}
	svc := NewService(repo)
	ctx := context.Background()

	cases := []struct {
		name     string
		userID   string
		objName  string
		recordID string
	}{
		{"empty userID", "", "Account", "001ABC"},
		{"empty objectAPIName", "user1", "", "001ABC"},
		{"empty recordID", "user1", "Account", ""},
		{"all empty", "", "", ""},
	}

	for _, tc := range cases {
		trackRecentCalled = false
		trackInteractionCalled = false
		svc.TrackRecordAccess(ctx, tc.userID, tc.objName, tc.recordID)
		if trackRecentCalled {
			t.Errorf("%s: should not call TrackRecentItem", tc.name)
		}
		if trackInteractionCalled {
			t.Errorf("%s: should not call TrackObjectInteraction", tc.name)
		}
	}
}

func TestTrackRecordAccess_Success(t *testing.T) {
	var recentCalled, interactionCalled bool
	repo := &mockMRURepository{
		trackRecentItemFn: func(_ context.Context, _, _, _ string) error {
			recentCalled = true
			return nil
		},
		trackObjectInteractionFn: func(_ context.Context, _, _ string) error {
			interactionCalled = true
			return nil
		},
	}
	svc := NewService(repo)
	svc.TrackRecordAccess(context.Background(), "user1", "Account", "001ABC")

	if !recentCalled {
		t.Error("TrackRecordAccess should call TrackRecentItem")
	}
	if !interactionCalled {
		t.Error("TrackRecordAccess should also call TrackObjectInteraction")
	}
}

func TestTrackRecordAccess_TrackRecentItemError(t *testing.T) {
	interactionCalled := false
	repo := &mockMRURepository{
		trackRecentItemFn: func(_ context.Context, _, _, _ string) error {
			return errors.New("db error")
		},
		trackObjectInteractionFn: func(_ context.Context, _, _ string) error {
			interactionCalled = true
			return nil
		},
	}
	svc := NewService(repo)
	// Even when TrackRecentItem fails, TrackObjectInteraction should still be called
	svc.TrackRecordAccess(context.Background(), "user1", "Account", "001ABC")
	if !interactionCalled {
		t.Error("TrackObjectInteraction should still be called even when TrackRecentItem errors")
	}
}

// ---------------------------------------------------------------------------
// GetScopeOrder
// ---------------------------------------------------------------------------

func TestGetScopeOrder_DefaultApiVersion(t *testing.T) {
	repo := &mockMRURepository{
		getScopeOrderFn: func(_ context.Context, _ string) ([]ports.MRUObjectInteraction, error) {
			return nil, nil // empty → defaults
		},
		getObjectMetaBatchFn: func(_ context.Context, _ []string) (map[string]*models.ObjectMetadata, error) {
			return make(map[string]*models.ObjectMetadata), nil
		},
	}
	svc := NewService(repo)
	result := svc.GetScopeOrder(context.Background(), "user1", "")
	// Should not panic and should return defaults
	if len(result) == 0 {
		t.Error("GetScopeOrder should return default scope objects when apiVersion is empty")
	}
}

func TestGetScopeOrder_RepoError_FallsBackToDefaults(t *testing.T) {
	repo := &mockMRURepository{
		getScopeOrderFn: func(_ context.Context, _ string) ([]ports.MRUObjectInteraction, error) {
			return nil, errors.New("db error")
		},
		getObjectMetaBatchFn: func(_ context.Context, _ []string) (map[string]*models.ObjectMetadata, error) {
			return make(map[string]*models.ObjectMetadata), nil
		},
	}
	svc := NewService(repo)
	result := svc.GetScopeOrder(context.Background(), "user1", "v63.0")
	defaults := DefaultScopeObjects()
	if len(result) != len(defaults) {
		t.Errorf("expected %d default items, got %d", len(defaults), len(result))
	}
}

func TestGetScopeOrder_EmptyInteractions_FallsBackToDefaults(t *testing.T) {
	repo := &mockMRURepository{
		getScopeOrderFn: func(_ context.Context, _ string) ([]ports.MRUObjectInteraction, error) {
			return []ports.MRUObjectInteraction{}, nil
		},
		getObjectMetaBatchFn: func(_ context.Context, _ []string) (map[string]*models.ObjectMetadata, error) {
			return make(map[string]*models.ObjectMetadata), nil
		},
	}
	svc := NewService(repo)
	result := svc.GetScopeOrder(context.Background(), "user1", "v63.0")
	defaults := DefaultScopeObjects()
	if len(result) != len(defaults) {
		t.Errorf("expected %d default items, got %d", len(defaults), len(result))
	}
}

func TestGetScopeOrder_WithInteractions(t *testing.T) {
	repo := &mockMRURepository{
		getScopeOrderFn: func(_ context.Context, _ string) ([]ports.MRUObjectInteraction, error) {
			return []ports.MRUObjectInteraction{
				{ObjectAPIName: "Account"},
				{ObjectAPIName: "Contact"},
			}, nil
		},
		getObjectMetaBatchFn: func(_ context.Context, names []string) (map[string]*models.ObjectMetadata, error) {
			m := map[string]*models.ObjectMetadata{
				"Account": {Label: "Accounts"},
				"Contact": {Label: "Contacts"},
			}
			return m, nil
		},
	}
	svc := NewService(repo)
	result := svc.GetScopeOrder(context.Background(), "user1", "v63.0")

	if len(result) != 2 {
		t.Fatalf("expected 2 scope items, got %d", len(result))
	}
	if result[0].Name != "Account" || result[0].Label != "Accounts" {
		t.Errorf("first item: got Name=%q Label=%q, want Account/Accounts", result[0].Name, result[0].Label)
	}
	if result[1].Name != "Contact" || result[1].Label != "Contacts" {
		t.Errorf("second item: got Name=%q Label=%q, want Contact/Contacts", result[1].Name, result[1].Label)
	}
}

func TestGetScopeOrder_MetadataBatchError(t *testing.T) {
	repo := &mockMRURepository{
		getScopeOrderFn: func(_ context.Context, _ string) ([]ports.MRUObjectInteraction, error) {
			return []ports.MRUObjectInteraction{
				{ObjectAPIName: "Account"},
			}, nil
		},
		getObjectMetaBatchFn: func(_ context.Context, _ []string) (map[string]*models.ObjectMetadata, error) {
			return nil, errors.New("metadata error")
		},
	}
	svc := NewService(repo)
	result := svc.GetScopeOrder(context.Background(), "user1", "v63.0")

	// Should still return items; label defaults to API name
	if len(result) != 1 {
		t.Fatalf("expected 1 item, got %d", len(result))
	}
	if result[0].Label != "Account" {
		t.Errorf("label should fall back to API name, got %q", result[0].Label)
	}
}

func TestGetScopeOrder_MetadataPartiallyMissing(t *testing.T) {
	repo := &mockMRURepository{
		getScopeOrderFn: func(_ context.Context, _ string) ([]ports.MRUObjectInteraction, error) {
			return []ports.MRUObjectInteraction{
				{ObjectAPIName: "Account"},
				{ObjectAPIName: "UnknownObj"},
			}, nil
		},
		getObjectMetaBatchFn: func(_ context.Context, _ []string) (map[string]*models.ObjectMetadata, error) {
			return map[string]*models.ObjectMetadata{
				"Account": {Label: "Accounts"},
				// UnknownObj not present
			}, nil
		},
	}
	svc := NewService(repo)
	result := svc.GetScopeOrder(context.Background(), "user1", "v63.0")

	if len(result) != 2 {
		t.Fatalf("expected 2 items, got %d", len(result))
	}
	// First item has metadata
	if result[0].Label != "Accounts" {
		t.Errorf("expected label 'Accounts', got %q", result[0].Label)
	}
	// Second item falls back to API name
	if result[1].Label != "UnknownObj" {
		t.Errorf("expected label 'UnknownObj' (fallback), got %q", result[1].Label)
	}
}

// ---------------------------------------------------------------------------
// SetObjectPinned
// ---------------------------------------------------------------------------

func TestSetObjectPinned_Success(t *testing.T) {
	var gotUserID, gotObject string
	var gotPinned bool
	repo := &mockMRURepository{
		setObjectPinnedFn: func(_ context.Context, userID, objectAPIName string, pinned bool) error {
			gotUserID = userID
			gotObject = objectAPIName
			gotPinned = pinned
			return nil
		},
	}
	svc := NewService(repo)
	err := svc.SetObjectPinned(context.Background(), "user1", "Account", true)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if gotUserID != "user1" || gotObject != "Account" || !gotPinned {
		t.Errorf("got (%s, %s, %v), want (user1, Account, true)", gotUserID, gotObject, gotPinned)
	}
}

func TestSetObjectPinned_Unpin(t *testing.T) {
	var gotPinned bool
	repo := &mockMRURepository{
		setObjectPinnedFn: func(_ context.Context, _, _ string, pinned bool) error {
			gotPinned = pinned
			return nil
		},
	}
	svc := NewService(repo)
	err := svc.SetObjectPinned(context.Background(), "user1", "Account", false)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if gotPinned {
		t.Error("expected pinned=false")
	}
}

func TestSetObjectPinned_RepoError(t *testing.T) {
	repo := &mockMRURepository{
		setObjectPinnedFn: func(_ context.Context, _, _ string, _ bool) error {
			return errors.New("db error")
		},
	}
	svc := NewService(repo)
	err := svc.SetObjectPinned(context.Background(), "user1", "Account", true)
	if err == nil {
		t.Error("expected error from repo, got nil")
	}
}

// ---------------------------------------------------------------------------
// GetRelevantItems
// ---------------------------------------------------------------------------

func TestGetRelevantItems_EmptyResults(t *testing.T) {
	repo := &mockMRURepository{
		getRelevantItemsFn: func(_ context.Context, _ string, _ []string, _ int64) ([]ports.MRURecentItem, int64, error) {
			return []ports.MRURecentItem{}, 0, nil
		},
	}
	svc := NewService(repo)
	items, counter, err := svc.GetRelevantItems(context.Background(), "user1", nil, "", "v63.0")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(items) != 0 {
		t.Errorf("expected 0 items, got %d", len(items))
	}
	if counter != "0" {
		t.Errorf("expected counter '0', got %q", counter)
	}
}

func TestGetRelevantItems_DefaultApiVersion(t *testing.T) {
	repo := &mockMRURepository{
		getRelevantItemsFn: func(_ context.Context, _ string, _ []string, _ int64) ([]ports.MRURecentItem, int64, error) {
			return []ports.MRURecentItem{
				{ObjectAPIName: "Account", RecordID: "001ABC"},
			}, 42, nil
		},
		getRecordNamesBatchFn: func(_ context.Context, _ map[string][]string) (map[string]string, error) {
			return map[string]string{"001ABC": "Acme Corp"}, nil
		},
	}
	svc := NewService(repo)
	items, _, err := svc.GetRelevantItems(context.Background(), "user1", nil, "", "")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	// Default version is v63.0
	if len(items) != 1 {
		t.Fatalf("expected 1 item, got %d", len(items))
	}
	expectedURL := "/services/data/v63.0/sobjects/Account/001ABC"
	if items[0].Attributes.URL != expectedURL {
		t.Errorf("expected URL %q, got %q", expectedURL, items[0].Attributes.URL)
	}
}

func TestGetRelevantItems_WithLastUpdatedID(t *testing.T) {
	var gotLastUpdated int64
	repo := &mockMRURepository{
		getRelevantItemsFn: func(_ context.Context, _ string, _ []string, lastUpdatedId int64) ([]ports.MRURecentItem, int64, error) {
			gotLastUpdated = lastUpdatedId
			return nil, 0, nil
		},
	}
	svc := NewService(repo)
	_, _, _ = svc.GetRelevantItems(context.Background(), "user1", nil, "100", "v63.0")

	if gotLastUpdated != 100 {
		t.Errorf("expected lastUpdatedId=100, got %d", gotLastUpdated)
	}
}

func TestGetRelevantItems_InvalidLastUpdatedID(t *testing.T) {
	var gotLastUpdated int64
	repo := &mockMRURepository{
		getRelevantItemsFn: func(_ context.Context, _ string, _ []string, lastUpdatedId int64) ([]ports.MRURecentItem, int64, error) {
			gotLastUpdated = lastUpdatedId
			return nil, 0, nil
		},
	}
	svc := NewService(repo)
	_, _, _ = svc.GetRelevantItems(context.Background(), "user1", nil, "not-a-number", "v63.0")

	// Invalid parse should result in 0
	if gotLastUpdated != 0 {
		t.Errorf("expected lastUpdatedId=0 for invalid input, got %d", gotLastUpdated)
	}
}

func TestGetRelevantItems_RepoError(t *testing.T) {
	repo := &mockMRURepository{
		getRelevantItemsFn: func(_ context.Context, _ string, _ []string, _ int64) ([]ports.MRURecentItem, int64, error) {
			return nil, 0, errors.New("db error")
		},
	}
	svc := NewService(repo)
	_, _, err := svc.GetRelevantItems(context.Background(), "user1", nil, "", "v63.0")
	if err == nil {
		t.Error("expected error from repo, got nil")
	}
}

func TestGetRelevantItems_WithRecordNames(t *testing.T) {
	repo := &mockMRURepository{
		getRelevantItemsFn: func(_ context.Context, _ string, _ []string, _ int64) ([]ports.MRURecentItem, int64, error) {
			return []ports.MRURecentItem{
				{ObjectAPIName: "Account", RecordID: "001ABC"},
				{ObjectAPIName: "Contact", RecordID: "003XYZ"},
			}, 99, nil
		},
		getRecordNamesBatchFn: func(_ context.Context, byObject map[string][]string) (map[string]string, error) {
			// Verify correct grouping
			if len(byObject["Account"]) != 1 || byObject["Account"][0] != "001ABC" {
				return nil, errors.New("unexpected Account records")
			}
			if len(byObject["Contact"]) != 1 || byObject["Contact"][0] != "003XYZ" {
				return nil, errors.New("unexpected Contact records")
			}
			return map[string]string{
				"001ABC": "Acme Corp",
				"003XYZ": "Jane Doe",
			}, nil
		},
	}
	svc := NewService(repo)
	items, counter, err := svc.GetRelevantItems(context.Background(), "user1", nil, "", "v63.0")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if counter != "99" {
		t.Errorf("expected counter '99', got %q", counter)
	}
	if len(items) != 2 {
		t.Fatalf("expected 2 items, got %d", len(items))
	}

	// Check first item
	if items[0].Id != "001ABC" {
		t.Errorf("expected Id '001ABC', got %q", items[0].Id)
	}
	if items[0].Name != "Acme Corp" {
		t.Errorf("expected Name 'Acme Corp', got %q", items[0].Name)
	}
	if items[0].Attributes.Type != "Account" {
		t.Errorf("expected Type 'Account', got %q", items[0].Attributes.Type)
	}
	expectedURL := "/services/data/v63.0/sobjects/Account/001ABC"
	if items[0].Attributes.URL != expectedURL {
		t.Errorf("expected URL %q, got %q", expectedURL, items[0].Attributes.URL)
	}

	// Check second item
	if items[1].Name != "Jane Doe" {
		t.Errorf("expected Name 'Jane Doe', got %q", items[1].Name)
	}
}

func TestGetRelevantItems_RecordNamesBatchError(t *testing.T) {
	repo := &mockMRURepository{
		getRelevantItemsFn: func(_ context.Context, _ string, _ []string, _ int64) ([]ports.MRURecentItem, int64, error) {
			return []ports.MRURecentItem{
				{ObjectAPIName: "Account", RecordID: "001ABC"},
			}, 10, nil
		},
		getRecordNamesBatchFn: func(_ context.Context, _ map[string][]string) (map[string]string, error) {
			return nil, errors.New("name lookup error")
		},
	}
	svc := NewService(repo)
	items, _, err := svc.GetRelevantItems(context.Background(), "user1", nil, "", "v63.0")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	// When name lookup fails, name should default to record ID
	if len(items) != 1 {
		t.Fatalf("expected 1 item, got %d", len(items))
	}
	if items[0].Name != "001ABC" {
		t.Errorf("expected name to fall back to record ID '001ABC', got %q", items[0].Name)
	}
}

func TestGetRelevantItems_RecordNameMissing(t *testing.T) {
	repo := &mockMRURepository{
		getRelevantItemsFn: func(_ context.Context, _ string, _ []string, _ int64) ([]ports.MRURecentItem, int64, error) {
			return []ports.MRURecentItem{
				{ObjectAPIName: "Account", RecordID: "001ABC"},
			}, 5, nil
		},
		getRecordNamesBatchFn: func(_ context.Context, _ map[string][]string) (map[string]string, error) {
			// Return empty map — name not found
			return map[string]string{}, nil
		},
	}
	svc := NewService(repo)
	items, _, err := svc.GetRelevantItems(context.Background(), "user1", nil, "", "v63.0")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if items[0].Name != "001ABC" {
		t.Errorf("expected name to fall back to record ID '001ABC', got %q", items[0].Name)
	}
}

func TestGetRelevantItems_RecordNameEmpty(t *testing.T) {
	repo := &mockMRURepository{
		getRelevantItemsFn: func(_ context.Context, _ string, _ []string, _ int64) ([]ports.MRURecentItem, int64, error) {
			return []ports.MRURecentItem{
				{ObjectAPIName: "Account", RecordID: "001ABC"},
			}, 5, nil
		},
		getRecordNamesBatchFn: func(_ context.Context, _ map[string][]string) (map[string]string, error) {
			return map[string]string{"001ABC": ""}, nil
		},
	}
	svc := NewService(repo)
	items, _, err := svc.GetRelevantItems(context.Background(), "user1", nil, "", "v63.0")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	// Empty string name should fall back to record ID
	if items[0].Name != "001ABC" {
		t.Errorf("expected name to fall back to record ID '001ABC' for empty name, got %q", items[0].Name)
	}
}

func TestGetRelevantItems_WithSobjectFilter(t *testing.T) {
	var gotSobjects []string
	repo := &mockMRURepository{
		getRelevantItemsFn: func(_ context.Context, _ string, sobjects []string, _ int64) ([]ports.MRURecentItem, int64, error) {
			gotSobjects = sobjects
			return nil, 0, nil
		},
	}
	svc := NewService(repo)
	filter := []string{"Account", "Contact"}
	_, _, _ = svc.GetRelevantItems(context.Background(), "user1", filter, "", "v63.0")

	if len(gotSobjects) != 2 || gotSobjects[0] != "Account" || gotSobjects[1] != "Contact" {
		t.Errorf("expected sobjects filter [Account Contact], got %v", gotSobjects)
	}
}

func TestGetRelevantItems_MultipleRecordsPerObject(t *testing.T) {
	repo := &mockMRURepository{
		getRelevantItemsFn: func(_ context.Context, _ string, _ []string, _ int64) ([]ports.MRURecentItem, int64, error) {
			return []ports.MRURecentItem{
				{ObjectAPIName: "Account", RecordID: "001AAA"},
				{ObjectAPIName: "Account", RecordID: "001BBB"},
				{ObjectAPIName: "Account", RecordID: "001CCC"},
			}, 50, nil
		},
		getRecordNamesBatchFn: func(_ context.Context, byObject map[string][]string) (map[string]string, error) {
			if len(byObject["Account"]) != 3 {
				return nil, errors.New("expected 3 Account records grouped together")
			}
			return map[string]string{
				"001AAA": "Alpha Inc",
				"001BBB": "Beta LLC",
				"001CCC": "Charlie Co",
			}, nil
		},
	}
	svc := NewService(repo)
	items, _, err := svc.GetRelevantItems(context.Background(), "user1", nil, "", "v63.0")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(items) != 3 {
		t.Fatalf("expected 3 items, got %d", len(items))
	}
	for i, expected := range []string{"Alpha Inc", "Beta LLC", "Charlie Co"} {
		if items[i].Name != expected {
			t.Errorf("item[%d].Name = %q, want %q", i, items[i].Name, expected)
		}
	}
}

func TestGetRelevantItems_CustomApiVersion(t *testing.T) {
	repo := &mockMRURepository{
		getRelevantItemsFn: func(_ context.Context, _ string, _ []string, _ int64) ([]ports.MRURecentItem, int64, error) {
			return []ports.MRURecentItem{
				{ObjectAPIName: "Lead", RecordID: "00Q123"},
			}, 1, nil
		},
		getRecordNamesBatchFn: func(_ context.Context, _ map[string][]string) (map[string]string, error) {
			return map[string]string{"00Q123": "Test Lead"}, nil
		},
	}
	svc := NewService(repo)
	items, _, err := svc.GetRelevantItems(context.Background(), "user1", nil, "", "v58.0")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	expectedURL := "/services/data/v58.0/sobjects/Lead/00Q123"
	if items[0].Attributes.URL != expectedURL {
		t.Errorf("expected URL %q, got %q", expectedURL, items[0].Attributes.URL)
	}
}

// ---------------------------------------------------------------------------
// GenerateCustomPrefix
// ---------------------------------------------------------------------------

func TestGenerateCustomPrefix(t *testing.T) {
	tests := []struct {
		index    int
		expected string
	}{
		{0, "a00"},   // first index: series=0, offset=0
		{1, "a01"},   // second: series=0, offset=1
		{9, "a09"},   // last digit
		{10, "a0A"},  // first letter
		{35, "a0Z"},  // last in first series
		{36, "a10"},  // second series
		{71, "a1Z"},  // last in second series
		{72, "a20"},  // third series
	}

	for _, tt := range tests {
		t.Run(tt.expected, func(t *testing.T) {
			result := GenerateCustomPrefix(tt.index)
			if result != tt.expected {
				t.Errorf("GenerateCustomPrefix(%d) = %q; want %q", tt.index, result, tt.expected)
			}
		})
	}
}

func TestGenerateCustomPrefix_LargeSeries(t *testing.T) {
	// series wraps around at 36
	// index 36*36 = 1296 → series=36%36=0, offset=0 → "a00"
	result := GenerateCustomPrefix(36 * 36)
	if result != "a00" {
		t.Errorf("GenerateCustomPrefix(%d) = %q; expected wrap-around to 'a00'", 36*36, result)
	}
}

// ---------------------------------------------------------------------------
// ValidateObjectAPIName
// ---------------------------------------------------------------------------

func TestValidateObjectAPIName(t *testing.T) {
	valid := []string{
		"Account",
		"Custom_Object__c",
		"_InternalObj",
		"A",
		"a123",
		"My_Custom_Obj_123__c",
	}
	invalid := []string{
		"",
		"123Start",
		"has space",
		"has-dash",
		"has.dot",
		"has/slash",
		"has@symbol",
	}

	for _, name := range valid {
		if !ValidateObjectAPIName(name) {
			t.Errorf("ValidateObjectAPIName(%q) = false; want true", name)
		}
	}
	for _, name := range invalid {
		if ValidateObjectAPIName(name) {
			t.Errorf("ValidateObjectAPIName(%q) = true; want false", name)
		}
	}
}

// ---------------------------------------------------------------------------
// ResolveTypeFromID — 2-char prefix (custom object) path
// ---------------------------------------------------------------------------

func TestResolveTypeFromID_TwoCharPrefix(t *testing.T) {
	// Register a 2-char prefix to test the 2-char lookup branch
	RegisterKeyPrefix("a0", "CustomTwoChar__c")
	defer func() {
		// Clean up to avoid polluting other tests
		keyPrefixMutex.Lock()
		delete(keyPrefixToObject, "a0")
		delete(objectToKeyPrefix, "CustomTwoChar__c")
		keyPrefixMutex.Unlock()
	}()

	gotType, ok := ResolveTypeFromID("a0X5000001ABC12")
	if !ok {
		t.Fatal("expected ResolveTypeFromID to succeed for 2-char prefix")
	}
	if gotType != "CustomTwoChar__c" {
		t.Errorf("expected 'CustomTwoChar__c', got %q", gotType)
	}
}

func TestResolveTypeFromID_ThreeCharPrefixTakesPrecedence(t *testing.T) {
	// Register both 2-char and 3-char prefix where 3-char should win
	RegisterKeyPrefix("a1", "TwoCharObj__c")
	RegisterKeyPrefix("a1B", "ThreeCharObj__c")
	defer func() {
		keyPrefixMutex.Lock()
		delete(keyPrefixToObject, "a1")
		delete(keyPrefixToObject, "a1B")
		delete(objectToKeyPrefix, "TwoCharObj__c")
		delete(objectToKeyPrefix, "ThreeCharObj__c")
		keyPrefixMutex.Unlock()
	}()

	// ID starting with "a1B" should match the 3-char prefix first
	gotType, ok := ResolveTypeFromID("a1B5000001ABC12")
	if !ok {
		t.Fatal("expected ResolveTypeFromID to succeed")
	}
	if gotType != "ThreeCharObj__c" {
		t.Errorf("expected 'ThreeCharObj__c' (3-char takes precedence), got %q", gotType)
	}
}

func TestResolveTypeFromID_ExactlyThreeChars(t *testing.T) {
	gotType, ok := ResolveTypeFromID("005")
	if !ok {
		t.Fatal("expected success for exactly 3-char input matching User prefix")
	}
	if gotType != "User" {
		t.Errorf("expected 'User', got %q", gotType)
	}
}
