import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:nuvolari/core/region/region_config.dart'; import 'package:nuvolari/data/radar/arpa_radar_source.dart'; import 'package:nuvolari/data/radar/mock_radar_source.dart'; import 'package:nuvolari/data/radar/radar_manifest.dart'; import 'package:nuvolari/data/radar/radar_source.dart'; /// Serves the repository's real asset files, since `flutter test` does not /// populate the asset bundle itself. class _DiskAssetBundle extends CachingAssetBundle { @override Future load(String key) async { final file = File(key); if (!file.existsSync()) { throw FlutterError('asset not found: $key'); } return ByteData.sublistView(Uint8List.fromList(file.readAsBytesSync())); } } class _EmptyAssetBundle extends CachingAssetBundle { @override Future load(String key) async => throw FlutterError('asset not found: $key'); } class _FixedAssetBundle extends CachingAssetBundle { _FixedAssetBundle(this.content); final String content; @override Future load(String key) async => ByteData.sublistView(Uint8List.fromList(utf8.encode(content))); } void main() { group('MockRadarSource against the bundled assets', () { late MockRadarSource source; setUp(() { source = MockRadarSource(bundle: _DiskAssetBundle()); }); test('parses the generated manifest', () async { final manifest = await source.getLatestManifest(); expect(manifest.regionId, 'piemonte'); expect(manifest.product, 'VMI'); expect(manifest.frames, isNotEmpty); expect(manifest.legend.unit, 'dBZ'); }); test('frames are evenly spaced and ascending', () async { final frames = await source.getFrames(); expect(frames.length, greaterThanOrEqualTo(12)); for (var i = 1; i < frames.length; i++) { expect( frames[i].timestamp.difference(frames[i - 1].timestamp), const Duration(minutes: 5), reason: 'gap before frame $i', ); } }); test('every frame in the manifest actually exists', () async { final frames = await source.getFrames(); for (final frame in frames) { final bytes = await source.loadFrameBytes(frame); expect(bytes, isNotEmpty, reason: '${frame.path} is empty'); // PNG magic number, so a truncated or misnamed file is caught here // rather than as a blank overlay on the map. expect( bytes.sublist(0, 8), orderedEquals([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]), reason: '${frame.path} is not a PNG', ); } }); // The demo frames are drawn for the region's extent. If the two drifted // apart the overlay would sit off the map with nothing to signal it. test('the manifest bbox matches the region config', () async { final manifest = await source.getLatestManifest(); final region = RegionConfig.parse( File('assets/regions/piemonte.json').readAsStringSync(), ); expect(manifest.bounds, region.bounds); }); test('resolves frame paths relative to the manifest', () { final resolved = source.resolveAssetPath( RadarFrame( timestamp: DateTime.utc(2026), path: 'frames/1768478400000.png', ), ); expect(resolved, 'assets/mock/frames/1768478400000.png'); }); test('parses the manifest once and reuses it', () async { final first = await source.getLatestManifest(); final second = await source.getLatestManifest(); expect(identical(first, second), isTrue); }); }); group('MockRadarSource failure handling', () { // Demo mode degrades exactly like a live source: the caller sees // unavailability, not a packaging error it cannot act on. test('reports a missing manifest as unavailable', () async { final source = MockRadarSource(bundle: _EmptyAssetBundle()); await expectLater( source.getLatestManifest(), throwsA(isA()), ); }); test('reports a malformed manifest as unavailable', () async { final source = MockRadarSource(bundle: _FixedAssetBundle('{"crs": 1}')); await expectLater( source.getLatestManifest(), throwsA(isA()), ); }); test('reports a missing frame as unavailable', () async { final source = MockRadarSource(bundle: _EmptyAssetBundle()); await expectLater( source.loadFrameBytes( RadarFrame(timestamp: DateTime.utc(2026), path: 'frames/gone.png'), ), throwsA(isA()), ); }); }); // The adapter is named by configuration but has no authorization behind it. // Failing loudly is the point: silently serving something else would misreport // where the data came from. group('ArpaRadarSource', () { const source = ArpaRadarSource(); test('every entry point refuses', () async { await expectLater( source.getLatestManifest(), throwsA(isA()), ); await expectLater( source.getFrames(), throwsA(isA()), ); await expectLater( source.loadFrameBytes( RadarFrame(timestamp: DateTime.utc(2026), path: 'x.png'), ), throwsA(isA()), ); }); test('says why it is disabled', () async { await expectLater( source.getLatestManifest(), throwsA( isA().having( (e) => e.message, 'message', contains('authorization'), ), ), ); }); }); }