Newer
Older
SmTIAS-Capture / test / services / file_service_test.dart
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.generateFileNameSync', () {
    test('同秒のファイルが存在しない場合,サフィックスなしのファイル名を返す', () {
      final result = FileService.generateFileNameSync('20260404_120000', []);
      expect(result, 'MiniTIAS_20260404_120000.png');
    });

    test('同秒のファイルが存在する場合,_1 サフィックスを付与する', () {
      final result = FileService.generateFileNameSync('20260404_120000', [
        'MiniTIAS_20260404_120000.png',
      ]);
      expect(result, 'MiniTIAS_20260404_120000_1.png');
    });

    test('_1 も存在する場合,_2 サフィックスを付与する', () {
      final result = FileService.generateFileNameSync('20260404_120000', [
        'MiniTIAS_20260404_120000.png',
        'MiniTIAS_20260404_120000_1.png',
      ]);
      expect(result, 'MiniTIAS_20260404_120000_2.png');
    });

    test('異なるタイムスタンプのファイルが存在しても影響しない', () {
      final result = FileService.generateFileNameSync('20260404_120000', [
        'MiniTIAS_20260404_120001.png',
      ]);
      expect(result, 'MiniTIAS_20260404_120000.png');
    });

    // --- 追加テスト: ファイル命名規則の詳細検証 ---

    test('返り値が MiniTIAS_ プレフィックスで始まる', () {
      final result = FileService.generateFileNameSync('20260404_120000', []);
      expect(result, startsWith('MiniTIAS_'));
    });

    test('返り値が .png 拡張子で終わる', () {
      final result = FileService.generateFileNameSync('20260404_120000', []);
      expect(result, endsWith('.png'));
    });

    test('連番が連続して欠けている場合(_1 が欠落),_2 ではなく _1 を返す', () {
      // 仕様: suffix=1 から順に探すため _1 が存在しなければ _1 を返す
      final result = FileService.generateFileNameSync('20260404_120000', [
        'MiniTIAS_20260404_120000.png',
        'MiniTIAS_20260404_120000_2.png', // _1 が存在しない状態
      ]);
      expect(result, 'MiniTIAS_20260404_120000_1.png');
    });

    test('連番が多数存在する場合,次の番号を付与する', () {
      final existing = [
        'MiniTIAS_20260404_120000.png',
        'MiniTIAS_20260404_120000_1.png',
        'MiniTIAS_20260404_120000_2.png',
        'MiniTIAS_20260404_120000_3.png',
        'MiniTIAS_20260404_120000_4.png',
      ];
      final result = FileService.generateFileNameSync(
        '20260404_120000',
        existing,
      );
      expect(result, 'MiniTIAS_20260404_120000_5.png');
    });

    test('空のタイムスタンプでも形式を維持する', () {
      final result = FileService.generateFileNameSync('', []);
      expect(result, 'MiniTIAS_.png');
    });
  });

  group('FileService.saveImage', () {
    late Directory tempDir;
    late _FileServiceTestable service;

    setUp(() async {
      tempDir = await Directory.systemTemp.createTemp('smtias_capture_test_');
      service = _FileServiceTestable(tempDir.path);
    });

    tearDown(() async {
      if (await tempDir.exists()) {
        await tempDir.delete(recursive: true);
      }
    });

    test('PNG バイト列を渡すと,ファイルパスを返す', () async {
      final pngBytes = Uint8List.fromList([0, 1, 2, 3]);
      final path = await service.saveImage(pngBytes);
      expect(path, isNotEmpty);
    });

    test('PNG バイト列を渡すと,.png 拡張子のファイルが生成される', () async {
      final pngBytes = Uint8List.fromList([0, 1, 2, 3]);
      final path = await service.saveImage(pngBytes);
      expect(path, endsWith('.png'));
    });

    test('PNG バイト列を渡すと,ファイル名が MiniTIAS_ プレフィックスを持つ', () async {
      final pngBytes = Uint8List.fromList([0, 1, 2, 3]);
      final path = await service.saveImage(pngBytes);
      final fileName = path.split('/').last;
      expect(fileName, startsWith('MiniTIAS_'));
    });

    test('渡したバイト列がそのままファイルに書き込まれる', () async {
      final pngBytes = Uint8List.fromList([137, 80, 78, 71, 13, 10, 26, 10]);
      final path = await service.saveImage(pngBytes);
      final written = await File(path).readAsBytes();
      expect(written, equals(pngBytes));
    });

    test('空のバイト列でもファイルを生成できる', () async {
      final pngBytes = Uint8List(0);
      final path = await service.saveImage(pngBytes);
      expect(await File(path).exists(), isTrue);
      final written = await File(path).readAsBytes();
      expect(written.length, 0);
    });

    test('同秒に 2 回呼び出すと,2 つ目のファイル名に _1 サフィックスが付く', () async {
      final pngBytes = Uint8List.fromList([0, 1, 2, 3]);
      // 最初の saveImage でファイルを作成
      final path1 = await service.saveImage(pngBytes);
      // 同秒内に再度呼び出す(ファイルが既に存在する状態を擬似的に作る)
      // generateFileName はファイルシステムを参照するため,1 ファイル作成後に呼ぶ
      final path2 = await service.saveImage(pngBytes);

      final fileName1 = path1.split('/').last;
      final fileName2 = path2.split('/').last;

      // 2 つのファイル名は異なるはず
      // (同秒の場合 _1 サフィックスが付く,異なる秒の場合も名前が異なる)
      if (fileName1.replaceAll(RegExp(r'_\d+\.png$'), '.png') ==
          fileName2.replaceAll(RegExp(r'_\d+\.png$'), '.png')) {
        // 同秒の場合: 2 つ目は _1 サフィックスを持つはず
        expect(fileName2, contains('_1.png'));
      } else {
        // 異なる秒の場合: それぞれ独立したタイムスタンプ付きファイル名
        expect(fileName1, isNot(equals(fileName2)));
      }
    });

    test('ディレクトリが存在しない場合でも自動作成してファイルを保存する', () async {
      final nonExistentSubDir = Directory('${tempDir.path}/sub/dir');
      final serviceWithSubDir = _FileServiceTestable(nonExistentSubDir.path);
      expect(await nonExistentSubDir.exists(), isFalse);

      final pngBytes = Uint8List.fromList([0, 1, 2, 3]);
      final path = await serviceWithSubDir.saveImage(pngBytes);
      expect(await File(path).exists(), isTrue);
    });
  });

  group('FileService.directoryPath', () {
    test('保存先ディレクトリのパスを返す', () {
      final service = FileService();
      expect(service.directoryPath, '/storage/emulated/0/Pictures/MiniTIAS');
    });
  });

  group('FileService.savePreviewJpeg', () {
    late Directory tempDir;
    late _FileServiceTestablePreview service;

    setUp(() async {
      tempDir = await Directory.systemTemp.createTemp(
        'smtias_capture_preview_test_',
      );
      service = _FileServiceTestablePreview(tempDir.path);
    });

    tearDown(() async {
      if (await tempDir.exists()) {
        await tempDir.delete(recursive: true);
      }
    });

    test('指定した baseName で <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 = _FileServiceTestablePreview(
        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', () {
    test('同秒のファイルが存在しない場合,サフィックスなしの baseName を返す', () {
      final result = FileService.generateQuantitativeBaseNameSync(
        '20260404_120000',
        [],
      );
      expect(result, 'MiniTIAS_QM_20260404_120000');
    });

    test('同秒の .dng が存在する場合,_1 サフィックスを付与する', () {
      final result = FileService.generateQuantitativeBaseNameSync(
        '20260404_120000',
        ['MiniTIAS_QM_20260404_120000.dng'],
      );
      expect(result, 'MiniTIAS_QM_20260404_120000_1');
    });

    test('_1.dng も存在する場合,_2 サフィックスを付与する', () {
      final result = FileService.generateQuantitativeBaseNameSync(
        '20260404_120000',
        [
          'MiniTIAS_QM_20260404_120000.dng',
          'MiniTIAS_QM_20260404_120000_1.dng',
        ],
      );
      expect(result, 'MiniTIAS_QM_20260404_120000_2');
    });

    test('異なるタイムスタンプの .dng が存在しても影響しない', () {
      final result = FileService.generateQuantitativeBaseNameSync(
        '20260404_120000',
        ['MiniTIAS_QM_20260404_120001.dng'],
      );
      expect(result, 'MiniTIAS_QM_20260404_120000');
    });

    test('返り値が MiniTIAS_QM_ プレフィックスで始まる', () {
      final result = FileService.generateQuantitativeBaseNameSync(
        '20260404_120000',
        [],
      );
      expect(result, startsWith('MiniTIAS_QM_'));
    });

    test('連番に欠番がある場合(_1.dng が欠落),_2 ではなく _1 を返す', () {
      // 仕様: suffix=1 から順に探すため _1 が存在しなければ _1 を返す
      final result = FileService.generateQuantitativeBaseNameSync(
        '20260404_120000',
        [
          'MiniTIAS_QM_20260404_120000.dng',
          'MiniTIAS_QM_20260404_120000_2.dng', // _1 が存在しない状態
        ],
      );
      expect(result, 'MiniTIAS_QM_20260404_120000_1');
    });

    test('連番が多数存在する場合(_1〜_5.dng),_6 を付与する', () {
      final existing = [
        'MiniTIAS_QM_20260404_120000.dng',
        'MiniTIAS_QM_20260404_120000_1.dng',
        'MiniTIAS_QM_20260404_120000_2.dng',
        'MiniTIAS_QM_20260404_120000_3.dng',
        'MiniTIAS_QM_20260404_120000_4.dng',
        'MiniTIAS_QM_20260404_120000_5.dng',
      ];
      final result = FileService.generateQuantitativeBaseNameSync(
        '20260404_120000',
        existing,
      );
      expect(result, 'MiniTIAS_QM_20260404_120000_6');
    });

    test('候補 .meta.json のみが存在し .dng がない場合,サフィックスなしを返す', () {
      // 重複判定は .dng の存在チェックのみであるため,
      // .meta.json のみ存在しても重複とみなさない
      final result = FileService.generateQuantitativeBaseNameSync(
        '20260404_120000',
        ['MiniTIAS_QM_20260404_120000.meta.json'],
      );
      expect(result, 'MiniTIAS_QM_20260404_120000');
    });
  });

  group('FileService.saveQuantitativeMetadata', () {
    late Directory tempDir;
    late _FileServiceTestableQuantitative service;

    setUp(() async {
      tempDir = await Directory.systemTemp.createTemp('smtias_capture_qm_test_');
      service = _FileServiceTestableQuantitative(tempDir.path);
    });

    tearDown(() async {
      if (await tempDir.exists()) {
        await tempDir.delete(recursive: true);
      }
    });

    test('LSC マップなしで呼び出すと .meta.json 拡張子のファイルが生成される', () async {
      final metadata = <String, dynamic>{'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 = <String, dynamic>{'settings': {}};
        final path = await service.saveQuantitativeMetadata(
          'MiniTIAS_QM_20260404_120000',
          metadata,
        );
        final jsonStr = await File(path).readAsString();
        final decoded = jsonDecode(jsonStr) as Map<String, dynamic>;
        expect(decoded.containsKey('lscMap'), isFalse);
        expect(decoded.containsKey('lscMapRowCount'), isFalse);
        expect(decoded.containsKey('lscMapColumnCount'), isFalse);
      },
    );

    test('LSC マップありで呼び出すと JSON 内に lscMap の二重配列が含まれる', () async {
      final metadata = <String, dynamic>{'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<String, dynamic>;
      expect(decoded.containsKey('lscMap'), isTrue);
      final storedLscMap = decoded['lscMap'] as List<dynamic>;
      expect(storedLscMap, hasLength(2));
      expect(storedLscMap[0], isA<List<dynamic>>());
    });

    test(
      'LSC マップありの JSON で lscMapRowCount と lscMapColumnCount が指定値で保存される',
      () async {
        final metadata = <String, dynamic>{'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<String, dynamic>;
        expect(decoded['lscMapRowCount'], 3);
        expect(decoded['lscMapColumnCount'], 3);
      },
    );

    test('メタデータの中身が JSON 内に保持される', () async {
      final metadata = <String, dynamic>{
        '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<String, dynamic>;
      expect(decoded.containsKey('settings'), isTrue);
      expect(decoded.containsKey('actual'), isTrue);
      expect(decoded.containsKey('sensorCharacteristics'), isTrue);
      final settings = decoded['settings'] as Map<String, dynamic>;
      expect(settings['iso'], 400);
    });

    test('ディレクトリが存在しない場合でも自動作成してファイルを保存する', () async {
      final nonExistentSubDir = Directory('${tempDir.path}/sub/dir');
      final serviceWithSubDir = _FileServiceTestableQuantitative(
        nonExistentSubDir.path,
      );
      expect(await nonExistentSubDir.exists(), isFalse);

      final metadata = <String, dynamic>{'settings': {}};
      final path = await serviceWithSubDir.saveQuantitativeMetadata(
        'MiniTIAS_QM_20260404_120000',
        metadata,
      );
      expect(await File(path).exists(), isTrue);
    });
  });
}

/// テスト用: 保存先ディレクトリをオーバーライドできる FileService サブクラス.
class _FileServiceTestable extends FileService {
  _FileServiceTestable(this._testBasePath);

  final String _testBasePath;

  @override
  String get directoryPath => _testBasePath;

  @override
  Future<String> saveImage(Uint8List pngBytes) async {
    final directory = Directory(_testBasePath);
    if (!await directory.exists()) {
      await directory.create(recursive: true);
    }
    final fileName = await _generateFileNameForTest();
    final filePath = '$_testBasePath/$fileName';
    await File(filePath).writeAsBytes(pngBytes);
    return filePath;
  }

  Future<String> _generateFileNameForTest() async {
    final now = DateTime.now();
    final timestamp =
        '${now.year}'
        '${now.month.toString().padLeft(2, '0')}'
        '${now.day.toString().padLeft(2, '0')}'
        '_'
        '${now.hour.toString().padLeft(2, '0')}'
        '${now.minute.toString().padLeft(2, '0')}'
        '${now.second.toString().padLeft(2, '0')}';

    final baseName = 'MiniTIAS_$timestamp';
    final candidate = '$baseName.png';

    if (!await File('$_testBasePath/$candidate').exists()) {
      return candidate;
    }

    var suffix = 1;
    while (await File('$_testBasePath/${baseName}_$suffix.png').exists()) {
      suffix++;
    }
    return '${baseName}_$suffix.png';
  }
}

/// テスト用: savePreviewJpeg の保存先をオーバーライドする FileService サブクラス.
class _FileServiceTestablePreview extends FileService {
  _FileServiceTestablePreview(this._testBasePath);

  final String _testBasePath;

  @override
  String get directoryPath => _testBasePath;

  @override
  Future<String> savePreviewJpeg(
    String baseName,
    Uint8List previewBytes,
  ) async {
    final directory = Directory(_testBasePath);
    if (!await directory.exists()) {
      await directory.create(recursive: true);
    }
    final filePath = '$_testBasePath/$baseName.preview.jpg';
    await File(filePath).writeAsBytes(previewBytes);
    return filePath;
  }
}

/// テスト用: saveQuantitativeMetadata の保存先をオーバーライドする FileService サブクラス.
class _FileServiceTestableQuantitative extends FileService {
  _FileServiceTestableQuantitative(this._testBasePath);

  final String _testBasePath;

  @override
  String get directoryPath => _testBasePath;

  @override
  Future<String> saveQuantitativeMetadata(
    String baseName,
    Map<String, dynamic> metadata, {
    List<List<double>>? lscMap,
    int? lscMapRowCount,
    int? lscMapColumnCount,
  }) async {
    final directory = Directory(_testBasePath);
    if (!await directory.exists()) {
      await directory.create(recursive: true);
    }

    final Map<String, dynamic> jsonMap = Map<String, dynamic>.from(metadata);
    if (lscMap != null) {
      jsonMap['lscMap'] = lscMap;
      jsonMap['lscMapRowCount'] = lscMapRowCount;
      jsonMap['lscMapColumnCount'] = lscMapColumnCount;
    }

    const encoder = JsonEncoder.withIndent('  ');
    final filePath = '$_testBasePath/$baseName.meta.json';
    await File(filePath).writeAsString(encoder.convert(jsonMap));
    return filePath;
  }
}