# Testing Strategy - BuyerKiosk Live

## Current State

### Test Infrastructure
- **Single smoke test** exists at `test/widget_test.dart` - verifies `BuyerKioskApp` builds and renders `MaterialApp`
- **Mocktail 1.0.3** configured in `pubspec.yaml` but completely unused
- **flutter_test** SDK included
- **Zero coverage** on unit tests, provider tests, integration tests, or widget tests beyond smoke test

### Code Analysis
```
lib/
├── core/           # 3 network files, exceptions, constants - UNTESTED
├── data/           # 13 mappers, 8 repositories, 15+ models - UNTESTED
├── domain/         # 10+ entities with computed properties - UNTESTED
├── presentation/   # 15+ providers, 20+ screens - UNTESTED
└── router/         # GoRouter with auth logic - UNTESTED
```

---

## High-Priority Test Targets

### 1. Core Layer (Unit Tests)

#### API Client (`lib/core/network/api_client.dart`)

**Critical behaviors to test:**
- Error type mapping from `DioException` to typed exceptions
- Timeout handling (connection, send, receive)
- Status code interpretation (401, 403, 404, 500+)

**Exception hierarchy:**
```dart
ApiException (base)
├── TimeoutException    - DioExceptionType.connectionTimeout/sendTimeout/receiveTimeout
├── AuthenticationException - 401, 403 responses
├── ServerException     - 4xx/5xx responses (with statusCode)
├── NetworkException    - DioExceptionType.connectionError
├── CertificateException - DioExceptionType.badCertificate
├── RequestCancelledException - DioExceptionType.cancel
└── UnknownException    - DioExceptionType.unknown
```

**Test file:** `test/core/network/api_client_test.dart`

```dart
import 'package:dio/dio.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:buyer_kiosk_live/core/network/api_client.dart';

class MockDio extends Mock implements Dio {}

void main() {
  late MockDio mockDio;
  late ApiClient apiClient;

  setUpAll(() {
    registerFallbackValue(RequestOptions(path: ''));
  });

  setUp(() {
    mockDio = MockDio();
    when(() => mockDio.options).thenReturn(BaseOptions());
    when(() => mockDio.interceptors).thenReturn(Interceptors());
    apiClient = ApiClient(dio: mockDio);
  });

  group('ApiClient error handling', () {
    test('should throw TimeoutException on connectionTimeout', () async {
      when(() => mockDio.post<dynamic>(
        any(),
        data: any(named: 'data'),
        queryParameters: any(named: 'queryParameters'),
        options: any(named: 'options'),
        cancelToken: any(named: 'cancelToken'),
        onSendProgress: any(named: 'onSendProgress'),
        onReceiveProgress: any(named: 'onReceiveProgress'),
      )).thenThrow(DioException(
        type: DioExceptionType.connectionTimeout,
        requestOptions: RequestOptions(path: '/test'),
        message: 'Connection timeout',
      ));

      expect(
        () => apiClient.post('/test'),
        throwsA(isA<TimeoutException>()),
      );
    });

    test('should throw AuthenticationException on 403', () async {
      when(() => mockDio.post<dynamic>(
        any(),
        data: any(named: 'data'),
        queryParameters: any(named: 'queryParameters'),
        options: any(named: 'options'),
        cancelToken: any(named: 'cancelToken'),
        onSendProgress: any(named: 'onSendProgress'),
        onReceiveProgress: any(named: 'onReceiveProgress'),
      )).thenThrow(DioException(
        type: DioExceptionType.badResponse,
        response: Response(
          statusCode: 403,
          requestOptions: RequestOptions(path: '/test'),
        ),
        requestOptions: RequestOptions(path: '/test'),
      ));

      expect(
        () => apiClient.post('/test'),
        throwsA(isA<AuthenticationException>()),
      );
    });

    test('should throw ServerException on 500+ errors', () async {
      when(() => mockDio.post<dynamic>(
        any(),
        data: any(named: 'data'),
        queryParameters: any(named: 'queryParameters'),
        options: any(named: 'options'),
        cancelToken: any(named: 'cancelToken'),
        onSendProgress: any(named: 'onSendProgress'),
        onReceiveProgress: any(named: 'onReceiveProgress'),
      )).thenThrow(DioException(
        type: DioExceptionType.badResponse,
        response: Response(
          statusCode: 500,
          requestOptions: RequestOptions(path: '/test'),
        ),
        requestOptions: RequestOptions(path: '/test'),
      ));

      final result = apiClient.post('/test');

      await expectLater(result, throwsA(isA<ServerException>()));
    });

    test('should throw NetworkException on connection error', () async {
      when(() => mockDio.post<dynamic>(
        any(),
        data: any(named: 'data'),
        queryParameters: any(named: 'queryParameters'),
        options: any(named: 'options'),
        cancelToken: any(named: 'cancelToken'),
        onSendProgress: any(named: 'onSendProgress'),
        onReceiveProgress: any(named: 'onReceiveProgress'),
      )).thenThrow(DioException(
        type: DioExceptionType.connectionError,
        requestOptions: RequestOptions(path: '/test'),
      ));

      expect(
        () => apiClient.post('/test'),
        throwsA(isA<NetworkException>()),
      );
    });
  });
}
```

---

#### Auth Interceptor (`lib/core/network/api_interceptors.dart`)

**Critical behaviors to test:**
- APIKey injection into POST body (Map data)
- FormData handling (extract fields, add APIKey, recreate)
- 403 response triggers credential clearing
- Empty/null API key handling

**Test file:** `test/core/network/api_interceptors_test.dart`

