import { expect, test } from '@playwright/test';

/**
 * Recette R5 (plan de releases) : la bibliothèque EST le corpus (bandeau
 * 09-08, statut d'indexation, interrupteur, réindexation explicite) ; puis
 * l'assistant « sourcé ou silencieux » (réponse citée depuis le PDF téléversé,
 * refus honnête et chiffré sinon) ; enfin la suggestion de bilan sur un OT
 * (codes existants, appliqués par le geste humain, liseré « suggéré »).
 * Le service IA tourne avec l'embeddeur déterministe et des seuils abaissés
 * (ci.yml) — c'est le CIRCUIT qui se recette, le vrai modèle se recette en
 * local (journal R5.1).
 */

const suffix = Date.now().toString(36);

/** PDF 1 page minimal mais VALIDE (xref calculée) — pypdf doit pouvoir en
 *  extraire le texte : c'est ce qui alimente l'index côté service IA. */
function pdfMinimal(texte: string): Buffer {
  const contenu = `BT /F1 12 Tf 72 720 Td (${texte.replace(/[()\\]/g, '\\$&')}) Tj ET`;
  const objets = [
    '<< /Type /Catalog /Pages 2 0 R >>',
    '<< /Type /Pages /Kids [3 0 R] /Count 1 >>',
    '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R ' +
      '/Resources << /Font << /F1 5 0 R >> >> >>',
    `<< /Length ${contenu.length} >>\nstream\n${contenu}\nendstream`,
    '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>',
  ];
  let corps = '%PDF-1.4\n';
  const offsets: number[] = [];
  objets.forEach((objet, i) => {
    offsets.push(corps.length);
    corps += `${i + 1} 0 obj\n${objet}\nendobj\n`;
  });
  const debutXref = corps.length;
  corps += `xref\n0 ${objets.length + 1}\n0000000000 65535 f \n`;
  for (const offset of offsets) corps += `${String(offset).padStart(10, '0')} 00000 n \n`;
  corps += `trailer\n<< /Size ${objets.length + 1} /Root 1 0 R >>\nstartxref\n${debutXref}\n%%EOF`;
  return Buffer.from(corps, 'latin1');
}

async function connexionDemo(page: import('@playwright/test').Page, nom: RegExp) {
  await page.goto('/connexion');
  await page.getByRole('button', { name: nom }).click();
  await expect(page.locator('.topbar')).toBeVisible();
}

