// test/vault_search_index_test.dart
//
// Phase 6: the search index is rebuilt by SCANNING the vault sidecars (the
// source of truth), NOT the SQLite cache. These tests seed a real vault on disk
// — a file-backed PDF notebook and a standalone free-ink notebook, each with a
// sidecar carrying typed text and/or handwriting OCR text — then assert
// VaultSearchIndex finds them by:
//   * the title / source filename,
//   * a typed text box (EditorStroke.textContent),
//   * the persisted handwriting OCR text (sidecar `ocrText`),
//   * a CJK substring (this user writes Chinese).
// It also documents the known GAP: a PDF's embedded text layer is NOT indexed.

import 'dart:io';

import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p;
import 'package:shared_preferences/shared_preferences.dart';

import 'package:badnote/editor/engine/stroke_model.dart';
import 'package:badnote/services/vault_search_index.dart';
import 'package:badnote/services/vault_service.dart';
import 'package:badnote/storage/badnote_sidecar.dart';
import 'package:badnote/storage/sidecar_store.dart';

EditorStroke _textStroke(String text) => EditorStroke(
      id: 't_$text',
      points: const [EditorPoint(x: 0.1, y: 0.2, pressure: 0.5)],
      tool: EditorTool.pen,
      color: 0xFF000000,
      width: 0.005,
      textContent: text,
    );

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();

  late Directory vaultDir;
  late VaultService vault;

  setUp(() async {
    SharedPreferences.setMockInitialValues({});
    vaultDir = await Directory.systemTemp.createTemp('vault_search_test');
    final prefs = await SharedPreferences.getInstance();
    vault = VaultService.forTest(prefs);
    await vault.setVaultRoot(vaultDir.path);
  });

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

  // Seed one file-backed PDF notebook: <vault>/<folder>/<file>.pdf + sidecar.
  Future<void> seedDocNotebook({
    required String folder,
    required String pdfName,
    List<EditorStroke> page0 = const [],
    String? ocrText,
  }) async {
    final dir = Directory(p.join(vaultDir.path, folder));
    await dir.create(recursive: true);
    final pdfPath = p.join(dir.path, pdfName);
    await File(pdfPath).writeAsString('%PDF-1.7 fake');
    final sidecar = BadnoteSidecar(
      sourceFile: pdfName,
      docType: 'pdf',
      strokes: page0.isEmpty ? null : {0: page0},
      ocrText: ocrText,
      createdAt: DateTime.now().toUtc(),
    );
    await SidecarStore.writeAtomic(
      File('$pdfPath$kVaultSidecarSuffix'),
      sidecar,
    );
  }

  // Seed one standalone free-ink notebook: <vault>/<folder>/notebook.badnote.json
  Future<void> seedNote({
    required String folder,
    required String title,
    List<EditorStroke> page0 = const [],
    String? ocrText,
  }) async {
    final dir = Directory(p.join(vaultDir.path, folder));
    await dir.create(recursive: true);
    final sidecar = BadnoteSidecar(
      docType: 'notebook',
      title: title,
      strokes: page0.isEmpty ? null : {0: page0},
      ocrText: ocrText,
      createdAt: DateTime.now().toUtc(),
    );
    await SidecarStore.writeAtomic(
      File(p.join(dir.path, kNotebookSidecarName)),
      sidecar,
    );
  }

  test('finds a file-backed PDF by its filename', () async {
    await seedDocNotebook(folder: 'Calculus Lecture 3', pdfName: 'Calculus.pdf');
    final index = VaultSearchIndex(vault);

    final hits = await index.search('calculus');
    expect(hits, hasLength(1));
    expect(hits.single.entry.isNote, isFalse);
    expect(hits.single.entry.docType, 'pdf');
    expect(hits.single.entry.openPath, endsWith('Calculus.pdf'));
  });

  test('finds a typed text box inside a PDF sidecar', () async {
    await seedDocNotebook(
      folder: 'Notes',
      pdfName: 'doc.pdf',
      page0: [_textStroke('eigenvalue decomposition')],
    );
    final index = VaultSearchIndex(vault);

    final hits = await index.search('eigenvalue');
    expect(hits, hasLength(1));
    expect(hits.single.entry.openPath, endsWith('doc.pdf'));
  });

  test('finds a standalone note by handwriting OCR text', () async {
    await seedNote(
      folder: 'My freehand notes',
      title: 'Untitled',
      ocrText: 'remember the quadratic formula',
    );
    final index = VaultSearchIndex(vault);

    final hits = await index.search('quadratic');
    expect(hits, hasLength(1));
    expect(hits.single.entry.isNote, isTrue);
    expect(hits.single.entry.docType, 'notebook');
    // Opening a note re-keys its synthetic `<folder>/notebook` path.
    expect(hits.single.entry.openPath, endsWith(kNotebookBaseName));
  });

  test('finds a note by its title and a CJK substring', () async {
    await seedNote(folder: '数学笔记', title: '微积分笔记', ocrText: '导数与积分');
    final index = VaultSearchIndex(vault);

    expect(await index.search('微积分'), hasLength(1));
    // CJK OCR substring (no inter-word spaces) still matches.
    expect(await index.search('导数'), hasLength(1));
  });

  test('searches BOTH notes and docs in one query', () async {
    await seedDocNotebook(
      folder: 'Doc',
      pdfName: 'd.pdf',
      page0: [_textStroke('shared keyword apple')],
    );
    await seedNote(folder: 'Note', title: 'n', ocrText: 'shared keyword apple');
    final index = VaultSearchIndex(vault);

    final hits = await index.search('apple');
    expect(hits, hasLength(2));
    expect(hits.where((h) => h.entry.isNote), hasLength(1));
    expect(hits.where((h) => !h.entry.isNote), hasLength(1));
  });

  test('empty query returns no hits', () async {
    await seedNote(folder: 'Note', title: 'anything');
    final index = VaultSearchIndex(vault);
    expect(await index.search('   '), isEmpty);
  });

  test('an empty/missing vault yields an empty index (never throws)', () async {
    final index = VaultSearchIndex(vault);
    await index.rebuild();
    expect(index.entries, isEmpty);
    expect(await index.search('x'), isEmpty);
  });

  test('KNOWN GAP: a PDF embedded text layer is NOT indexed', () async {
    // The sidecar carries no annotations; only the PDF body would contain the
    // word "bodytext". Search does NOT read the PDF text layer (documented
    // limitation), so this returns nothing.
    await seedDocNotebook(folder: 'Plain', pdfName: 'plain.pdf');
    final index = VaultSearchIndex(vault);
    expect(await index.search('bodytext'), isEmpty);
  });
}
