// test/sidecar_repository_test.dart
//
// Phase 2: SidecarRepository is the pen editor's persistence sink (replacing the
// SQLite EditorRepository / DatabaseService scratch storage). These tests drive
// the repository directly with a tiny debounce + flush() so writes are
// deterministic, then RE-OPEN the same file and assert everything restored:
//   * per-page strokes
//   * per-page highlights (the previously in-memory-only data)
//   * scratch-link anchors + their embedded scratchpad ink (absolute world px)
//   * removing a highlight persists (the "un-highlight" action)

import 'dart:io';

import 'package:flutter_test/flutter_test.dart';

import 'package:badnote/editor/engine/stroke_model.dart';
import 'package:badnote/editor/persistence/sidecar_repository.dart';
import 'package:badnote/models/ink_point.dart';
import 'package:badnote/models/ink_stroke.dart';
import 'package:badnote/models/pen_tool.dart';
import 'package:badnote/models/scratch_link.dart';
import 'package:badnote/storage/badnote_sidecar.dart';

const _fast = Duration(milliseconds: 1);

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

InkStroke _ink(String id, double x) => InkStroke(
      id: id,
      points: [InkPoint(x: x, y: x + 1, timestamp: 0)],
      tool: PenTool.pen,
      createdAt: DateTime.utc(2026, 1, 1),
    );

