// src/lib/keywords/prompt/build-prompt.test.ts
import { describe, expect, test } from "bun:test";
import type { SerpCompetitor } from "@/lib/keywords/serp-types";
import type { KeywordBrief } from "@/lib/keywords/ytg-types";
import type { StoredSemanticProfile } from "@/lib/semantic/profile-types";
import type { GapTerm, GuideTerm, NgramAggregate } from "@/lib/semantic/types";
import { buildKeywordPrompt } from "./build-prompt";
import {
  type KeywordPromptBundle,
  PROMPT_TARGETS,
  PROMPT_TASKS,
} from "./types";

function competitor(over: Partial<SerpCompetitor> = {}): SerpCompetitor {
  return {
    position: 1,
    domain: "example.com",
    url: "https://example.com/page",
    backlinks: 120,
    refDomains: 30,
    pageAuthority: 45,
    searchTraffic: null,
    urlKeywords: null,
    title: null,
    metaDescription: null,
    lang: null,
    images: null,
    imagesWithoutAlt: null,
    imageAlts: null,
    missingAltSrcs: null,
    hasJsonLd: null,
    jsonLdTypes: null,
    crawledAt: null,
    ...over,
  };
}

const brief: KeywordBrief = {
  source: "ytg",
  query: "bague diamant",
  language: "French (France)",
  lang: "fr",
  country: "FR",
  importedAt: "2026-06-18",
  primaryIntent: "commercial",
  objectives: ["informer", "convertir"],
  keySubjects: ["diamant", "or"],
  questions: ["Quel diamant choisir ?", "Quel budget ?"],
  tone: ["expert"],
  style: ["clair"],
  valueProps: ["éthique"],
  topTerms: ["diamant", "carat", "or", "alliance"],
  twoGrams: ["diamant synthétique", "or blanc"],
  threeGrams: ["bague en or"],
  entities: ["GIA", "4C"],
  rawBrief: "",
  rawGuide: "",
};

function fullBundle(): KeywordPromptBundle {
  return {
    id: "k1",
    term: "bague diamant",
    country: "FR",
    lang: "fr",
    intent: ["commercial", "transactional"],
    primarySource: "seranking",
    sources: ["seranking", "gsc"],
    vol: 4400,
    kd: 38,
    cpc: 1.25,
    competitors: [
      competitor({ position: 1, domain: "a.com", title: "Guide diamant" }),
      competitor({ position: 2, domain: "b.com", backlinks: null }),
    ],
    serpFeatures: ["PAA"],
    brief,
    hasGsc: true,
    gscConnected: true,
    semanticProfile: null,
    readiness: { serp: "ready", onpage: "ready", brief: "ready" },
  };
}

function missingBriefBundle(): KeywordPromptBundle {
  return {
    ...fullBundle(),
    brief: null,
    readiness: { serp: "ready", onpage: "progress", brief: "missing" },
  };
}

/** Minimal-but-valid fixture — mirrors `compute-readiness.test.ts`. One term
 *  present in the Top 3 (`bague acier`), one absent from it (`argent`,
 *  `top3: {0,0,0}`). */