```dart
import 'package:dio/dio.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:buyer_kiosk_live/core/network/api_interceptors.dart';
import 'package:buyer_kiosk_live/core/constants/app_constants.dart';

void main() {
  group('AuthInterceptor', () {
    late AuthInterceptor interceptor;

    setUp(() {
      interceptor = AuthInterceptor();
      SharedPreferences.setMockInitialValues({});
    });

    group('onRequest', () {
      test('should add APIKey to Map data when key exists', () async {
        // Arrange
        SharedPreferences.setMockInitialValues({
          AppConstants.storageKeyApiKey: 'test-api-key',
        });

        final options = RequestOptions(
          path: '/test',
          data: {'action': 'dashboard'},
        );
        final handler = _MockRequestHandler();

        // Act
        interceptor.onRequest(options, handler);

        // Allow async operations to complete
        await Future.delayed(Duration.zero);

        // Assert - handler.next should be called with modified options
        expect(handler.nextCalled, isTrue);
        expect(handler.capturedOptions?.data, isA<Map>());
        expect((handler.capturedOptions?.data as Map)['APIKey'], 'test-api-key');
      });

      test('should handle FormData by adding APIKey', () async {
        SharedPreferences.setMockInitialValues({
          AppConstants.storageKeyApiKey: 'test-api-key',
        });

        final formData = FormData.fromMap({'action': 'dashboard'});
        final options = RequestOptions(
          path: '/test',
          data: formData,
        );
        final handler = _MockRequestHandler();

        interceptor.onRequest(options, handler);
        await Future.delayed(Duration.zero);

        expect(handler.nextCalled, isTrue);
        expect(handler.capturedOptions?.data, isA<FormData>());
      });

      test('should proceed without modification when no API key', () async {
        SharedPreferences.setMockInitialValues({});

        final options = RequestOptions(
          path: '/test',
          data: {'action': 'dashboard'},
        );
        final handler = _MockRequestHandler();

        interceptor.onRequest(options, handler);
        await Future.delayed(Duration.zero);

        expect(handler.nextCalled, isTrue);
      });
    });

    group('onResponse', () {
      test('should clear auth data on 403 response', () async {
        SharedPreferences.setMockInitialValues({
          AppConstants.storageKeyApiKey: 'test-api-key',
          AppConstants.storageKeyStoreId: 'store-123',
        });

        final response = Response(
          statusCode: 403,
          requestOptions: RequestOptions(path: '/test'),
        );
        final handler = _MockResponseHandler();

        interceptor.onResponse(response, handler);
        await Future.delayed(const Duration(milliseconds: 100));

        final prefs = await SharedPreferences.getInstance();
        expect(prefs.getString(AppConstants.storageKeyApiKey), isNull);
      });

      test('should not clear auth data on 200 response', () async {
        SharedPreferences.setMockInitialValues({
          AppConstants.storageKeyApiKey: 'test-api-key',
        });

        final response = Response(
          statusCode: 200,
          requestOptions: RequestOptions(path: '/test'),
        );
        final handler = _MockResponseHandler();

        interceptor.onResponse(response, handler);
        await Future.delayed(const Duration(milliseconds: 100));

        final prefs = await SharedPreferences.getInstance();
        expect(prefs.getString(AppConstants.storageKeyApiKey), 'test-api-key');
      });
    });
  });
}

// Test helpers
class _MockRequestHandler implements RequestInterceptorHandler {
  bool nextCalled = false;
  RequestOptions? capturedOptions;

  @override
  void next(RequestOptions options) {
    nextCalled = true;
    capturedOptions = options;
  }

  @override
  void reject(DioException error, [bool callFollowingErrorInterceptor = false]) {}

  @override
  void resolve(Response response, [bool callFollowingResponseInterceptor = false]) {}
}

class _MockResponseHandler implements ResponseInterceptorHandler {
  bool nextCalled = false;

  @override
  void next(Response response) {
    nextCalled = true;
  }

  @override
  void reject(DioException error, [bool callFollowingErrorInterceptor = false]) {}

  @override
  void resolve(Response response, [bool callFollowingResponseInterceptor = false]) {}
}
```

---

#### JSON Parsing Helpers

**Location:** Various model files use inline parsing functions.

**Test file:** `test/core/utils/json_parsing_test.dart`

```dart
import 'package:flutter_test/flutter_test.dart';

// Helper functions to test (copy from models or extract to utils)
double parseDouble(dynamic value) {
  if (value == null) return 0.0;
  if (value is num) return value.toDouble();
  if (value is String) return double.tryParse(value) ?? 0.0;
  return 0.0;
}

int parseInt(dynamic value) {
  if (value == null) return 0;
  if (value is int) return value;
  if (value is num) return value.toInt();
  if (value is String) return int.tryParse(value) ?? 0;
  return 0;
}

String parseCurrency(String? value) {
  if (value == null || value.isEmpty) return '0';
  return value.replaceAll(RegExp(r'[^\d.]'), '');
}

void main() {
  group('parseDouble', () {
    test('should return 0.0 for null', () {
      expect(parseDouble(null), 0.0);
    });

    test('should convert int to double', () {
      expect(parseDouble(42), 42.0);
    });

    test('should pass through double', () {
      expect(parseDouble(3.14), 3.14);
    });

    test('should parse string to double', () {
      expect(parseDouble('1875.25'), 1875.25);
    });

    test('should return 0.0 for invalid string', () {
      expect(parseDouble('not-a-number'), 0.0);
    });

    test('should handle empty string', () {
      expect(parseDouble(''), 0.0);
    });
  });

  group('parseInt', () {
    test('should return 0 for null', () {
      expect(parseInt(null), 0);
    });

    test('should pass through int', () {
      expect(parseInt(42), 42);
    });

    test('should truncate double', () {
      expect(parseInt(3.9), 3);
    });

    test('should parse string to int', () {
      expect(parseInt('42'), 42);
    });

    test('should return 0 for invalid string', () {
      expect(parseInt('not-a-number'), 0);
    });
  });

  group('parseCurrency', () {
    test('should strip currency symbol', () {
      expect(parseCurrency('\$500.00'), '500.00');
    });

    test('should strip comma separators', () {
      expect(parseCurrency('\$1,500.00'), '1500.00');
    });

    test('should handle null', () {
      expect(parseCurrency(null), '0');
    });

    test('should handle empty string', () {
      expect(parseCurrency(''), '0');
    });

    test('should handle plain numbers', () {
      expect(parseCurrency('500.00'), '500.00');
    });
  });
}
```

---

### 2. Data Layer (Unit Tests)

#### Model Mappers

**Test file:** `test/data/models/mappers/store_mapper_test.dart`

```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:buyer_kiosk_live/data/models/store_model.dart';
import 'package:buyer_kiosk_live/data/models/mappers/store_mapper.dart';

void main() {
  group('StoreModelX.toEntity()', () {
    test('should map all fields correctly', () {
      final model = StoreModel(
        typeNum: 'pc00',
        storeCity: 'Anna',
        storeType: 1,
        numBuyers: 3,
        numSorters: 2,
        numInQueue: 4,
        numBuysToday: 25,
        waitTimeMinutes: 45,
        liveFinancials: LiveFinancialsModel(
          buyGoal: '\$500.00',
          buyCurrent: '\$350.00',
          salesGoal: '\$2000.00',
          salesCurrent: '\$1500.00',
        ),
      );

      final entity = model.toEntity();

      expect(entity.typeNum, 'pc00');
      expect(entity.storeName, 'Anna');
      expect(entity.storeType, 1);
      expect(entity.numBuyers, 3);
      expect(entity.numSorters, 2);
      expect(entity.numInQueue, 4);
      expect(entity.numBuysToday, 25);
      expect(entity.waitTimeMinutes, 45);
      expect(entity.buyGoal, 500.0);
      expect(entity.buyCurrent, 350.0);
      expect(entity.salesGoal, 2000.0);
      expect(entity.salesCurrent, 1500.0);
    });

    test('should handle currency string parsing', () {
      final model = StoreModel(
        typeNum: 'pc00',
        liveFinancials: LiveFinancialsModel(
          buyGoal: '\$1,500.50',  // With comma
          buyCurrent: '750',       // No symbol
          salesGoal: null,         // Null
          salesCurrent: '',        // Empty
        ),
      );

      final entity = model.toEntity();

      expect(entity.buyGoal, 1500.50);
      expect(entity.buyCurrent, 750.0);
      expect(entity.salesGoal, 0.0);
      expect(entity.salesCurrent, 0.0);
    });

    test('should use fallback fields for legacy API', () {
      final model = StoreModel(
        typeNum: 'pc00',
        storeName: 'LegacyName',  // Legacy field
        inQueue: 5,               // Legacy field
        waitTime: 30,             // Legacy field
      );

      final entity = model.toEntity();

      expect(entity.storeName, 'LegacyName');
      expect(entity.numInQueue, 5);
      expect(entity.waitTimeMinutes, 30);
    });
  });
}
```

