// Gestion des tokens d'accès via l'API REST (onglet Réglages) : create → list → revoke,
// flag « courant », jamais de hash exposé, garde anti lock-out sur le dernier token.
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 { openDb, type Db } from '../src/db/index.js';
import type { Config } from '../src/config.js';
import type { CreateTokenResponse, MeResponse, TokensListResponse } from '@arboretum/shared';

// Mêmes stubs que les autres tests de routes : pas de vrai claude ni de vrai PTY.
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-tokens-'));
  const dbPath = join(dir, 'tokens.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('routes de gestion des tokens', () => {
  it('GET /auth/me expose le tokenId courant', async () => {
    const res = await bundle.app.inject({ method: 'GET', url: '/api/v1/auth/me', headers: auth() });
    expect(res.statusCode).toBe(200);
    const me = res.json() as MeResponse;
    expect(typeof me.tokenId).toBe('string');
    expect(me.tokenId.length).toBeGreaterThan(0);
  });

  it('liste le token initial et le marque « courant », sans jamais exposer de hash', async () => {
    const res = await bundle.app.inject({ method: 'GET', url: '/api/v1/auth/tokens', headers: auth() });
    expect(res.statusCode).toBe(200);
    const body = res.json() as TokensListResponse;
    expect(body.tokens).toHaveLength(1);
    expect(body.tokens[0]?.label).toBe('initial');
    expect(body.tokens[0]?.current).toBe(true);
    // aucune fuite de hash / valeur en clair
    expect(JSON.stringify(body)).not.toMatch(/token_hash|tokenHash/);
  });

  it('crée un token (valeur en clair renvoyée une fois), puis utilisable pour s’authentifier', async () => {
    const res = await bundle.app.inject({
      method: 'POST',
      url: '/api/v1/auth/tokens',
      headers: auth(),
      payload: { label: 'laptop' },
    });
    expect(res.statusCode).toBe(201);
    const created = res.json() as CreateTokenResponse;
    expect(created.label).toBe('laptop');
    expect(created.token).toMatch(/^arb_[0-9a-f]{48}$/);

    // le nouveau token authentifie réellement
    const me = await bundle.app.inject({
      method: 'GET',
      url: '/api/v1/auth/me',
      headers: { authorization: `Bearer ${created.token}` },
    });
    expect((me.json() as MeResponse).tokenLabel).toBe('laptop');
  });

  it('rejette un label vide ou trop long (400)', async () => {
    const empty = await bundle.app.inject({ method: 'POST', url: '/api/v1/auth/tokens', headers: auth(), payload: { label: '  ' } });
    expect(empty.statusCode).toBe(400);
    const tooLong = await bundle.app.inject({ method: 'POST', url: '/api/v1/auth/tokens', headers: auth(), payload: { label: 'x'.repeat(65) } });
    expect(tooLong.statusCode).toBe(400);
  });

  it('révoque un token non courant (204), qui disparaît de la liste et n’authentifie plus', async () => {
    const before = (await bundle.app.inject({ method: 'GET', url: '/api/v1/auth/tokens', headers: auth() })).json() as TokensListResponse;
    const victim = before.tokens.find((t) => !t.current);
    expect(victim).toBeDefined();
    const del = await bundle.app.inject({ method: 'DELETE', url: `/api/v1/auth/tokens/${victim!.id}`, headers: auth() });
    expect(del.statusCode).toBe(200);
    const after = (await bundle.app.inject({ method: 'GET', url: '/api/v1/auth/tokens', headers: auth() })).json() as TokensListResponse;
    expect(after.tokens.find((t) => t.id === victim!.id)).toBeUndefined();
  });

  it('404 sur un id inconnu', async () => {
    const res = await bundle.app.inject({ method: 'DELETE', url: '/api/v1/auth/tokens/nope-xyz', headers: auth() });
    expect(res.statusCode).toBe(404);
  });

  it('409 LAST_TOKEN : refuse de révoquer le dernier token actif', async () => {
    const list = (await bundle.app.inject({ method: 'GET', url: '/api/v1/auth/tokens', headers: auth() })).json() as TokensListResponse;
    expect(list.tokens).toHaveLength(1); // seul le token courant subsiste
    const res = await bundle.app.inject({ method: 'DELETE', url: `/api/v1/auth/tokens/${list.tokens[0]!.id}`, headers: auth() });
    expect(res.statusCode).toBe(409);
    expect(res.json()).toMatchObject({ error: { code: 'LAST_TOKEN' } });
  });

  it('sans authentification → 401', async () => {
    const res = await bundle.app.inject({ method: 'GET', url: '/api/v1/auth/tokens' });
    expect(res.statusCode).toBe(401);
  });
});