function semanticProfile(
  over: Partial<StoredSemanticProfile> = {},
): StoredSemanticProfile {
  return {
    intent: {
      curve: "transactionnelle",
      mode: "inferred",
      mixedIntent: false,
    },
    coverage: {
      analyzed: 8,
      total: 10,
      excludedByUser: [],
      missing: [],
      flags: [],
    },
    terms: [
      {
        term: "bague acier",
        display: "bagues en acier",
        pagesWithTerm: 6,
        score: 0.83,
        medianWeighted: 1.2,
        isOutlier: false,
        zones: {
          title: 0,
          h1: 1,
          h2h3: 0,
          slug: 0,
          alt: 0,
          anchor: 0,
          meta: 0,
          body: 4,
        },
        top3: { min: 4, median: 6, max: 12 },
      },
      {
        term: "argent",
        display: "argent",
        pagesWithTerm: 3,
        score: 0.4,
        medianWeighted: 0.5,
        isOutlier: false,
        zones: {
          title: 1,
          h1: 0,
          h2h3: 0,
          slug: 0,
          alt: 0,
          anchor: 0,
          meta: 0,
          body: 1,
        },
        top3: { min: 0, median: 0, max: 0 },
      },
    ],
    ngrams: {
      bigrams: [{ text: "bijou acier", count: 5, pages: 3 }],
      trigrams: [{ text: "fabricant bijoux acier", count: 3, pages: 2 }],
    },
    questions: [
      { text: "Quel budget prévoir ?", sourcePositions: [1, 2] },
      { text: "Comment entretenir un bijou en acier ?", sourcePositions: [3] },
    ],
    structure: {
      wordCount: { p25: 1200, median: 1850, p75: 2200 },
      h2Count: 7,
      lists: 3,
      tables: 1,
      schemaTypes: {},
      archetypes: {},
      top3VsTop10: {
        wordCountTop3: 1900,
        wordCountTop10: 1850,
        h2CountTop3: 7,
        h2CountTop10: 7,
      },
    },
    guide: {
      terms: [
        {
          term: "bague acier",
          display: "bagues en acier",
          top3: { min: 4, median: 6, max: 12 },
          recommendedZone: "body",
        },
        {
          term: "argent",
          display: "argent",
          top3: { min: 0, median: 0, max: 0 },
          recommendedZone: "title",
        },
      ],
      ngrams: [{ text: "fabricant bijoux acier", count: 3, pages: 2 }],
      questions: [
        "Quel budget prévoir ?",
        "Comment entretenir un bijou en acier ?",
      ],
      objectives: {
        wordCount: { p25: 1200, median: 1850, p75: 2200 },
        h2Count: 7,
        lists: 3,
        tables: 1,
      },
    },
    gap: null,
    analyzedAt: "2026-07-21T00:00:00.000Z",
    jobId: "job-1",
    exports: {
      pagesKey: "serp-semantics-results/site-a/k1/FR/job-1/pages.jsonl.gz",
      termsKey: "serp-semantics-results/site-a/k1/FR/job-1/terms.jsonl.gz",
    },
    ...over,
  };
}

function gapTerms(count: number): GapTerm[] {
  return Array.from({ length: count }, (_, i) => ({
    term: `gap-terme-${i + 1}`,
    expectedScore: 0.9,
    clientScore: 0.1,
    delta: 0.8 - i * 0.01,
  }));
}

function guideTerms(count: number): GuideTerm[] {
  return Array.from({ length: count }, (_, i) => ({
    term: `terme-${i + 1}`,
    display: `terme-${i + 1}`,
    top3: { min: 1, median: 2, max: 3 },
    recommendedZone: "body",
  }));
}

function ngrams(count: number, prefix: string): NgramAggregate[] {
  return Array.from({ length: count }, (_, i) => ({
    text: `${prefix}-${i + 1}`,
    count: 1,
    pages: 1,
  }));
}

function missingSerpBundle(): KeywordPromptBundle {
  return {
    ...fullBundle(),
    vol: null,
    kd: null,
    cpc: null,
    competitors: [],
    readiness: { serp: "missing", onpage: "missing", brief: "ready" },
  };
}

const ROLE_TEXT: Record<string, string> = {
  brief: "stratège éditorial SEO senior",
  redaction: "rédacteur SEO expert francophone",
  optim: "consultant SEO on-page senior",
};

