/**
 * E2E R2 — exploitation : machine à états stricte, garde de clôture
 * (bilan + checklist), demande → OT (1-1), scoping « voir autre ».
 * Rejoue la recette officielle : demande gardien → approbation → OT →
 * intervention → bilan codé → clôture → suivi demandeur.
 */
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('Exploitation (e2e)', () => {
  let app: INestApplication;
  let salma: string; // Dispatcher — voir autre, approuve
  let ahmed: string; // Technicien — SES OT seulement
  let karim: string; // Demandeur — SES demandes seulement
  let ahmedId: string;
  const prisma = new PrismaClient();
  const http = () => request(app.getHttpServer());
  const suffix = 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);
      const res = await http().post('/auth/demo-login').send({ userId: compte.id });
      return res.body.accessToken as string;
    };
    salma = await login('Dispatcher');
    ahmed = await login('Technicien');
    karim = await login('Demandeur');
    ahmedId = (
      await prisma.user.findUniqueOrThrow({ where: { email: 'technicien@demo.siop.ma' } })
    ).id;
  });

  afterAll(async () => {
    await prisma.request.deleteMany({ where: { description: { contains: suffix } } });
    await prisma.workOrder.deleteMany({ where: { title: { contains: suffix } } });
    await app?.close();
    await prisma.$disconnect();
  });

  const auth = (t: string) => ({ Authorization: `Bearer ${t}` });

  it('recette : demande → approbation → OT assigné → bilan → clôture → suivi', async () => {
    // 1. Karim (gardien) signale
    const { body: assets } = await http().get('/assets').set(auth(salma));
    const a1 = assets.assets.find((a: { reference: string }) => a.reference === 'A1');
    const demande = await http()
      .post('/requests')
      .set(auth(karim))
      .send({ assetId: a1.id, description: `Porte qui grince (${suffix})` })
      .expect(201);
    expect(demande.body.status).toBe('RECEIVED');

    // 2. Salma approuve → OT créé et lié (1-1)
    const ot = await http()
      .post(`/requests/${demande.body.id}/approve`)
      .set(auth(salma))
      .send({ title: `Réparer porte qui grince (${suffix})`, priority: 'MEDIUM', assigneeIds: [ahmedId] })
      .expect(201);
    expect(ot.body.status).toBe('OPEN');
    expect(ot.body.request.reference).toBe(demande.body.reference);
    // double approbation → refus
    await http()
      .post(`/requests/${demande.body.id}/approve`)
      .set(auth(salma))
      .send({})
      .expect(409);

    // 3. Ahmed démarre, commente
    const id = ot.body.id;
    await http()
      .post(`/work-orders/${id}/transition`)
      .set(auth(ahmed))
      .send({ to: 'IN_PROGRESS' })
      .expect(200);
    await http()
      .post(`/work-orders/${id}/comments`)
      .set(auth(ahmed))
      .send({ message: 'Charnière usée, graissage effectué.' })
      .expect(201);

    // 4. Clôture SANS bilan → bloquée (garde)
    const refus = await http()
      .post(`/work-orders/${id}/transition`)
      .set(auth(ahmed))
      .send({ to: 'DONE' })
      .expect(409);
    expect(refus.body.message).toContain('bilan incomplet');

    // 5. Bilan codé (3 champs requis) puis clôture
    const { body: refs } = await http().get('/reference-values').set(auth(ahmed));
    const valeur = (field: string, label: string) =>
      refs.referenceValues.find(
        (v: { field: string; label: string }) => v.field === field && v.label === label,
      ).id;
    await http()
      .put(`/work-orders/${id}/report`)
      .set(auth(ahmed))
      .send({
        doorStateId: valeur('DOOR_STATE', 'Fonctionnement normal'),
        actionTakenId: valeur('ACTION_TAKEN', 'Nettoyage / graissage'),
        componentConcernedId: valeur('COMPONENT_CONCERNED', 'Portes'),
      })
      .expect(200);
    const clos = await http()
      .post(`/work-orders/${id}/transition`)
      .set(auth(ahmed))
      .send({ to: 'DONE', comment: 'RAS après graissage' })
      .expect(200);
    expect(clos.body.status).toBe('DONE');
    expect(clos.body.completedAt).toBeTruthy();

    // 6. Karim suit SA demande : l'OT lié est terminé
    const { body: mesDemandes } = await http().get('/requests').set(auth(karim)).expect(200);
    const laMienne = mesDemandes.requests.find(
      (r: { id: string }) => r.id === demande.body.id,
    );
    expect(laMienne.workOrder.status).toBe('DONE');

    // 6 bis. Régression recette R2 : l'OT fraîchement clôturé remonte EN TÊTE
    // de la liste de Salma (après les urgences actives) — il doit apparaître
    // dans les « Interventions récentes » de tout le monde, pas seulement
    // chez l'assigné.
    const { body: listeSalma } = await http()
      .get('/work-orders')
      .set(auth(salma))
      .expect(200);
    const position = listeSalma.workOrders.findIndex((w: { id: string }) => w.id === id);
    expect(position).toBeGreaterThanOrEqual(0);
    // Invariant DÉTERMINISTE (les specs parallèles créent des OT plus frais,
    // les positions absolues sont donc instables) : notre OT fraîchement
    // clôturé précède les OT du seed modifiés avant lui — avec l'ancien tri
    // « par statut », tout En cours passait devant tous les Terminés.
    const annee = new Date().getFullYear();
    const posGrilleSeed = listeSalma.workOrders.findIndex(
      (w: { reference: string }) => w.reference === `OT-${annee}-0338`, // En cours, seed
    );
    const posAncienDone = listeSalma.workOrders.findIndex(
      (w: { reference: string }) => w.reference === `OT-${annee}-0332`, // Terminé, seed
    );
    expect(position).toBeLessThan(posGrilleSeed);
    expect(position).toBeLessThan(posAncienDone);

    // 7. Terminal : plus aucune transition
    await http()
      .post(`/work-orders/${id}/transition`)
      .set(auth(ahmed))
      .send({ to: 'IN_PROGRESS' })
      .expect(409);
  });

  it('machine à états : OPEN → DONE direct interdit ; OPEN → ON_HOLD interdit', async () => {
    const { body } = await http().get('/work-orders').set(auth(salma));
    const ouvert = body.workOrders.find(
      (w: { status: string; priority: string }) =>
        w.status === 'OPEN' && w.priority === 'PERSON_TRAPPED',
    );
    await http()
      .post(`/work-orders/${ouvert.id}/transition`)
      .set(auth(salma))
      .send({ to: 'DONE' })
      .expect(409);
    await http()
      .post(`/work-orders/${ouvert.id}/transition`)
      .set(auth(salma))
      .send({ to: 'ON_HOLD' })
      .expect(409);
  });

  it('garde de clôture : la checklist incomplète bloque la grille du mois', async () => {
    const { body } = await http().get('/work-orders').set(auth(ahmed));
    const grille = body.workOrders.find((w: { type: string }) => w.type === 'PREVENTIVE');
    const detail = await http().get(`/work-orders/${grille.id}`).set(auth(ahmed)).expect(200);
    expect(detail.body.closureBlockers.join(' ')).toContain('checklist');

    // Régler toutes les tâches (Fait/N-A) → le blocage checklist disparaît
    for (const [i, item] of detail.body.checklist.entries()) {
      await http()
        .patch(`/work-orders/${grille.id}/checklist/${item.id}`)
        .set(auth(ahmed))
        .send({ state: i % 2 ? 'NA' : 'DONE' })
        .expect(200);
    }
    const apres = await http().get(`/work-orders/${grille.id}`).set(auth(ahmed)).expect(200);
    expect(apres.body.closureBlockers.join(' ')).not.toContain('checklist');
    expect(apres.body.checklist.every((c: { doneBy: unknown }) => c.doneBy)).toBe(true);
    // remise en état (seed idempotent)
    for (const item of apres.body.checklist) {
      await http()
        .patch(`/work-orders/${grille.id}/checklist/${item.id}`)
        .set(auth(ahmed))
        .send({ state: 'PENDING' })
        .expect(200);
    }
  });

  it('« voir autre » : Ahmed ne voit que SES OT ; Karim que SES demandes', async () => {
    const { body: tous } = await http().get('/work-orders').set(auth(salma));
    const { body: siens } = await http().get('/work-orders').set(auth(ahmed));
    expect(tous.workOrders.length).toBeGreaterThan(siens.workOrders.length);
    expect(
      siens.workOrders.every((w: { assignees: { id: string }[] }) =>
        w.assignees.some((a) => a.id === ahmedId),
      ),
    ).toBe(true);
    // un OT non assigné à Ahmed lui est invisible (404, pas 403 — pas de fuite)
    const autre = tous.workOrders.find(
      (w: { assignees: { id: string }[] }) => !w.assignees.some((a) => a.id === ahmedId),
    );
    await http().get(`/work-orders/${autre.id}`).set(auth(ahmed)).expect(404);

    const { body: demandes } = await http().get('/requests').set(auth(karim));
    expect(demandes.requests.length).toBeGreaterThan(0);
    // Karim (create seulement) ne peut pas approuver
    await http()
      .post(`/requests/${demandes.requests[0].id}/approve`)
      .set(auth(karim))
      .send({})
      .expect(403);
  });

  it('rejet : motif obligatoire ; bilan : valeur hors champ refusée', async () => {
    const { body: assets } = await http().get('/assets').set(auth(salma));
    const c1 = assets.assets.find((a: { reference: string }) => a.reference === 'C1');
    const demande = await http()
      .post('/requests')
      .set(auth(karim))
      .send({ assetId: c1.id, description: `À rejeter (${suffix})` })
      .expect(201);
    await http()
      .post(`/requests/${demande.body.id}/reject`)
      .set(auth(salma))
      .send({ reason: '' })
      .expect(400);
    const rejetee = await http()
      .post(`/requests/${demande.body.id}/reject`)
      .set(auth(salma))
      .send({ reason: 'Doublon de la demande précédente.' })
      .expect(200);
    expect(rejetee.body.rejectionReason).toContain('Doublon');

    // bilan : une valeur ACTION_TAKEN dans le champ DOOR_STATE → 400
    const { body: refs } = await http().get('/reference-values').set(auth(salma));
    const action = refs.referenceValues.find((v: { field: string }) => v.field === 'ACTION_TAKEN');
    const { body: wos } = await http().get('/work-orders').set(auth(salma));
    const enCours = wos.workOrders.find((w: { status: string }) => w.status === 'IN_PROGRESS');
    await http()
      .put(`/work-orders/${enCours.id}/report`)
      .set(auth(salma))
      .send({ doorStateId: action.id })
      .expect(400);
  });

  it('« personne bloquée » saute en tête de liste', async () => {
    const { body } = await http().get('/work-orders').set(auth(salma));
    expect(body.workOrders[0].priority).toBe('PERSON_TRAPPED');
  });

  it('OT manuel : cycle complet En attente ↔ En cours, puis annulation motivée', async () => {
    const { body: assets } = await http().get('/assets').set(auth(salma));
    const e2 = assets.assets.find((a: { reference: string }) => a.reference === 'E2');
    const ot = await http()
      .post('/work-orders')
      .set(auth(salma))
      .send({
        title: `Travaux cabine (${suffix})`,
        type: 'WORKS',
        priority: 'LOW',
        assetId: e2.id,
        dueDate: new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString(),
        assigneeIds: [ahmedId],
      })
      .expect(201);
    const id = ot.body.id;
    await http().post(`/work-orders/${id}/transition`).set(auth(salma)).send({ to: 'IN_PROGRESS' }).expect(200);
    const attente = await http()
      .post(`/work-orders/${id}/transition`)
      .set(auth(salma))
      .send({ to: 'ON_HOLD', comment: 'Attente de pièce' })
      .expect(200);
    expect(attente.body.allowedTransitions).toEqual(['IN_PROGRESS', 'CANCELLED']);
    await http().post(`/work-orders/${id}/transition`).set(auth(salma)).send({ to: 'IN_PROGRESS' }).expect(200);
    const annule = await http()
      .post(`/work-orders/${id}/transition`)
      .set(auth(salma))
      .send({ to: 'CANCELLED', comment: 'Travaux reportés au trimestre prochain' })
      .expect(200);
    expect(annule.body.cancelledAt).toBeTruthy();
  });

  it('refus propres : équipement inconnu, personne inconnue, tâche inconnue, OT invisible', async () => {
    const GHOST = '00000000-0000-4000-8000-000000000000';
    await http()
      .post('/work-orders')
      .set(auth(salma))
      .send({ title: 'X', type: 'CORRECTIVE', assetId: GHOST })
      .expect(400);
    await http()
      .post('/requests')
      .set(auth(karim))
      .send({ assetId: GHOST, description: 'X' })
      .expect(400);
    const { body: wos } = await http().get('/work-orders').set(auth(salma));
    const un = wos.workOrders[0];
    await http()
      .put(`/work-orders/${un.id}/assignees`)
      .set(auth(salma))
      .send({ assigneeIds: [GHOST] })
      .expect(400);
    await http()
      .patch(`/work-orders/${un.id}/checklist/${GHOST}`)
      .set(auth(salma))
      .send({ state: 'DONE' })
      .expect(404);
    await http().get(`/work-orders/${GHOST}`).set(auth(salma)).expect(404);
    await http()
      .post(`/requests/${GHOST}/reject`)
      .set(auth(salma))
      .send({ reason: 'motif' })
      .expect(404);
  });

  it('référentiels du bilan : ajout (admin), doublon 409, renommage, 404', async () => {
    const { body } = await http().get('/auth/demo-accounts');
    const adminCompte = body.accounts.find(
      (a: { roleName: string }) => a.roleName === 'Administrateur',
    );
    const admin = (
      await http().post('/auth/demo-login').send({ userId: adminCompte.id })
    ).body.accessToken as string;

    const creee = await http()
      .post('/reference-values')
      .set(auth(admin))
      .send({ field: 'ANOMALY', label: `Anomalie test ${suffix}` })
      .expect(201);
    await http()
      .post('/reference-values')
      .set(auth(admin))
      .send({ field: 'ANOMALY', label: `Anomalie test ${suffix}` })
      .expect(409);
    await http()
      .patch(`/reference-values/${creee.body.id}`)
      .set(auth(admin))
      .send({ isActive: false })
      .expect(200);
    await http()
      .patch(`/reference-values/00000000-0000-4000-8000-000000000000`)
      .set(auth(admin))
      .send({ label: 'X' })
      .expect(404);
    // Salma (Dispatcher, sans SETTINGS.create) → 403
    await http()
      .post('/reference-values')
      .set(auth(salma))
      .send({ field: 'ANOMALY', label: 'Interdit' })
      .expect(403);
    await prisma.referenceValue.delete({ where: { id: creee.body.id } });
  });
});
