// test/sidecar_flush_observer_test.dart
//
// Phase 6 / §F.3: lifecycle-flush hardening. Proves the data-safety guarantee
// "never lose the last strokes on app close":
//   * an OPEN SidecarRepository registers itself in SidecarRepositoryRegistry;
//   * a pending (debounced, not-yet-fired) write is flushed to disk when the
//     SidecarFlushObserver receives a paused/inactive/detached lifecycle event,
//     WITHOUT waiting for the debounce timer;
//   * the flush is awaited (SidecarRepositoryRegistry.flushAll awaits each
//     repo.flush()), so the bytes are on disk before the process could freeze;
//   * dispose() unregisters the repo so it isn't flushed after closing.

import 'dart:io';

import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';

import 'package:badnote/editor/engine/stroke_model.dart';
import 'package:badnote/editor/persistence/sidecar_flush_observer.dart';
import 'package:badnote/editor/persistence/sidecar_repository.dart';
import 'package:badnote/storage/sidecar_store.dart';

// A long debounce so the timer NEVER fires during the test — only an explicit
// flush (the lifecycle path) can persist the pending write.
const _slow = Duration(seconds: 30);

EditorStroke _stroke(String id) => EditorStroke(
      id: id,
      points: const [EditorPoint(x: 0.1, y: 0.2, pressure: 0.5)],
      tool: EditorTool.pen,
      color: 0xFF112233,
      width: 0.005,
    );

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();

  late Directory tmpDir;
  late String src;

  setUp(() async {
    SidecarRepositoryRegistry.resetForTest();
    tmpDir = await Directory.systemTemp.createTemp('flush_observer_test');
    src = '${tmpDir.path}/Lecture.pdf';
    await File(src).writeAsString('%PDF-1.7 fake');
  });

  tearDown(() async {
    SidecarRepositoryRegistry.resetForTest();
    if (await tmpDir.exists()) await tmpDir.delete(recursive: true);
  });

  test('open registers the repo; dispose unregisters it', () async {
    final repo = await SidecarRepository.open(src, docType: 'pdf', debounce: _slow);
    expect(SidecarRepositoryRegistry.open, contains(repo));
    expect(SidecarRepositoryRegistry.forPath(src), same(repo));

    repo.dispose();
    expect(SidecarRepositoryRegistry.open, isNot(contains(repo)));
    expect(SidecarRepositoryRegistry.forPath(src), isNull);
  });

  test('a paused lifecycle event flushes a pending debounced write to disk',
      () async {
    final repo = await SidecarRepository.open(src, docType: 'pdf', debounce: _slow);
    addTearDown(repo.dispose);

    // Schedule a write; the 30s debounce means nothing is on disk yet.
    repo.scheduleStrokeSave(0, [_stroke('s1')]);
    expect(await repo.sidecarFile.exists(), isFalse,
        reason: 'debounce has not fired and no lifecycle flush yet');

    // The app goes to the background → the observer drains pending writes.
    final observer = SidecarFlushObserver()..attach();
    addTearDown(observer.detach);
    observer.didChangeAppLifecycleState(AppLifecycleState.paused);

    // flushAll() is fire-and-forget at the framework boundary; await the same
    // path the observer triggered so we can assert the bytes landed.
    await SidecarRepositoryRegistry.flushAll();

    expect(await repo.sidecarFile.exists(), isTrue);
    final reloaded = await SidecarStore.read(repo.sidecarFile);
    expect(reloaded, isNotNull);
    expect(reloaded!.strokes[0]?.single.id, 's1');
  });

  test('flushAll drains EVERY open repo', () async {
    final a = await SidecarRepository.open(
        '${tmpDir.path}/A.pdf', docType: 'pdf', debounce: _slow);
    final b = await SidecarRepository.open(
        '${tmpDir.path}/B.pdf', docType: 'pdf', debounce: _slow);
    addTearDown(a.dispose);
    addTearDown(b.dispose);

    a.scheduleStrokeSave(0, [_stroke('a1')]);
    b.scheduleStrokeSave(0, [_stroke('b1')]);

    await SidecarRepositoryRegistry.flushAll();

    expect(await a.sidecarFile.exists(), isTrue);
    expect(await b.sidecarFile.exists(), isTrue);
  });

  test('a disposed repo is not flushed by a later lifecycle event', () async {
    final repo = await SidecarRepository.open(src, docType: 'pdf', debounce: _slow);
    repo.scheduleStrokeSave(0, [_stroke('s1')]);
    // Closing the editor without flushing: dispose() cancels the timer AND
    // unregisters, so a later background event can't resurrect it.
    repo.dispose();

    final observer = SidecarFlushObserver()..attach();
    addTearDown(observer.detach);
    observer.didChangeAppLifecycleState(AppLifecycleState.detached);
    await SidecarRepositoryRegistry.flushAll();

    // The dropped repo wrote nothing (its pending edit is intentionally lost on
    // an explicit dispose-without-flush; the editors flush in their own
    // dispose() before calling this).
    expect(await repo.sidecarFile.exists(), isFalse);
  });
}
