// test/sidecar_store_test.dart
//
// Verifies SidecarStore (§F.1 atomic write + .bak fallback):
//   * round-trip through disk preserves the model;
//   * a failed (interrupted) write leaves no partial sidecar at the target —
//     the previous good content survives;
//   * .bak recovery when the primary file is corrupt or missing.

import 'dart:convert';
import 'dart:io';

import 'package:flutter_test/flutter_test.dart';

import 'package:badnote/editor/engine/stroke_model.dart';
import 'package:badnote/models/bookmark.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';
import 'package:badnote/storage/sidecar_store.dart';

BadnoteSidecar _sidecar({String sourceFile = 'doc.pdf'}) => BadnoteSidecar(
      sourceFile: sourceFile,
      docType: 'pdf',
      pageCount: 3,
      strokes: {
        0: [
          EditorStroke(
            id: 's0',
            points: const [EditorPoint(x: 0.1, y: 0.2, pressure: 0.5)],
            color: 0xFF000000,
          ),
        ],
      },
      highlights: {
        0: const [SidecarHighlight(l: 0.1, t: 0.1, r: 0.9, b: 0.2)],
      },
      bookmarks: [
        Bookmark(
          id: 'bm',
          documentId: 'd',
          pageNumber: 1,
          createdAt: DateTime.utc(2026, 1, 1),
        ),
      ],
      scratchLinks: [
        SidecarScratchLink(
          link: const ScratchLink(
            id: 'a',
            documentId: 'd',
            pageIndex: 0,
            nx: 0.5,
            ny: 0.5,
          ),
          scratchpad: SidecarScratchpad(
            strokes: [
              InkStroke(
                id: 'ink',
                points: const [InkPoint(x: 1, y: 2, timestamp: 0)],
                tool: PenTool.pen,
                createdAt: DateTime.utc(2026, 1, 1),
              ),
            ],
          ),
        ),
      ],
    );

void main() {
  late Directory tmpDir;

  setUp(() async {
    tmpDir = await Directory.systemTemp.createTemp('sidecar_store_test');
  });

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

  File targetFile() => File('${tmpDir.path}/doc.pdf.badnote.json');

  test('writeAtomic then read round-trips the model', () async {
    final target = targetFile();
    final original = _sidecar();

    await SidecarStore.writeAtomic(target, original);
    expect(await target.exists(), isTrue);

    final loaded = await SidecarStore.read(target);
    expect(loaded, isNotNull);
    expect(loaded!.sourceFile, 'doc.pdf');
    expect(loaded.strokes[0], original.strokes[0]);
    expect(loaded.highlights[0], original.highlights[0]);
    expect(loaded.bookmarks, original.bookmarks);
    expect(loaded.scratchLinks, original.scratchLinks);
  });

  test('write leaves no leftover .tmp file', () async {
    final target = targetFile();
    await SidecarStore.writeAtomic(target, _sidecar());
    final tmp = File('${target.path}${SidecarStore.tmpSuffix}');
    expect(await tmp.exists(), isFalse);
  });

  test('writeAtomic creates parent directories', () async {
    final nested =
        File('${tmpDir.path}/nested/folder/doc.pdf.badnote.json');
    await SidecarStore.writeAtomic(nested, _sidecar());
    expect(await nested.exists(), isTrue);
  });

  test('second write keeps the previous content as .bak', () async {
    final target = targetFile();
    await SidecarStore.writeAtomic(target, _sidecar(sourceFile: 'v1.pdf'));
    await SidecarStore.writeAtomic(target, _sidecar(sourceFile: 'v2.pdf'));

    final bak = File('${target.path}${SidecarStore.bakSuffix}');
    expect(await bak.exists(), isTrue);

    final bakModel =
        BadnoteSidecar.fromJson(jsonDecode(await bak.readAsString()));
    expect(bakModel.sourceFile, 'v1.pdf'); // previous good copy

    final current = await SidecarStore.read(target);
    expect(current!.sourceFile, 'v2.pdf');
  });

  test('simulated interrupted write leaves previous good file intact', () async {
    final target = targetFile();
    await SidecarStore.writeAtomic(target, _sidecar(sourceFile: 'good.pdf'));

    // Simulate a crash mid-write: a partial .tmp is created but the rename
    // never happened.
    final tmp = File('${target.path}${SidecarStore.tmpSuffix}');
    await tmp.writeAsString('{ "badnoteSidecarVersion": 1, "sourceFil');

    // The target still holds the last good content — no partial leakage.
    final loaded = await SidecarStore.read(target);
    expect(loaded, isNotNull);
    expect(loaded!.sourceFile, 'good.pdf');
  });

  test('.bak recovery when primary is corrupt', () async {
    final target = targetFile();
    await SidecarStore.writeAtomic(target, _sidecar(sourceFile: 'v1.pdf'));
    await SidecarStore.writeAtomic(target, _sidecar(sourceFile: 'v2.pdf'));

    // Corrupt the primary file.
    await target.writeAsString('}{ not json at all');

    final recovered = await SidecarStore.read(target);
    expect(recovered, isNotNull);
    expect(recovered!.sourceFile, 'v1.pdf'); // fell back to .bak
  });

  test('.bak recovery when primary is missing', () async {
    final target = targetFile();
    await SidecarStore.writeAtomic(target, _sidecar(sourceFile: 'v1.pdf'));
    await SidecarStore.writeAtomic(target, _sidecar(sourceFile: 'v2.pdf'));

    await target.delete();

    final recovered = await SidecarStore.read(target);
    expect(recovered, isNotNull);
    expect(recovered!.sourceFile, 'v1.pdf');
  });

  test('read returns null when nothing exists', () async {
    final loaded = await SidecarStore.read(targetFile());
    expect(loaded, isNull);
  });
}