**Test file:** `test/data/models/mappers/today_performance_mapper_test.dart`

```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:buyer_kiosk_live/data/models/today_performance_model.dart';
import 'package:buyer_kiosk_live/data/models/mappers/today_performance_mapper.dart';

void main() {
  group('TodayPerformanceModelX.toEntity()', () {
    test('should map all KPI sections', () {
      final model = TodayPerformanceModel(
        date: DateTime(2024, 1, 15),
        storeId: 'pc00',
        sales: SalesKpiModel(
          current: 1500.0,
          formatted: '\$1,500.00',
          goal: 2000.0,
          goalFormatted: '\$2,000.00',
          variance: -500.0,
          varianceFormatted: '-\$500.00',
          comp: 1200.0,
          compFormatted: '\$1,200.00',
          status: 'below',
        ),
        // ... other KPIs
      );

      final entity = model.toEntity();

      expect(entity.date, DateTime(2024, 1, 15));
      expect(entity.storeId, 'pc00');
      expect(entity.sales.current, 1500.0);
      expect(entity.sales.goal, 2000.0);
      expect(entity.sales.variance, -500.0);
      expect(entity.sales.status, 'below');
    });

    test('should provide default values for null KPIs', () {
      final model = TodayPerformanceModel(
        date: DateTime(2024, 1, 15),
        storeId: 'pc00',
        sales: null,
        avgTrans: null,
        tradePercent: null,
        salesLabor: null,
        backstock: null,
        buys: null,
      );

      final entity = model.toEntity();

      expect(entity.sales.current, 0.0);
      expect(entity.sales.formatted, '\$0.00');
      expect(entity.avgTrans.current, 0.0);
      expect(entity.backstock.pulled, 0);
      expect(entity.backstock.recentActivity, isEmpty);
    });
  });
}
```

---

#### Repository Error Handling

**Test file:** `test/data/repositories/dashboard_repository_test.dart`

```dart
import 'package:dio/dio.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:buyer_kiosk_live/core/errors/exceptions.dart';
import 'package:buyer_kiosk_live/data/repositories/dashboard_repository_impl.dart';

class MockDio extends Mock implements Dio {}

void main() {
  late MockDio mockDio;
  late DashboardRepositoryImpl repository;

  setUp(() {
    mockDio = MockDio();
    repository = DashboardRepositoryImpl(dio: mockDio);
  });

  setUpAll(() {
    registerFallbackValue(Options());
  });

  group('getDashboard', () {
    test('should throw AuthException on 401', () async {
      when(() => mockDio.post(
        any(),
        options: any(named: 'options'),
      )).thenThrow(DioException(
        type: DioExceptionType.badResponse,
        response: Response(
          statusCode: 401,
          requestOptions: RequestOptions(path: ''),
        ),
        requestOptions: RequestOptions(path: ''),
      ));

      expect(
        () => repository.getDashboard(),
        throwsA(isA<AuthException>()),
      );
    });

    test('should throw AuthException on 403', () async {
      when(() => mockDio.post(
        any(),
        options: any(named: 'options'),
      )).thenThrow(DioException(
        type: DioExceptionType.badResponse,
        response: Response(
          statusCode: 403,
          requestOptions: RequestOptions(path: ''),
        ),
        requestOptions: RequestOptions(path: ''),
      ));

      expect(
        () => repository.getDashboard(),
        throwsA(isA<AuthException>()),
      );
    });

    test('should throw NetworkException on timeout', () async {
      when(() => mockDio.post(
        any(),
        options: any(named: 'options'),
      )).thenThrow(DioException(
        type: DioExceptionType.connectionTimeout,
        requestOptions: RequestOptions(path: ''),
      ));

      expect(
        () => repository.getDashboard(),
        throwsA(isA<NetworkException>()),
      );
    });

    test('should throw ServerException on other errors', () async {
      when(() => mockDio.post(
        any(),
        options: any(named: 'options'),
      )).thenThrow(DioException(
        type: DioExceptionType.unknown,
        message: 'Unknown error',
        requestOptions: RequestOptions(path: ''),
      ));

      expect(
        () => repository.getDashboard(),
        throwsA(isA<ServerException>()),
      );
    });

    test('should parse direct array response', () async {
      when(() => mockDio.post(
        any(),
        options: any(named: 'options'),
      )).thenAnswer((_) async => Response(
        statusCode: 200,
        data: [
          {'typeNum': 'pc00', 'storeCity': 'Anna'},
          {'typeNum': 'pc01', 'storeCity': 'Dallas'},
        ],
        requestOptions: RequestOptions(path: ''),
      ));

      final result = await repository.getDashboard();

      expect(result.length, 2);
      expect(result[0].typeNum, 'pc00');
      expect(result[1].typeNum, 'pc01');
    });

    test('should parse wrapped response', () async {
      when(() => mockDio.post(
        any(),
        options: any(named: 'options'),
      )).thenAnswer((_) async => Response(
        statusCode: 200,
        data: {
          'stores': [
            {'typeNum': 'pc00', 'storeCity': 'Anna'},
          ],
        },
        requestOptions: RequestOptions(path: ''),
      ));

      final result = await repository.getDashboard();

      expect(result.length, 1);
      expect(result[0].typeNum, 'pc00');
    });

    test('should return empty list for null data', () async {
      when(() => mockDio.post(
        any(),
        options: any(named: 'options'),
      )).thenAnswer((_) async => Response(
        statusCode: 200,
        data: null,
        requestOptions: RequestOptions(path: ''),
      ));

      final result = await repository.getDashboard();

      expect(result, isEmpty);
    });
  });
}
```

---

### 3. Domain Layer (Unit Tests)

#### Permission System

**Test file:** `test/core/constants/permission_constants_test.dart`

