/**
 * E2E R3 — gestion : la recette officielle (consommer sous seuil → BC →
 * réception → réappro ; coût complet d'un OT) et les invariants comptables
 * (stock dérivé jamais négatif, prix et taux FIGÉS, ajustement motivé).
 */
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';

describe('Gestion (e2e)', () => {
  let app: INestApplication;
  let nadia: string; // Gestionnaire
  let salma: string; // Dispatcher
  let ahmed: string; // Technicien (PARTS view seulement)
  let admin: string;
  const prisma = new PrismaClient();
  const http = () => request(app.getHttpServer());
  const auth = (t: string) => ({ Authorization: `Bearer ${t}` });
  const suffix = `G3-${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');
    salma = await login('Dispatcher');
    ahmed = await login('Technicien');
    admin = await login('Administrateur');
  });

  afterAll(async () => {
    await prisma.stockMovement.deleteMany({
      where: {
        OR: [
          { part: { designation: { contains: suffix } } },
          { workOrder: { title: { contains: suffix } } },
        ],
      },
    });
    await prisma.workOrder.deleteMany({ where: { title: { contains: suffix } } });
    await prisma.purchaseOrder.deleteMany({
      where: { lines: { some: { part: { designation: { contains: suffix } } } } },
    });
    await prisma.part.deleteMany({ where: { designation: { contains: suffix } } });
    await prisma.partner.deleteMany({ where: { name: { contains: suffix } } });
    await app?.close();
    await prisma.$disconnect();
  });

  it('recette R3 : consommer sous seuil → BC → réception → réappro (prix figés)', async () => {
    // Pièce dédiée au test : stock 6, seuil 4, PU initial 100
    const piece = await http()
      .post('/parts')
      .set(auth(nadia))
      .send({ designation: `Pièce ${suffix}`, threshold: 4, initialPrice: 100 })
      .expect(201);
    await http()
      .post(`/parts/${piece.body.id}/movements`)
      .set(auth(nadia))
      .send({ kind: 'ENTRY', quantity: 6 })
      .expect(201);

    // OT + consommation de 3 → stock 3, SOUS le seuil
    const { body: assets } = await http().get('/assets').set(auth(salma));
    const ot = await http()
      .post('/work-orders')
      .set(auth(salma))
      .send({
        title: `Réparation ${suffix}`,
        type: 'CORRECTIVE',
        assetId: assets.assets[0].id,
      })
      .expect(201);
    const apresConso = await http()
      .post(`/work-orders/${ot.body.id}/consume-part`)
      .set(auth(salma))
      .send({ partId: piece.body.id, quantity: 3 })
      .expect(201);
    expect(apresConso.body.costs.parts[0]).toMatchObject({
      quantity: 3,
      unitPrice: 100,
      total: 300,
    });
    const sousSeuil = await http().get(`/parts/${piece.body.id}`).set(auth(nadia));
    expect(sousSeuil.body.stock).toBe(3);
    expect(sousSeuil.body.belowThreshold).toBe(true);

    // BC : brouillon → envoyé → reçu (PU 110) → stock 13, plus d'alerte
    const { body: partners } = await http().get('/partners').set(auth(nadia));
    const fournisseur = partners.partners.find((p: { kind: string }) => p.kind === 'SUPPLIER');
    const bc = await http()
      .post('/purchase-orders')
      .set(auth(nadia))
      .send({
        supplierId: fournisseur.id,
        lines: [{ partId: piece.body.id, quantity: 10, unitPrice: 110 }],
      })
      .expect(201);
    expect(bc.body.status).toBe('DRAFT');
    expect(bc.body.total).toBe(1100);
    await http()
      .post(`/purchase-orders/${bc.body.id}/transition`)
      .set(auth(nadia))
      .send({ to: 'SENT' })
      .expect(200);
    const recu = await http()
      .post(`/purchase-orders/${bc.body.id}/transition`)
      .set(auth(nadia))
      .send({ to: 'RECEIVED' })
      .expect(200);
    expect(recu.body.receivedAt).toBeTruthy();

    const apresReception = await http().get(`/parts/${piece.body.id}`).set(auth(nadia));
    expect(apresReception.body.stock).toBe(13);
    expect(apresReception.body.belowThreshold).toBe(false);
    expect(apresReception.body.lastUnitPrice).toBe(110); // nouveau prix courant
    expect(
      apresReception.body.movements.some(
        (m: { kind: string; purchaseOrderReference: string | null }) =>
          m.kind === 'RECEIPT' && m.purchaseOrderReference === bc.body.reference,
      ),
    ).toBe(true);

    // PRIX FIGÉ : la consommation passée reste à 100, malgré le nouveau PU 110
    const otRelu = await http().get(`/work-orders/${ot.body.id}`).set(auth(salma));
    expect(otRelu.body.costs.parts[0].unitPrice).toBe(100);
  });

  it('coût complet d’un OT : pièces + main-d’œuvre — le TAUX est figé à la saisie', async () => {
    const { body: users } = await http().get('/users').set(auth(admin));
    const ahmedUser = users.users.find(
      (u: { email: string }) => u.email === 'technicien@demo.siop.ma',
    );
    const tauxInitial = ahmedUser.hourlyRate; // 120 (seed)
    const { body: assets } = await http().get('/assets').set(auth(salma));
    const ot = await http()
      .post('/work-orders')
      .set(auth(salma))
      .send({ title: `Coût complet ${suffix}`, type: 'CORRECTIVE', assetId: assets.assets[0].id })
      .expect(201);

    // Salma (sans taux défini) tente de saisir pour elle-même → refus motivé
    await http()
      .post(`/work-orders/${ot.body.id}/labor`)
      .set(auth(salma))
      .send({ minutes: 30 })
      .expect(409);

    // 1 h 30 d'Ahmed au taux courant
    const avecMO = await http()
      .post(`/work-orders/${ot.body.id}/labor`)
      .set(auth(salma))
      .send({ minutes: 90, userId: ahmedUser.id })
      .expect(201);
    expect(avecMO.body.costs.labor[0].hourlyRate).toBe(tauxInitial);
    expect(avecMO.body.costs.labor[0].total).toBe(
      Math.round((90 / 60) * tauxInitial * 100) / 100,
    );
    const totalAvant = avecMO.body.costs.total;

    // L'admin augmente le taux courant d'Ahmed → le coût de l'OT NE BOUGE PAS
    await http()
      .patch(`/users/${ahmedUser.id}`)
      .set(auth(admin))
      .send({ hourlyRate: tauxInitial + 30 })
      .expect(200);
    const otApres = await http().get(`/work-orders/${ot.body.id}`).set(auth(salma));
    expect(otApres.body.costs.total).toBe(totalAvant);
    // remise en l'état (le seed ne réécrit pas les taux existants)
    await http()
      .patch(`/users/${ahmedUser.id}`)
      .set(auth(admin))
      .send({ hourlyRate: tauxInitial })
      .expect(200);
  });

  it('invariants : motif requis, stock jamais négatif, prix inconnu refusé, coûts figés après clôture', async () => {
    const piece = await http()
      .post('/parts')
      .set(auth(nadia))
      .send({ designation: `Sans prix ${suffix}`, threshold: 0 })
      .expect(201);

    // ajustement sans motif → 400 ; stock négatif → 409
    await http()
      .post(`/parts/${piece.body.id}/movements`)
      .set(auth(nadia))
      .send({ kind: 'ADJUSTMENT', quantity: -1 })
      .expect(400);
    await http()
      .post(`/parts/${piece.body.id}/movements`)
      .set(auth(nadia))
      .send({ kind: 'ADJUSTMENT', quantity: -1, reason: 'test' })
      .expect(409);
    await http()
      .post(`/parts/${piece.body.id}/movements`)
      .set(auth(nadia))
      .send({ kind: 'ENTRY', quantity: -3 })
      .expect(400);

    // consommation sans prix connu → 409 ; stock insuffisant → 409
    await http()
      .post(`/parts/${piece.body.id}/movements`)
      .set(auth(nadia))
      .send({ kind: 'ENTRY', quantity: 5 })
      .expect(201);
    const { body: assets } = await http().get('/assets').set(auth(salma));
    const ot = await http()
      .post('/work-orders')
      .set(auth(salma))
      .send({ title: `Invariants ${suffix}`, type: 'CORRECTIVE', assetId: assets.assets[0].id })
      .expect(201);
    await http()
      .post(`/work-orders/${ot.body.id}/consume-part`)
      .set(auth(salma))
      .send({ partId: piece.body.id, quantity: 2 })
      .expect(409); // pas de prix connu
    // Technicien : PARTS en lecture seule
    await http()
      .post('/parts')
      .set(auth(ahmed))
      .send({ designation: `Interdit ${suffix}` })
      .expect(403);
    // BC : réception d'un brouillon interdite
    const { body: partners } = await http().get('/partners').set(auth(nadia));
    const fournisseur = partners.partners.find((p: { kind: string }) => p.kind === 'SUPPLIER');
    const bc = await http()
      .post('/purchase-orders')
      .set(auth(nadia))
      .send({ supplierId: fournisseur.id, lines: [{ partId: piece.body.id, quantity: 1, unitPrice: 10 }] })
      .expect(201);
    await http()
      .post(`/purchase-orders/${bc.body.id}/transition`)
      .set(auth(nadia))
      .send({ to: 'RECEIVED' })
      .expect(409);
    await http()
      .post(`/purchase-orders/${bc.body.id}/transition`)
      .set(auth(nadia))
      .send({ to: 'CANCELLED' })
      .expect(200);
  });

  it('refus propres : doublons, inconnus, transitions interdites, coûts figés après clôture', async () => {
    const GHOST = '00000000-0000-4000-8000-000000000000';
    // tiers : doublon 409, 404, désactivation
    const tiers = await http()
      .post('/partners')
      .set(auth(nadia))
      .send({ name: `Tiers ${suffix}`, kind: 'SUPPLIER' })
      .expect(201);
    await http()
      .post('/partners')
      .set(auth(nadia))
      .send({ name: `Tiers ${suffix}`, kind: 'SUPPLIER' })
      .expect(409);
    await http()
      .patch(`/partners/${tiers.body.id}`)
      .set(auth(nadia))
      .send({ isActive: false })
      .expect(200);
    await http().patch(`/partners/${GHOST}`).set(auth(nadia)).send({ city: 'X' }).expect(404);
    // pièce : fournisseur inconnu 400, 404, désactivée non consommable
    await http()
      .post('/parts')
      .set(auth(nadia))
      .send({ designation: `Fournisseur fantôme ${suffix}`, supplierId: GHOST })
      .expect(400);
    await http().patch(`/parts/${GHOST}`).set(auth(nadia)).send({ threshold: 1 }).expect(404);
    await http().get(`/parts/${GHOST}`).set(auth(nadia)).expect(404);
    const desactivee = await http()
      .post('/parts')
      .set(auth(nadia))
      .send({ designation: `Désactivée ${suffix}`, initialPrice: 10 })
      .expect(201);
    await http()
      .patch(`/parts/${desactivee.body.id}`)
      .set(auth(nadia))
      .send({ isActive: false })
      .expect(200);
    const { body: assets } = await http().get('/assets').set(auth(salma));
    const ot = await http()
      .post('/work-orders')
      .set(auth(salma))
      .send({ title: `Refus ${suffix}`, type: 'CORRECTIVE', assetId: assets.assets[0].id })
      .expect(201);
    await http()
      .post(`/work-orders/${ot.body.id}/consume-part`)
      .set(auth(salma))
      .send({ partId: desactivee.body.id, quantity: 1 })
      .expect(400);
    // BC : fournisseur inactif 400, pièce inconnue 400, 404, transition sur terminal 409
    await http()
      .post('/purchase-orders')
      .set(auth(nadia))
      .send({ supplierId: tiers.body.id, lines: [{ partId: desactivee.body.id, quantity: 1, unitPrice: 5 }] })
      .expect(400); // désactivé plus haut
    await http()
      .post('/purchase-orders')
      .set(auth(nadia))
      .send({ supplierId: GHOST, lines: [{ partId: desactivee.body.id, quantity: 1, unitPrice: 5 }] })
      .expect(400);
    await http().get(`/purchase-orders/${GHOST}`).set(auth(nadia)).expect(404);
    // main-d'œuvre : personne inconnue 400 ; OT annulé → coûts figés 409
    await http()
      .post(`/work-orders/${ot.body.id}/labor`)
      .set(auth(salma))
      .send({ minutes: 10, userId: GHOST })
      .expect(400);
    await http()
      .post(`/work-orders/${ot.body.id}/transition`)
      .set(auth(salma))
      .send({ to: 'CANCELLED', comment: 'test' })
      .expect(200);
    await http()
      .post(`/work-orders/${ot.body.id}/consume-part`)
      .set(auth(salma))
      .send({ partId: desactivee.body.id, quantity: 1 })
      .expect(409);
    await http()
      .post(`/work-orders/${ot.body.id}/labor`)
      .set(auth(salma))
      .send({ minutes: 10 })
      .expect(409);
  });

  it('le seed rejoue la carte maquette : OT-0341 coûte 505 MAD', async () => {
    const annee = new Date().getFullYear();
    const { body } = await http().get('/work-orders').set(auth(salma));
    const ot341 = body.workOrders.find(
      (w: { reference: string }) => w.reference === `OT-${annee}-0341`,
    );
    const detail = await http().get(`/work-orders/${ot341.id}`).set(auth(salma)).expect(200);
    expect(detail.body.costs.total).toBe(505); // 240 + 85 + 180
    expect(detail.body.costs.labor[0].hourlyRate).toBe(120);
  });
});
