/**
 * E2E R5.2 — assistant au contrat : le service IA reste interne, l'API porte
 * l'auth et la matrice ; le dialecte interne est traduit vers @siop/shared.
 * Le service IA est joué par un STUB HTTP local (la vraie chaîne se vérifie
 * en recette réelle — convention R5.1).
 */
process.env.DEMO_MODE = 'true';

import { createServer, type Server } from 'node:http';
import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import request from 'supertest';
import { AppModule } from '../src/app.module';

const REPONSE_ASK = {
  mode: 'extractif',
  answer: null,
  extraits: [
    {
      source_type: 'DOCUMENT',
      document_id: '7d7bfa5c-2f43-4f9e-9e59-3c1f0a5df001',
      work_order_id: null,
      titre: 'Notice Gen2.pdf',
      locator: 'p. 42',
      content: 'Serrer les coulisseaux au couple de 25 N·m.',
      score: 0.61,
    },
  ],
  corpus: { documents: 6, bilans: 214 },
};

const REPONSE_SUGGEST = {
  suggestions: [
    {
      field: 'ANOMALY',
      value_id: '7d7bfa5c-2f43-4f9e-9e59-3c1f0a5df002',
      label: 'Cellule/barrière encrassée',
      confidence: 'FORTE',
      similar_reports: 9,
      score: 0.62,
    },
  ],
};

const REPONSE_REINDEX = {
  documents_indexes: 6,
  documents_ignores: 2,
  bilans_indexes: 214,
  extraits: 180,
};

const REPONSE_TRANSCRIRE = { texte: 'Porte cabine qui rebondit, cellule encrassée.' };

