// src/lib/spec/audit/run.test.ts — pure engine tests (no network, no DB).
// Build fixture snapshots and assert per-item statuses + score math.
import { describe, expect, test } from "bun:test";
import { CHECKS } from "./checks";
import { runSiteAudit } from "./run";
import type { SiteSnapshot } from "./snapshot";

function snapshot(over: Partial<SiteSnapshot> = {}): SiteSnapshot {
  return {
    baseUrl: "https://example.com",
    home: { ok: false, status: 0, headers: {}, html: "", finalUrl: "" },
    robotsTxt: { ok: false, status: 0, text: "" },
    sitemapXml: { ok: false, status: 0, text: "" },
    securityTxt: { ok: false, status: 0, text: "" },
    llmsTxt: { ok: false, status: 0, text: "" },
    ...over,
  };
}

const GOOD_HTML = `<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Accueil — Exemple</title>
<meta name="description" content="Une description.">
<meta name="theme-color" content="#0a0a0a">
<link rel="canonical" href="https://example.com/">
<link rel="icon" href="https://example.com/favicon.ico">
<link rel="alternate" type="application/rss+xml" href="https://example.com/feed.xml">
<meta property="og:title" content="Exemple">
<meta property="og:image" content="https://example.com/og.png">
<script type="application/ld+json">{"@context":"https://schema.org","@type":"Organization"}</script>
</head>
<body>
<main>
<h1>Bienvenue sur Exemple</h1>
<p>${"Contenu rendu côté serveur, suffisamment long pour le heuristique SSR. ".repeat(
  10,
)}</p>
</main>
</body>
</html>`;

const GOOD_HEADERS: Record<string, string> = {
  "strict-transport-security": "max-age=31536000; includeSubDomains",
  "content-security-policy":
    "default-src 'self'; require-trusted-types-for 'script'",
  "x-content-type-options": "nosniff",
  "referrer-policy": "strict-origin-when-cross-origin",
  "x-frame-options": "DENY",
  "permissions-policy": "geolocation=(), camera=()",
  "content-encoding": "br",
  "cache-control": "public, max-age=300",
  etag: '"abc123"',
  "alt-svc": 'h3=":443"; ma=86400',
  "set-cookie": "session=xyz; Secure; HttpOnly; SameSite=Lax",
  "cross-origin-opener-policy": "same-origin",
  "cross-origin-embedder-policy": "require-corp",
  "cross-origin-resource-policy": "same-origin",
  "reporting-endpoints": 'default="https://example.com/reports"',
  "server-timing": "edge;dur=12",
};

function goodSnapshot(): SiteSnapshot {
  return snapshot({
    home: {
      ok: true,
      status: 200,
      headers: GOOD_HEADERS,
      html: GOOD_HTML,
      finalUrl: "https://example.com/",
    },
    robotsTxt: {
      ok: true,
      status: 200,
      text: "User-agent: *\nDisallow:\nSitemap: https://example.com/sitemap.xml",
    },
    sitemapXml: {
      ok: true,
      status: 200,
      text: '<?xml version="1.0"?><urlset><url><loc>https://example.com/</loc></url></urlset>',
    },
    securityTxt: {
      ok: true,
      status: 200,
      text: "Contact: mailto:security@example.com",
    },
    llmsTxt: { ok: true, status: 200, text: "# Example\n> A site." },
  });
}

const NA_CTX = { targetCountries: ["FR"] };
const MULTI_CTX = { targetCountries: ["FR", "BE"] };

function statusOf(res: ReturnType<typeof runSiteAudit>, id: string) {
  return res.results.find((r) => r.itemId === id)?.status;
}