```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:buyer_kiosk_live/core/constants/permission_constants.dart';

void main() {
  group('AccessLevel', () {
    group('canAccess', () {
      test('owner can access all levels', () {
        expect(AccessLevel.owner.canAccess(AccessLevel.owner), isTrue);
        expect(AccessLevel.owner.canAccess(AccessLevel.manager), isTrue);
        expect(AccessLevel.owner.canAccess(AccessLevel.shiftLead), isTrue);
        expect(AccessLevel.owner.canAccess(AccessLevel.employee), isTrue);
      });

      test('manager can access manager and below', () {
        expect(AccessLevel.manager.canAccess(AccessLevel.owner), isFalse);
        expect(AccessLevel.manager.canAccess(AccessLevel.manager), isTrue);
        expect(AccessLevel.manager.canAccess(AccessLevel.shiftLead), isTrue);
        expect(AccessLevel.manager.canAccess(AccessLevel.employee), isTrue);
      });

      test('shiftLead can access shiftLead and below', () {
        expect(AccessLevel.shiftLead.canAccess(AccessLevel.owner), isFalse);
        expect(AccessLevel.shiftLead.canAccess(AccessLevel.manager), isFalse);
        expect(AccessLevel.shiftLead.canAccess(AccessLevel.shiftLead), isTrue);
        expect(AccessLevel.shiftLead.canAccess(AccessLevel.employee), isTrue);
      });

      test('employee can only access employee level', () {
        expect(AccessLevel.employee.canAccess(AccessLevel.owner), isFalse);
        expect(AccessLevel.employee.canAccess(AccessLevel.manager), isFalse);
        expect(AccessLevel.employee.canAccess(AccessLevel.shiftLead), isFalse);
        expect(AccessLevel.employee.canAccess(AccessLevel.employee), isTrue);
      });
    });

    group('fromRole', () {
      test('should map API role values correctly', () {
        expect(AccessLevel.fromRole(1), AccessLevel.owner);
        expect(AccessLevel.fromRole(2), AccessLevel.manager);
        expect(AccessLevel.fromRole(3), AccessLevel.shiftLead);
        expect(AccessLevel.fromRole(4), AccessLevel.employee);
      });

      test('should default to employee for unknown roles', () {
        expect(AccessLevel.fromRole(0), AccessLevel.employee);
        expect(AccessLevel.fromRole(5), AccessLevel.employee);
        expect(AccessLevel.fromRole(99), AccessLevel.employee);
        expect(AccessLevel.fromRole(-1), AccessLevel.employee);
      });
    });

    group('displayName', () {
      test('should return correct display names', () {
        expect(AccessLevel.owner.displayName, 'Owner');
        expect(AccessLevel.manager.displayName, 'Manager');
        expect(AccessLevel.shiftLead.displayName, 'Shift Lead');
        expect(AccessLevel.employee.displayName, 'Employee');
      });
    });
  });

  group('DefaultPermissions', () {
    test('should return employee for basic pages', () {
      expect(DefaultPermissions.getDefault(AppPage.dashboard), AccessLevel.employee);
      expect(DefaultPermissions.getDefault(AppPage.buyQueue), AccessLevel.employee);
      expect(DefaultPermissions.getDefault(AppPage.todayTasks), AccessLevel.employee);
    });

    test('should return shiftLead for stats pages', () {
      expect(DefaultPermissions.getDefault(AppPage.buyerStats), AccessLevel.shiftLead);
      expect(DefaultPermissions.getDefault(AppPage.storeMetrics), AccessLevel.shiftLead);
      expect(DefaultPermissions.getDefault(AppPage.shiftNotes), AccessLevel.shiftLead);
    });

    test('should return manager for admin pages', () {
      expect(DefaultPermissions.getDefault(AppPage.editTaskList), AccessLevel.manager);
      expect(DefaultPermissions.getDefault(AppPage.backstockAdmin), AccessLevel.manager);
    });

    test('should return owner for settings pages', () {
      expect(DefaultPermissions.getDefault(AppPage.settings), AccessLevel.owner);
      expect(DefaultPermissions.getDefault(AppPage.permissionSettings), AccessLevel.owner);
    });
  });
}
```

---

#### Entity Computed Properties

**Test file:** `test/domain/entities/store_test.dart`

```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:buyer_kiosk_live/domain/entities/store.dart';

void main() {
  group('Store', () {
    group('salesProgress', () {
      test('should return 0 when salesGoal is 0', () {
        final store = Store(
          typeNum: 'pc00',
          storeName: 'Test',
          numBuyers: 0,
          numSorters: 0,
          numInQueue: 0,
          numBuysToday: 0,
          waitTimeMinutes: 0,
          buyGoal: 0,
          buyCurrent: 0,
          salesGoal: 0,
          salesCurrent: 500,
        );

        expect(store.salesProgress, 0.0);
      });

      test('should calculate correct progress', () {
        final store = Store(
          typeNum: 'pc00',
          storeName: 'Test',
          numBuyers: 0,
          numSorters: 0,
          numInQueue: 0,
          numBuysToday: 0,
          waitTimeMinutes: 0,
          buyGoal: 0,
          buyCurrent: 0,
          salesGoal: 1000,
          salesCurrent: 500,
        );

        expect(store.salesProgress, 0.5);
      });

      test('should clamp progress to 1.0 when over goal', () {
        final store = Store(
          typeNum: 'pc00',
          storeName: 'Test',
          numBuyers: 0,
          numSorters: 0,
          numInQueue: 0,
          numBuysToday: 0,
          waitTimeMinutes: 0,
          buyGoal: 0,
          buyCurrent: 0,
          salesGoal: 1000,
          salesCurrent: 1500,
        );

        expect(store.salesProgress, 1.0);
      });
    });

    group('salesPercentage', () {
      test('should return 0 when salesGoal is 0', () {
        final store = Store(
          typeNum: 'pc00',
          storeName: 'Test',
          numBuyers: 0,
          numSorters: 0,
          numInQueue: 0,
          numBuysToday: 0,
          waitTimeMinutes: 0,
          buyGoal: 0,
          buyCurrent: 0,
          salesGoal: 0,
          salesCurrent: 500,
        );

        expect(store.salesPercentage, 0.0);
      });

      test('should calculate percentage without clamping', () {
        final store = Store(
          typeNum: 'pc00',
          storeName: 'Test',
          numBuyers: 0,
          numSorters: 0,
          numInQueue: 0,
          numBuysToday: 0,
          waitTimeMinutes: 0,
          buyGoal: 0,
          buyCurrent: 0,
          salesGoal: 1000,
          salesCurrent: 1500,
        );

        expect(store.salesPercentage, 150.0);
      });
    });

    group('normalizedTypeNum', () {
      test('should map PC80000 to PC00', () {
        final store = Store(
          typeNum: 'PC80000',
          storeName: 'Test',
          numBuyers: 0,
          numSorters: 0,
          numInQueue: 0,
          numBuysToday: 0,
          waitTimeMinutes: 0,
          buyGoal: 0,
          buyCurrent: 0,
          salesGoal: 0,
          salesCurrent: 0,
        );

        expect(store.normalizedTypeNum, 'PC00');
      });

      test('should pass through other typeNums unchanged', () {
        final store = Store(
          typeNum: 'pc01',
          storeName: 'Test',
          numBuyers: 0,
          numSorters: 0,
          numInQueue: 0,
          numBuysToday: 0,
          waitTimeMinutes: 0,
          buyGoal: 0,
          buyCurrent: 0,
          salesGoal: 0,
          salesCurrent: 0,
        );

        expect(store.normalizedTypeNum, 'pc01');
      });
    });
  });
}
```

