// test/scratch_link_test.dart
//
// Guards the PDF-anchored scratch-link feature:
//   1. ScratchLink toJson/fromJson round-trip (pure model).
//   2. saveScratchLink / loadScratchLinks / deleteScratchLink round-trip a set
//      of anchors through DatabaseService (anchors survive a reload).
//   3. A per-anchor scratchpad keyed by the anchor id is private to that anchor
//      and is removed when the anchor is deleted (proves the reuse of the
//      existing scratchpad storage keyed by anchor id, not document id).
//
// DatabaseService.getInstance() needs getApplicationDocumentsDirectory(); we
// mock PathProviderPlatform to a temp dir so the singleton opens a real (ffi)
// sqlite DB on disk.

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

import 'package:flutter_test/flutter_test.dart';
// ignore: depend_on_referenced_packages
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
// ignore: depend_on_referenced_packages
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.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/services/database_service.dart';

class _FakePathProvider extends PathProviderPlatform
    with MockPlatformInterfaceMixin {
  _FakePathProvider(this.dir);
  final String dir;
  @override
  Future<String?> getApplicationDocumentsPath() async => dir;
}

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();
  sqfliteFfiInit();
  databaseFactory = databaseFactoryFfi;

  group('ScratchLink model', () {
    test('toJson/fromJson round-trips', () {
      const link = ScratchLink(
        id: 'anchor-1',
        documentId: 'doc-abc',
        pageIndex: 3,
        nx: 0.25,
        ny: 0.8,
      );
      final restored = ScratchLink.fromJson(jsonDecode(jsonEncode(link.toJson()))
          as Map<String, dynamic>);
      expect(restored, link);
      expect(restored.pageIndex, 3);
      expect(restored.nx, 0.25);
      expect(restored.ny, 0.8);
    });
  });

  group('ScratchLink persistence', () {
    late Directory tmp;

    setUp(() async {
      tmp = await Directory.systemTemp.createTemp('scratch_link_test_');
      final dbFile = File('${tmp.path}/badnote.db');
      if (dbFile.existsSync()) dbFile.deleteSync();
      PathProviderPlatform.instance = _FakePathProvider(tmp.path);
      await DatabaseService.resetForTest();
    });

    tearDown(() async {
      await DatabaseService.resetForTest();
      try {
        tmp.deleteSync(recursive: true);
      } catch (_) {}
    });

    test('save / load / delete anchors round-trip', () async {
      final db = await DatabaseService.getInstance();

      const a = ScratchLink(
        id: 'a',
        documentId: 'doc-1',
        pageIndex: 0,
        nx: 0.1,
        ny: 0.2,
      );
      const b = ScratchLink(
        id: 'b',
        documentId: 'doc-1',
        pageIndex: 4,
        nx: 0.7,
        ny: 0.9,
      );
      // A different document's anchor must not leak into doc-1's list.
      const other = ScratchLink(
        id: 'c',
        documentId: 'doc-2',
        pageIndex: 1,
        nx: 0.5,
        ny: 0.5,
      );

      await db.saveScratchLink(a);
      await db.saveScratchLink(b);
      await db.saveScratchLink(other);

      final loaded = await db.loadScratchLinks('doc-1');
      expect(loaded.length, 2);
      expect(loaded.map((l) => l.id).toSet(), {'a', 'b'});
      final reloadedA = loaded.firstWhere((l) => l.id == 'a');
      expect(reloadedA.pageIndex, 0);
      expect(reloadedA.nx, 0.1);
      expect(reloadedA.ny, 0.2);

      // doc-2 keeps its own anchor.
      expect((await db.loadScratchLinks('doc-2')).single.id, 'c');

      await db.deleteScratchLink('a');
      final after = await db.loadScratchLinks('doc-1');
      expect(after.map((l) => l.id), ['b']);
    });

    test('each anchor has its own private scratchpad keyed by anchor id',
        () async {
      final db = await DatabaseService.getInstance();

      InkStroke stroke(String id, double x) => InkStroke(
            id: id,
            points: [
              InkPoint(x: x, y: x, pressure: 0.5, timestamp: 0),
            ],
            tool: PenTool.pen,
            createdAt: DateTime.fromMillisecondsSinceEpoch(0),
          );

      // Two anchors, two different private scratchpads (keyed by anchor id).
      final inkA = [stroke('s1', 10)];
      final inkB = [stroke('s2', 20)];
      await db.saveScratchpad(
          'anchor-A', jsonEncode(inkA.map((s) => s.toJson()).toList()));
      await db.saveScratchpad(
          'anchor-B', jsonEncode(inkB.map((s) => s.toJson()).toList()));

      final loadedA = await db.loadScratchpad('anchor-A');
      final loadedB = await db.loadScratchpad('anchor-B');
      expect(loadedA.single.points.single.x, 10);
      expect(loadedB.single.points.single.x, 20);

      // Deleting the anchor removes its private scratchpad too.
      await db.saveScratchLink(const ScratchLink(
        id: 'anchor-A',
        documentId: 'doc-9',
        pageIndex: 0,
        nx: 0.0,
        ny: 0.0,
      ));
      await db.deleteScratchLink('anchor-A');
      expect(await db.loadScratchpad('anchor-A'), isEmpty);
      // anchor-B is untouched.
      expect((await db.loadScratchpad('anchor-B')).single.points.single.x, 20);
    });
  });
}