void main() {
  late Directory tmpDir;
  late String src;

  setUp(() async {
    tmpDir = await Directory.systemTemp.createTemp('sidecar_repo_test');
    src = '${tmpDir.path}/Lecture.pdf';
    // The source file doesn't have to exist for the sidecar to work, but create
    // it so the layout matches reality (sidecar lives alongside the file).
    await File(src).writeAsString('%PDF-1.7 fake');
  });

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

  test('sidecar path is <sourceFile>.badnote.json alongside the file', () async {
    final repo = await SidecarRepository.open(src, debounce: _fast);
    expect(repo.sidecarFile.path, '$src.badnote.json');
    repo.dispose();
  });

  test('strokes + highlight + scratch-link + scratchpad restore on reopen',
      () async {
    final repo = await SidecarRepository.open(src, debounce: _fast);

    // Page 0 strokes.
    repo.scheduleStrokeSave(0, [_stroke('a'), _stroke('b')]);
    // Page 0 highlight (normalized rect).
    repo.scheduleHighlightSave(
      0,
      const [SidecarHighlight(l: 0.1, t: 0.15, r: 0.8, b: 0.2, color: 0xFFFFFF00)],
    );
    // A scratch-link anchor on page 3, then its private scratchpad ink.
    const link = ScratchLink(
      id: 'anchor-1',
      documentId: 'ignored-uses-path',
      pageIndex: 3,
      nx: 0.5,
      ny: 0.5,
    );
    repo.scheduleScratchLinkUpsert(link);
    repo.scheduleScratchpadSave(
      'anchor-1',
      SidecarScratchpad(
        canvasWidth: 5000,
        canvasHeight: 6000,
        strokes: [_ink('w0', 100), _ink('w1', 200)],
      ),
    );
    await repo.flush();
    repo.dispose();

    // Re-open the SAME file: everything must come back.
    final reopened = await SidecarRepository.open(src, debounce: _fast);

    expect(reopened.loadedStrokes[0]?.map((s) => s.id), ['a', 'b']);
    expect(reopened.loadedStrokes[0]!.first.color, 0xFF112233);

    final hl = reopened.loadedHighlights[0]!.single;
    expect(hl.l, 0.1);
    expect(hl.r, 0.8);
    expect(hl.color, 0xFFFFFF00);

    final sl = reopened.loadedScratchLinks.single;
    expect(sl.link.id, 'anchor-1');
    expect(sl.link.pageIndex, 3);
    expect(sl.scratchpad.canvasWidth, 5000);
    expect(sl.scratchpad.canvasHeight, 6000);
    expect(sl.scratchpad.strokes.map((s) => s.id), ['w0', 'w1']);
    expect(sl.scratchpad.strokes.first.points.single.x, 100);

    // The scratchpad is addressable by anchor id.
    expect(reopened.scratchpadFor('anchor-1')!.strokes.length, 2);
    expect(reopened.scratchpadFor('missing'), isNull);

    reopened.dispose();
  });

  test('removing a highlight persists (un-highlight)', () async {
    final repo = await SidecarRepository.open(src, debounce: _fast);
    repo.scheduleHighlightSave(0, const [
      SidecarHighlight(l: 0.1, t: 0.1, r: 0.4, b: 0.2),
      SidecarHighlight(l: 0.5, t: 0.5, r: 0.9, b: 0.6),
    ]);
    await repo.flush();
    repo.dispose();

    // Re-open, drop one highlight (mirrors _removeHighlightAt → save), reopen.
    final mid = await SidecarRepository.open(src, debounce: _fast);
    expect(mid.loadedHighlights[0]!.length, 2);
    final remaining = mid.loadedHighlights[0]!
        .where((h) => h.l != 0.1) // remove the first
        .toList();
    mid.scheduleHighlightSave(0, remaining);
    await mid.flush();
    mid.dispose();

    final after = await SidecarRepository.open(src, debounce: _fast);
    expect(after.loadedHighlights[0]!.length, 1);
    expect(after.loadedHighlights[0]!.single.l, 0.5);
    after.dispose();
  });

  test('removing the last highlight on a page clears the page entry', () async {
    final repo = await SidecarRepository.open(src, debounce: _fast);
    repo.scheduleHighlightSave(
        0, const [SidecarHighlight(l: 0.1, t: 0.1, r: 0.4, b: 0.2)]);
    await repo.flush();
    repo.scheduleHighlightSave(0, const []);
    await repo.flush();
    repo.dispose();

    final after = await SidecarRepository.open(src, debounce: _fast);
    expect(after.loadedHighlights.containsKey(0), isFalse);
    after.dispose();
  });

  test('deleting a scratch link removes it and its scratchpad', () async {
    final repo = await SidecarRepository.open(src, debounce: _fast);
    repo.scheduleScratchLinkUpsert(const ScratchLink(
      id: 'x',
      documentId: 'd',
      pageIndex: 0,
      nx: 0.2,
      ny: 0.2,
    ));
    repo.scheduleScratchpadSave(
      'x',
      SidecarScratchpad(strokes: [_ink('s', 1)]),
    );
    await repo.flush();
    repo.scheduleScratchLinkDelete('x');
    await repo.flush();
    repo.dispose();

    final after = await SidecarRepository.open(src, debounce: _fast);
    expect(after.loadedScratchLinks, isEmpty);
    after.dispose();
  });

  test('upserting a scratch link preserves its existing scratchpad', () async {
    final repo = await SidecarRepository.open(src, debounce: _fast);
    const link =
        ScratchLink(id: 'k', documentId: 'd', pageIndex: 1, nx: 0.1, ny: 0.1);
    repo.scheduleScratchLinkUpsert(link);
    repo.scheduleScratchpadSave('k', SidecarScratchpad(strokes: [_ink('s', 7)]));
    // Re-upsert the same anchor (e.g. moved) — scratchpad must survive.
    repo.scheduleScratchLinkUpsert(link.copyWith(nx: 0.9));
    await repo.flush();
    repo.dispose();

    final after = await SidecarRepository.open(src, debounce: _fast);
    final sl = after.loadedScratchLinks.single;
    expect(sl.link.nx, 0.9);
    expect(sl.scratchpad.strokes.single.id, 's');
    after.dispose();
  });

  group('Phase 4 standalone notebook (notebook.badnote.json)', () {
    late String notePath;

    setUp(() {
      // The synthetic note "source" is <folder>/notebook → sidecar is
      // <folder>/notebook.badnote.json (no real source file on disk).
      notePath = '${tmpDir.path}/notebook';
    });

    test('a note\'s strokes (page 0) + title round-trip through the sidecar',
        () async {
      final repo =
          await SidecarRepository.open(notePath, debounce: _fast, docType: 'notebook');
      expect(repo.sidecarFile.path, '$notePath.badnote.json');

      repo.scheduleTitleSave('My Note');
      repo.scheduleStrokeSave(0, [
        _stroke('p', tool: EditorTool.pen),
        _stroke('h', tool: EditorTool.highlighter),
      ]);
      await repo.flush();
      repo.dispose();

      // Re-open the same standalone notebook sidecar.
      final reopened =
          await SidecarRepository.open(notePath, debounce: _fast, docType: 'notebook');
      expect(reopened.loadedTitle, 'My Note');
      expect(reopened.sidecar.docType, 'notebook');
      final page0 = reopened.loadedStrokes[0]!;
      expect(page0.map((s) => s.id), ['p', 'h']);
      expect(page0.first.tool, EditorTool.pen);
      expect(page0.last.tool, EditorTool.highlighter);
      expect(page0.first.color, 0xFF112233);
      reopened.dispose();
    });

    test('editing the title persists; unchanged title is a no-op', () async {
      final repo =
          await SidecarRepository.open(notePath, debounce: _fast, docType: 'notebook');
      repo.scheduleTitleSave('First');
      await repo.flush();
      // Same title again: no write needed, but flush stays safe.
      repo.scheduleTitleSave('First');
      repo.scheduleTitleSave('Second');
      await repo.flush();
      repo.dispose();

      final reopened =
          await SidecarRepository.open(notePath, debounce: _fast, docType: 'notebook');
      expect(reopened.loadedTitle, 'Second');
      reopened.dispose();
    });
  });
}
