/**
 * E2E R3.2 — bibliothèque (upload MinIO via FileStorage, download streamé,
 * refus typés, permission sur la cible) + analytics (dérivé du réel).
 * Nécessite MinIO (infra locale / service CI).
 */
process.env.DEMO_MODE = 'true';

import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { PrismaClient } from '@prisma/client';
import request from 'supertest';
import { seed } from '../prisma/seed';
import { AppModule } from '../src/app.module';

const PNG_1PX = Buffer.from(
  'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==',
  'base64',
);

describe('Bibliothèque & analytics (e2e)', () => {
  let app: INestApplication;
  let nadia: string;
  let karim: string; // Demandeur : aucune permission d'édition
  const prisma = new PrismaClient();
  const http = () => request(app.getHttpServer());
  const auth = (t: string) => ({ Authorization: `Bearer ${t}` });
  const suffix = `doc-${Date.now().toString(36)}`;

  beforeAll(async () => {
    await seed(prisma);
    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;
    };
    nadia = await login('Gestionnaire');
    karim = await login('Demandeur');
  });

  afterAll(async () => {
    await prisma.document.deleteMany({ where: { fileName: { contains: suffix } } });
    await app?.close();
    await prisma.$disconnect();
  });

  it('upload → liste filtrée → download identique → suppression', async () => {
    const { body: assets } = await http().get('/assets').set(auth(nadia));
    const a1 = assets.assets.find((a: { reference: string }) => a.reference === 'A1');

    const envoye = await http()
      .post('/documents')
      .set(auth(nadia))
      .field('kind', 'CERTIFICATE')
      .field('assetId', a1.id)
      .attach('file', PNG_1PX, { filename: `certificat-${suffix}.png`, contentType: 'image/png' })
      .expect(201);
    expect(envoye.body.assetReference).toBe('A1');
    expect(envoye.body.size).toBe(PNG_1PX.length);

    const liste = await http()
      .get(`/documents?assetId=${a1.id}&kind=CERTIFICATE`)
      .set(auth(nadia))
      .expect(200);
    expect(
      liste.body.documents.some((d: { fileName: string }) => d.fileName.includes(suffix)),
    ).toBe(true);

    const telecharge = await http()
      .get(`/documents/${envoye.body.id}/download`)
      .set(auth(nadia))
      .buffer(true)
      .parse((res, cb) => {
        const chunks: Buffer[] = [];
        res.on('data', (c) => chunks.push(c));
        res.on('end', () => cb(null, Buffer.concat(chunks)));
      })
      .expect(200);
    expect(Buffer.compare(telecharge.body as Buffer, PNG_1PX)).toBe(0); // octets identiques

    await http().delete(`/documents/${envoye.body.id}`).set(auth(nadia)).expect(204);
    await http().get(`/documents/${envoye.body.id}/download`).set(auth(nadia)).expect(404);
  });

  it('corpus (R5, D3) : nouveau document inclus par défaut, bascule réversible et gardée', async () => {
    const { body: assets } = await http().get('/assets').set(auth(nadia));
    const a1 = assets.assets.find((a: { reference: string }) => a.reference === 'A1');
    const envoye = await http()
      .post('/documents')
      .set(auth(nadia))
      .field('kind', 'NOTICE')
      .field('assetId', a1.id)
      .attach('file', PNG_1PX, { filename: `corpus-${suffix}.png`, contentType: 'image/png' })
      .expect(201);
    // le contrat expose l'état d'indexation — jamais indexé à la naissance
    expect(envoye.body).toMatchObject({ inCorpus: true, indexedAt: null, chunkCount: 0 });

    const exclu = await http()
      .patch(`/documents/${envoye.body.id}/corpus`)
      .set(auth(nadia))
      .send({ inCorpus: false })
      .expect(200);
    expect(exclu.body.inCorpus).toBe(false);

    // Karim (Demandeur) n'administre pas le corpus
    await http()
      .patch(`/documents/${envoye.body.id}/corpus`)
      .set(auth(karim))
      .send({ inCorpus: true })
      .expect(403);

    await http().delete(`/documents/${envoye.body.id}`).set(auth(nadia)).expect(204);
  });

  it('refus typés : format, rattachement manquant, cible inconnue, permission', async () => {
    const { body: assets } = await http().get('/assets').set(auth(nadia));
    const a1 = assets.assets.find((a: { reference: string }) => a.reference === 'A1');

    await http()
      .post('/documents')
      .set(auth(nadia))
      .field('kind', 'OTHER')
      .field('assetId', a1.id)
      .attach('file', Buffer.from('binaire'), { filename: `x-${suffix}.exe`, contentType: 'application/octet-stream' })
      .expect(400);
    await http()
      .post('/documents')
      .set(auth(nadia))
      .field('kind', 'PHOTO')
      .attach('file', PNG_1PX, { filename: `orphelin-${suffix}.png`, contentType: 'image/png' })
      .expect(400); // aucun rattachement
    await http()
      .post('/documents')
      .set(auth(nadia))
      .field('kind', 'PHOTO')
      .field('assetId', '00000000-0000-4000-8000-000000000000')
      .attach('file', PNG_1PX, { filename: `fantome-${suffix}.png`, contentType: 'image/png' })
      .expect(400);
    // Karim (Demandeur) : lecture de la bibliothèque OK, édition refusée
    await http().get('/documents').set(auth(karim)).expect(200);
    await http()
      .post('/documents')
      .set(auth(karim))
      .field('kind', 'PHOTO')
      .field('assetId', a1.id)
      .attach('file', PNG_1PX, { filename: `interdit-${suffix}.png`, contentType: 'image/png' })
      .expect(403);
  });

  it('analytics : le tableau de la direction est dérivé du réel (bilans, coûts figés)', async () => {
    const res = await http().get('/analytics/summary').set(auth(nadia)).expect(200);
    const s = res.body;
    expect(s.costsByMonth).toHaveLength(12); // période par défaut
    // Le seed a consommé 325 MAD de pièces + 180 de MO sur OT-0341 ce mois-ci
    expect(s.monthCost).toBeGreaterThanOrEqual(505);
    // Pannes par organe : OT-0332 (seed) a un bilan « Guides »
    expect(
      s.failuresByComponent.some((f: { label: string }) => f.label === 'Guides'),
    ).toBe(true);
    // Top équipements : A1 porte les coûts du seed
    expect(s.topAssets.some((t: { reference: string }) => t.reference === 'A1')).toBe(true);
    expect(s.closed.total).toBeGreaterThanOrEqual(1);
    // Karim (Demandeur, sans ANALYTICS) → 403
    await http().get('/analytics/summary').set(auth(karim)).expect(403);
  });
});
