// test/pdf_text_indexer_test.dart
//
// Unit tests for the import-time PDF document-body indexer. The two native text
// sources (pdfrx embedded-text loader + page-render OCR) are FAKED here — no
// real pdfium render and no real OCR channel run in CI; those are exercised only
// on-device (see PdfrxPageTextSource). These tests pin:
//   * the text-layer-vs-rasterized DECISION (empty text layer → OCR fallback),
//   * the background flow: embedded text OR OCR text lands in the sidecar's
//     `pageText`,
//   * IDEMPOTENCY: a sidecar that already has `pageText` is not re-indexed,
//   * graceful degradation: no OCR backend (empty OCR result) → no `pageText`,
//     no crash.

import 'dart:io';

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

import 'package:badnote/editor/persistence/sidecar_repository.dart';
import 'package:badnote/services/pdf_text_indexer.dart';
import 'package:badnote/storage/badnote_sidecar.dart';
import 'package:badnote/storage/sidecar_store.dart';

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();

  late Directory dir;
  late String pdfPath;

  setUp(() async {
    SidecarRepositoryRegistry.resetForTest();
    dir = await Directory.systemTemp.createTemp('pdf_indexer_test');
    pdfPath = p.join(dir.path, 'doc.pdf');
    await File(pdfPath).writeAsString('%PDF-1.7 fake');
  });

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

  Future<BadnoteSidecar?> readSidecar() =>
      SidecarStore.read(File('$pdfPath$kSidecarSuffix'));

  // A text-layer PDF: embedded loader returns real text, OCR should NOT run.
  test('a PDF with a usable text layer persists the embedded text', () async {
    var ocrCalled = false;
    final indexer = PdfTextIndexer(
      loadEmbeddedText: (_) async => ['Chapter one introduction', 'page two'],
      ocrPages: (_) async {
        ocrCalled = true;
        return ['should not be used'];
      },
    );

    final result = await indexer.indexPdf(pdfPath);

    expect(ocrCalled, isFalse, reason: 'text layer present → no OCR');
    expect(result, contains('Chapter one introduction'));
    expect(result, contains('page two'));
    final sidecar = await readSidecar();
    expect(sidecar?.pageText, contains('introduction'));
  });

  // A rasterized/scanned PDF: empty embedded text → OCR fallback runs.
  test('a rasterized PDF (empty text layer) is OCR\'d into pageText', () async {
    var ocrCalled = false;
    final indexer = PdfTextIndexer(
      loadEmbeddedText: (_) async => ['', '', ''], // scanned: no text layer
      ocrPages: (_) async {
        ocrCalled = true;
        return ['扫描出的文字', 'recognized line two'];
      },
    );

    final result = await indexer.indexPdf(pdfPath);

    expect(ocrCalled, isTrue, reason: 'no text layer → OCR runs');
    expect(result, contains('扫描出的文字'));
    final sidecar = await readSidecar();
    expect(sidecar?.pageText, contains('扫描出的文字'));
    expect(sidecar?.pageText, contains('recognized line two'));
  });

  test('the text-layer decision: empty/near-empty text → needs OCR', () {
    final indexer = PdfTextIndexer(
      loadEmbeddedText: (_) async => const [],
      ocrPages: (_) async => const [],
      textLayerThreshold: 16,
    );

    // Empty pages → rasterized (needs OCR).
    expect(indexer.hasUsableTextLayer(['', '', '']), isFalse);
    // A few stray ligature chars below threshold → still rasterized.
    expect(indexer.hasUsableTextLayer(['  ', 'fi', ' ']), isFalse);
    // Real text above threshold → usable text layer.
    expect(
      indexer.hasUsableTextLayer(['This is real printed body text']),
      isTrue,
    );
  });

  test('idempotent: a sidecar that already has pageText is not re-indexed',
      () async {
    // Pre-seed a sidecar with existing pageText.
    await SidecarStore.writeAtomic(
      File('$pdfPath$kSidecarSuffix'),
      BadnoteSidecar(
        sourceFile: 'doc.pdf',
        docType: 'pdf',
        pageText: 'already indexed body',
      ),
    );

    var embeddedCalled = false;
    var ocrCalled = false;
    final indexer = PdfTextIndexer(
      loadEmbeddedText: (_) async {
        embeddedCalled = true;
        return ['new text'];
      },
      ocrPages: (_) async {
        ocrCalled = true;
        return ['new ocr'];
      },
    );

    final result = await indexer.indexPdf(pdfPath);

    expect(result, isNull, reason: 'already indexed → no-op');
    expect(embeddedCalled, isFalse);
    expect(ocrCalled, isFalse);
    // Existing text untouched.
    final sidecar = await readSidecar();
    expect(sidecar?.pageText, 'already indexed body');
  });

  test('no OCR backend (empty OCR result) leaves pageText unset, no crash',
      () async {
    final indexer = PdfTextIndexer(
      loadEmbeddedText: (_) async => ['', ''], // rasterized
      ocrPages: (_) async => const [], // no OCR backend → clean no-op
    );

    final result = await indexer.indexPdf(pdfPath);

    expect(result, isNull);
    final sidecar = await readSidecar();
    // No sidecar written (nothing to index) — or, if present, no pageText.
    expect(sidecar?.pageText, isNull);
  });

  test('writes THROUGH an open editor repo (no second on-disk race)', () async {
    // Open a repo for the same path: the indexer must write through it.
    final repo = await SidecarRepository.open(pdfPath, docType: 'pdf');
    addTearDown(repo.dispose);

    final indexer = PdfTextIndexer(
      loadEmbeddedText: (_) async => ['printed body via open editor'],
      ocrPages: (_) async => const [],
    );

    final result = await indexer.indexPdf(pdfPath);

    expect(result, contains('printed body'));
    // The in-memory sidecar held by the editor now carries the pageText.
    expect(repo.loadedPageText, contains('printed body'));
    // And it was flushed to disk.
    final sidecar = await readSidecar();
    expect(sidecar?.pageText, contains('printed body'));
  });

  test('preserves existing annotations when merging pageText on disk', () async {
    // A sidecar with annotations but no pageText (e.g. user annotated before the
    // background OCR finished). Indexing must not clobber the annotations.
    await SidecarStore.writeAtomic(
      File('$pdfPath$kSidecarSuffix'),
      BadnoteSidecar(
        sourceFile: 'doc.pdf',
        docType: 'pdf',
        ocrText: 'handwriting note',
      ),
    );

    final indexer = PdfTextIndexer(
      loadEmbeddedText: (_) async => ['printed document body text here'],
      ocrPages: (_) async => const [],
    );
    await indexer.indexPdf(pdfPath);

    final sidecar = await readSidecar();
    expect(sidecar?.ocrText, 'handwriting note'); // preserved
    expect(sidecar?.pageText, contains('printed document body'));
  });
}
