import 'dart:io';
import 'dart:typed_data';

import 'package:commons_core/commons_core.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tane/db/database.dart';
import 'package:tane/security/secure_key_store.dart';
import 'package:tane/services/auto_backup_service.dart';
import 'package:tane/services/auto_backup_store.dart';
import 'package:tane/services/export_import_service.dart';
import 'package:tane/services/file_service.dart';

import '../support/test_support.dart';

/// [FileService] the exporter never actually calls for [buildSealedBackup]; the
/// automatic path writes files itself.
class _UnusedFiles implements FileService {
  @override
  Future<String?> saveFile({
    required String suggestedName,
    required Uint8List bytes,
  }) async => null;

  @override
  Future<Uint8List?> pickFileBytes({List<String>? allowedExtensions}) async =>
      null;
}

/// An exporter whose backup build always fails, to prove failures stay silent.
class _ThrowingExporter extends ExportImportService {
  _ThrowingExporter(AppDatabase db)
    : super(
        repository: newTestRepository(db),
        files: _UnusedFiles(),
        keys: SecureKeyStore(store: InMemorySecretStore()),
      );

  @override
  Future<Uint8List> buildSealedBackup() => throw Exception('disk full');
}

void main() {
  late AppDatabase db;
  late Directory dir;

  setUp(() async {
    db = newTestDatabase();
    dir = await Directory.systemTemp.createTemp('tane_auto_backup_test');
  });
  tearDown(() async {
    await db.close();
    if (dir.existsSync()) await dir.delete(recursive: true);
  });

  ExportImportService newExporter() => ExportImportService(
    repository: newTestRepository(db),
    files: _UnusedFiles(),
    keys: SecureKeyStore(store: InMemorySecretStore()),
  );

  AutoBackupService newService(
    ExportImportService exporter, {
    required AutoBackupStore store,
    required DateTime now,
  }) => AutoBackupService(
    exporter: exporter,
    store: store,
    directory: () async => dir,
    now: () => now,
  );

  List<String> autoCopies() =>
      dir
          .listSync()
          .whereType<File>()
          .map((f) => f.uri.pathSegments.last)
          .where((n) => n.startsWith('tanemaki-auto-'))
          .toList()
        ..sort();

  test('first run writes one sealed copy', () async {
    await newTestRepository(db).addQuickVariety(label: 'Maize');
    final service = newService(
      newExporter(),
      store: AutoBackupStore(InMemorySecretStore()),
      now: DateTime.utc(2026, 7, 10),
    );

    expect(await service.runIfDue(), isTrue);

    final copies = autoCopies();
    expect(copies, ['tanemaki-auto-2026-07-10.tanemaki']);
    final bytes = File('${dir.path}/${copies.single}').readAsBytesSync();
    expect(BackupBox.looksSealed(bytes), isTrue);
  });

  test('a second run within the interval is a no-op', () async {
    final store = AutoBackupStore(InMemorySecretStore());
    await store.markBackupAt(DateTime.utc(2026, 7, 8));
    final service = newService(
      newExporter(),
      store: store,
      now: DateTime.utc(2026, 7, 10), // 2 days later < 7-day interval
    );

    expect(await service.runIfDue(), isFalse);
    expect(autoCopies(), isEmpty);
  });

  test('a run after the interval writes again', () async {
    final store = AutoBackupStore(InMemorySecretStore());
    await store.markBackupAt(DateTime.utc(2026, 7, 1));
    final service = newService(
      newExporter(),
      store: store,
      now: DateTime.utc(2026, 7, 10), // 9 days later >= 7
    );

    expect(await service.runIfDue(), isTrue);
    expect(autoCopies(), ['tanemaki-auto-2026-07-10.tanemaki']);
  });

  test('rotation keeps only the newest three copies', () async {
    for (final d in ['2020-01-01', '2020-01-02', '2020-01-03', '2020-01-04']) {
      File('${dir.path}/tanemaki-auto-$d.tanemaki').writeAsBytesSync([0]);
    }
    final service = newService(
      newExporter(),
      store: AutoBackupStore(InMemorySecretStore()),
      now: DateTime.utc(2026, 7, 10),
    );

    await service.runIfDue();

    expect(autoCopies(), [
      'tanemaki-auto-2020-01-03.tanemaki',
      'tanemaki-auto-2020-01-04.tanemaki',
      'tanemaki-auto-2026-07-10.tanemaki',
    ]);
  });

  test('backupNow writes even when a copy is not yet due', () async {
    final store = AutoBackupStore(InMemorySecretStore());
    await store.markBackupAt(DateTime.utc(2026, 7, 10)); // today: not due
    final service = newService(
      newExporter(),
      store: store,
      now: DateTime.utc(2026, 7, 10),
    );

    // runIfDue is a no-op today, but an explicit tap still makes a copy.
    expect(await service.runIfDue(), isFalse);
    expect(await service.backupNow(), isTrue);
    expect(autoCopies(), ['tanemaki-auto-2026-07-10.tanemaki']);
  });

  test('a failed backup returns false and never throws', () async {
    final service = newService(
      _ThrowingExporter(db),
      store: AutoBackupStore(InMemorySecretStore()),
      now: DateTime.utc(2026, 7, 10),
    );

    expect(await service.runIfDue(), isFalse);
    expect(autoCopies(), isEmpty);
  });
}