**Test file:** `test/domain/entities/workbook_note_test.dart`

```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:buyer_kiosk_live/domain/entities/workbook_note.dart';

void main() {
  group('WorkbookNote', () {
    group('isVisible', () {
      test('should return true when within date range', () {
        final note = _createNote(
          startDate: DateTime.now().subtract(const Duration(days: 1)),
          endDate: DateTime.now().add(const Duration(days: 1)),
        );

        expect(note.isVisible, isTrue);
      });

      test('should return false when before start date', () {
        final note = _createNote(
          startDate: DateTime.now().add(const Duration(days: 1)),
          endDate: DateTime.now().add(const Duration(days: 2)),
        );

        expect(note.isVisible, isFalse);
      });

      test('should return false when after end date', () {
        final note = _createNote(
          startDate: DateTime.now().subtract(const Duration(days: 2)),
          endDate: DateTime.now().subtract(const Duration(days: 1)),
        );

        expect(note.isVisible, isFalse);
      });

      test('should return true when no end date and after start', () {
        final note = _createNote(
          startDate: DateTime.now().subtract(const Duration(days: 1)),
          endDate: null,
        );

        expect(note.isVisible, isTrue);
      });
    });

    group('formattedDate', () {
      test('should return "Just now" for recent', () {
        final note = _createNote(
          createdAt: DateTime.now(),
        );

        expect(note.formattedDate, 'Just now');
      });

      test('should return minutes for under an hour', () {
        final note = _createNote(
          createdAt: DateTime.now().subtract(const Duration(minutes: 30)),
        );

        expect(note.formattedDate, '30m ago');
      });

      test('should return hours for under a day', () {
        final note = _createNote(
          createdAt: DateTime.now().subtract(const Duration(hours: 5)),
        );

        expect(note.formattedDate, '5h ago');
      });

      test('should return "Yesterday" for 1 day ago', () {
        final note = _createNote(
          createdAt: DateTime.now().subtract(const Duration(days: 1)),
        );

        expect(note.formattedDate, 'Yesterday');
      });

      test('should return days for under a week', () {
        final note = _createNote(
          createdAt: DateTime.now().subtract(const Duration(days: 3)),
        );

        expect(note.formattedDate, '3d ago');
      });

      test('should return date for over a week', () {
        final note = _createNote(
          createdAt: DateTime(2024, 1, 15),
        );

        expect(note.formattedDate, '1/15/2024');
      });
    });

    group('reaction counts', () {
      test('should return correct like count', () {
        final note = _createNote(
          reactionCounts: {'like': 5, 'heart': 3},
        );

        expect(note.likeCount, 5);
      });

      test('should return 0 for missing reaction type', () {
        final note = _createNote(
          reactionCounts: {'heart': 3},
        );

        expect(note.likeCount, 0);
      });
    });
  });
}

WorkbookNote _createNote({
  DateTime? startDate,
  DateTime? endDate,
  DateTime? createdAt,
  Map<String, int>? reactionCounts,
}) {
  return WorkbookNote(
    id: 1,
    authorName: 'Test Author',
    content: 'Test content',
    startDate: startDate ?? DateTime.now(),
    endDate: endDate,
    isManagerOnly: false,
    isPinned: false,
    reactionCount: 0,
    commentCount: 0,
    userHasReacted: false,
    reactionCounts: reactionCounts ?? {},
    reactionNames: {},
    createdAt: createdAt ?? DateTime.now(),
  );
}
```

**Test file:** `test/domain/entities/workbook_task_item_test.dart`

```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:buyer_kiosk_live/domain/entities/workbook_task_list.dart';

void main() {
  group('WorkbookTaskItem', () {
    group('status checks', () {
      test('isCompleted should be true when status is completed', () {
        final task = _createTask(status: TaskStatus.completed);
        expect(task.isCompleted, isTrue);
        expect(task.isInProgress, isFalse);
        expect(task.isNotStarted, isFalse);
      });

      test('isInProgress should be true when status is inProgress', () {
        final task = _createTask(status: TaskStatus.inProgress);
        expect(task.isCompleted, isFalse);
        expect(task.isInProgress, isTrue);
        expect(task.isNotStarted, isFalse);
      });

      test('isNotStarted should be true when status is notStarted', () {
        final task = _createTask(status: TaskStatus.notStarted);
        expect(task.isCompleted, isFalse);
        expect(task.isInProgress, isFalse);
        expect(task.isNotStarted, isTrue);
      });

      test('isNotStarted should be true when status is null', () {
        final task = _createTask(status: null);
        expect(task.isNotStarted, isTrue);
      });
    });

    group('isCarryover', () {
      test('should be true when carryoverFromList is set', () {
        final task = _createTask(carryoverFromList: 'OPENING');
        expect(task.isCarryover, isTrue);
      });

      test('should be false when carryoverFromList is null', () {
        final task = _createTask(carryoverFromList: null);
        expect(task.isCarryover, isFalse);
      });
    });

    group('completedByName', () {
      test('should return full name when both parts present', () {
        final task = _createTask(
          completedByFirstName: 'John',
          completedByLastName: 'Doe',
        );
        expect(task.completedByName, 'John Doe');
      });

      test('should return first name only when last name missing', () {
        final task = _createTask(
          completedByFirstName: 'John',
          completedByLastName: null,
        );
        expect(task.completedByName, 'John');
      });

      test('should return null when both names missing', () {
        final task = _createTask(
          completedByFirstName: null,
          completedByLastName: null,
        );
        expect(task.completedByName, isNull);
      });
    });

    group('priorityLabel', () {
      test('should return High for priority 1', () {
        final task = _createTask(priority: 1);
        expect(task.priorityLabel, 'High');
      });

      test('should return Normal for priority 2', () {
        final task = _createTask(priority: 2);
        expect(task.priorityLabel, 'Normal');
      });

      test('should return Low for priority 3', () {
        final task = _createTask(priority: 3);
        expect(task.priorityLabel, 'Low');
      });
    });
  });

  group('TaskStatus', () {
    test('fromValue should map correctly', () {
      expect(TaskStatus.fromValue(0), TaskStatus.notStarted);
      expect(TaskStatus.fromValue(1), TaskStatus.inProgress);
      expect(TaskStatus.fromValue(2), TaskStatus.completed);
    });

    test('fromValue should default to notStarted for unknown', () {
      expect(TaskStatus.fromValue(99), TaskStatus.notStarted);
    });

    test('fromValue should return null for null input', () {
      expect(TaskStatus.fromValue(null), isNull);
    });
  });
}

WorkbookTaskItem _createTask({
  TaskStatus? status,
  String? carryoverFromList,
  String? completedByFirstName,
  String? completedByLastName,
  int priority = 2,
}) {
  return WorkbookTaskItem(
    id: 1,
    taskName: 'Test Task',
    taskGroup: 1,
    priority: priority,
    status: status,
    carryoverFromList: carryoverFromList,
    completedByFirstName: completedByFirstName,
    completedByLastName: completedByLastName,
  );
}
```

