import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:recipe_flutter/core/ui/app_shell.dart'; import 'package:recipe_flutter/features/admin/data/admin_repository.dart'; import 'package:recipe_flutter/features/admin/domain/admin_ai_trace.dart'; import 'package:recipe_flutter/features/admin/domain/admin_ai_trace_detail.dart'; import 'package:recipe_flutter/features/admin/presentation/admin_ai_panel.dart'; import 'package:recipe_flutter/features/admin/presentation/admin_screen.dart'; import 'package:recipe_flutter/features/auth/data/auth_providers.dart'; class _FakeAdminRepository implements AdminRepository { final AdminAiTraceListResponse flyerList; final AdminAiTraceListResponse receiptList; final Map details; _FakeAdminRepository({ required this.flyerList, required this.receiptList, required this.details, }); @override Future listAiTraces({ required AdminAiTraceSource source, int limit = 25, String? cursor, String? period, bool onlyErrors = false, }) async { return source == AdminAiTraceSource.flyer ? flyerList : receiptList; } @override Future getAiTraceById(String traceId) async { return details[traceId] ?? details.values.first; } @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } Widget _buildPanelApp(AdminRepository repo) { return ProviderScope( overrides: [ adminRepositoryProvider.overrideWithValue(repo), ], child: const MaterialApp( home: Scaffold( body: AdminAiPanel(embedded: true), ), ), ); } void main() { final veryLargeOutput = '{"payload":"${List.filled(13050, 'x').join()}"}'; final flyerItem = AdminAiTraceListItem( id: 'flyer-101', source: AdminAiTraceSource.flyer, status: AdminAiTraceStatus.warning, createdAt: DateTime.parse('2026-05-20T12:34:56.000Z'), userId: 7, userLabel: 'admin', sessionId: 101, fileName: 'willys-v20.pdf', model: 'ministral-8b-2512', durationMs: 1880, warningsCount: 2, hasPrompt: true, hasOutput: true, error: 'Det finns 2 varningar i detaljvyn.', ); final flyerDetail = AdminAiTraceDetail( id: 'flyer-101', source: AdminAiTraceSource.flyer, status: AdminAiTraceStatus.warning, createdAt: DateTime.parse('2026-05-20T12:34:56.000Z'), userId: 7, userLabel: 'admin', sessionId: 101, fileName: 'willys-v20.pdf', model: 'ministral-8b-2512', durationMs: 1880, retryCount: 1, chunkCount: 3, warnings: const ['parse:low_confidence', 'match:no_match'], error: null, prompt: 'Prompttext exempel', rawOutput: veryLargeOutput, normalizedOutput: null, summary: const {'itemCount': 1}, ); group('Admin AI tab and panel', () { testWidgets('Admin main route query tab=ai renders AI panel', (tester) async { final fakeRepo = _FakeAdminRepository( flyerList: AdminAiTraceListResponse(items: [flyerItem], nextCursor: null), receiptList: const AdminAiTraceListResponse(items: [], nextCursor: null), details: {'flyer-101': flyerDetail}, ); await tester.pumpWidget( ProviderScope( overrides: [ adminRepositoryProvider.overrideWithValue(fakeRepo), ], child: const MaterialApp( home: Scaffold( body: AdminScreen(initialTab: AdminViewTab.ai), ), ), ), ); await tester.pumpAndSettle(); expect(find.text('Kvitto'), findsOneWidget); expect(find.text('Flyer'), findsOneWidget); expect(find.text('willys-v20.pdf'), findsOneWidget); }); testWidgets('AppShell shows AI top chip and navigates with query', (tester) async { String? navigatedTo; await tester.pumpWidget( ProviderScope( overrides: [ isAdminProvider.overrideWithValue(true), ], child: MaterialApp( home: AppShell( location: '/admin?tab=ai', onNavigateToPath: (path) => navigatedTo = path, child: const SizedBox.shrink(), ), ), ), ); await tester.pumpAndSettle(); expect(find.text('AI'), findsOneWidget); await tester.tap(find.text('Databas')); await tester.pump(); expect(navigatedTo, '/admin?tab=database'); }); testWidgets('Source switching toggles between Flyer and Kvitto views', (tester) async { final fakeRepo = _FakeAdminRepository( flyerList: AdminAiTraceListResponse(items: [flyerItem], nextCursor: null), receiptList: const AdminAiTraceListResponse(items: [], nextCursor: null), details: {'flyer-101': flyerDetail}, ); await tester.pumpWidget(_buildPanelApp(fakeRepo)); await tester.pumpAndSettle(); expect(find.text('willys-v20.pdf'), findsOneWidget); await tester.tap(find.text('Kvitto')); await tester.pumpAndSettle(); expect(find.text('Receipt trace-data saknas i recipe-api i denna fas.'), findsOneWidget); await tester.tap(find.text('Flyer')); await tester.pumpAndSettle(); expect(find.text('willys-v20.pdf'), findsOneWidget); }); testWidgets('Prompt/output are selectable and warning details are visible', (tester) async { await tester.binding.setSurfaceSize(const Size(1400, 1200)); final fakeRepo = _FakeAdminRepository( flyerList: AdminAiTraceListResponse(items: [flyerItem], nextCursor: null), receiptList: const AdminAiTraceListResponse(items: [], nextCursor: null), details: {'flyer-101': flyerDetail}, ); await tester.pumpWidget(_buildPanelApp(fakeRepo)); await tester.pumpAndSettle(); await tester.pump(const Duration(milliseconds: 500)); await tester.tap(find.byType(ListTile).first); await tester.pumpAndSettle(); await tester.pump(const Duration(milliseconds: 500)); expect(find.text('Sammanfattning'), findsOneWidget); expect(find.text('Varningar (2)'), findsOneWidget); expect(find.text('parse:low_confidence'), findsOneWidget); expect(find.text('match:no_match'), findsOneWidget); final detailScroll = find.byType(Scrollable).last; await tester.scrollUntilVisible( find.text('Model Output'), 200, scrollable: detailScroll, ); await tester.pumpAndSettle(); expect(find.text('Model Output'), findsOneWidget); expect(find.byType(SelectableText), findsWidgets); expect(find.text('Visa hela outputen'), findsOneWidget); await tester.tap(find.text('Visa hela outputen')); await tester.pumpAndSettle(); expect(find.text('Visa mindre'), findsOneWidget); final copyPrompt = find.byTooltip('Kopiera'); final copyOutput = find.byTooltip('Kopiera JSON'); final copyWarnings = find.byTooltip('Kopiera alla varningar'); expect(copyPrompt, findsOneWidget); expect(copyOutput, findsOneWidget); expect(copyWarnings, findsOneWidget); await tester.tap(copyPrompt); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); await tester.tap(copyOutput); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); await tester.tap(copyWarnings); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); addTearDown(() => tester.binding.setSurfaceSize(null)); }); }); }