import 'package:flutter_test/flutter_test.dart'; import 'package:pilotvault_fly_agent/session/flight_session.dart'; void main() { final DateTime t0 = DateTime(2026, 10, 22, 14, 5); FixSample fix(int s, {bool airborne = true, double height = 30, double lat = 55.0, double lon = 12.0}) => FixSample(at: t0.add(Duration(seconds: s)), airborne: airborne, lat: lat, lon: lon, height: height, speed: 3); FlightSession armed() => FlightSession() ..droneId = 'd1' ..ridSerial = 'SN1' ..arm(t0); test('armed session starts on the first airborne fix', () { final FlightSession s = armed(); expect(s.onFix(fix(0, airborne: false, height: 0)), isFalse); expect(s.phase, SessionPhase.armed); expect(s.onFix(fix(5)), isTrue); expect(s.phase, SessionPhase.active); expect(s.autoStarted, isTrue); expect(s.startedAt, t0.add(const Duration(seconds: 5))); }); test('stops at touchdown after the landed hold, not at the end of the hold', () { final FlightSession s = armed(); s.onFix(fix(0)); s.onFix(fix(10, airborne: false, height: 0)); expect(s.onFix(fix(20, airborne: false, height: 0)), isFalse); expect(s.onFix(fix(26, airborne: false, height: 0)), isTrue); expect(s.phase, SessionPhase.landed); expect(s.endedAt, t0.add(const Duration(seconds: 10))); }); test('a brief low hover does not end the flight', () { final FlightSession s = armed(); s.onFix(fix(0)); s.onFix(fix(10, airborne: false, height: 0.5)); s.onFix(fix(20, airborne: true)); s.onFix(fix(30, airborne: false, height: 0)); s.onFix(fix(40, airborne: false, height: 0)); expect(s.phase, SessionPhase.active); }); test('Remote ID silence stops the flight at last contact', () { final FlightSession s = armed(); s.onFix(fix(0)); s.onFix(fix(30)); expect(s.onTick(t0.add(const Duration(seconds: 60))), isFalse); expect(s.onTick(t0.add(const Duration(seconds: 91))), isTrue); expect(s.endedAt, t0.add(const Duration(seconds: 30))); expect(s.alerts.single.code, 'rid_lost'); }); test('tracks max height, distance and a single height alert', () { final FlightSession s = armed(); s.onFix(fix(0, height: 50, lat: 55.0)); s.onFix(fix(1, height: 125, lat: 55.001)); s.onFix(fix(2, height: 130, lat: 55.002)); expect(s.maxHeight, 130); expect(s.distance, closeTo(222, 2)); expect(s.alerts.where((SessionAlert a) => a.code == 'height').length, 1); }); test('flight input carries the § 5 manual fields and a GeoJSON route', () { final FlightSession s = armed() ..purpose = 'commercial' ..category = 'open' ..weather = '12°C'; s.onFix(fix(0, lat: 55.0)); s.onFix(fix(60, lat: 55.001, height: 80)); s.stop(t0.add(const Duration(minutes: 12))); final Map j = s.toFlightInput(); expect(j['operationDate'], '2026-10-22'); expect(j['startTime'], '14:05'); expect(j['endTime'], '14:17'); expect(j['drone'], 'd1'); expect(j['maxAltitudeAgl'], 80.0); expect(j['loggingPath'], 'automatic_flyagentapp'); expect((j['areaRoute'] as String), contains('Remote ID track')); expect(j['routeGeojson']['type'], 'LineString'); expect((j['routeGeojson']['coordinates'] as List).first, [12.0, 55.0, 30.0]); expect(j['notes'], contains('serial SN1')); }); test('survives a round trip through persistence', () { final FlightSession s = armed(); s.onFix(fix(0)); s.onFix(fix(30, height: 140, lat: 55.001)); final FlightSession r = FlightSession.decode(s.encode()); expect(r.phase, SessionPhase.active); expect(r.track.length, 2); expect(r.maxHeight, 140); expect(r.alerts.single.code, 'height'); expect(r.startedAt, s.startedAt); }); }