describe("runSiteAudit — good snapshot", () => {
  const res = runSiteAudit(goodSnapshot(), NA_CTX);

  test.each([
    "foundations/doctype",
    "foundations/html-lang",
    "foundations/meta-charset",
    "foundations/meta-viewport",
    "foundations/title",
    "foundations/meta-description",
    "foundations/canonical-url",
    "foundations/open-graph",
    "foundations/feed-discovery",
    "foundations/favicons",
    "seo/meta-robots",
    "seo/robots-txt",
    "seo/xml-sitemaps",
    "seo/structured-data",
    "seo/heading-hierarchy",
    "seo/server-side-rendering",
    "security/https-tls",
    "security/hsts",
    "security/content-security-policy",
    "security/x-content-type-options",
    "security/referrer-policy",
    "security/frame-ancestors",
    "security/security-txt",
    "security/mixed-content",
    "security/permissions-policy",
    "security/cookie-attributes",
    "security/cross-origin-isolation",
    "security/reporting-endpoints",
    "security/trusted-types",
    "performance/compression",
    "performance/cache-control",
    "performance/conditional-requests",
    "performance/server-timing",
    "agent-readiness/llms-txt",
    "agent-readiness/structured-data-for-agents",
  ])("%s passes", (id) => {
    expect(statusOf(res, id)).toBe("pass");
  });

  test("theme-color passes (meta present)", () => {
    expect(statusOf(res, "foundations/theme-color")).toBe("pass");
  });
  test("http3 passes (alt-svc advertises h3)", () => {
    expect(statusOf(res, "performance/http3")).toBe("pass");
  });
  test("robots-for-ai-crawlers is na (no AI UA in good robots)", () => {
    expect(statusOf(res, "agent-readiness/robots-for-ai-crawlers")).toBe("na");
  });

  test("hreflang is na when mono-locale", () => {
    expect(statusOf(res, "i18n/hreflang")).toBe("na");
  });

  test("HSTS detail reports max-age", () => {
    const row = res.results.find((r) => r.itemId === "security/hsts");
    expect(row?.detail).toContain("31536000");
  });

  test("unmatched items are manual with empty detail", () => {
    const manual = res.results.find(
      (r) => r.itemId === "accessibility/color-contrast",
    );
    expect(manual?.status).toBe("manual");
    expect(manual?.detail).toBe("");
  });
});

describe("runSiteAudit — bare snapshot", () => {
  const bare = snapshot({
    home: {
      ok: true,
      status: 200,
      headers: {},
      html: "<html><head></head><body>hi</body></html>",
      finalUrl: "http://example.com/",
    },
  });
  const res = runSiteAudit(bare, NA_CTX);

  test("doctype fails", () => {
    expect(statusOf(res, "foundations/doctype")).toBe("fail");
  });
  test("html-lang fails", () => {
    expect(statusOf(res, "foundations/html-lang")).toBe("fail");
  });
  test("meta-charset fails", () => {
    expect(statusOf(res, "foundations/meta-charset")).toBe("fail");
  });
  test("title fails", () => {
    expect(statusOf(res, "foundations/title")).toBe("fail");
  });
  test("https fails (http finalUrl)", () => {
    expect(statusOf(res, "security/https-tls")).toBe("fail");
  });
  test("robots-txt fails (404)", () => {
    expect(statusOf(res, "seo/robots-txt")).toBe("fail");
  });
  test("xml-sitemaps fails", () => {
    expect(statusOf(res, "seo/xml-sitemaps")).toBe("fail");
  });
  test("hsts warns (header absent)", () => {
    expect(statusOf(res, "security/hsts")).toBe("warn");
  });
  test("csp warns (header absent)", () => {
    expect(statusOf(res, "security/content-security-policy")).toBe("warn");
  });
  test("cross-origin-isolation warns (aucun en-tête)", () => {
    expect(statusOf(res, "security/cross-origin-isolation")).toBe("warn");
  });
  test("reporting-endpoints warns (header absent)", () => {
    expect(statusOf(res, "security/reporting-endpoints")).toBe("warn");
  });
  test("trusted-types warns (pas de CSP)", () => {
    expect(statusOf(res, "security/trusted-types")).toBe("warn");
  });
  test("server-timing na (absent, topic optionnel)", () => {
    expect(statusOf(res, "performance/server-timing")).toBe("na");
  });
  test("meta-description warns", () => {
    expect(statusOf(res, "foundations/meta-description")).toBe("warn");
  });
  test("security-txt warns", () => {
    expect(statusOf(res, "security/security-txt")).toBe("warn");
  });
  test("llms-txt warns", () => {
    expect(statusOf(res, "agent-readiness/llms-txt")).toBe("warn");
  });
  test("favicons warns (no rel=icon)", () => {
    expect(statusOf(res, "foundations/favicons")).toBe("warn");
  });
  test("feed-discovery na (no feed link)", () => {
    expect(statusOf(res, "foundations/feed-discovery")).toBe("na");
  });
  test("theme-color na (no meta)", () => {
    expect(statusOf(res, "foundations/theme-color")).toBe("na");
  });
  test("structured-data warns (no JSON-LD)", () => {
    expect(statusOf(res, "seo/structured-data")).toBe("warn");
  });
  test("heading-hierarchy fails (no h1)", () => {
    expect(statusOf(res, "seo/heading-hierarchy")).toBe("fail");
  });
  test("server-side-rendering warns (near-empty body)", () => {
    expect(statusOf(res, "seo/server-side-rendering")).toBe("warn");
  });
  test("permissions-policy warns (header absent)", () => {
    expect(statusOf(res, "security/permissions-policy")).toBe("warn");
  });
  test("cookie-attributes na (no set-cookie)", () => {
    expect(statusOf(res, "security/cookie-attributes")).toBe("na");
  });
  test("compression warns (no content-encoding)", () => {
    expect(statusOf(res, "performance/compression")).toBe("warn");
  });
  test("cache-control warns (header absent)", () => {
    expect(statusOf(res, "performance/cache-control")).toBe("warn");
  });
  test("conditional-requests warns (no etag/last-modified)", () => {
    expect(statusOf(res, "performance/conditional-requests")).toBe("warn");
  });
  test("http3 na (no alt-svc)", () => {
    expect(statusOf(res, "performance/http3")).toBe("na");
  });
  test("mixed-content na (http home, not https)", () => {
    expect(statusOf(res, "security/mixed-content")).toBe("na");
  });
});

