import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import { buildApp, type AppBundle } from '../src/app.js';
import { openDb, type Db } from '../src/db/index.js';
import type { Config } from '../src/config.js';
import type { FsListResponse } from '@arboretum/shared';

// Mêmes stubs que app.e2e : pas de vrai claude ni de vrai PTY en CI.
vi.mock('node:child_process', () => ({ execFileSync: () => '/usr/bin/claude\n' }));
vi.mock('@homebridge/node-pty-prebuilt-multiarch', () => {
  class FakePty {
    pid = 424242;
    write = vi.fn();
    resize = vi.fn();
    pause = vi.fn();
    resume = vi.fn();
    kill = vi.fn();
    onData(): { dispose: () => void } {
      return { dispose: () => {} };
    }
    onExit(): { dispose: () => void } {
      return { dispose: () => {} };
    }
  }
  return { default: { spawn: (): FakePty => new FakePty() } };
});

process.env.ARBORETUM_LOG = 'silent';

let dir: string;
let root: string;
let bundle: AppBundle;
let db: Db;
let token: string;

const auth = (): { authorization: string } => ({ authorization: `Bearer ${token}` });

beforeAll(() => {
  dir = mkdtempSync(join(tmpdir(), 'arboretum-fs-'));
  // Arbo de test : root/{alpha, Beta, .hidden, a-repo/.git, file.txt}
  root = join(dir, 'root');
  mkdirSync(join(root, 'alpha'), { recursive: true });
  mkdirSync(join(root, 'Beta'), { recursive: true });
  mkdirSync(join(root, '.hidden'), { recursive: true });
  mkdirSync(join(root, 'a-repo', '.git'), { recursive: true });
  writeFileSync(join(root, 'file.txt'), 'x');

  const dbPath = join(dir, 'fs.db');
  db = openDb(dbPath);
  const config: Config = {
    port: 7317,
    bind: '127.0.0.1',
    dbPath,
    dataDir: dir,
    allowedOrigins: [],
    printToken: false,
    claudeProjectsDir: join(dir, 'claude', 'projects'),
    claudeSessionsDir: join(dir, 'claude', 'sessions'),
    vapidContact: 'mailto:test@localhost',
  };
  bundle = buildApp(config, db, '0.0.0-test');
  const t = bundle.auth.ensureBootstrapToken();
  if (!t) throw new Error('bootstrap token attendu sur une base vierge');
  token = t;
});

afterAll(async () => {
  await bundle.app.close();
  db.close();
  rmSync(dir, { recursive: true, force: true });
});

describe('GET /api/v1/fs/list', () => {
  it('liste les sous-dossiers triés, masque les dotfiles et les fichiers', async () => {
    const res = await bundle.app.inject({ method: 'GET', url: `/api/v1/fs/list?path=${encodeURIComponent(root)}`, headers: auth() });
    expect(res.statusCode).toBe(200);
    const body = res.json() as FsListResponse;
    const names = body.entries.map((e) => e.name);
    expect(names).toEqual(['a-repo', 'alpha', 'Beta']); // tri insensible à la casse, fichiers/dotfiles exclus
    expect(body.path).toBe(root);
    expect(body.parent).toBe(dir);
    expect(typeof body.home).toBe('string');
  });

  it('showHidden=1 inclut les dossiers cachés', async () => {
    const res = await bundle.app.inject({ method: 'GET', url: `/api/v1/fs/list?path=${encodeURIComponent(root)}&showHidden=1`, headers: auth() });
    const body = res.json() as FsListResponse;
    expect(body.entries.map((e) => e.name)).toContain('.hidden');
  });

  it('markRepos=1 annote le dossier contenant un .git', async () => {
    const res = await bundle.app.inject({ method: 'GET', url: `/api/v1/fs/list?path=${encodeURIComponent(root)}&markRepos=1`, headers: auth() });
    const body = res.json() as FsListResponse;
    expect(body.entries.find((e) => e.name === 'a-repo')?.isRepo).toBe(true);
    expect(body.entries.find((e) => e.name === 'alpha')?.isRepo).toBeUndefined();
  });

  it('sans markRepos, aucune annotation isRepo', async () => {
    const res = await bundle.app.inject({ method: 'GET', url: `/api/v1/fs/list?path=${encodeURIComponent(root)}`, headers: auth() });
    const body = res.json() as FsListResponse;
    expect(body.entries.every((e) => e.isRepo === undefined)).toBe(true);
  });

  it('path absent → liste le home (réponse 200 cohérente)', async () => {
    const res = await bundle.app.inject({ method: 'GET', url: '/api/v1/fs/list', headers: auth() });
    expect(res.statusCode).toBe(200);
    const body = res.json() as FsListResponse;
    expect(body.path).toBe(body.home);
  });

  it('chemin relatif → 400 BAD_PATH', async () => {
    const res = await bundle.app.inject({ method: 'GET', url: '/api/v1/fs/list?path=relatif/x', headers: auth() });
    expect(res.statusCode).toBe(400);
    expect(res.json()).toMatchObject({ error: { code: 'BAD_PATH' } });
  });

  it('échappement par `..` neutralisé : resolve clampe, pas de fuite (200 sur un dossier réel)', async () => {
    // `<root>/../..` résout vers un ancêtre réel : la requête réussit mais ne contient jamais de `..`.
    const res = await bundle.app.inject({ method: 'GET', url: `/api/v1/fs/list?path=${encodeURIComponent(join(root, '..', '..'))}`, headers: auth() });
    expect(res.statusCode).toBe(200);
    expect((res.json() as FsListResponse).path).not.toContain('..');
  });

  it('chemin inexistant → 404 NOT_FOUND', async () => {
    const res = await bundle.app.inject({ method: 'GET', url: `/api/v1/fs/list?path=${encodeURIComponent(join(root, 'nope-xyz'))}`, headers: auth() });
    expect(res.statusCode).toBe(404);
    expect(res.json()).toMatchObject({ error: { code: 'NOT_FOUND' } });
  });

  it('cible non-dossier → 400 NOT_A_DIRECTORY', async () => {
    const res = await bundle.app.inject({ method: 'GET', url: `/api/v1/fs/list?path=${encodeURIComponent(join(root, 'file.txt'))}`, headers: auth() });
    expect(res.statusCode).toBe(400);
    expect(res.json()).toMatchObject({ error: { code: 'NOT_A_DIRECTORY' } });
  });

  it('sans authentification → 401', async () => {
    const res = await bundle.app.inject({ method: 'GET', url: `/api/v1/fs/list?path=${encodeURIComponent(root)}` });
    expect(res.statusCode).toBe(401);
    expect(res.json()).toMatchObject({ error: { code: 'UNAUTHORIZED' } });
  });
});
