import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import Database from 'better-sqlite3';
import {
  mkdtempSync,
  rmSync,
  existsSync,
  mkdirSync,
  writeFileSync,
  readFileSync
} 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 { ImportService } from '@main/services/ImportService.js';
import {
  composeMarkdown,
  composeFilename,
  type ExportNote
} from '@main/services/exportFormat.js';

function buildExportNote(overrides: Partial<ExportNote> = {}): ExportNote {
  return {
    id: '014a3b9c-1234-7890-abcd-000000000001',
    createdAt: '2026-04-25T14:23:11.000Z',
    updatedAt: '2026-04-25T14:24:02.000Z',
    rawText: '회고 메모 본문',
    aiTitle: '주간 회고 PR 리뷰',
    aiSummary: '회고 양식 통일을 위한 메모.',
    titleEditedByUser: false,
    summaryEditedByUser: false,
    aiProvider: 'local-ollama/gemma4:e4b',
    aiGeneratedAt: '2026-04-25T14:23:34.000Z',
    userIntent: null,
    intentPromptedAt: null,
    // v0.3.0 Cut E — frontmatter round-trip 5 필드 (Cut B status + Cut C dueDate).
    status: 'active',
    statusChangedAt: null,
    moveReason: null,
    dueDate: null,
    dueDateEditedByUser: false,
    tags: [{ name: 'pr', source: 'ai' }],
    media: [],
    ...overrides
  };
}

function writeNote(sourceDir: string, note: ExportNote): string {
  const filename = composeFilename({
    id: note.id,
    createdAt: note.createdAt,
    aiTitle: note.aiTitle
  });
  const md = composeMarkdown(note);
  mkdirSync(join(sourceDir, 'notes'), { recursive: true });
  const abs = join(sourceDir, 'notes', filename);
  writeFileSync(abs, md, 'utf8');
  return abs;
}

function writeMedia(sourceDir: string, rel: string, bytes: Buffer): void {
  const dirIdx = rel.lastIndexOf('/');
  const subdir = dirIdx === -1 ? '' : rel.slice(0, dirIdx);
  if (subdir) {
    mkdirSync(join(sourceDir, subdir), { recursive: true });
  }
  writeFileSync(join(sourceDir, rel), bytes);
}