describe('Assistant (e2e — stub du service IA)', () => {
  let app: INestApplication;
  let stub: Server;
  let ahmed: string; // Technicien : view + edit sur WORK_ORDERS, ASSETS en lecture
  let karim: string; // Demandeur : aucun droit WORK_ORDERS
  let rachid: string; // Vue seule : view sans edit
  let nadia: string; // Gestionnaire : ASSETS.edit — administre le corpus
  const requetesRecues: { url: string; jeton: string | undefined }[] = [];
  const http = () => request(app.getHttpServer());
  const auth = (t: string) => ({ Authorization: `Bearer ${t}` });

  beforeAll(async () => {
    // Stub du service IA sur un port éphémère
    stub = createServer((req, res) => {
      requetesRecues.push({
        url: req.url ?? '',
        jeton: req.headers['x-service-token'] as string | undefined,
      });
      res.setHeader('Content-Type', 'application/json');
      if (req.url === '/internal/ask') res.end(JSON.stringify(REPONSE_ASK));
      else if (req.url === '/internal/suggest') res.end(JSON.stringify(REPONSE_SUGGEST));
      else if (req.url === '/internal/reindex') res.end(JSON.stringify(REPONSE_REINDEX));
      else if (req.url === '/internal/transcrire') res.end(JSON.stringify(REPONSE_TRANSCRIRE));
      else {
        res.statusCode = 404;
        res.end('{}');
      }
    });
    await new Promise<void>((resolve) => stub.listen(0, '127.0.0.1', resolve));
    const adresse = stub.address();
    const port = typeof adresse === 'object' && adresse ? adresse.port : 0;
    process.env.AI_SERVICE_URL = `http://127.0.0.1:${port}`;
    process.env.AI_SERVICE_TOKEN = 'jeton-de-test';

    const moduleRef = await Test.createTestingModule({
      imports: [AppModule.forRoot()],
    }).compile();
    app = moduleRef.createNestApplication();
    await app.init();
    const { body } = await http().get('/auth/demo-accounts');
    const login = async (roleName: string) => {
      const compte = body.accounts.find((a: { roleName: string }) => a.roleName === roleName);
      return (await http().post('/auth/demo-login').send({ userId: compte.id })).body
        .accessToken as string;
    };
    ahmed = await login('Technicien');
    karim = await login('Demandeur');
    rachid = await login('Vue seule');
    nadia = await login('Gestionnaire');
  });

  afterAll(async () => {
    await app?.close();
    await new Promise<void>((resolve) => stub.close(() => resolve()));
  });

  it('ask : traduit le dialecte interne vers le contrat, avec le jeton de service', async () => {
    const res = await http()
      .post('/assistant/ask')
      .set(auth(ahmed))
      .send({ question: 'quel couple de serrage pour les guides ?' })
      .expect(200);
    expect(res.body.mode).toBe('EXTRACTIVE');
    expect(res.body.excerpts[0]).toMatchObject({
      sourceType: 'DOCUMENT',
      title: 'Notice Gen2.pdf',
      locator: 'p. 42',
    });
    expect(res.body.corpus).toEqual({ documents: 6, reports: 214 });
    const derniere = requetesRecues.at(-1)!;
    expect(derniere.url).toBe('/internal/ask');
    expect(derniere.jeton).toBe('jeton-de-test'); // ADR-004 §4
  });

  it('suggest-bilan : codes existants traduits (FORTE → HIGH, snake → camel)', async () => {
    const res = await http()
      .post('/assistant/suggest-bilan')
      .set(auth(ahmed))
      .send({ description: 'porte cabine qui rebondit, cellule encrassée, nettoyage fait' })
      .expect(200);
    expect(res.body.suggestions[0]).toMatchObject({
      field: 'ANOMALY',
      confidence: 'HIGH',
      similarReports: 9,
    });
  });

  it('la matrice s’applique : demandeur sans WORK_ORDERS → 403 sur ask', async () => {
    await http()
      .post('/assistant/ask')
      .set(auth(karim))
      .send({ question: 'où sont les notices ?' })
      .expect(403);
  });

  it('vue seule : ask autorisé (view), suggest refusé (edit requis — D1)', async () => {
    await http()
      .post('/assistant/ask')
      .set(auth(rachid))
      .send({ question: 'historique du parc ?' })
      .expect(200);
    await http()
      .post('/assistant/suggest-bilan')
      .set(auth(rachid))
      .send({ description: 'une description suffisamment longue ici' })
      .expect(403);
  });

  it('reindex : traduit le bilan d’indexation, réservé à ASSETS.edit (D3)', async () => {
    const res = await http().post('/assistant/reindex').set(auth(nadia)).expect(200);
    expect(res.body).toEqual({
      documentsIndexed: 6,
      documentsSkipped: 2,
      reportsIndexed: 214,
      chunks: 180,
    });
    expect(requetesRecues.at(-1)!.url).toBe('/internal/reindex');
    // Ahmed (Technicien) lit le parc mais n'administre pas le corpus
    await http().post('/assistant/reindex').set(auth(ahmed)).expect(403);
  });

  it('transcribe : dictée (D5) — texte transcrit, réservé à qui édite les OT', async () => {
    const res = await http()
      .post('/assistant/transcribe')
      .set(auth(ahmed))
      .attach('file', Buffer.from('faux-audio'), { filename: 'note.m4a', contentType: 'audio/m4a' })
      .expect(200);
    expect(res.body).toEqual({ text: 'Porte cabine qui rebondit, cellule encrassée.' });
    expect(requetesRecues.at(-1)!.url).toBe('/internal/transcrire');
    expect(requetesRecues.at(-1)!.jeton).toBe('jeton-de-test');
    // Karim (Demandeur) n'a pas WORK_ORDERS.edit
    await http()
      .post('/assistant/transcribe')
      .set(auth(karim))
      .attach('file', Buffer.from('faux-audio'), { filename: 'note.m4a', contentType: 'audio/m4a' })
      .expect(403);
  });

  it('transcribe : refuse proprement sans fichier joint', async () => {
    await http().post('/assistant/transcribe').set(auth(ahmed)).expect(400);
  });

  it('question trop courte : 400 avant tout appel au service IA', async () => {
    const avant = requetesRecues.length;
    await http().post('/assistant/ask').set(auth(ahmed)).send({ question: 'ab' }).expect(400);
    expect(requetesRecues.length).toBe(avant);
  });

  it('service IA éteint : 503 propre, jamais un 500', async () => {
    await new Promise<void>((resolve) => stub.close(() => resolve()));
    await http()
      .post('/assistant/ask')
      .set(auth(ahmed))
      .send({ question: 'le service est-il là ?' })
      .expect(503);
  });
});
