// Proves the Phase-0 vault root recording: the path persists through
// SharedPreferences, vaultRootValid() is true ONLY when a path is set AND its
// directory exists, and clearVaultRoot() forgets it. Uses a real temp dir for
// existence checks and a mock SharedPreferences for persistence.

import 'dart:io';

import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';

import 'package:badnote/services/vault_service.dart';
import 'package:badnote/storage/sidecar_store.dart';

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();

  late Directory tempDir;

  setUp(() async {
    SharedPreferences.setMockInitialValues({});
    tempDir = await Directory.systemTemp.createTemp('vault_service_test');
  });

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

  Future<VaultService> makeService() async {
    final prefs = await SharedPreferences.getInstance();
    return VaultService.forTest(prefs);
  }

  test('starts with no vault root and is invalid', () async {
    final vault = await makeService();
    expect(vault.vaultRoot, isNull);
    expect(await vault.vaultRootValid(), isFalse);
  });

  test('setVaultRoot persists the path', () async {
    final vault = await makeService();
    await vault.setVaultRoot(tempDir.path);
    expect(vault.vaultRoot, tempDir.path);

    // Re-read through a fresh service over the SAME prefs store: persisted.
    final prefs = await SharedPreferences.getInstance();
    expect(prefs.getString(VaultService.vaultRootKey), tempDir.path);
    final reopened = VaultService.forTest(prefs);
    expect(reopened.vaultRoot, tempDir.path);
  });

  test('vaultRootValid is true only when set AND the dir exists', () async {
    final vault = await makeService();

    // Set to an existing dir → valid.
    await vault.setVaultRoot(tempDir.path);
    expect(await vault.vaultRootValid(), isTrue);

    // Point at a path that does not exist → invalid (folder went missing).
    final missing = '${tempDir.path}${Platform.pathSeparator}gone';
    await vault.setVaultRoot(missing);
    expect(vault.vaultRoot, missing);
    expect(await vault.vaultRootValid(), isFalse);
  });

  test('clearVaultRoot forgets the path and becomes invalid', () async {
    final vault = await makeService();
    await vault.setVaultRoot(tempDir.path);
    expect(await vault.vaultRootValid(), isTrue);

    await vault.clearVaultRoot();
    expect(vault.vaultRoot, isNull);
    expect(await vault.vaultRootValid(), isFalse);
  });

  test('empty stored path is treated as invalid', () async {
    final vault = await makeService();
    await vault.setVaultRoot('');
    expect(await vault.vaultRootValid(), isFalse);
  });

  group('createNotebook', () {
    late Directory srcDir;

    setUp(() async {
      srcDir = await Directory.systemTemp.createTemp('vault_src');
    });

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

    Future<String> makeSource(String name, [String content = 'pdf-bytes']) async {
      final f = File('${srcDir.path}${Platform.pathSeparator}$name');
      await f.writeAsString(content);
      return f.path;
    }

    test('creates folder, copies file, returns the in-vault path', () async {
      final vault = await makeService();
      await vault.setVaultRoot(tempDir.path);
      final src = await makeSource('Calculus Lecture 3.pdf', 'hello');

      final vaultPath = await vault.createNotebook(src);

      // Returned path is inside the vault, in a folder named from the file.
      final expectedFolder =
          '${tempDir.path}${Platform.pathSeparator}Calculus Lecture 3';
      expect(Directory(expectedFolder).existsSync(), isTrue);
      expect(
        vaultPath,
        '$expectedFolder${Platform.pathSeparator}Calculus Lecture 3.pdf',
      );
      // The file was copied with its contents intact.
      expect(File(vaultPath).existsSync(), isTrue);
      expect(File(vaultPath).readAsStringSync(), 'hello');
      // The original is untouched (copy, not move).
      expect(File(src).existsSync(), isTrue);
    });

    test('de-duplicates the folder name on collision', () async {
      final vault = await makeService();
      await vault.setVaultRoot(tempDir.path);

      final a = await makeSource('Notes.pdf', 'A');
      final firstPath = await vault.createNotebook(a);
      expect(
        firstPath,
        '${tempDir.path}${Platform.pathSeparator}Notes${Platform.pathSeparator}Notes.pdf',
      );

      // Re-import a file with the same basename → suffixed folder.
      final b = await makeSource('Notes.pdf', 'B');
      final secondPath = await vault.createNotebook(b);
      expect(
        secondPath,
        '${tempDir.path}${Platform.pathSeparator}Notes 2${Platform.pathSeparator}Notes.pdf',
      );
      expect(File(secondPath).readAsStringSync(), 'B');
      // First copy still intact.
      expect(File(firstPath).readAsStringSync(), 'A');
    });

    test('throws when no vault root is set', () async {
      final vault = await makeService();
      final src = await makeSource('x.pdf');
      expect(() => vault.createNotebook(src), throwsStateError);
    });
  });

  group('scanNotebooks', () {
    Future<String> writeFile(String path, [String content = 'x']) async {
      final f = File(path);
      await f.create(recursive: true);
      await f.writeAsString(content);
      return f.path;
    }

    test('empty / missing vault yields an empty list', () async {
      final vault = await makeService();
      // No root set.
      expect(await vault.scanNotebooks(), isEmpty);
      // Root set but empty.
      await vault.setVaultRoot(tempDir.path);
      expect(await vault.scanNotebooks(), isEmpty);
    });

    test('lists notebook folders that contain a source file', () async {
      final vault = await makeService();
      await vault.setVaultRoot(tempDir.path);
      final sep = Platform.pathSeparator;

      // A PDF notebook with a sidecar.
      await writeFile('${tempDir.path}${sep}Lecture${sep}Lecture.pdf');
      await writeFile(
        '${tempDir.path}${sep}Lecture${sep}Lecture.pdf.badnote.json',
        '{}',
      );
      // A PPTX notebook without a sidecar.
      await writeFile('${tempDir.path}${sep}Deck${sep}Deck.pptx');
      // A hidden vault-metadata folder is skipped.
      await writeFile('${tempDir.path}$sep.badnote${sep}vault.json', '{}');
      // A folder with no importable file is skipped.
      await writeFile('${tempDir.path}${sep}Empty${sep}readme.txt');

      final notebooks = await vault.scanNotebooks();
      final byName = {for (final n in notebooks) n.filename: n};

      expect(byName.keys, containsAll(['Lecture.pdf', 'Deck.pptx']));
      expect(notebooks.length, 2);

      expect(byName['Lecture.pdf']!.docType, 'pdf');
      expect(byName['Lecture.pdf']!.hasSidecar, isTrue);
      expect(byName['Deck.pptx']!.docType, 'pptx');
      expect(byName['Deck.pptx']!.hasSidecar, isFalse);
    });

    test('a created notebook is discoverable by the scan', () async {
      final vault = await makeService();
      await vault.setVaultRoot(tempDir.path);
      final srcDir = await Directory.systemTemp.createTemp('vault_scan_src');
      try {
        final src = '${srcDir.path}${Platform.pathSeparator}Doc.pdf';
        await File(src).writeAsString('bytes');
        await vault.createNotebook(src);

        final notebooks = await vault.scanNotebooks();
        expect(notebooks.length, 1);
        expect(notebooks.first.filename, 'Doc.pdf');
        expect(notebooks.first.docType, 'pdf');
      } finally {
        await srcDir.delete(recursive: true);
      }
    });
  });

  group('createEmptyNotebook + scanNotes (Phase 4 standalone notebooks)', () {
    test('writes notebook.badnote.json with the title and docType notebook',
        () async {
      final vault = await makeService();
      await vault.setVaultRoot(tempDir.path);
      final sep = Platform.pathSeparator;

      final notePath = await vault.createEmptyNotebook('My Algebra Notes');

      // Synthetic note path is <folder>/notebook, folder named from the title.
      final expectedFolder = '${tempDir.path}${sep}My Algebra Notes';
      expect(notePath, '$expectedFolder${sep}notebook');

      // The sidecar exists at <folder>/notebook.badnote.json and carries the
      // title + docType, but NO fake source file was created.
      final sidecarFile = File('$notePath.badnote.json');
      expect(sidecarFile.existsSync(), isTrue);
      final loaded =
          await SidecarStore.read(sidecarFile);
      expect(loaded, isNotNull);
      expect(loaded!.title, 'My Algebra Notes');
      expect(loaded.docType, 'notebook');
      // The folder holds only the sidecar (and possibly its .bak/.tmp), never
      // an importable source file.
      final files = Directory(expectedFolder)
          .listSync()
          .whereType<File>()
          .map((f) => f.uri.pathSegments.last)
          .toList();
      expect(files, contains('notebook.badnote.json'));
      expect(
        files.any((n) =>
            n.endsWith('.pdf') ||
            n.endsWith('.pptx') ||
            n.endsWith('.docx') ||
            n.endsWith('.ppt')),
        isFalse,
      );
    });

    test('de-duplicates the notebook folder name on title collision', () async {
      final vault = await makeService();
      await vault.setVaultRoot(tempDir.path);
      final sep = Platform.pathSeparator;

      final a = await vault.createEmptyNotebook('Journal');
      final b = await vault.createEmptyNotebook('Journal');
      expect(a, '${tempDir.path}${sep}Journal${sep}notebook');
      expect(b, '${tempDir.path}${sep}Journal 2${sep}notebook');
    });

    test('blank title falls back to an Untitled folder, null sidecar title',
        () async {
      final vault = await makeService();
      await vault.setVaultRoot(tempDir.path);
      final sep = Platform.pathSeparator;

      final notePath = await vault.createEmptyNotebook('   ');
      expect(notePath, '${tempDir.path}${sep}Untitled${sep}notebook');
      final loaded = await SidecarStore.read(File('$notePath.badnote.json'));
      expect(loaded!.title, isNull);
    });

    test('scanNotes lists note folders; scanNotebooks excludes them', () async {
      final vault = await makeService();
      await vault.setVaultRoot(tempDir.path);
      final sep = Platform.pathSeparator;

      // A standalone note.
      final notePath = await vault.createEmptyNotebook('Ideas');
      // A file-backed document notebook.
      final docFolder = '${tempDir.path}${sep}Lecture';
      final doc = File('$docFolder${sep}Lecture.pdf');
      await doc.create(recursive: true);
      await doc.writeAsString('pdf');

      final notes = await vault.scanNotes();
      expect(notes.length, 1, reason: 'only the standalone note is a note');
      expect(notes.first.title, 'Ideas');
      expect(notes.first.notePath, notePath);
      expect(notes.first.folderPath, '${tempDir.path}${sep}Ideas');

      // The document notebook is NOT a note...
      expect(notes.any((n) => n.folderPath == docFolder), isFalse);
      // ...and the standalone note is NOT a document.
      final notebooks = await vault.scanNotebooks();
      expect(notebooks.length, 1);
      expect(notebooks.first.filename, 'Lecture.pdf');
      expect(
        notebooks.any((nb) => nb.folderPath == '${tempDir.path}${sep}Ideas'),
        isFalse,
      );
    });

    test('note title round-trips through the sidecar (title persists)',
        () async {
      final vault = await makeService();
      await vault.setVaultRoot(tempDir.path);

      final notePath = await vault.createEmptyNotebook('Round Trip');
      final notes = await vault.scanNotes();
      expect(notes.single.title, 'Round Trip');

      // Re-open the sidecar directly: the title is still there.
      final loaded = await SidecarStore.read(File('$notePath.badnote.json'));
      expect(loaded!.title, 'Round Trip');
    });

    test('a note folder whose sidecar title is missing falls back to folder',
        () async {
      final vault = await makeService();
      await vault.setVaultRoot(tempDir.path);
      final sep = Platform.pathSeparator;

      // Hand-write a note sidecar with no title field.
      final folder = '${tempDir.path}${sep}Loose Note';
      final sidecar = File('$folder${sep}notebook.badnote.json');
      await sidecar.create(recursive: true);
      await sidecar.writeAsString('{"docType":"notebook"}');

      final notes = await vault.scanNotes();
      expect(notes.single.title, 'Loose Note');
    });
  });

  group('notebook container (OneNote-style)', () {
    test('createNotebookContainer writes manifest + blank page', () async {
      final vault = await makeService();
      await vault.setVaultRoot(tempDir.path);

      final c = await vault.createNotebookContainer('Linear Algebra');
      expect(c.title, 'Linear Algebra');
      expect(c.memberCount, 1);

      final containers = await vault.scanContainers();
      expect(containers.map((x) => x.folderPath), contains(c.folderPath));

      // Containers are excluded from the single-doc / free-note scans.
      expect(await vault.scanNotes(), isEmpty);
      expect(await vault.scanNotebooks(), isEmpty);

      final page = await vault.addBlankPageToContainer(
        c.folderPath,
        title: 'Lecture 2',
      );
      expect(page.title, 'Lecture 2');
      final again = await vault.scanContainers();
      expect(again.single.memberCount, 2);
    });
  });
}