describe("new HTML/header checks — edge cases", () => {
  test("cross-origin-isolation partiel warns et nomme les manquants", () => {
    const snap = goodSnapshot();
    snap.home.headers = { ...snap.home.headers };
    delete snap.home.headers["cross-origin-embedder-policy"];
    delete snap.home.headers["cross-origin-resource-policy"];
    const res = runSiteAudit(snap, NA_CTX);
    const row = res.results.find(
      (r) => r.itemId === "security/cross-origin-isolation",
    );
    expect(row?.status).toBe("warn");
    expect(row?.detail).toContain("cross-origin-embedder-policy");
    expect(row?.detail).toContain("cross-origin-resource-policy");
  });

  test("report-to legacy seul warns comme legacy", () => {
    const snap = goodSnapshot();
    snap.home.headers = { ...snap.home.headers, "report-to": '{"group":"g"}' };
    delete snap.home.headers["reporting-endpoints"];
    const res = runSiteAudit(snap, NA_CTX);
    const row = res.results.find(
      (r) => r.itemId === "security/reporting-endpoints",
    );
    expect(row?.status).toBe("warn");
    expect(row?.detail).toContain("legacy");
  });

  test("CSP sans require-trusted-types-for warns", () => {
    const snap = goodSnapshot();
    snap.home.headers = {
      ...snap.home.headers,
      "content-security-policy": "default-src 'self'",
    };
    const res = runSiteAudit(snap, NA_CTX);
    expect(statusOf(res, "security/trusted-types")).toBe("warn");
  });

  test("multiple h1 warns", () => {
    const snap = goodSnapshot();
    snap.home.html = snap.home.html.replace(
      "</main>",
      "<h1>Deuxième titre</h1></main>",
    );
    const res = runSiteAudit(snap, NA_CTX);
    expect(statusOf(res, "seo/heading-hierarchy")).toBe("warn");
  });

  test("mixed content: http subresource on https page fails", () => {
    const snap = goodSnapshot();
    snap.home.html = snap.home.html.replace(
      "</head>",
      '<script src="http://cdn.example.com/lib.js"></script></head>',
    );
    const res = runSiteAudit(snap, NA_CTX);
    const row = res.results.find((r) => r.itemId === "security/mixed-content");
    expect(row?.status).toBe("fail");
    expect(row?.detail).toContain("http://");
  });

  test("cookie missing flags warns and names them", () => {
    const snap = goodSnapshot();
    snap.home.headers = { ...snap.home.headers, "set-cookie": "id=1; Path=/" };
    const res = runSiteAudit(snap, NA_CTX);
    const row = res.results.find(
      (r) => r.itemId === "security/cookie-attributes",
    );
    expect(row?.status).toBe("warn");
    expect(row?.detail).toContain("secure");
  });

  test("compression detail reports the encoding (br)", () => {
    const res = runSiteAudit(goodSnapshot(), NA_CTX);
    const row = res.results.find((r) => r.itemId === "performance/compression");
    expect(row?.detail).toContain("br");
  });

  test("robots-for-ai-crawlers passes when GPTBot is targeted", () => {
    const snap = goodSnapshot();
    snap.robotsTxt.text = `${snap.robotsTxt.text}\nUser-agent: GPTBot\nDisallow: /private`;
    const res = runSiteAudit(snap, NA_CTX);
    const row = res.results.find(
      (r) => r.itemId === "agent-readiness/robots-for-ai-crawlers",
    );
    expect(row?.status).toBe("pass");
    expect(row?.detail).toContain("GPTBot");
  });

  test("feed-discovery passes for application/json feed", () => {
    const snap = goodSnapshot();
    snap.home.html = snap.home.html.replace(
      'type="application/rss+xml"',
      'type="application/feed+json"',
    );
    const res = runSiteAudit(snap, NA_CTX);
    expect(statusOf(res, "foundations/feed-discovery")).toBe("pass");
  });
});

