import 'dart:convert';
import 'dart:typed_data';

import 'package:commons_core/commons_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tane/data/export_import/inventory_json_codec.dart';
import 'package:tane/db/database.dart';
import 'package:tane/i18n/strings.g.dart';
import 'package:tane/security/secure_key_store.dart';
import 'package:tane/services/export_import_service.dart';
import 'package:tane/services/file_service.dart';
import 'package:tane/services/recovery_sheet_service.dart';
import 'package:tane/ui/backup_section.dart';
import 'package:tane/ui/settings_screen.dart';

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

/// Records calls instead of opening real dialogs.
class FakeFileService implements FileService {
  String? savedName;
  Uint8List? savedBytes;
  Uint8List? bytesToPick;

  @override
  Future<String?> saveFile({
    required String suggestedName,
    required Uint8List bytes,
  }) async {
    savedName = suggestedName;
    savedBytes = bytes;
    return '/picked/$suggestedName';
  }

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

void main() {
  late AppDatabase db;
  late FakeFileService files;
  late SecureKeyStore keys;
  late ExportImportService service;
  late RecoverySheetService sheet;

  setUp(() {
    db = newTestDatabase();
    files = FakeFileService();
    keys = SecureKeyStore(store: InMemorySecretStore());
    service = ExportImportService(
      repository: newTestRepository(db),
      files: files,
      keys: keys,
    );
    sheet = RecoverySheetService(files: files);
  });
  tearDown(() => db.close());

  Widget wrap(Widget child) {
    LocaleSettings.setLocaleSync(AppLocale.en);
    return TranslationProvider(
      child: MaterialApp(
        supportedLocales: AppLocaleUtils.supportedLocales,
        localizationsDelegates: const [
          GlobalMaterialLocalizations.delegate,
          GlobalWidgetsLocalizations.delegate,
          GlobalCupertinoLocalizations.delegate,
        ],
        home: child,
      ),
    );
  }

  Widget section() => wrap(
    Scaffold(
      body: BackupSection(service: service, sheet: sheet),
    ),
  );

  testWidgets('settings shows the backup section with its actions', (
    tester,
  ) async {
    await tester.binding.setSurfaceSize(const Size(800, 1400));
    addTearDown(() => tester.binding.setSurfaceSize(null));
    await tester.pumpWidget(wrap(SettingsScreen(exportImport: service)));

    expect(find.text('Backup & restore'), findsOneWidget);
    expect(find.text('Save a backup'), findsOneWidget);
    expect(find.text('Restore a backup'), findsOneWidget);
    expect(find.text('Your recovery code'), findsOneWidget);
    expect(find.text('Export to a spreadsheet'), findsOneWidget);
    expect(find.text('Import a list'), findsOneWidget);
  });

  testWidgets('spreadsheet export saves a .csv file and confirms', (
    tester,
  ) async {
    await tester.pumpWidget(section());
    await tester.tap(find.text('Export to a spreadsheet'));
    await tester.pumpAndSettle();

    expect(files.savedName, endsWith('.csv'));
    expect(utf8.decode(files.savedBytes!), startsWith('variety_id,'));
    expect(find.text('Copy saved'), findsOneWidget);
  });

  testWidgets('saving a backup writes a sealed .tanemaki file', (tester) async {
    await tester.pumpWidget(section());
    await tester.tap(find.text('Save a backup'));
    await tester.pumpAndSettle();

    expect(files.savedName, endsWith('.tanemaki'));
    expect(BackupBox.looksSealed(files.savedBytes!), isTrue);
    expect(find.text('Copy saved'), findsOneWidget);
  });

  testWidgets('restoring my own sealed copy needs no code', (tester) async {
    final otherDb = newTestDatabase();
    addTearDown(otherDb.close);
    final otherFiles = FakeFileService();
    final mine = ExportImportService(
      repository: newTestRepository(otherDb, nodeId: 'other'),
      files: otherFiles,
      keys: keys, // same identity
    );
    final other = newTestRepository(otherDb, nodeId: 'other');
    await other.addQuickVariety(label: 'Imported bean');
    await mine.exportBackup();
    files.bytesToPick = otherFiles.savedBytes;

    await tester.pumpWidget(section());
    await tester.tap(find.text('Restore a backup'));
    await tester.pumpAndSettle();
    expect(find.text('Restore a backup?'), findsOneWidget);

    await tester.tap(find.text('Import'));
    await tester.pumpAndSettle();

    expect(find.text('Imported: 1 new, 0 updated'), findsOneWidget);
    final varieties = await db.select(db.varieties).get();
    expect(varieties.single.label, 'Imported bean');
  });

  testWidgets('a copy from another identity asks for the recovery code', (
    tester,
  ) async {
    final otherDb = newTestDatabase();
    addTearDown(otherDb.close);
    final otherFiles = FakeFileService();
    final otherKeys = SecureKeyStore(store: InMemorySecretStore());
    final theirs = ExportImportService(
      repository: newTestRepository(otherDb, nodeId: 'other'),
      files: otherFiles,
      keys: otherKeys,
    );
    final other = newTestRepository(otherDb, nodeId: 'other');
    await other.addQuickVariety(label: 'Recovered bean');
    await theirs.exportBackup();
    final code = await theirs.recoveryCode();
    files.bytesToPick = otherFiles.savedBytes;

    await tester.pumpWidget(section());
    await tester.tap(find.text('Restore a backup'));
    await tester.pumpAndSettle();
    await tester.tap(find.text('Import'));
    await tester.pumpAndSettle();

    // Wrong seed → the code prompt appears; type the printed code.
    expect(find.text('Enter your recovery code'), findsOneWidget);
    await tester.enterText(find.byKey(const Key('backup.recoveryField')), code);
    await tester.tap(find.byKey(const Key('backup.recoveryConfirm')));
    await tester.pumpAndSettle();

    expect(find.text('Imported: 1 new, 0 updated'), findsOneWidget);
    final varieties = await db.select(db.varieties).get();
    expect(varieties.single.label, 'Recovered bean');
    // The identity travelled with the code.
    expect(await keys.rootSeedHex(), await otherKeys.rootSeedHex());
  });

  testWidgets('legacy plain exports still restore', (tester) async {
    final otherDb = newTestDatabase();
    addTearDown(otherDb.close);
    final other = newTestRepository(otherDb, nodeId: 'node-other');
    await other.addQuickVariety(label: 'Old bean');
    files.bytesToPick = utf8.encode(
      const InventoryJsonCodec().encode(await other.exportInventory()),
    );

    await tester.pumpWidget(section());
    await tester.tap(find.text('Restore a backup'));
    await tester.pumpAndSettle();
    await tester.tap(find.text('Import'));
    await tester.pumpAndSettle();

    expect(find.text('Imported: 1 new, 0 updated'), findsOneWidget);
  });

  testWidgets('an unreadable file reports a friendly error', (tester) async {
    files.bytesToPick = utf8.encode('this is not json');
    await tester.pumpWidget(section());
    await tester.tap(find.text('Restore a backup'));
    await tester.pumpAndSettle();
    await tester.tap(find.text('Import'));
    await tester.pumpAndSettle();

    expect(
      find.text('This file could not be read as a Tanemaki copy'),
      findsOneWidget,
    );
  });

  testWidgets('the recovery dialog shows the code and saves the sheet', (
    tester,
  ) async {
    await tester.pumpWidget(section());
    await tester.tap(find.byKey(const Key('backup.recovery')));
    await tester.pumpAndSettle();

    final code = await service.recoveryCode();
    expect(find.text(code), findsOneWidget);

    await tester.tap(find.byKey(const Key('backup.recoverySave')));
    await tester.pumpAndSettle();

    expect(files.savedName, 'tanemaki-recovery.pdf');
    expect(String.fromCharCodes(files.savedBytes!.take(5)), '%PDF-');
    // The seed itself never appears in the clear in the PDF stream metadata;
    // the QR encodes the human code, which IS the secret — that's the point
    // of the sheet. Just assert it saved and confirmed.
    expect(find.text('Copy saved'), findsOneWidget);
  });
}
