import { test, expect } from "@playwright/test";
import { assert, configure } from "passmark";
import fs from "node:fs";
import path from "node:path";

// ── Config Passmark : Vercel AI Gateway (clé partagée AI_GATEWAY_API_KEY du VPS).
//    Exécution → Gemini Flash ; jugement (assertions) → Claude Sonnet 4.6 (consensus + arbitre).
configure({
  ai: {
    gateway: "vercel",
    models: {
      stepExecution: "google/gemini-3.5-flash",
      userFlowLow: "google/gemini-3-flash-preview",
      userFlowHigh: "google/gemini-3.5-flash",
      assertionPrimary: "anthropic/claude-sonnet-4.6",
      assertionSecondary: "google/gemini-3-flash",
      assertionArbiter: "anthropic/claude-sonnet-4.6",
    },
  },
});

type UrlEntry = {
  path: string;
  url: string;
  locale: "fr" | "en" | "es";
  type: string;
  aiChecks: boolean;
  isTool: boolean;
};

const SHARD_FILE = process.env.SHARD_FILE || path.resolve(process.cwd(), "urls.json");
const AI_ENABLED = process.env.AI_CHECKS !== "0";
const shardName = path.basename(SHARD_FILE).replace(/\.json$/, "");
const RESULTS_DIR = process.env.RESULTS_DIR || path.resolve(process.cwd(), "results", shardName);
const SHOTS_DIR = process.env.SCREENSHOT_DIR || path.resolve(process.cwd(), "screenshots", shardName);

const entries: UrlEntry[] = JSON.parse(fs.readFileSync(SHARD_FILE, "utf8"));

function sanitize(s: string): string {
  return s.replace(/[^a-z0-9]+/gi, "_").replace(/^_|_$/g, "") || "root";
}

// Heuristiques de tokens parasites (volontairement précises pour limiter les faux positifs).
const PLACEHOLDER_PATTERNS: Array<[string, RegExp]> = [
  ["undefined", /\bundefined\b/],
  ["NaN", /(?<![A-Za-z])NaN(?![A-Za-z])/],
  ["[object Object]", /\[object Object\]/],
  ["Lorem ipsum", /lorem ipsum/i],
  ["unrendered mustache {{…}}", /\{\{[^}]+\}\}/],
  ["WP shortcode non rendu", /\[(?:vc_|et_pb|gallery\b|caption\b|embed\b|contact-form|wpforms|\/vc_|\/et_pb)[^\]]*\]/i],
];

test.describe.configure({ mode: "parallel" });