describe("buildKeywordPrompt", () => {
  test("every target × task yields a non-empty prompt with the right role", () => {
    const bundle = fullBundle();
    for (const target of PROMPT_TARGETS) {
      for (const task of PROMPT_TASKS) {
        const out = buildKeywordPrompt(bundle, { target, task });
        expect(out.length).toBeGreaterThan(0);
        expect(out).toContain("# RÔLE");
        expect(out).toContain(ROLE_TEXT[task]);
        expect(out).toContain(`cible ${target}`);
      }
    }
  });

  test("claude-code embeds the MCP tools section and get_keyword ref", () => {
    const out = buildKeywordPrompt(fullBundle(), {
      target: "claude-code",
      task: "brief",
    });
    expect(out).toContain("# OUTILS (MCP)");
    expect(out).toContain("seo-insight.get_keyword");
    expect(out).toContain("Tu écris la page directement dans le projet.");
  });

  test("non claude-code targets never mention seo-insight.get_keyword", () => {
    for (const target of PROMPT_TARGETS.filter((t) => t !== "claude-code")) {
      const out = buildKeywordPrompt(fullBundle(), { target, task: "brief" });
      expect(out).not.toContain("seo-insight.get_keyword");
      expect(out).not.toContain("# OUTILS (MCP)");
    }
  });

  test("missing brief produces a 'non disponible' notice", () => {
    const out = buildKeywordPrompt(missingBriefBundle(), {
      target: "chatgpt",
      task: "brief",
    });
    expect(out).toContain("non disponible");
  });

  test("missing serp + metrics produce 'non disponible'", () => {
    const out = buildKeywordPrompt(missingSerpBundle(), {
      target: "gemini",
      task: "redaction",
    });
    expect(out).toContain("non disponible");
    expect(out).toContain("Métriques: non disponibles");
  });

  test("metrics line carries vol/kd/cpc + source when present", () => {
    const out = buildKeywordPrompt(fullBundle(), {
      target: "claude-desktop",
      task: "optim",
    });
    expect(out).toContain("Volume: 4400/mois · KD: 38/100 · CPC: $1.25");
    expect(out).toContain("(source: seranking)");
  });

  test("crawled competitor adds a title sub-block", () => {
    const out = buildKeywordPrompt(fullBundle(), {
      target: "chatgpt",
      task: "brief",
    });
    expect(out).toContain("1. a.com — Guide diamant");
  });

  function contentBundle(): KeywordPromptBundle {
    return {
      ...fullBundle(),
      competitors: [
        competitor({
          position: 1,
          domain: "a.com",
          title: "Guide diamant",
          wordCount: 1850,
          hnOutline: [
            { level: 1, text: "Bien choisir sa bague diamant" },
            { level: 2, text: "Les 4C du diamant" },
            { level: 3, text: "La pureté" },
          ],
          markdown:
            "Choisir une bague en diamant demande de comprendre les 4C.",
        }),
        competitor({
          position: 2,
          domain: "b.com",
          backlinks: null,
          hnOutline: [{ level: 2, text: "Budget et financement" }],
        }),
      ],
    };
  }

  function noContentBundle(): KeywordPromptBundle {
    return {
      ...fullBundle(),
      competitors: [
        competitor({ position: 1, domain: "a.com" }),
        competitor({ position: 2, domain: "b.com", backlinks: null }),
      ],
    };
  }

  test("crawled content embeds word count + Hn outline (non claude-code)", () => {
    const out = buildKeywordPrompt(contentBundle(), {
      target: "chatgpt",
      task: "brief",
    });
    expect(out).toContain("# CONTENU CONCURRENTS (extrait du crawl)");
    expect(out).toContain("1. a.com — 1850 mots");
    expect(out).toContain("Les 4C du diamant");
    expect(out).toContain("Budget et financement");
    expect(out).toContain("Extrait : ");
    expect(out).not.toContain("seo-insight.get_keyword");
  });

  test("crawled content embeds word count + Hn outline (claude-code)", () => {
    const out = buildKeywordPrompt(contentBundle(), {
      target: "claude-code",
      task: "redaction",
    });
    expect(out).toContain("# CONTENU CONCURRENTS (extrait du crawl)");
    expect(out).toContain("1. a.com — 1850 mots");
    expect(out).toContain("Les 4C du diamant");
    // claude-code keeps its MCP framing
    expect(out).toContain("# OUTILS (MCP)");
    expect(out).toContain("seo-insight.get_keyword");
  });

  test("a competitor without word count omits the '— mots' suffix", () => {
    const out = buildKeywordPrompt(contentBundle(), {
      target: "chatgpt",
      task: "brief",
    });
    // b.com has an outline but no wordCount
    expect(out).toContain("2. b.com\n");
    expect(out).not.toContain("2. b.com —");
  });

  test("no crawled content keeps the delegation and omits the detail block", () => {
    for (const target of ["chatgpt", "claude-code"] as const) {
      const out = buildKeywordPrompt(noContentBundle(), {
        target,
        task: "brief",
      });
      expect(out).not.toContain("# CONTENU CONCURRENTS");
      expect(out).toContain("# ANALYSE CONCURRENTIELLE");
      expect(out).toContain("scrape la page");
    }
  });

  test("no crawled content: claude-code keeps fetch/MCP framing, others delegate to list", () => {
    const cc = buildKeywordPrompt(noContentBundle(), {
      target: "claude-code",
      task: "brief",
    });
    expect(cc).toContain(
      "Utilise les outils MCP listés ci-dessous + `fetch(url)`.",
    );
    const other = buildKeywordPrompt(noContentBundle(), {
      target: "chatgpt",
      task: "brief",
    });
    expect(other).toContain(
      "Travaille à partir des concurrents listés ci-dessus.",
    );
    expect(other).not.toContain("seo-insight.get_keyword");
  });

  describe("GUIDE SÉMANTIQUE (semantic profile)", () => {
    test("no semanticProfile → section absent", () => {
      const out = buildKeywordPrompt(fullBundle(), {
        target: "chatgpt",
        task: "brief",
      });
      expect(out).not.toContain("GUIDE SÉMANTIQUE");
    });

    test("semanticProfile present → section with terms, ngrams, questions, objectives, positioned after YTG and before the competitive analysis", () => {
      const out = buildKeywordPrompt(
        { ...fullBundle(), semanticProfile: semanticProfile() },
        { target: "chatgpt", task: "brief" },
      );
      expect(out).toContain("# GUIDE SÉMANTIQUE (profil pondéré de la SERP)");
      expect(out).toContain(
        "Intention détectée : transactionnelle (inférée, 8/10 concurrents analysés)",
      );
      expect(out).toContain(
        "- « bagues en acier » : 4–12 occurrences (zone clé : corps de texte) — score 0.83",
      );
      expect(out).toContain("Expressions (2 mots) : « bijou acier »");
      expect(out).toContain(
        "Expressions (3 mots) : « fabricant bijoux acier »",
      );
      expect(out).toContain(
        "Questions à traiter (issues des Hn concurrents) : Quel budget prévoir ?, Comment entretenir un bijou en acier ?",
      );
      expect(out).toContain(
        "Objectifs structurels : ~1 850 mots (médiane SERP), 7 H2, 3 listes, 1 tableau",
      );
      expect(out.indexOf("# GRAMMAGES & TERMES")).toBeLessThan(
        out.indexOf("# GUIDE SÉMANTIQUE"),
      );
      expect(out.indexOf("# GUIDE SÉMANTIQUE")).toBeLessThan(
        out.indexOf("# ANALYSE CONCURRENTIELLE"),
      );
    });

    test("term absent from the Top 3 ({0,0,0}) renders 'présent hors top 3' instead of a range", () => {
      const out = buildKeywordPrompt(
        { ...fullBundle(), semanticProfile: semanticProfile() },
        { target: "chatgpt", task: "brief" },
      );
      expect(out).toContain(
        "- « argent » : présent hors top 3 (zone clé : Title) — score 0.40",
      );
    });

    test("gap non null adds the 'Ajustements (gap analysis)' sub-section, capped at 10 deltas", () => {
      const profile = semanticProfile({
        gap: {
          intentMismatch: false,
          terms: gapTerms(11),
          structure: { wordCount: 0, h2Count: 0, lists: 0, tables: 0 },
        },
      });
      const out = buildKeywordPrompt(
        { ...fullBundle(), semanticProfile: profile },
        { target: "chatgpt", task: "brief" },
      );
      expect(out).toContain("Ajustements (gap analysis) :");
      expect(out).toContain(
        "- « gap-terme-1 » : attendu 0.90 vs client 0.10 (écart 0.80)",
      );
      expect(out).toContain("gap-terme-10");
      expect(out).not.toContain("gap-terme-11");
    });

    test("gap null omits the 'Ajustements' sub-section", () => {
      const out = buildKeywordPrompt(
        { ...fullBundle(), semanticProfile: semanticProfile() },
        { target: "chatgpt", task: "brief" },
      );
      expect(out).not.toContain("Ajustements");
    });

    test("gap term display is resolved from profile.terms — the stem never renders", () => {
      // "bague acier" is the stem; the fixture's matching profile.terms entry
      // carries display "bagues en acier" (same pattern as the score lookup).
      const profile = semanticProfile({
        gap: {
          intentMismatch: false,
          terms: [
            {
              term: "bague acier",
              expectedScore: 0.9,
              clientScore: 0.2,
              delta: 0.7,
            },
          ],
          structure: { wordCount: 0, h2Count: 0, lists: 0, tables: 0 },
        },
      });
      const out = buildKeywordPrompt(
        { ...fullBundle(), semanticProfile: profile },
        { target: "chatgpt", task: "brief" },
      );
      expect(out).toContain(
        "- « bagues en acier » : attendu 0.90 vs client 0.20 (écart 0.70)",
      );
      expect(out).not.toContain("« bague acier »");
    });

    test("gap term with no matching profile.terms entry falls back to the stem", () => {
      const profile = semanticProfile({
        gap: {
          intentMismatch: false,
          terms: [
            {
              term: "terme-inconnu",
              expectedScore: 0.5,
              clientScore: 0.1,
              delta: 0.4,
            },
          ],
          structure: { wordCount: 0, h2Count: 0, lists: 0, tables: 0 },
        },
      });
      const out = buildKeywordPrompt(
        { ...fullBundle(), semanticProfile: profile },
        { target: "chatgpt", task: "brief" },
      );
      expect(out).toContain("« terme-inconnu »");
    });

    test("guide.terms capped at GUIDE_TERMS_CAP — 21 terms render only the first 20", () => {
      const base = semanticProfile();
      const profile = semanticProfile({
        guide: { ...base.guide, terms: guideTerms(21) },
      });
      const out = buildKeywordPrompt(
        { ...fullBundle(), semanticProfile: profile },
        { target: "chatgpt", task: "brief" },
      );
      expect(out).toContain("« terme-20 »");
      expect(out).not.toContain("« terme-21 »");
    });

    test("bigrams capped at GUIDE_BIGRAMS_CAP — 16 bigrams render only the first 15", () => {
      const profile = semanticProfile({
        ngrams: {
          bigrams: ngrams(16, "bigramme"),
          trigrams: [],
        },
      });
      const out = buildKeywordPrompt(
        { ...fullBundle(), semanticProfile: profile },
        { target: "chatgpt", task: "brief" },
      );
      expect(out).toContain("« bigramme-15 »");
      expect(out).not.toContain("« bigramme-16 »");
    });

    test("trigrams capped at GUIDE_TRIGRAMS_CAP — 11 trigrams render only the first 10", () => {
      const profile = semanticProfile({
        ngrams: {
          bigrams: [],
          trigrams: ngrams(11, "trigramme"),
        },
      });
      const out = buildKeywordPrompt(
        { ...fullBundle(), semanticProfile: profile },
        { target: "chatgpt", task: "brief" },
      );
      expect(out).toContain("« trigramme-10 »");
      expect(out).not.toContain("« trigramme-11 »");
    });
  });
});