describe("hreflang context logic", () => {
  test("multi-locale without hreflang fails", () => {
    const res = runSiteAudit(goodSnapshot(), MULTI_CTX);
    expect(statusOf(res, "i18n/hreflang")).toBe("fail");
  });

  test("multi-locale with hreflang passes", () => {
    const snap = goodSnapshot();
    snap.home.html = snap.home.html.replace(
      "</head>",
      '<link rel="alternate" hreflang="fr" href="https://example.com/fr"></head>',
    );
    const res = runSiteAudit(snap, MULTI_CTX);
    expect(statusOf(res, "i18n/hreflang")).toBe("pass");
  });
});

describe("meta-robots noindex", () => {
  test("noindex on home fails", () => {
    const snap = goodSnapshot();
    snap.home.html = snap.home.html.replace(
      "</head>",
      '<meta name="robots" content="noindex, follow"></head>',
    );
    const res = runSiteAudit(snap, NA_CTX);
    expect(statusOf(res, "seo/meta-robots")).toBe("fail");
  });

  test("noindex via x-robots-tag header fails", () => {
    const snap = goodSnapshot();
    snap.home.headers = { ...snap.home.headers, "x-robots-tag": "noindex" };
    const res = runSiteAudit(snap, NA_CTX);
    expect(statusOf(res, "seo/meta-robots")).toBe("fail");
  });
});

describe("viewport user-scalable", () => {
  test("user-scalable=no warns", () => {
    const snap = goodSnapshot();
    snap.home.html = snap.home.html.replace(
      'content="width=device-width, initial-scale=1"',
      'content="width=device-width, user-scalable=no"',
    );
    const res = runSiteAudit(snap, NA_CTX);
    expect(statusOf(res, "foundations/meta-viewport")).toBe("warn");
  });
});

describe("score math", () => {
  test("good snapshot scores 100 (no fail/warn among auto checks)", () => {
    const res = runSiteAudit(goodSnapshot(), NA_CTX);
    expect(res.score).toBe(100);
  });

  test("score excludes na and manual from denominator", () => {
    // Run only a known mix of items via the registry directly to verify math.
    const good = goodSnapshot();
    // Craft a snapshot: 3 pass, 1 fail, 1 warn among auto checks → 60.
    const mixed = snapshot({
      home: {
        ok: true,
        status: 200,
        headers: {}, // no hsts/csp → warns; no nosniff → warn
        html: "<html><head></head><body>x</body></html>", // doctype fail
        finalUrl: "https://example.com/", // https pass
      },
    });
    const onlyThree = [
      { id: "foundations/doctype" }, // fail
      { id: "security/https-tls" }, // pass
      { id: "security/hsts" }, // warn
    ];
    const denom = onlyThree.length;
    let pass = 0;
    for (const it of onlyThree) {
      const r = CHECKS[it.id](mixed, NA_CTX);
      if (r.status === "pass") pass++;
    }
    expect(Math.round((100 * pass) / denom)).toBe(33);
    // sanity: the good snapshot reference still scores 100
    expect(runSiteAudit(good, NA_CTX).score).toBe(100);
  });

  test("empty denominator → score 0", () => {
    const empty = runSiteAudit(snapshot(), { targetCountries: [] }, []);
    expect(empty.score).toBe(0);
  });
});
