@TestOn('windows')
library;

import 'dart:ffi';
import 'dart:io';

import 'package:flutter_test/flutter_test.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:sqlite3/open.dart';
import 'package:pyramid/core/app_database.dart';

/// Diese Tests fahren die ECHTE Klartext→SQLCipher-Migration – mit derselben
/// SQLCipher-Bibliothek, die auch die App benutzt: der vom
/// sqlcipher_flutter_libs-Plugin gebauten `sqlite3.dll` aus dem Windows-Build.
/// Existiert kein Build-Output (z. B. auf dem Pi), werden sie übersprungen –
/// dann zählt der Lauf als UNGETESTET im Sinne von CLAUDE.md.
String? _findCipherDll() {
  for (final config in ['Debug', 'Profile', 'Release']) {
    final file = File('build/windows/x64/runner/$config/sqlite3.dll');
    if (file.existsSync()) return file.absolute.path;
  }
  return null;
}

const _hexKey =
    'a3b1c2d3e4f50617a3b1c2d3e4f50617a3b1c2d3e4f50617a3b1c2d3e4f50617';

void main() {
  final dllPath = _findCipherDll();
  final skip = dllPath == null
      ? 'SQLCipher-DLL fehlt (erst "flutter build windows" bzw. '
          '"flutter run -d windows" ausführen)'
      : null;

  // noIsolate: der Override muss im selben Isolate wirken wie die DB-Zugriffe.
  final factory = dllPath == null
      ? null
      : createDatabaseFactoryFfi(
          ffiInit: () {
            open.overrideFor(
              OperatingSystem.windows,
              () => DynamicLibrary.open(dllPath),
            );
          },
          noIsolate: true,
        );

  late Directory tempDir;

  setUp(() async {
    tempDir = await Directory.systemTemp.createTemp('pyramid_db_test');
  });

  tearDown(() async {
    try {
      await tempDir.delete(recursive: true);
    } catch (_) {}
  });

  /// Legt eine Klartext-DB mit zwei Tabellen und bekannten Zeilen an.
  Future<String> createPlaintextDb() async {
    final dbPath = '${tempDir.path}${Platform.pathSeparator}pyramid.sqlite';
    final db = await factory!.openDatabase(dbPath);
    await db.execute('CREATE TABLE box_client (k TEXT PRIMARY KEY, v TEXT)');
    await db.execute(
      'CREATE TABLE box_inbound_group_session (k TEXT PRIMARY KEY, v TEXT)',
    );
    for (var i = 0; i < 50; i++) {
      await db.insert('box_client', {'k': 'key$i', 'v': 'wert$i'});
    }
    for (var i = 0; i < 7; i++) {
      await db.insert(
        'box_inbound_group_session',
        {'k': 'session$i', 'v': 'geheim$i'},
      );
    }
    await db.close();
    return dbPath;
  }

  test('Geladene Bibliothek ist wirklich SQLCipher (cipher_version)',
      () async {
    final db = await factory!.openDatabase(inMemoryDatabasePath);
    final rows = await db.rawQuery('PRAGMA cipher_version');
    await db.close();
    expect(rows, isNotEmpty,
        reason: 'PRAGMA cipher_version leer → normales sqlite3 geladen');
    expect(rows.first.values.first.toString(), isNotEmpty);
  }, skip: skip);

  test('Header-Erkennung: Klartext ja, verschlüsselt nein, Mini-Datei ja',
      () async {
    final dbPath = await createPlaintextDb();
    expect(await AppDatabase.isPlaintextSqlite(File(dbPath)), isTrue);

    final tiny = File('${tempDir.path}${Platform.pathSeparator}tiny.sqlite');
    await tiny.writeAsString('kurz');
    expect(await AppDatabase.isPlaintextSqlite(tiny), isTrue);

    final encPath = '${tempDir.path}${Platform.pathSeparator}enc.sqlite';
    final enc = await AppDatabase.openEncrypted(factory!, encPath, _hexKey);
    await enc.execute('CREATE TABLE t (x INTEGER)');
    await enc.close();
    expect(await AppDatabase.isPlaintextSqlite(File(encPath)), isFalse);
  }, skip: skip);

  test('Migration: verschlüsselt, alle Zeilen erhalten, Backup liegt da',
      () async {
    final dbPath = await createPlaintextDb();

    final ok = await AppDatabase.migratePlaintextToEncrypted(
      factory: factory!,
      dbPath: dbPath,
      hexKey: _hexKey,
    );
    expect(ok, isTrue);

    // Datei ist jetzt verschlüsselt, Aufräum-Artefakte sind weg.
    expect(await AppDatabase.isPlaintextSqlite(File(dbPath)), isFalse);
    expect(File('$dbPath.enc').existsSync(), isFalse);
    expect(File('$dbPath.migrating').existsSync(), isFalse);

    // Byte-Backup existiert und ist die alte Klartext-DB.
    final backup = File('$dbPath.premigration');
    expect(backup.existsSync(), isTrue);
    expect(await AppDatabase.isPlaintextSqlite(backup), isTrue);

    // Inhalt vollständig? Mit Schlüssel öffnen und nachzählen.
    final db = await AppDatabase.openEncrypted(factory, dbPath, _hexKey);
    final clients = await db.rawQuery('SELECT count(*) AS c FROM box_client');
    final sessions = await db
        .rawQuery('SELECT count(*) AS c FROM box_inbound_group_session');
    final probe = await db.rawQuery(
        "SELECT v FROM box_inbound_group_session WHERE k = 'session3'");
    await db.close();
    expect(clients.first['c'], 50);
    expect(sessions.first['c'], 7);
    expect(probe.first['v'], 'geheim3');
  }, skip: skip);

  test('Falscher Schlüssel scheitert sofort beim Öffnen', () async {
    final dbPath = await createPlaintextDb();
    await AppDatabase.migratePlaintextToEncrypted(
      factory: factory!,
      dbPath: dbPath,
      hexKey: _hexKey,
    );
    final wrongKey = 'ff${_hexKey.substring(2)}';
    await expectLater(
      AppDatabase.openEncrypted(factory, dbPath, wrongKey),
      throwsA(anything),
    );
  }, skip: skip);

  test('Fehlschlag lässt die Klartext-DB unangetastet weiterlaufen', () async {
    final dbPath = await createPlaintextDb();
    // Fehler erzwingen: an der Stelle des Exports liegt ein VERZEICHNIS –
    // ATTACH kann die Datei nicht anlegen.
    await Directory('$dbPath.enc').create();

    final ok = await AppDatabase.migratePlaintextToEncrypted(
      factory: factory!,
      dbPath: dbPath,
      hexKey: _hexKey,
    );
    expect(ok, isFalse);

    // Original unversehrt, Marker weg, App könnte normal weiterlaufen.
    expect(await AppDatabase.isPlaintextSqlite(File(dbPath)), isTrue);
    expect(File('$dbPath.migrating').existsSync(), isFalse);
    final db = await factory.openDatabase(dbPath);
    final clients = await db.rawQuery('SELECT count(*) AS c FROM box_client');
    await db.close();
    expect(clients.first['c'], 50);
  }, skip: skip);
}
