import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:smtias_capture/services/file_service.dart'; void main() { group('FileService.directoryPath', () { test('保存先ディレクトリのパスを返す', () { final service = FileService(); expect(service.directoryPath, '/storage/emulated/0/Pictures/MiniTIAS'); }); }); group('FileService.savePreviewJpeg', () { late Directory tempDir; late FileService service; setUp(() async { tempDir = await Directory.systemTemp.createTemp( 'smtias_capture_preview_test_', ); service = FileService(directoryPath: tempDir.path); }); tearDown(() async { if (await tempDir.exists()) { await tempDir.delete(recursive: true); } }); test('指定した baseName で .preview.jpg ファイルが生成される', () async { const baseName = 'MiniTIAS_QM_20260531_120000'; final previewBytes = Uint8List.fromList([0xFF, 0xD8, 0xFF, 0xE0]); await service.savePreviewJpeg(baseName, previewBytes); final expectedFile = File('${tempDir.path}/$baseName.preview.jpg'); expect(await expectedFile.exists(), isTrue); }); test('戻り値が保存パス(baseName.preview.jpg を含む)になる', () async { const baseName = 'MiniTIAS_QM_20260531_120000'; final previewBytes = Uint8List.fromList([0xFF, 0xD8, 0xFF, 0xE0]); final path = await service.savePreviewJpeg(baseName, previewBytes); expect(path, contains('$baseName.preview.jpg')); }); test('渡したバイト列がそのままファイルに書き込まれる', () async { const baseName = 'MiniTIAS_QM_20260531_120001'; final previewBytes = Uint8List.fromList([ 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, ]); final path = await service.savePreviewJpeg(baseName, previewBytes); final written = await File(path).readAsBytes(); expect(written, equals(previewBytes)); }); test('ディレクトリが存在しない場合でも自動作成してファイルを保存する', () async { final nonExistentSubDir = Directory('${tempDir.path}/sub/dir'); final serviceWithSubDir = FileService( directoryPath: nonExistentSubDir.path, ); expect(await nonExistentSubDir.exists(), isFalse); const baseName = 'MiniTIAS_QM_20260531_120002'; final previewBytes = Uint8List.fromList([0xFF, 0xD8, 0xFF, 0xE0]); final path = await serviceWithSubDir.savePreviewJpeg( baseName, previewBytes, ); expect(await File(path).exists(), isTrue); }); test('空のバイト列でも .preview.jpg ファイルが生成される', () async { const baseName = 'MiniTIAS_QM_20260531_120003'; final previewBytes = Uint8List(0); final path = await service.savePreviewJpeg(baseName, previewBytes); expect(await File(path).exists(), isTrue); final written = await File(path).readAsBytes(); expect(written.length, 0); }); }); group('FileService.generateQuantitativeBaseNameSync', () { const timestamp = '20260404_120000'; const baseName = 'MiniTIAS_QM_$timestamp'; // 「タイムスタンプ+既存ファイル一覧 → baseName」の等価検証は, // 入力(existing)と期待値だけが異なる同型ケースのためテーブル駆動でまとめる. // 重複判定は `.dng` の存在チェックのみで行い,suffix=1 から順に空きを探す仕様. final cases = <({String name, List existing, String expected})>[ ( name: '同秒のファイルが存在しない場合,サフィックスなしの baseName を返す', existing: [], expected: baseName, ), ( name: '同秒の .dng が存在する場合,_1 サフィックスを付与する', existing: ['$baseName.dng'], expected: '${baseName}_1', ), ( name: '_1.dng も存在する場合,_2 サフィックスを付与する', existing: ['$baseName.dng', '${baseName}_1.dng'], expected: '${baseName}_2', ), ( name: '異なるタイムスタンプの .dng が存在しても影響しない', existing: ['MiniTIAS_QM_20260404_120001.dng'], expected: baseName, ), ( // _1 が存在しないため,欠番の _1 を返す name: '連番に欠番がある場合(_1.dng が欠落),_2 ではなく _1 を返す', existing: ['$baseName.dng', '${baseName}_2.dng'], expected: '${baseName}_1', ), ( name: '連番が多数存在する場合(_1〜_5.dng),_6 を付与する', existing: [ '$baseName.dng', '${baseName}_1.dng', '${baseName}_2.dng', '${baseName}_3.dng', '${baseName}_4.dng', '${baseName}_5.dng', ], expected: '${baseName}_6', ), ( // 重複判定は .dng のみを見るため,.meta.json のみでは重複とみなさない name: '候補 .meta.json のみが存在し .dng がない場合,サフィックスなしを返す', existing: ['$baseName.meta.json'], expected: baseName, ), ]; for (final c in cases) { test(c.name, () { final result = FileService.generateQuantitativeBaseNameSync( timestamp, c.existing, ); expect(result, c.expected); }); } test('返り値が MiniTIAS_QM_ プレフィックスで始まる', () { final result = FileService.generateQuantitativeBaseNameSync( timestamp, [], ); expect(result, startsWith('MiniTIAS_QM_')); }); }); group('FileService.generateQuantitativeBaseName (async)', () { late Directory tempDir; late FileService service; setUp(() async { tempDir = await Directory.systemTemp.createTemp( 'smtias_capture_qm_async_test_', ); service = FileService(directoryPath: tempDir.path); }); tearDown(() async { if (await tempDir.exists()) { await tempDir.delete(recursive: true); } }); test('生成した baseName の .dng が既存の場合,衝突しない名前を返す', () async { // 実時刻に依存せず「既存 .dng と衝突しない名前を返す」契約を決定的に検証する. // 同秒なら _1 サフィックス,秒跨ぎでも別タイムスタンプとなり,いずれにせよ // 既存ファイルと異なる名前になる(本番の衝突回避の実挙動). final base1 = await service.generateQuantitativeBaseName(); await File('${tempDir.path}/$base1.dng').writeAsBytes([0]); final base2 = await service.generateQuantitativeBaseName(); expect(base2, startsWith('MiniTIAS_QM_')); expect(base2, isNot(base1)); expect(File('${tempDir.path}/$base2.dng').existsSync(), isFalse); }); }); group('FileService.saveQuantitativeMetadata', () { late Directory tempDir; late FileService service; setUp(() async { tempDir = await Directory.systemTemp.createTemp( 'smtias_capture_qm_test_', ); service = FileService(directoryPath: tempDir.path); }); tearDown(() async { if (await tempDir.exists()) { await tempDir.delete(recursive: true); } }); test('LSC マップなしで呼び出すと .meta.json 拡張子のファイルが生成される', () async { final metadata = {'settings': {}}; final path = await service.saveQuantitativeMetadata( 'MiniTIAS_QM_20260404_120000', metadata, ); expect(path, endsWith('.meta.json')); expect(await File(path).exists(), isTrue); }); test( 'LSC マップなしの JSON には lscMap / lscMapRowCount / lscMapColumnCount キーが含まれない', () async { final metadata = {'settings': {}}; final path = await service.saveQuantitativeMetadata( 'MiniTIAS_QM_20260404_120000', metadata, ); final jsonStr = await File(path).readAsString(); final decoded = jsonDecode(jsonStr) as Map; expect(decoded.containsKey('lscMap'), isFalse); expect(decoded.containsKey('lscMapRowCount'), isFalse); expect(decoded.containsKey('lscMapColumnCount'), isFalse); }, ); test('LSC マップありで呼び出すと JSON 内に lscMap の二重配列が含まれる', () async { final metadata = {'settings': {}}; final lscMap = [ [1.0, 2.0], [3.0, 4.0], ]; final path = await service.saveQuantitativeMetadata( 'MiniTIAS_QM_20260404_120000', metadata, lscMap: lscMap, lscMapRowCount: 2, lscMapColumnCount: 2, ); final jsonStr = await File(path).readAsString(); final decoded = jsonDecode(jsonStr) as Map; expect(decoded.containsKey('lscMap'), isTrue); final storedLscMap = decoded['lscMap'] as List; expect(storedLscMap, hasLength(2)); expect(storedLscMap[0], isA>()); }); test( 'LSC マップありの JSON で lscMapRowCount と lscMapColumnCount が指定値で保存される', () async { final metadata = {'settings': {}}; final lscMap = [ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0], ]; final path = await service.saveQuantitativeMetadata( 'MiniTIAS_QM_20260404_120000', metadata, lscMap: lscMap, lscMapRowCount: 3, lscMapColumnCount: 3, ); final jsonStr = await File(path).readAsString(); final decoded = jsonDecode(jsonStr) as Map; expect(decoded['lscMapRowCount'], 3); expect(decoded['lscMapColumnCount'], 3); }, ); test('メタデータの中身が JSON 内に保持される', () async { final metadata = { 'settings': {'exposure_time_ns': 10000000, 'iso': 400}, 'actual': {'frame_duration_ns': 33333333}, 'sensorCharacteristics': {'make': 'TestMake'}, }; final path = await service.saveQuantitativeMetadata( 'MiniTIAS_QM_20260404_120000', metadata, ); final jsonStr = await File(path).readAsString(); final decoded = jsonDecode(jsonStr) as Map; expect(decoded.containsKey('settings'), isTrue); expect(decoded.containsKey('actual'), isTrue); expect(decoded.containsKey('sensorCharacteristics'), isTrue); final settings = decoded['settings'] as Map; expect(settings['iso'], 400); }); test('ディレクトリが存在しない場合でも自動作成してファイルを保存する', () async { final nonExistentSubDir = Directory('${tempDir.path}/sub/dir'); final serviceWithSubDir = FileService( directoryPath: nonExistentSubDir.path, ); expect(await nonExistentSubDir.exists(), isFalse); final metadata = {'settings': {}}; final path = await serviceWithSubDir.saveQuantitativeMetadata( 'MiniTIAS_QM_20260404_120000', metadata, ); expect(await File(path).exists(), isTrue); }); }); group('FileService.saveDiagnosticsJson', () { late Directory tempDir; late FileService service; setUp(() async { tempDir = await Directory.systemTemp.createTemp( 'smtias_capture_diag_test_', ); service = FileService(directoryPath: tempDir.path); }); tearDown(() async { if (await tempDir.exists()) { await tempDir.delete(recursive: true); } }); test('camera_diagnostics_ プレフィックスの .json ファイルが生成される', () async { final diagnostics = {'make': 'TestMake'}; final path = await service.saveDiagnosticsJson(diagnostics); final fileName = path.split('/').last; expect(fileName, startsWith('camera_diagnostics_')); expect(fileName, endsWith('.json')); expect(await File(path).exists(), isTrue); }); test('診断内容が JSON として保持される', () async { final diagnostics = { 'make': 'TestMake', 'model': 'TestModel', 'sensor': { 'iso_range': [40, 800], }, }; final path = await service.saveDiagnosticsJson(diagnostics); final jsonStr = await File(path).readAsString(); final decoded = jsonDecode(jsonStr) as Map; expect(decoded['make'], 'TestMake'); expect(decoded['model'], 'TestModel'); expect((decoded['sensor'] as Map)['iso_range'], [ 40, 800, ]); }); test('インデント付き(pretty print)で書き込まれる', () async { final diagnostics = {'make': 'TestMake'}; final path = await service.saveDiagnosticsJson(diagnostics); final jsonStr = await File(path).readAsString(); // JsonEncoder.withIndent(' ') により改行とインデントを含む expect(jsonStr, contains('\n')); expect(jsonStr, contains(' "make"')); }); test('ディレクトリが存在しない場合でも自動作成して保存する', () async { final nonExistentSubDir = Directory('${tempDir.path}/sub/dir'); final serviceWithSubDir = FileService( directoryPath: nonExistentSubDir.path, ); expect(await nonExistentSubDir.exists(), isFalse); final path = await serviceWithSubDir.saveDiagnosticsJson({'make': 'X'}); expect(await File(path).exists(), isTrue); }); }); }