import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { PieceCatalog } from './piece-catalog.js';

// Minimal valid piece YAML helper
function makeYaml(name: string, description: string, keywords: string[] = []): string {
  const kwBlock = keywords.length > 0
    ? `triggers:\n  keywords:\n${keywords.map(k => `    - ${k}`).join('\n')}\n`
    : '';
  return `name: ${name}\ndescription: |\n  ${description}\n${kwBlock}movements: []\n`;
}

let tmpRoot: string;
let builtinDir: string;
let dataDir: string;

beforeEach(() => {
  tmpRoot = mkdtempSync(join(tmpdir(), 'piece-catalog-'));
  builtinDir = join(tmpRoot, 'pieces');
  dataDir = join(tmpRoot, 'data');
  mkdirSync(builtinDir, { recursive: true });
  mkdirSync(dataDir, { recursive: true });
});

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

describe('PieceCatalog', () => {
  it('returns built-ins when user has no custom pieces', () => {
    writeFileSync(join(builtinDir, 'chat.yaml'), makeYaml('chat', 'Chat piece'));
    writeFileSync(join(builtinDir, 'research.yaml'), makeYaml('research', 'Research piece', ['調査']));

    const catalog = new PieceCatalog(builtinDir, dataDir);
    const entries = catalog.getForUser('user-1');

    expect(entries).toHaveLength(2);
    const chat = entries.find(e => e.name === 'chat');
    expect(chat).toBeDefined();
    expect(chat!.source).toBe('builtin');
    expect(chat!.description).toContain('Chat piece');

    const research = entries.find(e => e.name === 'research');
    expect(research!.keywords).toContain('調査');
  });

  it('layers user pieces on top of built-ins (same name → custom wins)', () => {
    writeFileSync(join(builtinDir, 'chat.yaml'), makeYaml('chat', 'Built-in chat'));
    writeFileSync(join(builtinDir, 'general.yaml'), makeYaml('general', 'Built-in general'));

    // Create user custom pieces dir
    const userPiecesDir = join(dataDir, 'user-2', 'pieces');
    mkdirSync(userPiecesDir, { recursive: true });
    writeFileSync(join(userPiecesDir, 'chat.yaml'), makeYaml('chat', 'Custom chat override'));
    writeFileSync(join(userPiecesDir, 'my-custom.yaml'), makeYaml('my-custom', 'Custom-only piece'));

    const catalog = new PieceCatalog(builtinDir, dataDir);
    const entries = catalog.getForUser('user-2');

    // Should have: chat (custom), general (builtin), my-custom (custom) = 3
    expect(entries).toHaveLength(3);

    const chat = entries.find(e => e.name === 'chat');
    expect(chat!.source).toBe('custom');
    expect(chat!.description).toContain('Custom chat override');

    const general = entries.find(e => e.name === 'general');
    expect(general!.source).toBe('builtin');

    const myCustom = entries.find(e => e.name === 'my-custom');
    expect(myCustom!.source).toBe('custom');
  });

  it('invalidate(userId) causes the next call to re-read disk', () => {
    writeFileSync(join(builtinDir, 'chat.yaml'), makeYaml('chat', 'Built-in chat'));

    const userPiecesDir = join(dataDir, 'user-3', 'pieces');
    mkdirSync(userPiecesDir, { recursive: true });

    const catalog = new PieceCatalog(builtinDir, dataDir);

    // First call: no custom piece → only 1 entry
    const before = catalog.getForUser('user-3');
    expect(before).toHaveLength(1);

    // Write a custom piece to disk
    writeFileSync(join(userPiecesDir, 'new-piece.yaml'), makeYaml('new-piece', 'New custom piece'));

    // Without invalidate, result should be cached (still 1 entry)
    const cached = catalog.getForUser('user-3');
    expect(cached).toHaveLength(1);

    // After invalidate, next call should re-read disk and see 2 entries
    catalog.invalidate('user-3');
    const after = catalog.getForUser('user-3');
    expect(after).toHaveLength(2);
    expect(after.find(e => e.name === 'new-piece')).toBeDefined();
  });

  it('caches within TTL (no second disk read until TTL expires)', () => {
    writeFileSync(join(builtinDir, 'chat.yaml'), makeYaml('chat', 'Built-in chat'));

    const userPiecesDir = join(dataDir, 'user-4', 'pieces');
    mkdirSync(userPiecesDir, { recursive: true });

    const catalog = new PieceCatalog(builtinDir, dataDir);

    // First call populates cache
    const first = catalog.getForUser('user-4');
    expect(first).toHaveLength(1);

    // Write a custom piece without invalidating
    writeFileSync(join(userPiecesDir, 'surprise.yaml'), makeYaml('surprise', 'Should not be visible yet'));

    // Second call within TTL should return cached result (1 entry, not 2)
    const second = catalog.getForUser('user-4');
    expect(second).toHaveLength(1);
    expect(second).toBe(first); // same array reference → cache hit

    // Simulate TTL expiry by back-dating the cache entry
    const entry = (catalog as unknown as { cache: Map<string, { ts: number; entries: unknown[] }> }).cache.get('user-4')!;
    entry.ts = Date.now() - 61_000;

    // After TTL expiry, re-reads disk and sees the new piece
    const third = catalog.getForUser('user-4');
    expect(third).toHaveLength(2);
    expect(third.find((e: { name: string }) => e.name === 'surprise')).toBeDefined();
  });
});
