import 'package:drift/drift.dart' hide isNull, isNotNull; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:kooltab2/database/app_database.dart'; import 'package:kooltab2/models/product.dart'; import 'package:kooltab2/services/bar_tab_service.dart'; import 'package:kooltab2/services/product_service.dart'; void main() { late AppDatabase database; late DriftBarTabService service; setUp(() async { database = AppDatabase(NativeDatabase.memory()); service = DriftBarTabService(database: database); await database .into(database.products) .insert( ProductsCompanion.insert( id: 'prod-1', name: 'Test Beer', category: 'Drinks', stockQuantity: 100, lowStockThreshold: 10, priceInCents: 400, ), ); }); tearDown(() async { await database.close(); }); Product createTestProduct({ String id = 'prod-1', String name = 'Test Beer', int priceInCents = 400, }) { return Product( id: id, name: name, category: 'Drinks', stockQuantity: 100, lowStockThreshold: 10, priceInCents: priceInCents, ); } group('DriftBarTabService', () { group('createTab', () { test('creates tab with open status', () async { final tab = await service.createTab(customerName: 'John'); expect(tab.customerName, 'John'); expect(tab.status, 'open'); expect(tab.items, isEmpty); }); test('creates tab with unique id', () async { final tab1 = await service.createTab(customerName: 'John'); final tab2 = await service.createTab(customerName: 'Jane'); expect(tab1.id, isNot(equals(tab2.id))); }); }); group('getOpenTabs', () { test('returns empty list when no tabs', () async { final tabs = await service.getOpenTabs(); expect(tabs, isEmpty); }); test('returns only open tabs', () async { await service.createTab(customerName: 'Open Tab'); await database .into(database.barTabs) .insert( BarTabsCompanion.insert( id: 'closed-tab', customerName: 'Closed Tab', status: const Value('closed'), openedAt: DateTime.now(), ), ); final tabs = await service.getOpenTabs(); expect(tabs.length, 1); expect(tabs.first.customerName, 'Open Tab'); }); test('tabs sorted by openedAt descending', () async { final now = DateTime.now(); await database .into(database.barTabs) .insert( BarTabsCompanion.insert( id: 'tab-first', customerName: 'First', status: const Value('open'), openedAt: now.subtract(const Duration(hours: 1)), ), ); await database .into(database.barTabs) .insert( BarTabsCompanion.insert( id: 'tab-second', customerName: 'Second', status: const Value('open'), openedAt: now, ), ); final tabs = await service.getOpenTabs(); expect(tabs.first.customerName, 'Second'); expect(tabs.last.customerName, 'First'); }); }); group('getTabById', () { test('returns tab when exists', () async { final created = await service.createTab(customerName: 'Test'); final tab = await service.getTabById(created.id); expect(tab, isNotNull); expect(tab!.customerName, 'Test'); }); test('returns null when not exists', () async { final tab = await service.getTabById('nonexistent'); expect(tab, isNull); }); }); group('renameTab', () { test('updates the tab customer name', () async { final tab = await service.createTab(customerName: 'John'); await service.renameTab(tabId: tab.id, customerName: ' Jane '); final updatedTab = await service.getTabById(tab.id); expect(updatedTab!.customerName, 'Jane'); }); }); group('deleteTab', () { test('deletes the tab and restores item stock', () async { final tab = await service.createTab(customerName: 'John'); final product = createTestProduct(); await service.addProductToTab(tabId: tab.id, product: product); await service.addProductToTab(tabId: tab.id, product: product); final deletedItems = await service.deleteTab(tab.id); expect(deletedItems.length, 1); expect(deletedItems.first.quantity, 2); expect(await service.getTabById(tab.id), isNull); final updatedProduct = await (database.select( database.products, )..where((row) => row.id.equals(product.id))).getSingle(); expect(updatedProduct.stockQuantity, 100); final remainingItems = await database.select(database.tabItems).get(); expect(remainingItems, isEmpty); }); }); group('addProductToTab', () { test('adds new product to tab', () async { final tab = await service.createTab(customerName: 'John'); final product = createTestProduct(); await service.addProductToTab(tabId: tab.id, product: product); final updatedTab = await service.getTabById(tab.id); expect(updatedTab!.items.length, 1); expect(updatedTab.items.first.productName, 'Test Beer'); expect(updatedTab.items.first.quantity, 1); }); test('increments quantity when product already in tab', () async { final tab = await service.createTab(customerName: 'John'); final product = createTestProduct(); await service.addProductToTab(tabId: tab.id, product: product); await service.addProductToTab(tabId: tab.id, product: product); await service.addProductToTab(tabId: tab.id, product: product); final updatedTab = await service.getTabById(tab.id); expect(updatedTab!.items.length, 1); expect(updatedTab.items.first.quantity, 3); }); test('records each addition as a separate purchase event', () async { final tab = await service.createTab(customerName: 'John'); final product = createTestProduct(); await service.addProductToTab(tabId: tab.id, product: product); await service.addProductToTab(tabId: tab.id, product: product); final barTab = (await service.getOpenTabs()).first; final historyTab = (await service.getOpenTabs( includeRemoved: true, )).first; expect(barTab.items.first.quantity, 2); expect(barTab.items.first.purchases, hasLength(2)); expect(historyTab.items.first.purchases, hasLength(2)); expect( historyTab.items.first.purchases.every( (purchase) => !purchase.isRemoved, ), isTrue, ); }); test('adding product calculates correct line total', () async { final tab = await service.createTab(customerName: 'John'); final product = createTestProduct(priceInCents: 450); await service.addProductToTab(tabId: tab.id, product: product); await service.addProductToTab(tabId: tab.id, product: product); final updatedTab = await service.getTabById(tab.id); expect(updatedTab!.items.first.lineTotalInCents, 900); }); test('decreases stock as part of adding the item', () async { final tab = await service.createTab(customerName: 'John'); final product = createTestProduct(); await service.addProductToTab(tabId: tab.id, product: product); final storedProduct = await (database.select( database.products, )..where((row) => row.id.equals(product.id))).getSingle(); expect(storedProduct.stockQuantity, 99); }); test('rolls back the tab insert when stock is unavailable', () async { final tab = await service.createTab(customerName: 'John'); final product = createTestProduct(); await (database.update(database.products) ..where((row) => row.id.equals(product.id))) .write(const ProductsCompanion(stockQuantity: Value(0))); await expectLater( service.addProductToTab(tabId: tab.id, product: product), throwsA(isA()), ); final updatedTab = await service.getTabById(tab.id); expect(updatedTab!.items, isEmpty); }); }); group('adjustTabItemQuantity', () { test('updates item quantity', () async { final tab = await service.createTab(customerName: 'John'); final product = createTestProduct(); await service.addProductToTab(tabId: tab.id, product: product); final tabItemId = (await service.getTabById(tab.id))!.items.first.id; await service.adjustTabItemQuantity(tabItemId: tabItemId, delta: 4); final updatedTab = await service.getTabById(tab.id); expect(updatedTab!.items.first.quantity, 5); }); test('hides item when the delta removes all quantity', () async { final tab = await service.createTab(customerName: 'John'); final product = createTestProduct(); await service.addProductToTab(tabId: tab.id, product: product); final tabItemId = (await service.getTabById(tab.id))!.items.first.id; await service.adjustTabItemQuantity(tabItemId: tabItemId, delta: -1); final updatedTab = await service.getTabById(tab.id); expect(updatedTab!.items, isEmpty); }); test('restores stock when quantity decreases', () async { final tab = await service.createTab(customerName: 'John'); final product = createTestProduct(); await service.addProductToTab(tabId: tab.id, product: product); final tabItemId = (await service.getTabById(tab.id))!.items.first.id; await service.adjustTabItemQuantity(tabItemId: tabItemId, delta: -1); final updatedTab = await service.getTabById(tab.id); expect(updatedTab!.items, isEmpty); final updatedProduct = await (database.select( database.products, )..where((row) => row.id.equals(product.id))).getSingle(); expect(updatedProduct.stockQuantity, 100); }); test('keeps a removed purchase in the history audit trail', () async { final tab = await service.createTab(customerName: 'John'); final product = createTestProduct(); await service.addProductToTab(tabId: tab.id, product: product); await service.addProductToTab(tabId: tab.id, product: product); final tabItemId = (await service.getTabById(tab.id))!.items.first.id; await service.adjustTabItemQuantity(tabItemId: tabItemId, delta: -1); final historyTab = (await service.getOpenTabs( includeRemoved: true, )).first; final item = historyTab.items.first; expect(item.quantity, 1); expect( item.purchases.where((purchase) => purchase.isRemoved), hasLength(1), ); expect( item.purchases.where((purchase) => !purchase.isRemoved), hasLength(1), ); }); test( 'applies concurrent deltas without losing stock consistency', () async { final tab = await service.createTab(customerName: 'John'); final product = createTestProduct(); await service.addProductToTab(tabId: tab.id, product: product); final tabItemId = (await service.getTabById(tab.id))!.items.first.id; final deltas = await Future.wait([ service.adjustTabItemQuantity(tabItemId: tabItemId, delta: 1), service.adjustTabItemQuantity(tabItemId: tabItemId, delta: 1), ]); expect(deltas, [1, 1]); final updatedTab = await service.getTabById(tab.id); expect(updatedTab!.items.first.quantity, 3); final updatedProduct = await (database.select( database.products, )..where((row) => row.id.equals(product.id))).getSingle(); expect(updatedProduct.stockQuantity, 97); }, ); }); group('closeTab', () { test('archives tab items to closed tabs', () async { final tab = await service.createTab(customerName: 'John'); final product = createTestProduct(); await service.addProductToTab(tabId: tab.id, product: product); await service.addProductToTab(tabId: tab.id, product: product); await service.closeTab(tab.id); final closedTabs = await service.getClosedTabs(); expect(closedTabs.length, 1); expect(closedTabs.first.customerName, 'John'); expect(closedTabs.first.items.length, 2); expect( closedTabs.first.items.every((item) => item.quantity == 1), true, ); expect( closedTabs.first.items.every((item) => item.purchasedAt != null), true, ); }); test( 'keeps removed purchases visible but excludes them from totals', () async { final tab = await service.createTab(customerName: 'John'); final product = createTestProduct(); await service.addProductToTab(tabId: tab.id, product: product); await service.addProductToTab(tabId: tab.id, product: product); final tabItemId = (await service.getTabById(tab.id))!.items.first.id; await service.adjustTabItemQuantity(tabItemId: tabItemId, delta: -1); await service.closeTab(tab.id); final closedTab = (await service.getClosedTabs()).first; expect(closedTab.items, hasLength(2)); expect(closedTab.items.where((item) => item.isRemoved), hasLength(1)); expect(closedTab.itemCount, 1); expect(closedTab.totalInCents, 400); }, ); test('clears tab items after closing', () async { final tab = await service.createTab(customerName: 'John'); final product = createTestProduct(); await service.addProductToTab(tabId: tab.id, product: product); await service.closeTab(tab.id); final updatedTab = await service.getTabById(tab.id); expect(updatedTab!.items, isEmpty); }); test('tab remains open but items cleared', () async { final tab = await service.createTab(customerName: 'John'); final product = createTestProduct(); await service.addProductToTab(tabId: tab.id, product: product); await service.closeTab(tab.id); final updatedTab = await service.getTabById(tab.id); expect(updatedTab!.isOpen, true); expect(updatedTab.items, isEmpty); }); test('closed tab preserves item prices', () async { final tab = await service.createTab(customerName: 'John'); final product = createTestProduct(priceInCents: 550); await service.addProductToTab(tabId: tab.id, product: product); await service.closeTab(tab.id); final closedTabs = await service.getClosedTabs(); expect(closedTabs.first.items.first.unitPriceInCents, 550); }); }); group('getClosedTabs', () { test('returns empty when no closed tabs', () async { final tabs = await service.getClosedTabs(); expect(tabs, isEmpty); }); test('returns closed tabs sorted by closedAt descending', () async { final now = DateTime.now(); await database .into(database.closedTabs) .insert( ClosedTabsCompanion.insert( id: 'closed-first', originalTabId: 'original-1', customerName: 'First', closedAt: now.subtract(const Duration(hours: 1)), ), ); await database .into(database.closedTabs) .insert( ClosedTabsCompanion.insert( id: 'closed-second', originalTabId: 'original-2', customerName: 'Second', closedAt: now, ), ); final closedTabs = await service.getClosedTabs(); expect(closedTabs.first.customerName, 'Second'); expect(closedTabs.last.customerName, 'First'); }); }); }); }