for (const entry of entries) {
  test(`[${entry.locale}|${entry.type}] ${entry.path}`, async ({ page }, testInfo) => {
    test.setTimeout(170_000);
    const viewport = testInfo.project.name; // "desktop" | "mobile"

    const consoleErrors: string[] = [];
    const consoleWarnings: string[] = [];
    const pageErrors: string[] = [];
    const failedRequests: Array<{ url: string; status: number; type: string }> = [];

    page.on("console", (msg) => {
      const t = msg.type();
      if (t === "error") consoleErrors.push(msg.text().slice(0, 400));
      else if (t === "warning") {
        const txt = msg.text();
        if (/hydrat|did not match|Warning:|controlled|uncontrolled|each child|key prop|act\(/i.test(txt)) {
          consoleWarnings.push(txt.slice(0, 400));
        }
      }
    });
    page.on("pageerror", (err) => pageErrors.push(String(err?.message || err).slice(0, 400)));
    page.on("response", (res) => {
      const s = res.status();
      if (s >= 400) {
        failedRequests.push({ url: res.url(), status: s, type: res.request().resourceType() });
      }
    });

    let httpStatus = 0;
    let loadOk = true;
    let navError = "";
    const t0 = Date.now();
    try {
      const resp = await page.goto(entry.url, { waitUntil: "domcontentloaded", timeout: 60_000 });
      httpStatus = resp?.status() ?? 0;
      await page.waitForLoadState("networkidle", { timeout: 6_000 }).catch(() => {});
      await page.waitForTimeout(600);
    } catch (e) {
      loadOk = false;
      navError = String((e as Error).message).slice(0, 300);
    }

    // ── Checks déterministes (DOM) ──────────────────────────────────────────
    const dom = await page
      .evaluate(() => {
        const de = document.documentElement;
        const body = document.body;
        const innerWidth = window.innerWidth;
        const scrollWidth = Math.max(de.scrollWidth, body ? body.scrollWidth : 0);
        const scrollHeight = Math.max(de.scrollHeight, body ? body.scrollHeight : 0);
        const bodyText = body ? body.innerText : "";
        const imgs = Array.from(document.querySelectorAll("img"));
        const brokenImgs = imgs
          .filter((im) => im.complete && im.naturalWidth === 0 && (im.currentSrc || im.src))
          .map((im) => im.currentSrc || im.src)
          .slice(0, 25);
        const imgsNoAlt = imgs.filter((im) => !im.getAttribute("alt") && !im.hasAttribute("aria-hidden")).length;
        // Hiérarchie des titres (Hn) dans l'ordre du DOM.
        const headings = Array.from(document.querySelectorAll("h1,h2,h3,h4,h5,h6")).map((h) => ({
          level: Number(h.tagName[1]),
          text: (h.textContent || "").trim().replace(/\s+/g, " ").slice(0, 140),
        }));
        const attr = (sel: string, a: string) => document.querySelector(sel)?.getAttribute(a) || "";
        return {
          innerWidth,
          scrollWidth,
          scrollHeight,
          bodyTextLen: bodyText.length,
          bodyText: bodyText.slice(0, 20000),
          title: document.title,
          metaDesc: attr('meta[name="description"]', "content"),
          robots: attr('meta[name="robots"]', "content"),
          canonical: attr('link[rel="canonical"]', "href"),
          ogTitle: attr('meta[property="og:title"]', "content"),
          ogDesc: attr('meta[property="og:description"]', "content"),
          ogImage: attr('meta[property="og:image"]', "content"),
          htmlLang: document.documentElement.getAttribute("lang") || "",
          hasViewportMeta: !!document.querySelector('meta[name="viewport"]'),
          hasHeader: !!document.querySelector("header, nav, [role=banner]"),
          hasFooter: !!document.querySelector("footer, [role=contentinfo]"),
          h1Count: document.querySelectorAll("h1").length,
          headings: headings.slice(0, 120),
          imgCount: imgs.length,
          imgsNoAlt,
          brokenImgs,
        };
      })
      .catch(() => null);

    const placeholderTokens: string[] = [];
    if (dom?.bodyText) {
      for (const [name, re] of PLACEHOLDER_PATTERNS) {
        if (re.test(dom.bodyText)) placeholderTokens.push(name);
      }
    }

    // ── SEO déterministe : Hn, title, meta desc, canonical, robots, alt… ─────
    const seoIssues: string[] = [];
    if (dom && httpStatus < 400) {
      const h1s = dom.headings.filter((h) => h.level === 1);
      if (dom.headings.length === 0) seoIssues.push("aucun titre Hn");
      if (h1s.length === 0) seoIssues.push("aucun H1");
      else if (h1s.length > 1) seoIssues.push(`H1 multiples (${h1s.length})`);
      let prev = 0;
      for (const h of dom.headings) {
        if (prev && h.level > prev + 1) {
          seoIssues.push(`saut de hiérarchie H${prev}→H${h.level}`);
          break;
        }
        prev = h.level;
      }
      const t = (dom.title || "").trim();
      if (!t) seoIssues.push("title vide");
      else if (t.length < 25) seoIssues.push(`title court (${t.length})`);
      else if (t.length > 65) seoIssues.push(`title long (${t.length})`);
      const md = (dom.metaDesc || "").trim();
      if (!md) seoIssues.push("meta description absente");
      else if (md.length < 50) seoIssues.push(`meta desc courte (${md.length})`);
      else if (md.length > 165) seoIssues.push(`meta desc longue (${md.length})`);
      if (!dom.canonical) seoIssues.push("canonical absent");
      if (/noindex/i.test(dom.robots)) seoIssues.push("noindex");
      if (!dom.hasViewportMeta) seoIssues.push("meta viewport absente");
      if (!dom.htmlLang) seoIssues.push("html lang absent");
      if (dom.imgsNoAlt > 0) seoIssues.push(`${dom.imgsNoAlt} image(s) sans alt`);
    }

    const overflowDelta = dom ? dom.scrollWidth - dom.innerWidth : 0;
    const overflow = overflowDelta > 4;
    const emptyish = !!dom && dom.bodyTextLen < 200; // page quasi-vide

    // ── Screenshot VIEWPORT d'abord (toujours sûr, borné) ───────────────────
    //    Sert d'image IA ET de capture de secours. Le full-page (lourd, peut
    //    crasher le renderer sur les pages géantes) est tenté APRÈS l'IA.
    fs.mkdirSync(SHOTS_DIR, { recursive: true });
    const shotPath = path.join(SHOTS_DIR, `${sanitize(entry.path)}__${viewport}.png`);
    const fullShotPath = path.join(SHOTS_DIR, `${sanitize(entry.path)}__${viewport}__full.png`);
    let viewportBuf: Buffer | null = null;
    try {
      viewportBuf = await page.screenshot({ path: shotPath, fullPage: false });
    } catch { /* ignore */ }

    // ── Assertions IA passmark (consensus) — page encore saine ──────────────
    const ai: Record<string, unknown> = {};
    const runAi = AI_ENABLED && entry.aiChecks && loadOk && httpStatus < 400 && !emptyish;
    if (runAi) {
      const images =
        viewportBuf && viewportBuf.byteLength < 6_000_000 ? [viewportBuf.toString("base64")] : undefined;

      try {
        const r = await assert({
          page,
          expect,
          failSilently: true,
          images,
          assertion:
            "This page renders as a complete, coherent web page for a B2B roofing/insulation company (Covalba). " +
            "There is NO broken layout, NO elements overlapping or cut off, NO large empty/blank sections where content should be, " +
            "the main images appear loaded (not broken placeholders), and the text is readable. " +
            "Answer false ONLY if the page looks visibly broken, blank, unstyled, or like an error page.",
        });
        ai.coherence = { passed: r.includes("✅ passed"), summary: r.slice(0, 1200) };
      } catch (e) {
        ai.coherence = { error: String((e as Error).message).slice(0, 300) };
      }

      if (entry.isTool) {
        try {
          const r = await assert({
            page,
            expect,
            failSilently: true,
            assertion:
              "The page's main interactive tool or form (a multi-step diagnostic/estimate form, an energy-savings calculator, or a contact form) " +
              "is visible and not visibly broken: inputs, labels and buttons render correctly and are not empty or overlapping.",
          });
          ai.tool = { passed: r.includes("✅ passed"), summary: r.slice(0, 1000) };
        } catch (e) {
          ai.tool = { error: String((e as Error).message).slice(0, 300) };
        }
      }

      if (entry.locale !== "fr") {
        const langName = entry.locale === "en" ? "English" : "Spanish";
        try {
          const r = await assert({
            page,
            expect,
            failSilently: true,
            assertion:
              `The MAIN visible content of this page (headings, paragraphs, buttons, navigation) is written in ${langName}. ` +
              `Answer false if the main content is actually in French (untranslated / fallback) or a clear mix of French and ${langName}.`,
          });
          ai.i18n = { inTargetLang: r.includes("✅ passed"), summary: r.slice(0, 1000) };
        } catch (e) {
          ai.i18n = { error: String((e as Error).message).slice(0, 300) };
        }
      }
    }

    // ── Full-page en best-effort APRÈS l'IA, et seulement si la page n'est pas
    //    géante (au-delà, le screenshot full-page crashe le renderer → on garde
    //    le viewport ; les checks déterministes couvrent déjà tout le DOM). ────
    let hasFullShot = false;
    const tallPage = (dom?.scrollHeight ?? 0) > 10_000;
    if (process.env.FULLPAGE !== "0" && !tallPage) {
      try {
        await page.screenshot({ path: fullShotPath, fullPage: true });
        hasFullShot = true;
      } catch { /* on garde le viewport */ }
    }

    const result = {
      path: entry.path,
      url: entry.url,
      locale: entry.locale,
      type: entry.type,
      viewport,
      aiChecks: entry.aiChecks,
      httpStatus,
      loadOk,
      navError,
      durationMs: Date.now() - t0,
      pageErrors,
      consoleErrorCount: consoleErrors.length,
      consoleErrors: consoleErrors.slice(0, 25),
      consoleWarnings: consoleWarnings.slice(0, 15),
      failedRequests: failedRequests.slice(0, 40),
      failedRequestCount: failedRequests.length,
      overflow,
      overflowDelta,
      scrollWidth: dom?.scrollWidth ?? null,
      innerWidth: dom?.innerWidth ?? null,
      emptyish,
      bodyTextLen: dom?.bodyTextLen ?? 0,
      hasHeader: dom?.hasHeader ?? false,
      hasFooter: dom?.hasFooter ?? false,
      h1Count: dom?.h1Count ?? 0,
      headings: dom?.headings ?? [],
      title: dom?.title ?? "",
      metaDesc: dom?.metaDesc ?? "",
      robots: dom?.robots ?? "",
      canonical: dom?.canonical ?? "",
      ogTitle: dom?.ogTitle ?? "",
      ogDesc: dom?.ogDesc ?? "",
      ogImage: dom?.ogImage ?? "",
      htmlLang: dom?.htmlLang ?? "",
      hasViewportMeta: dom?.hasViewportMeta ?? false,
      imgCount: dom?.imgCount ?? 0,
      imgsNoAlt: dom?.imgsNoAlt ?? 0,
      brokenImgs: dom?.brokenImgs ?? [],
      placeholderTokens,
      seoIssues,
      ai,
      screenshot: viewportBuf ? shotPath : null,
      fullScreenshot: hasFullShot ? fullShotPath : null,
    };

    fs.mkdirSync(RESULTS_DIR, { recursive: true });
    fs.writeFileSync(
      path.join(RESULTS_DIR, `${sanitize(entry.path)}__${viewport}.json`),
      JSON.stringify(result, null, 2),
    );
    await testInfo.attach("result", {
      body: JSON.stringify(result, null, 2),
      contentType: "application/json",
    });

    // Audit : on n'échoue PAS le test sur un finding (tout est enregistré).
    // On fait échouer UNIQUEMENT sur une vraie casse serveur (nav KO ou 5xx) pour la visibilité.
    expect(loadOk, `Navigation échouée: ${navError}`).toBe(true);
    expect(httpStatus, `HTTP ${httpStatus} pour ${entry.path}`).toBeLessThan(500);
  });
}
