import 'package:drift/drift.dart' hide isNull, isNotNull; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:kooltap/database/app_database.dart'; import 'package:kooltap/models/product.dart'; import 'package:kooltap/services/product_service.dart'; void main() { late AppDatabase database; late DriftProductService service; setUp(() { database = AppDatabase(NativeDatabase.memory()); service = DriftProductService(database: database); }); tearDown(() async { await database.close(); }); group('DriftProductService', () { group('getProducts', () { test('returns empty list when no products', () async { final products = await service.getProducts(); expect(products, isEmpty); }); test('returns only active products', () async { await database .into(database.products) .insert( ProductsCompanion.insert( id: '1', name: 'Active Product', category: 'Drinks', stockQuantity: 10, lowStockThreshold: 2, priceInCents: 500, ), ); await database .into(database.products) .insert( ProductsCompanion.insert( id: '2', name: 'Inactive Product', category: 'Drinks', stockQuantity: 5, lowStockThreshold: 1, priceInCents: 300, active: const Value(false), ), ); final products = await service.getProducts(); expect(products.length, 1); expect(products.first.name, 'Active Product'); }); test('returns products sorted by name', () async { await database .into(database.products) .insert( ProductsCompanion.insert( id: '1', name: 'Zebra', category: 'Drinks', stockQuantity: 10, lowStockThreshold: 2, priceInCents: 500, ), ); await database .into(database.products) .insert( ProductsCompanion.insert( id: '2', name: 'Apple', category: 'Drinks', stockQuantity: 5, lowStockThreshold: 1, priceInCents: 300, ), ); final products = await service.getProducts(); expect(products.first.name, 'Apple'); expect(products.last.name, 'Zebra'); }); }); group('getProductById', () { test('returns product when exists', () async { await database .into(database.products) .insert( ProductsCompanion.insert( id: 'prod-123', name: 'Test Product', category: 'Drinks', stockQuantity: 10, lowStockThreshold: 2, priceInCents: 500, ), ); final product = await service.getProductById('prod-123'); expect(product, isNotNull); expect(product!.name, 'Test Product'); }); test('returns null when not exists', () async { final product = await service.getProductById('nonexistent'); expect(product, isNull); }); }); group('createProduct', () { test('creates product successfully', () async { await service.createProduct( name: 'New Product', category: 'Food', stockQuantity: 20, lowStockThreshold: 5, priceInCents: 850, imagePath: null, ); final products = await service.getProducts(); expect(products.length, 1); expect(products.first.name, 'New Product'); expect(products.first.stockQuantity, 20); }); }); group('updateProduct', () { test('updates product fields', () async { await database .into(database.products) .insert( ProductsCompanion.insert( id: 'update-test', name: 'Original', category: 'Drinks', stockQuantity: 10, lowStockThreshold: 2, priceInCents: 500, ), ); await service.updateProduct( const Product( id: 'update-test', name: 'Updated', category: 'Food', stockQuantity: 15, lowStockThreshold: 3, priceInCents: 750, active: true, ), ); final product = await service.getProductById('update-test'); expect(product!.name, 'Updated'); expect(product.stockQuantity, 15); expect(product.priceInCents, 750); }); }); group('deleteProduct', () { test('hides the product but keeps its row', () async { await database .into(database.products) .insert( ProductsCompanion.insert( id: 'delete-test', name: 'To Delete', category: 'Drinks', stockQuantity: 10, lowStockThreshold: 2, priceInCents: 500, ), ); await service.deleteProduct('delete-test'); expect(await service.getProducts(), isEmpty); final row = await service.getProductById('delete-test'); expect(row!.active, isFalse); await service.increaseStock('delete-test', 1); }); }); group('decreaseStock', () { test('decreases stock quantity', () async { await database .into(database.products) .insert( ProductsCompanion.insert( id: 'stock-test', name: 'Stock Test', category: 'Drinks', stockQuantity: 10, lowStockThreshold: 2, priceInCents: 500, ), ); await service.decreaseStock('stock-test', 3); final product = await service.getProductById('stock-test'); expect(product!.stockQuantity, 7); }); test('throws when product not found', () async { expect( () => service.decreaseStock('nonexistent', 1), throwsA(isA()), ); }); test('goes below zero instead of refusing', () async { await database .into(database.products) .insert( ProductsCompanion.insert( id: 'low-stock', name: 'Low Stock', category: 'Drinks', stockQuantity: 3, lowStockThreshold: 1, priceInCents: 500, ), ); await service.decreaseStock('low-stock', 5); final product = await service.getProductById('low-stock'); expect(product!.stockQuantity, -2); }); }); group('increaseStock', () { test('increases stock quantity', () async { await database .into(database.products) .insert( ProductsCompanion.insert( id: 'increase-test', name: 'Increase Test', category: 'Drinks', stockQuantity: 10, lowStockThreshold: 2, priceInCents: 500, ), ); await service.increaseStock('increase-test', 5); final product = await service.getProductById('increase-test'); expect(product!.stockQuantity, 15); }); test('throws when product not found', () async { expect( () => service.increaseStock('nonexistent', 1), throwsA(isA()), ); }); test('rejects non-positive stock adjustments', () async { await database .into(database.products) .insert( ProductsCompanion.insert( id: 'invalid-adjustment', name: 'Invalid Adjustment', category: 'Drinks', stockQuantity: 10, lowStockThreshold: 2, priceInCents: 500, ), ); expect( () => service.decreaseStock('invalid-adjustment', 0), throwsA(isA()), ); expect( () => service.increaseStock('invalid-adjustment', -1), throwsA(isA()), ); }); test('counts every concurrent decrement', () async { await database .into(database.products) .insert( ProductsCompanion.insert( id: 'concurrent-stock', name: 'Concurrent Stock', category: 'Drinks', stockQuantity: 1, lowStockThreshold: 0, priceInCents: 500, ), ); await Future.wait([ service.decreaseStock('concurrent-stock', 1), service.decreaseStock('concurrent-stock', 1), ]); final product = await service.getProductById('concurrent-stock'); expect(product!.stockQuantity, -1); }); }); group('infinite stock', () { setUp(() async { await database .into(database.products) .insert( ProductsCompanion.insert( id: 'water', name: 'Water', category: 'Drinks', stockQuantity: 0, lowStockThreshold: 0, infiniteStock: const Value(true), priceInCents: 100, ), ); }); test('sells without stock and leaves the count alone', () async { await service.decreaseStock('water', 5); await service.increaseStock('water', 2); final product = await service.getProductById('water'); expect(product!.stockQuantity, 0); expect(product.infiniteStock, true); expect(product.isOutOfStock, false); expect(product.isLowStock, false); }); test('still reports a missing product', () async { await expectLater( service.increaseStock('nonexistent', 1), throwsA(isA()), ); }); test('is saved by create and update', () async { await service.createProduct( name: 'Tap water', category: 'Drinks', stockQuantity: 0, lowStockThreshold: 0, infiniteStock: true, priceInCents: 0, imagePath: null, ); final created = (await service.getProducts()).firstWhere( (product) => product.name == 'Tap water', ); expect(created.infiniteStock, true); await service.updateProduct( created.copyWith(infiniteStock: false, stockQuantity: 4), ); final updated = await service.getProductById(created.id); expect(updated!.infiniteStock, false); expect(updated.stockQuantity, 4); }); }); }); }