import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { writeFile, mkdtemp, mkdir, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import Database from 'better-sqlite3';
import { runMigrations } from '@main/db/migrations/index.js';
import { NoteRepository } from '@main/repository/NoteRepository.js';
import { AiWorker } from '@main/ai/AiWorker.js';
import { ProviderHolder } from '@main/ai/ProviderHolder.js';
import { MediaStore } from '@main/services/MediaStore.js';
import type { AiResponse } from '@main/ai/schema.js';
import type { InferenceProvider } from '@main/ai/InferenceProvider.js';

describe('AiWorker — vision path (v0.3.1 Cut F)', () => {
  let db: Database.Database;
  let repo: NoteRepository;
  let workDir: string;
  let mediaStore: MediaStore;

  beforeEach(async () => {
    db = new Database(':memory:');
    db.pragma('foreign_keys = ON');
    runMigrations(db);
    repo = new NoteRepository(db);
    workDir = await mkdtemp(join(tmpdir(), 'inkling-vision-'));
    mediaStore = new MediaStore(workDir);
  });

  afterEach(async () => {
    db.close();
    await rm(workDir, { recursive: true, force: true });
  });

  function makeWorker(
    generate: (input: Parameters<InferenceProvider['generate']>[0], opts?: Parameters<InferenceProvider['generate']>[1]) => Promise<AiResponse>,
    getVisionModel: () => Promise<string | null>
  ): AiWorker {
    const provider: InferenceProvider = {
      name: 'fake',
      generate,
      abort: () => {},
      healthCheck: vi.fn(async () => ({ ok: true }))
    };
    const holder = new ProviderHolder(provider);
    const settings = { getVisionModel };
    const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
    return new AiWorker(repo, holder, {
      backoffsMs: [0, 0, 0],
      logger,
      settings,
      mediaStore,
      now: () => new Date('2026-05-10T05:00:00Z')
    });
  }

  it('visionModel + media 있음 → provider.generate 가 images + opts 받음', async () => {
    const { id } = repo.create({ rawText: '이미지 메모' });
    await mkdir(join(workDir, 'media', id), { recursive: true });
    await writeFile(join(workDir, 'media', id, '1.png'), Buffer.from([0x89, 0x50, 0x4e, 0x47]));
    repo.insertMedia([{ noteId: id, kind: 'image', relPath: `media/${id}/1.png`, mime: 'image/png', bytes: 4 }]);

    const calls: Array<Parameters<InferenceProvider['generate']>> = [];
    const generate = vi.fn(async (
      input: Parameters<InferenceProvider['generate']>[0],
      opts?: Parameters<InferenceProvider['generate']>[1]
    ): Promise<AiResponse> => {
      calls.push([input, opts]);
      return { title: 't', summary: 'a\nb\nc', tags: [], dueDate: null };
    });
    const getVisionModel = vi.fn(async (): Promise<string | null> => 'gemma3:12b-vision');
    const worker = makeWorker(generate, getVisionModel);
    await worker.enqueue(id);
    await worker.drain();

    expect(calls.length).toBeGreaterThan(0);
    const [callInput, callOpts] = calls[0]!;
    expect(callInput.images).toHaveLength(1);
    expect(callInput.images![0]!.mime).toBe('image/png');
    expect(callOpts?.visionModel).toBe('gemma3:12b-vision');
  });

  it('visionModel null이면 text-only (images undefined)', async () => {
    const { id } = repo.create({ rawText: 'just text' });
    const calls: Array<Parameters<InferenceProvider['generate']>> = [];
    const generate = vi.fn(async (
      input: Parameters<InferenceProvider['generate']>[0],
      opts?: Parameters<InferenceProvider['generate']>[1]
    ): Promise<AiResponse> => {
      calls.push([input, opts]);
      return { title: 't', summary: 'a\nb\nc', tags: [], dueDate: null };
    });
    const getVisionModel = vi.fn(async (): Promise<string | null> => null);
    const worker = makeWorker(generate, getVisionModel);
    await worker.enqueue(id);
    await worker.drain();

    expect(calls.length).toBeGreaterThan(0);
    expect(calls[0]![0].images).toBeUndefined();
  });

  it('5MB 초과 이미지 → throw → AiWorker 의 fail 분기 (generate 미호출)', async () => {
    const { id } = repo.create({ rawText: 'big image' });
    await mkdir(join(workDir, 'media', id), { recursive: true });
    await writeFile(join(workDir, 'media', id, '1.png'), Buffer.alloc(6 * 1024 * 1024));
    repo.insertMedia([{ noteId: id, kind: 'image', relPath: `media/${id}/1.png`, mime: 'image/png', bytes: 6 * 1024 * 1024 }]);

    const calls: Array<Parameters<InferenceProvider['generate']>> = [];
    const generate = vi.fn(async (
      input: Parameters<InferenceProvider['generate']>[0],
      opts?: Parameters<InferenceProvider['generate']>[1]
    ): Promise<AiResponse> => {
      calls.push([input, opts]);
      return { title: 't', summary: 'a\nb\nc', tags: [], dueDate: null };
    });
    const getVisionModel = vi.fn(async (): Promise<string | null> => 'gemma3:12b-vision');
    const worker = makeWorker(generate, getVisionModel);
    await worker.enqueue(id);
    await worker.drain();

    expect(calls.length).toBe(0);
    // AiWorker catch 분기가 처리 — note 는 여전히 DB 에 존재
    const note = repo.findById(id);
    expect(note).toBeTruthy();
  });
});
