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/bar_tab_service.dart'; import 'package:kooltap/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 an empty tab at version 1', () async { final tab = await service.createTab(customerName: 'John'); expect(tab.customerName, 'John'); expect(tab.version, 1); expect(tab.items, isEmpty); }); test('rejects a blank customer name', () async { await expectLater( service.createTab(customerName: ' '), throwsA(isA()), ); }); 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('tabs sorted by openedAt descending', () async { final now = DateTime.now(); await database .into(database.barTabs) .insert( BarTabsCompanion.insert( id: 'tab-first', customerName: 'First', openedAt: now.subtract(const Duration(hours: 1)), ), ); await database .into(database.barTabs) .insert( BarTabsCompanion.insert( id: 'tab-second', customerName: 'Second', 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); await service.deleteTab(tab.id); 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); expect(await database.select(database.tabItems).get(), isEmpty); expect(await database.select(database.tabItemPurchases).get(), 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( 'keeps the tab under the same name and drops its purchases', () 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!.customerName, 'John'); expect(updatedTab.items, isEmpty); expect( await database.select(database.tabItemPurchases).get(), isEmpty, ); }, ); test('throws when the tab changed since the expected version', () async { final tab = await service.createTab(customerName: 'John'); final product = createTestProduct(); await service.addProductToTab(tabId: tab.id, product: product); await expectLater( service.closeTab(tab.id, expectedVersion: tab.version), throwsA(isA()), ); expect((await service.getTabById(tab.id))!.items, hasLength(1)); expect(await service.getClosedTabs(), isEmpty); }); test('closes when the expected version matches', () async { final tab = await service.createTab(customerName: 'John'); final product = createTestProduct(); await service.addProductToTab(tabId: tab.id, product: product); final current = await service.getTabById(tab.id); await service.closeTab(tab.id, expectedVersion: current!.version); expect(await service.getClosedTabs(), hasLength(1)); }); test('throws for an unknown tab', () async { await expectLater( service.closeTab('missing'), throwsA(isA()), ); }); 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('versioning', () { test('every change bumps the tab version', () async { final tab = await service.createTab(customerName: 'John'); final product = createTestProduct(); await service.addProductToTab(tabId: tab.id, product: product); final afterAdd = (await service.getTabById(tab.id))!; await service.adjustTabItemQuantity( tabItemId: afterAdd.items.first.id, delta: 1, ); final afterAdjust = (await service.getTabById(tab.id))!; await service.renameTab(tabId: tab.id, customerName: 'Jane'); final afterRename = (await service.getTabById(tab.id))!; expect(afterAdd.version, tab.version + 1); expect(afterAdjust.version, afterAdd.version + 1); expect(afterRename.version, afterAdjust.version + 1); }); test('a failed add leaves the version untouched', () 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()), ); expect((await service.getTabById(tab.id))!.version, tab.version); }); }); group('watchOpenTabs', () { test('emits again when an item is added', () async { final tab = await service.createTab(customerName: 'John'); final emissions = service.watchOpenTabs(); final expectation = expectLater( emissions.map((tabs) => tabs.single.itemCount), emitsThrough(1), ); await service.addProductToTab( tabId: tab.id, product: createTestProduct(), ); await expectation; }); }); group('getDistinctCustomerNames', () { test('returns each closed customer once, sorted', () async { for (final name in ['Zoe', 'Adam', 'Zoe']) { final tab = await service.createTab(customerName: name); await service.addProductToTab( tabId: tab.id, product: createTestProduct(), ); await service.closeTab(tab.id); } expect(await service.getDistinctCustomerNames(), ['Adam', 'Zoe']); }); }); 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'); }); }); }); }