**Test file:** `test/domain/entities/queue_item_test.dart`

```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:buyer_kiosk_live/domain/entities/queue_item.dart';

void main() {
  group('QueueItem', () {
    group('progressPercentage', () {
      test('should return 0 when numContainers is 0', () {
        final item = QueueItem(
          customerName: 'Test',
          buyerName: 'Buyer',
          numContainers: 0,
          processedContainers: 0,
          waitTime: '45m',
          inStore: true,
        );

        expect(item.progressPercentage, 0.0);
      });

      test('should calculate correct percentage', () {
        final item = QueueItem(
          customerName: 'Test',
          buyerName: 'Buyer',
          numContainers: 10,
          processedContainers: 5,
          waitTime: '45m',
          inStore: true,
        );

        expect(item.progressPercentage, 50.0);
      });
    });

    group('isInProgress', () {
      test('should return true when partially processed', () {
        final item = QueueItem(
          customerName: 'Test',
          buyerName: 'Buyer',
          numContainers: 10,
          processedContainers: 5,
          waitTime: '45m',
          inStore: true,
        );

        expect(item.isInProgress, isTrue);
      });

      test('should return false when not started', () {
        final item = QueueItem(
          customerName: 'Test',
          buyerName: 'Buyer',
          numContainers: 10,
          processedContainers: 0,
          waitTime: '45m',
          inStore: true,
        );

        expect(item.isInProgress, isFalse);
      });

      test('should return false when completed', () {
        final item = QueueItem(
          customerName: 'Test',
          buyerName: 'Buyer',
          numContainers: 10,
          processedContainers: 10,
          waitTime: '45m',
          inStore: true,
        );

        expect(item.isInProgress, isFalse);
      });
    });

    group('isCompleted', () {
      test('should return true when all containers processed', () {
        final item = QueueItem(
          customerName: 'Test',
          buyerName: 'Buyer',
          numContainers: 10,
          processedContainers: 10,
          waitTime: '45m',
          inStore: true,
        );

        expect(item.isCompleted, isTrue);
      });

      test('should return true when over-processed', () {
        final item = QueueItem(
          customerName: 'Test',
          buyerName: 'Buyer',
          numContainers: 10,
          processedContainers: 12,
          waitTime: '45m',
          inStore: true,
        );

        expect(item.isCompleted, isTrue);
      });
    });
  });
}
```

---

### 4. Presentation Layer (Provider Tests)

**Test file:** `test/presentation/providers/dashboard_provider_test.dart`

```dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:buyer_kiosk_live/core/errors/exceptions.dart';
import 'package:buyer_kiosk_live/domain/entities/store.dart';
import 'package:buyer_kiosk_live/domain/repositories/dashboard_repository.dart';
import 'package:buyer_kiosk_live/presentation/providers/dashboard_provider.dart';
import 'package:buyer_kiosk_live/presentation/providers/providers.dart';

class MockDashboardRepository extends Mock implements DashboardRepository {}

void main() {
  late MockDashboardRepository mockRepo;
  late ProviderContainer container;

  setUp(() {
    mockRepo = MockDashboardRepository();
    container = ProviderContainer(
      overrides: [
        dashboardRepositoryProvider.overrideWithValue(mockRepo),
      ],
    );
  });

  tearDown(() {
    container.dispose();
  });

  group('DashboardNotifier', () {
    test('should return stores on successful fetch', () async {
      final stores = [
        Store(
          typeNum: 'pc00',
          storeName: 'Anna',
          numBuyers: 3,
          numSorters: 2,
          numInQueue: 4,
          numBuysToday: 25,
          waitTimeMinutes: 45,
          buyGoal: 500,
          buyCurrent: 350,
          salesGoal: 2000,
          salesCurrent: 1500,
        ),
      ];

      when(() => mockRepo.getDashboard()).thenAnswer((_) async => stores);

      final result = await container.read(dashboardProvider.future);

      expect(result, stores);
      expect(result.length, 1);
      expect(result[0].typeNum, 'pc00');
    });

    test('should throw on auth error', () async {
      when(() => mockRepo.getDashboard())
          .thenThrow(const AuthException('Invalid API key'));

      expect(
        () => container.read(dashboardProvider.future),
        throwsA(isA<AuthException>()),
      );
    });

    test('should cache auth errors for 30 seconds', () async {
      when(() => mockRepo.getDashboard())
          .thenThrow(const AuthException('Invalid API key'));

      // First call - throws
      await expectLater(
        container.read(dashboardProvider.future),
        throwsA(isA<AuthException>()),
      );

      // Second call within 30s - should throw cached error without calling repo
      reset(mockRepo);

      await expectLater(
        container.read(dashboardProvider.future),
        throwsA(isA<AuthException>()),
      );

      // Verify repo was not called second time
      verifyNever(() => mockRepo.getDashboard());
    });

    test('refresh should clear auth error cache', () async {
      when(() => mockRepo.getDashboard())
          .thenThrow(const AuthException('Invalid API key'));

      // First call - throws
      await expectLater(
        container.read(dashboardProvider.future),
        throwsA(isA<AuthException>()),
      );

      // Setup success response
      reset(mockRepo);
      final stores = [
        Store(
          typeNum: 'pc00',
          storeName: 'Anna',
          numBuyers: 0,
          numSorters: 0,
          numInQueue: 0,
          numBuysToday: 0,
          waitTimeMinutes: 0,
          buyGoal: 0,
          buyCurrent: 0,
          salesGoal: 0,
          salesCurrent: 0,
        ),
      ];
      when(() => mockRepo.getDashboard()).thenAnswer((_) async => stores);

      // Explicit refresh should clear cache and call repo
      await container.read(dashboardProvider.notifier).refresh();

      verify(() => mockRepo.getDashboard()).called(1);
    });
  });
}
```

**Test file:** `test/presentation/providers/permission_provider_test.dart`

```dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:buyer_kiosk_live/core/constants/permission_constants.dart';
import 'package:buyer_kiosk_live/presentation/providers/permission_provider.dart';
import 'package:buyer_kiosk_live/presentation/providers/providers.dart';
import 'package:buyer_kiosk_live/data/datasources/secure_storage_datasource.dart';

class MockSecureStorageDataSource extends Mock implements SecureStorageDataSource {}

void main() {
  late MockSecureStorageDataSource mockStorage;
  late ProviderContainer container;

  setUp(() {
    mockStorage = MockSecureStorageDataSource();
    container = ProviderContainer(
      overrides: [
        secureStorageDataSourceProvider.overrideWithValue(mockStorage),
      ],
    );
  });

  tearDown(() {
    container.dispose();
  });

  group('PermissionState', () {
    test('canAccess should evaluate correctly for owner', () {
      final state = PermissionState(
        userAccessLevel: AccessLevel.owner,
        pagePermissions: {
          AppPage.settings: AccessLevel.owner,
          AppPage.buyerStats: AccessLevel.shiftLead,
        },
        isLoading: false,
      );

      expect(state.canAccess(AppPage.settings), isTrue);
      expect(state.canAccess(AppPage.buyerStats), isTrue);
      expect(state.canAccess(AppPage.dashboard), isTrue);
    });

    test('canAccess should evaluate correctly for employee', () {
      final state = PermissionState(
        userAccessLevel: AccessLevel.employee,
        pagePermissions: {
          AppPage.settings: AccessLevel.owner,
          AppPage.buyerStats: AccessLevel.shiftLead,
          AppPage.dashboard: AccessLevel.employee,
        },
        isLoading: false,
      );

      // In debug mode this returns true due to bypass
      // In release mode these would be:
      // expect(state.canAccess(AppPage.settings), isFalse);
      // expect(state.canAccess(AppPage.buyerStats), isFalse);
      // expect(state.canAccess(AppPage.dashboard), isTrue);
    });

    test('isOwner should return true only for owner level', () {
      final ownerState = PermissionState(
        userAccessLevel: AccessLevel.owner,
        isLoading: false,
      );
      final managerState = PermissionState(
        userAccessLevel: AccessLevel.manager,
        isLoading: false,
      );

      expect(ownerState.isOwner, isTrue);
      expect(managerState.isOwner, isFalse);
    });

    test('isManager should return true for manager and above', () {
      expect(
        PermissionState(userAccessLevel: AccessLevel.owner, isLoading: false).isManager,
        isTrue,
      );
      expect(
        PermissionState(userAccessLevel: AccessLevel.manager, isLoading: false).isManager,
        isTrue,
      );
      expect(
        PermissionState(userAccessLevel: AccessLevel.shiftLead, isLoading: false).isManager,
        isFalse,
      );
    });
  });

  group('PermissionNotifier', () {
    test('should load permissions from storage', () async {
      when(() => mockStorage.getAccessLevel()).thenAnswer((_) async => 1); // Owner
      when(() => mockStorage.getPermissionConfig()).thenAnswer((_) async => {});

      final state = container.read(permissionProvider);

      // Initial state is loading
      expect(state.isLoading, isTrue);

      // Wait for async load
      await Future.delayed(const Duration(milliseconds: 100));

      final loadedState = container.read(permissionProvider);
      expect(loadedState.userAccessLevel, AccessLevel.owner);
      expect(loadedState.isLoading, isFalse);
    });

    test('updatePagePermission should fail for non-owner', () async {
      when(() => mockStorage.getAccessLevel()).thenAnswer((_) async => 4); // Employee
      when(() => mockStorage.getPermissionConfig()).thenAnswer((_) async => {});

      await Future.delayed(const Duration(milliseconds: 100));

      final result = await container
          .read(permissionProvider.notifier)
          .updatePagePermission(AppPage.buyerStats, AccessLevel.employee);

      expect(result, isFalse);
    });
  });
}
```

---

### 5. Widget Tests

**Test file:** `test/presentation/widgets/common/error_widget_test.dart`

```dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:buyer_kiosk_live/presentation/widgets/common/error_widget.dart';

void main() {
  group('getErrorMessage', () {
    test('should return invalid API key message for AuthException', () {
      final message = getErrorMessage(Exception('AuthException: Invalid'));
      expect(message, contains('Invalid API key'));
    });

    test('should return network message for connection errors', () {
      final message = getErrorMessage(Exception('Network error'));
      expect(message, contains('internet connection'));
    });

    test('should return timeout message for timeout errors', () {
      final message = getErrorMessage(Exception('Request timeout'));
      expect(message, contains('took too long'));
    });

    test('should return 403 as invalid API key', () {
      final message = getErrorMessage(Exception('403 Forbidden'));
      expect(message, contains('Invalid API key'));
    });

    test('should return server error for 500', () {
      final message = getErrorMessage(Exception('500 Server Error'));
      expect(message, contains('server error'));
    });

    test('should return generic message for unknown errors', () {
      final message = getErrorMessage(Exception('Something weird'));
      expect(message, contains('unexpected error'));
    });
  });

  group('ErrorDisplay', () {
    testWidgets('should display error message', (tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: Scaffold(
            body: ErrorDisplay(message: 'Test error message'),
          ),
        ),
      );

      expect(find.text('Test error message'), findsOneWidget);
      expect(find.text('Oops! Something went wrong'), findsOneWidget);
    });

    testWidgets('should show retry button when callback provided', (tester) async {
      bool retryPressed = false;

      await tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: ErrorDisplay(
              message: 'Test error',
              onRetry: () => retryPressed = true,
            ),
          ),
        ),
      );

      expect(find.text('Retry'), findsOneWidget);

      await tester.tap(find.text('Retry'));
      await tester.pump();

      expect(retryPressed, isTrue);
    });

    testWidgets('should hide retry button when no callback', (tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: Scaffold(
            body: ErrorDisplay(message: 'Test error'),
          ),
        ),
      );

      expect(find.text('Retry'), findsNothing);
    });

    testWidgets('should display custom retry button text', (tester) async {
      await tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: ErrorDisplay(
              message: 'Test error',
              onRetry: () {},
              retryButtonText: 'Try Again',
            ),
          ),
        ),
      );

      expect(find.text('Try Again'), findsOneWidget);
    });

    testWidgets('fromError factory should convert errors', (tester) async {
      await tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: ErrorDisplay.fromError(
              error: Exception('403 Forbidden'),
            ),
          ),
        ),
      );

      expect(find.textContaining('Invalid API key'), findsOneWidget);
    });
  });
}
```

---

## Recommended Test Structure

```
test/
├── core/
│   ├── network/
│   │   ├── api_client_test.dart
│   │   └── api_interceptors_test.dart
│   ├── constants/
│   │   └── permission_constants_test.dart
│   └── utils/
│       └── json_parsing_test.dart
├── data/
│   ├── models/
│   │   └── mappers/
│   │       ├── store_mapper_test.dart
│   │       ├── today_performance_mapper_test.dart
│   │       └── workbook_note_mapper_test.dart
│   └── repositories/
│       ├── dashboard_repository_test.dart
│       ├── auth_repository_test.dart
│       └── store_repository_test.dart
├── domain/
│   └── entities/
│       ├── store_test.dart
│       ├── workbook_note_test.dart
│       ├── workbook_task_item_test.dart
│       └── queue_item_test.dart
├── presentation/
│   ├── providers/
│   │   ├── dashboard_provider_test.dart
│   │   ├── permission_provider_test.dart
│   │   └── auth_provider_test.dart
│   └── widgets/
│       └── common/
│           ├── error_widget_test.dart
│           ├── empty_state_test.dart
│           └── loading_indicator_test.dart
├── integration_test/
│   ├── auth_flow_test.dart
│   └── dashboard_flow_test.dart
└── fixtures/
    ├── store_response.json
    ├── dashboard_response.json
    └── today_performance_response.json
```

---

## Testing Dependencies

