// Routes Réglages : GET expose la config non sensible (jamais les secrets), PATCH n'écrit que
// l'allow-list (découverte des dépôts) et ignore toute clé hors allow-list.
import { mkdtempSync, 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 { getSetting, openDb, type Db } from '../src/db/index.js';
import type { Config } from '../src/config.js';
import type { SettingsResponse } from '@arboretum/shared';

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 bundle: AppBundle;
let db: Db;
let token: string;

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

beforeAll(() => {
  dir = mkdtempSync(join(tmpdir(), 'arboretum-settings-'));
  const dbPath = join(dir, 'settings.db');
  db = openDb(dbPath);
  const config: Config = {
    port: 9999,
    bind: '127.0.0.1',
    dbPath,
    dataDir: dir,
    allowedOrigins: ['https://host.tailnet.ts.net'],
    printToken: false,
    claudeProjectsDir: join(dir, 'claude', 'projects'),
    claudeSessionsDir: join(dir, 'claude', 'sessions'),
    vapidContact: 'mailto:test@localhost',
  };
  bundle = buildApp(config, db, '1.2.3-test');
  const t = bundle.auth.ensureBootstrapToken();
  if (!t) throw new Error('bootstrap token attendu');
  token = t;
});

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

describe('GET /api/v1/settings', () => {
  it('renvoie la config serveur non sensible et aucune racine de scan par défaut', async () => {
    const res = await bundle.app.inject({ method: 'GET', url: '/api/v1/settings', headers: auth() });
    expect(res.statusCode).toBe(200);
    const body = res.json() as SettingsResponse;
    expect(body.server.version).toBe('1.2.3-test');
    expect(body.server.port).toBe(9999);
    expect(body.server.bind).toBe('127.0.0.1');
    expect(body.server.allowedOrigins).toEqual(['https://host.tailnet.ts.net']);
    expect(body.server.vapidPublicKey).toBeTruthy(); // clé publique = sûre à exposer
    // défauts de découverte : AUCUNE racine (clean install → pas de scan) + intervalle 5 min
    expect(body.settings.scanRoots).toEqual([]);
    expect(body.settings.scanIntervalMin).toBe(5);
  });

  it('n’expose AUCUN secret (server_secret, clé privée VAPID)', async () => {
    const res = await bundle.app.inject({ method: 'GET', url: '/api/v1/settings', headers: auth() });
    const raw = res.body;
    const secret = getSetting(db, 'server_secret');
    const vapidPrivate = getSetting(db, 'vapid_private');
    expect(secret).toBeTruthy();
    expect(raw).not.toContain(secret as string);
    expect(raw).not.toContain(vapidPrivate as string);
    expect(raw).not.toMatch(/server_secret|vapid_private|privateKey/);
  });
});

describe('PATCH /api/v1/settings — sécurité', () => {
  it('ignore toute clé hors allow-list (ne touche pas aux secrets)', async () => {
    const before = getSetting(db, 'server_secret');
    await bundle.app.inject({ method: 'PATCH', url: '/api/v1/settings', headers: auth(), payload: { server_secret: 'pwned', vapid_private: 'pwned' } });
    expect(getSetting(db, 'server_secret')).toBe(before); // inchangé
    expect(getSetting(db, 'vapid_private')).not.toBe('pwned');
  });

  it('sans authentification → 401', async () => {
    const res = await bundle.app.inject({ method: 'PATCH', url: '/api/v1/settings', payload: { scanRoots: ['/home/u/work'] } });
    expect(res.statusCode).toBe(401);
  });
});

describe('PATCH /api/v1/settings — découverte des dépôts', () => {
  it('enregistre des scanRoots et un intervalle valides et les renvoie', async () => {
    const res = await bundle.app.inject({
      method: 'PATCH',
      url: '/api/v1/settings',
      headers: auth(),
      payload: { scanRoots: ['/home/u/work', '/srv/code'], scanIntervalMin: 10 },
    });
    expect(res.statusCode).toBe(200);
    const body = res.json() as SettingsResponse;
    expect(body.settings.scanRoots).toEqual(['/home/u/work', '/srv/code']);
    expect(body.settings.scanIntervalMin).toBe(10);
    // persisté
    const get = await bundle.app.inject({ method: 'GET', url: '/api/v1/settings', headers: auth() });
    expect((get.json() as SettingsResponse).settings.scanRoots).toEqual(['/home/u/work', '/srv/code']);
  });

  it('rejette des racines non absolues, "/", avec ".." ou en surnombre (400)', async () => {
    const bads: unknown[] = [['relative/path'], ['/'], ['/a/../b'], Array.from({ length: 17 }, (_, i) => `/r${i}`)];
    for (const scanRoots of bads) {
      const res = await bundle.app.inject({ method: 'PATCH', url: '/api/v1/settings', headers: auth(), payload: { scanRoots } });
      expect(res.statusCode).toBe(400);
    }
  });

  it('rejette un intervalle hors borne (400)', async () => {
    const res = await bundle.app.inject({ method: 'PATCH', url: '/api/v1/settings', headers: auth(), payload: { scanIntervalMin: 5000 } });
    expect(res.statusCode).toBe(400);
    const neg = await bundle.app.inject({ method: 'PATCH', url: '/api/v1/settings', headers: auth(), payload: { scanIntervalMin: -1 } });
    expect(neg.statusCode).toBe(400);
  });
});