describe('ImportService', () => {
  let tmpRoot: string;
  let sourceDir: string;
  let profileDir: string;
  let db: Database.Database;
  let repo: NoteRepository;
  let mediaStore: MediaStore;
  let svc: ImportService;

  beforeEach(() => {
    tmpRoot = mkdtempSync(join(tmpdir(), 'inkling-import-'));
    sourceDir = join(tmpRoot, 'src');
    profileDir = join(tmpRoot, 'profile');
    mkdirSync(sourceDir, { recursive: true });
    mkdirSync(join(profileDir, 'media'), { recursive: true });
    db = new Database(':memory:');
    runMigrations(db);
    repo = new NoteRepository(db);
    mediaStore = new MediaStore(profileDir);
    svc = new ImportService(repo, mediaStore);
  });

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

  it('preview() of empty notes/ directory → all zeros', async () => {
    mkdirSync(join(sourceDir, 'notes'), { recursive: true });
    const plan = await svc.preview(sourceDir);
    expect(plan).toEqual({
      total: 0,
      newCount: 0,
      unchangedCount: 0,
      forkedCount: 0,
      mediaCount: 0
    });
  });

  it('preview() of single new note → newCount=1', async () => {
    writeNote(sourceDir, buildExportNote());
    const plan = await svc.preview(sourceDir);
    expect(plan.total).toBe(1);
    expect(plan.newCount).toBe(1);
    expect(plan.unchangedCount).toBe(0);
    expect(plan.forkedCount).toBe(0);
  });

  it('run() inserts a new note with tags + provenance', async () => {
    writeNote(
      sourceDir,
      buildExportNote({
        tags: [
          { name: 'pr', source: 'ai' },
          { name: 'review', source: 'user' }
        ],
        titleEditedByUser: true
      })
    );
    const r = await svc.run(sourceDir);
    expect(r.newCount).toBe(1);
    expect(r.unchangedCount).toBe(0);
    expect(r.forkedCount).toBe(0);

    const note = repo.findById('014a3b9c-1234-7890-abcd-000000000001');
    expect(note).not.toBeNull();
    expect(note!.aiTitle).toBe('주간 회고 PR 리뷰');
    expect(note!.aiStatus).toBe('done');
    expect(note!.titleEditedByUser).toBe(true);
    expect(note!.aiProvider).toBe('local-ollama/gemma4:e4b');
    const tagNames = note!.tags.map((t) => `${t.name}:${t.source}`).sort();
    expect(tagNames).toEqual(['pr:ai', 'review:user']);
  });

  it('run() with id collision + identical raw_text → status=skipped, no extra row', async () => {
    // Pre-seed DB.
    repo.importNote({
      id: '014a3b9c-1234-7890-abcd-000000000001',
      rawText: '회고 메모 본문',
      createdAt: '2026-04-25T14:23:11.000Z',
      updatedAt: '2026-04-25T14:24:02.000Z',
      aiTitle: '기존 제목',
      aiSummary: null,
      titleEditedByUser: false,
      summaryEditedByUser: false,
      aiProvider: null,
      aiGeneratedAt: null,
      userIntent: null,
      intentPromptedAt: null,
      tags: []
    });

    writeNote(sourceDir, buildExportNote()); // same id, same rawText

    const r = await svc.run(sourceDir);
    expect(r.unchangedCount).toBe(1);
    expect(r.newCount).toBe(0);
    expect(r.forkedCount).toBe(0);

    const allRows = db.prepare('SELECT id FROM notes').all();
    expect(allRows.length).toBe(1);
    // Original title preserved (skip means no overwrite).
    const note = repo.findById('014a3b9c-1234-7890-abcd-000000000001');
    expect(note!.aiTitle).toBe('기존 제목');
  });

  it('run() with id collision + different raw_text → forked, new id, original untouched', async () => {
    // Pre-seed DB with raw_text "OLD".
    repo.importNote({
      id: '014a3b9c-1234-7890-abcd-000000000001',
      rawText: 'OLD body',
      createdAt: '2026-04-25T14:23:11.000Z',
      updatedAt: '2026-04-25T14:24:02.000Z',
      aiTitle: '기존',
      aiSummary: null,
      titleEditedByUser: false,
      summaryEditedByUser: false,
      aiProvider: null,
      aiGeneratedAt: null,
      userIntent: null,
      intentPromptedAt: null,
      tags: []
    });

    // Export note with same id, different rawText.
    writeNote(sourceDir, buildExportNote({ rawText: 'NEW body' }));

    const r = await svc.run(sourceDir);
    expect(r.forkedCount).toBe(1);
    expect(r.newCount).toBe(0);

    // Two rows now.
    const allRows = db.prepare('SELECT id, raw_text FROM notes ORDER BY raw_text').all() as Array<{
      id: string;
      raw_text: string;
    }>;
    expect(allRows.length).toBe(2);
    expect(allRows.map((r) => r.raw_text)).toEqual(['NEW body', 'OLD body']);

    // Original id still has OLD body (raw_text invariant).
    const original = repo.findById('014a3b9c-1234-7890-abcd-000000000001');
    expect(original!.rawText).toBe('OLD body');

    // Mapping records the rename.
    expect(r.finalNoteIds.get('014a3b9c-1234-7890-abcd-000000000001')).not.toBe(
      '014a3b9c-1234-7890-abcd-000000000001'
    );
  });

  it('run() copies media file to profileDir + inserts media row', async () => {
    const note = buildExportNote({
      media: [{ rel: 'media/014a3b9c__1.png', mime: 'image/png', bytes: 7 }]
    });
    writeNote(sourceDir, note);
    writeMedia(sourceDir, 'media/014a3b9c__1.png', Buffer.from('PNGDATA'));

    const r = await svc.run(sourceDir);
    expect(r.mediaCount).toBe(1);

    const finalId = r.finalNoteIds.get(note.id)!;
    const expectedAbs = join(profileDir, 'media', finalId, '1.png');
    expect(existsSync(expectedAbs)).toBe(true);
    expect(readFileSync(expectedAbs).toString()).toBe('PNGDATA');

    const dbNote = repo.findById(finalId);
    expect(dbNote!.media.length).toBe(1);
    expect(dbNote!.media[0]!.relPath).toBe(`media/${finalId}/1.png`);
    expect(dbNote!.media[0]!.mime).toBe('image/png');
    expect(dbNote!.media[0]!.bytes).toBe(7);
  });
});

