import type { AdminUserViewData } from '@/lib/view-data/AdminUserViewData'; import type { DashboardStatsViewData } from '@/lib/view-data/DashboardStatsViewData'; import { describe, expect, it } from 'vitest'; import { AdminUserViewModel, DashboardStatsViewModel, UserListViewModel } from './AdminUserViewModel'; describe('AdminUserViewModel', () => { const createBaseViewData = (): AdminUserViewData => ({ id: 'user-123', email: 'test@example.com', displayName: 'Test User', roles: ['user'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-02T00:00:00Z', lastLoginAt: '2024-01-15T10:30:00Z', primaryDriverId: 'driver-456', }); it('maps core fields from ViewData', () => { const viewData = createBaseViewData(); const vm = new AdminUserViewModel(viewData); expect(vm.id).toBe('user-123'); expect(vm.email).toBe('test@example.com'); expect(vm.displayName).toBe('Test User'); expect(vm.roles).toEqual(['user']); expect(vm.status).toBe('active'); expect(vm.isSystemAdmin).toBe(false); expect(vm.primaryDriverId).toBe('driver-456'); }); it('converts dates to Date objects', () => { const viewData = createBaseViewData(); const vm = new AdminUserViewModel(viewData); expect(vm.createdAt).toBeInstanceOf(Date); expect(vm.updatedAt).toBeInstanceOf(Date); expect(vm.lastLoginAt).toBeInstanceOf(Date); expect(vm.createdAt.toISOString()).toBe('2024-01-01T00:00:00.000Z'); }); it('handles missing lastLoginAt', () => { const viewData = createBaseViewData(); delete viewData.lastLoginAt; const vm = new AdminUserViewModel(viewData); expect(vm.lastLoginAt).toBeUndefined(); expect(vm.lastLoginFormatted).toBe('Never'); }); it('formats role badges correctly', () => { const owner = new AdminUserViewModel({ ...createBaseViewData(), roles: ['owner'] }); const admin = new AdminUserViewModel({ ...createBaseViewData(), roles: ['admin'] }); const user = new AdminUserViewModel({ ...createBaseViewData(), roles: ['user'] }); const custom = new AdminUserViewModel({ ...createBaseViewData(), roles: ['custom-role'] }); expect(owner.roleBadges).toEqual(['Owner']); expect(admin.roleBadges).toEqual(['Admin']); expect(user.roleBadges).toEqual(['User']); expect(custom.roleBadges).toEqual(['custom-role']); }); it('derives status badge correctly', () => { const active = new AdminUserViewModel({ ...createBaseViewData(), status: 'active' }); const suspended = new AdminUserViewModel({ ...createBaseViewData(), status: 'suspended' }); const deleted = new AdminUserViewModel({ ...createBaseViewData(), status: 'deleted' }); expect(active.statusBadgeLabel).toBe('Active'); expect(active.statusBadgeVariant).toBe('performance-green'); expect(suspended.statusBadgeLabel).toBe('Suspended'); expect(suspended.statusBadgeVariant).toBe('yellow-500'); expect(deleted.statusBadgeLabel).toBe('Deleted'); expect(deleted.statusBadgeVariant).toBe('racing-red'); }); it('formats dates for display', () => { const viewData = createBaseViewData(); const vm = new AdminUserViewModel(viewData); expect(vm.lastLoginFormatted).toBe('Jan 15, 2024'); expect(vm.createdAtFormatted).toBe('Jan 1, 2024'); }); it('handles multiple roles', () => { const viewData = { ...createBaseViewData(), roles: ['owner', 'admin'] }; const vm = new AdminUserViewModel(viewData); expect(vm.roleBadges).toEqual(['Owner', 'Admin']); }); }); describe('DashboardStatsViewModel', () => { const createBaseData = (): DashboardStatsViewData => ({ totalUsers: 100, activeUsers: 70, suspendedUsers: 10, deletedUsers: 20, systemAdmins: 5, recentLogins: 25, newUsersToday: 3, userGrowth: [ { label: 'Mon', value: 5, color: 'text-primary-blue' }, { label: 'Tue', value: 8, color: 'text-primary-blue' }, ], roleDistribution: [ { label: 'Owner', value: 2, color: 'text-purple-500' }, { label: 'Admin', value: 3, color: 'text-blue-500' }, { label: 'User', value: 95, color: 'text-gray-500' }, ], statusDistribution: { active: 70, suspended: 10, deleted: 20, }, activityTimeline: [ { date: 'Mon', newUsers: 2, logins: 10 }, { date: 'Tue', newUsers: 3, logins: 15 }, ], }); it('maps all core fields from data', () => { const data = createBaseData(); const vm = new DashboardStatsViewModel(data); expect(vm.totalUsers).toBe(100); expect(vm.activeUsers).toBe(70); expect(vm.suspendedUsers).toBe(10); expect(vm.deletedUsers).toBe(20); expect(vm.systemAdmins).toBe(5); expect(vm.recentLogins).toBe(25); expect(vm.newUsersToday).toBe(3); }); it('computes active rate correctly', () => { const vm = new DashboardStatsViewModel(createBaseData()); expect(vm.activeRate).toBe(70); // 70% expect(vm.activeRateFormatted).toBe('70%'); }); it('computes admin ratio correctly', () => { const vm = new DashboardStatsViewModel(createBaseData()); // 5 admins, 95 non-admins => 1:19 expect(vm.adminRatio).toBe('1:19'); }); it('derives activity level correctly', () => { const lowEngagement = new DashboardStatsViewModel({ ...createBaseData(), totalUsers: 100, recentLogins: 10, // 10% engagement }); expect(lowEngagement.activityLevelLabel).toBe('Low'); expect(lowEngagement.activityLevelValue).toBe('low'); const mediumEngagement = new DashboardStatsViewModel({ ...createBaseData(), totalUsers: 100, recentLogins: 35, // 35% engagement }); expect(mediumEngagement.activityLevelLabel).toBe('Medium'); expect(mediumEngagement.activityLevelValue).toBe('medium'); const highEngagement = new DashboardStatsViewModel({ ...createBaseData(), totalUsers: 100, recentLogins: 60, // 60% engagement }); expect(highEngagement.activityLevelLabel).toBe('High'); expect(highEngagement.activityLevelValue).toBe('high'); }); it('handles zero users safely', () => { const vm = new DashboardStatsViewModel({ ...createBaseData(), totalUsers: 0, activeUsers: 0, systemAdmins: 0, recentLogins: 0, }); expect(vm.activeRate).toBe(0); expect(vm.activeRateFormatted).toBe('0%'); expect(vm.adminRatio).toBe('1:1'); expect(vm.activityLevelLabel).toBe('Low'); expect(vm.activityLevelValue).toBe('low'); }); it('preserves arrays from input', () => { const data = createBaseData(); const vm = new DashboardStatsViewModel(data); expect(vm.userGrowth).toEqual(data.userGrowth); expect(vm.roleDistribution).toEqual(data.roleDistribution); expect(vm.activityTimeline).toEqual(data.activityTimeline); }); }); describe('UserListViewModel', () => { const createViewData = (overrides: Partial = {}): AdminUserViewData => ({ id: 'user-1', email: 'test@example.com', displayName: 'Test User', roles: ['user'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-02T00:00:00Z', ...overrides, }); it('wraps user ViewData in AdminUserViewModel instances', () => { const data = { users: [createViewData({ id: 'user-1' }), createViewData({ id: 'user-2' })], total: 2, page: 1, limit: 10, totalPages: 1, }; const vm = new UserListViewModel(data); expect(vm.users).toHaveLength(2); expect(vm.users[0]).toBeInstanceOf(AdminUserViewModel); expect(vm.users[0].id).toBe('user-1'); expect(vm.users[1].id).toBe('user-2'); }); it('exposes pagination metadata', () => { const data = { users: [createViewData()], total: 50, page: 2, limit: 10, totalPages: 5, }; const vm = new UserListViewModel(data); expect(vm.total).toBe(50); expect(vm.page).toBe(2); expect(vm.limit).toBe(10); expect(vm.totalPages).toBe(5); }); it('derives hasUsers correctly', () => { const withUsers = new UserListViewModel({ users: [createViewData()], total: 1, page: 1, limit: 10, totalPages: 1, }); const withoutUsers = new UserListViewModel({ users: [], total: 0, page: 1, limit: 10, totalPages: 0, }); expect(withUsers.hasUsers).toBe(true); expect(withoutUsers.hasUsers).toBe(false); }); it('derives showPagination correctly', () => { const withPagination = new UserListViewModel({ users: [createViewData()], total: 20, page: 1, limit: 10, totalPages: 2, }); const withoutPagination = new UserListViewModel({ users: [createViewData()], total: 5, page: 1, limit: 10, totalPages: 1, }); expect(withPagination.showPagination).toBe(true); expect(withoutPagination.showPagination).toBe(false); }); it('calculates start and end indices correctly', () => { const vm = new UserListViewModel({ users: [createViewData(), createViewData(), createViewData()], total: 50, page: 2, limit: 10, totalPages: 5, }); expect(vm.startIndex).toBe(11); // (2-1) * 10 + 1 expect(vm.endIndex).toBe(13); // min(2 * 10, 50) }); it('handles empty list indices', () => { const vm = new UserListViewModel({ users: [], total: 0, page: 1, limit: 10, totalPages: 0, }); expect(vm.startIndex).toBe(0); expect(vm.endIndex).toBe(0); }); });