// src/lib/semantic/engine.test.ts — tests bun du wrapper wasm semantic-core.
import { describe, expect, test } from "bun:test";
import { buildSerpProfile, extractSemanticPage } from "./engine";
import type { PageInput, SemanticConfig } from "./types";

const HTML = `<html lang="fr"><head><title>Fabricant bijoux</title></head>
<body><main><h1>Fabricant de bijoux</h1><p>Bijoux en acier pour revendeurs. Bijoux gravés.</p></main></body></html>`;

describe("semantic engine (wasm)", () => {
  test("extractSemanticPage renvoie zones et structure", () => {
    const p = extractSemanticPage(HTML, "https://ex.com/fabricant-bijoux");
    expect(p.langSupported).toBe(true);
    expect(Object.keys(p.termFreqs).length).toBeGreaterThan(0);
    expect(p.structure.wordCount).toBeGreaterThan(0);
  });

  test("buildSerpProfile agrège et pondère", () => {
    const page = extractSemanticPage(HTML, "https://ex.com/fabricant-bijoux");
    const profile = buildSerpProfile(
      [
        {
          position: 1,
          url: "https://ex.com/fabricant-bijoux",
          included: true,
          fetchOk: true,
          semantics: page,
        },
      ],
      null,
    );
    expect(profile.coverage.analyzed).toBe(1);
    expect(profile.terms.length).toBeGreaterThan(0);
    expect(profile.terms[0].score).toBeGreaterThan(0);
  });

  test("SERP FR e-commerce bout-en-bout : curation, gap, curveOverride, Fix 1 verrouillé", () => {
    // ⚠️ SYMÉTRIE DE CONFIG (cf. engine.ts) : la MÊME config est passée à
    // extractSemanticPage (chaque page) et à buildSerpProfile.
    const config: Partial<SemanticConfig> = { curveOverride: "transactional" };

    const productHtml = (title: string, body: string) => `
      <html lang="fr"><head><title>${title}</title>
      <meta name="description" content="Grossiste bijoux acier inoxydable pour revendeurs">
      <script type="application/ld+json">{"@type":"Product","name":"${title}"}</script>
      </head><body><main>
      <h1>${title}</h1>
      <ul class="products">
        <li class="product-card"><img src="/p1.jpg" alt="produit 1"><a href="/p1">Bague acier</a></li>
        <li class="product-card"><img src="/p2.jpg" alt="produit 2"><a href="/p2">Collier acier</a></li>
        <li class="product-card"><img src="/p3.jpg" alt="produit 3"><a href="/p3">Boucles acier</a></li>
      </ul>
      <p>${body}</p>
      </main></body></html>`;

    // Positions 1-3 : pages produit analysées (archétype transactionnel via
    // schema Product JSON-LD, urls en `.html` → verrouille le Fix 1 slug).
    const page1 = extractSemanticPage(
      productHtml(
        "Bijoux acier inoxydable",
        "Bijoux en acier inoxydable résistant pour bijoutiers et grossistes.",
      ),
      "https://ex.com/bijoux-acier-1.html",
      config,
    );
    const page2 = extractSemanticPage(
      productHtml(
        "Boucles d'oreilles argent",
        "Boucles d'oreilles en argent massif pour boutiques spécialisées.",
      ),
      "https://ex.com/boucles-oreilles-argent.html",
      config,
    );
    const page3 = extractSemanticPage(
      productHtml(
        "Collier perles naturelles",
        "Collier de perles naturelles pour grossistes en bijouterie fine.",
      ),
      "https://ex.com/collier-perles-naturelles.html",
      config,
    );

    // Client éditorial (page article) : archétype différent du dominant
    // transactionnel de la SERP → doit déclencher gap.intentMismatch.
    const clientHtml = `
      <html lang="fr"><head><title>Guide du bijou artisanal</title>
      <script type="application/ld+json">{"@type":"Article","headline":"Guide du bijou artisanal"}</script>
      </head><body><main>
      <h1>Guide complet du bijou artisanal</h1>
      <p>${"Le bijou artisanal en acier se travaille avec des techniques ancestrales de bijouterie. ".repeat(20)}</p>
      </main></body></html>`;
    const clientPage = extractSemanticPage(
      clientHtml,
      "https://ex.com/guide-bijou-artisanal.html",
      config,
    );

    const pages: PageInput[] = [
      {
        position: 1,
        url: "https://ex.com/bijoux-acier-1.html",
        included: true,
        fetchOk: true,
        semantics: page1,
      },
      {
        position: 2,
        url: "https://ex.com/boucles-oreilles-argent.html",
        included: true,
        fetchOk: true,
        semantics: page2,
      },
      {
        position: 3,
        url: "https://ex.com/collier-perles-naturelles.html",
        included: true,
        fetchOk: true,
        semantics: page3,
      },
      // Position exclue par curation utilisateur (≠ manquante).
      {
        position: 4,
        url: "https://ex.com/exclue-curation.html",
        included: false,
        fetchOk: true,
        semantics: null,
      },
      // Position manquante (fetch/render en échec).
      {
        position: 5,
        url: "https://ex.com/introuvable.html",
        included: true,
        fetchOk: false,
        semantics: null,
      },
    ];

    const profile = buildSerpProfile(pages, clientPage, config);

    // Couverture : exclusion utilisateur et manquante bien séparées.
    expect(profile.coverage.excludedByUser).toEqual([4]);
    expect(profile.coverage.missing).toEqual([5]);
    expect(profile.coverage.analyzed).toBe(3);
    expect(profile.coverage.total).toBe(5);

    // Intention : curveOverride camelCase bien reçu côté wasm, mode manuel.
    expect(profile.intent.curve).toBe("transactionnelle");
    expect(profile.intent.mode).toBe("manual");

    // Termes : non vide, avec zones par zone peuplées.
    expect(profile.terms.length).toBeGreaterThan(0);
    const totalZoneCounts = profile.terms.reduce(
      (sum, t) =>
        sum + t.zones.title + t.zones.h1 + t.zones.body + t.zones.slug,
      0,
    );
    expect(totalZoneCounts).toBeGreaterThan(0);

    // Gap analysis : mismatch d'intention (client éditorial vs SERP transactionnelle).
    expect(profile.gap).not.toBeNull();
    expect(profile.gap?.intentMismatch).toBe(true);

    // Fix 1 verrouillé à travers toute la chaîne wasm : aucun token "html"
    // dans les termes du profil malgré les urls en `.html`.
    expect(profile.terms.some((t) => t.term.includes("html"))).toBe(false);
  });

  // Follow-up js_rendered_suspected (cf. rapport .superpowers/sdd/
  // followup-semantic-report.md) : `tl` (parseur HTML du crate) n'implémente
  // pas l'état « raw text » HTML5 pour `<script>`/`<style>` — un `<`
  // littéral dans du JS réel (`i < n`, quasi systématique) fait sortir `tl`
  // du contenu du script et reparser le reste du document comme des balises.
  // Le `</script>` réel qui suit est mal apparié : tout ce qui vient après
  // (le `<main>` éditorial, donc le corpus entier) devient structurellement
  // DESCENDANT du `<script>` → exclu par `SKIP_TAGS` → `wordCount` proche de
  // 0 sur une page pourtant pleinement rendue. Repro isolée sur une page
  // réelle en prod (script #26/80, page « jewelry casting » pos 9).
  describe("sanitizeRawTextElements (fix js_rendered_suspected faux positif)", () => {
    test("un `<` de comparaison JS dans <script> ne doit plus faire disparaître le corpus qui suit", () => {
      const html = `<html lang="fr"><head><title>Fabricant bijoux</title>
        <script>for (var i = 0; i < 10; i++) { console.log(i); }</script>
        </head><body><main>
        <h1>Fabricant de bijoux artisanal</h1>
        <p>${"Bijoux en acier inoxydable pour bijoutiers et grossistes spécialisés. ".repeat(20)}</p>
        </main></body></html>`;
      const p = extractSemanticPage(html, "https://ex.com/fabricant-bijoux");
      expect(p.structure.wordCount).toBeGreaterThan(100);
      expect(p.structure.hnDepth).toBeGreaterThan(0); // le h1 est récupéré
    });

    test("les scripts application/ld+json restent intacts (schema_types/archétype préservés)", () => {
      const html = `<html lang="fr"><head><title>Fabricant bijoux</title>
        <script>for (var i = 0; i < 10; i++) {}</script>
        <script type="application/ld+json">{"@type":"Article","headline":"Fabricant bijoux"}</script>
        </head><body><main>
        <h1>Fabricant de bijoux artisanal</h1>
        <p>${"Bijoux en acier inoxydable pour bijoutiers et grossistes spécialisés. ".repeat(20)}</p>
        </main></body></html>`;
      const p = extractSemanticPage(html, "https://ex.com/fabricant-bijoux");
      expect(p.structure.schemaTypes).toContain("Article");
    });

    test("une page réellement pauvre (peu de contenu, pas de script piégeux) reste thin", () => {
      const html = `<html lang="fr"><head><title>Accès refusé</title></head>
        <body><h1>Vérification en cours</h1>
        <p>Veuillez patienter pendant que nous vérifions votre navigateur.</p>
        </body></html>`;
      const p = extractSemanticPage(html, "https://ex.com/coquille");
      expect(p.structure.wordCount).toBeLessThan(120);
    });

    // Régression 1 (review Fable) : un `>` légal dans une valeur d'attribut
    // quoté (JSON dans un `data-*`, courant WordPress/Shopify/GTM) coupait la
    // sous-expression attributs `([^>]*)` en plein milieu de la quote —
    // laissait la quote ouverte, la regex sur-matchait alors bien plus loin
    // que le vrai `</script>` et effaçait tout le contenu éditorial qui suit.
    test("un `>` dans un attribut quoté du <script> ne doit pas sur-effacer le contenu qui suit", () => {
      const html = `<html lang="fr"><head><title>Fabricant bijoux</title></head>
        <body>
        <script data-x="a>b">x=1</script>
        <main><h1>Fabricant de bijoux artisanal</h1>
        <p>${"Bijoux en acier inoxydable pour bijoutiers et grossistes spécialisés. ".repeat(20)}</p>
        </main>
        </body></html>`;
      const p = extractSemanticPage(html, "https://ex.com/fabricant-bijoux");
      expect(p.structure.wordCount).toBeGreaterThan(0);
    });

    // Régression 2 (review Fable) : un commentaire HTML contenant un
    // `<script>` littéral non fermé, suivi plus loin d'un vrai `</script>`,
    // faisait avaler tout l'éditorial entre les deux par la regex (le premier
    // `</script>` rencontré appariait avec le `<script>` du commentaire).
    test("un <script> littéral dans un commentaire HTML ne doit pas avaler l'éditorial qui suit", () => {
      const html = `<html lang="fr"><head><title>Fabricant bijoux</title></head>
        <body>
        <!-- <script> -->
        <main><h1>Fabricant de bijoux artisanal</h1>
        <p>${"Bijoux en acier inoxydable pour bijoutiers et grossistes spécialisés. ".repeat(20)}</p>
        </main>
        <script>i<n</script>
        </body></html>`;
      const p = extractSemanticPage(html, "https://ex.com/fabricant-bijoux");
      expect(p.structure.wordCount).toBeGreaterThan(0);
    });

    // Régression 3 (review Fable) : `JSON_TYPE_RE` non ancré matchait
    // `type="application/ld+json"` même porté par un attribut `data-type`
    // (JS déguisé), le préservant à tort. Seul l'attribut `type` réel doit
    // déclencher la préservation.
    test("le vrai JSON-LD (attribut `type`) est préservé", () => {
      const html = `<html lang="fr"><head><title>Fabricant bijoux</title>
        <script type="application/ld+json">{"@type":"Article"}</script>
        </head><body><main><h1>Fabricant</h1><p>contenu</p></main></body></html>`;
      const p = extractSemanticPage(html, "https://ex.com/fabricant-bijoux");
      expect(p.structure.schemaTypes).toContain("Article");
    });

    test('un `data-type="application/ld+json"` sur du JS déguisé n\'est pas préservé (traité comme JS, vidé)', () => {
      // Avant le fix, `JSON_TYPE_RE` non ancré matchait `data-type=…` (bug
      // tl latent, cf. commentaire engine.ts) et préservait ce script à
      // tort. Contrat verrouillé ici : pas de faux schemaTypes, et le
      // contenu éditorial qui suit reste intact quel que soit le `<` brut
      // dans ce script JS.
      const html = `<html lang="fr"><head><title>Fabricant bijoux</title>
        <script data-type="application/ld+json">i<n</script>
        </head><body><main><h1>Fabricant de bijoux artisanal</h1>
        <p>${"Bijoux en acier inoxydable pour bijoutiers et grossistes spécialisés. ".repeat(20)}</p>
        </main></body></html>`;
      const p = extractSemanticPage(html, "https://ex.com/fabricant-bijoux");
      expect(p.structure.wordCount).toBeGreaterThan(100);
      expect(p.structure.schemaTypes).not.toContain("Article");
    });
  });
});