describe('ImportService — deletedAt preservation (v0.2.3 #4)', () => {
  it('id-collide skip: source deleted_at IS NOT NULL → dest deleted_at 갱신', () => {
    const db = new Database(':memory:');
    runMigrations(db);
    const repo = new NoteRepository(db);
    const { id } = repo.create({ rawText: 'identical' });
    const r = repo.importNote({
      id, rawText: 'identical',
      createdAt: '2026-04-01T00:00:00Z', updatedAt: '2026-04-01T00:00:00Z',
      aiTitle: null, aiSummary: null,
      titleEditedByUser: false, summaryEditedByUser: false,
      aiProvider: null, aiGeneratedAt: null,
      userIntent: null, intentPromptedAt: null,
      tags: [],
      deletedAt: '2026-05-01T12:00:00.000Z'
    });
    expect(r.status).toBe('skipped');
    expect(repo.findById(id)!.deletedAt).toBe('2026-05-01T12:00:00.000Z');
  });

  it('id-collide skip: source deleted_at NULL + dest IS NOT NULL → dest 유지', () => {
    const db = new Database(':memory:');
    runMigrations(db);
    const repo = new NoteRepository(db);
    const { id } = repo.create({ rawText: 'identical' });
    repo.trash(id, '2026-05-01T00:00:00.000Z');
    repo.importNote({
      id, rawText: 'identical',
      createdAt: '2026-04-01T00:00:00Z', updatedAt: '2026-04-01T00:00:00Z',
      aiTitle: null, aiSummary: null,
      titleEditedByUser: false, summaryEditedByUser: false,
      aiProvider: null, aiGeneratedAt: null,
      userIntent: null, intentPromptedAt: null,
      tags: [],
      deletedAt: null
    });
    expect(repo.findById(id)!.deletedAt).toBe('2026-05-01T00:00:00.000Z');
  });

  it('id-new insert: source deletedAt 보존', () => {
    const db = new Database(':memory:');
    runMigrations(db);
    const repo = new NoteRepository(db);
    const r = repo.importNote({
      id: 'fresh-id', rawText: 'fresh',
      createdAt: '2026-04-01T00:00:00Z', updatedAt: '2026-04-01T00:00:00Z',
      aiTitle: null, aiSummary: null,
      titleEditedByUser: false, summaryEditedByUser: false,
      aiProvider: null, aiGeneratedAt: null,
      userIntent: null, intentPromptedAt: null,
      tags: [],
      deletedAt: '2026-05-01T12:00:00.000Z'
    });
    expect(r.status).toBe('inserted');
    expect(repo.findById('fresh-id')!.deletedAt).toBe('2026-05-01T12:00:00.000Z');
  });

  it('id-collide forked: deletedAt 도 fork 노트에 보존', () => {
    const db = new Database(':memory:');
    runMigrations(db);
    const repo = new NoteRepository(db);
    const { id } = repo.create({ rawText: 'original' });
    const r = repo.importNote({
      id, rawText: 'different',
      createdAt: '2026-04-01T00:00:00Z', updatedAt: '2026-04-01T00:00:00Z',
      aiTitle: null, aiSummary: null,
      titleEditedByUser: false, summaryEditedByUser: false,
      aiProvider: null, aiGeneratedAt: null,
      userIntent: null, intentPromptedAt: null,
      tags: [],
      deletedAt: '2026-05-01T12:00:00.000Z'
    });
    expect(r.status).toBe('forked');
    expect(r.id).not.toBe(id);
    expect(repo.findById(r.id)!.deletedAt).toBe('2026-05-01T12:00:00.000Z');
  });
});