### Already Configured in `pubspec.yaml`
```yaml
dev_dependencies:
  flutter_test:
    sdk: flutter
  mocktail: ^1.0.3
```

### Recommended Additions
```yaml
dev_dependencies:
  # ... existing

  # Golden tests for UI snapshots
  golden_toolkit: ^0.15.0

  # Integration tests
  integration_test:
    sdk: flutter

  # Network mocking for integration tests
  http_mock_adapter: ^0.6.1
```

---

## Priority Implementation Order

### Phase 1: Foundation (Week 1)
1. **JSON parsing helpers** - Quick wins, high value
2. **Permission constants** - Critical security logic
3. **Entity computed properties** - Business logic validation

### Phase 2: Core Infrastructure (Week 2)
4. **API Client error handling** - Reliability foundation
5. **Auth Interceptor** - Security critical
6. **Repository error handling** - Error propagation

### Phase 3: State Management (Week 3)
7. **Dashboard Provider** - Most used provider
8. **Permission Provider** - Access control
9. **Auth Provider** - Authentication flow

### Phase 4: UI Layer (Week 4)
10. **ErrorDisplay widget** - Error presentation
11. **EmptyState widget** - Empty state handling
12. **LoadingIndicator widget** - Loading states

### Phase 5: Integration (Week 5+)
13. **Auth flow integration test** - End-to-end authentication
14. **Dashboard flow integration test** - Main user journey
15. **Store detail flow integration test** - Secondary journey

---

## Test Patterns Reference

### Unit Test Pattern (with Mocktail)

```dart
import 'package:mocktail/mocktail.dart';
import 'package:flutter_test/flutter_test.dart';

// 1. Create mock class
class MockDio extends Mock implements Dio {}

void main() {
  // 2. Declare dependencies
  late MockDio mockDio;
  late ApiClient sut; // System Under Test

  // 3. Register fallback values for complex types
  setUpAll(() {
    registerFallbackValue(RequestOptions(path: ''));
    registerFallbackValue(Options());
  });

  // 4. Setup fresh mocks before each test
  setUp(() {
    mockDio = MockDio();
    sut = ApiClient(dio: mockDio);
  });

  group('ApiClient', () {
    test('should throw AuthException on 403', () async {
      // Arrange
      when(() => mockDio.post(any(), data: any(named: 'data')))
          .thenThrow(DioException(
            type: DioExceptionType.badResponse,
            response: Response(statusCode: 403, requestOptions: RequestOptions(path: '')),
            requestOptions: RequestOptions(path: ''),
          ));

      // Act & Assert
      expect(
        () => sut.post('/endpoint'),
        throwsA(isA<AuthException>()),
      );
    });
  });
}
```

### Provider Test Pattern

```dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

class MockRepository extends Mock implements MyRepository {}

void main() {
  late MockRepository mockRepo;
  late ProviderContainer container;

  setUp(() {
    mockRepo = MockRepository();
    container = ProviderContainer(
      overrides: [
        myRepositoryProvider.overrideWithValue(mockRepo),
      ],
    );
  });

  tearDown(() {
    container.dispose();
  });

  test('should load data successfully', () async {
    // Arrange
    when(() => mockRepo.getData()).thenAnswer((_) async => ['item1']);

    // Act
    final result = await container.read(myProvider.future);

    // Assert
    expect(result, ['item1']);
    verify(() => mockRepo.getData()).called(1);
  });

  test('should handle errors', () async {
    // Arrange
    when(() => mockRepo.getData()).thenThrow(ServerException('Error'));

    // Act & Assert
    await expectLater(
      container.read(myProvider.future),
      throwsA(isA<ServerException>()),
    );
  });
}
```

### Widget Test Pattern

```dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mocktail/mocktail.dart';

void main() {
  testWidgets('should display loading state', (tester) async {
    await tester.pumpWidget(
      ProviderScope(
        overrides: [
          myProvider.overrideWith((ref) => AsyncLoading()),
        ],
        child: const MaterialApp(
          home: MyScreen(),
        ),
      ),
    );

    expect(find.byType(CircularProgressIndicator), findsOneWidget);
  });

  testWidgets('should display data', (tester) async {
    await tester.pumpWidget(
      ProviderScope(
        overrides: [
          myProvider.overrideWith((ref) => AsyncData(['item1'])),
        ],
        child: const MaterialApp(
          home: MyScreen(),
        ),
      ),
    );

    expect(find.text('item1'), findsOneWidget);
  });

  testWidgets('should handle user interaction', (tester) async {
    bool buttonPressed = false;

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: ElevatedButton(
            onPressed: () => buttonPressed = true,
            child: const Text('Press Me'),
          ),
        ),
      ),
    );

    await tester.tap(find.text('Press Me'));
    await tester.pump();

    expect(buttonPressed, isTrue);
  });
}
```

### Integration Test Pattern

```dart
// integration_test/auth_flow_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:buyer_kiosk_live/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('Auth Flow', () {
    testWidgets('should redirect to install screen when no API key', (tester) async {
      app.main();
      await tester.pumpAndSettle();

      // Should be on installation screen
      expect(find.text('Enter API Key'), findsOneWidget);
    });

    testWidgets('should navigate to dashboard after valid API key', (tester) async {
      app.main();
      await tester.pumpAndSettle();

      // Enter API key
      await tester.enterText(find.byType(TextField), 'valid-api-key');
      await tester.tap(find.text('Submit'));
      await tester.pumpAndSettle();

      // Should be on dashboard
      expect(find.text('Dashboard'), findsOneWidget);
    });
  });
}
```

---

## CI/CD Integration

### GitHub Actions Workflow

```yaml
# .github/workflows/test.yml
name: Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.38.3'

      - name: Install dependencies
        run: flutter pub get

      - name: Generate code
        run: dart run build_runner build --delete-conflicting-outputs

      - name: Analyze
        run: flutter analyze

      - name: Run unit tests
        run: flutter test --coverage

      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          file: coverage/lcov.info
```

---

## Coverage Goals

| Category | Current | Target (3 months) | Target (6 months) |
|----------|---------|-------------------|-------------------|
| Core (network, utils) | 0% | 80% | 90% |
| Data (models, repos) | 0% | 70% | 85% |
| Domain (entities) | 0% | 90% | 95% |
| Presentation (providers) | 0% | 60% | 80% |
| Widgets | 0% | 40% | 60% |
| **Overall** | **~0%** | **~60%** | **~75%** |

---

## Key Takeaways

1. **Start with pure functions** - JSON parsing, computed properties, permission logic are easiest to test and highest value
2. **Mock external dependencies** - Dio, SharedPreferences, SecureStorage should always be mocked
3. **Test error paths** - Error handling is where bugs hide; test all exception types
4. **Use provider overrides** - Riverpod's override system makes provider testing straightforward
5. **Avoid testing implementation details** - Test behavior, not internal state
6. **Keep tests fast** - Avoid real network calls, file I/O, or delays
7. **Name tests clearly** - `should return X when Y` pattern makes failures obvious
