import { describe, expect, it, afterEach } from 'vitest';
import { execFileSync } from 'node:child_process';
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { FsWatcherService, isIgnoredPath } from '../src/core/fs-watcher.js';

const dirs: string[] = [];
const services: FsWatcherService[] = [];
afterEach(async () => {
  for (const s of services.splice(0)) await s.closeAll();
  for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true });
});

function makeTmpRepo(): string {
  const dir = mkdtempSync(join(tmpdir(), 'arb-fsw-'));
  dirs.push(dir);
  const run = (...args: string[]): void => void execFileSync('git', args, { cwd: dir, stdio: 'pipe' });
  run('init', '-b', 'main');
  run('config', 'user.email', 'test@arboretum.dev');
  run('config', 'user.name', 'Test');
  writeFileSync(join(dir, 'README.md'), '# test\n');
  run('add', '-A');
  run('commit', '-m', 'init');
  return dir;
}

/** Attend le prochain event `worktree_fs_change` (ou rejette au timeout). */
function nextChange(s: FsWatcherService, timeoutMs = 4000): Promise<{ repoId: string; path: string }> {
  return new Promise((resolveP, reject) => {
    const t = setTimeout(() => reject(new Error('timeout: aucun worktree_fs_change')), timeoutMs);
    s.once('worktree_fs_change', (e) => {
      clearTimeout(t);
      resolveP(e);
    });
  });
}

describe('isIgnoredPath', () => {
  it('ignore .git interne sauf HEAD/index, et node_modules', () => {
    expect(isIgnoredPath('/r/.git/objects/ab/cd')).toBe(true);
    expect(isIgnoredPath('/r/.git/refs/heads/main')).toBe(true);
    expect(isIgnoredPath('/r/.git/HEAD')).toBe(false);
    expect(isIgnoredPath('/r/.git/index')).toBe(false);
    expect(isIgnoredPath('/r/node_modules/x/y.js')).toBe(true);
    expect(isIgnoredPath('/r/src/a.ts')).toBe(false);
  });
});

describe('FsWatcherService', () => {
  it('refcount : watch ouvre, unwatch ne ferme pas tant que le plafond n’est pas atteint', () => {
    const repo = makeTmpRepo();
    const s = new FsWatcherService();
    services.push(s);
    expect(s.isWatching('r1', repo)).toBe(false);
    s.watch('r1', repo);
    expect(s.isWatching('r1', repo)).toBe(true);
    expect(s.size()).toBe(1);
    s.unwatch('r1', repo);
    expect(s.isWatching('r1', repo)).toBe(true); // idle, pas encore évincé
  });

  it('émet un worktree_fs_change débouncé à l’édition d’un fichier', async () => {
    const repo = makeTmpRepo();
    const s = new FsWatcherService({ debounceMs: 50 });
    services.push(s);
    s.watch('r1', repo);
    await s.whenReady('r1', repo);
    const p = nextChange(s);
    writeFileSync(join(repo, 'edited.txt'), 'hello\n');
    const e = await p;
    expect(e.repoId).toBe('r1');
  });

  it('détecte un changement de branche externe (git checkout) via .git/HEAD', async () => {
    const repo = makeTmpRepo();
    execFileSync('git', ['branch', 'feature'], { cwd: repo });
    const s = new FsWatcherService({ debounceMs: 50 });
    services.push(s);
    s.watch('r1', repo);
    await s.whenReady('r1', repo);
    const p = nextChange(s);
    execFileSync('git', ['checkout', 'feature'], { cwd: repo, stdio: 'pipe' });
    const e = await p;
    expect(e.path).toContain('arb-fsw-');
  });

  it('le pool LRU évince les entrées idle au-delà du plafond, jamais les épinglées', () => {
    const s = new FsWatcherService({ maxWatchers: 2 });
    services.push(s);
    const a = makeTmpRepo();
    const b = makeTmpRepo();
    const c = makeTmpRepo();
    s.pinSession('a', a); // épinglé → jamais évincé
    s.watch('b', b);
    s.unwatch('b', b); // idle
    s.watch('c', c); // dépasse le plafond → évince le plus ancien idle (b)
    expect(s.isWatching('a', a)).toBe(true);
    expect(s.isWatching('c', c)).toBe(true);
    expect(s.isWatching('b', b)).toBe(false);
  });

  it('pinRepo (checkout principal, P11) : jamais évincé par la LRU', () => {
    const s = new FsWatcherService({ maxWatchers: 1 });
    services.push(s);
    const a = makeTmpRepo();
    const b = makeTmpRepo();
    s.pinRepo('a', a); // épingle permanente
    s.watch('b', b);
    s.unwatch('b', b);
    expect(s.isWatching('a', a)).toBe(true); // toujours là malgré le dépassement du plafond
    s.unpinRepo('a', a);
    const c = makeTmpRepo();
    s.watch('c', c); // 'a' n'est plus épinglé et idle → évinçable
    expect(s.isWatching('a', a)).toBe(false);
  });

  it('closeAll libère tout', async () => {
    const repo = makeTmpRepo();
    const s = new FsWatcherService();
    s.watch('r1', repo);
    expect(s.size()).toBe(1);
    await s.closeAll();
    expect(s.size()).toBe(0);
  });
});
