import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import Database from 'better-sqlite3';
import { mkdtempSync, rmSync, existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { runMigrations } from '@main/db/migrations/index.js';
import { NoteRepository } from '@main/repository/NoteRepository.js';
import { MediaStore } from '@main/services/MediaStore.js';
import { ExportService } from '@main/services/ExportService.js';

describe('ExportService', () => {
  let tmpRoot: string;
  let outDir: string;
  let profileDir: string;
  let db: Database.Database;
  let repo: NoteRepository;
  let mediaStore: MediaStore;
  let svc: ExportService;
  const NOW = () => new Date('2026-04-26T00:00:00.000Z');

  beforeEach(() => {
    tmpRoot = mkdtempSync(join(tmpdir(), 'inkling-export-'));
    outDir = join(tmpRoot, 'out');
    profileDir = join(tmpRoot, 'profile');
    mkdirSync(join(profileDir, 'media'), { recursive: true });
    db = new Database(':memory:');
    runMigrations(db);
    repo = new NoteRepository(db);
    mediaStore = new MediaStore(profileDir);
    svc = new ExportService(repo, mediaStore, NOW);
  });

  afterEach(() => {
    db.close();
    rmSync(tmpRoot, { recursive: true, force: true });
  });

  it('empty DB exports manifest with note_count=0', async () => {
    const r = await svc.export(outDir, { includeMedia: true });
    expect(r.noteCount).toBe(0);
    expect(r.mediaCount).toBe(0);
    const manifest = JSON.parse(readFileSync(join(outDir, 'manifest.json'), 'utf8'));
    expect(manifest.note_count).toBe(0);
    expect(manifest.media_count).toBe(0);
    expect(manifest.inkling_export_version).toBe(1);
  });

  it('single note creates one notes/*.md and one-line index.jsonl', async () => {
    const { id } = repo.create({ rawText: '회고 메모' });
    repo.updateAiResult(id, { title: '주간 회고', summary: '한 줄', tags: ['pr'], provider: 'local-ollama/x' });

    const r = await svc.export(outDir, { includeMedia: true });
    expect(r.noteCount).toBe(1);

    const noteFiles = readdirSync(join(outDir, 'notes'));
    expect(noteFiles.length).toBe(1);
    expect(noteFiles[0]).toMatch(/^\d{4}-\d{2}-\d{2}-[0-9a-f]{8}-주간-회고\.md$/);

    const md = readFileSync(join(outDir, 'notes', noteFiles[0]!), 'utf8');
    expect(md).toContain('# 주간 회고');
    expect(md).toContain('> 한 줄');
    expect(md).toContain('회고 메모');

    const jsonl = readFileSync(join(outDir, 'index.jsonl'), 'utf8');
    const lines = jsonl.trimEnd().split('\n');
    expect(lines.length).toBe(1);
    const obj = JSON.parse(lines[0]!);
    expect(obj.id).toBe(id);
    expect(obj.embedding_text).toBe('회고 메모');
  });

  it('two notes produce two-line jsonl ordered ascending by created_at', async () => {
    const { id: id1 } = repo.create({ rawText: 'first' });
    // tiny delay to ensure created_at differs
    await new Promise((r) => setTimeout(r, 5));
    const { id: id2 } = repo.create({ rawText: 'second' });

    const r = await svc.export(outDir, { includeMedia: true });
    expect(r.noteCount).toBe(2);

    const lines = readFileSync(join(outDir, 'index.jsonl'), 'utf8').trimEnd().split('\n');
    expect(lines.length).toBe(2);
    const a = JSON.parse(lines[0]!);
    const b = JSON.parse(lines[1]!);
    expect(a.id).toBe(id1);
    expect(b.id).toBe(id2);
  });

  it('note with image: media file copied with id8__n naming', async () => {
    const { id } = repo.create({ rawText: 'with image' });
    // Create a fake media file in profile + DB row
    const noteMediaDir = join(profileDir, 'media', id);
    mkdirSync(noteMediaDir, { recursive: true });
    writeFileSync(join(noteMediaDir, 'orig.png'), Buffer.from('PNGDATA'));
    repo.insertMedia([{
      noteId: id,
      kind: 'image',
      relPath: `media/${id}/orig.png`,
      mime: 'image/png',
      bytes: 7
    }]);

    const r = await svc.export(outDir, { includeMedia: true });
    expect(r.mediaCount).toBe(1);

    const id8 = id.slice(0, 8);
    const expectedFlat = join(outDir, 'media', `${id8}__1.png`);
    expect(existsSync(expectedFlat)).toBe(true);
    expect(readFileSync(expectedFlat).toString()).toBe('PNGDATA');
  });

  it('includeMedia=false: no media dir, but frontmatter still references images', async () => {
    const { id } = repo.create({ rawText: 'with image' });
    const noteMediaDir = join(profileDir, 'media', id);
    mkdirSync(noteMediaDir, { recursive: true });
    writeFileSync(join(noteMediaDir, 'orig.png'), Buffer.from('XX'));
    repo.insertMedia([{
      noteId: id,
      kind: 'image',
      relPath: `media/${id}/orig.png`,
      mime: 'image/png',
      bytes: 2
    }]);

    const r = await svc.export(outDir, { includeMedia: false });
    expect(r.mediaCount).toBe(0);
    expect(existsSync(join(outDir, 'media'))).toBe(false);

    const noteFiles = readdirSync(join(outDir, 'notes'));
    const md = readFileSync(join(outDir, 'notes', noteFiles[0]!), 'utf8');
    expect(md).toContain('images:');
    expect(md).toContain(`![](media/${id.slice(0, 8)}__1.png)`);
  });

  it('writes README.md mentioning RAG and inkling_export_version', async () => {
    await svc.export(outDir, { includeMedia: true });
    const readme = readFileSync(join(outDir, 'README.md'), 'utf8');
    expect(readme).toContain('RAG');
    expect(readme).toContain('inkling_export_version');
  });

  it('does NOT export trashed notes (listAll filter — v0.2.3 #4 회귀 가드)', async () => {
    const a = repo.create({ rawText: 'active note' }).id;
    const t = repo.create({ rawText: 'trashed note' }).id;
    repo.updateAiResult(a, { title: '활성', summary: 'a\nb\nc', tags: [], provider: 'p', dueDate: null });
    repo.updateAiResult(t, { title: '버려짐', summary: 'a\nb\nc', tags: [], provider: 'p', dueDate: null });
    repo.trash(t, '2026-05-01T00:00:00.000Z');
    const r = await svc.export(outDir, { includeMedia: false });
    expect(r.noteCount).toBe(1);
    // index.jsonl 도 trash 미포함
    const indexPath = join(outDir, 'index.jsonl');
    const lines = readFileSync(indexPath, 'utf8').trim().split('\n');
    expect(lines).toHaveLength(1);
  });
});
