@Tags(['integration', 'postgres']) library; import 'package:cyberhybridhub_server/trading/market_data_db.dart'; import 'package:cyberhybridhub_server/trading/market_history_four_hour_slot.dart'; import 'package:test/test.dart'; import '../helpers/test_db.dart'; void main() { TestDb? testDb; setUpAll(() async { testDb = await TestDb.open(); }); tearDown(() async { if (testDb != null) { await testDb!.truncateTradingTables(); } }); tearDownAll(() async { await testDb?.close(); }); test('insertSnapshot then latestForSymbol returns newest by as_of', () async { if (testDb == null) { markTestSkipped('Set DATABASE_URL or TEST_DATABASE_URL for integration tests'); return; } final MarketDataDb db = testDb!.marketDataDb; final DateTime older = DateTime.utc(2026, 5, 23, 10); final DateTime newer = DateTime.utc(2026, 5, 23, 11); await db.insertSnapshot( symbol: 'SPY', metric: 'last_trade', price: 490, asOf: older, ); await db.insertSnapshot( symbol: 'SPY', metric: 'last_trade', price: 492, asOf: newer, ); final MarketDataSnapshot? latest = await db.latestForSymbol('SPY', 'last_trade'); expect(latest, isNotNull); expect(latest!.price, 492); expect( latest.asOf.toUtc().millisecondsSinceEpoch, greaterThan(older.toUtc().millisecondsSinceEpoch), ); }); group('upsertSnapshot', () { test('re-upsert updates price and raw without duplicating row', () async { if (testDb == null) { markTestSkipped( 'Set DATABASE_URL or TEST_DATABASE_URL for integration tests', ); return; } final MarketDataDb db = testDb!.marketDataDb; final DateTime asOf = DateTime.utc(2026, 5, 23, 13); await db.upsertSnapshot( symbol: 'SPY', metric: 'bar', timeframe: '1Day', asOf: asOf, price: 500, volume: 1000, raw: {'c': 500, 'v': 1000}, ); await db.upsertSnapshot( symbol: 'SPY', metric: 'bar', timeframe: '1Day', asOf: asOf, price: 505, volume: 1100, raw: {'c': 505, 'v': 1100}, ); final List rows = await db.barsForSymbol( symbol: 'SPY', timeframe: '1Day', since: asOf.subtract(const Duration(seconds: 1)), until: asOf.add(const Duration(seconds: 1)), ); expect(rows, hasLength(1)); expect(rows.single.price, 505); expect(rows.single.volume, 1100); expect(rows.single.raw?['c'], 505); expect(rows.single.raw?['v'], 1100); }); }); group('barsForSymbol', () { test('returns rows ordered by as_of ASC within [since, until)', () async { if (testDb == null) { markTestSkipped( 'Set DATABASE_URL or TEST_DATABASE_URL for integration tests', ); return; } final MarketDataDb db = testDb!.marketDataDb; final DateTime t1 = DateTime.utc(2026, 5, 20); final DateTime t2 = DateTime.utc(2026, 5, 21); final DateTime t3 = DateTime.utc(2026, 5, 22); for (final DateTime t in [t1, t2, t3]) { await db.upsertSnapshot( symbol: 'SPY', metric: 'bar', timeframe: '1Day', asOf: t, price: t.day.toDouble(), ); } final List rows = await db.barsForSymbol( symbol: 'SPY', timeframe: '1Day', since: t1, until: t3, ); expect(rows, hasLength(2)); expect(rows.map((MarketDataSnapshot r) => r.asOf), [t1, t2]); }); test('returns empty list when no rows match', () async { if (testDb == null) { markTestSkipped( 'Set DATABASE_URL or TEST_DATABASE_URL for integration tests', ); return; } final List rows = await testDb!.marketDataDb.barsForSymbol( symbol: 'NOPE', timeframe: '1Day', since: DateTime.utc(2026, 1, 1), until: DateTime.utc(2026, 1, 2), ); expect(rows, isEmpty); }); }); test('latestSyncedAsOf returns newest as_of or null', () async { if (testDb == null) { markTestSkipped( 'Set DATABASE_URL or TEST_DATABASE_URL for integration tests', ); return; } final MarketDataDb db = testDb!.marketDataDb; final DateTime older = DateTime.utc(2026, 5, 20); final DateTime newer = DateTime.utc(2026, 5, 22); expect(await db.latestSyncedAsOf('SPY', '1Day'), isNull); await db.upsertSnapshot( symbol: 'SPY', metric: 'bar', timeframe: '1Day', asOf: older, price: 490, ); await db.upsertSnapshot( symbol: 'SPY', metric: 'bar', timeframe: '1Day', asOf: newer, price: 500, ); expect(await db.latestSyncedAsOf('SPY', '1Day'), newer); }); test('symbolsWithBarForSlot matches canonical slot_start wire string', () async { if (testDb == null) { markTestSkipped( 'Set DATABASE_URL or TEST_DATABASE_URL for integration tests', ); return; } final MarketDataDb db = testDb!.marketDataDb; const String timeframe = '4Hour'; final DateTime slotStart = DateTime.utc(2026, 5, 26, 8); final String slotWire = MarketHistoryFourHourSlot.slotStartWire(slotStart); await db.upsertSnapshot( symbol: 'AAPL', metric: 'bar', timeframe: timeframe, asOf: slotStart, price: 186, raw: { 'slot_start': slotWire, 't': slotWire, }, ); final Set synced = await db.symbolsWithBarForSlot( symbols: ['AAPL', 'MSFT'], slotStart: slotStart, timeframe: timeframe, ); expect(synced, {'AAPL'}); }); test('symbolsWithBarForSlot falls back to as_of slot bucket for legacy rows', () async { if (testDb == null) { markTestSkipped( 'Set DATABASE_URL or TEST_DATABASE_URL for integration tests', ); return; } final MarketDataDb db = testDb!.marketDataDb; const String timeframe = '4Hour'; final DateTime slotStart = DateTime.utc(2026, 5, 26, 8); final DateTime barAt = slotStart.add(const Duration(hours: 1)); await db.upsertSnapshot( symbol: 'AAPL', metric: 'bar', timeframe: timeframe, asOf: barAt, price: 186, raw: { // Different wire format than Dart's toIso8601String() — must still count. 'slot_start': '2026-05-26T08:00:00Z', }, ); final Set synced = await db.symbolsWithBarForSlot( symbols: ['AAPL', 'MSFT'], slotStart: slotStart, timeframe: timeframe, ); expect(synced, {'AAPL'}); }); test('symbolsWithBarForSlot matches via slot_start bucket when wire differs', () async { if (testDb == null) { markTestSkipped( 'Set DATABASE_URL or TEST_DATABASE_URL for integration tests', ); return; } final MarketDataDb db = testDb!.marketDataDb; const String timeframe = '4Hour'; final DateTime slotStart = DateTime.utc(2026, 5, 26, 8); await db.upsertSnapshot( symbol: 'AAPL', metric: 'bar', timeframe: timeframe, asOf: slotStart.add(const Duration(hours: 4)), price: 186, raw: { 'slot_start': '2026-05-26T08:00:00.000Z', }, ); final Set synced = await db.symbolsWithBarForSlot( symbols: ['AAPL'], slotStart: slotStart, timeframe: timeframe, ); expect(synced, {'AAPL'}); }); test('symbolsWithBarForSlot does not match the next slot boundary as prior slot', () async { if (testDb == null) { markTestSkipped( 'Set DATABASE_URL or TEST_DATABASE_URL for integration tests', ); return; } final MarketDataDb db = testDb!.marketDataDb; const String timeframe = '4Hour'; final DateTime slotStart = DateTime.utc(2026, 5, 26, 8); await db.upsertSnapshot( symbol: 'AAPL', metric: 'bar', timeframe: timeframe, asOf: DateTime.utc(2026, 5, 26, 12), price: 186, ); final Set synced = await db.symbolsWithBarForSlot( symbols: ['AAPL'], slotStart: slotStart, timeframe: timeframe, ); expect(synced, isEmpty); }); test('symbolsWithBarForSlot counts no_data placeholder as synced', () async { if (testDb == null) { markTestSkipped( 'Set DATABASE_URL or TEST_DATABASE_URL for integration tests', ); return; } final MarketDataDb db = testDb!.marketDataDb; const String timeframe = '4Hour'; final DateTime slotStart = DateTime.utc(2026, 5, 30, 20); await db.upsertNoDataBarPlaceholder( symbol: 'A', slotStart: slotStart, timeframe: timeframe, checkedAt: DateTime.utc(2026, 5, 31), ); final Set synced = await db.symbolsWithBarForSlot( symbols: ['A', 'B'], slotStart: slotStart, timeframe: timeframe, ); expect(synced, {'A'}); }); }