test('recette R5 : corpus → réindexation → assistant sourcé → refus honnête', async ({ page }) => {
  test.setTimeout(180_000); // deux réindexations complètes du corpus réel
  const fichier = `e2e-notice-${suffix}.pdf`;

  // 1 · Nadia (gestionnaire, ASSETS.edit) — la bibliothèque est le corpus
  await connexionDemo(page, /Nadia Berrada/);
  await page.getByRole('link', { name: 'Fichiers' }).click();
  await expect(page.getByRole('heading', { name: /corpus de l'assistant/ })).toBeVisible();
  await expect(page.locator('.avert')).toContainText('Loi 09-08'); // l'anonymisation est DITE

  // 2 · Téléverser une notice PDF de test rattachée à A1
  await page.getByRole('button', { name: 'Téléverser' }).click();
  await page.getByLabel("Rattacher à l'appareil *").selectOption({ index: 1 });
  await page.getByLabel('Choisir un fichier').setInputFiles({
    name: fichier,
    mimeType: 'application/pdf',
    buffer: pdfMinimal(
      'Couple de serrage des coulisseaux de guides : 25 Nm. Verifier le jeu lateral.',
    ),
  });
  await page.getByRole('button', { name: 'Téléverser', exact: true }).last().click();
  const vignette = page.locator('tr', { hasText: fichier }); // tableau du corpus (arbitrage référent)
  await expect(vignette).toBeVisible();
  await expect(vignette.locator('.st')).toHaveText('à indexer'); // jamais indexé à la naissance

  // 3 · Réindexer tout — geste explicite, bilan chiffré, statut à jour
  await page.getByRole('button', { name: 'Réindexer tout' }).click();
  // l'ingestion réelle (MinIO + pypdf + embeddings) peut dépasser les 5 s
  await expect(page.getByText('Réindexation terminée')).toBeVisible({ timeout: 60_000 });
  await expect(vignette.locator('.st')).toContainText('indexé ·');
  await expect(vignette.locator('.st')).toContainText('extrait');

  // 4 · L'assistant répond SOURCÉ depuis ce PDF (embeddeur déterministe :
  //     la question reprend les mots de la notice)
  await page.getByRole('link', { name: 'Assistant' }).click();
  await expect(page.getByText('répond UNIQUEMENT depuis votre bibliothèque')).toBeVisible();
  await page
    .getByLabel('Poser une question')
    .fill('couple de serrage des coulisseaux de guides ?');
  await page.getByRole('button', { name: 'Envoyer' }).click();
  const source = page.locator('.source', { hasText: fichier });
  await expect(source).toBeVisible();
  await expect(source.locator('.extrait')).toContainText('25 Nm'); // l'extrait EXACT
  await expect(source.locator('.ou')).toContainText('p. 1'); // la citation pointe la page
  await expect(page.locator('.msg-r .avert')).toContainText('vous validez'); // pas un disclaimer caché

  // 5 · Sans source au-dessus du seuil : refus honnête, chiffré, avec l'action utile
  await page.getByLabel('Poser une question').fill('xylophone quantique zorglub ?');
  await page.getByRole('button', { name: 'Envoyer' }).click();
  const refus = page.locator('.refus');
  await expect(refus).toBeVisible();
  await expect(refus).toContainText('je préfère ne pas inventer');
  await expect(refus).toContainText(/J'ai cherché dans \d+ documents? indexés? et \d+ bilans?/);
  await expect(refus.getByRole('link', { name: /Téléverser la notice/ })).toBeVisible();

  // 6 · L'interrupteur exclut la notice — réversible, effectif à la réindexation
  await page.getByRole('link', { name: 'Fichiers' }).click();
  await vignette.getByRole('switch').click();
  await expect(vignette.locator('.st')).toHaveText('exclu du corpus');
  await page.getByRole('button', { name: 'Réindexer tout' }).click();
  await expect(page.getByText('Réindexation terminée')).toBeVisible({ timeout: 60_000 });

  // Ménage : la notice de test sort de la bibliothèque
  page.on('dialog', (d) => void d.accept());
  await vignette.getByRole('button', { name: 'Supprimer' }).click();
  await expect(vignette).toHaveCount(0);
});

test('recette R5 : suggestion de bilan — codes existants, appliqués par l\'humain', async ({
  page,
}) => {
  const titre = `E2E Suggestion ${suffix}`;

  // 1 · Nadia crée un OT de dépannage sur A1
  await connexionDemo(page, /Nadia Berrada/);
  await page.getByRole('link', { name: 'Ordres de travail' }).click();
  await page.getByRole('button', { name: '+ Nouvel OT' }).click();
  await page.getByLabel('Objet *').fill(titre);
  await page.getByLabel('Équipement *').selectOption({ index: 1 });
  await page.getByRole('button', { name: "Créer l'OT (statut Ouvert)" }).click();
  await expect(page.getByRole('heading', { name: titre })).toBeVisible();

  // 2 · Décrire pour suggérer — l'IA propose des codes EXISTANTS, justifiés
  await page
    .getByLabel('Décrire pour suggérer (optionnel)')
    .fill('Frottement mécanique sur les portes, nettoyage et graissage effectués, essais OK.');
  await page.getByRole('button', { name: /Suggérer les codes/ }).click();
  const suggestions = page.locator('.suggestion');
  await expect(suggestions.first()).toBeVisible();
  await expect(page.locator('.confiance').first()).toContainText('confiance');

  // 3 · « Appliquer les N » : les sélecteurs se pré-remplissent, liseré « suggéré »
  await page.getByRole('button', { name: /Appliquer les \d/ }).click();
  await expect(page.locator('.champ-b[data-suggere]').first()).toBeVisible();
  const anomalie = page.locator('#bilan-ANOMALY');
  await expect(anomalie).not.toHaveValue('');

  // 4 · Un choix MANUEL retire le liseré du champ concerné (l'humain a repris la main)
  const champAnomalie = page.locator('.champ-b', { has: anomalie });
  await expect(champAnomalie).toHaveAttribute('data-suggere', 'true');
  await anomalie.selectOption({ index: 1 });
  await expect(champAnomalie).not.toHaveAttribute('data-suggere', 'true');
});
